diff --git a/.env.example b/.env.example index 92bb2c98..e475166c 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,8 @@ PRIVY_APP_SECRET= # Required in production. Use two independent authenticated Ethereum RPCs. ETHEREUM_RPC_URL= ETHEREUM_RPC_URL_B= +PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL= +PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL= # Optional for contract rehearsal. Use a reliable Sepolia RPC before broadcasting. SEPOLIA_RPC_URL= @@ -28,6 +30,75 @@ UNISWAP_API_KEY= UNISWAP_V4_SUBGRAPH_URL=https://gateway.thegraph.com/api/subgraphs/id/DiYPVdygkfjDWhbxGSqAQxwBKmfKnkWQojqeM2rkLb3G UNISWAP_V4_SUBGRAPH_API_KEY= +# Server-only realtime read-model providers. Keep every indexed route disabled +# until the migrated database, full backfill, parity, reorg and fallback gates +# have passed. The Envio endpoint is public by design; Postgres and Graph +# credentials must never use a NEXT_PUBLIC_ prefix. +PROGRAMMABLE_PROJECTOR_ACTIVE=false +PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE=false +PROGRAMMABLE_ENVIO_GRAPHQL_URL=https://indexer.hyperindex.xyz/d7a39a2/v1/graphql +PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN= +PROGRAMMABLE_ENVIO_MAXIMUM_BODY_BYTES=131072 +# Remote URLs must include an explicit port and sslmode=verify-full. +# Store the downloaded Supabase root certificate as this server-only PEM value. +PROGRAMMABLE_API_READER_DATABASE_URL= +# Operator-only direct connection. This must be the verified +# db..supabase.co:5432 endpoint and must never be added to Vercel. +PROGRAMMABLE_MIGRATOR_DATABASE_URL= +# Dedicated least-privilege login used only to consume signed release-probe nonces. +PROGRAMMABLE_RELEASE_PROBE_DATABASE_URL= +# Writer-only projector connection. It must authenticate as the constrained +# programmable_projector_login role and must never use a NEXT_PUBLIC_ prefix. +PROGRAMMABLE_PROJECTOR_DATABASE_URL= +PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_URL= +# Dedicated server-only shadow-reconciler login. It receives only the narrow +# preparity/corpus/commit capabilities, serves the market projector through the +# same private source, and must not reuse a projector or API reader credential. +PROGRAMMABLE_RECONCILER_DATABASE_URL= +# PEM trust root used only by the reconciler database connection. +PROGRAMMABLE_RECONCILER_DATABASE_SSL_CA= +# Every market page is bound to this exact source projector generation. +PROGRAMMABLE_SOURCE_PROJECTOR_VERSION=projector-v1 +PROGRAMMABLE_POSTGRES_SSL_CA_PEM= +PROGRAMMABLE_POSTGRES_MAX_CONNECTIONS=2 +PROGRAMMABLE_POSTGRES_CONNECT_TIMEOUT_MS=1000 +PROGRAMMABLE_POSTGRES_IDLE_TIMEOUT_MS=5000 +PROGRAMMABLE_POSTGRES_LOCK_TIMEOUT_MS=250 +PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL=https://gateway.thegraph.com +PROGRAMMABLE_UNISWAP_GRAPH_API_KEY= +# Public provenance labels and commitments for the reviewed official Uniswap +# v4 subgraph query/parser contract. These are evidence, never credentials. +PROGRAMMABLE_UNISWAP_GRAPH_REDACTED_IDENTITY=uniswap-v4-official +PROGRAMMABLE_UNISWAP_GRAPH_DEPLOYMENT_COMMITMENT=0x44c8d7127503563653f7f53ea339caa383453e00224a6c33cf95fc29f5c3e35c +PROGRAMMABLE_UNISWAP_GRAPH_SCHEMA_COMMITMENT=0xd0d2087059ca0a7c1e7c633999ff75ea34fcc00d42cee8985a79d0ef76e6813c +# Generated by the reviewed bootstrap plan from the credential-free provider +# endpoint identity. These values must match the exact release commit. +PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT= +PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT= +# Random server-only secret used to verify route-bound release-probe HMACs. +PROGRAMMABLE_SHADOW_PROBE_TOKEN= + +INDEXED_EXPLORE_LIST_READS_ENABLED=false +INDEXED_EXPLORE_TOKEN_READS_ENABLED=false +INDEXED_EXPLORE_CHART_READS_ENABLED=false +INDEXED_CREATOR_PROFILE_READS_ENABLED=false +INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED=false +INDEXED_LAUNCH_LOOKUP_ENABLED=false +INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED=false +INDEXED_READ_SHADOW_COMPARE_ENABLED=false +INDEXED_READ_REQUIRE_PARITY_ENABLED=true +INDEXED_READ_LIVE_FALLBACK_ENABLED=true + +# Exact public identities and reviewed commitments recorded with each +# projector generation. These values are evidence labels, not credentials. +PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY=envio:production-7f24e63 +PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT=7ffd15c2a28c481a2d3632e30b315262c2471b2e +PROGRAMMABLE_PROJECTOR_BINDING_MODE=release + +# Private token for a staged read-model performance capture. Never expose it +# through a NEXT_PUBLIC_ variable or include it in captured evidence. +PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN= + # Required by release tooling when publishing and checking source # verification on Etherscan. ETHERSCAN_API_KEY= @@ -53,5 +124,3 @@ DEEP_V3_LAUNCHER_TRANSACTION= DEEP_V3_RELEASE_COMMIT= # Required by Vercel Cron when refreshing the durable Mainnet index. CRON_SECRET= - -OPS_BLOB_READ_WRITE_TOKEN= diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index cec60300..d687c30c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -15,13 +15,98 @@ concurrency: cancel-in-progress: false jobs: + release-gate: + name: Verify exact production commit + if: >- + github.repository == '0xprogrammable/programmable' && + github.ref == 'refs/heads/production' + runs-on: ubuntu-latest + timeout-minutes: 60 + outputs: + verified_sha: ${{ steps.exact-head.outputs.sha }} + + steps: + - name: Check out exact production commit + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Scan exact production change history + env: + PROGRAMMABLE_GITLEAKS_BASE_SHA: ${{ github.event.before }} + run: bash scripts/security/run-gitleaks-ci.sh + + - name: Set up Node.js + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 24.14.0 + cache: npm + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.13" + + - name: Set up Foundry + uses: foundry-rs/foundry-toolchain@b00af27efadbc7b4ca8b82abbd903b17cc874d2a # v1 + with: + version: v1.7.1 + + - name: Install dependencies + run: | + npm ci + pipx install slither-analyzer==0.11.5 + + - name: Pin pnpm + run: | + npm install --global pnpm@10.32.0 + test "$(pnpm --version)" = "10.32.0" + + - name: Verify indexer + run: | + pnpm --dir indexer install --frozen-lockfile + pnpm --dir indexer audit --prod --audit-level high + pnpm --dir indexer codegen + pnpm --dir indexer typecheck + pnpm --dir indexer test + + - name: Audit production dependencies + run: npm run audit:prod + + - name: Verify operations source contract + run: npm run perf:read-model:ops-gate + + - name: Verify interface and release gates + run: npm run verify + + - name: Verify fresh database migrations + run: npm run db:test:pglite + + - name: Verify contracts + run: | + npm run contracts:verify:ci + npm run contracts:official-deployments + npm run contracts:slither + + - name: Bind verified evidence to exact commit + id: exact-head + run: | + verified_sha="$(git rev-parse HEAD)" + test "$verified_sha" = "$GITHUB_SHA" + echo "sha=$verified_sha" >> "$GITHUB_OUTPUT" + deploy: name: Deploy programmable.family + needs: release-gate if: >- github.repository == '0xprogrammable/programmable' && - github.ref == 'refs/heads/production' + github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/production' && + needs.release-gate.outputs.verified_sha == github.sha runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 30 environment: name: production url: ${{ steps.deploy.outputs.url }} @@ -32,11 +117,14 @@ jobs: steps: - name: Check out production uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + ref: ${{ needs.release-gate.outputs.verified_sha }} + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 22 + node-version: 24.14.0 cache: npm - name: Install dependencies @@ -50,17 +138,37 @@ jobs: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: vercel pull --yes --environment=production --token="$VERCEL_TOKEN" + - name: Capture current production rollback target + id: production-before + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: >- + npm run perf:read-model:production-binding -- + --target-url "https://programmable.family" + --reject-git-head "$GITHUB_SHA" + --github-output "$GITHUB_OUTPUT" + + - name: Resolve read-model release policy + id: read-model-policy + env: + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: ${{ vars.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT }} + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: ${{ vars.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT }} + run: >- + npm run perf:read-model:deploy-policy -- + --env-file .vercel/.env.production.local + --github-output "$GITHUB_OUTPUT" + - name: Build production deployment env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: vercel build --prod --token="$VERCEL_TOKEN" - - name: Deploy production build + - name: Stage production build without assigning domains id: deploy env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | - deployment_url="$(vercel deploy --prebuilt --prod --archive=tgz --token="$VERCEL_TOKEN")" + deployment_url="$(vercel deploy --prebuilt --prod --skip-domain --archive=tgz --meta githubCommitSha="$GITHUB_SHA" --token="$VERCEL_TOKEN")" case "$deployment_url" in https://*) ;; *) @@ -70,16 +178,323 @@ jobs: esac echo "url=$deployment_url" >> "$GITHUB_OUTPUT" - - name: Verify deployment and production domain + - name: Resolve exact staged deployment + id: staged-deployment + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }} + run: >- + npm run perf:read-model:staged-deployment -- + --target-url "$DEPLOYMENT_URL" + --github-output "$GITHUB_OUTPUT" + + - name: Attest exact staged release policy + id: release-attestation + env: + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: ${{ vars.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT }} + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: ${{ vars.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT }} + STAGED_DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }} + STAGED_TARGET_URL: ${{ steps.staged-deployment.outputs.target_url }} + POLICY_MODE: ${{ steps.read-model-policy.outputs.mode }} + run: >- + npm run perf:read-model:deploy-policy -- + --env-file .vercel/.env.production.local + --verified-sha "$GITHUB_SHA" + --vercel-project-id "$VERCEL_PROJECT_ID" + --staged-deployment-id "$STAGED_DEPLOYMENT_ID" + --staged-target-url "$STAGED_TARGET_URL" + --production-origin "https://programmable.family" + --expected-mode "$POLICY_MODE" + --attestation-output "$RUNNER_TEMP/staged-release-attestation.json" + --github-output "$GITHUB_OUTPUT" + + - name: Preserve staged release attestation + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: staged-release-attestation-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.release-attestation.outputs.attestation_path }} + if-no-files-found: error + retention-days: 30 + + - name: Record staged release attestation + run: | + { + echo "## Staged release attestation" + echo + echo "- SHA-256: \`${{ steps.release-attestation.outputs.attestation_sha256 }}\`" + echo "- Runtime mode: \`${{ steps.read-model-policy.outputs.mode }}\`" + echo "- Production target: https://programmable.family" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Verify staged deployment env: DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }} run: | curl --fail --silent --show-error \ --retry 12 --retry-all-errors --retry-delay 5 \ --max-time 30 "$DEPLOYMENT_URL/" > /dev/null - curl --fail --silent --show-error \ - --retry 12 --retry-all-errors --retry-delay 5 \ - --max-time 30 "https://programmable.family/" > /dev/null + + - name: Smoke legacy staged public APIs + if: steps.read-model-policy.outputs.evidence_required == 'false' + env: + STAGED_TARGET_URL: ${{ steps.staged-deployment.outputs.target_url }} + run: | + node --input-type=module <<'NODE' + const origin = new URL(process.env.STAGED_TARGET_URL); + if ( + origin.protocol !== "https:" || + !origin.hostname.endsWith(".vercel.app") || + origin.pathname !== "/" || + origin.search || + origin.hash + ) { + throw new Error("legacy smoke target is not an exact Vercel origin"); + } + + const requestJson = async (path) => { + let lastError; + for (let attempt = 1; attempt <= 12; attempt += 1) { + try { + const response = await fetch(new URL(path, origin), { + redirect: "error", + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(30_000), + }); + const text = await response.text(); + if (!response.ok) { + throw new Error(`${path} returned ${response.status}`); + } + if (Buffer.byteLength(text, "utf8") > 2 * 1024 * 1024) { + throw new Error(`${path} returned an oversized response`); + } + return JSON.parse(text); + } catch (error) { + lastError = error; + if (attempt < 12) { + await new Promise((resolve) => setTimeout(resolve, 5_000)); + } + } + } + throw lastError ?? new Error(`${path} failed`); + }; + + const health = await requestJson("/api/ops/health"); + if (health.status !== "healthy") { + throw new Error("legacy staged health is not healthy"); + } + const explore = await requestJson( + "/api/explore?limit=6&page=1&sort=market-cap", + ); + if (explore.status !== "ready" || !Array.isArray(explore.tokens)) { + throw new Error("legacy staged Explore is not ready"); + } + const releaseToken = explore.tokens.find( + (token) => + /^0x[0-9a-fA-F]{40}$/.test(token?.tokenAddress ?? "") && + /^0x[0-9a-fA-F]{40}$/.test(token?.creatorAddress ?? ""), + ); + if (!releaseToken) { + throw new Error("legacy staged Explore has no release-corpus identity"); + } + const tokenList = await requestJson("/api/indexers/v1/token-list"); + if ( + !Array.isArray(tokenList.tokens) || + !tokenList.tokens.some( + (token) => + String(token?.address ?? token?.tokenAddress ?? "").toLowerCase() === + releaseToken.tokenAddress.toLowerCase(), + ) + ) { + throw new Error("legacy staged token list is not populated with the release token"); + } + const tokenDetail = await requestJson( + `/api/explore/token?address=${encodeURIComponent(releaseToken.tokenAddress)}`, + ); + if ( + tokenDetail.status !== "ready" || + tokenDetail.token?.tokenAddress?.toLowerCase() !== + releaseToken.tokenAddress.toLowerCase() + ) { + throw new Error("legacy staged token lookup did not resolve the release token"); + } + const profile = await requestJson( + `/api/explore/profile?account=${encodeURIComponent(releaseToken.creatorAddress)}`, + ); + if ( + profile.status !== "ready" || + profile.account?.toLowerCase() !== releaseToken.creatorAddress.toLowerCase() || + !Array.isArray(profile.tokens) || + !profile.tokens.some( + (token) => + token?.tokenAddress?.toLowerCase() === + releaseToken.tokenAddress.toLowerCase(), + ) + ) { + throw new Error("legacy staged creator profile did not resolve the release token"); + } + process.stdout.write( + `${JSON.stringify({ + status: "verified-legacy-staged-public-apis", + tokenAddress: releaseToken.tokenAddress, + creatorAddress: releaseToken.creatorAddress, + })}\n`, + ); + NODE + + - name: Record legacy-only read path + if: steps.read-model-policy.outputs.evidence_required == 'false' + run: | + { + echo "## Read-model release policy" + echo + echo "Legacy-only: every indexed route flag and shadow comparison are exactly false." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Capture staged read-model evidence + id: read-model-capture + if: steps.read-model-policy.outputs.evidence_required == 'true' + env: + PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN: ${{ secrets.PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN }} + PROGRAMMABLE_SHADOW_PROBE_TOKEN: ${{ secrets.PROGRAMMABLE_SHADOW_PROBE_TOKEN }} + PROGRAMMABLE_READ_MODEL_TARGET_URL: ${{ steps.staged-deployment.outputs.target_url }} + PROGRAMMABLE_READ_MODEL_VERCEL_DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }} + run: >- + npm run perf:read-model:capture -- + --target-url "$PROGRAMMABLE_READ_MODEL_TARGET_URL" + --deployment-id "$PROGRAMMABLE_READ_MODEL_VERCEL_DEPLOYMENT_ID" + --output-directory "$RUNNER_TEMP/read-model-release-evidence" + --kind production-canary + --github-output "$GITHUB_OUTPUT" + + - name: Preserve staged read-model evidence + if: steps.read-model-policy.outputs.evidence_required == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: read-model-evidence-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.read-model-capture.outputs.evidence_directory }} + if-no-files-found: error + retention-days: 7 + + - name: Gate indexed or shadow read path + if: steps.read-model-policy.outputs.evidence_required == 'true' + env: + PROGRAMMABLE_READ_MODEL_PERF_EVIDENCE_PATH: ${{ steps.read-model-capture.outputs.evidence_path }} + PROGRAMMABLE_READ_MODEL_TARGET_URL: ${{ steps.staged-deployment.outputs.target_url }} + PROGRAMMABLE_READ_MODEL_VERCEL_DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }} + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: ${{ vars.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT }} + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: ${{ vars.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: npm run perf:read-model:gate + + - name: Reverify staged binding immediately before promotion + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + STAGED_TARGET_URL: ${{ steps.staged-deployment.outputs.target_url }} + EXPECTED_DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }} + run: | + binding_output="$RUNNER_TEMP/pre-promote-staged-binding" + test ! -e "$binding_output" + npm run perf:read-model:staged-deployment -- \ + --target-url "$STAGED_TARGET_URL" \ + --github-output "$binding_output" + grep -Fx "deployment_id=$EXPECTED_DEPLOYMENT_ID" "$binding_output" + grep -Fx "target_url=$STAGED_TARGET_URL" "$binding_output" + + - name: Promote verified staged deployment + id: promote + continue-on-error: true + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }} + run: vercel promote "$DEPLOYMENT_ID" --yes --token="$VERCEL_TOKEN" + + - name: Verify promoted production routes + id: post-promotion + if: steps.promote.outcome == 'success' + env: + EVIDENCE_REQUIRED: ${{ steps.read-model-policy.outputs.evidence_required }} + EVIDENCE_PATH: ${{ steps.read-model-capture.outputs.evidence_path }} + DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + if [ "$EVIDENCE_REQUIRED" = "true" ]; then + npm run perf:read-model:post-promotion -- \ + --target-url "https://programmable.family" \ + --deployment-id "$DEPLOYMENT_ID" \ + --git-head "$GITHUB_SHA" \ + --evidence "$EVIDENCE_PATH" + else + npm run perf:read-model:post-promotion -- \ + --target-url "https://programmable.family" \ + --deployment-id "$DEPLOYMENT_ID" \ + --git-head "$GITHUB_SHA" + fi + + - name: Reconcile an unsuccessful promotion attempt + if: >- + always() && + steps.promote.outcome != 'skipped' && + (steps.promote.outcome != 'success' || + steps.post-promotion.outcome != 'success') + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + CANDIDATE_DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }} + PREVIOUS_DEPLOYMENT_ID: ${{ steps.production-before.outputs.deployment_id }} + ROLLBACK_DEPLOYMENT_URL: ${{ steps.production-before.outputs.deployment_url }} + PREVIOUS_GIT_HEAD: ${{ steps.production-before.outputs.git_head }} + run: | + observed_previous=false + for attempt in $(seq 1 12); do + binding_output="$RUNNER_TEMP/production-after-promote-$attempt" + if npm run perf:read-model:production-binding -- \ + --target-url "https://programmable.family" \ + --github-output "$binding_output"; then + current_deployment_id="$(sed -n 's/^deployment_id=//p' "$binding_output")" + if [ "$current_deployment_id" = "$CANDIDATE_DEPLOYMENT_ID" ]; then + if ! vercel rollback "$ROLLBACK_DEPLOYMENT_URL" \ + --yes --timeout 60s --token="$VERCEL_TOKEN"; then + echo "Rollback command returned an uncertain result; verifying the live binding" >&2 + fi + rollback_verified=false + for rollback_attempt in $(seq 1 12); do + rollback_output="$RUNNER_TEMP/verified-rollback-binding-$rollback_attempt" + if npm run perf:read-model:production-binding -- \ + --target-url "https://programmable.family" \ + --expected-deployment-id "$PREVIOUS_DEPLOYMENT_ID" \ + --expected-git-head "$PREVIOUS_GIT_HEAD" \ + --github-output "$rollback_output"; then + rollback_verified=true + break + fi + sleep 5 + done + test "$rollback_verified" = "true" + exit 0 + fi + if [ "$current_deployment_id" = "$PREVIOUS_DEPLOYMENT_ID" ]; then + observed_previous=true + else + echo "Production resolved to an unexpected deployment" >&2 + exit 1 + fi + fi + sleep 5 + done + test "$observed_previous" = "true" + final_output="$RUNNER_TEMP/verified-previous-production-binding" + npm run perf:read-model:production-binding -- \ + --target-url "https://programmable.family" \ + --expected-deployment-id "$PREVIOUS_DEPLOYMENT_ID" \ + --expected-git-head "$PREVIOUS_GIT_HEAD" \ + --github-output "$final_output" + + - name: Fail an unsuccessful promotion attempt + if: >- + always() && + steps.promote.outcome != 'skipped' && + (steps.promote.outcome != 'success' || + steps.post-promotion.outcome != 'success') + run: exit 1 - name: Record deployment env: @@ -91,4 +506,5 @@ jobs: echo "- Commit: \`$GITHUB_SHA\`" echo "- Deployment: $DEPLOYMENT_URL" echo "- Domain: https://programmable.family" + echo "- Read model: ${{ steps.read-model-policy.outputs.mode }}" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/mainnet-monitor.yml b/.github/workflows/mainnet-monitor.yml index b68e6de4..280a4bb2 100644 --- a/.github/workflows/mainnet-monitor.yml +++ b/.github/workflows/mainnet-monitor.yml @@ -26,7 +26,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 22 + node-version: 24.14.0 cache: npm - name: Install dependencies diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 284338da..ac3abcf2 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -15,6 +15,74 @@ concurrency: cancel-in-progress: true jobs: + secret-scan: + name: Credential leak gate + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check out complete change history + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + fetch-depth: 0 + + - name: Scan exact change history + env: + PROGRAMMABLE_GITLEAKS_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + run: bash scripts/security/run-gitleaks-ci.sh + + indexer: + name: Realtime indexer + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - name: Set up Node.js + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 24.14.0 + + - name: Pin pnpm + run: | + npm install --global pnpm@10.32.0 + test "$(pnpm --version)" = "10.32.0" + + - name: Install indexer dependencies + run: pnpm --dir indexer install --frozen-lockfile + + - name: Audit indexer production dependencies + run: pnpm --dir indexer audit --prod --audit-level high + + - name: Generate and verify indexer + run: | + pnpm --dir indexer codegen + pnpm --dir indexer typecheck + pnpm --dir indexer test + + database-pglite: + name: Database (PGlite) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - name: Set up Node.js + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 + with: + node-version: 24.14.0 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run fresh-database SQL tests + run: npm run db:test:pglite + interface: name: Interface runs-on: ubuntu-latest @@ -27,7 +95,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 22 + node-version: 24.14.0 cache: npm - name: Set up Foundry @@ -41,6 +109,9 @@ jobs: - name: Audit production dependencies run: npm run audit:prod + - name: Verify operations source contract + run: npm run perf:read-model:ops-gate + - name: Verify interface run: npm run verify @@ -56,7 +127,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: - node-version: 22 + node-version: 24.14.0 cache: npm - name: Set up Python diff --git a/.gitignore b/.gitignore index 051f6740..1b9a108d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ # vercel .vercel +# supabase local CLI state +/supabase/.temp/ + # test and tooling /coverage /work/ @@ -23,6 +26,7 @@ /outputs/qa/ /artifacts/ /.codex-temp-programmable-readme-gif/ +/.superpowers/ /.worktrees/ *.tsbuildinfo diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..21768824 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,10 @@ +[extend] +useDefault = true + +[[allowlists]] +description = "Committed Envio evidence contains public Ethereum addresses under creator fields" +paths = [ + '''docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z\.json''', + '''docs/data-pipeline/envio-candidate-7f24e63-baseline-20260801T042058Z\.json''', + '''tests/data-pipeline/read-model-performance-explore-matrix\.test\.ts''', +] diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..d845d9d8 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.14.0 diff --git a/README.md b/README.md index 2f9106bd..b57a9f9d 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,7 @@ Programmable is an interface for launching tokens whose market behavior is defin | Model | Status | Purpose | | --- | --- | --- | | Classic | Live on Ethereum Mainnet | Fixed supply, permanently locked one-sided liquidity and creator rewards in ETH | -| Stock-Paired | Coming soon | Fixed supply traded against an allowlisted Ondo tokenized stock or ETF quote asset | -| Deep | Design only, unavailable | A proposed growth-fee model for adding both assets to the original permanently locked pool | +| Stock-Paired | Historical, new launches closed | Fixed supply traded against an allowlisted Ondo tokenized stock or ETF quote asset | Only models with a completed deployment manifest, matching runtime code and verified lifecycle are exposed for production launches. @@ -42,9 +41,10 @@ Optional X and Telegram links are encoded as versioned UTF-8 JSON in `extraData` ## Stock-Paired -Stock-Paired launches use an allowlisted Ondo tokenized stock or ETF as the -canonical v4 pool's quote asset. An initial ETH buy is routed through USDC and -the selected quote asset before purchasing the launched token. +Historical Stock-Paired launches use an allowlisted Ondo tokenized stock or ETF +as the canonical v4 pool's quote asset. An initial ETH buy was routed through +USDC and the selected quote asset before purchasing the launched token. New +Stock-Paired launches are closed. - The launched token remains a separate fixed-supply ERC-20 - The pool charges a fixed 1.00% hook fee @@ -70,12 +70,10 @@ The public read model pairs canonical launch events, ignores unrecognized shared The active Classic deployment is recorded in [`contracts/deployments/mainnet-classic-v3.json`](./contracts/deployments/mainnet-classic-v3.json). Its deployment receipts, constructor configuration, runtime code hashes and signed launch, buy, sell and claim lifecycle have been reconciled through two RPC providers. The deployed contracts have exact source matches on Etherscan and Sourcify. -The V1 and V2 Stock-Paired deployments remain immutable historical releases. -Their public indexer records preserve the quote asset, v4 pool ordering, hook, -fees and exact release. New launches stay closed while the V3 starting-price -release is prepared and verified. - -Deep V3 is not deployed. Its automated keeper has been removed, and the model remains unavailable while its execution design is reconsidered. A passing local test suite is not a production release. +The V1, V2 and V3 Stock-Paired deployments remain immutable historical +releases. Their public indexer records preserve the quote asset, v4 pool +ordering, hook, fees and exact release. Existing token pages, trading, profile +history and reward claims remain supported while new launches stay closed. There has been no external smart-contract audit or public security contest. This repository does not promise that a token is immune to abuse or accepted by third-party scanners. @@ -105,7 +103,5 @@ Key references: - [`docs/uniswap-source-provenance.md`](./docs/uniswap-source-provenance.md) - [`contracts/security/MAINNET-READINESS.md`](./contracts/security/MAINNET-READINESS.md) - [`contracts/security/CLASSIC-V3.md`](./contracts/security/CLASSIC-V3.md) -- [`contracts/security/DEEP-V3.md`](./contracts/security/DEEP-V3.md) -- [`contracts/release/DEEP-FULL-RANGE-V3.md`](./contracts/release/DEEP-FULL-RANGE-V3.md) - [`docs/frontend-transaction-preflight.md`](./docs/frontend-transaction-preflight.md) - [`docs/public-indexer-feed.md`](./docs/public-indexer-feed.md) diff --git a/app/api/explore/launch/deep-v3/route.ts b/app/api/explore/launch/deep-v3/route.ts index 6b3e13f7..4fdd5ddd 100644 --- a/app/api/explore/launch/deep-v3/route.ts +++ b/app/api/explore/launch/deep-v3/route.ts @@ -1,215 +1,16 @@ -import { NextRequest, NextResponse } from "next/server"; -import { - createPublicClient, - getAddress, - http, - isAddress, - isHex, - type Address, - type Hex, -} from "viem"; -import { mainnet } from "viem/chains"; - -import { - parseDeepV3LaunchReceipts, - type DeepV3LaunchReceipt, -} from "@/lib/deep-v3-launch-confirmation"; -import { - configuredMainnetDeepV3Manifest, -} from "@/lib/deep-v3-release"; -import { requireIndependentDeepV3RpcUrls } from "@/lib/deep-v3-runtime-binding"; -import { - resolveVerifiedDeepV3ReadRelease, -} from "@/lib/onchain/deep-v3-read-model"; -import { - readDeepV3ProfileToken, - type DeepV3ProfileClient, -} from "@/lib/profile/deep-v3-profile.server"; -import { safeServerErrorSummary } from "@/lib/server/safe-error"; +import { NextResponse } from "next/server"; export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -const CONFIRMATIONS = 12n; - -function json(body: unknown, status = 200) { - return NextResponse.json(body, { - status, - headers: { "Cache-Control": "private, max-age=0, no-store" }, - }); -} - -function isReceiptPending(error: unknown) { - return ( - error instanceof Error && - (error.name === "TransactionReceiptNotFoundError" || - error.name === "TransactionNotFoundError") - ); -} - -function receiptShape( - receipt: Awaited< - ReturnType< - ReturnType["getTransactionReceipt"] - > - >, -): DeepV3LaunchReceipt { - return { - status: receipt.status, - from: receipt.from, - to: receipt.to, - blockNumber: receipt.blockNumber, - blockHash: receipt.blockHash, - transactionHash: receipt.transactionHash, - transactionIndex: receipt.transactionIndex, - logs: receipt.logs.map((log) => ({ - address: log.address, - topics: log.topics as readonly Hex[], - data: log.data, - logIndex: log.logIndex, - })), - }; -} - -export async function GET(request: NextRequest) { - const search = request.nextUrl.searchParams; - if ( - [...search.keys()].some( - (key) => key !== "account" && key !== "transaction", - ) || - search.getAll("account").length !== 1 || - search.getAll("transaction").length !== 1 - ) { - return json({ error: "Unsupported query parameters" }, 400); - } - const rawAccount = search.get("account")?.trim() ?? ""; - const rawTransaction = search.get("transaction")?.trim() ?? ""; - if (!isAddress(rawAccount)) { - return json({ error: "Enter a valid Ethereum wallet" }, 400); - } - if ( - !isHex(rawTransaction, { strict: true }) || - rawTransaction.length !== 66 - ) { - return json({ error: "Enter a valid launch transaction hash" }, 400); - } - - const release = resolveVerifiedDeepV3ReadRelease( - configuredMainnetDeepV3Manifest, - 1, - ); - if (!release) { - return json( - { error: "Deep is not enabled by the verified release manifest" }, - 503, - ); - } - let endpoints: readonly [string, string]; - try { - endpoints = requireIndependentDeepV3RpcUrls( - process.env.ETHEREUM_RPC_URL, - process.env.ETHEREUM_RPC_URL_B, - ); - } catch (error) { - console.error( - "Deep V3 launch confirmation RPC setup failed", - safeServerErrorSummary(error), - ); - return json( - { error: "Launch confirmation is temporarily unavailable" }, - 503, - ); - } - const clients = endpoints.map((endpoint) => - createPublicClient({ - chain: mainnet, - transport: http(endpoint, { - retryCount: 1, - timeout: 12_000, - }), - }), +export function GET() { + return NextResponse.json( + { + code: "deep_launches_closed", + error: "New Deep launches are not available", + }, + { + status: 410, + headers: { "Cache-Control": "no-store" }, + }, ); - - let receipts; - try { - receipts = await Promise.all( - clients.map((client) => - client.getTransactionReceipt({ - hash: rawTransaction as Hex, - }), - ), - ); - } catch (error) { - if (isReceiptPending(error)) { - return json({ status: "pending", launch: null }, 202); - } - console.error( - "Deep V3 receipt lookup failed", - safeServerErrorSummary(error), - ); - return json( - { error: "Launch confirmation is temporarily unavailable" }, - 503, - ); - } - if (receipts.some((receipt) => receipt.status !== "success")) { - return json({ error: "The Deep launch transaction reverted" }, 409); - } - - try { - const heads = await Promise.all( - clients.map((client) => client.getBlockNumber()), - ); - const lowestHead = heads[0] < heads[1] ? heads[0] : heads[1]; - const launchBlock = - receipts[0].blockNumber < receipts[1].blockNumber - ? receipts[0].blockNumber - : receipts[1].blockNumber; - if (lowestHead < launchBlock + CONFIRMATIONS) { - return json({ status: "pending", launch: null }, 202); - } - - const account = getAddress(rawAccount) as Address; - const transactionHash = rawTransaction as Hex; - const provenance = parseDeepV3LaunchReceipts({ - receipts: receipts.map(receiptShape), - release: { - startBlock: release.startBlock, - launcher: release.addresses.launcher, - feeHook: release.addresses.feeHook, - }, - account, - transactionHash, - }); - const profile = await readDeepV3ProfileToken({ - manifest: configuredMainnetDeepV3Manifest, - chainId: 1, - account, - candidate: provenance, - clients: - clients as unknown as readonly DeepV3ProfileClient[], - }); - - return json({ - status: "ready", - launch: { - tokenAddress: profile.token.tokenAddress, - name: profile.token.tokenName, - symbol: profile.token.tokenSymbol, - deepReleaseVersion: profile.token.deepReleaseVersion, - deepV3Provenance: provenance, - }, - snapshot: profile.snapshot, - }); - } catch (error) { - console.error( - "Deep V3 launch confirmation failed", - safeServerErrorSummary(error), - ); - return json( - { error: "Launch confirmation is temporarily unavailable" }, - 503, - ); - } } diff --git a/app/api/explore/launch/route.ts b/app/api/explore/launch/route.ts index aebbbab0..4fdd5ddd 100644 --- a/app/api/explore/launch/route.ts +++ b/app/api/explore/launch/route.ts @@ -1,49 +1,16 @@ -import { NextRequest, NextResponse } from "next/server"; - -import { readExploreModel } from "@/lib/onchain"; -import { findDeepV2LaunchByTransaction } from "@/lib/onchain/deep-v2-read-model"; +import { NextResponse } from "next/server"; export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -export async function GET(request: NextRequest) { - const search = request.nextUrl.searchParams; - if ( - [...search.keys()].some((key) => key !== "transaction") || - search.getAll("transaction").length !== 1 - ) { - return NextResponse.json( - { error: "Unsupported query parameters" }, - { status: 400, headers: { "Cache-Control": "no-store" } }, - ); - } - const transactionHash = search.get("transaction")?.trim() ?? ""; - if (!/^0x[a-fA-F0-9]{64}$/.test(transactionHash)) { - return NextResponse.json( - { error: "Enter a valid launch transaction hash" }, - { status: 400, headers: { "Cache-Control": "no-store" } }, - ); - } - try { - const model = await readExploreModel(); - const launch = findDeepV2LaunchByTransaction(model, transactionHash); - return NextResponse.json( - { - status: model.status, - launch, - snapshot: model.snapshot, - }, - { - status: launch ? 200 : 404, - headers: { "Cache-Control": "private, max-age=0, no-store" }, - }, - ); - } catch (error) { - console.error("Deep V2 launch confirmation failed", error); - return NextResponse.json( - { error: "Launch confirmation is temporarily unavailable" }, - { status: 503, headers: { "Cache-Control": "no-store" } }, - ); - } +export function GET() { + return NextResponse.json( + { + code: "deep_launches_closed", + error: "New Deep launches are not available", + }, + { + status: 410, + headers: { "Cache-Control": "no-store" }, + }, + ); } diff --git a/app/api/explore/launch/stock-paired/route.ts b/app/api/explore/launch/stock-paired/route.ts index 89e856d0..4b371fb0 100644 --- a/app/api/explore/launch/stock-paired/route.ts +++ b/app/api/explore/launch/stock-paired/route.ts @@ -14,6 +14,12 @@ import { mainnet } from "viem/chains"; import { uerc20ReadAbi } from "@/lib/onchain/abis"; import { safeServerErrorSummary } from "@/lib/server/safe-error"; import { getConfiguredStockPairedReleases } from "@/lib/stock-paired-release"; +import { + coordinatePublicRouteRead, + PUBLIC_INDEXED_ROUTE_READS, + preparePublicRouteRequest, + STOCK_PAIRED_ROUTE_SCOPES, +} from "@/lib/data-pipeline/public-route-readiness.server"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -45,8 +51,9 @@ function endpoints() { return [primary, secondary] as const; } -export async function GET(request: NextRequest) { - const search = request.nextUrl.searchParams; +async function readLegacyLaunchLookup(request: NextRequest) { + const search = new URLSearchParams(request.nextUrl.searchParams); + search.delete("__read_model_probe"); if ( [...search.keys()].some( (key) => key !== "account" && key !== "transaction", @@ -233,3 +240,60 @@ export async function GET(request: NextRequest) { return json({ error: "The launch receipt is temporarily unavailable" }, 503); } } + +export async function GET(request: NextRequest) { + const routeRequest = await preparePublicRouteRequest( + request.nextUrl.searchParams, + request.headers, + "launch-lookup", + ); + if (routeRequest.probeFailure) return routeRequest.probeFailure; + const search = routeRequest.searchParams; + if ( + [...search.keys()].some( + (key) => key !== "account" && key !== "transaction", + ) || + search.getAll("account").length !== 1 || + search.getAll("transaction").length !== 1 + ) { + return json({ error: "Unsupported query parameters" }, 400); + } + const accountInput = search.get("account")?.trim() ?? ""; + const transactionInput = search.get("transaction")?.trim() ?? ""; + if ( + !isAddress(accountInput) || + !isHex(transactionInput, { strict: true }) || + transactionInput.length !== 66 + ) { + return json({ error: "Invalid Stock-Paired launch lookup" }, 400); + } + + try { + return await coordinatePublicRouteRead({ + route: "launch-lookup", + scope: STOCK_PAIRED_ROUTE_SCOPES, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (readTransaction) => + PUBLIC_INDEXED_ROUTE_READS.launchLookup(readTransaction, { + chainId: 1, + surface: "stock-paired", + account: getAddress(accountInput), + transactionHash: transactionInput, + }), + async legacy() { + return { + source: "rpc" as const, + response: await readLegacyLaunchLookup(request), + }; + }, + }); + } catch (error) { + console.error( + "Stock-Paired launch lookup coordination failed", + safeServerErrorSummary(error), + ); + return json({ error: "The launch receipt is temporarily unavailable" }, 503); + } +} diff --git a/app/api/explore/profile/claim/route.ts b/app/api/explore/profile/claim/route.ts index c816258f..0611a637 100644 --- a/app/api/explore/profile/claim/route.ts +++ b/app/api/explore/profile/claim/route.ts @@ -1,5 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; -import { createPublicClient, http } from "viem"; +import { + createPublicClient, + encodeFunctionData, + formatUnits, + getAddress, + http, + keccak256, + type Address, + type Hex, + type PublicClient, +} from "viem"; import { mainnet, sepolia } from "viem/chains"; import { @@ -9,18 +19,256 @@ import { getOnchainDeployment, parseCreatorClaimRequest, readExploreModel, - resolveCreatorClaimIntent, } from "../../../../../lib/onchain"; +import { creatorFeeHookReadAbi } from "../../../../../lib/onchain/abis"; +import { + ActionLookupError, + lookupActionTokenByPoolId, + type ActionTokenLookup, +} from "../../../../../lib/data-pipeline/action-lookup"; +import { indexedLaunchLookupEnabled } from "../../../../../lib/data-pipeline/route-activation.server"; import { errorChainIncludesData, safeServerErrorSummary, } from "../../../../../lib/server/safe-error"; +import { creatorClaimRpcProviders } from "../../../../../lib/server/action-rpc-quorum.server"; +import { computeOfficialV4PoolId } from "../../../../../lib/uniswap/liquidity-launcher-sdk"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; const MAX_REQUEST_BYTES = 2_048; const NO_FEES_TO_CLAIM_SELECTOR = "0x846d8c5c"; +const NATIVE_ETH = "0x0000000000000000000000000000000000000000" as Address; + +type CreatorClaimTokenIdentity = Readonly<{ + tokenAddress: Address; + hookAddress: Address; + poolId: Hex; + creatorAddress: Address; + totalSwapFeeBps: number; + buySwapFeeBps: number; + sellSwapFeeBps: number; + creatorFeeBps: number; + launcherFeeBps: number; + transferTaxBps: number; + lpFeePips: number; +}>; + +function canonicalClaimPoolId( + tokenAddress: Address, + hookAddress: Address, +) { + return computeOfficialV4PoolId({ + currency0: NATIVE_ETH, + currency1: tokenAddress, + fee: 0, + tickSpacing: 200, + hooks: hookAddress, + }); +} + +function maximum(left: bigint, right: bigint) { + return left > right ? left : right; +} + +function minimum(left: bigint, right: bigint) { + return left < right ? left : right; +} + +function claimClient( + deployment: ReturnType, + endpoint: string, +) { + return createPublicClient({ + chain: deployment.chainId === 1 ? mainnet : sepolia, + transport: http(endpoint, { retryCount: 1, timeout: 12_000 }), + }); +} + +async function sharedVerifiedBlock(clients: readonly PublicClient[]) { + if (clients.length !== 2) { + throw new CreatorClaimUnavailableError( + "rpc-unavailable", + "Creator claims require two independent Ethereum RPCs", + ); + } + const heads = await Promise.all(clients.map((client) => client.getBlockNumber())); + const blockNumber = minimum(heads[0]!, heads[1]!); + const blocks = await Promise.all( + clients.map((client) => client.getBlock({ blockNumber })), + ); + if ( + !blocks[0]?.hash || + !blocks[1]?.hash || + blocks[0].hash.toLowerCase() !== blocks[1].hash.toLowerCase() + ) { + throw new CreatorClaimUnavailableError( + "rpc-disagreement", + "Independent Ethereum RPCs disagree on the current claim state", + ); + } + return { blockNumber, blockHash: blocks[0].hash }; +} + +async function readCurrentClaimState(input: { + client: PublicClient; + deployment: Extract, { status: "ready" }>; + token: CreatorClaimTokenIdentity; + blockNumber: bigint; +}) { + const { client, deployment, token, blockNumber } = input; + const [hookCode, launcherCode, config, disclosure] = await Promise.all([ + client.getCode({ address: deployment.feeHook, blockNumber }), + client.getCode({ address: deployment.launcher, blockNumber }), + client.readContract({ + address: deployment.feeHook, + abi: creatorFeeHookReadAbi, + functionName: "poolFeeConfig", + args: [token.poolId], + blockNumber, + }), + client.readContract({ + address: deployment.feeHook, + abi: creatorFeeHookReadAbi, + functionName: "feeDisclosure", + args: [token.poolId], + blockNumber, + }), + ]); + if ( + !hookCode || + hookCode === "0x" || + keccak256(hookCode).toLowerCase() !== + deployment.feeHookRuntimeCodeHash.toLowerCase() || + !launcherCode || + launcherCode === "0x" || + keccak256(launcherCode).toLowerCase() !== + deployment.launcherRuntimeCodeHash.toLowerCase() + ) { + throw new CreatorClaimUnavailableError( + "runtime-mismatch", + "The creator claim release does not match its verified runtime", + ); + } + const [creator, registrar, totalSwapFeeBps, registered, claimable] = config; + const [buyFee, sellFee, creatorFee, launcherFee, transferTax, lpFee] = + disclosure; + if ( + !registered || + getAddress(creator).toLowerCase() !== token.creatorAddress.toLowerCase() || + getAddress(registrar).toLowerCase() !== deployment.launcher.toLowerCase() || + Number(totalSwapFeeBps) !== token.totalSwapFeeBps || + Number(buyFee) !== token.buySwapFeeBps || + Number(sellFee) !== token.sellSwapFeeBps || + Number(creatorFee) !== token.creatorFeeBps || + Number(launcherFee) !== token.launcherFeeBps || + Number(transferTax) !== token.transferTaxBps || + Number(lpFee) !== token.lpFeePips + ) { + throw new CreatorClaimUnavailableError( + "identity-mismatch", + "The current creator fee state does not match the indexed launch", + ); + } + return { claimable }; +} + +function indexedClaimToken( + token: ActionTokenLookup, + deployment: Extract, { status: "ready" }>, +): CreatorClaimTokenIdentity { + if ( + token.releaseVersion !== "classic-v2" || + token.modelVersion !== "classic" || + token.hookAddress.toLowerCase() !== deployment.feeHook.toLowerCase() || + canonicalClaimPoolId(token.tokenAddress, token.hookAddress).toLowerCase() !== + token.poolId.toLowerCase() || + token.creatorFeeBps === null + ) { + throw new CreatorClaimUnavailableError( + "noncanonical-hook", + "The pool does not use the canonical creator fee hook", + ); + } + return { + tokenAddress: token.tokenAddress, + hookAddress: token.hookAddress, + poolId: token.poolId, + creatorAddress: token.creatorAddress, + totalSwapFeeBps: token.totalSwapFeeBps, + buySwapFeeBps: token.buySwapFeeBps, + sellSwapFeeBps: token.sellSwapFeeBps, + creatorFeeBps: token.creatorFeeBps, + launcherFeeBps: token.launcherFeeBps, + transferTaxBps: token.transferTaxBps, + lpFeePips: token.lpFeePips, + }; +} + +async function legacyClaimToken( + request: ReturnType, + deployment: Extract, { status: "ready" }>, +): Promise { + const model = await readExploreModel(deployment); + if (model.status !== "ready" || model.snapshot.chainId !== deployment.chainId) { + throw new CreatorClaimUnavailableError( + "registry-unavailable", + "The verified Programmable launch registry is unavailable", + ); + } + const token = model.tokens.find( + (candidate) => + candidate.poolId.toLowerCase() === request.poolId.toLowerCase(), + ); + if (!token) { + throw new CreatorClaimUnavailableError( + "unknown-pool", + "This pool is not a verified Programmable launch", + ); + } + const feeValues = [ + token.buyHookFeeBps, + token.sellHookFeeBps, + token.creatorFeeBps, + token.launcherFeeBps, + token.transferTaxBps, + token.lpFeePips, + ]; + if ( + token.launchModel !== "classic" || + token.hookAddress.toLowerCase() !== deployment.feeHook.toLowerCase() || + !token.creatorAddress || + canonicalClaimPoolId( + getAddress(token.tokenAddress), + getAddress(token.hookAddress), + ).toLowerCase() !== request.poolId.toLowerCase() || + feeValues.some( + (value) => + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < 0, + ) + ) { + throw new CreatorClaimUnavailableError( + "noncanonical-hook", + "The pool does not use the canonical creator fee hook", + ); + } + return { + tokenAddress: getAddress(token.tokenAddress), + hookAddress: getAddress(token.hookAddress), + poolId: request.poolId, + creatorAddress: getAddress(token.creatorAddress), + totalSwapFeeBps: token.totalSwapFeeBps, + buySwapFeeBps: token.buyHookFeeBps!, + sellSwapFeeBps: token.sellHookFeeBps!, + creatorFeeBps: token.creatorFeeBps!, + launcherFeeBps: token.launcherFeeBps!, + transferTaxBps: token.transferTaxBps!, + lpFeePips: token.lpFeePips!, + }; +} function json(body: unknown, status = 200) { return NextResponse.json(body, { @@ -95,46 +343,136 @@ export async function POST(request: NextRequest) { try { const claimRequest = parseCreatorClaimRequest(input); const deployment = getOnchainDeployment(); - const model = await readExploreModel(deployment); - const intent = resolveCreatorClaimIntent( - claimRequest, - deployment, - model, + if (deployment.status !== "ready") { + throw new CreatorClaimUnavailableError( + "not-deployed", + "Creator claims are unavailable until the production contracts are deployed", + ); + } + if (claimRequest.chainId !== deployment.chainId) { + throw new CreatorClaimUnavailableError( + "wrong-chain", + `Switch to chain ${deployment.chainId}`, + ); + } + const token = indexedLaunchLookupEnabled() + ? indexedClaimToken( + await lookupActionTokenByPoolId({ + chainId: deployment.chainId, + poolId: claimRequest.poolId, + }), + deployment, + ) + : await legacyClaimToken(claimRequest, deployment); + if ( + token.creatorAddress.toLowerCase() !== claimRequest.account.toLowerCase() + ) { + throw new CreatorClaimUnavailableError( + "not-creator", + "This account is not the recorded creator for the pool", + ); + } + + const clients = creatorClaimRpcProviders(deployment).map((provider) => + claimClient(deployment, provider.endpoint), ); - const chain = deployment.chainId === 1 ? mainnet : sepolia; - const client = createPublicClient({ - chain, - transport: http(deployment.rpcUrl, { - retryCount: 1, - timeout: 12_000, - }), + const snapshot = await sharedVerifiedBlock(clients); + const states = await Promise.all( + clients.map((client) => + readCurrentClaimState({ + client, + deployment, + token, + blockNumber: snapshot.blockNumber, + }), + ), + ); + if (states[0]!.claimable !== states[1]!.claimable) { + throw new CreatorClaimUnavailableError( + "rpc-disagreement", + "Independent Ethereum RPCs disagree on the current claim balance", + ); + } + const claimable = states[0]!.claimable; + if (claimable <= 0n) { + throw new CreatorClaimUnavailableError( + "nothing-to-claim", + "There are no creator fees to claim for this pool", + ); + } + const data = encodeFunctionData({ + abi: creatorFeeHookReadAbi, + functionName: "claimCreatorFees", + args: [claimRequest.poolId], }); const value = 0n; - - await client.call({ - account: intent.account, - to: intent.transaction.to, - data: intent.transaction.data, - value, - }); - const [estimatedGas, gasPrice, accountBalance] = - await Promise.all([ - client.estimateGas({ - account: intent.account, - to: intent.transaction.to, - data: intent.transaction.data, + const simulations = await Promise.all( + clients.map(async (client) => { + const transaction = { + account: claimRequest.account, + to: deployment.feeHook, + data, value, - }), - client.getGasPrice(), - client.getBalance({ address: intent.account }), - ]); + }; + await client.call(transaction); + const [estimatedGas, gasPrice, accountBalance] = await Promise.all([ + client.estimateGas(transaction), + client.getGasPrice(), + client.getBalance({ address: claimRequest.account }), + ]); + return { estimatedGas, gasPrice, accountBalance }; + }), + ); + const intent = { + account: claimRequest.account, + poolId: claimRequest.poolId, + tokenAddress: token.tokenAddress, + hookAddress: deployment.feeHook, + snapshotClaimableWei: claimable.toString(), + snapshotClaimableEth: formatUnits(claimable, 18), + snapshot: { + chainId: deployment.chainId, + blockNumber: snapshot.blockNumber.toString(), + blockHash: snapshot.blockHash, + confirmations: 0, + }, + transaction: { + kind: "claim-creator-fees" as const, + chainId: deployment.chainId, + from: claimRequest.account, + to: deployment.feeHook, + data, + value: "0" as const, + }, + }; const response = buildPreparedCreatorClaim(intent, { - estimatedGas, - gasPriceWei: gasPrice, - accountBalanceWei: accountBalance, + estimatedGas: maximum( + simulations[0]!.estimatedGas, + simulations[1]!.estimatedGas, + ), + gasPriceWei: maximum( + simulations[0]!.gasPrice, + simulations[1]!.gasPrice, + ), + accountBalanceWei: minimum( + simulations[0]!.accountBalance, + simulations[1]!.accountBalance, + ), }); return json(response); } catch (error) { + if (error instanceof ActionLookupError) { + return json( + blockedResponse( + "blocked", + error.code === "not-found" ? "unknown-pool" : "registry-unavailable", + error.code === "not-found" + ? "This pool is not a verified Programmable launch" + : "The verified Programmable launch registry is unavailable", + ), + 409, + ); + } if (error instanceof CreatorClaimInputError) { return json( blockedResponse("blocked", error.code, error.message), diff --git a/app/api/explore/profile/route.ts b/app/api/explore/profile/route.ts index d608c28e..f1190e4d 100644 --- a/app/api/explore/profile/route.ts +++ b/app/api/explore/profile/route.ts @@ -5,12 +5,39 @@ import { buildCreatorProfile, readExploreModel, } from "../../../../lib/onchain"; +import { + coordinatePublicRouteRead, + PUBLIC_INDEXED_ROUTE_READS, + PUBLIC_DISCOVERY_ROUTE_SCOPES, + preparePublicRouteRequest, + publicSnapshotCheckpoint, +} from "../../../../lib/data-pipeline/public-route-readiness.server"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; export async function GET(request: NextRequest) { - const input = request.nextUrl.searchParams.get("account")?.trim(); + const routeRequest = await preparePublicRouteRequest( + request.nextUrl.searchParams, + request.headers, + "creator-profile", + ); + if (routeRequest.probeFailure) return routeRequest.probeFailure; + const search = routeRequest.searchParams; + if ( + [...search.keys()].some( + (key) => key !== "account" && key !== "launch" && key !== "attempt", + ) || + search.getAll("account").length !== 1 || + search.getAll("launch").length > 1 || + search.getAll("attempt").length > 1 + ) { + return NextResponse.json( + { error: "Unsupported query parameters" }, + { status: 400, headers: { "Cache-Control": "no-store" } }, + ); + } + const input = search.get("account")?.trim(); if (!input || !isAddress(input)) { return NextResponse.json( { error: "Enter a valid Ethereum account address" }, @@ -19,18 +46,37 @@ export async function GET(request: NextRequest) { } try { - const model = await readExploreModel(); - return NextResponse.json( - buildCreatorProfile(model, getAddress(input)), - { - headers: { - "Cache-Control": - model.status === "ready" - ? "private, max-age=0, s-maxage=15" - : "private, max-age=0, s-maxage=60", - }, + const account = getAddress(input); + return await coordinatePublicRouteRead({ + route: "creator-profile", + scope: PUBLIC_DISCOVERY_ROUTE_SCOPES, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (transaction) => + PUBLIC_INDEXED_ROUTE_READS.creatorProfile(transaction, { + chainId: 1, + account, + }), + async legacy() { + const model = await readExploreModel(); + return { + source: "rpc" as const, + checkpoint: publicSnapshotCheckpoint(model.snapshot), + response: NextResponse.json( + buildCreatorProfile(model, account), + { + headers: { + "Cache-Control": + model.status === "ready" + ? "private, max-age=0, s-maxage=15" + : "private, max-age=0, s-maxage=60", + }, + }, + ), + }; }, - ); + }); } catch (error) { console.error("Creator profile onchain read failed", error); return NextResponse.json( diff --git a/app/api/explore/route.ts b/app/api/explore/route.ts index 77451685..51095773 100644 --- a/app/api/explore/route.ts +++ b/app/api/explore/route.ts @@ -11,6 +11,13 @@ import { } from "../../../lib/onchain/uniswap-v4-subgraph"; import type { ExplorePage } from "../../../lib/onchain/types"; import type { LauncherToken } from "../../../lib/tokens"; +import { + coordinatePublicRouteRead, + PUBLIC_INDEXED_ROUTE_READS, + PUBLIC_DISCOVERY_ROUTE_SCOPES, + preparePublicRouteRequest, + publicSnapshotCheckpoint, +} from "../../../lib/data-pipeline/public-route-readiness.server"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -44,7 +51,13 @@ function tokenIdentity(token: LauncherToken) { } export async function GET(request: NextRequest) { - const search = request.nextUrl.searchParams; + const routeRequest = await preparePublicRouteRequest( + request.nextUrl.searchParams, + request.headers, + "explore-list", + ); + if (routeRequest.probeFailure) return routeRequest.probeFailure; + const search = routeRequest.searchParams; if (!hasCanonicalQueryShape(search)) { return NextResponse.json( { error: "Unsupported query parameters" }, @@ -53,56 +66,74 @@ export async function GET(request: NextRequest) { } try { - const model = await readExploreModel(); const options = { query: search.get("q") ?? "", sort: parseExploreSort(search.get("sort")), page: integerQuery(search.get("page"), 1), pageSize: integerQuery(search.get("limit"), 6), } as const; - const completeCandidate = paginateExplore(model, { - ...options, - page: 1, - pageSize: OFFICIAL_V4_SUBGRAPH_MAXIMUM_POOL_IDS, - }); + return await coordinatePublicRouteRead({ + route: "explore-list", + scope: PUBLIC_DISCOVERY_ROUTE_SCOPES, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (transaction) => + PUBLIC_INDEXED_ROUTE_READS.explore(transaction, { + chainId: 1, + ...options, + }), + async legacy() { + const model = await readExploreModel(); + const completeCandidate = paginateExplore(model, { + ...options, + page: 1, + pageSize: OFFICIAL_V4_SUBGRAPH_MAXIMUM_POOL_IDS, + }); - let response: ExplorePage; - if ( - completeCandidate.total <= - OFFICIAL_V4_SUBGRAPH_MAXIMUM_POOL_IDS - ) { - const enrichedCandidate = - await enrichExplorePageWithOfficialV4Subgraph( - completeCandidate, - ); - const enrichedByIdentity = new Map( - enrichedCandidate.tokens.map((token) => [ - tokenIdentity(token), - token, - ]), - ); - response = paginateExplore( - { - ...model, - tokens: model.tokens.map( - (token) => - enrichedByIdentity.get(tokenIdentity(token)) ?? token, - ), - }, - options, - ); - } else { - response = await enrichExplorePageWithOfficialV4Subgraph( - paginateExplore(model, options), - ); - } + let response: ExplorePage; + if ( + completeCandidate.total <= + OFFICIAL_V4_SUBGRAPH_MAXIMUM_POOL_IDS + ) { + const enrichedCandidate = + await enrichExplorePageWithOfficialV4Subgraph( + completeCandidate, + ); + const enrichedByIdentity = new Map( + enrichedCandidate.tokens.map((token) => [ + tokenIdentity(token), + token, + ]), + ); + response = paginateExplore( + { + ...model, + tokens: model.tokens.map( + (token) => + enrichedByIdentity.get(tokenIdentity(token)) ?? token, + ), + }, + options, + ); + } else { + response = await enrichExplorePageWithOfficialV4Subgraph( + paginateExplore(model, options), + ); + } - return NextResponse.json(response, { - headers: { - "Cache-Control": - response.status === "ready" - ? "public, max-age=0, s-maxage=10, stale-while-revalidate=10" - : "public, max-age=0, s-maxage=60", + return { + source: "rpc" as const, + checkpoint: publicSnapshotCheckpoint(model.snapshot), + response: NextResponse.json(response, { + headers: { + "Cache-Control": + response.status === "ready" + ? "public, max-age=0, s-maxage=2, stale-while-revalidate=2" + : "public, max-age=0, s-maxage=60", + }, + }), + }; }, }); } catch (error) { diff --git a/app/api/explore/token/chart/route.ts b/app/api/explore/token/chart/route.ts index b287acbf..046c0378 100644 --- a/app/api/explore/token/chart/route.ts +++ b/app/api/explore/token/chart/route.ts @@ -9,14 +9,40 @@ import { isTokenChartRange, readTokenChartSeries, } from "../../../../../lib/onchain/chart"; +import { + coordinatePublicRouteRead, + PUBLIC_INDEXED_ROUTE_READS, + PUBLIC_DISCOVERY_ROUTE_SCOPES, + preparePublicRouteRequest, + publicSnapshotCheckpoint, +} from "../../../../../lib/data-pipeline/public-route-readiness.server"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; export async function GET(request: NextRequest) { - const input = request.nextUrl.searchParams.get("address")?.trim(); + const routeRequest = await preparePublicRouteRequest( + request.nextUrl.searchParams, + request.headers, + "explore-chart", + ); + if (routeRequest.probeFailure) return routeRequest.probeFailure; + const search = routeRequest.searchParams; + if ( + [...search.keys()].some( + (key) => key !== "address" && key !== "range", + ) || + search.getAll("address").length !== 1 || + search.getAll("range").length > 1 + ) { + return NextResponse.json( + { error: "Unsupported query parameters" }, + { status: 400, headers: { "Cache-Control": "no-store" } }, + ); + } + const input = search.get("address")?.trim(); const requestedRange = - request.nextUrl.searchParams.get("range")?.trim().toLowerCase() ?? "all"; + search.get("range")?.trim().toLowerCase() ?? "all"; if (!input || !isAddress(input)) { return NextResponse.json( { error: "Enter a valid Ethereum token address" }, @@ -31,57 +57,85 @@ export async function GET(request: NextRequest) { } try { - const deployment = getPublicOnchainDeployment(); - const model = await readExploreModel(deployment); - if (deployment.status !== "ready" || model.status !== "ready") { - return NextResponse.json( - { - status: "not-deployed", - points: [], - swapCount: 0, - volumeWei: "0", - volumeEth: "0", - }, - { - headers: { - "Cache-Control": "public, max-age=0, s-maxage=60", - }, - }, - ); - } - const address = getAddress(input); - const token = model.tokens.find( - (candidate) => - candidate.tokenAddress.toLowerCase() === address.toLowerCase(), - ); - if (!token) { - return NextResponse.json( - { error: "Token not found" }, - { status: 404, headers: { "Cache-Control": "no-store" } }, - ); - } + return await coordinatePublicRouteRead({ + route: "explore-chart", + scope: PUBLIC_DISCOVERY_ROUTE_SCOPES, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (transaction) => + PUBLIC_INDEXED_ROUTE_READS.tokenChart(transaction, { + chainId: 1, + address, + range: requestedRange, + }), + async legacy() { + const deployment = getPublicOnchainDeployment(); + const model = await readExploreModel(deployment); + if (deployment.status !== "ready" || model.status !== "ready") { + return { + source: "rpc" as const, + response: NextResponse.json( + { + status: "not-deployed", + address, + points: [], + swapCount: 0, + volumeWei: "0", + volumeEth: "0", + }, + { + headers: { + "Cache-Control": "public, max-age=0, s-maxage=60", + }, + }, + ), + }; + } - const series = await readTokenChartSeries({ - deployment, - token, - snapshotBlock: BigInt(model.snapshot.blockNumber), - ethUsdQuote: model.snapshot.ethUsdQuote, - range: requestedRange, - }); - return NextResponse.json( - { - ...series, - range: requestedRange, - snapshotBlock: model.snapshot.blockNumber, - }, - { - headers: { - "Cache-Control": - "public, max-age=0, s-maxage=15, stale-while-revalidate=15", - }, + const token = model.tokens.find( + (candidate) => + candidate.tokenAddress.toLowerCase() === address.toLowerCase(), + ); + if (!token) { + return { + source: "rpc" as const, + checkpoint: publicSnapshotCheckpoint(model.snapshot), + response: NextResponse.json( + { error: "Token not found" }, + { status: 404, headers: { "Cache-Control": "no-store" } }, + ), + }; + } + + const series = await readTokenChartSeries({ + deployment, + token, + snapshotBlock: BigInt(model.snapshot.blockNumber), + ethUsdQuote: model.snapshot.ethUsdQuote, + range: requestedRange, + }); + return { + source: "rpc" as const, + checkpoint: publicSnapshotCheckpoint(model.snapshot), + response: NextResponse.json( + { + ...series, + address, + range: requestedRange, + snapshotBlock: model.snapshot.blockNumber, + }, + { + headers: { + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }, + }, + ), + }; }, - ); + }); } catch (error) { console.error("Token chart onchain read failed", error); return NextResponse.json( diff --git a/app/api/explore/token/route.ts b/app/api/explore/token/route.ts index d1924300..baed980c 100644 --- a/app/api/explore/token/route.ts +++ b/app/api/explore/token/route.ts @@ -6,12 +6,25 @@ import type { ExplorePage } from "../../../../lib/onchain/types"; import { enrichExplorePageWithOfficialV4Subgraph, } from "../../../../lib/onchain/uniswap-v4-subgraph"; +import { + coordinatePublicRouteRead, + PUBLIC_INDEXED_ROUTE_READS, + PUBLIC_DISCOVERY_ROUTE_SCOPES, + preparePublicRouteRequest, + publicSnapshotCheckpoint, +} from "../../../../lib/data-pipeline/public-route-readiness.server"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; export async function GET(request: NextRequest) { - const search = request.nextUrl.searchParams; + const routeRequest = await preparePublicRouteRequest( + request.nextUrl.searchParams, + request.headers, + "explore-token", + ); + if (routeRequest.probeFailure) return routeRequest.probeFailure; + const search = routeRequest.searchParams; if ( [...search.keys()].some((key) => key !== "address") || search.getAll("address").length !== 1 @@ -31,106 +44,128 @@ export async function GET(request: NextRequest) { try { const address = getAddress(input); - const model = await readExploreModel(); - const token = - model.tokens.find( - (candidate) => - candidate.tokenAddress.toLowerCase() === - address.toLowerCase(), - ) ?? null; + return await coordinatePublicRouteRead({ + route: "explore-token", + scope: PUBLIC_DISCOVERY_ROUTE_SCOPES, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (transaction) => + PUBLIC_INDEXED_ROUTE_READS.tokenDetail(transaction, { + chainId: 1, + address, + }), + async legacy() { + const model = await readExploreModel(); + const token = + model.tokens.find( + (candidate) => + candidate.tokenAddress.toLowerCase() === + address.toLowerCase(), + ) ?? null; - if (model.status === "ready" && !token) { - return NextResponse.json( - { - status: model.status, - token: null, - snapshot: model.snapshot, - }, - { - status: 404, - headers: { "Cache-Control": "no-store" }, - }, - ); - } + if (model.status === "ready" && !token) { + return { + source: "rpc" as const, + checkpoint: publicSnapshotCheckpoint(model.snapshot), + response: NextResponse.json( + { + status: model.status, + token: null, + snapshot: model.snapshot, + }, + { + status: 404, + headers: { "Cache-Control": "no-store" }, + }, + ), + }; + } - let enrichedToken = token; - if (token) { - const tokenPage = { - status: model.status, - tokens: [token], - page: 1, - pageSize: 1, - total: 1, - totalPages: 1, - sort: "market-cap", - query: address, - snapshot: model.snapshot, - launcherFeesAccruedWei: model.launcherFeesAccruedWei, - launcherFeesAccruedEth: model.launcherFeesAccruedEth, - } satisfies ExplorePage; - const enriched = - await enrichExplorePageWithOfficialV4Subgraph(tokenPage); - const enrichedCandidate = enriched.tokens.find( - (candidate) => - candidate.id === token.id && - candidate.tokenAddress.toLowerCase() === - token.tokenAddress.toLowerCase() && - candidate.hookAddress.toLowerCase() === - token.hookAddress.toLowerCase() && - candidate.poolId.toLowerCase() === token.poolId.toLowerCase(), - ); - enrichedToken = enrichedCandidate - ? { - ...token, - ...(enrichedCandidate.indexedMarketCapEth === undefined - ? {} - : { - indexedMarketCapEth: - enrichedCandidate.indexedMarketCapEth, - }), - ...(enrichedCandidate.indexedMarketCapEthWei === undefined - ? {} - : { - indexedMarketCapEthWei: - enrichedCandidate.indexedMarketCapEthWei, - }), - ...(enrichedCandidate.indexedMarketCapUsdWad === undefined - ? {} - : { - indexedMarketCapUsdWad: - enrichedCandidate.indexedMarketCapUsdWad, - }), - ...(enrichedCandidate.indexedValuationBlockNumber === - undefined - ? {} - : { - indexedValuationBlockNumber: - enrichedCandidate.indexedValuationBlockNumber, - }), - ...(enrichedCandidate.uniswapV4Pool === undefined - ? {} - : { - uniswapV4Pool: enrichedCandidate.uniswapV4Pool, - }), - } - : token; - } + let enrichedToken = token; + if (token) { + const tokenPage = { + status: model.status, + tokens: [token], + page: 1, + pageSize: 1, + total: 1, + totalPages: 1, + sort: "market-cap", + query: address, + snapshot: model.snapshot, + launcherFeesAccruedWei: model.launcherFeesAccruedWei, + launcherFeesAccruedEth: model.launcherFeesAccruedEth, + } satisfies ExplorePage; + const enriched = + await enrichExplorePageWithOfficialV4Subgraph(tokenPage); + const enrichedCandidate = enriched.tokens.find( + (candidate) => + candidate.id === token.id && + candidate.tokenAddress.toLowerCase() === + token.tokenAddress.toLowerCase() && + candidate.hookAddress.toLowerCase() === + token.hookAddress.toLowerCase() && + candidate.poolId.toLowerCase() === token.poolId.toLowerCase(), + ); + enrichedToken = enrichedCandidate + ? { + ...token, + ...(enrichedCandidate.indexedMarketCapEth === undefined + ? {} + : { + indexedMarketCapEth: + enrichedCandidate.indexedMarketCapEth, + }), + ...(enrichedCandidate.indexedMarketCapEthWei === undefined + ? {} + : { + indexedMarketCapEthWei: + enrichedCandidate.indexedMarketCapEthWei, + }), + ...(enrichedCandidate.indexedMarketCapUsdWad === undefined + ? {} + : { + indexedMarketCapUsdWad: + enrichedCandidate.indexedMarketCapUsdWad, + }), + ...(enrichedCandidate.indexedValuationBlockNumber === + undefined + ? {} + : { + indexedValuationBlockNumber: + enrichedCandidate.indexedValuationBlockNumber, + }), + ...(enrichedCandidate.uniswapV4Pool === undefined + ? {} + : { + uniswapV4Pool: enrichedCandidate.uniswapV4Pool, + }), + } + : token; + } - return NextResponse.json( - { - status: model.status, - token: enrichedToken, - snapshot: model.snapshot, + return { + source: "rpc" as const, + checkpoint: publicSnapshotCheckpoint(model.snapshot), + response: NextResponse.json( + { + status: model.status, + token: enrichedToken, + snapshot: model.snapshot, + }, + { + headers: { + "Cache-Control": + model.status === "ready" + ? "public, max-age=0, s-maxage=2, stale-while-revalidate=2" + : "public, max-age=0, s-maxage=60", + }, + }, + ), + }; }, - { - headers: { - "Cache-Control": - model.status === "ready" - ? "public, max-age=0, s-maxage=15, stale-while-revalidate=30" - : "public, max-age=0, s-maxage=60", - }, - }, - ); + }); } catch (error) { console.error("Token detail onchain read failed", error); return NextResponse.json( diff --git a/app/api/indexers/v1/read-indexed-feed.server.ts b/app/api/indexers/v1/read-indexed-feed.server.ts new file mode 100644 index 00000000..414abad7 --- /dev/null +++ b/app/api/indexers/v1/read-indexed-feed.server.ts @@ -0,0 +1,706 @@ +import "server-only"; + +import { createHash } from "node:crypto"; + +import { + bytes32FromBytea, + canonicalAddress, + canonicalBytes32, + parseNonnegativeIntegerText, +} from "../../../../lib/data-pipeline/codecs"; +import type { PostgresTransaction } from "../../../../lib/data-pipeline/postgres"; +import { getServerReadModel } from "../../../../lib/data-pipeline/read-model.server"; +import { serializeIndexerToken } from "../../../../lib/onchain/indexer-feed"; +import type { ExploreSnapshot } from "../../../../lib/onchain/types"; +import type { + LauncherToken, + TokenLink, + TokenLinkKind, +} from "../../../../lib/tokens"; +import type { IndexedFeedSnapshot } from "./response"; + +const CHAIN_ID = 1 as const; +const ROUTE_KEY = "explore-list"; +const ADAPTER_VERSION = "indexed-route-adapters-v2"; +const MAX_SNAPSHOT_AGE_MS = 10 * 60 * 1_000; +const MAX_CLOCK_SKEW_MS = 30 * 1_000; +const MAX_PROJECTION_LAG_BLOCKS = 12n; + +const RELEASE_SCOPES = Object.freeze([ + Object.freeze({ release: "classic-v2", model: "classic" }), + Object.freeze({ release: "classic-v3", model: "classic" }), + Object.freeze({ release: "stock-paired-v1", model: "stock-paired" }), + Object.freeze({ release: "stock-paired-v2", model: "stock-paired" }), + Object.freeze({ release: "stock-paired-v3", model: "stock-paired" }), +] as const); + +type ReleaseVersion = (typeof RELEASE_SCOPES)[number]["release"]; +type ModelVersion = (typeof RELEASE_SCOPES)[number]["model"]; +type JsonRecord = Record; + +const RELEASES = RELEASE_SCOPES.map((scope) => scope.release); +const RELEASE_SET = new Set(RELEASES); +const UUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const RAW_HEX = /^0x(?:[0-9a-f]{2})*$/; +const LINK_KINDS = new Set(["website", "x", "telegram"]); + +type IndexedReadModel = Readonly<{ + repeatableReadSnapshot( + work: (transaction: PostgresTransaction) => Promise, + ): Promise; +}>; + +type ReleasePointer = Readonly<{ + routeKey: typeof ROUTE_KEY; + chainId: typeof CHAIN_ID; + releaseVersion: ReleaseVersion; + modelVersion: ModelVersion; + sourceGroup: "core"; + projectorVersion: string; + epochId: string; + pointerGeneration: string; + checkpointId: string; + checkpointGeneration: string; + reorgGeneration: string; + checkpointBlockNumber: string; + checkpointBlockHash: `0x${string}`; +}>; + +type RecordSource = ReleasePointer & + Readonly<{ + snapshotCommitment: `0x${string}`; + projectionRunId: string; + publicationCommitment: `0x${string}`; + promotedBlockNumber: string; + promotedBlockHash: `0x${string}`; + }>; + +type ParityEvidence = Readonly<{ + releaseVersion: ReleaseVersion; + modelVersion: ModelVersion; + parityRecordId: string; + reconciliationId: string; + parityEvidenceCommitment: `0x${string}`; + parityBindingId: string; + parityBindingCommitment: `0x${string}`; + parityBoundAt: string; +}>; + +type ParsedToken = Readonly<{ + token: LauncherToken; + source: RecordSource; +}>; + +function fail(): never { + throw new Error("Indexed feed is not ready"); +} + +function record(value: unknown): JsonRecord { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return fail(); + } + return value as JsonRecord; +} + +function array(value: unknown, maximum = 1_000_000): unknown[] { + if (!Array.isArray(value) || value.length > maximum) return fail(); + return value; +} + +function text(value: unknown, maximum = 4_096): string { + if ( + typeof value !== "string" || + value.length === 0 || + value.length > maximum + ) { + return fail(); + } + return value; +} + +function nullableText(value: unknown, maximum = 4_096): string | undefined { + if (value === undefined || value === null) return undefined; + return text(value, maximum); +} + +function uuid(value: unknown): string { + const parsed = text(value, 64); + if (!UUID.test(parsed)) return fail(); + return parsed.toLowerCase(); +} + +function uintText(value: unknown, maximumDigits = 78): string { + if (typeof value === "bigint") { + if (value < 0n) return fail(); + return value.toString(); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) return fail(); + return String(value); + } + try { + return parseNonnegativeIntegerText(value, maximumDigits); + } catch { + return fail(); + } +} + +function nullableUint(value: unknown): string | undefined { + if (value === undefined || value === null) return undefined; + return uintText(value); +} + +function safeInteger(value: unknown, maximum = Number.MAX_SAFE_INTEGER): number { + const parsed = uintText(value, 16); + const result = Number(parsed); + if (!Number.isSafeInteger(result) || result > maximum) return fail(); + return result; +} + +function nullableSignedInteger( + value: unknown, + maximum = Number.MAX_SAFE_INTEGER, +): number | undefined { + if (value === undefined || value === null) return undefined; + if ( + (typeof value !== "number" && typeof value !== "string") || + !/^-?(?:0|[1-9][0-9]*)$/.test(String(value)) + ) { + return fail(); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || Math.abs(parsed) > maximum) { + return fail(); + } + return parsed; +} + +function bool(value: unknown): boolean { + if (typeof value !== "boolean") return fail(); + return value; +} + +function canonicalTimestamp(value: unknown): string { + const date = value instanceof Date ? value : new Date(text(value, 128)); + if (Number.isNaN(date.valueOf())) return fail(); + return date.toISOString(); +} + +function freshTimestamp(value: unknown, nowMs: number): string { + const parsed = canonicalTimestamp(value); + const age = nowMs - Date.parse(parsed); + if (age < -MAX_CLOCK_SKEW_MS || age > MAX_SNAPSHOT_AGE_MS) return fail(); + return parsed; +} + +function address(value: unknown): `0x${string}` { + try { + return canonicalAddress(value); + } catch { + return fail(); + } +} + +function nullableAddress(value: unknown): `0x${string}` | undefined { + if (value === undefined || value === null) return undefined; + return address(value); +} + +function bytes32(value: unknown): `0x${string}` { + try { + return canonicalBytes32(value); + } catch { + return fail(); + } +} + +function byteaBytes32(value: unknown): `0x${string}` { + try { + return bytes32FromBytea(value); + } catch { + return fail(); + } +} + +function rawHex(value: unknown): `0x${string}` { + const parsed = text(value, 8_192).toLowerCase(); + if (!RAW_HEX.test(parsed)) return fail(); + return parsed as `0x${string}`; +} + +function expectExact(value: unknown, expected: unknown): void { + if (value !== expected) fail(); +} + +function release(value: unknown): ReleaseVersion { + const parsed = text(value, 64); + if (!RELEASE_SET.has(parsed)) return fail(); + return parsed as ReleaseVersion; +} + +function expectedModel(releaseVersion: ReleaseVersion): ModelVersion { + const scope = RELEASE_SCOPES.find( + (candidate) => candidate.release === releaseVersion, + ); + if (!scope) return fail(); + return scope.model; +} + +function model(value: unknown, releaseVersion: ReleaseVersion): ModelVersion { + const expected = expectedModel(releaseVersion); + expectExact(value, expected); + return expected; +} + +function parseLinks(value: unknown): TokenLink[] | undefined { + if (value === undefined || value === null) return undefined; + const entries = array(value, 3); + const kinds = new Set(); + return entries.map((entry) => { + const link = record(entry); + const kind = text(link.kind, 16) as TokenLinkKind; + if (!LINK_KINDS.has(kind) || kinds.has(kind)) return fail(); + kinds.add(kind); + const url = text(link.url, 2_048); + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return fail(); + } + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + return fail(); + } + return { kind, url }; + }); +} + +function parseReleasePointer(value: unknown): ReleasePointer { + const input = record(value); + expectExact(input.routeKey, ROUTE_KEY); + if (safeInteger(input.chainId, 10_000_000) !== CHAIN_ID) return fail(); + const releaseVersion = release(input.releaseVersion); + return Object.freeze({ + routeKey: ROUTE_KEY, + chainId: CHAIN_ID, + releaseVersion, + modelVersion: model(input.modelVersion, releaseVersion), + sourceGroup: (() => { + expectExact(input.sourceGroup, "core"); + return "core" as const; + })(), + projectorVersion: text(input.projectorVersion, 128), + epochId: uuid(input.epochId), + pointerGeneration: uintText(input.pointerGeneration), + checkpointId: uuid(input.checkpointId), + checkpointGeneration: uintText(input.checkpointGeneration), + reorgGeneration: uintText(input.reorgGeneration), + checkpointBlockNumber: uintText(input.checkpointBlockNumber), + checkpointBlockHash: bytes32(input.checkpointBlockHash), + }); +} + +function parseRecordSource(value: unknown): RecordSource { + const input = record(value); + return Object.freeze({ + ...parseReleasePointer(input), + snapshotCommitment: bytes32(input.snapshotCommitment), + projectionRunId: uuid(input.projectionRunId), + publicationCommitment: bytes32(input.publicationCommitment), + promotedBlockNumber: uintText(input.promotedBlockNumber), + promotedBlockHash: bytes32(input.promotedBlockHash), + }); +} + +function parseParityEvidence(value: unknown): ParityEvidence { + const input = record(value); + const releaseVersion = release(input.releaseVersion); + return Object.freeze({ + releaseVersion, + modelVersion: model(input.modelVersion, releaseVersion), + parityRecordId: uuid(input.parityRecordId), + reconciliationId: uuid(input.reconciliationId), + parityEvidenceCommitment: bytes32(input.parityEvidenceCommitment), + parityBindingId: uuid(input.parityBindingId), + parityBindingCommitment: bytes32(input.parityBindingCommitment), + parityBoundAt: canonicalTimestamp(input.parityBoundAt), + }); +} + +function parseSnapshot( + value: unknown, + nowMs: number, +): { + publicSnapshot: ExploreSnapshot; + capturedAt: string; + reconciledAt: string; + safeBlockNumber: string; + snapshotCommitment: `0x${string}`; + pointers: readonly ReleasePointer[]; +} { + const input = record(value); + expectExact(input.adapterVersion, ADAPTER_VERSION); + if (safeInteger(input.chainId, 10_000_000) !== CHAIN_ID) return fail(); + const blockNumber = uintText(input.blockNumber); + const blockHash = bytes32(input.blockHash); + const confirmations = safeInteger(input.confirmations, 1_000_000); + const capturedAt = freshTimestamp(input.capturedAt, nowMs); + const reconciledAt = freshTimestamp(input.reconciledAt, nowMs); + const safeBlockNumber = uintText(input.safeBlockNumber); + const snapshotCommitment = bytes32(input.snapshotCommitment); + if (snapshotCommitment !== blockHash) return fail(); + const lag = BigInt(safeBlockNumber) - BigInt(blockNumber); + if (lag < 0n || lag > MAX_PROJECTION_LAG_BLOCKS) return fail(); + + const pointers = array(input.releasePointers, RELEASES.length).map( + parseReleasePointer, + ); + const pointerMap = uniqueReleaseMap(pointers); + for (const releaseVersion of RELEASES) { + const pointer = pointerMap.get(releaseVersion)!; + if ( + pointer.checkpointBlockNumber !== blockNumber || + pointer.checkpointBlockHash !== blockHash + ) { + return fail(); + } + } + + return Object.freeze({ + publicSnapshot: Object.freeze({ + chainId: CHAIN_ID, + blockNumber, + blockHash, + confirmations, + }), + capturedAt, + reconciledAt, + safeBlockNumber, + snapshotCommitment, + pointers, + }); +} + +function uniqueReleaseMap( + values: readonly T[], +): ReadonlyMap { + const result = new Map(); + for (const value of values) { + if (result.has(value.releaseVersion)) return fail(); + result.set(value.releaseVersion, value); + } + if ( + result.size !== RELEASES.length || + RELEASES.some((releaseVersion) => !result.has(releaseVersion)) + ) { + return fail(); + } + return result; +} + +function samePointer(source: RecordSource, pointer: ReleasePointer): boolean { + return ( + source.routeKey === pointer.routeKey && + source.chainId === pointer.chainId && + source.releaseVersion === pointer.releaseVersion && + source.modelVersion === pointer.modelVersion && + source.sourceGroup === pointer.sourceGroup && + source.projectorVersion === pointer.projectorVersion && + source.epochId === pointer.epochId && + source.pointerGeneration === pointer.pointerGeneration && + source.checkpointId === pointer.checkpointId && + source.checkpointGeneration === pointer.checkpointGeneration && + source.reorgGeneration === pointer.reorgGeneration && + source.checkpointBlockNumber === pointer.checkpointBlockNumber && + source.checkpointBlockHash === pointer.checkpointBlockHash + ); +} + +function parseRawToken(value: unknown): ParsedToken { + const input = record(value); + const source = parseRecordSource(input.source); + const metadata = + input.metadata === null || input.metadata === undefined + ? undefined + : record(input.metadata); + const liquidity = record(input.liquidity); + const fees = record(input.fees); + + const token: LauncherToken = { + id: `${CHAIN_ID}:${address(input.tokenAddress)}`, + name: text(input.name, 128), + symbol: text(input.symbol, 32), + tokenAddress: address(input.tokenAddress), + hookAddress: address(input.hookAddress), + poolId: bytes32(input.poolId), + creatorAddress: address(input.creatorAddress), + launchedAt: canonicalTimestamp(input.launchedAt), + totalSupplyRaw: uintText(input.totalSupplyRaw), + tokenDecimals: safeInteger(input.decimals, 36), + tokenLiquidityAmountRaw: nullableUint( + liquidity.tokenLiquidityAmountRaw, + ), + lockedTokenDustRaw: nullableUint(liquidity.lockedTokenDustRaw), + currentTick: nullableSignedInteger(liquidity.currentTick, 8_388_607), + initialTick: nullableSignedInteger(liquidity.initialTick, 8_388_607), + tickLower: nullableSignedInteger(liquidity.tickLower, 8_388_607), + tickUpper: nullableSignedInteger(liquidity.tickUpper, 8_388_607), + activeLiquidity: nullableUint(liquidity.activeLiquidity), + totalSwapFeeBps: safeInteger(fees.totalSwapFeeBps, 10_000), + buyHookFeeBps: safeInteger(fees.buySwapFeeBps, 10_000), + sellHookFeeBps: safeInteger(fees.sellSwapFeeBps, 10_000), + buyCreatorFeeBps: safeInteger(fees.buyCreatorFeeBps, 10_000), + sellCreatorFeeBps: safeInteger(fees.sellCreatorFeeBps, 10_000), + launcherFeeBps: safeInteger(fees.launcherFeeBps, 10_000), + programmableFeeBps: safeInteger(fees.launcherFeeBps, 10_000), + transferTaxBps: safeInteger(fees.transferTaxBps, 10_000), + lpFeePips: safeInteger(fees.lpFeePips, 1_000_000), + launchModel: source.modelVersion, + liquidityPath: "meme", + }; + + if (token.buyCreatorFeeBps === token.sellCreatorFeeBps) { + token.creatorFeeBps = token.buyCreatorFeeBps; + } + + if (metadata) { + uintText(metadata.revision); + canonicalTimestamp(metadata.createdAt); + const description = nullableText(metadata.description, 4_096); + if (description !== undefined) token.description = description; + const imageUrl = nullableText(metadata.imageUrl, 2_048); + if (imageUrl !== undefined) token.imageUrl = imageUrl; + const links = parseLinks(metadata.links); + if (links !== undefined) token.links = links; + if (metadata.extraData !== undefined && metadata.extraData !== null) { + token.metadataExtraData = rawHex(metadata.extraData); + } + } + + const positionRecipient = nullableAddress(input.positionRecipient); + if (positionRecipient) token.positionRecipient = positionRecipient; + const positionTokenId = nullableUint(input.positionTokenId); + if (positionTokenId !== undefined) token.positionTokenId = positionTokenId; + const rewardVaultAddress = nullableAddress(input.rewardVaultAddress); + if (rewardVaultAddress) token.rewardVaultAddress = rewardVaultAddress; + token.launchHash = bytes32(input.launchHash); + token.launchBlockNumber = uintText(input.launchBlockNumber); + token.launchTransactionHash = bytes32(input.launchTransactionHash); + token.launchTransactionIndex = safeInteger( + input.launchTransactionIndex, + 0xffff_ffff, + ); + token.launchLogIndex = safeInteger(input.launchLogIndex, 0xffff_ffff); + + if (source.releaseVersion === "classic-v3") { + token.launchModelVersion = "classic-v3"; + } else if (source.modelVersion === "stock-paired") { + token.launchModelVersion = source.releaseVersion as + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + const quote = record(input.quote); + token.quoteAssetAddress = address(quote.address); + token.quoteAssetSymbol = text(quote.symbol, 32); + token.quoteAssetName = text(quote.name, 128); + safeInteger(quote.decimals, 36); + token.quoteIsCurrency0 = bool(quote.isCurrency0); + } else if (input.quote !== null && input.quote !== undefined) { + return fail(); + } + + serializeIndexerToken(token, CHAIN_ID); + return Object.freeze({ token: Object.freeze(token), source }); +} + +function parseRecordScope(value: unknown): { + releaseVersion: ReleaseVersion; + modelVersion: ModelVersion; +} { + const input = record(value); + const releaseVersion = release(input.releaseVersion); + return Object.freeze({ + releaseVersion, + modelVersion: model(input.model, releaseVersion), + }); +} + +function sourceCommitment(value: unknown): `0x${string}` { + return `0x${createHash("sha256") + .update(JSON.stringify(value)) + .digest("hex")}`; +} + +async function querySnapshot( + transaction: PostgresTransaction, + nowMs: number, +): Promise { + const rows = await transaction.query( + `select + http_status, + payload, + payload_complete, + record_count::text as record_count, + record_scopes, + comparison_checkpoint_block_number::text + as comparison_checkpoint_block_number, + comparison_checkpoint_block_hash, + route_evidence, + snapshot, + tokens, + record_sources, + captured_at, + reconciled_at, + snapshot_commitment + from programmable_private.get_public_indexer_feed_v1($1)`, + [CHAIN_ID], + ); + if (rows.length !== 1) return fail(); + const row = rows[0]!; + if (safeInteger(row.http_status, 599) !== 200 || row.payload_complete !== true) { + return fail(); + } + + const parsedSnapshot = parseSnapshot(row.snapshot, nowMs); + const capturedAt = freshTimestamp(row.captured_at, nowMs); + const reconciledAt = freshTimestamp(row.reconciled_at, nowMs); + if ( + capturedAt !== parsedSnapshot.capturedAt || + reconciledAt !== parsedSnapshot.reconciledAt || + byteaBytes32(row.snapshot_commitment) !== + parsedSnapshot.snapshotCommitment || + uintText(row.comparison_checkpoint_block_number) !== + parsedSnapshot.publicSnapshot.blockNumber || + byteaBytes32(row.comparison_checkpoint_block_hash) !== + parsedSnapshot.publicSnapshot.blockHash + ) { + return fail(); + } + + const pointerMap = uniqueReleaseMap(parsedSnapshot.pointers); + const routeEvidence = array(row.route_evidence, RELEASES.length).map( + parseParityEvidence, + ); + const evidenceMap = uniqueReleaseMap(routeEvidence); + const oldestParityBoundAt = routeEvidence + .map((evidence) => evidence.parityBoundAt) + .sort()[0]!; + if (oldestParityBoundAt !== reconciledAt) return fail(); + + const parsedTokens = array(row.tokens).map(parseRawToken); + const recordSources = array(row.record_sources); + if ( + safeInteger(row.record_count, 1_000_000) !== parsedTokens.length || + recordSources.length !== parsedTokens.length + ) { + return fail(); + } + + const tokenAddresses = new Set(); + for (let index = 0; index < parsedTokens.length; index += 1) { + const parsed = parsedTokens[index]!; + const pointer = pointerMap.get(parsed.source.releaseVersion)!; + if ( + !samePointer(parsed.source, pointer) || + parsed.source.snapshotCommitment !== + parsedSnapshot.snapshotCommitment || + tokenAddresses.has(parsed.token.tokenAddress) + ) { + return fail(); + } + tokenAddresses.add(parsed.token.tokenAddress); + + const recordSource = record(recordSources[index]); + if (address(recordSource.tokenAddress) !== parsed.token.tokenAddress) { + return fail(); + } + const repeatedSource = parseRecordSource(recordSource.source); + if (JSON.stringify(repeatedSource) !== JSON.stringify(parsed.source)) { + return fail(); + } + const parity = parseParityEvidence(recordSource.parity); + const expectedParity = evidenceMap.get(parsed.source.releaseVersion)!; + if (JSON.stringify(parity) !== JSON.stringify(expectedParity)) { + return fail(); + } + } + + const actualScopes = new Map(); + for (const token of parsedTokens) { + const key = `${token.source.modelVersion}:${token.source.releaseVersion}`; + actualScopes.set(key, (actualScopes.get(key) ?? 0) + 1); + } + const declaredScopes = array(row.record_scopes, RELEASES.length).map( + parseRecordScope, + ); + const declaredKeys = new Set( + declaredScopes.map( + (scope) => `${scope.modelVersion}:${scope.releaseVersion}`, + ), + ); + if ( + declaredKeys.size !== declaredScopes.length || + declaredKeys.size !== actualScopes.size || + [...actualScopes.keys()].some((key) => !declaredKeys.has(key)) + ) { + return fail(); + } + + const payload = record(row.payload); + expectExact(payload.status, "ready"); + const payloadData = record(payload.data); + if ( + JSON.stringify(payload.snapshot) !== JSON.stringify(row.snapshot) || + JSON.stringify(payloadData.tokens) !== JSON.stringify(row.tokens) || + JSON.stringify(payloadData.recordSources) !== + JSON.stringify(row.record_sources) + ) { + return fail(); + } + + return Object.freeze({ + chainId: CHAIN_ID, + model: Object.freeze({ + status: "ready" as const, + tokens: parsedTokens.map((item) => item.token), + snapshot: parsedSnapshot.publicSnapshot, + // These fields are not serialized by either public indexer contract. + // Their neutral values avoid introducing a second claims/totals query. + creatorClaims: [], + launcherFeesAccruedWei: "0", + launcherFeesAccruedEth: "0", + }), + capturedAt, + snapshotCommitment: parsedSnapshot.snapshotCommitment, + sourceCommitment: sourceCommitment({ routeEvidence, recordSources }), + projectionLag: Number( + BigInt(parsedSnapshot.safeBlockNumber) - + BigInt(parsedSnapshot.publicSnapshot.blockNumber), + ), + reconciledAt, + releaseVersions: Object.freeze([...RELEASES]), + }); +} + +export async function readIndexedFeedSnapshotWithModel( + readModel: IndexedReadModel, + nowMs = Date.now(), +): Promise { + if (!Number.isSafeInteger(nowMs) || nowMs < 0) return fail(); + return readModel.repeatableReadSnapshot((transaction) => + querySnapshot(transaction, nowMs), + ); +} + +/** + * Reads the complete GMGN/indexer feed through the API-reader-only aggregate + * function. The database returns no row unless all five supported releases + * share one current checkpoint and every launch has complete source, parity + * and publication evidence; the adapter independently validates that envelope. + */ +export async function readIndexedFeedSnapshot(): Promise { + const readModel = await getServerReadModel(); + if (!readModel) return fail(); + return readIndexedFeedSnapshotWithModel(readModel); +} diff --git a/app/api/indexers/v1/response.ts b/app/api/indexers/v1/response.ts new file mode 100644 index 00000000..661c198a --- /dev/null +++ b/app/api/indexers/v1/response.ts @@ -0,0 +1,88 @@ +import type { ExploreReadModel } from "../../../../lib/onchain/types"; + +export type IndexedFeedSnapshot = Readonly<{ + chainId: 1; + model: ExploreReadModel & { status: "ready" }; + capturedAt: string; + snapshotCommitment: `0x${string}`; + sourceCommitment: `0x${string}`; + projectionLag: number; + reconciledAt: string; + releaseVersions: readonly string[]; +}>; + +const EXPOSED_PROVENANCE_HEADERS = [ + "X-Programmable-Read-Source", + "X-Programmable-Projection-Block", + "X-Programmable-Projection-Hash", + "X-Programmable-Projection-Lag", + "X-Programmable-Reconciled-At", + "X-Programmable-Release-Version", + "X-Programmable-Snapshot-Commitment", + "X-Programmable-Source-Commitment", +] as const; + +export const INDEXER_READY_CACHE_CONTROL = + "public, max-age=0, s-maxage=2, stale-while-revalidate=2"; + +const BYTES32 = /^0x[0-9a-f]{64}$/; +const RELEASE_VERSION = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +function exactTimestamp(value: string, field: string) { + const parsed = new Date(value); + if (Number.isNaN(parsed.valueOf()) || parsed.toISOString() !== value) { + throw new Error(`Indexed feed ${field} is invalid`); + } +} + +function assertHeaderEvidence(snapshot: IndexedFeedSnapshot) { + if ( + !snapshot.model.snapshot || + snapshot.model.snapshot.chainId !== snapshot.chainId || + !BYTES32.test(snapshot.model.snapshot.blockHash) || + !BYTES32.test(snapshot.snapshotCommitment) || + !BYTES32.test(snapshot.sourceCommitment) || + !Number.isSafeInteger(snapshot.projectionLag) || + snapshot.projectionLag < 0 || + snapshot.projectionLag > 1_000_000 || + snapshot.releaseVersions.length === 0 || + snapshot.releaseVersions.length > 32 || + snapshot.releaseVersions.some( + (release, index) => + !RELEASE_VERSION.test(release) || + release.length > 64 || + (index > 0 && snapshot.releaseVersions[index - 1]! >= release), + ) + ) { + throw new Error("Indexed feed provenance is invalid"); + } + exactTimestamp(snapshot.capturedAt, "capture time"); + exactTimestamp(snapshot.reconciledAt, "reconciliation time"); +} + +export function indexedFeedHeaders( + snapshot: IndexedFeedSnapshot, + cacheControl = INDEXER_READY_CACHE_CONTROL, +): Readonly> { + assertHeaderEvidence(snapshot); + + return Object.freeze({ + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": EXPOSED_PROVENANCE_HEADERS.join(", "), + "Cache-Control": cacheControl, + "X-Programmable-Read-Source": "indexed", + "X-Programmable-Projection-Block": + snapshot.model.snapshot!.blockNumber, + "X-Programmable-Projection-Hash": snapshot.model.snapshot!.blockHash, + "X-Programmable-Projection-Lag": String(snapshot.projectionLag), + "X-Programmable-Reconciled-At": snapshot.reconciledAt, + "X-Programmable-Release-Version": snapshot.releaseVersions.join(","), + "X-Programmable-Snapshot-Commitment": snapshot.snapshotCommitment, + "X-Programmable-Source-Commitment": snapshot.sourceCommitment, + }); +} + +export const INDEXER_NO_STORE_HEADERS = Object.freeze({ + "Access-Control-Allow-Origin": "*", + "Cache-Control": "no-store", +}); diff --git a/app/api/indexers/v1/token-list/route.ts b/app/api/indexers/v1/token-list/route.ts index 7f37643c..3f447cd6 100644 --- a/app/api/indexers/v1/token-list/route.ts +++ b/app/api/indexers/v1/token-list/route.ts @@ -5,44 +5,83 @@ import { getPublicOnchainDeployment, readExploreModel, } from "../../../../../lib/onchain"; +import { indexedPublicIndexerFeedEnabled } from "../../../../../lib/data-pipeline/route-activation.server"; +import { readIndexedFeedSnapshot } from "../read-indexed-feed.server"; +import { + indexedFeedHeaders, + INDEXER_NO_STORE_HEADERS, +} from "../response"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; export async function GET() { try { - const deployment = getPublicOnchainDeployment(); - const model = await readExploreModel(deployment); - if (model.tokens.length === 0) { + if (!indexedPublicIndexerFeedEnabled()) { + const deployment = getPublicOnchainDeployment(); + const model = await readExploreModel(deployment); + if (model.tokens.length === 0) { + return NextResponse.json( + { + status: model.status, + error: + "The token list will be available after the first verified launch", + }, + { + status: 503, + headers: { + "Access-Control-Allow-Origin": "*", + "Cache-Control": "public, max-age=0, s-maxage=60", + "Retry-After": "60", + }, + }, + ); + } return NextResponse.json( + buildUniswapTokenList(model, deployment.chainId), { - status: model.status, + headers: { + "Access-Control-Allow-Origin": "*", + "Cache-Control": + model.status === "ready" + ? "public, max-age=0, s-maxage=60, stale-while-revalidate=300" + : "public, max-age=0, s-maxage=60", + }, + }, + ); + } + + const snapshot = await readIndexedFeedSnapshot(); + if (snapshot.model.tokens.length === 0) { + return NextResponse.json( + { + status: snapshot.model.status, error: "The token list will be available after the first verified launch", }, { status: 503, headers: { - "Access-Control-Allow-Origin": "*", - "Cache-Control": "public, max-age=0, s-maxage=60", + ...indexedFeedHeaders( + snapshot, + "public, max-age=0, s-maxage=60", + ), "Retry-After": "60", }, }, ); } const tokenList = buildUniswapTokenList( - model, - deployment.chainId, + snapshot.model, + snapshot.chainId, + new Date(snapshot.capturedAt), ); return NextResponse.json(tokenList, { - headers: { - "Access-Control-Allow-Origin": "*", - "Cache-Control": - model.status === "ready" - ? "public, max-age=0, s-maxage=60, stale-while-revalidate=300" - : "public, max-age=0, s-maxage=60", - }, + headers: indexedFeedHeaders( + snapshot, + "public, max-age=0, s-maxage=60, stale-while-revalidate=300", + ), }); } catch (error) { console.error("Public token list failed", error); @@ -50,10 +89,7 @@ export async function GET() { { error: "Token list is temporarily unavailable" }, { status: 503, - headers: { - "Access-Control-Allow-Origin": "*", - "Cache-Control": "no-store", - }, + headers: INDEXER_NO_STORE_HEADERS, }, ); } diff --git a/app/api/indexers/v1/tokens/route.ts b/app/api/indexers/v1/tokens/route.ts index 0274bc95..4bb5e07e 100644 --- a/app/api/indexers/v1/tokens/route.ts +++ b/app/api/indexers/v1/tokens/route.ts @@ -7,15 +7,21 @@ import { getPublicOnchainDeployment, readExploreModel, } from "../../../../../lib/onchain"; +import { indexedPublicIndexerFeedEnabled } from "../../../../../lib/data-pipeline/route-activation.server"; +import { readIndexedFeedSnapshot } from "../read-indexed-feed.server"; +import { + indexedFeedHeaders, + INDEXER_NO_STORE_HEADERS, +} from "../response"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; -const publicHeaders = { +const LEGACY_PUBLIC_HEADERS = Object.freeze({ "Access-Control-Allow-Origin": "*", "Cache-Control": - "public, max-age=0, s-maxage=15, stale-while-revalidate=30", -}; + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", +}); export async function GET(request: Request) { const address = new URL(request.url).searchParams.get("address"); @@ -25,22 +31,51 @@ export async function GET(request: Request) { { error: "Invalid token address" }, { status: 400, - headers: { - "Access-Control-Allow-Origin": "*", - "Cache-Control": "no-store", - }, + headers: INDEXER_NO_STORE_HEADERS, }, ); } try { - const deployment = getPublicOnchainDeployment(); - const model = await readExploreModel(deployment); + if (!indexedPublicIndexerFeedEnabled()) { + const deployment = getPublicOnchainDeployment(); + const model = await readExploreModel(deployment); + if (address) { + const token = findIndexerToken( + model, + deployment.chainId, + getAddress(address), + ); + if (!token) { + return NextResponse.json( + { error: "Programmable token not found" }, + { status: 404, headers: LEGACY_PUBLIC_HEADERS }, + ); + } + return NextResponse.json(token, { + headers: LEGACY_PUBLIC_HEADERS, + }); + } + return NextResponse.json( + buildIndexerFeed(model, deployment.chainId), + { + headers: { + ...LEGACY_PUBLIC_HEADERS, + "Cache-Control": + model.status === "ready" + ? LEGACY_PUBLIC_HEADERS["Cache-Control"] + : "public, max-age=0, s-maxage=60", + }, + }, + ); + } + + const snapshot = await readIndexedFeedSnapshot(); if (address) { const token = findIndexerToken( - model, - deployment.chainId, + snapshot.model, + snapshot.chainId, getAddress(address), ); @@ -49,26 +84,20 @@ export async function GET(request: Request) { { error: "Programmable token not found" }, { status: 404, - headers: publicHeaders, + headers: indexedFeedHeaders(snapshot), }, ); } return NextResponse.json(token, { - headers: publicHeaders, + headers: indexedFeedHeaders(snapshot), }); } - const feed = buildIndexerFeed(model, deployment.chainId); + const feed = buildIndexerFeed(snapshot.model, snapshot.chainId); return NextResponse.json(feed, { - headers: { - "Access-Control-Allow-Origin": "*", - "Cache-Control": - model.status === "ready" - ? publicHeaders["Cache-Control"] - : "public, max-age=0, s-maxage=60", - }, + headers: indexedFeedHeaders(snapshot), }); } catch (error) { console.error("Public indexer feed failed", error); @@ -76,10 +105,7 @@ export async function GET(request: Request) { { error: "Indexer data is temporarily unavailable" }, { status: 503, - headers: { - "Access-Control-Allow-Origin": "*", - "Cache-Control": "no-store", - }, + headers: INDEXER_NO_STORE_HEADERS, }, ); } diff --git a/app/api/launch/preflight/route.ts b/app/api/launch/preflight/route.ts index 6a8cd0a2..0b2fd8b9 100644 --- a/app/api/launch/preflight/route.ts +++ b/app/api/launch/preflight/route.ts @@ -114,7 +114,6 @@ import { getConfiguredStockPairedLaunchRelease, type VerifiedStockPairedRelease, } from "@/lib/stock-paired-release"; -import { isStockPairedPublicLaunchEnabled } from "@/lib/stock-paired-access"; import { assessStockPairedRuntimeFdv, STOCK_PAIRED_RUNTIME_FDV_PROBE_WEI, @@ -150,11 +149,6 @@ const selectedStockPairedRelease = launchEnvironment === "production" ? getConfiguredStockPairedLaunchRelease() : null; -const stockPairedPublicLaunchEnabled = - isStockPairedPublicLaunchEnabled( - launchEnvironment, - selectedStockPairedRelease, - ); const client = createPublicClient({ chain: launchChain, @@ -346,6 +340,34 @@ function errorResponse(message: string, status = 400) { ); } +function deepLaunchClosedResponse() { + return NextResponse.json( + { + code: "deep_launches_closed", + error: "New Deep launches are not available", + }, + { + status: 410, + headers: { "Cache-Control": "no-store" }, + }, + ); +} + +function requestsClosedDeepLaunch(input: unknown) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + return false; + } + const model = (input as Record).launchModel; + if (typeof model !== "string") return false; + const normalized = model.trim().toLowerCase(); + return ( + normalized === "deep" || + normalized.startsWith("deep-") || + normalized === "liquidity-growth" || + normalized.startsWith("liquidity-growth-") + ); +} + function parseDraft(input: unknown): LaunchDraft { if (!input || typeof input !== "object") { throw new LaunchInputError("The launch setup is missing"); @@ -1410,6 +1432,9 @@ async function prepareClassicV3Launch( }); } +// Retained as historical release evidence. The public route closes Deep before +// this transaction builder can be reached. +// eslint-disable-next-line @typescript-eslint/no-unused-vars async function prepareDeepLaunch( account: Address, draft: LaunchDraft, @@ -2462,6 +2487,9 @@ async function findStockPairedCurrency0Salt({ ); } +// Retained as release evidence for historical Stock-Paired deployments. No +// public route calls this transaction builder after new launches were closed. +// eslint-disable-next-line @typescript-eslint/no-unused-vars async function prepareStockPairedLaunch( account: Address, draft: LaunchDraft, @@ -2709,6 +2737,9 @@ export async function POST(request: NextRequest) { } const record = body as Record; + if (requestsClosedDeepLaunch(record.draft)) { + return deepLaunchClosedResponse(); + } if (typeof record.account !== "string" || !isAddress(record.account)) { return errorResponse("Connect a valid Ethereum wallet"); } @@ -2733,16 +2764,18 @@ export async function POST(request: NextRequest) { ); } if (draft.launchModel === "deep") { - return await prepareDeepLaunch(account, draft, connectedWalletCheck); + return deepLaunchClosedResponse(); } if (draft.launchModel === "stock-paired") { - if (!stockPairedPublicLaunchEnabled) { - return errorResponse("Stock-Paired is coming soon", 403); - } - return await prepareStockPairedLaunch( - account, - draft, - connectedWalletCheck, + return NextResponse.json( + { + code: "stock_paired_launches_closed", + error: "New Stock-Paired launches are no longer available", + }, + { + status: 410, + headers: { "Cache-Control": "no-store" }, + }, ); } return await prepareMemeLaunch( diff --git a/app/api/ops/index-v2/route.ts b/app/api/ops/index-v2/route.ts index 9875c23f..51831e05 100644 --- a/app/api/ops/index-v2/route.ts +++ b/app/api/ops/index-v2/route.ts @@ -1,5 +1,131 @@ -export { GET } from "../index/route"; +import { timingSafeEqual } from "node:crypto"; + +import { NextRequest, NextResponse } from "next/server"; + +import { + getOperationalOnchainDeployment, + readLiveExploreModel, + writeDurableExploreModel, +} from "../../../../lib/onchain"; +import { writePortfolioHistorySnapshot } from "../../../../lib/profile/portfolio-history-storage.server"; export const dynamic = "force-dynamic"; export const maxDuration = 300; export const runtime = "nodejs"; + +const INDEX_READ_ATTEMPTS = 2; + +function isAuthorized(request: NextRequest) { + const cronSecret = process.env.CRON_SECRET; + const authorization = request.headers.get("authorization"); + const secretLength = typeof cronSecret === "string" + ? Buffer.byteLength(cronSecret, "utf8") + : 0; + if ( + typeof cronSecret !== "string" || + secretLength < 32 || + secretLength > 1_024 || + !authorization?.startsWith("Bearer ") + ) { + return false; + } + + const provided = Buffer.from(authorization.slice(7), "utf8"); + const expected = Buffer.from(cronSecret, "utf8"); + return ( + provided.length === expected.length && + timingSafeEqual(provided, expected) + ); +} + +export async function GET(request: NextRequest) { + if (!isAuthorized(request)) { + return NextResponse.json( + { error: "Unauthorized" }, + { + status: 401, + headers: { "Cache-Control": "no-store" }, + }, + ); + } + + const startedAt = Date.now(); + try { + const deployment = getOperationalOnchainDeployment("production"); + if (deployment.status !== "ready") { + throw new Error( + "The verified production release is not operationally eligible", + ); + } + let model: Awaited> | null = + null; + let lastReadError: unknown; + for (let attempt = 1; attempt <= INDEX_READ_ATTEMPTS; attempt += 1) { + try { + model = await readLiveExploreModel(deployment); + break; + } catch (error) { + lastReadError = error; + if (attempt < INDEX_READ_ATTEMPTS) { + console.warn("Programmable index read will retry", { + attempt, + errorName: + error instanceof Error ? error.name : "UnknownIndexError", + }); + } + } + } + if (!model) { + throw lastReadError ?? new Error("Index read failed"); + } + if (model.status !== "ready") { + throw new Error("The live Explore model is not ready"); + } + const [result, history] = await Promise.all([ + writeDurableExploreModel(deployment, model), + writePortfolioHistorySnapshot(model), + ]); + console.info("Programmable index refresh completed", { + blockNumber: result.blockNumber, + tokenCount: result.tokenCount, + updated: result.updated, + portfolioHistoryStatus: history.status, + portfolioHistoryPath: history.path, + deepReleaseVersion: result.deepReleaseVersion, + deepLifecycleEvidenceHash: + result.deepLifecycleEvidenceHash, + durationMs: Date.now() - startedAt, + }); + return NextResponse.json( + { + ok: true, + blockNumber: result.blockNumber, + tokenCount: result.tokenCount, + updated: result.updated, + portfolioHistory: { + status: history.status, + path: history.path, + tokenCount: history.tokenCount, + blockNumber: history.blockNumber, + }, + deepReleaseVersion: result.deepReleaseVersion, + deepLifecycleEvidenceHash: + result.deepLifecycleEvidenceHash, + }, + { headers: { "Cache-Control": "no-store" } }, + ); + } catch (error) { + console.error("Programmable index refresh failed", { + errorName: + error instanceof Error ? error.name : "UnknownIndexError", + durationMs: Date.now() - startedAt, + }); + return NextResponse.json( + { error: "Index refresh failed" }, + { + status: 503, + headers: { "Cache-Control": "no-store" }, + }, + ); + } +} diff --git a/app/api/ops/index/route.ts b/app/api/ops/index/route.ts index 84da64e6..399406c8 100644 --- a/app/api/ops/index/route.ts +++ b/app/api/ops/index/route.ts @@ -1,123 +1,17 @@ -import { timingSafeEqual } from "node:crypto"; - -import { NextRequest, NextResponse } from "next/server"; - -import { - getOperationalOnchainDeployment, - readLiveExploreModel, - writeDurableExploreModel, -} from "../../../../lib/onchain"; -import { writePortfolioHistorySnapshot } from "../../../../lib/profile/portfolio-history-storage.server"; +import { NextResponse } from "next/server"; export const dynamic = "force-dynamic"; -export const maxDuration = 300; export const runtime = "nodejs"; -const INDEX_READ_ATTEMPTS = 2; - -function isAuthorized(request: NextRequest) { - const cronSecret = process.env.CRON_SECRET; - const authorization = request.headers.get("authorization"); - if (!cronSecret || !authorization?.startsWith("Bearer ")) { - return false; - } - - const provided = Buffer.from(authorization.slice(7)); - const expected = Buffer.from(cronSecret); - return ( - provided.length === expected.length && - timingSafeEqual(provided, expected) +export async function GET() { + return NextResponse.json( + { + error: "Legacy index route closed", + code: "legacy_index_route_closed", + }, + { + status: 410, + headers: { "Cache-Control": "no-store" }, + }, ); } - -export async function GET(request: NextRequest) { - if (!isAuthorized(request)) { - return NextResponse.json( - { error: "Unauthorized" }, - { - status: 401, - headers: { "Cache-Control": "no-store" }, - }, - ); - } - - const startedAt = Date.now(); - try { - const deployment = getOperationalOnchainDeployment("production"); - if (deployment.status !== "ready") { - throw new Error( - "The verified production release is not operationally eligible", - ); - } - let model: Awaited> | null = - null; - let lastReadError: unknown; - for (let attempt = 1; attempt <= INDEX_READ_ATTEMPTS; attempt += 1) { - try { - model = await readLiveExploreModel(deployment); - break; - } catch (error) { - lastReadError = error; - if (attempt < INDEX_READ_ATTEMPTS) { - console.warn("Programmable index read will retry", { - attempt, - errorName: - error instanceof Error ? error.name : "UnknownIndexError", - }); - } - } - } - if (!model) { - throw lastReadError ?? new Error("Index read failed"); - } - if (model.status !== "ready") { - throw new Error("The live Explore model is not ready"); - } - const [result, history] = await Promise.all([ - writeDurableExploreModel(deployment, model), - writePortfolioHistorySnapshot(model), - ]); - console.info("Programmable index refresh completed", { - blockNumber: result.blockNumber, - tokenCount: result.tokenCount, - updated: result.updated, - portfolioHistoryStatus: history.status, - portfolioHistoryPath: history.path, - deepReleaseVersion: result.deepReleaseVersion, - deepLifecycleEvidenceHash: - result.deepLifecycleEvidenceHash, - durationMs: Date.now() - startedAt, - }); - return NextResponse.json( - { - ok: true, - blockNumber: result.blockNumber, - tokenCount: result.tokenCount, - updated: result.updated, - portfolioHistory: { - status: history.status, - path: history.path, - tokenCount: history.tokenCount, - blockNumber: history.blockNumber, - }, - deepReleaseVersion: result.deepReleaseVersion, - deepLifecycleEvidenceHash: - result.deepLifecycleEvidenceHash, - }, - { headers: { "Cache-Control": "no-store" } }, - ); - } catch (error) { - console.error("Programmable index refresh failed", { - errorName: - error instanceof Error ? error.name : "UnknownIndexError", - durationMs: Date.now() - startedAt, - }); - return NextResponse.json( - { error: "Index refresh failed" }, - { - status: 503, - headers: { "Cache-Control": "no-store" }, - }, - ); - } -} diff --git a/app/api/ops/market-projector/route.ts b/app/api/ops/market-projector/route.ts new file mode 100644 index 00000000..cd17d143 --- /dev/null +++ b/app/api/ops/market-projector/route.ts @@ -0,0 +1,67 @@ +import { timingSafeEqual } from "node:crypto"; + +import { NextRequest, NextResponse } from "next/server"; + +import { + runConfiguredMarketProjectorCycle, + safeMarketProjectorError, +} from "../../../../lib/data-pipeline/market-projector-runtime.server"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 90; +export const runtime = "nodejs"; + +function isAuthorized(request: NextRequest) { + const secret = process.env.CRON_SECRET; + const authorization = request.headers.get("authorization"); + const secretLength = secret ? Buffer.byteLength(secret, "utf8") : 0; + if ( + !secret || + secretLength < 32 || + secretLength > 1_024 || + !authorization?.startsWith("Bearer ") + ) { + return false; + } + const provided = Buffer.from(authorization.slice(7), "utf8"); + const expected = Buffer.from(secret, "utf8"); + return ( + provided.length === expected.length && timingSafeEqual(provided, expected) + ); +} + +export async function GET(request: NextRequest) { + if (!isAuthorized(request)) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: { "Cache-Control": "no-store" } }, + ); + } + + const startedAt = Date.now(); + try { + const result = await runConfiguredMarketProjectorCycle(); + console.info("Programmable market projector completed", { + status: result.status, + releaseId: "releaseId" in result ? result.releaseId : undefined, + blockNumber: "blockNumber" in result ? result.blockNumber : undefined, + lagBlocks: result.lagBlocks, + closeCount: result.closeCount, + candleCount: result.candleCount, + caughtUp: result.caughtUp, + durationMs: Date.now() - startedAt, + }); + return NextResponse.json(result, { + headers: { "Cache-Control": "no-store" }, + }); + } catch (error) { + console.error("Programmable market projector failed", { + ...safeMarketProjectorError(error), + durationMs: Date.now() - startedAt, + }); + return NextResponse.json( + { error: "Market projection failed" }, + { status: 503, headers: { "Cache-Control": "no-store" } }, + ); + } +} diff --git a/app/api/ops/projector-wake/route.ts b/app/api/ops/projector-wake/route.ts new file mode 100644 index 00000000..52d1fe38 --- /dev/null +++ b/app/api/ops/projector-wake/route.ts @@ -0,0 +1,82 @@ +import { after, NextRequest, NextResponse } from "next/server"; + +import { + QuickNodeStreamWakeError, + verifyQuickNodeStreamWake, +} from "../../../../lib/data-pipeline/quicknode-stream-wake.server"; +import { runConfiguredProjectorCycle } from "../../../../lib/data-pipeline/projector-runtime-config.server"; +import { + runConfiguredMarketProjectorCycle, + safeMarketProjectorError, +} from "../../../../lib/data-pipeline/market-projector-runtime.server"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 180; +export const runtime = "nodejs"; + +const NO_STORE_HEADERS = Object.freeze({ "Cache-Control": "no-store" }); + +function resultStatus(value: unknown): string { + return value !== null && + typeof value === "object" && + "status" in value && + typeof value.status === "string" + ? value.status + : "unknown"; +} + +async function runWakeCycle() { + const startedAt = Date.now(); + try { + const source = await runConfiguredProjectorCycle(); + console.info("Programmable stream-woken source projector completed", { + status: resultStatus(source), + durationMs: Date.now() - startedAt, + }); + } catch { + console.error("Programmable stream-woken source projector failed", { + durationMs: Date.now() - startedAt, + }); + } + + const marketStartedAt = Date.now(); + try { + const market = await runConfiguredMarketProjectorCycle(); + console.info("Programmable stream-woken market projector completed", { + status: resultStatus(market), + durationMs: Date.now() - marketStartedAt, + }); + } catch (error) { + console.error("Programmable stream-woken market projector failed", { + ...safeMarketProjectorError(error), + durationMs: Date.now() - marketStartedAt, + }); + } +} + +export async function POST(request: NextRequest) { + try { + await verifyQuickNodeStreamWake(request); + } catch (error) { + const status = + error instanceof QuickNodeStreamWakeError ? error.status : 400; + console.warn("Programmable stream wake rejected", { status }); + return NextResponse.json( + { + error: + status === 503 + ? "Wake trigger unavailable" + : status === 401 + ? "Unauthorized" + : "Wake trigger rejected", + }, + { status, headers: NO_STORE_HEADERS }, + ); + } + + after(runWakeCycle); + return NextResponse.json( + { accepted: true }, + { status: 202, headers: NO_STORE_HEADERS }, + ); +} diff --git a/app/api/ops/projector/route.ts b/app/api/ops/projector/route.ts new file mode 100644 index 00000000..b0084ab5 --- /dev/null +++ b/app/api/ops/projector/route.ts @@ -0,0 +1,505 @@ +import { timingSafeEqual } from "node:crypto"; + +import { NextRequest, NextResponse } from "next/server"; + +import { + projectorRuntimeActivationState, + runConfiguredProjectorCycle, +} from "../../../../lib/data-pipeline/projector-runtime-config.server"; +import { + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + PROJECTOR_MAXIMUM_CANDIDATES_PER_CYCLE, + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE, + PROJECTOR_MAXIMUM_RUNTIME_ROUNDS, +} from "../../../../lib/data-pipeline/projector-runtime-limits"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 90; +export const runtime = "nodejs"; + +const NO_STORE_HEADERS = Object.freeze({ "Cache-Control": "no-store" }); +const CUTOVER_CANDIDATES_PER_COMMIT = 512; +const CUTOVER_MODE_HEADER = "raw-backfill-v1"; + +type SafeCheckpoint = Readonly<{ + status: + | "idle" + | "committed" + | "committed-empty" + | "staged-dynamic-parent" + | "failed"; + candidateCount?: number; + pageCount?: number; + snapshotBlock?: string; + generation?: string | null; + atomicGroupCount?: number; +}>; + +const RELEASE_IDS = Object.freeze([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const); + +type SafeProjection = Readonly<{ + releaseId: (typeof RELEASE_IDS)[number]; + status: "idle" | "committed" | "deferred" | "failed"; + projectedCandidateCount?: number; + ignoredCandidateCount?: number; + pageCount?: number; + checkpointGeneration?: string; + atomicGroupCount?: number; +}>; + +type SafeReadiness = Readonly<{ + status: "caught-up" | "progressed" | "incomplete"; + activationReady: boolean; + lagging: boolean; + terminalSweepComplete: boolean; + stoppedForDeadline: boolean; + completedRounds: number; + snapshotBlock: string | null; +}>; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function canonicalNonnegativeInteger(value: unknown): string | null { + if ( + typeof value !== "string" || + value.length > 78 || + !/^(?:0|[1-9]\d*)$/u.test(value) + ) { + return null; + } + return value; +} + +function safeCheckpoint(value: unknown): SafeCheckpoint { + if (!isRecord(value)) throw new Error("Invalid projector checkpoint"); + const { status, candidateCount, pageCount } = value; + if (status === "failed") return Object.freeze({ status }); + const snapshotBlock = canonicalNonnegativeInteger(value.snapshotBlock); + if ( + (status !== "idle" && + status !== "committed" && + status !== "committed-empty" && + status !== "staged-dynamic-parent") || + typeof candidateCount !== "number" || + !Number.isSafeInteger(candidateCount) || + candidateCount < 0 || + candidateCount > PROJECTOR_MAXIMUM_CANDIDATES_PER_CYCLE || + typeof pageCount !== "number" || + !Number.isSafeInteger(pageCount) || + pageCount < 1 || + pageCount > PROJECTOR_MAXIMUM_RUNTIME_ROUNDS || + snapshotBlock === null + ) { + throw new Error("Invalid projector checkpoint"); + } + const atomicGroupCount = value.atomicGroupCount === undefined + ? 0 + : typeof value.atomicGroupCount === "number" + ? value.atomicGroupCount + : Number.NaN; + const maximumCandidateCount = + atomicGroupCount * PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + + (pageCount - atomicGroupCount) * PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE; + if ( + !Number.isSafeInteger(atomicGroupCount) || + atomicGroupCount < 0 || + atomicGroupCount > 1 || + atomicGroupCount > pageCount || + candidateCount > maximumCandidateCount + ) { + throw new Error("Invalid projector checkpoint"); + } + if (status === "staged-dynamic-parent") { + if ( + candidateCount < 1 || + value.generation !== undefined || + atomicGroupCount !== 1 + ) { + throw new Error("Invalid projector checkpoint"); + } + return Object.freeze({ + status, + candidateCount, + pageCount, + snapshotBlock, + atomicGroupCount: 1, + }); + } + if (status === "idle") { + if (candidateCount !== 0 || atomicGroupCount !== 0) { + throw new Error("Invalid projector checkpoint"); + } + return Object.freeze({ + status, + candidateCount, + pageCount, + snapshotBlock, + generation: null, + }); + } + const generation = canonicalNonnegativeInteger(value.generation); + if ( + generation === null || + (status === "committed" && candidateCount === 0) || + (status === "committed-empty" && + (candidateCount !== 0 || atomicGroupCount !== 0)) + ) { + throw new Error("Invalid projector checkpoint"); + } + return Object.freeze({ + status, + candidateCount, + pageCount, + snapshotBlock, + generation, + ...(atomicGroupCount === 1 ? { atomicGroupCount } : {}), + }); +} + +function safeProjection(value: unknown, expectedReleaseId: string): SafeProjection { + if ( + !isRecord(value) || + value.releaseId !== expectedReleaseId || + !RELEASE_IDS.includes(value.releaseId as (typeof RELEASE_IDS)[number]) || + (value.status !== "idle" && + value.status !== "committed" && + value.status !== "deferred" && + value.status !== "failed") + ) { + throw new Error("Invalid release projection result"); + } + const releaseId = value.releaseId as (typeof RELEASE_IDS)[number]; + if (value.status === "failed") { + return Object.freeze({ releaseId, status: value.status }); + } + const pageCount = value.pageCount; + if (value.status === "deferred") { + if ( + pageCount !== 0 || + value.projectedCandidateCount !== undefined || + value.ignoredCandidateCount !== undefined || + value.checkpointGeneration !== undefined || + value.atomicGroupCount !== undefined + ) { + throw new Error("Invalid release projection result"); + } + return Object.freeze({ releaseId, status: value.status, pageCount }); + } + if ( + typeof pageCount !== "number" || + !Number.isSafeInteger(pageCount) || + pageCount < 1 || + pageCount > PROJECTOR_MAXIMUM_RUNTIME_ROUNDS + ) { + throw new Error("Invalid release projection result"); + } + if (value.status === "idle") { + return Object.freeze({ releaseId, status: value.status, pageCount }); + } + const projectedCandidateCount = value.projectedCandidateCount; + const ignoredCandidateCount = value.ignoredCandidateCount; + const checkpointGeneration = canonicalNonnegativeInteger( + value.checkpointGeneration, + ); + const atomicGroupCount = value.atomicGroupCount === undefined + ? 0 + : typeof value.atomicGroupCount === "number" + ? value.atomicGroupCount + : -1; + const pageCapacity = + atomicGroupCount * PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + + (pageCount - atomicGroupCount) * PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE; + if ( + typeof projectedCandidateCount !== "number" || + !Number.isSafeInteger(projectedCandidateCount) || + projectedCandidateCount < 0 || + projectedCandidateCount > PROJECTOR_MAXIMUM_CANDIDATES_PER_CYCLE || + typeof ignoredCandidateCount !== "number" || + !Number.isSafeInteger(ignoredCandidateCount) || + ignoredCandidateCount < 0 || + ignoredCandidateCount > PROJECTOR_MAXIMUM_CANDIDATES_PER_CYCLE || + projectedCandidateCount + ignoredCandidateCount < 1 || + projectedCandidateCount + ignoredCandidateCount > + PROJECTOR_MAXIMUM_CANDIDATES_PER_CYCLE || + projectedCandidateCount + ignoredCandidateCount > + pageCapacity || + !Number.isSafeInteger(atomicGroupCount) || + atomicGroupCount < 0 || + atomicGroupCount > 1 || + atomicGroupCount > pageCount || + checkpointGeneration === null + ) { + throw new Error("Invalid release projection result"); + } + return Object.freeze({ + releaseId, + status: "committed" as const, + projectedCandidateCount, + ignoredCandidateCount, + pageCount, + checkpointGeneration, + ...(atomicGroupCount === 1 ? { atomicGroupCount } : {}), + }); +} + +function safeReadiness( + value: unknown, + ingestion: SafeCheckpoint, + projections: readonly SafeProjection[], +): SafeReadiness { + if (!isRecord(value)) throw new Error("Invalid projector readiness"); + const status = value.status; + const activationReady = value.activationReady; + const lagging = value.lagging; + const terminalSweepComplete = value.terminalSweepComplete; + const stoppedForDeadline = value.stoppedForDeadline; + const completedRounds = value.completedRounds; + const snapshotBlock = value.snapshotBlock === null + ? null + : canonicalNonnegativeInteger(value.snapshotBlock); + const failed = + ingestion.status === "failed" || + projections.some(({ status: projectionStatus }) => + projectionStatus === "failed" + ); + const hasDeferredProjection = projections.some( + ({ status: projectionStatus }) => projectionStatus === "deferred", + ); + const progressed = + ingestion.status === "committed" || + ingestion.status === "committed-empty" || + ingestion.status === "staged-dynamic-parent" || + projections.some(({ status: projectionStatus }) => + projectionStatus === "committed" + ); + if ( + (status !== "caught-up" && + status !== "progressed" && + status !== "incomplete") || + typeof activationReady !== "boolean" || + typeof lagging !== "boolean" || + typeof terminalSweepComplete !== "boolean" || + typeof stoppedForDeadline !== "boolean" || + typeof completedRounds !== "number" || + !Number.isSafeInteger(completedRounds) || + completedRounds < 0 || + completedRounds > PROJECTOR_MAXIMUM_RUNTIME_ROUNDS || + (value.snapshotBlock !== null && snapshotBlock === null) || + (ingestion.status !== "failed" && + snapshotBlock !== ingestion.snapshotBlock) || + activationReady !== (status === "caught-up") || + lagging === activationReady || + terminalSweepComplete !== (status === "caught-up") || + (status === "caught-up" && + (failed || + stoppedForDeadline || + completedRounds < 1 || + ingestion.status === "staged-dynamic-parent" || + hasDeferredProjection)) || + (status === "progressed" && (!progressed || failed)) || + (status === "incomplete" && !failed && progressed) || + (ingestion.status === "staged-dynamic-parent" && + status !== "progressed") + ) { + throw new Error("Invalid projector readiness"); + } + return Object.freeze({ + status, + activationReady, + lagging, + terminalSweepComplete, + stoppedForDeadline, + completedRounds, + snapshotBlock, + }); +} + +function safeRuntimeResult( + value: unknown, + allowCutoverDeferred = false, +) { + if ( + isRecord(value) && + value.ok === true && + (value.status === "busy" || value.status === "disabled") && + Object.keys(value).length === 3 && + isRecord(value.readiness) && + value.readiness.status === value.status && + value.readiness.activationReady === false && + value.readiness.lagging === true && + Object.keys(value.readiness).length === 3 + ) { + return Object.freeze({ + ok: true as const, + status: value.status, + readiness: Object.freeze({ + status: value.status, + activationReady: false as const, + lagging: true as const, + }), + }); + } + if ( + !isRecord(value) || + typeof value.ok !== "boolean" || + !Array.isArray(value.projections) || + value.projections.length !== RELEASE_IDS.length + ) { + throw new Error("Invalid projector runtime result"); + } + const ingestion = safeCheckpoint(value.ingestion); + const projections = value.projections.map((projection, index) => + safeProjection(projection, RELEASE_IDS[index]!), + ); + if ( + projections.some(({ status }) => status === "deferred") && + ingestion.status !== "staged-dynamic-parent" && + !allowCutoverDeferred + ) { + throw new Error("Invalid projector runtime result"); + } + const readiness = safeReadiness(value.readiness, ingestion, projections); + const derivedOk = + ingestion.status !== "failed" && + projections.every(({ status }) => status !== "failed"); + if (value.ok !== derivedOk) { + throw new Error("Invalid projector runtime result"); + } + return Object.freeze({ + ok: derivedOk, + ingestion, + projections: Object.freeze(projections), + readiness, + }); +} + +function matchesBearer(request: NextRequest, secret: unknown): boolean { + const authorization = request.headers.get("authorization"); + if ( + typeof secret !== "string" || + Buffer.byteLength(secret, "utf8") < 32 || + Buffer.byteLength(secret, "utf8") > 1_024 || + !authorization?.startsWith("Bearer ") + ) { + return false; + } + const provided = Buffer.from(authorization.slice("Bearer ".length), "utf8"); + const expected = Buffer.from(secret, "utf8"); + return ( + provided.length === expected.length && + timingSafeEqual(provided, expected) + ); +} + +function authorizationMode( + request: NextRequest, +): "standard" | "cutover" | null { + const requestedMode = request.headers.get("x-programmable-cutover-mode"); + if (requestedMode !== null) { + return requestedMode === CUTOVER_MODE_HEADER && + process.env.PROGRAMMABLE_CUTOVER_BACKFILL_ACTIVE === "true" && + matchesBearer( + request, + process.env.PROGRAMMABLE_CUTOVER_OPERATOR_SECRET, + ) + ? "cutover" + : null; + } + return matchesBearer(request, process.env.CRON_SECRET) + ? "standard" + : null; +} + +export async function GET(request: NextRequest) { + const mode = authorizationMode(request); + if (mode === null) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: NO_STORE_HEADERS }, + ); + } + + const startedAt = Date.now(); + try { + if (projectorRuntimeActivationState() === "disabled") { + const disabled = Object.freeze({ + ok: true as const, + status: "disabled" as const, + readiness: Object.freeze({ + status: "disabled" as const, + activationReady: false as const, + lagging: true as const, + }), + }); + console.info("Programmable projector cycle completed", { + durationMs: Math.min(90_000, Math.max(0, Date.now() - startedAt)), + status: "disabled", + readiness: disabled.readiness, + }); + return NextResponse.json(disabled, { + status: 200, + headers: NO_STORE_HEADERS, + }); + } + const cycle = safeRuntimeResult( + await (mode === "cutover" + ? runConfiguredProjectorCycle({ + ingestionOnly: true, + preferredCandidatesPerCommit: CUTOVER_CANDIDATES_PER_COMMIT, + }) + : runConfiguredProjectorCycle()), + mode === "cutover", + ); + const durationMs = Math.min( + 90_000, + Math.max(0, Date.now() - startedAt), + ); + if ("status" in cycle) { + console.info("Programmable projector cycle completed", { + durationMs, + status: cycle.status, + readiness: cycle.readiness, + }); + } else { + console.info("Programmable projector cycle completed", { + durationMs, + ok: cycle.ok, + ingestion: { + status: cycle.ingestion.status, + candidateCount: cycle.ingestion.candidateCount ?? 0, + pageCount: cycle.ingestion.pageCount ?? 0, + }, + readiness: cycle.readiness, + projections: cycle.projections.map((projection) => ({ + releaseId: projection.releaseId, + status: projection.status, + candidateCount: + (projection.projectedCandidateCount ?? 0) + + (projection.ignoredCandidateCount ?? 0), + pageCount: projection.pageCount ?? 0, + })), + }); + } + return NextResponse.json( + cycle, + { status: cycle.ok ? 200 : 503, headers: NO_STORE_HEADERS }, + ); + } catch (error) { + console.error("Programmable projector cycle failed", { + errorName: error instanceof Error ? error.name : "UnknownProjectorError", + durationMs: Date.now() - startedAt, + }); + return NextResponse.json( + { error: "Projector cycle failed" }, + { status: 503, headers: NO_STORE_HEADERS }, + ); + } +} diff --git a/app/api/ops/read-model-performance-capture/route.ts b/app/api/ops/read-model-performance-capture/route.ts new file mode 100644 index 00000000..7527b2b1 --- /dev/null +++ b/app/api/ops/read-model-performance-capture/route.ts @@ -0,0 +1,179 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +import { NextRequest, NextResponse } from "next/server"; + +import { DataPipelineError } from "../../../../lib/data-pipeline/errors"; +import { captureReadModelPerformance } from "../../../../lib/data-pipeline/read-model-performance-capture.server"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 90; +export const runtime = "nodejs"; + +const MAXIMUM_BODY_BYTES = 4_096; +const RELEASE_PROFILE_ID = "read-model-release-v1"; +const RELEASE_RATE_LIMIT_MS = 30_000; +const RELEASE_REPLAY_TTL_MS = 60_000; +const releaseCaptures = new Map(); +const PRIVATE_NO_STORE = Object.freeze({ + "Cache-Control": "private, no-store", +}); + +function isValidProbeSecret(secret: unknown): secret is string { + if (typeof secret !== "string") return false; + const byteLength = Buffer.byteLength(secret, "utf8"); + return byteLength >= 32 && byteLength <= 1_024; +} + +function isAuthorized(request: NextRequest): boolean { + const secret = process.env.PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN; + const provided = request.headers.get( + "x-programmable-performance-probe-token", + ); + if ( + request.headers.get("x-programmable-performance-probe") !== "1" || + !isValidProbeSecret(secret) || + provided === null + ) { + return false; + } + const expectedBytes = Buffer.from(secret, "utf8"); + const providedBytes = Buffer.from(provided, "utf8"); + return ( + expectedBytes.length === providedBytes.length && + timingSafeEqual(expectedBytes, providedBytes) + ); +} + +function errorResponse(error: string, status: number) { + return NextResponse.json( + { error }, + { status, headers: PRIVATE_NO_STORE }, + ); +} + +function releaseCaptureAuthorization( + request: NextRequest, + rawBody: string, + body: unknown, +): "not-release" | "authorized" | "unauthorized" | "rate-limited" { + if ( + body === null || + typeof body !== "object" || + Array.isArray(body) || + Reflect.get(body, "profileId") !== RELEASE_PROFILE_ID + ) { + return "not-release"; + } + const secret = process.env.PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN; + const supplied = request.headers.get( + "x-programmable-release-capture-signature", + ); + if ( + !isValidProbeSecret(secret) || + typeof supplied !== "string" || + !/^v1=[0-9a-f]{64}$/u.test(supplied) + ) { + return "unauthorized"; + } + const expected = Buffer.from( + createHmac("sha256", secret).update(rawBody, "utf8").digest("hex"), + "hex", + ); + const provided = Buffer.from(supplied.slice(3), "hex"); + if ( + expected.length !== provided.length || + !timingSafeEqual(expected, provided) + ) { + return "unauthorized"; + } + const nonce = Reflect.get(body, "captureNonce"); + const deploymentId = Reflect.get(body, "vercelDeploymentId"); + if ( + typeof nonce !== "string" || + !/^0x[0-9a-f]{64}$/u.test(nonce) || + typeof deploymentId !== "string" || + !/^dpl_[A-Za-z0-9]{20,128}$/u.test(deploymentId) + ) { + return "unauthorized"; + } + const nowMs = Date.now(); + for (const [key, capturedAtMs] of releaseCaptures) { + if (nowMs - capturedAtMs > RELEASE_REPLAY_TTL_MS) { + releaseCaptures.delete(key); + } + } + if ( + releaseCaptures.has(`nonce:${nonce}`) || + nowMs - (releaseCaptures.get(`deployment:${deploymentId}`) ?? 0) < + RELEASE_RATE_LIMIT_MS + ) { + return "rate-limited"; + } + releaseCaptures.set(`nonce:${nonce}`, nowMs); + releaseCaptures.set(`deployment:${deploymentId}`, nowMs); + return "authorized"; +} + +export async function POST(request: NextRequest) { + if (!isAuthorized(request)) return errorResponse("Unauthorized", 401); + const contentType = request.headers.get("content-type") ?? ""; + if (!/^application\/json(?:\s*;|$)/iu.test(contentType)) { + return errorResponse("JSON body required", 415); + } + const declaredLength = Number(request.headers.get("content-length")); + if ( + Number.isFinite(declaredLength) && + declaredLength > MAXIMUM_BODY_BYTES + ) { + return errorResponse("Request body too large", 413); + } + + let rawBody: string; + try { + rawBody = await request.text(); + } catch { + return errorResponse("Invalid request body", 400); + } + if (Buffer.byteLength(rawBody, "utf8") > MAXIMUM_BODY_BYTES) { + return errorResponse("Request body too large", 413); + } + let body: unknown; + try { + body = JSON.parse(rawBody); + } catch { + return errorResponse("Invalid JSON", 400); + } + const releaseAuthorization = releaseCaptureAuthorization( + request, + rawBody, + body, + ); + if (releaseAuthorization === "unauthorized") { + return errorResponse("Unauthorized", 401); + } + if (releaseAuthorization === "rate-limited") { + const response = errorResponse("Release capture rate limited", 429); + response.headers.set("Retry-After", "30"); + return response; + } + + const startedAt = Date.now(); + try { + const capture = await captureReadModelPerformance(body); + return NextResponse.json(capture, { headers: PRIVATE_NO_STORE }); + } catch (error) { + if ( + error instanceof DataPipelineError && + error.code === "invalid_input" && + error.safeMetadata?.operation === "performance-capture-request" + ) { + return errorResponse("Invalid capture request", 400); + } + console.error("Programmable performance capture failed", { + errorName: + error instanceof Error ? error.name : "UnknownPerformanceCaptureError", + durationMs: Date.now() - startedAt, + }); + return errorResponse("Performance capture unavailable", 503); + } +} diff --git a/app/api/ops/reconcile-preparity/route.ts b/app/api/ops/reconcile-preparity/route.ts new file mode 100644 index 00000000..64b9f5a4 --- /dev/null +++ b/app/api/ops/reconcile-preparity/route.ts @@ -0,0 +1,111 @@ +import { timingSafeEqual } from "node:crypto"; + +import { NextRequest, NextResponse } from "next/server"; + +import { DataPipelineError } from "../../../../lib/data-pipeline/errors"; +import { ReconcilerDatabaseError } from "../../../../lib/data-pipeline/postgres-reconciler"; +import { canonicalReconcilerCheckpointRequest } from "../../../../lib/data-pipeline/reconciler-preparity"; +import { runConfiguredReconcilerPreParity } from "../../../../lib/data-pipeline/reconciler-preparity.server"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 90; +export const runtime = "nodejs"; + +const MAXIMUM_REQUEST_BYTES = 16 * 1024; + +function isAuthorized(request: NextRequest): boolean { + const secret = process.env.CRON_SECRET; + const authorization = request.headers.get("authorization"); + const secretLength = typeof secret === "string" + ? Buffer.byteLength(secret, "utf8") + : 0; + if ( + typeof secret !== "string" || + secretLength < 32 || + secretLength > 1_024 || + !authorization?.startsWith("Bearer ") + ) { + return false; + } + const provided = Buffer.from(authorization.slice(7), "utf8"); + const expected = Buffer.from(secret, "utf8"); + return ( + provided.length === expected.length && timingSafeEqual(provided, expected) + ); +} + +function noStore(status: number) { + return { + status, + headers: { "Cache-Control": "no-store" }, + }; +} + +export async function POST(request: NextRequest) { + if (!isAuthorized(request)) { + return NextResponse.json({ error: "Unauthorized" }, noStore(401)); + } + + let checkpointRequest; + try { + const body = await request.text(); + if ( + body.length === 0 || + Buffer.byteLength(body, "utf8") > MAXIMUM_REQUEST_BYTES + ) { + return NextResponse.json( + { error: "Invalid checkpoint request" }, + noStore(400), + ); + } + checkpointRequest = canonicalReconcilerCheckpointRequest( + JSON.parse(body) as unknown, + ); + } catch { + return NextResponse.json( + { error: "Invalid checkpoint request" }, + noStore(400), + ); + } + + const startedAt = Date.now(); + try { + const result = await runConfiguredReconcilerPreParity({ + request: checkpointRequest, + }); + console.info("Programmable reconciliation completed", { + status: result.status, + routeCount: result.routeCount, + mismatchCount: result.mismatchCount, + checkpointBlockNumber: result.checkpointBlockNumber, + durationMs: Date.now() - startedAt, + }); + return NextResponse.json( + { + ok: result.status === "succeeded", + status: result.status, + routeCount: result.routeCount, + mismatchCount: result.mismatchCount, + checkpointId: result.checkpointId, + checkpointBlockNumber: result.checkpointBlockNumber, + checkpointBlockHash: result.checkpointBlockHash, + }, + noStore(result.status === "succeeded" ? 200 : 409), + ); + } catch (error) { + console.error("Programmable reconciliation failed", { + errorName: error instanceof Error ? error.name : "UnknownError", + ...(error instanceof DataPipelineError + ? { dependency: error.dependency, code: error.code } + : {}), + ...(error instanceof ReconcilerDatabaseError + ? { disposition: error.disposition, retryable: error.retryable } + : {}), + durationMs: Date.now() - startedAt, + }); + return NextResponse.json( + { error: "Reconciliation unavailable" }, + noStore(503), + ); + } +} diff --git a/app/api/profile/classic-v3/route.ts b/app/api/profile/classic-v3/route.ts index 30c69e04..4575c818 100644 --- a/app/api/profile/classic-v3/route.ts +++ b/app/api/profile/classic-v3/route.ts @@ -25,8 +25,21 @@ import { getConfiguredClassicV3Release, isClassicV3ReleaseVerified, } from "@/lib/classic-v3-release"; +import { + ActionLookupError, + lookupActionReward, + type ActionRewardLookup, +} from "@/lib/data-pipeline/action-lookup"; +import { indexedLaunchLookupEnabled } from "@/lib/data-pipeline/route-activation.server"; import { uerc20ReadAbi } from "@/lib/onchain/abis"; import { encodeClassicV3RewardAction } from "@/lib/profile/classic-v3-rewards"; +import { classicV3ActionRpcProviders } from "@/lib/server/action-rpc-quorum.server"; +import { + CLASSIC_V3_ROUTE_SCOPE, + coordinatePublicRouteRead, + PUBLIC_INDEXED_ROUTE_READS, + preparePublicRouteRequest, +} from "@/lib/data-pipeline/public-route-readiness.server"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; @@ -34,6 +47,17 @@ export const runtime = "nodejs"; const MAX_REQUEST_BYTES = 2_048; const LOG_RANGE = 10_000n; const CONFIRMATIONS = 12n; +type ClassicActionRewardIdentity = Readonly<{ + vaultAddress: Address; + poolId: Hex; + buySwapFeeBps: number; + sellSwapFeeBps: number; + buyCreatorFeeBps: number | null; + sellCreatorFeeBps: number | null; + launcherFeeBps: number; + transferTaxBps: number; + lpFeePips: number; +}>; const classicV3LaunchEvent = parseAbiItem( "event MemeTokenLaunchedV2(address indexed deployer,address indexed token,bytes32 indexed poolId,address feeHook,address rewardVault,address positionRecipient,uint256 positionTokenId,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,bytes32 rewardConfigurationHash,bytes32 launchHash)", ); @@ -68,6 +92,56 @@ function createClient() { }); } +function classicActionRpcEndpoints() { + return classicV3ActionRpcProviders(environment); +} + +function createActionClients() { + return classicActionRpcEndpoints().map((provider) => + createPublicClient({ + chain, + batch: { multicall: true }, + transport: http(provider.endpoint, { retryCount: 1, timeout: 12_000 }), + }), + ); +} + +async function sharedActionBlock(clients: readonly PublicClient[]) { + if (clients.length !== 2) { + throw new Error("Classic actions require two independent RPCs"); + } + const heads = await Promise.all(clients.map((client) => client.getBlockNumber())); + const blockNumber = heads[0]! < heads[1]! ? heads[0]! : heads[1]!; + const blocks = await Promise.all( + clients.map((client) => client.getBlock({ blockNumber })), + ); + if ( + !blocks[0]?.hash || + !blocks[1]?.hash || + blocks[0].hash.toLowerCase() !== blocks[1].hash.toLowerCase() + ) { + throw new Error("Independent RPCs disagree on Classic action state"); + } + return blockNumber; +} + +async function assertCodeHashAtBlock( + client: PublicClient, + address: Address, + expectedHash: string, + blockNumber: bigint, + label: string, +) { + const code = await client.getCode({ address, blockNumber }); + if ( + !code || + code === "0x" || + keccak256(code).toLowerCase() !== expectedHash.toLowerCase() + ) { + throw new Error(`${label} does not match the Classic manifest`); + } +} + async function assertCodeHash( client: PublicClient, address: Address, @@ -136,6 +210,210 @@ function prospectiveAllocation( return accountAmount; } +async function readClassicActionState(input: { + client: PublicClient; + reward: ClassicActionRewardIdentity; + account: Address; + blockNumber: bigint; + allocationIndex?: number; +}) { + const { client, reward, account, blockNumber, allocationIndex } = input; + const hook = getAddress(manifest.ethCreatorFeeHookV3 as string); + const launcher = getAddress(manifest.memeLaunchV2 as string); + const vaultFactory = getAddress( + manifest.classicRewardVaultFactoryV1 as string, + ); + await Promise.all([ + assertCodeHashAtBlock( + client, + launcher, + manifest.runtimeCodeHashes?.memeLaunchV2 as string, + blockNumber, + "Classic launcher", + ), + assertCodeHashAtBlock( + client, + hook, + manifest.runtimeCodeHashes?.ethCreatorFeeHookV3 as string, + blockNumber, + "Classic hook", + ), + assertCodeHashAtBlock( + client, + vaultFactory, + manifest.runtimeCodeHashes?.classicRewardVaultFactoryV1 as string, + blockNumber, + "Classic reward factory", + ), + ]); + const [ + vaultCode, + factoryVault, + vaultHook, + vaultPoolId, + beneficiaryCount, + shareBps, + checkpointed, + claimed, + disclosure, + poolConfig, + ] = await Promise.all([ + client.getCode({ address: reward.vaultAddress, blockNumber }), + client.readContract({ + address: vaultFactory, + abi: classicRewardVaultFactoryAbi, + functionName: "isFactoryVault", + args: [reward.vaultAddress], + blockNumber, + }), + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "feeHook", + blockNumber, + }), + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "poolId", + blockNumber, + }), + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "beneficiaryCount", + blockNumber, + }), + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "shareBpsOf", + args: [account], + blockNumber, + }), + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "claimable", + args: [account], + blockNumber, + }), + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "claimedBy", + args: [account], + blockNumber, + }), + client.readContract({ + address: hook, + abi: classicV3HookAbi, + functionName: "feeDisclosure", + args: [reward.poolId], + blockNumber, + }), + client.readContract({ + address: hook, + abi: classicV3HookAbi, + functionName: "poolFeeConfig", + args: [reward.poolId], + blockNumber, + }), + ]); + if ( + !vaultCode || + vaultCode === "0x" || + !factoryVault || + getAddress(vaultHook).toLowerCase() !== hook.toLowerCase() || + vaultPoolId.toLowerCase() !== reward.poolId.toLowerCase() || + beneficiaryCount < 1n || + beneficiaryCount > 5n || + disclosure[7].toLowerCase() !== reward.vaultAddress.toLowerCase() || + poolConfig[0].toLowerCase() !== reward.vaultAddress.toLowerCase() || + getAddress(poolConfig[1]).toLowerCase() !== launcher.toLowerCase() || + !poolConfig[4] || + Number(disclosure[0]) !== reward.buySwapFeeBps || + Number(disclosure[1]) !== reward.sellSwapFeeBps || + (reward.buyCreatorFeeBps !== null && + Number(disclosure[2]) !== reward.buyCreatorFeeBps) || + (reward.sellCreatorFeeBps !== null && + Number(disclosure[3]) !== reward.sellCreatorFeeBps) || + Number(disclosure[2]) + Number(disclosure[4]) !== + Number(disclosure[0]) || + Number(disclosure[3]) + Number(disclosure[4]) !== + Number(disclosure[1]) || + Number(disclosure[4]) !== reward.launcherFeeBps || + Number(disclosure[5]) !== reward.transferTaxBps || + Number(disclosure[6]) !== reward.lpFeePips || + Number(poolConfig[2]) !== reward.buySwapFeeBps || + Number(poolConfig[3]) !== reward.sellSwapFeeBps + ) { + throw new Error("Classic reward provenance does not match the indexed launch"); + } + const allocations = await Promise.all( + Array.from({ length: Number(beneficiaryCount) }, async (_, index) => { + const [beneficiary, allocationShare] = await Promise.all([ + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "beneficiaryAt", + args: [BigInt(index)], + blockNumber, + }), + client.readContract({ + address: reward.vaultAddress, + abi: classicRewardVaultAbi, + functionName: "shareBpsAt", + args: [BigInt(index)], + blockNumber, + }), + ]); + return { + allocationIndex: index, + payoutAddress: getAddress(beneficiary), + shareBps: Number(allocationShare), + }; + }), + ); + if ( + new Set(allocations.map((item) => item.payoutAddress.toLowerCase())).size !== + allocations.length || + allocations.some((item) => item.shareBps <= 0) || + allocations.reduce((sum, item) => sum + item.shareBps, 0) !== 10_000 || + allocations + .filter( + (item) => item.payoutAddress.toLowerCase() === account.toLowerCase(), + ) + .reduce((sum, item) => sum + item.shareBps, 0) !== Number(shareBps) + ) { + throw new Error("Classic reward allocation is invalid"); + } + const prospective = prospectiveAllocation( + poolConfig[5], + allocations, + account, + ); + const claimable = checkpointed + prospective; + const ownsAllocation = + allocationIndex === undefined + ? false + : allocations.some( + (item) => + item.allocationIndex === allocationIndex && + item.payoutAddress.toLowerCase() === account.toLowerCase(), + ); + return { + claimableWei: claimable.toString(), + claimedWei: claimed.toString(), + shareBps: Number(shareBps), + ownsAllocation, + allocations: allocations.map((item) => ({ + ...item, + payoutAddress: item.payoutAddress.toLowerCase(), + })), + }; +} + async function readRewards(account: Address) { if (!isClassicV3ReleaseVerified(manifest, releaseManifest, chain.id)) { return { @@ -415,21 +693,78 @@ async function readLaunchByTransaction(account: Address, transactionHash: Hex) { } export async function GET(request: NextRequest) { - const input = request.nextUrl.searchParams.get("account")?.trim(); + const routeRequest = await preparePublicRouteRequest( + request.nextUrl.searchParams, + request.headers, + request.nextUrl.searchParams.get("launch")?.trim() + ? "launch-lookup" + : "classic-v3-profile", + ); + if (routeRequest.probeFailure) return routeRequest.probeFailure; + const search = routeRequest.searchParams; + if ( + [...search.keys()].some( + (key) => key !== "account" && key !== "launch", + ) || + search.getAll("account").length !== 1 || + search.getAll("launch").length > 1 + ) { + return json({ error: "Unsupported query parameters" }, 400); + } + const input = search.get("account")?.trim(); if (!input || !isAddress(input)) { return json({ error: "Enter a valid Ethereum account address" }, 400); } try { - const launch = request.nextUrl.searchParams.get("launch")?.trim(); + const launch = search.get("launch")?.trim(); if (launch) { if (!isHex(launch, { strict: true }) || launch.length !== 66) { return json({ error: "Enter a valid launch transaction hash" }, 400); } - return json( - await readLaunchByTransaction(getAddress(input), launch as Hex), - ); + return await coordinatePublicRouteRead({ + route: "launch-lookup", + scope: CLASSIC_V3_ROUTE_SCOPE, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (transaction) => + PUBLIC_INDEXED_ROUTE_READS.launchLookup(transaction, { + chainId: 1, + surface: "classic-v3", + account: getAddress(input), + transactionHash: launch, + }), + async legacy() { + return { + source: "rpc" as const, + response: json( + await readLaunchByTransaction( + getAddress(input), + launch as Hex, + ), + ), + }; + }, + }); } - return json(await readRewards(getAddress(input))); + return await coordinatePublicRouteRead({ + route: "classic-v3-profile", + scope: CLASSIC_V3_ROUTE_SCOPE, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (transaction) => + PUBLIC_INDEXED_ROUTE_READS.classicV3Profile(transaction, { + chainId: 1, + account: getAddress(input), + }), + async legacy() { + return { + source: "rpc" as const, + response: json(await readRewards(getAddress(input))), + }; + }, + }); } catch (error) { console.error("Classic profile read failed", error); return json({ error: "Classic rewards are temporarily unavailable" }, 503); @@ -487,32 +822,94 @@ export async function POST(request: NextRequest) { } try { - const profile = await readRewards(account); - if (profile.status !== "ready") { + if (!isClassicV3ReleaseVerified(manifest, releaseManifest, chain.id)) { return json({ error: "Classic is not deployed yet" }, 409); } - const reward = profile.rewards.find( - (item) => - item.vaultAddress.toLowerCase() === vaultAddress.toLowerCase(), - ); - if (!reward) { - return json( - { error: "Only a current or historic payout wallet can continue" }, - 403, + let reward: ClassicActionRewardIdentity; + if (indexedLaunchLookupEnabled()) { + const indexedReward: ActionRewardLookup = await lookupActionReward({ + chainId: chain.id, + account, + vaultAddress, + }); + if ( + indexedReward.releaseVersion !== "classic-v3" || + indexedReward.modelVersion !== "classic" || + indexedReward.token.rewardVaultAddress?.toLowerCase() !== + vaultAddress.toLowerCase() || + indexedReward.hookAddress.toLowerCase() !== + getAddress(manifest.ethCreatorFeeHookV3 as string).toLowerCase() + ) { + return json( + { error: "Only a current or historic payout wallet can continue" }, + 403, + ); + } + reward = { + vaultAddress: indexedReward.vaultAddress, + poolId: indexedReward.poolId, + buySwapFeeBps: indexedReward.token.buySwapFeeBps, + sellSwapFeeBps: indexedReward.token.sellSwapFeeBps, + buyCreatorFeeBps: indexedReward.token.buyCreatorFeeBps, + sellCreatorFeeBps: indexedReward.token.sellCreatorFeeBps, + launcherFeeBps: indexedReward.token.launcherFeeBps, + transferTaxBps: indexedReward.token.transferTaxBps, + lpFeePips: indexedReward.token.lpFeePips, + }; + } else { + const profile = await readRewards(account); + if (profile.status !== "ready") { + return json({ error: "Classic is not deployed yet" }, 409); + } + const legacyReward = profile.rewards.find( + (candidate) => + candidate.vaultAddress.toLowerCase() === vaultAddress.toLowerCase(), ); + if (!legacyReward) { + return json( + { error: "Only a current or historic payout wallet can continue" }, + 403, + ); + } + reward = { + vaultAddress: legacyReward.vaultAddress, + poolId: legacyReward.poolId, + buySwapFeeBps: legacyReward.buySwapFeeBps, + sellSwapFeeBps: legacyReward.sellSwapFeeBps, + buyCreatorFeeBps: null, + sellCreatorFeeBps: null, + launcherFeeBps: legacyReward.platformFeeBps, + transferTaxBps: 0, + lpFeePips: 0, + }; + } + const clients = createActionClients(); + const blockNumber = await sharedActionBlock(clients); + const actionStates = await Promise.all( + clients.map((client) => + readClassicActionState({ + client, + reward, + account, + blockNumber, + allocationIndex, + }), + ), + ); + if (JSON.stringify(actionStates[0]) !== JSON.stringify(actionStates[1])) { + throw new Error("Independent RPCs disagree on Classic reward state"); } + const actionState = actionStates[0]!; if ( input.action === "update-payout" && - !reward.ownedAllocations.some( - (item) => item.allocationIndex === allocationIndex, - ) + !actionState.ownsAllocation ) { return json( { error: "Only the current owner of this reward allocation can change it" }, 403, ); } - if (input.action === "claim" && BigInt(reward.claimableWei) === 0n) { + if (input.action === "claim" && BigInt(actionState.claimableWei) === 0n) { return json({ error: "There are no rewards to claim" }, 409); } const data = encodeClassicV3RewardAction({ @@ -520,23 +917,40 @@ export async function POST(request: NextRequest) { allocationIndex, newPayoutAddress, }); - const client = createClient(); - await client.call({ - account, - to: vaultAddress, - data, - value: 0n, - }); - const [estimatedGas, gasPrice, balance] = await Promise.all([ - client.estimateGas({ - account, - to: vaultAddress, - data, - value: 0n, + const simulations = await Promise.all( + clients.map(async (client) => { + const transaction = { + account, + to: vaultAddress, + data, + value: 0n, + }; + await client.call(transaction); + const [estimatedGas, gasPrice, balance] = await Promise.all([ + client.estimateGas(transaction), + client.getGasPrice(), + client.getBalance({ address: account }), + ]); + return { estimatedGas, gasPrice, balance }; }), - client.getGasPrice(), - client.getBalance({ address: account }), - ]); + ); + const estimatedGas = simulations.reduce( + (largest, candidate) => + candidate.estimatedGas > largest + ? candidate.estimatedGas + : largest, + 0n, + ); + const gasPrice = simulations.reduce( + (largest, candidate) => + candidate.gasPrice > largest ? candidate.gasPrice : largest, + 0n, + ); + const balance = simulations.reduce( + (smallest, candidate) => + candidate.balance < smallest ? candidate.balance : smallest, + simulations[0]!.balance, + ); const gasLimit = (estimatedGas * 120n + 99n) / 100n; if (balance < gasLimit * gasPrice) { return json( @@ -564,6 +978,12 @@ export async function POST(request: NextRequest) { }, }); } catch (error) { + if (error instanceof ActionLookupError && error.code === "not-found") { + return json( + { error: "Only a current or historic payout wallet can continue" }, + 403, + ); + } console.error("Classic reward preparation failed", error); return json( { error: "The reward action could not be simulated from current onchain state" }, diff --git a/app/api/profile/deep/route.ts b/app/api/profile/deep/route.ts index 093deb12..be0a2098 100644 --- a/app/api/profile/deep/route.ts +++ b/app/api/profile/deep/route.ts @@ -1,1352 +1,24 @@ -import { NextRequest, NextResponse } from "next/server"; -import { - createPublicClient, - formatUnits, - getAddress, - http, - isAddress, - isHex, - keccak256, - type Address, - type Hex, - type PublicClient, -} from "viem"; -import { mainnet, sepolia } from "viem/chains"; - -import appDeployments from "@/contracts/config/app-deployments.v1.json"; -import { DEEP_GROWTH_TARGET_WEI, DEEP_TOKEN_RESERVE_WHOLE } from "@/lib/launch"; -import { - deepAutomationReadAbi, - deepGrowthVaultFactoryReadAbi, - deepGrowthVaultReadAbi, - deepHookReadAbi, - deepTokenLaunchedEvent, - DEEP_COMPLETION_TOLERANCE_WEI, - DEEP_MINIMUM_NATIVE_LIQUIDITY_FOR_COMPLETION_WEI, -} from "@/lib/deep-v1"; -import { - getVerifiedDeepRelease, - getVerifiedDeepV2Release, - type DeepLaunchModelRelease, - type LaunchModelReleaseManifest, -} from "@/lib/launch-model-gating"; -import { configuredMainnetDeepV3Manifest } from "@/lib/deep-v3-release"; -import { requireIndependentDeepV3RpcUrls } from "@/lib/deep-v3-runtime-binding"; -import { uerc20ReadAbi } from "@/lib/onchain/abis"; -import { getOperationalOnchainDeployment } from "@/lib/onchain/config"; -import { - resolveVerifiedDeepV3ReadRelease, -} from "@/lib/onchain/deep-v3-read-model"; -import { readDurableExploreModel } from "@/lib/onchain/durable-model"; -import { sanitizeImageUrl } from "@/lib/onchain/metadata"; -import { - authorizeDeepRewardVault, - deepCandidatesFromDurableTokens, - deepConfirmedTailScanStart, - deepFallbackScanStart, - paginateDeepCandidates, - requireDeepProviderAgreement, - resolveDeepProfileRpcUrls, - resolveDeepProfileSnapshot, - type DeepLaunchCandidate, - type DeepProfileSnapshot, - validateCanonicalDeepLaunchIdentities, - validateDeepCandidates, -} from "@/lib/profile/deep-profile-server"; -import { encodeDeepRewardAction } from "@/lib/profile/deep-rewards"; -import { - readDeepV3CreatorProfile, -} from "@/lib/profile/deep-v3-api.server"; -import type { DeepV3ProfileClient } from "@/lib/profile/deep-v3-profile.server"; -import { - deepV2IndexedTokensForAccount, - prepareIndexedDeepV2RewardAction, - readDeepV2ProfileRewards, -} from "@/lib/profile/deep-v2-api.server"; -import type { DeepV2ProfileClient } from "@/lib/profile/deep-v2-profile.server"; -import { safeServerErrorSummary } from "@/lib/server/safe-error"; +import { NextResponse } from "next/server"; export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -const MAX_REQUEST_BYTES = 2_048; -const LOG_RANGE = 10_000n; -const DEEP_TOKEN_RESERVE_RAW = BigInt(DEEP_TOKEN_RESERVE_WHOLE) * 10n ** 18n; - -const environment = - process.env.PROGRAMMABLE_ONCHAIN_NETWORK === "rehearsal" - ? "rehearsal" - : "production"; -const chain = environment === "rehearsal" ? sepolia : mainnet; -const deployment = appDeployments[ - environment -] as unknown as LaunchModelReleaseManifest; - -type VerifiedDeepRelease = DeepLaunchModelRelease & { - launcher: Address; - feeHook: Address; - growthVaultFactory: Address; - automation: Address; - deploymentBlock: number; - runtimeCodeHashes: { - launcher: Hex; - feeHook: Hex; - growthVaultFactory: Hex; - automation: Hex; - }; -}; - -function json(body: unknown, status = 200) { - return NextResponse.json(body, { - status, - headers: { "Cache-Control": "no-store" }, - }); -} - -function release(): VerifiedDeepRelease | null { - return getVerifiedDeepRelease( - deployment, - chain.id, - ) as VerifiedDeepRelease | null; -} - -function createClients() { - const rpcUrls = resolveDeepProfileRpcUrls(environment); - return rpcUrls.map((rpcUrl) => - createPublicClient({ - chain, - batch: { multicall: true }, - transport: http(rpcUrl, { retryCount: 1, timeout: 12_000 }), - }), - ); -} - -function asDeepV2Clients( - clients: readonly PublicClient[], -): readonly DeepV2ProfileClient[] { - return clients as unknown as readonly DeepV2ProfileClient[]; -} - -function createDeepV3Clients(): readonly DeepV3ProfileClient[] { - const endpoints = requireIndependentDeepV3RpcUrls( - process.env.ETHEREUM_RPC_URL, - process.env.ETHEREUM_RPC_URL_B, - ); - return endpoints.map((endpoint) => - createPublicClient({ - chain: mainnet, - batch: { multicall: true }, - transport: http(endpoint, { retryCount: 1, timeout: 12_000 }), - }), - ) as unknown as readonly DeepV3ProfileClient[]; -} - -async function readVerifiedDurableModel() { - const operational = getOperationalOnchainDeployment(environment); - if (operational.status !== "ready") { - throw new Error("The verified launch registry is unavailable"); - } - const durable = await readDurableExploreModel(operational); - if (durable.status !== "ready") { - throw new Error("The verified launch registry is unavailable"); - } - return durable.envelope.payload.model; -} - -async function assertCodeHash( - client: PublicClient, - address: Address, - expectedHash: Hex, - label: string, - snapshotBlock: bigint, -) { - const code = await client.getCode({ address, blockNumber: snapshotBlock }); - if (!code || code === "0x" || keccak256(code) !== expectedHash) { - throw new Error(`${label} does not match the Deep release`); - } -} - -function minimum(left: bigint, right: bigint) { - return left < right ? left : right; -} - -async function readLaunchLogs( - client: PublicClient, - verifiedRelease: VerifiedDeepRelease, - fromBlock: bigint, - toBlock: bigint, -) { - const logs = []; - for ( - let rangeStart = fromBlock; - rangeStart <= toBlock; - rangeStart += LOG_RANGE - ) { - logs.push( - ...(await client.getLogs({ - address: verifiedRelease.launcher, - event: deepTokenLaunchedEvent, - fromBlock: rangeStart, - toBlock: minimum(toBlock, rangeStart + LOG_RANGE - 1n), - strict: true, - })), - ); - } - return logs; -} - -async function confirmedSnapshot( - clients: readonly PublicClient[], - verifiedRelease: VerifiedDeepRelease, -) { - const snapshot = await resolveDeepProfileSnapshot(clients, chain.id); - await Promise.all( - clients.flatMap((client) => [ - assertCodeHash( - client, - verifiedRelease.launcher, - verifiedRelease.runtimeCodeHashes.launcher, - "Deep launcher", - snapshot.blockNumber, - ), - assertCodeHash( - client, - verifiedRelease.feeHook, - verifiedRelease.runtimeCodeHashes.feeHook, - "Deep hook", - snapshot.blockNumber, - ), - assertCodeHash( - client, - verifiedRelease.growthVaultFactory, - verifiedRelease.runtimeCodeHashes.growthVaultFactory, - "Deep growth vault factory", - snapshot.blockNumber, - ), - assertCodeHash( - client, - verifiedRelease.automation, - verifiedRelease.runtimeCodeHashes.automation, - "Deep automation", - snapshot.blockNumber, - ), - ]), - ); - return snapshot; -} - -type DeepLaunchLog = Awaited>[number]; - -let launchCatalogCache: - | { - release: Address; - expiresAt: number; - snapshotBlock: bigint; - candidates: DeepLaunchCandidate[]; - } - | undefined; - -function launchLogFingerprint(log: DeepLaunchLog) { - return { - removed: log.removed, - blockNumber: log.blockNumber.toString(), - blockHash: log.blockHash, - transactionHash: log.transactionHash, - transactionIndex: log.transactionIndex, - logIndex: log.logIndex, - args: log.args, - }; -} - -async function assertCanonicalBlock( - clients: readonly PublicClient[], - blockNumber: bigint, - expectedHash?: Hex, -) { - const blocks = await Promise.all( - clients.map((client) => client.getBlock({ blockNumber })), - ); - const block = requireDeepProviderAgreement( - `Deep block ${blockNumber}`, - blocks.map((item) => ({ - number: item.number.toString(), - hash: item.hash, - })), - ); - if ( - !block.hash || - (expectedHash && block.hash.toLowerCase() !== expectedHash.toLowerCase()) - ) { - throw new Error("The Deep launch catalog is not canonical"); - } - return block.hash; -} - -async function readCanonicalLaunchLog( - clients: readonly PublicClient[], - verifiedRelease: VerifiedDeepRelease, - candidate: DeepLaunchCandidate, - snapshot: DeepProfileSnapshot, -) { - if (candidate.blockNumber > snapshot.blockNumber) { - throw new Error("The Deep launch is newer than the confirmed snapshot"); - } - const blockHash = await assertCanonicalBlock(clients, candidate.blockNumber); - const providerLogs = await Promise.all( - clients.map((client) => - client.getLogs({ - address: verifiedRelease.launcher, - event: deepTokenLaunchedEvent, - fromBlock: candidate.blockNumber, - toBlock: candidate.blockNumber, - strict: true, - }), - ), - ); - const matches = providerLogs.map((logs) => - logs.filter( - (log) => - !log.removed && - log.blockHash?.toLowerCase() === blockHash.toLowerCase() && - log.transactionHash.toLowerCase() === - candidate.transactionHash.toLowerCase() && - getAddress(log.args.token) === candidate.tokenAddress && - getAddress(log.args.growthVault) === candidate.vaultAddress, - ), - ); - if (matches.some((logs) => logs.length !== 1)) { - throw new Error("The canonical Deep launch event is missing"); - } - requireDeepProviderAgreement( - "canonical Deep launch provenance", - matches.map((logs) => launchLogFingerprint(logs[0])), - ); - validateCanonicalDeepLaunchIdentities( - candidate, - snapshot.blockNumber, - matches.map((logs) => ({ - tokenAddress: getAddress(logs[0].args.token), - vaultAddress: getAddress(logs[0].args.growthVault), - blockNumber: logs[0].blockNumber, - blockHash: logs[0].blockHash as Hex, - transactionHash: logs[0].transactionHash, - removed: logs[0].removed, - })), - ); - return matches[0][0]; -} - -async function readLaunchCatalog( - clients: readonly PublicClient[], - verifiedRelease: VerifiedDeepRelease, - snapshot: DeepProfileSnapshot, -) { - if ( - launchCatalogCache && - launchCatalogCache.release === verifiedRelease.launcher && - launchCatalogCache.expiresAt > Date.now() && - launchCatalogCache.snapshotBlock === snapshot.blockNumber - ) { - return launchCatalogCache.candidates; - } - - const operationalDeployment = getOperationalOnchainDeployment(environment); - const durable = - operationalDeployment.status === "ready" - ? await readDurableExploreModel(operationalDeployment) - : null; - let candidates: DeepLaunchCandidate[]; - if (durable?.status === "ready") { - const durableSnapshot = durable.envelope.payload.model.snapshot; - const durableBlock = BigInt(durableSnapshot.blockNumber); - if (durableBlock > snapshot.blockNumber) { - throw new Error("The durable Deep catalog is ahead of the snapshot"); - } - await assertCanonicalBlock( - clients, - durableBlock, - durableSnapshot.blockHash, - ); - const durableCandidates = deepCandidatesFromDurableTokens( - durable.envelope.payload.model.tokens, - durableBlock, - ); - const tailStart = deepConfirmedTailScanStart( - durableBlock, - snapshot.blockNumber, - ); - if (tailStart > snapshot.blockNumber) { - candidates = durableCandidates; - } else { - const providerTailLogs = await Promise.all( - clients.map((client) => - readLaunchLogs( - client, - verifiedRelease, - tailStart, - snapshot.blockNumber, - ), - ), - ); - requireDeepProviderAgreement( - "confirmed Deep launch tail", - providerTailLogs.map((logs) => - logs.filter((log) => !log.removed).map(launchLogFingerprint), - ), - ); - candidates = validateDeepCandidates([ - ...durableCandidates, - ...providerTailLogs[0] - .filter((log) => !log.removed) - .map((log) => ({ - tokenAddress: getAddress(log.args.token), - vaultAddress: getAddress(log.args.growthVault), - blockNumber: log.blockNumber, - transactionHash: log.transactionHash, - })), - ]); - } - } else { - const fromBlock = deepFallbackScanStart( - BigInt(verifiedRelease.deploymentBlock), - snapshot.blockNumber, - ); - if (fromBlock > snapshot.blockNumber) { - candidates = []; - } else { - const providerLogs = await Promise.all( - clients.map((client) => - readLaunchLogs( - client, - verifiedRelease, - fromBlock, - snapshot.blockNumber, - ), - ), - ); - requireDeepProviderAgreement( - "bounded Deep launch events", - providerLogs.map((logs) => - logs.filter((log) => !log.removed).map(launchLogFingerprint), - ), - ); - candidates = validateDeepCandidates( - providerLogs[0] - .filter((log) => !log.removed) - .map((log) => ({ - tokenAddress: getAddress(log.args.token), - vaultAddress: getAddress(log.args.growthVault), - blockNumber: log.blockNumber, - transactionHash: log.transactionHash, - })), - ); - } - } - - launchCatalogCache = { - release: verifiedRelease.launcher, - expiresAt: Date.now() + 30_000, - snapshotBlock: snapshot.blockNumber, - candidates, - }; - return candidates; -} - -async function readBeneficiaries( - client: PublicClient, - vaultAddress: Address, - beneficiaryCount: bigint, - snapshotBlock: bigint, -) { - return Promise.all( - Array.from({ length: Number(beneficiaryCount) }, async (_, index) => { - const beneficiary = await client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "beneficiaryAt", - args: [BigInt(index)], - blockNumber: snapshotBlock, - }); - const [shareBps, payoutAddress] = await Promise.all([ - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "shareBpsOf", - args: [beneficiary], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "payoutAddressOf", - args: [beneficiary], - blockNumber: snapshotBlock, - }), - ]); - return { - beneficiary: getAddress(beneficiary), - payoutAddress: getAddress(payoutAddress), - shareBps, - }; - }), - ); -} - -async function hydrateReward( - client: PublicClient, - verifiedRelease: VerifiedDeepRelease, - log: Awaited>[number], - account: Address, - snapshotBlock: bigint, -) { - const tokenAddress = getAddress(log.args.token); - const vaultAddress = getAddress(log.args.growthVault); - const poolId = log.args.poolId; - const [ - tokenName, - tokenSymbol, - metadata, - vaultHook, - vaultPoolManager, - vaultPoolId, - vaultToken, - vaultOracleGuard, - upstreamVault, - configurationHash, - shareBps, - payoutAddress, - claimed, - claimable, - beneficiaryCount, - growthTarget, - completionTolerance, - minimumNativeLiquidityForCompletion, - tokenReserve, - nativeAllocated, - nativeAdded, - pendingGrowth, - deferredRewardFees, - growthTargetReached, - oracleReady, - workState, - factoryVault, - feeDisclosure, - poolConfig, - automationAction, - ] = await Promise.all([ - client.readContract({ - address: tokenAddress, - abi: uerc20ReadAbi, - functionName: "name", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: tokenAddress, - abi: uerc20ReadAbi, - functionName: "symbol", - blockNumber: snapshotBlock, - }), - client - .readContract({ - address: tokenAddress, - abi: uerc20ReadAbi, - functionName: "metadata", - blockNumber: snapshotBlock, - }) - .catch(() => null), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "feeHook", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "poolManager", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "poolId", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "token", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "oracleGuard", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "upstreamVault", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "configurationHash", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "shareBpsOf", - args: [account], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "payoutAddressOf", - args: [account], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "claimedBy", - args: [account], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "claimable", - args: [account], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "beneficiaryCount", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "growthTargetNative", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "completionToleranceNative", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "minimumNativeLiquidityForCompletion", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "tokenReserveTarget", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "totalNativeAllocatedToGrowth", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "totalNativeAddedToLiquidity", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "pendingGrowthNative", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "deferredRewardFees", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "growthTargetReached", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "oracleReady", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "workState", - blockNumber: snapshotBlock, - }), - client.readContract({ - address: verifiedRelease.growthVaultFactory, - abi: deepGrowthVaultFactoryReadAbi, - functionName: "isFactoryVault", - args: [vaultAddress], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: verifiedRelease.feeHook, - abi: deepHookReadAbi, - functionName: "feeDisclosure", - args: [poolId], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: verifiedRelease.feeHook, - abi: deepHookReadAbi, - functionName: "poolFeeConfig", - args: [poolId], - blockNumber: snapshotBlock, - }), - client.readContract({ - address: verifiedRelease.automation, - abi: deepAutomationReadAbi, - functionName: "checkVault", - args: [vaultAddress], - blockNumber: snapshotBlock, - }), - ]); - - if ( - !factoryVault || - getAddress(vaultHook) !== verifiedRelease.feeHook || - getAddress(vaultPoolManager) === - "0x0000000000000000000000000000000000000000" || - vaultPoolId !== poolId || - getAddress(vaultToken) !== tokenAddress || - getAddress(vaultOracleGuard) !== getAddress(log.args.oracleGuard) || - getAddress(upstreamVault) !== getAddress(log.args.upstreamRewardVault) || - configurationHash !== log.args.vaultConfigurationHash || - growthTarget !== DEEP_GROWTH_TARGET_WEI || - completionTolerance !== DEEP_COMPLETION_TOLERANCE_WEI || - minimumNativeLiquidityForCompletion !== - DEEP_MINIMUM_NATIVE_LIQUIDITY_FOR_COMPLETION_WEI || - tokenReserve !== DEEP_TOKEN_RESERVE_RAW || - beneficiaryCount < 1n || - beneficiaryCount > 8n || - workState[2] !== pendingGrowth || - workState[0] > 2 || - automationAction > 3 || - feeDisclosure[0] !== log.args.buySwapFeeBps || - feeDisclosure[1] !== log.args.sellSwapFeeBps || - feeDisclosure[4] !== 10 || - feeDisclosure[5] !== 0 || - feeDisclosure[6] !== 0 || - getAddress(feeDisclosure[7]) !== getAddress(log.args.upstreamRewardVault) || - getAddress(poolConfig[0]) !== getAddress(log.args.upstreamRewardVault) || - getAddress(poolConfig[1]) !== verifiedRelease.launcher || - poolConfig[2] !== log.args.buySwapFeeBps || - poolConfig[3] !== log.args.sellSwapFeeBps || - !poolConfig[4] - ) { - throw new Error("Deep reward configuration is inconsistent"); - } - const beneficiaries = await readBeneficiaries( - client, - vaultAddress, - beneficiaryCount, - snapshotBlock, - ); - if ( - beneficiaries.reduce((sum, item) => sum + item.shareBps, 0) !== 10_000 || - beneficiaries.filter( - (item) => item.beneficiary.toLowerCase() === account.toLowerCase(), - ).length !== 1 - ) { - throw new Error("Deep reward split is inconsistent"); - } - - return { - model: "deep" as const, - deepReleaseVersion: "deep-full-range-v1" as const, - tokenAddress, - tokenName, - tokenSymbol, - ...(sanitizeImageUrl(metadata?.[2] ?? "") - ? { imageUrl: sanitizeImageUrl(metadata?.[2] ?? "") } - : {}), - poolId, - vaultAddress, - oracleGuardAddress: getAddress(log.args.oracleGuard), - upstreamRewardVaultAddress: getAddress(log.args.upstreamRewardVault), - beneficiary: account, - payoutAddress: getAddress(payoutAddress), - shareBps, - claimableWei: claimable.toString(), - claimableEth: formatUnits(claimable, 18), - claimedWei: claimed.toString(), - claimedEth: formatUnits(claimed, 18), - buySwapFeeBps: feeDisclosure[0], - sellSwapFeeBps: feeDisclosure[1], - platformFeeBps: 10 as const, - beneficiaries, - growthTargetWei: growthTarget.toString(), - growthTargetEth: formatUnits(growthTarget, 18), - completionToleranceWei: completionTolerance.toString(), - minimumNativeLiquidityForCompletionWei: - minimumNativeLiquidityForCompletion.toString(), - nativeAllocatedToGrowthWei: nativeAllocated.toString(), - nativeAllocatedToGrowthEth: formatUnits(nativeAllocated, 18), - nativeAddedToLiquidityWei: nativeAdded.toString(), - nativeAddedToLiquidityEth: formatUnits(nativeAdded, 18), - pendingGrowthNativeWei: pendingGrowth.toString(), - pendingGrowthNativeEth: formatUnits(pendingGrowth, 18), - deferredRewardFeesWei: deferredRewardFees.toString(), - deferredRewardFeesEth: formatUnits(deferredRewardFees, 18), - tokenReserveRaw: tokenReserve.toString(), - growthTargetReached, - oracleReady, - automationAction, - nextCompoundTimestamp: workState[3].toString(), - trustedNativeDepthWei: workState[4].toString(), - depthCapNativeWei: workState[5].toString(), - automationGuaranteed: false as const, - launchTransactionHash: log.transactionHash, - }; -} - -async function readV1Rewards(account: Address) { - const verifiedRelease = release(); - if (!verifiedRelease) { - return { - status: "not-deployed" as const, - account, - chainId: chain.id, - rewards: [], - }; - } - const clients = createClients(); - const snapshot = await confirmedSnapshot(clients, verifiedRelease); - const candidates = await readLaunchCatalog( - clients, - verifiedRelease, - snapshot, - ); - const rewards = []; - for (const page of paginateDeepCandidates(candidates)) { - const relevant = ( - await Promise.all( - page.map(async (candidate) => { - const shares = await Promise.all( - clients.map((client) => - client.readContract({ - address: candidate.vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "shareBpsOf", - args: [account], - blockNumber: snapshot.blockNumber, - }), - ), - ); - const share = requireDeepProviderAgreement( - "Deep beneficiary share", - shares, - ); - return share > 0n ? candidate : null; - }), - ) - ).filter((candidate) => candidate !== null); - for (const candidate of relevant) { - const log = await readCanonicalLaunchLog( - clients, - verifiedRelease, - candidate, - snapshot, - ); - const providerRewards = await Promise.all( - clients.map((client) => - hydrateReward( - client, - verifiedRelease, - log, - account, - snapshot.blockNumber, - ), - ), - ); - rewards.push( - requireDeepProviderAgreement("Deep reward accounting", providerRewards), - ); - } - } - - return { - status: "ready" as const, - account, - chainId: chain.id, - rewards: rewards.reverse(), - }; -} -async function readV1LaunchByTransaction( - account: Address, - transactionHash: Hex, -) { - const verifiedRelease = release(); - if (!verifiedRelease) { - return { status: "not-deployed" as const, launch: null }; - } - const clients = createClients(); - const snapshot = await confirmedSnapshot(clients, verifiedRelease); - const candidates = await readLaunchCatalog( - clients, - verifiedRelease, - snapshot, - ); - const candidate = candidates.find( - (item) => - item.transactionHash.toLowerCase() === transactionHash.toLowerCase(), - ); - if (!candidate) return { status: "ready" as const, launch: null }; - const launch = await readCanonicalLaunchLog( - clients, - verifiedRelease, - candidate, - snapshot, - ); - if (getAddress(launch.args.deployer) !== account) { - return { status: "ready" as const, launch: null }; - } - if (!launch) return { status: "ready" as const, launch: null }; - const tokenAddress = getAddress(launch.args.token); - const tokenDetails = requireDeepProviderAgreement( - "Deep launch token details", - await Promise.all( - clients.map(async (client) => ({ - name: await client.readContract({ - address: tokenAddress, - abi: uerc20ReadAbi, - functionName: "name", - blockNumber: snapshot.blockNumber, - }), - symbol: await client.readContract({ - address: tokenAddress, - abi: uerc20ReadAbi, - functionName: "symbol", - blockNumber: snapshot.blockNumber, - }), - })), - ), - ); - return { - status: "ready" as const, - launch: { - tokenAddress, - name: tokenDetails.name, - symbol: tokenDetails.symbol, - launchTransactionHash: launch.transactionHash, - deepReleaseVersion: "deep-full-range-v1" as const, +function closedDeepProfileResponse() { + return NextResponse.json( + { + code: "deep_profile_closed", + error: "The Deep profile endpoint is not available", }, - }; -} - -async function readRewards(account: Address) { - const v1 = await readV1Rewards(account); - const verifiedV2 = getVerifiedDeepV2Release(deployment, chain.id); - let v2Rewards: Awaited< - ReturnType - > = []; - if (verifiedV2) { - const model = await readVerifiedDurableModel(); - const clients = createClients(); - v2Rewards = await readDeepV2ProfileRewards({ - manifest: deployment, - chainId: chain.id, - account, - model, - clients: asDeepV2Clients(clients), - }); - } - if (v1.status === "not-deployed" && v2Rewards.length === 0) { - return v1; - } - return { - status: "ready" as const, - account, - chainId: chain.id, - rewards: [ - ...(v1.status === "ready" ? v1.rewards : []), - ...v2Rewards, - ], - }; -} - -async function readV3CreatorTokens(account: Address) { - const release = - environment === "production" - ? resolveVerifiedDeepV3ReadRelease( - configuredMainnetDeepV3Manifest, - 1, - ) - : null; - if (!release) { - return { - status: "not-deployed" as const, - account, - chainId: 1 as const, - tokens: [], - }; - } - const model = await readVerifiedDurableModel(); - return readDeepV3CreatorProfile({ - manifest: configuredMainnetDeepV3Manifest, - chainId: 1, - account, - model, - clients: createDeepV3Clients(), - }); -} - -async function readLaunchByTransaction( - account: Address, - transactionHash: Hex, -) { - if (getVerifiedDeepV2Release(deployment, chain.id)) { - const model = await readVerifiedDurableModel(); - const token = deepV2IndexedTokensForAccount( - model, - chain.id, - account, - ).find( - (candidate) => - candidate.deepV2Provenance?.transactionHash.toLowerCase() === - transactionHash.toLowerCase(), - ); - if (token) { - const clients = createClients(); - await readDeepV2ProfileRewards({ - manifest: deployment, - chainId: chain.id, - account, - model: { - ...model, - tokens: [token], - }, - clients: asDeepV2Clients(clients), - }); - return { - status: "ready" as const, - launch: { - tokenAddress: token.tokenAddress, - name: token.name, - symbol: token.symbol, - launchTransactionHash: transactionHash, - deepReleaseVersion: "deep-full-range-v2" as const, - }, - }; - } - } - return readV1LaunchByTransaction(account, transactionHash); + { + status: 410, + headers: { "Cache-Control": "no-store" }, + }, + ); } -export async function GET(request: NextRequest) { - const input = request.nextUrl.searchParams.get("account")?.trim(); - if (!input || !isAddress(input)) { - return json({ error: "Enter a valid Ethereum account address" }, 400); - } - try { - const launch = request.nextUrl.searchParams.get("launch")?.trim(); - const requestedRelease = request.nextUrl.searchParams - .get("deepReleaseVersion") - ?.trim(); - if ( - requestedRelease !== undefined && - requestedRelease !== "deep-full-range-v3" - ) { - return json({ error: "Unsupported Deep release version" }, 400); - } - if (requestedRelease === "deep-full-range-v3") { - if (launch) { - return json( - { error: "Choose either a profile or launch lookup" }, - 400, - ); - } - return json(await readV3CreatorTokens(getAddress(input))); - } - if (launch) { - if (!isHex(launch, { strict: true }) || launch.length !== 66) { - return json({ error: "Enter a valid launch transaction hash" }, 400); - } - return json( - await readLaunchByTransaction(getAddress(input), launch as Hex), - ); - } - return json(await readRewards(getAddress(input))); - } catch (error) { - console.error( - "Deep profile read failed", - safeServerErrorSummary(error), - ); - return json( - { error: "Deep profile data is temporarily unavailable" }, - 503, - ); - } +export function GET() { + return closedDeepProfileResponse(); } -export async function POST(request: NextRequest) { - const length = Number(request.headers.get("content-length") ?? "0"); - if (Number.isFinite(length) && length > MAX_REQUEST_BYTES) { - return json({ error: "The reward request is too large" }, 413); - } - let input: Record; - try { - const text = await request.text(); - if (text.length > MAX_REQUEST_BYTES) { - return json({ error: "The reward request is too large" }, 413); - } - const parsed = JSON.parse(text); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - throw new Error("invalid"); - } - input = parsed as Record; - } catch { - return json({ error: "Send a valid reward request" }, 400); - } - - if ( - (input.action !== "claim" && input.action !== "update-payout") || - typeof input.account !== "string" || - !isAddress(input.account) || - typeof input.vaultAddress !== "string" || - !isAddress(input.vaultAddress) || - (input.deepReleaseVersion !== "deep-full-range-v1" && - input.deepReleaseVersion !== "deep-full-range-v2") || - input.chainId !== chain.id - ) { - return json({ error: "The reward request is invalid" }, 400); - } - const account = getAddress(input.account); - const vaultAddress = getAddress(input.vaultAddress); - let newPayoutAddress: Address | undefined; - if (input.action === "update-payout") { - if ( - typeof input.newPayoutAddress !== "string" || - !isAddress(input.newPayoutAddress) - ) { - return json({ error: "Enter a valid payout address" }, 400); - } - newPayoutAddress = getAddress(input.newPayoutAddress); - } - - try { - if (input.deepReleaseVersion === "deep-full-range-v2") { - if (!getVerifiedDeepV2Release(deployment, chain.id)) { - return json( - { error: "Deep V2 is not enabled by a verified release" }, - 409, - ); - } - const model = await readVerifiedDurableModel(); - const clients = createClients(); - try { - return json( - await prepareIndexedDeepV2RewardAction({ - manifest: deployment, - chainId: chain.id, - account, - model, - clients: asDeepV2Clients(clients), - vaultAddress, - action: input.action, - newPayoutAddress, - }), - ); - } catch (error) { - const message = - error instanceof Error ? error.message : "Deep V2 action failed"; - if ( - message.includes("not an indexed Deep V2 reward") || - message.includes("does not belong") - ) { - return json( - { error: "Only this vault's creator can continue" }, - 403, - ); - } - if ( - message.includes("no Deep V2 rewards") || - message.includes("unchanged") - ) { - return json({ error: message }, 409); - } - throw error; - } - } - - const verifiedRelease = release(); - if (!verifiedRelease) { - return json({ error: "Deep is not deployed yet" }, 409); - } - const clients = createClients(); - const snapshot = await confirmedSnapshot(clients, verifiedRelease); - const candidates = await readLaunchCatalog( - clients, - verifiedRelease, - snapshot, - ); - const candidate = candidates.find( - (item) => item.vaultAddress.toLowerCase() === vaultAddress.toLowerCase(), - ); - if (!candidate) { - return json( - { error: "Only this vault's immutable beneficiary can continue" }, - 403, - ); - } - const shares = await Promise.all( - clients.map((client) => - client.readContract({ - address: vaultAddress, - abi: deepGrowthVaultReadAbi, - functionName: "shareBpsOf", - args: [account], - blockNumber: snapshot.blockNumber, - }), - ), - ); - let authorizedCandidate: DeepLaunchCandidate; - try { - authorizedCandidate = authorizeDeepRewardVault( - account, - vaultAddress, - candidate, - shares, - ); - } catch { - return json( - { error: "Only this vault's immutable beneficiary can continue" }, - 403, - ); - } - const launch = await readCanonicalLaunchLog( - clients, - verifiedRelease, - authorizedCandidate, - snapshot, - ); - const reward = requireDeepProviderAgreement( - "Deep reward action state", - await Promise.all( - clients.map((client) => - hydrateReward( - client, - verifiedRelease, - launch, - account, - snapshot.blockNumber, - ), - ), - ), - ); - if (input.action === "claim" && BigInt(reward.claimableWei) === 0n) { - return json({ error: "There are no rewards to claim" }, 409); - } - const data = encodeDeepRewardAction({ - action: input.action, - newPayoutAddress, - }); - const [simulations, estimatedGasValues, gasPrices, balances] = - await Promise.all([ - Promise.all( - clients.map((client) => - client.call({ - account, - to: vaultAddress, - data, - value: 0n, - blockNumber: snapshot.blockNumber, - }), - ), - ), - Promise.all( - clients.map((client) => - client.estimateGas({ - account, - to: vaultAddress, - data, - value: 0n, - blockNumber: snapshot.blockNumber, - }), - ), - ), - Promise.all(clients.map((client) => client.getGasPrice())), - Promise.all( - clients.map((client) => - client.getBalance({ - address: account, - blockNumber: snapshot.blockNumber, - }), - ), - ), - ]); - requireDeepProviderAgreement( - "Deep reward simulation", - simulations.map((simulation) => simulation.data ?? "0x"), - ); - const balance = requireDeepProviderAgreement( - "Deep beneficiary balance", - balances, - ); - const estimatedGas = - estimatedGasValues[0] > estimatedGasValues[1] - ? estimatedGasValues[0] - : estimatedGasValues[1]; - const gasPrice = gasPrices[0] > gasPrices[1] ? gasPrices[0] : gasPrices[1]; - const gasLimit = (estimatedGas * 120n + 99n) / 100n; - if (balance < gasLimit * gasPrice) { - return json( - { error: "This wallet needs more ETH for the network fee" }, - 409, - ); - } - return json({ - status: "ready", - action: input.action, - deepReleaseVersion: "deep-full-range-v1", - account, - vaultAddress, - transaction: { - kind: - input.action === "claim" - ? "claim-deep-rewards" - : "update-deep-payout", - chainId: chain.id, - from: account, - to: vaultAddress, - data, - value: "0", - gasLimit: gasLimit.toString(), - }, - }); - } catch (error) { - console.error("Deep reward preparation failed", error); - return json( - { - error: - "The reward action could not be simulated from current onchain state", - }, - 502, - ); - } +export function POST() { + return closedDeepProfileResponse(); } diff --git a/app/api/profile/stock-paired/route.ts b/app/api/profile/stock-paired/route.ts index c027f30b..37a135e5 100644 --- a/app/api/profile/stock-paired/route.ts +++ b/app/api/profile/stock-paired/route.ts @@ -17,8 +17,24 @@ import { mainnet } from "viem/chains"; import { getOnchainDeployment, readExploreModel, + type ExploreReadModel, } from "@/lib/onchain"; +import { + ActionLookupError, + actionTokenAsExploreModel, + lookupActionReward, + type ActionRewardLookup, +} from "@/lib/data-pipeline/action-lookup"; +import { indexedLaunchLookupEnabled } from "@/lib/data-pipeline/route-activation.server"; +import { + coordinatePublicRouteRead, + PUBLIC_INDEXED_ROUTE_READS, + preparePublicRouteRequest, + publicSnapshotCheckpoint, + STOCK_PAIRED_ROUTE_SCOPES, +} from "@/lib/data-pipeline/public-route-readiness.server"; import { safeServerErrorSummary } from "@/lib/server/safe-error"; +import { stockPairedActionRpcProviders } from "@/lib/server/action-rpc-quorum.server"; import { StockPairedClaimReceiptError, verifyStockPairedClaimReceipt, @@ -59,36 +75,15 @@ function json(body: unknown, status = 200) { } function rpcEndpoints() { - const primary = - process.env.ETHEREUM_RPC_URL ?? - "https://ethereum-rpc.publicnode.com"; - const secondary = - process.env.ETHEREUM_RPC_URL_B ?? - process.env.ETHEREUM_RPC_URL_SECONDARY ?? - (primary === "https://ethereum-rpc.publicnode.com" - ? "https://rpc.mevblocker.io" - : "https://ethereum-rpc.publicnode.com"); - const endpoints = Array.from( - new Set([ - primary, - secondary, - "https://ethereum-rpc.publicnode.com", - "https://rpc.mevblocker.io", - "https://eth.drpc.org", - ]), - ); - if (endpoints.length < 2) { - throw new Error("Stock-Paired rewards require two independent RPCs"); - } - return endpoints; + return stockPairedActionRpcProviders(); } function clients() { - return rpcEndpoints().map((endpoint) => + return rpcEndpoints().map((provider) => createPublicClient({ chain: mainnet, batch: { multicall: true }, - transport: http(endpoint, { retryCount: 2, timeout: 12_000 }), + transport: http(provider.endpoint, { retryCount: 2, timeout: 12_000 }), }), ); } @@ -510,6 +505,7 @@ async function readRewardsWithClients( chainId: 1 as const, rewards: [], }, + checkpoint: undefined, rpcClients: [] as PublicClient[], }; } @@ -586,21 +582,165 @@ async function readRewardsWithClients( snapshotBlock: snapshotBlock.toString(), rewards, }, + checkpoint: publicSnapshotCheckpoint(model.snapshot), rpcClients: verifiedClients, }; } -async function readRewards(account: Address, includeEstimates = true) { - return (await readRewardsWithClients(account, includeEstimates)).response; +async function sharedStockActionClients() { + const candidates = clients(); + const heads = await Promise.allSettled( + candidates.map((client) => client.getBlockNumber()), + ); + const available = heads.flatMap((result, index) => + result.status === "fulfilled" + ? [{ client: candidates[index]!, head: result.value }] + : [], + ); + if (available.length < 2) { + throw new Error("Stock-Paired actions require two independent RPCs"); + } + const blockNumber = available.reduce( + (minimum, candidate) => + candidate.head < minimum ? candidate.head : minimum, + available[0]!.head, + ); + const blocks = await Promise.allSettled( + available.map(async ({ client }) => ({ + client, + block: await client.getBlock({ blockNumber }), + })), + ); + const byHash = new Map(); + for (const result of blocks) { + if (result.status !== "fulfilled" || !result.value.block.hash) continue; + const hash = result.value.block.hash.toLowerCase(); + byHash.set(hash, [...(byHash.get(hash) ?? []), result.value.client]); + } + const agreed = [...byHash.values()] + .sort((left, right) => right.length - left.length)[0]; + if (!agreed || agreed.length < 2) { + throw new Error( + "Independent RPCs disagree on the Stock-Paired action block", + ); + } + return { blockNumber, rpcClients: agreed }; +} + +async function readStockActionReward( + account: Address, + token: LauncherToken, +): Promise<{ + reward: NonNullable>>; + rpcClients: PublicClient[]; +}> { + if ( + token.launchModel !== "stock-paired" || + !token.rewardVaultAddress || + !token.quoteAssetAddress + ) { + throw new Error("The Stock-Paired reward identity is invalid"); + } + const release = getConfiguredStockPairedReleaseByHookAndVersion( + token.hookAddress, + token.launchModelVersion, + ); + if (!release) { + throw new Error("The Stock-Paired release is not configured"); + } + const snapshot = await sharedStockActionClients(); + const results = await Promise.allSettled( + snapshot.rpcClients.map(async (client) => { + await Promise.all([ + assertRuntime( + client, + release.addresses.feeHook, + release.runtimeCodeHashes.feeHook, + snapshot.blockNumber, + `${release.internalContractRelease} hook`, + ), + assertRuntime( + client, + release.addresses.feeSplitVaultFactory, + release.runtimeCodeHashes.feeSplitVaultFactory, + snapshot.blockNumber, + `${release.internalContractRelease} reward-vault factory`, + ), + ]); + const reward = await readVaultReward( + client, + token, + account, + snapshot.blockNumber, + ); + if (!reward) { + throw new Error("This wallet is not a current reward beneficiary"); + } + return { client, reward }; + }), + ); + const verified = results.flatMap((result) => + result.status === "fulfilled" ? [result.value] : [], + ); + if (verified.length < 2) { + throw new Error( + "Two independent RPCs could not verify the Stock-Paired reward", + ); + } + const fingerprint = JSON.stringify(verified[0]!.reward); + if ( + verified.some( + (candidate) => JSON.stringify(candidate.reward) !== fingerprint, + ) + ) { + throw new Error("Independent RPCs disagree on Stock-Paired rewards"); + } + return { + reward: verified[0]!.reward, + rpcClients: verified.map((candidate) => candidate.client), + }; } export async function GET(request: NextRequest) { - const accountInput = request.nextUrl.searchParams.get("account")?.trim(); + const routeRequest = await preparePublicRouteRequest( + request.nextUrl.searchParams, + request.headers, + "creator-profile", + ); + if (routeRequest.probeFailure) return routeRequest.probeFailure; + const search = routeRequest.searchParams; + if ( + [...search.keys()].some((key) => key !== "account") || + search.getAll("account").length !== 1 + ) { + return json({ error: "Unsupported query parameters" }, 400); + } + const accountInput = search.get("account")?.trim(); if (!accountInput || !isAddress(accountInput)) { return json({ error: "Enter a valid Ethereum account address" }, 400); } try { - return json(await readRewards(getAddress(accountInput))); + const account = getAddress(accountInput); + return await coordinatePublicRouteRead({ + route: "creator-profile", + scope: STOCK_PAIRED_ROUTE_SCOPES, + ...(routeRequest.releaseProbe + ? { releaseProbe: routeRequest.releaseProbe } + : {}), + indexed: (transaction) => + PUBLIC_INDEXED_ROUTE_READS.stockPairedProfile(transaction, { + chainId: 1, + account, + }), + async legacy() { + const result = await readRewardsWithClients(account); + return { + source: "rpc" as const, + ...(result.checkpoint ? { checkpoint: result.checkpoint } : {}), + response: json(result.response), + }; + }, + }); } catch (error) { console.error( "Stock-Paired profile read failed", @@ -686,22 +826,47 @@ export async function POST(request: NextRequest) { try { const account = getAddress(input.account); const vaultAddress = getAddress(input.vaultAddress); - const { response: profile, rpcClients } = - await readRewardsWithClients(account, false); - if (profile.status !== "ready") { - return json({ error: "Stock-Paired rewards are not deployed" }, 409); + let registry: ExploreReadModel; + if (indexedLaunchLookupEnabled()) { + const indexedReward: ActionRewardLookup = await lookupActionReward({ + chainId: 1, + account, + vaultAddress, + }); + if ( + indexedReward.modelVersion !== "stock-paired" || + !indexedReward.releaseVersion.startsWith("stock-paired-") || + indexedReward.token.rewardVaultAddress?.toLowerCase() !== + vaultAddress.toLowerCase() || + !indexedReward.quoteAssetAddress + ) { + throw new Error("The indexed Stock-Paired reward identity is invalid"); + } + registry = actionTokenAsExploreModel(indexedReward.token); + } else { + registry = await readExploreModel( + getOnchainDeployment("production"), + ); + if (registry.status !== "ready") { + return json({ error: "Stock-Paired rewards are not deployed" }, 409); + } } - const reward = profile.rewards.find( + const token = registry.tokens.find( (candidate) => - candidate.vaultAddress.toLowerCase() === - vaultAddress.toLowerCase(), + candidate.launchModel === "stock-paired" && + candidate.rewardVaultAddress?.toLowerCase() === + vaultAddress.toLowerCase(), ); - if (!reward) { + if (!token) { return json( { error: "This wallet is not a beneficiary of that reward vault" }, 403, ); } + const { reward, rpcClients } = await readStockActionReward( + account, + token, + ); if (isClaim && BigInt(reward.claimableRaw) === 0n) { return json({ error: "No Stock-Paired rewards are claimable" }, 409); } @@ -732,9 +897,6 @@ export async function POST(request: NextRequest) { quoteAsset: reward.quoteAsset, minimumAmount: amountIn, }); - const registry = await readExploreModel( - getOnchainDeployment("production"), - ); const { deployment, verifiedToken } = resolveStockPairedTradeDeployment( 1, @@ -843,19 +1005,28 @@ export async function POST(request: NextRequest) { const simulations = await Promise.allSettled( rpcClients.map(async (client) => { await client.call(requestForRpc); - return client.estimateGas(requestForRpc); + const [estimatedGas, gasPrice, balance] = await Promise.all([ + client.estimateGas(requestForRpc), + client.getGasPrice(), + client.getBalance({ address: account }), + ]); + return { estimatedGas, gasPrice, balance }; }), ); const estimates = simulations.flatMap((result) => result.status === "fulfilled" ? [result.value] : [], ); - if (estimates.length === 0) { - throw new Error("No Ethereum RPC could prepare the reward transaction"); + if (estimates.length < 2) { + throw new Error( + "Two independent RPCs could not prepare the reward transaction", + ); } const gasLimit = ((estimates.reduce( (largest, candidate) => - candidate > largest ? candidate : largest, + candidate.estimatedGas > largest + ? candidate.estimatedGas + : largest, 0n, ) * 120n) + @@ -864,6 +1035,22 @@ export async function POST(request: NextRequest) { if (gasLimit <= 0n) { throw new Error("The reward transaction gas estimate is invalid"); } + const gasPrice = estimates.reduce( + (largest, candidate) => + candidate.gasPrice > largest ? candidate.gasPrice : largest, + 0n, + ); + const balance = estimates.reduce( + (smallest, candidate) => + candidate.balance < smallest ? candidate.balance : smallest, + estimates[0]!.balance, + ); + if (gasPrice <= 0n || balance < gasLimit * gasPrice) { + return json( + { error: "This wallet needs more ETH for the network fee" }, + 409, + ); + } return json({ status: "ready", account, @@ -883,6 +1070,12 @@ export async function POST(request: NextRequest) { }, }); } catch (error) { + if (error instanceof ActionLookupError && error.code === "not-found") { + return json( + { error: "This wallet is not a beneficiary of that reward vault" }, + 403, + ); + } if (error instanceof ClassicTradeInputError) { return json({ error: error.message }, 400); } diff --git a/app/api/trade/prepare/route.ts b/app/api/trade/prepare/route.ts index e9a0216b..b452010b 100644 --- a/app/api/trade/prepare/route.ts +++ b/app/api/trade/prepare/route.ts @@ -7,6 +7,12 @@ import { } from "viem"; import { mainnet, sepolia } from "viem/chains"; +import { + ActionLookupError, + actionTokenAsExploreModel, + lookupActionTokenByAddress, +} from "../../../../lib/data-pipeline/action-lookup"; +import { indexedLaunchLookupEnabled } from "../../../../lib/data-pipeline/route-activation.server"; import { getOnchainDeployment, readExploreModel, @@ -27,6 +33,7 @@ import { resolveStockPairedTradeDeployment, StockPairedTradeUnavailableError, } from "../../../../lib/trade/stock-paired"; +import { tradeActionRpcProviders } from "../../../../lib/server/action-rpc-quorum.server"; import { safeServerErrorSummary } from "../../../../lib/server/safe-error"; export const dynamic = "force-dynamic"; @@ -97,31 +104,6 @@ function runtimeClient( }; } -function tradeRpcEndpoints(chainId: number) { - if (chainId === 1) { - const primary = - process.env.ETHEREUM_RPC_URL ?? - "https://ethereum-rpc.publicnode.com"; - const secondary = - process.env.ETHEREUM_RPC_URL_B ?? - process.env.ETHEREUM_RPC_URL_SECONDARY ?? - (primary === "https://ethereum-rpc.publicnode.com" - ? "https://rpc.mevblocker.io" - : "https://ethereum-rpc.publicnode.com"); - return [primary, secondary] as const; - } - const primary = - process.env.SEPOLIA_RPC_URL ?? - "https://ethereum-sepolia-rpc.publicnode.com"; - const secondary = - process.env.SEPOLIA_RPC_URL_B ?? - process.env.SEPOLIA_RPC_URL_SECONDARY ?? - (primary === "https://ethereum-sepolia-rpc.publicnode.com" - ? "https://rpc.sepolia.org" - : "https://ethereum-sepolia-rpc.publicnode.com"); - return [primary, secondary] as const; -} - function selectConservativeTradeQuote { + throw new ClassicTradeUnavailableError( + `Classic trading is not supported on chain ${tradeRequest.chainId}`, + ); + })(); + const registry = indexedLaunchLookupEnabled() + ? actionTokenAsExploreModel( + await lookupActionTokenByAddress({ + chainId: actionChainId, + token: tradeRequest.token, + }), + ) + : await readExploreModel( + getOnchainDeployment( + tradeRequest.chainId === 1 ? "production" : "rehearsal", + ), + ); + const indexedToken = registry.tokens.find( + (candidate) => + candidate.tokenAddress.toLowerCase() === + tradeRequest.token.toLowerCase(), ); - const registry = await readExploreModel(registryDeployment); - const indexedToken = - registry.status === "ready" - ? registry.tokens.find( - (candidate) => - candidate.tokenAddress.toLowerCase() === - tradeRequest.token.toLowerCase(), - ) - : undefined; if (indexedToken?.launchModel === "stock-paired") { const { deployment } = resolveStockPairedTradeDeployment( tradeRequest.chainId, registry, tradeRequest.token, ); - const endpoints = tradeRpcEndpoints(tradeRequest.chainId); + const providers = tradeActionRpcProviders(tradeRequest.chainId); const preparations = await Promise.allSettled( - endpoints.map((endpoint) => + providers.map((provider) => prepareStockPairedTrade( - runtimeClient(tradeRequest.chainId, endpoint), + runtimeClient(tradeRequest.chainId, provider.endpoint), deployment, tradeRequest, ), @@ -224,11 +219,11 @@ export async function POST(request: NextRequest) { registry, tradeRequest.token, ); - const endpoints = tradeRpcEndpoints(tradeRequest.chainId); + const providers = tradeActionRpcProviders(tradeRequest.chainId); const preparations = await Promise.allSettled( - endpoints.map((endpoint) => + providers.map((provider) => prepareClassicTrade( - runtimeClient(tradeRequest.chainId, endpoint), + runtimeClient(tradeRequest.chainId, provider.endpoint), deployment, tradeRequest, registry, @@ -268,6 +263,12 @@ export async function POST(request: NextRequest) { ) { return json({ error: error.message }, 409); } + if (error instanceof ActionLookupError) { + return json( + { error: "This token is not a verified Programmable launch" }, + 409, + ); + } console.error( "Trade preparation failed", safeServerErrorSummary(error), diff --git a/app/docs/models/[model]/page.tsx b/app/docs/models/[model]/page.tsx index b571872d..80e82ff3 100644 --- a/app/docs/models/[model]/page.tsx +++ b/app/docs/models/[model]/page.tsx @@ -24,7 +24,7 @@ const modelMetadata: Record< "stock-paired": { title: "Stock-Paired", description: - "A restricted model whose Uniswap v4 pool uses a reviewed stock token as its quote asset.", + "Historical Uniswap v4 pools that use a reviewed stock token as their quote asset.", }, }; @@ -378,9 +378,9 @@ function StockPairedDocs() { return (
Product boundary @@ -392,18 +392,18 @@ function StockPairedDocs() { selected stock.

- General public access is not enabled. + New Stock-Paired launches are closed.

- The current interface exposes this model only to an approved - account. Its verified deployment does not make it a public launch - option. + Existing tokens remain in Explore. Their token pages, trading, + profile history and reward claims remain supported, and their + deployment records stay public.

- Launch flow -

ETH in, stock-token pool underneath

+ Historical launch design +

How the existing pools were created

  1. Choose a supported quote asset @@ -492,15 +492,14 @@ function StockPairedDocs() { underlying share.
  2. - New launches fail closed when the reviewed quote-token runtime no - longer matches. + New launch preparation is closed server-side.
Contracts -

Restricted Mainnet deployment

+

Historical Mainnet deployment

diff --git a/app/docs/page.tsx b/app/docs/page.tsx index 03730e5f..75735244 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -40,8 +40,9 @@ export default function DocsPage() {
Release status is part of the product.

- Classic and Stock-Paired are available for public launches on - Ethereum Mainnet. + Classic is available for new launches on Ethereum Mainnet. + Existing Stock-Paired tokens remain supported, but new + Stock-Paired launches are closed.

@@ -69,16 +70,14 @@ export default function DocsPage() { > Stock-Paired - - Live - + Historical

- Pairs a new token with a reviewed Ondo Global Markets asset - instead of ETH. + Existing tokens retain their recorded Ondo quote asset, pool, + trading route and creator rewards.

- Read Stock-Paired + Read Stock-Paired history diff --git a/app/launch/page.tsx b/app/launch/page.tsx index 796993ce..81e132c6 100644 --- a/app/launch/page.tsx +++ b/app/launch/page.tsx @@ -1,21 +1,5 @@ import { LaunchExperience } from "@/components/launch-entry"; -import { isStockPairedPublicLaunchEnabled } from "@/lib/stock-paired-access"; -import { getConfiguredStockPairedLaunchRelease } from "@/lib/stock-paired-release"; export default function LaunchPage() { - const launchEnvironment = - process.env.PROGRAMMABLE_ONCHAIN_NETWORK === "rehearsal" - ? "rehearsal" - : "production"; - const stockPairedPublicLaunchEnabled = - isStockPairedPublicLaunchEnabled( - launchEnvironment, - getConfiguredStockPairedLaunchRelease(), - ); - - return ( - - ); + return ; } diff --git a/components/docs-data.ts b/components/docs-data.ts index 0541115c..9f14986b 100644 --- a/components/docs-data.ts +++ b/components/docs-data.ts @@ -18,7 +18,10 @@ export const docsNavigation = [ label: "Launch models", items: [ { href: "/docs/models/classic", label: "Classic" }, - { href: "/docs/models/stock-paired", label: "Stock-Paired" }, + { + href: "/docs/models/stock-paired", + label: "Stock-Paired history", + }, ], }, { @@ -61,9 +64,9 @@ export const docsSearchItems: DocsSearchItem[] = [ href: "/docs/models/classic", }, { - title: "Stock-Paired", + title: "Stock-Paired history", description: - "A restricted model whose pool uses a reviewed stock token.", + "Historical pools, quote assets and support for existing tokens.", href: "/docs/models/stock-paired", }, { diff --git a/components/explore-view.tsx b/components/explore-view.tsx index 1af201d3..4ec031e8 100644 --- a/components/explore-view.tsx +++ b/components/explore-view.tsx @@ -19,6 +19,11 @@ import { } from "@/components/animated-market-cap"; import { ScrambleText } from "@/components/scramble-text"; import { SiteFooter } from "@/components/site-footer"; +import { + LIVE_DATA_REFRESH_INTERVAL_MS, + shouldRefreshLiveData, + useLiveDataRefresh, +} from "@/components/use-live-data-refresh"; import { WebsiteLinkIcon } from "@/components/website-link-icon"; import { canOptimizeTokenImage, @@ -96,7 +101,7 @@ type PaginationItem = number | "start-gap" | "end-gap"; const TOKENS_PER_PAGE = 10; const QUERY_DEBOUNCE_MS = 200; const EXPLORE_REQUEST_TIMEOUT_MS = 12_000; -export const EXPLORE_REFRESH_INTERVAL_MS = 10_000; +export const EXPLORE_REFRESH_INTERVAL_MS = LIVE_DATA_REFRESH_INTERVAL_MS; const PROGRAMMABLE_TOKEN_ADDRESS = "0x7987f03462200b3d8a072e02c89a8a41dcb124ee"; const fallbackTokenImages = [ "/brand/programmable-token-fallback-01-dawn.webp", @@ -118,10 +123,10 @@ export function shouldRefreshExplore(input: { lastRefreshAt: number; now: number; }) { - return ( - input.visibilityState === "visible" && - input.now - input.lastRefreshAt >= EXPLORE_REFRESH_INTERVAL_MS - ); + return shouldRefreshLiveData({ + ...input, + intervalMs: EXPLORE_REFRESH_INTERVAL_MS, + }); } function isRecord(value: unknown): value is Record { @@ -485,11 +490,10 @@ export function ExploreView() { const [copiedAddress, setCopiedAddress] = useState(""); const [copyError, setCopyError] = useState(""); const [retryKey, setRetryKey] = useState(0); - const [refreshKey, setRefreshKey] = useState(0); + const refreshKey = useLiveDataRefresh(); const [state, setState] = useState({ phase: "loading" }); const copyResetTimer = useRef(null); const activeExploreContentKey = useRef(null); - const lastExploreRefreshAt = useRef(0); const filterRef = useRef(null); const contentKey = `${debouncedQuery}\u0000${sort}\u0000${currentPage}`; const requestKey = `${contentKey}\u0000${retryKey}\u0000${refreshKey}`; @@ -517,37 +521,6 @@ export function ExploreView() { return () => window.clearTimeout(timer); }, [debouncedQuery, normalizedQuery]); - useEffect(() => { - lastExploreRefreshAt.current = Date.now(); - - function refreshIfDue() { - const now = Date.now(); - if ( - !shouldRefreshExplore({ - visibilityState: document.visibilityState, - lastRefreshAt: lastExploreRefreshAt.current, - now, - }) - ) - return; - lastExploreRefreshAt.current = now; - setRefreshKey((value) => value + 1); - } - - const interval = window.setInterval( - refreshIfDue, - EXPLORE_REFRESH_INTERVAL_MS, - ); - document.addEventListener("visibilitychange", refreshIfDue); - window.addEventListener("focus", refreshIfDue); - - return () => { - window.clearInterval(interval); - document.removeEventListener("visibilitychange", refreshIfDue); - window.removeEventListener("focus", refreshIfDue); - }; - }, []); - useEffect(() => { function closeFilter(event: PointerEvent | KeyboardEvent) { const filter = filterRef.current; diff --git a/components/launch-builder.tsx b/components/launch-builder.tsx index f11e7cd7..cecebd2f 100644 --- a/components/launch-builder.tsx +++ b/components/launch-builder.tsx @@ -38,7 +38,6 @@ import { validateDeepV3LaunchDraft, } from "@/lib/deep-v3"; import { validatePreparedDeepV3LaunchTransaction } from "@/lib/deep-v3-launch-validation"; -import { isConfiguredDeepV3ReleaseReady } from "@/lib/deep-v3-release"; import { validatePreparedStockPairedLaunchTransaction } from "@/lib/stock-paired-launch-validation"; import { isStockPairedLocalPreviewEnabled } from "@/lib/stock-paired-access"; import { @@ -74,7 +73,6 @@ import { CLASSIC_TOTAL_SWAP_FEE_BPS, CLASSIC_TOTAL_SWAP_FEE_PERCENT, createClassicV3Draft, - createDeepDraft, createEmptyDraft, createStockPairedDraft, getClassicInitialBuyPreview, @@ -183,7 +181,6 @@ const SUPPORTED_LAUNCH_MODELS = new Set([ "classic", "classic-v3", "adaptive", - "deep", "stock-paired", ]); @@ -882,7 +879,6 @@ const classicV3LaunchAvailable = (process.env.NODE_ENV !== "production" && process.env.NEXT_PUBLIC_CLASSIC_V3_UI_PREVIEW === "true") || isConfiguredClassicV3ReleaseReady(launchEnvironment); -const deepLaunchAvailable = isConfiguredDeepV3ReleaseReady(launchEnvironment); function browserPendingLaunchStorages(): PendingLaunchStorage[] { if (typeof window === "undefined") return []; const storages: PendingLaunchStorage[] = []; @@ -1037,14 +1033,14 @@ export function LaunchBuilderForm({ onBackToModels: () => void; stockPairedPublicLaunchEnabled: boolean; }) { + if (model === "deep") return null; + const initialDraft = - model === "deep" - ? createDeepDraft() - : model === "stock-paired" - ? normalizeStockPairedDraft(createStockPairedDraft()) - : model === "classic-v3" - ? createClassicV3Draft() - : normalizeStandardDraft(createEmptyDraft()); + model === "stock-paired" + ? normalizeStockPairedDraft(createStockPairedDraft()) + : model === "classic-v3" + ? createClassicV3Draft() + : normalizeStandardDraft(createEmptyDraft()); return ( - {model === "deep" && !deepLaunchAvailable - ? "Deep is being finalized" - : model === "stock-paired" && !stockPairedLaunchAllowed - ? "Stock-Paired is coming soon" - : model === "classic-v3" && !classicV3LaunchAvailable - ? "Classic is not deployed" - : !pendingRestoreComplete - ? "Checking launch status" - : launchPhase === "preparing" - ? "Preparing launch" - : launchPhase === "confirming" - ? "Confirm in wallet" - : wallet - ? "Launch token" - : "Connect wallet"} + {model === "stock-paired" && !stockPairedLaunchAllowed + ? "Stock-Paired is coming soon" + : model === "classic-v3" && !classicV3LaunchAvailable + ? "Classic is not deployed" + : !pendingRestoreComplete + ? "Checking launch status" + : launchPhase === "preparing" + ? "Preparing launch" + : launchPhase === "confirming" + ? "Confirm in wallet" + : wallet + ? "Launch token" + : "Connect wallet"} ); diff --git a/components/launch-entry.tsx b/components/launch-entry.tsx index 57a7d18e..edc670e8 100644 --- a/components/launch-entry.tsx +++ b/components/launch-entry.tsx @@ -58,19 +58,16 @@ function LaunchFormLoading({ onBack }: { onBack: () => void }) { ); } -export function LaunchExperience({ - stockPairedPublicLaunchEnabled, -}: { - stockPairedPublicLaunchEnabled: boolean; -}) { +export function LaunchExperience() { const [selectedModel, setSelectedModel] = useState(null); function chooseModel(candidate: LaunchModel) { const model = resolveImplementedLaunchModel(candidate); if ( !model || - (model === "classic-v3" && !classicV3LaunchAvailable) || - (model === "stock-paired" && !stockPairedPublicLaunchEnabled) + model === "deep" || + model === "stock-paired" || + (model === "classic-v3" && !classicV3LaunchAvailable) ) { return; } @@ -85,12 +82,7 @@ export function LaunchExperience({ } if (!selectedModel) { - return ( - - ); + return ; } return ( @@ -98,7 +90,7 @@ export function LaunchExperience({ ); @@ -106,10 +98,8 @@ export function LaunchExperience({ export function LaunchModelPicker({ onChoose, - stockPairedPublicLaunchEnabled = false, }: { onChoose: (model: LaunchModel) => void; - stockPairedPublicLaunchEnabled?: boolean; }) { const preloadAvailableForm = () => { void loadLaunchForm(); @@ -181,64 +171,6 @@ export function LaunchModelPicker({ - - ); diff --git a/components/profile-experience.module.css b/components/profile-experience.module.css index 2c9de698..42c8879d 100644 --- a/components/profile-experience.module.css +++ b/components/profile-experience.module.css @@ -1,8 +1,7 @@ .page { - max-width: 1180px; + max-width: 1120px; min-height: calc(100svh - var(--header-height)); - padding-bottom: 48px; - padding-top: 24px; + padding-block: 20px 56px; } .hero, @@ -14,7 +13,7 @@ border: 1px solid var(--panel-border-soft); box-shadow: 0 1px 0 var(--card-shadow), - 0 22px 64px var(--card-shadow-soft); + 0 20px 56px var(--card-shadow-soft); } .hero { @@ -25,10 +24,10 @@ border-radius: 0; box-shadow: none; display: grid; - gap: 18px; + gap: 16px; grid-template-columns: auto minmax(0, 1fr); - min-height: 104px; - padding: 12px 8px; + min-height: 90px; + padding: 10px 8px; } .heroEditing { @@ -36,65 +35,72 @@ backdrop-filter: blur(22px) saturate(1.08); background: color-mix(in srgb, var(--glass-96) 94%, transparent); border: 1px solid var(--panel-border-soft); - border-radius: 26px; + border-radius: 22px; box-shadow: 0 1px 0 var(--card-shadow), - 0 22px 64px var(--card-shadow-soft); - padding: 20px 24px; + 0 20px 56px var(--card-shadow-soft); + padding: 18px 20px; } .avatar { align-items: center; background: var(--avatar-background); border: 1px solid color-mix(in srgb, var(--glass-94) 88%, var(--border)); - border-radius: 20px; + border-radius: 18px; color: var(--accent-strong); display: flex; flex: none; font-family: var(--font-plex-mono), monospace; - font-size: 20px; - font-weight: 500; - height: 70px; + font-size: 19px; + font-weight: 600; + height: 64px; justify-content: center; overflow: hidden; position: relative; - width: 70px; + width: 64px; } .avatar img { object-fit: cover; } -.heroCopy { +.heroCopy, +.nameRow, +.tokenCopy, +.tokenNameRow { min-width: 0; } .nameRow { align-items: center; display: flex; - gap: 12px; - min-width: 0; + gap: 10px; } .nameRow h1 { - font-size: clamp(32px, 3vw, 40px); - font-weight: 570; + font-size: clamp(30px, 3vw, 37px); + font-weight: 590; letter-spacing: -0.045em; - line-height: 1.08; - min-width: 0; + line-height: 1.06; overflow: hidden; - padding-bottom: 0.06em; + padding-block-end: 0.05em; text-overflow: ellipsis; white-space: nowrap; } +.address, +.tokenAddress, +.payoutRow a, +.rewardSettingHeader p { + font-family: var(--font-plex-mono), monospace; + font-variant-numeric: tabular-nums; +} + .address { color: var(--muted); - font-family: var(--font-plex-mono), monospace; font-size: 12px; - font-variant-numeric: tabular-nums; letter-spacing: -0.01em; - margin-top: 9px; + margin-block-start: 7px; } .editButton, @@ -102,11 +108,12 @@ .editAction, .claimButton, .secondaryAction, -.retryButton { +.retryButton, +.emptyAction { align-items: center; border: 1px solid var(--border); display: inline-flex; - font-weight: 620; + font-weight: 650; justify-content: center; } @@ -116,22 +123,22 @@ color: var(--text-soft); flex: none; font-size: 12px; - min-height: 42px; - padding: 0 14px; + min-height: 44px; + padding-inline: 14px; } .editForm { - border-top: 1px solid var(--border); - margin-top: 17px; + border-block-start: 1px solid var(--border); + margin-block-start: 15px; max-width: 760px; - padding-top: 16px; + padding-block-start: 15px; } .editGrid { align-items: end; display: grid; gap: 16px; - grid-template-columns: minmax(200px, 0.9fr) minmax(330px, 1.35fr); + grid-template-columns: minmax(190px, 0.85fr) minmax(330px, 1.4fr); } .imageControl, @@ -143,7 +150,7 @@ .fieldLabel { color: var(--text-soft); font-size: 12px; - font-weight: 620; + font-weight: 650; } .imageActions, @@ -159,7 +166,7 @@ color: var(--text-soft); font-size: 12px; min-height: 44px; - padding: 0 14px; + padding-inline: 14px; } .usernameRow { @@ -173,10 +180,11 @@ border: 1px solid var(--border-strong); border-radius: 12px; color: var(--text); + font-size: 16px; height: 44px; min-width: 0; outline: 0; - padding: 0 13px; + padding-inline: 13px; } .saveAction { @@ -188,8 +196,8 @@ .formHelp { color: var(--muted); font-size: 12px; - line-height: 1.45; - margin-top: 7px; + line-height: 1.5; + margin-block-start: 7px; } .formError { @@ -198,38 +206,38 @@ .connectCard { align-items: center; - border-radius: 28px; + border-radius: 26px; display: flex; flex-direction: column; justify-content: center; - margin: clamp(42px, 10svh, 92px) auto 0; - max-width: 580px; - min-height: 340px; - padding: 42px 32px; + margin: clamp(38px, 9svh, 82px) auto 0; + max-width: 560px; + min-height: 326px; + padding: 40px 30px; text-align: center; } .connectMark { filter: drop-shadow(0 14px 30px rgba(142, 68, 109, 0.1)); - height: 82px; - margin-bottom: 24px; + height: 78px; + margin-block-end: 22px; object-fit: contain; - width: 82px; + width: 78px; } .connectCard h1 { - font-size: clamp(38px, 4vw, 48px); - font-weight: 580; + font-size: clamp(36px, 4vw, 46px); + font-weight: 590; letter-spacing: -0.048em; line-height: 1; } .connectCard p { color: var(--text-soft); - font-size: 15.5px; + font-size: 15px; line-height: 1.5; - margin-top: 13px; - max-width: 400px; + margin-block-start: 12px; + max-width: 380px; } .connectButton { @@ -242,26 +250,26 @@ font-size: 14px; font-weight: 680; justify-content: center; - margin-top: 24px; + margin-block-start: 22px; min-height: 48px; - padding: 0 20px; + padding-inline: 20px; } .portfolio { - border-radius: 28px; - margin-top: 14px; + border-radius: 24px; + margin-block-start: 10px; min-width: 0; overflow: hidden; } .portfolioHeader { align-items: center; - border-bottom: 1px solid var(--border); + border-block-end: 1px solid var(--border); display: grid; - gap: 20px 36px; + gap: 16px 28px; grid-template-columns: minmax(0, 1fr) auto; - min-height: 124px; - padding: 24px 28px 22px; + min-height: 100px; + padding: 20px 24px 18px; } .portfolioTitle { @@ -269,82 +277,82 @@ } .portfolioTitle h2 { - font-size: 32px; - font-weight: 600; + font-size: 28px; + font-weight: 610; letter-spacing: -0.04em; line-height: 1; } .portfolioStats { - align-items: center; + align-items: stretch; display: flex; flex: none; - gap: 8px; } .portfolioStat { + align-content: center; display: grid; - gap: 5px; - min-width: 96px; - padding: 11px 18px; + gap: 4px; + min-width: 82px; + padding-block: 2px; + padding-inline: 18px; } .portfolioStat + .portfolioStat { - border-left: 0; + border-inline-start: 1px solid var(--border); } .portfolioStat:last-child { - padding-right: 18px; + padding-inline-end: 0; } .portfolioStat span { color: var(--muted); - font-size: 12px; - font-weight: 560; + font-size: 11px; + font-weight: 620; + letter-spacing: 0.03em; + text-transform: uppercase; } .portfolioStat strong { color: var(--text); - font-size: 22px; + font-size: 20px; font-variant-numeric: tabular-nums; - font-weight: 620; + font-weight: 640; letter-spacing: -0.025em; line-height: 1.1; white-space: nowrap; } -.portfolioTotal strong { - color: var(--accent-strong); +.portfolioTotal { + min-width: 176px; } -.portfolioTotal { - background: color-mix(in srgb, var(--accent) 8%, transparent); - border: 1px solid color-mix(in srgb, var(--accent) 16%, var(--border)); - border-radius: 16px; - min-width: 190px; +.portfolioTotal strong { + color: var(--accent-strong); } .portfolioTotal small { color: var(--muted); - font-size: 12px; + font-size: 11px; line-height: 1.2; white-space: nowrap; } .rewardDistribution { - background: color-mix(in srgb, var(--surface-soft) 76%, transparent); + background: color-mix(in srgb, var(--surface-soft) 82%, transparent); border-radius: 999px; display: flex; gap: 2px; grid-column: 1 / -1; - height: 5px; + height: 4px; overflow: hidden; width: 100%; } .rewardDistribution span { background: var(--accent); - min-width: 5px; + min-width: 4px; } .rewardDistribution span:nth-child(3n + 2) { @@ -358,7 +366,7 @@ .sourceWarning { align-items: center; background: var(--warning-background); - border-bottom: 1px solid var(--warning-border); + border-block-end: 1px solid var(--warning-border); color: var(--warning-text); display: flex; font-size: 12px; @@ -374,7 +382,8 @@ color: inherit; font-size: 12px; font-weight: 680; - min-height: 32px; + min-height: 36px; + padding-inline: 6px; } .ledger { @@ -385,39 +394,39 @@ .tokenMain { display: grid; grid-template-columns: - minmax(310px, 1.72fr) - minmax(128px, 0.7fr) - minmax(168px, 0.92fr) - minmax(178px, auto); + minmax(300px, 1.7fr) + minmax(118px, 0.66fr) + minmax(158px, 0.86fr) + minmax(182px, auto); } .ledgerHeader { align-items: center; color: var(--muted); font-size: 11px; - font-weight: 620; + font-weight: 640; letter-spacing: 0.045em; - min-height: 46px; - padding: 0 28px; + min-height: 40px; + padding-inline: 24px; text-transform: uppercase; } .ledgerHeader span:nth-child(2), .ledgerHeader span:nth-child(3) { - padding-left: 20px; + padding-inline-start: 18px; } .list { - border-top: 1px solid var(--border); + border-block-start: 1px solid var(--border); display: grid; } .tokenRow { align-self: stretch; background: color-mix(in srgb, var(--glass-82) 88%, transparent); - border-bottom: 1px solid var(--border); + border-block-end: 1px solid var(--border); min-width: 0; - padding: 0 28px; + padding-inline: 24px; transform: translate3d(0, 0, 0); transition: background-color 170ms ease, @@ -426,22 +435,22 @@ } .tokenRow:last-child { - border-bottom: 0; + border-block-end: 0; } .tokenRowClaimable { background: linear-gradient( 90deg, - color-mix(in srgb, var(--accent-soft) 72%, transparent), - transparent 44% + color-mix(in srgb, var(--accent-soft) 68%, transparent), + transparent 42% ), color-mix(in srgb, var(--glass-82) 90%, transparent); } .tokenMain { align-items: center; - min-height: 106px; + min-height: 96px; min-width: 0; } @@ -449,20 +458,20 @@ align-items: center; color: var(--text); display: flex; - gap: 16px; + gap: 14px; min-width: 0; - padding-right: 24px; + padding-inline-end: 20px; } .tokenArt { background: var(--surface-soft); border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); - border-radius: 18px; + border-radius: 16px; flex: none; - height: 62px; + height: 58px; overflow: hidden; position: relative; - width: 62px; + width: 58px; } .tokenArt img { @@ -472,19 +481,17 @@ .tokenCopy { display: flex; flex-direction: column; - min-width: 0; } .tokenNameRow { align-items: baseline; display: flex; - gap: 9px; - min-width: 0; + gap: 8px; } .tokenCopy strong { - font-size: 17.5px; - font-weight: 630; + font-size: 17px; + font-weight: 640; letter-spacing: -0.02em; overflow: hidden; text-overflow: ellipsis; @@ -495,7 +502,7 @@ color: var(--accent-strong); flex: none; font-size: 13px; - font-weight: 600; + font-weight: 640; } .tokenMeta { @@ -504,7 +511,7 @@ display: flex; font-size: 12px; gap: 8px; - margin-top: 5px; + margin-block-start: 4px; min-width: 0; } @@ -518,69 +525,48 @@ color: var(--accent-strong); flex: none; font-size: 12px; - font-weight: 620; + font-weight: 640; } .modelLabel::after { color: var(--muted); content: "·"; - margin-left: 8px; + margin-inline-start: 8px; } .tokenAddress { color: var(--muted); - font-family: var(--font-plex-mono), monospace; - font-size: 12px; - font-variant-numeric: tabular-nums; - margin-top: 5px; + font-size: 11px; + margin-block-start: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.openToken { - align-items: center; - background: color-mix(in srgb, var(--glass-88) 72%, transparent); - border: 1px solid var(--border); - border-radius: 12px; - color: var(--text-soft); - display: inline-flex; - flex: none; - font-size: 12px; - font-weight: 610; - height: 44px; - justify-content: center; - line-height: 1; - min-width: 94px; - padding: 0 15px; - white-space: nowrap; -} - .metric { display: flex; flex-direction: column; min-width: 0; - padding: 0 20px; + padding-inline: 18px; } .marketMetric, .rewardMetric { - border-left: 1px solid var(--border); + border-inline-start: 1px solid var(--border); } .metric span { color: var(--muted); display: none; - font-size: 12px; - font-weight: 560; + font-size: 11px; + font-weight: 590; } .metric strong { - font-size: 16px; + font-size: 15px; font-variant-numeric: tabular-nums; - font-weight: 630; + font-weight: 650; letter-spacing: -0.015em; - margin-top: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -588,8 +574,8 @@ .metric small { color: var(--muted); - font-size: 12px; - margin-top: 3px; + font-size: 11px; + margin-block-start: 3px; } .rewardMetricReady strong { @@ -600,9 +586,9 @@ align-items: center; display: flex; flex-wrap: wrap; - gap: 8px; + gap: 7px; justify-content: flex-end; - padding-left: 18px; + padding-inline-start: 16px; } .actions:empty { @@ -618,11 +604,11 @@ .stockClaimEstimate { color: var(--text-soft); font-size: 11px; - font-weight: 560; font-variant-numeric: tabular-nums; + font-weight: 580; line-height: 1.3; - padding: 0 2px; - text-align: right; + padding-inline: 2px; + text-align: end; white-space: nowrap; } @@ -633,17 +619,18 @@ .stockClaimButtons .claimButton, .stockClaimButtons .secondaryAction { - min-width: 98px; - padding-inline: 13px; + min-width: 96px; + padding-inline: 12px; } .claimButton, -.secondaryAction { +.secondaryAction, +.openToken { border-radius: 12px; font-size: 12px; min-height: 44px; min-width: 76px; - padding: 0 15px; + padding-inline: 14px; } .claimButton { @@ -652,11 +639,27 @@ color: var(--accent-ink); } -.claimButton:disabled { +.claimButton:disabled, +.secondaryAction:disabled, +.textAction:disabled { cursor: default; opacity: 0.5; } +.openToken { + align-items: center; + background: color-mix(in srgb, var(--glass-88) 72%, transparent); + border: 1px solid var(--border); + color: var(--text-soft); + display: inline-flex; + flex: none; + font-weight: 620; + justify-content: center; + line-height: 1; + min-width: 92px; + white-space: nowrap; +} + .actionState, .rowError { align-items: center; @@ -664,8 +667,8 @@ font-size: 12px; gap: 8px; justify-content: flex-end; - line-height: 1.4; - margin: 0 0 12px; + line-height: 1.45; + margin: -2px 0 10px; } .actionState { @@ -682,63 +685,104 @@ font-weight: 650; } -.rewardSettings { - border-top: 1px solid var(--border); +.advancedSettings { + border-block-start: 1px solid var(--border); margin: 0; padding: 0; } -.rewardSettings > summary { +.advancedSettings > summary { align-items: center; color: var(--text-soft); - cursor: default; display: flex; font-size: 12px; - gap: 8px; + gap: 10px; justify-content: space-between; list-style: none; - min-height: 46px; + min-height: 48px; outline: 0; } -.rewardSettings > summary::-webkit-details-marker { +.advancedSettings > summary::-webkit-details-marker { display: none; } -.rewardSettings > summary::after { - color: var(--muted); +.advancedSettings > summary::after { + color: var(--accent-strong); content: "+"; flex: none; - font-size: 15px; + font-size: 17px; + font-weight: 500; line-height: 1; + text-align: center; + width: 20px; } -.rewardSettings[open] > summary::after { +.advancedSettings[open] > summary::after { content: "−"; } -.rewardSettings > summary span { - font-weight: 590; +.advancedSettings > summary span { + font-weight: 650; min-width: 0; } -.rewardSettings > summary small { +.advancedSettings > summary small { color: var(--muted); - font-family: var(--font-plex-mono), monospace; - font-size: 12px; - margin-left: auto; - max-width: 150px; + font-size: 11px; + margin-inline-start: auto; +} + +.advancedSettingsBody { + background: color-mix(in srgb, var(--surface-soft) 52%, transparent); + border: 1px solid color-mix(in srgb, var(--border) 76%, transparent); + border-radius: 16px; + margin-block-end: 16px; + padding-inline: 16px; +} + +.advancedSettings[open] .advancedSettingsBody { + animation: profile-settings-enter 180ms var(--ease-out) both; +} + +.rewardSettingGroup { + padding-block: 16px; +} + +.rewardSettingGroup + .rewardSettingGroup { + border-block-start: 1px solid var(--border); +} + +.rewardSettingHeader { + align-items: baseline; + display: flex; + gap: 12px; + justify-content: space-between; + margin-block-end: 12px; +} + +.rewardSettingHeader h3 { + font-size: 13px; + font-weight: 660; + letter-spacing: -0.01em; + line-height: 1.3; + margin: 0; +} + +.rewardSettingHeader p { + color: var(--muted); + font-size: 11px; + margin: 0; + max-width: 48%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .settingsBody { - animation: profile-settings-enter 180ms var(--ease-out) both; display: grid; - gap: 16px; + gap: 14px; grid-template-columns: 1fr; - padding: 2px 0 17px; } @keyframes profile-settings-enter { @@ -754,44 +798,46 @@ } .rewardTerms { + background: color-mix(in srgb, var(--border) 68%, transparent); + border: 1px solid color-mix(in srgb, var(--border) 74%, transparent); + border-radius: 12px; display: grid; - gap: 7px; + gap: 1px; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; + overflow: hidden; } .rewardTerms > div { - background: color-mix(in srgb, var(--surface-soft) 72%, transparent); - border: 1px solid color-mix(in srgb, var(--border) 74%, transparent); - border-radius: 12px; + background: color-mix(in srgb, var(--glass-94) 94%, transparent); display: grid; gap: 3px; - padding: 10px; + padding: 10px 11px; } .rewardTerms dt { color: var(--muted); - font-size: 12px; + font-size: 11px; } .rewardTerms dd { font-size: 12px; font-variant-numeric: tabular-nums; - font-weight: 650; + font-weight: 660; margin: 0; overflow-wrap: anywhere; } .payout { display: grid; - gap: 8px; + gap: 7px; } .payoutLabel, .splitLabel { color: var(--muted); - font-size: 11px; - font-weight: 620; + font-size: 10px; + font-weight: 660; letter-spacing: 0.06em; text-transform: uppercase; } @@ -801,12 +847,16 @@ align-items: center; display: flex; gap: 8px; + min-width: 0; } .payoutRow a { color: var(--text-soft); - font-family: var(--font-plex-mono), monospace; font-size: 12px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .secondaryAction { @@ -818,21 +868,22 @@ background: transparent; border: 0; color: var(--accent-strong); + flex: none; font-size: 12px; font-weight: 650; min-height: 44px; - padding: 0 4px; + padding-inline: 6px; } .split { - border-top: 1px solid var(--border); - padding-top: 13px; + border-block-start: 1px solid var(--border); + padding-block-start: 12px; } .splitList { display: grid; gap: 7px; - margin-top: 8px; + margin-block-start: 8px; } .splitItem { @@ -853,7 +904,7 @@ } .splitItem small { - text-align: right; + text-align: end; } .emptySection { @@ -861,59 +912,96 @@ display: flex; flex-direction: column; justify-content: center; - min-height: 178px; - padding: 28px 24px; + min-height: 182px; + padding: 30px 24px; } .emptySection strong { - font-size: 17px; - font-weight: 610; + font-size: 18px; + font-weight: 630; } .emptySection p { color: var(--muted); font-size: 13px; - margin-top: 6px; + margin-block-start: 5px; +} + +.emptyAction { + background: var(--accent); + border-color: color-mix(in srgb, var(--accent-strong) 42%, transparent); + border-radius: 12px; + color: var(--accent-ink); + font-size: 12px; + margin-block-start: 16px; + min-height: 44px; + padding-inline: 16px; } .accountState { align-items: center; - border-radius: 26px; + border-radius: 24px; display: flex; flex-direction: column; justify-content: center; - margin-top: 16px; - min-height: 280px; - padding: 30px; + margin-block-start: 12px; + min-height: 260px; + overflow: hidden; + padding: 28px; + position: relative; text-align: center; } +.accountState[aria-busy="true"]::before { + animation: profile-loading 1.4s ease-in-out infinite; + background: linear-gradient( + 90deg, + transparent, + color-mix(in srgb, var(--accent) 48%, transparent), + transparent + ); + content: ""; + height: 2px; + inset-block-start: 0; + inset-inline: 0; + position: absolute; + transform: translateX(-100%); +} + +@keyframes profile-loading { + to { + transform: translateX(100%); + } +} + .accountState h2 { - font-size: 26px; - font-weight: 600; + font-size: 25px; + font-weight: 610; letter-spacing: -0.03em; } .accountState p { color: var(--muted); font-size: 13px; - margin-top: 7px; + line-height: 1.5; + margin-block-start: 6px; + max-width: 440px; } .retryButton { background: var(--surface-soft); border-radius: 12px; font-size: 12px; - margin-top: 16px; + margin-block-start: 16px; min-height: 44px; - padding: 0 15px; + padding-inline: 15px; } @media (hover: hover) and (pointer: fine) { .tokenRow:hover { background: color-mix(in srgb, var(--surface-soft) 88%, transparent); box-shadow: inset 3px 0 0 color-mix(in srgb, var(--accent) 58%, transparent); - transform: translate3d(2px, 0, 0); + transform: translate3d(0, -1px, 0); } .tokenIdentity:hover strong { @@ -924,18 +1012,20 @@ .imageAction:hover, .editAction:hover, .openToken:hover, - .secondaryAction:hover, + .secondaryAction:not(:disabled):hover, .retryButton:hover { border-color: var(--border-strong); color: var(--text); } - .textAction:hover { + .advancedSettings > summary:hover, + .textAction:not(:disabled):hover { color: var(--text); } .claimButton:not(:disabled):hover, - .connectButton:hover { + .connectButton:hover, + .emptyAction:hover { background: var(--accent-hover); } } @@ -948,12 +1038,13 @@ .claimButton:focus-visible, .secondaryAction:focus-visible, .openToken:focus-visible, -.rewardSettings > summary:focus-visible, +.advancedSettings > summary:focus-visible, .payoutEdit input:focus-visible, .textAction:focus-visible, .retryButton:focus-visible, .sourceWarning button:focus-visible, -.tokenIdentity:focus-visible { +.tokenIdentity:focus-visible, +.emptyAction:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } @@ -966,7 +1057,8 @@ .secondaryAction, .openToken, .textAction, -.retryButton { +.retryButton, +.emptyAction { transition: background-color 150ms ease, border-color 150ms ease, @@ -979,11 +1071,12 @@ .editAction:active, .connectButton:active, .claimButton:not(:disabled):active, -.secondaryAction:active, +.secondaryAction:not(:disabled):active, .openToken:active, -.textAction:active, -.retryButton:active { - transform: scale(0.96); +.textAction:not(:disabled):active, +.retryButton:active, +.emptyAction:active { + transform: scale(0.97); } @media (max-width: 980px) { @@ -1003,17 +1096,18 @@ .tokenRow { border: 1px solid color-mix(in srgb, var(--border) 90%, transparent); border-radius: 18px; - padding: 16px; + padding-inline: 16px; } .tokenRow:last-child { - border-bottom: 1px solid color-mix(in srgb, var(--border) 90%, transparent); + border-block-end: 1px solid color-mix(in srgb, var(--border) 90%, transparent); } .tokenMain { gap: 14px 0; grid-template-columns: minmax(0, 1fr) minmax(180px, auto); min-height: 0; + padding-block: 15px; } .tokenIdentity { @@ -1022,10 +1116,10 @@ } .marketMetric { - border-left: 0; + border-inline-start: 0; grid-column: 1; grid-row: 2; - padding-left: 0; + padding-inline-start: 0; } .rewardMetric { @@ -1034,8 +1128,8 @@ } .metric { - border-top: 1px solid var(--border); - padding-top: 13px; + border-block-start: 1px solid var(--border); + padding-block-start: 12px; } .metric span { @@ -1043,43 +1137,46 @@ } .metric strong { - margin-top: 4px; + margin-block-start: 3px; } .actions { grid-column: 2; grid-row: 1; } + + .advancedSettingsBody { + margin-block-end: 15px; + } } @media (max-width: 720px) { .page { min-height: calc(100svh - var(--header-height) - 68px); - padding-bottom: 104px; - padding-top: 16px; + padding-block: 14px 104px; } .hero, .portfolio, .connectCard, .accountState { - border-radius: 22px; + border-radius: 21px; } .hero { - gap: 14px; - min-height: 96px; - padding: 15px; + gap: 13px; + min-height: 86px; + padding: 12px; } .avatar { - border-radius: 19px; - height: 60px; - width: 60px; + border-radius: 17px; + height: 58px; + width: 58px; } .nameRow h1 { - font-size: 30px; + font-size: 29px; } .editGrid { @@ -1087,7 +1184,7 @@ } .connectCard { - margin-top: 18px; + margin-block-start: 18px; min-height: 272px; padding: 30px 22px; } @@ -1097,16 +1194,16 @@ } .portfolioHeader { - padding: 20px 18px; + padding: 18px; } .portfolioTitle h2 { - font-size: 27px; + font-size: 26px; } .portfolioStat { - min-width: 76px; - padding: 0 16px; + min-width: 72px; + padding-inline: 14px; } .portfolioStat strong { @@ -1119,7 +1216,7 @@ } } -@media (max-width: 520px) { +@media (max-width: 560px) { .hero { grid-template-columns: auto minmax(0, 1fr); } @@ -1128,22 +1225,16 @@ grid-template-columns: 1fr; } - .avatar { - border-radius: 18px; - height: 58px; - width: 58px; - } - .nameRow { gap: 8px; } .nameRow h1 { - font-size: 27px; + font-size: 26px; } .editButton { - padding: 0 12px; + padding-inline: 12px; } .usernameRow { @@ -1156,37 +1247,26 @@ .portfolioHeader { align-items: start; - gap: 18px; + gap: 16px; grid-template-columns: 1fr; } .portfolioStats { - justify-content: flex-start; width: 100%; } .portfolioStat:first-child { - padding-left: 0; + padding-inline-start: 0; } .portfolioStat:last-child { - padding-right: 16px; + padding-inline-end: 0; } .portfolioTotal { min-width: 0; } - .tokenArt { - border-radius: 16px; - height: 60px; - width: 60px; - } - - .tokenCopy strong { - font-size: 16px; - } - .tokenMain { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1194,7 +1274,7 @@ .tokenIdentity { grid-column: 1 / -1; grid-row: 1; - padding-right: 0; + padding-inline-end: 0; } .marketMetric { @@ -1205,15 +1285,16 @@ .rewardMetric { grid-column: 2; grid-row: 2; + padding-inline-start: 13px; } .actions { - border-top: 1px solid var(--border); + border-block-start: 1px solid var(--border); grid-column: 1 / -1; grid-row: 3; justify-content: stretch; - padding-left: 0; - padding-top: 13px; + padding-block-start: 12px; + padding-inline-start: 0; } .stockClaimActions { @@ -1222,7 +1303,7 @@ } .stockClaimEstimate { - text-align: left; + text-align: start; } .stockClaimButtons { @@ -1236,20 +1317,18 @@ width: 100%; } - .openToken { + .openToken, + .claimButton, + .secondaryAction { flex: 1 1 auto; - font-size: 12px; - height: 46px; - padding: 0 11px; + min-height: 46px; } - .rewardMetric { - padding-left: 14px; - } - - .claimButton { - flex: 1 1 auto; - min-height: 46px; + .advancedSettings > summary small { + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .rewardTerms { @@ -1261,30 +1340,89 @@ flex-direction: column; } + .payoutEdit .secondaryAction, + .payoutEdit .textAction { + width: 100%; + } + .actionState, .rowError { align-items: flex-start; flex-direction: column; gap: 3px; + justify-content: flex-start; } } @media (max-width: 380px) { + .page { + padding-block-start: 10px; + } + .hero { - gap: 11px; + gap: 10px; + padding-inline: 8px; } .avatar { - height: 54px; - width: 54px; + height: 52px; + width: 52px; } .nameRow h1 { - font-size: 24px; + font-size: 23px; } .address { - font-size: 12px; + font-size: 11px; + } + + .portfolioHeader { + padding-inline: 15px; + } + + .portfolioStat { + padding-inline: 11px; + } + + .portfolioStat strong { + font-size: 17px; + } + + .tokenRow { + padding-inline: 13px; + } + + .tokenArt { + border-radius: 14px; + height: 54px; + width: 54px; + } + + .tokenCopy strong { + font-size: 16px; + } + + .advancedSettings > summary { + gap: 6px; + } + + .advancedSettings > summary small { + display: none; + } + + .advancedSettingsBody { + padding-inline: 12px; + } + + .rewardSettingHeader { + align-items: flex-start; + flex-direction: column; + gap: 3px; + } + + .rewardSettingHeader p { + max-width: 100%; } .rewardTerms { @@ -1297,11 +1435,17 @@ .splitItem small { grid-column: 1 / -1; - text-align: left; + text-align: start; } } @media (prefers-reduced-motion: reduce) { + .accountState[aria-busy="true"]::before { + animation: none; + opacity: 0.55; + transform: none; + } + .tokenRow, .editButton, .imageAction, @@ -1311,11 +1455,25 @@ .secondaryAction, .openToken, .textAction, - .retryButton { + .retryButton, + .emptyAction { transition: none; } - .settingsBody { + .advancedSettings[open] .advancedSettingsBody { animation: none; } } + +@media (forced-colors: active) { + .tokenRowClaimable, + .advancedSettingsBody { + background: Canvas; + } + + .claimButton, + .connectButton, + .emptyAction { + border: 1px solid ButtonText; + } +} diff --git a/components/profile-view.tsx b/components/profile-view.tsx index 0b0f7203..be0e2547 100644 --- a/components/profile-view.tsx +++ b/components/profile-view.tsx @@ -16,7 +16,6 @@ import { import { useWallet } from "@/components/wallet-provider"; import { isConfiguredClassicV3ReleaseReady } from "@/lib/classic-v3-release"; -import { isConfiguredDeepV3ReleaseReady } from "@/lib/deep-v3-release"; import { prepareAvatarImage } from "@/lib/profile/avatar"; import { EMPTY_CLASSIC_V3_PROFILE, @@ -36,7 +35,6 @@ import { import { EMPTY_DEEP_PROFILE, fetchDeepProfileRewards, - isConfiguredDeepReleaseReady, prepareDeepRewardAction, type DeepProfileRewards, type DeepReward, @@ -89,9 +87,8 @@ const profileEnvironment = : "production"; const classicV3ReleaseAvailable = isConfiguredClassicV3ReleaseReady(profileEnvironment); -const deepReleaseAvailable = isConfiguredDeepReleaseReady(); -const deepV3ReleaseAvailable = - isConfiguredDeepV3ReleaseReady(profileEnvironment); +const deepReleaseAvailable = false; +const deepV3ReleaseAvailable = false; const stockPairedReleaseAvailable = isConfiguredStockPairedRewardsReady(); @@ -683,6 +680,41 @@ function getEmptyProfileSnapshot() { return ""; } +export function withoutClosedDeepProfileData( + data: ProfileOnchainData, +): ProfileOnchainData { + const closedTokenAddresses = new Set( + data.tokens + .filter((token) => token.launchModel === "deep") + .map((token) => token.address.toLowerCase()), + ); + if (closedTokenAddresses.size === 0) return data; + + const referencesClosedToken = (value: string) => { + const normalized = value.toLowerCase(); + return [...closedTokenAddresses].some((address) => + normalized.includes(address), + ); + }; + + return { + ...data, + tokens: data.tokens.filter((token) => token.launchModel !== "deep"), + positions: data.positions.filter( + (position) => + !closedTokenAddresses.has(position.tokenAddress.toLowerCase()), + ), + claims: data.claims.filter( + (claim) => !closedTokenAddresses.has(claim.tokenAddress.toLowerCase()), + ), + activity: data.activity.filter( + (activity) => + !referencesClosedToken(activity.href) && + !/\bdeep\b/iu.test(`${activity.label} ${activity.detail}`), + ), + }; +} + export function ProfileView({ onchainData }: ProfileViewProps = {}) { const { wallet, openWallet, sendTransaction } = useWallet(); const fileInputRef = useRef(null); @@ -1041,7 +1073,9 @@ export function ProfileView({ onchainData }: ProfileViewProps = {}) { } } - const requestedOnchainData = onchainData ?? remoteOnchainData; + const requestedOnchainData = withoutClosedDeepProfileData( + onchainData ?? remoteOnchainData, + ); const scopedOnchainData = account ? isProfileDataForAccount(requestedOnchainData, account) ? requestedOnchainData @@ -2400,7 +2434,7 @@ function ProfileAccountWorkspace({ return (

Connect your wallet

-

Your tokens and creator rewards will appear here

+

Connect to see your launches and claimable rewards.

@@ -2842,6 +2882,11 @@ function ProfilePortfolioRow({ : ""; const hasClaimableReward = totalClaimable > 0n || stockPairedClaimable > 0n; + const advancedSettingsCount = + ownedClassicRewards.length + + ownedDeepRewards.length + + ownedStockPairedRewards.length + + Number(Boolean(deepV3Token)); return (
- ) : null} + {advancedSettingsCount > 0 ? ( +
+ + Reward settings + Payouts, fee terms and splits + +
+ {deepV3Token ? ( + + ) : null} - {ownedClassicRewards.map((reward) => ( - - ))} - {ownedDeepRewards.map((reward) => ( - - ))} - {ownedStockPairedRewards.map((reward) => ( - - ))} + {ownedClassicRewards.map((reward) => ( + + ))} + {ownedDeepRewards.map((reward) => ( + + ))} + {ownedStockPairedRewards.map((reward) => ( + + ))} +
+
+ ) : null}
); } @@ -3132,17 +3187,22 @@ function ProfilePortfolioRow({ function DeepV3GrowthState({ token }: { token: DeepV3CreatorToken }) { const compoundCount = BigInt(token.compoundCount); return ( -
- - Liquidity growth - +
+
+

+ Liquidity growth +

+

{compoundCount === 0n ? "No compounds yet" : `${compoundCount.toString()} ${ compoundCount === 1n ? "compound" : "compounds" }`} - -

+

+
@@ -3159,7 +3219,7 @@ function DeepV3GrowthState({ token }: { token: DeepV3CreatorToken }) {
-
+ ); } @@ -3211,15 +3271,15 @@ function ClassicRewardSettings({ ) => void; }) { return ( -
- - Classic rewards - +
+
+

Classic rewards

+

{reward.shareBps > 0 ? `${(reward.shareBps / 100).toFixed(2)}% current share` : "Historic rewards"} - -

+

+
@@ -3265,7 +3325,7 @@ function ClassicRewardSettings({
) : null}
-
+ ); } @@ -3413,13 +3473,13 @@ function StockPairedRewardSettings({ const payoutPending = actionPending(payoutState); return ( -
- - +
+
+

Stock-Paired rewards · {reward.quoteAssetSymbol} - - {shortenAddress(reward.payoutAddress)} -

+ +

{shortenAddress(reward.payoutAddress)}

+
@@ -3523,7 +3583,7 @@ function StockPairedRewardSettings({
) : null}
-
+ ); } @@ -3587,16 +3647,16 @@ function DeepRewardSettings({ : null; return ( -
- - +
+
+

Deep liquidity ·{" "} {reward.growthTargetReached ? "Target reached" : `${(progressBps / 100).toFixed(2)}% added`} - - {shortenAddress(reward.payoutAddress)} -

+ +

{shortenAddress(reward.payoutAddress)}

+
@@ -3710,7 +3770,7 @@ function DeepRewardSettings({
-
+ ); } @@ -3721,14 +3781,23 @@ function minimumBigInt(left: bigint, right: bigint) { function ProfileSectionEmpty({ title, detail, + actionHref, + actionLabel, }: { title: string; detail: string; + actionHref?: string; + actionLabel?: string; }) { return (
{title}

{detail}

+ {actionHref && actionLabel ? ( + + {actionLabel} + + ) : null}
); } diff --git a/components/token-detail-view.tsx b/components/token-detail-view.tsx index 41a0837a..c3eb10d4 100644 --- a/components/token-detail-view.tsx +++ b/components/token-detail-view.tsx @@ -22,6 +22,7 @@ import { TokenPriceChart, type TokenChartVolume, } from "@/components/token-price-chart"; +import { useLiveDataRefresh } from "@/components/use-live-data-refresh"; import { WebsiteLinkIcon } from "@/components/website-link-icon"; import { useWallet } from "@/components/wallet-provider"; import { @@ -630,7 +631,7 @@ function MetricGrid({ metrics }: { metrics: TokenMetric[] }) { if (metrics.length === 0) return null; return ( -
+
{metrics.map((metric) => (
{metric.label}
@@ -982,33 +983,43 @@ function TokenDetailContent({
- {token.description?.trim() ? ( -

{token.description.trim()}

- ) : null} + {token.description?.trim() || + (token.links && token.links.length > 0) ? ( +
+ {token.description?.trim() ? ( +

{token.description.trim()}

+ ) : null} - {token.links && token.links.length > 0 ? ( -
- {token.links.map((link) => { - const label = getLinkLabel(link.kind); - return ( - - - - ); - })} + {token.links && token.links.length > 0 ? ( +
+ {token.links.map((link) => { + const label = getLinkLabel(link.kind); + return ( + + + + ); + })} +
+ ) : null}
) : null} + + - - {token.launchModel === "deep" && token.growthTargetNativeWei && token.totalNativeAddedToLiquidityWei && @@ -1176,6 +1185,7 @@ export function TokenDetailView({ address }: { address: string }) { const { wallet: activeWallet } = useWallet(); const normalizedAddress = isAddress(address) ? getAddress(address) : null; const [retryKey, setRetryKey] = useState(0); + const refreshKey = useLiveDataRefresh({ enabled: normalizedAddress !== null }); const requestKey = `${normalizedAddress ?? "invalid"}\u0000${retryKey}`; const [state, setState] = useState({ phase: "loading", @@ -1235,20 +1245,21 @@ export function TokenDetailView({ address }: { address: string }) { }); } catch (error) { if (controller.signal.aborted) return; - setState({ - phase: "error", - requestKey, - message: - error instanceof Error - ? error.message - : "Token data is temporarily unavailable", - }); + const message = + error instanceof Error + ? error.message + : "Token data is temporarily unavailable"; + setState((current) => + current.phase === "ready" && current.requestKey === requestKey + ? current + : { phase: "error", requestKey, message }, + ); } } void loadToken(); return () => controller.abort(); - }, [normalizedAddress, requestKey]); + }, [normalizedAddress, refreshKey, requestKey]); if (!normalizedAddress) { return ( diff --git a/components/token-experience.module.css b/components/token-experience.module.css index 5efcd129..a5065e47 100644 --- a/components/token-experience.module.css +++ b/components/token-experience.module.css @@ -49,14 +49,14 @@ flex-direction: column; min-width: 0; overflow: hidden; - padding: 26px; + padding: 28px; } .identity { align-items: center; display: grid; - gap: 24px; - grid-template-columns: 126px minmax(0, 1fr); + gap: 22px; + grid-template-columns: 122px minmax(0, 1fr); } .image { @@ -140,18 +140,29 @@ width: 44px; } +.tokenMeta { + align-items: flex-end; + display: flex; + gap: 18px; + justify-content: space-between; + margin-top: 20px; + min-width: 0; +} + .description { color: var(--text-soft); font-size: 14.5px; line-height: 1.55; - margin-top: 20px; + margin: 0; max-width: 68ch; } .links { + align-items: center; display: flex; + flex: none; gap: 7px; - margin-top: 13px; + margin: 0; } .socialLink { @@ -180,26 +191,21 @@ } .metrics { - border-top: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + background: color-mix(in srgb, var(--border) 84%, transparent); + border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + border-radius: 18px; display: grid; + gap: 1px; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); - margin: 20px 0 0; + margin: 22px 0 0; + overflow: hidden; padding: 0; } .metric { - border-right: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + background: color-mix(in srgb, var(--glass-96) 94%, transparent); min-width: 0; - padding: 17px; -} - -.metric:first-child { - padding-left: 2px; -} - -.metric:last-child { - border-right: 0; - padding-right: 2px; + padding: 15px 17px 16px; } .metric dt { @@ -280,7 +286,7 @@ } .tradeShell { - align-self: start; + align-self: stretch; border-radius: 30px; display: flex; min-width: 0; @@ -291,6 +297,7 @@ } .tradeShell > * { + flex: 1; width: 100%; } @@ -300,7 +307,7 @@ animation: token-panel-enter 180ms var(--ease-out) both; display: flex; flex-direction: column; - min-height: 0; + min-height: 100%; } .tradeHeader { @@ -385,11 +392,7 @@ } .amountCardInvalid { - border-color: color-mix( - in srgb, - var(--danger) 48%, - var(--border-strong) - ); + border-color: color-mix(in srgb, var(--danger) 48%, var(--border-strong)); } .amountHeader, @@ -406,11 +409,19 @@ min-height: 44px; } +.amountHeader > span:first-child { + flex: none; + white-space: nowrap; +} + .balanceRow { align-items: center; display: flex; + flex: 1; gap: 8px; + justify-content: flex-end; min-width: 0; + overflow: hidden; } .balance { @@ -418,6 +429,7 @@ font-size: 12px; font-variant-numeric: tabular-nums; max-width: 166px; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -425,9 +437,8 @@ .maxButton { appearance: none; - background: color-mix(in srgb, var(--accent) 9%, transparent); - border: 1px solid - color-mix(in srgb, var(--accent-strong) 36%, var(--border-strong)); + background: transparent; + border: 0; border-radius: 9px; box-shadow: none; color: var(--accent-strong); @@ -442,7 +453,6 @@ .maxButton:disabled { background: transparent; - border-color: transparent; color: var(--dim); } @@ -511,6 +521,46 @@ min-height: 16px; } +.tradeFacts { + border-bottom: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + border-top: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 16px 0 0; + padding: 0; +} + +.tradeFacts > div { + align-items: center; + display: flex; + gap: 10px; + justify-content: space-between; + min-width: 0; + padding: 12px 1px; +} + +.tradeFacts > div + div { + border-left: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + padding-left: 14px; +} + +.tradeFacts dt, +.tradeFacts dd { + font-size: 12px; + line-height: 1.25; +} + +.tradeFacts dt { + color: var(--muted); +} + +.tradeFacts dd { + color: var(--text); + font-variant-numeric: tabular-nums; + font-weight: 650; + margin: 0; +} + .error { background: color-mix(in srgb, var(--danger) 9%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 18%, transparent); @@ -528,6 +578,10 @@ min-height: 17px; } +.statusMessage:empty { + display: none; +} + .tradeFooter { margin-top: auto; padding-top: 20px; @@ -728,7 +782,7 @@ } .maxButton:not(:disabled):hover { - background: color-mix(in srgb, var(--accent) 14%, transparent); + background: color-mix(in srgb, var(--accent) 11%, transparent); } .primaryAction:not(:disabled):hover { @@ -791,6 +845,7 @@ } .tradeShell { + align-self: auto; display: block; max-width: none; position: static; @@ -799,6 +854,7 @@ .tradeForm, .review, .submitted { + height: auto; min-height: 320px; } } @@ -858,37 +914,37 @@ font-size: 12px; } + .tokenMeta { + align-items: flex-start; + flex-direction: column; + gap: 11px; + margin-top: 17px; + } + .description { font-size: 14px; - margin-top: 17px; } .links { - margin-top: 11px; + margin: 0; } .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); - margin-top: 14px; + margin-top: 17px; } .metric { - border-bottom: 1px solid var(--border); min-height: 72px; - padding: 13px; - } - - .metric:first-child { - padding-left: 2px; + padding: 13px 14px; } - .metric:nth-child(2n) { - border-right: 0; - padding-right: 2px; + .metrics[data-count="3"] { + grid-template-columns: repeat(3, minmax(0, 1fr)); } - .metric:nth-last-child(-n + 2) { - border-bottom: 0; + .metrics:not([data-count="3"]) .metric:last-child:nth-child(odd) { + grid-column: 1 / -1; } .metric dd { @@ -903,6 +959,7 @@ .tradeForm, .review, .submitted { + height: auto; min-height: 0; } @@ -926,8 +983,29 @@ font-size: clamp(27px, 8.5vw, 32px); } + .overview, + .tradeShell { + padding: 14px; + } + + .metrics[data-count="3"] { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .metric { + padding-inline: 10px; + } + + .metric dt { + font-size: 11px; + } + + .metric dd { + font-size: 16px; + } + .balance { - max-width: 132px; + max-width: 142px; } .amountInput { @@ -935,6 +1013,17 @@ } } +@media (max-width: 360px) { + .name { + font-size: clamp(24px, 7.8vw, 27px); + letter-spacing: -0.04em; + } + + .balance { + max-width: 112px; + } +} + @media (prefers-reduced-motion: reduce) { .back, .address, diff --git a/components/token-price-chart.module.css b/components/token-price-chart.module.css index 0912ced5..aa527f1e 100644 --- a/components/token-price-chart.module.css +++ b/components/token-price-chart.module.css @@ -8,7 +8,7 @@ color-mix(in srgb, var(--surface-tint) 72%, transparent); border: 1px solid color-mix(in srgb, var(--border) 82%, transparent); border-radius: 24px; - margin-top: 24px; + margin-top: 18px; min-height: 282px; overflow: hidden; padding: 19px 20px 14px; @@ -247,6 +247,24 @@ } } +@media (max-width: 430px) { + .header { + align-items: stretch; + flex-direction: column; + gap: 12px; + } + + .ranges { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + width: 100%; + } + + .rangeButton { + width: 100%; + } +} + @media (prefers-reduced-motion: reduce) { .area, .line, diff --git a/components/token-price-chart.tsx b/components/token-price-chart.tsx index 7f53e789..3fc2045c 100644 --- a/components/token-price-chart.tsx +++ b/components/token-price-chart.tsx @@ -9,6 +9,8 @@ import { useState, } from "react"; +import { useLiveDataRefresh } from "@/components/use-live-data-refresh"; + import styles from "./token-price-chart.module.css"; type ChartPoint = { @@ -63,6 +65,14 @@ export function getPriceHistoryEmptyMessage( : "Price history appears after confirmed trades"; } +export function shouldRenderPriceHistory(input: { + loading: boolean; + hasChart: boolean; + range: ChartRange; +}) { + return input.loading || input.hasChart || input.range !== "all"; +} + const CHART_RANGES: ReadonlyArray<{ value: ChartRange; label: string; @@ -175,6 +185,9 @@ export function TokenPriceChart({ failed: boolean; } | null>(null); const [range, setRange] = useState("all"); + const refreshKey = useLiveDataRefresh({ + enabled: launchModel !== "stock-paired", + }); const [activeIndex, setActiveIndex] = useState(null); const activeIndexRef = useRef(null); const plotRef = useRef(null); @@ -199,6 +212,7 @@ export function TokenPriceChart({ : request?.key === requestKey ? request.failed : false; + const loading = !payload && !failed; useEffect(() => { if (launchModel === "stock-paired") { @@ -226,15 +240,22 @@ export function TokenPriceChart({ .catch((error: unknown) => { if (error instanceof DOMException && error.name === "AbortError") return; - setRequest({ - key: requestKey, - payload: null, - failed: true, - }); + setRequest((current) => + current?.key === requestKey && current.payload + ? current + : { key: requestKey, payload: null, failed: true }, + ); }); return () => controller.abort(); - }, [launchModel, range, requestKey, setActiveIndexIfChanged, tokenAddress]); + }, [ + launchModel, + range, + refreshKey, + requestKey, + setActiveIndexIfChanged, + tokenAddress, + ]); const chart = useMemo(() => { if (!payload || payload.points.length < 2) return null; @@ -377,10 +398,20 @@ export function TokenPriceChart({ } } + if ( + !shouldRenderPriceHistory({ + loading, + hasChart: Boolean(chart), + range, + }) + ) { + return null; + } + return (
-

Price history

+

Price

{chart ? formatPrice(activePoint?.value ?? chart.current, chart.unit) - : launchModel === "stock-paired" + : !loading || launchModel === "stock-paired" ? "Unavailable" : "Onchain"}

@@ -429,7 +460,7 @@ export function TokenPriceChart({ ) : null}
- {!payload && !failed ? ( + {loading ? ( @@ -442,6 +473,7 @@ export function TokenPriceChart({ role="slider" tabIndex={0} aria-label={`${tokenName} price point`} + aria-orientation="horizontal" aria-valuemin={0} aria-valuemax={chart.points.length - 1} aria-valuenow={activeIndex ?? chart.points.length - 1} @@ -528,8 +560,8 @@ export function TokenPriceChart({
) : ( -
diff --git a/components/token-trade.tsx b/components/token-trade.tsx index 9fed7677..d83364b5 100644 --- a/components/token-trade.tsx +++ b/components/token-trade.tsx @@ -1,12 +1,6 @@ "use client"; -import { - useEffect, - useId, - useRef, - useState, - type FormEvent, -} from "react"; +import { useEffect, useId, useRef, useState, type FormEvent } from "react"; import { formatUnits, getAddress, @@ -53,9 +47,7 @@ export function calculateBuyMaxWei( } const estimatedReserve = - (gasPriceWei * BUY_GAS_RESERVE_UNITS * - BUY_GAS_RESERVE_MULTIPLIER) / - 100n; + (gasPriceWei * BUY_GAS_RESERVE_UNITS * BUY_GAS_RESERVE_MULTIPLIER) / 100n; const reserveWei = estimatedReserve > MIN_BUY_GAS_RESERVE_WEI ? estimatedReserve @@ -63,9 +55,7 @@ export function calculateBuyMaxWei( return { amountWei: - nativeBalanceWei > reserveWei - ? nativeBalanceWei - reserveWei - : 0n, + nativeBalanceWei > reserveWei ? nativeBalanceWei - reserveWei : 0n, reserveWei, }; } @@ -85,9 +75,7 @@ export function calculateTradeUsdValue(input: { } const amount = Number(input.amount); - const tokenUsd = Number( - formatUnits(BigInt(input.tokenPriceUsdWad), 18), - ); + const tokenUsd = Number(formatUnits(BigInt(input.tokenPriceUsdWad), 18)); if ( !Number.isFinite(amount) || !Number.isFinite(tokenUsd) || @@ -100,10 +88,7 @@ export function calculateTradeUsdValue(input: { if (input.side === "sell") { return amount * tokenUsd; } - if ( - !input.tokenPriceEth || - !/^\d+(?:\.\d+)?$/.test(input.tokenPriceEth) - ) { + if (!input.tokenPriceEth || !/^\d+(?:\.\d+)?$/.test(input.tokenPriceEth)) { return null; } @@ -130,9 +115,7 @@ export function calculateEthVolumeUsdValue(input: { const grossVolumeEth = Number(input.grossVolumeEth); const tokenPriceEth = Number(input.tokenPriceEth); - const tokenPriceUsd = Number( - formatUnits(BigInt(input.tokenPriceUsdWad), 18), - ); + const tokenPriceUsd = Number(formatUnits(BigInt(input.tokenPriceUsdWad), 18)); if ( !Number.isFinite(grossVolumeEth) || !Number.isFinite(tokenPriceEth) || @@ -158,6 +141,12 @@ function formatApproximateUsd(value: number | null) { }).format(value)}`; } +function formatBasisPoints(value: number) { + return `${new Intl.NumberFormat("en-US", { + maximumFractionDigits: 2, + }).format(value / 100)}%`; +} + function formatAmountForInput(value: bigint, decimals: number) { return formatUnits(value, decimals).replace(/(?:\.0+|(\.\d+?)0+)$/, "$1"); } @@ -209,10 +198,7 @@ export function buildTokenTradeApiRequest(input: { ) { throw new Error("Token decimals must be between 0 and 255"); } - if ( - !Number.isSafeInteger(input.nowSeconds) || - input.nowSeconds < 0 - ) { + if (!Number.isSafeInteger(input.nowSeconds) || input.nowSeconds < 0) { throw new Error("The current timestamp is invalid"); } if ( @@ -232,10 +218,7 @@ export function buildTokenTradeApiRequest(input: { let amountIn: bigint; try { - amountIn = parseUnits( - input.amount.trim(), - amountDecimals, - ); + amountIn = parseUnits(input.amount.trim(), amountDecimals); } catch { throw new Error("Enter a valid amount"); } @@ -331,8 +314,7 @@ export function TokenTrade({ const amountInputId = useId(); const amountErrorId = useId(); const amountInputRef = useRef(null); - const activeSwapFeeBps = - side === "buy" ? buySwapFeeBps : sellSwapFeeBps; + const activeSwapFeeBps = side === "buy" ? buySwapFeeBps : sellSwapFeeBps; const activeInputAsset = token; const activeInputSymbol = side === "buy" ? "ETH" : symbol; const activeBalanceState = @@ -342,9 +324,7 @@ export function TokenTrade({ ? balanceState : null; const balances = - activeBalanceState?.status === "ready" - ? activeBalanceState.balances - : null; + activeBalanceState?.status === "ready" ? activeBalanceState.balances : null; const approximateUsd = formatApproximateUsd( calculateTradeUsdValue({ side, @@ -355,8 +335,8 @@ export function TokenTrade({ ); const displayBalance = balances ? side === "buy" - ? `${formatWalletBalance(balances.nativeBalanceWei, 18)} ETH` - : `${formatWalletBalance( + ? `Balance ${formatWalletBalance(balances.nativeBalanceWei, 18)} ETH` + : `Balance ${formatWalletBalance( balances.tokenBalanceRaw, tokenDecimals, )} ${activeInputSymbol}` @@ -364,7 +344,7 @@ export function TokenTrade({ ? activeBalanceState?.status === "error" ? "Balance unavailable" : "Loading balance" - : "Wallet not connected"; + : "Connect to view balance"; useEffect(() => { if (!owner) return; @@ -419,10 +399,7 @@ export function TokenTrade({ throw new Error(`No ${activeInputSymbol} balance is available`); } setAmount( - formatAmountForInput( - balances.tokenBalanceRaw, - tokenDecimals, - ), + formatAmountForInput(balances.tokenBalanceRaw, tokenDecimals), ); return; } @@ -572,6 +549,7 @@ export function TokenTrade({ className={styles.tradeForm} onSubmit={prepare} aria-label={`Trade ${symbol}`} + aria-busy={pending} >

Trade ${symbol}

@@ -655,6 +633,17 @@ export function TokenTrade({
+
+
+
Swap fee
+
{formatBasisPoints(activeSwapFeeBps)}
+
+
+
Slippage
+
{formatBasisPoints(slippageBps)}
+
+
+ {error ? (

{approval ? "Approve token" : `Review ${prepared.side}`}

{approval ? (

- One approval is required before this trade. The approval is limited - to this amount. + One approval is required before this trade. The approval is limited to + this amount.

) : null}
@@ -761,7 +751,7 @@ export function PreparedTradeReview({ ) : null}
{launchModel === "deep" ? "Deep fee" : "Swap fee"}
-
{(totalSwapFeeBps / 100).toFixed(2)}%
+
{formatBasisPoints(totalSwapFeeBps)}
{!approval ? (
@@ -808,7 +798,7 @@ export function PreparedTradeReview({ {pending ? "Opening wallet" : approval - ? "Sign approval" + ? "Confirm approval" : `Confirm ${prepared.side}`}
@@ -823,10 +813,7 @@ export function calculatePriceImpactPercent(input: { tokenDecimals: number; tokenPriceEth?: string; }) { - if ( - !input.tokenPriceEth || - !/^\d+(?:\.\d+)?$/.test(input.tokenPriceEth) - ) { + if (!input.tokenPriceEth || !/^\d+(?:\.\d+)?$/.test(input.tokenPriceEth)) { return null; } const spot = Number(input.tokenPriceEth); diff --git a/components/use-live-data-refresh.ts b/components/use-live-data-refresh.ts new file mode 100644 index 00000000..d9254446 --- /dev/null +++ b/components/use-live-data-refresh.ts @@ -0,0 +1,64 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +export const LIVE_DATA_REFRESH_INTERVAL_MS = 5_000; + +export function shouldRefreshLiveData(input: { + visibilityState: DocumentVisibilityState; + lastRefreshAt: number; + now: number; + intervalMs?: number; +}) { + const intervalMs = input.intervalMs ?? LIVE_DATA_REFRESH_INTERVAL_MS; + return ( + input.visibilityState === "visible" && + Number.isSafeInteger(intervalMs) && + intervalMs >= 1_000 && + input.now - input.lastRefreshAt >= intervalMs + ); +} + +export function useLiveDataRefresh( + input: Readonly<{ + enabled?: boolean; + intervalMs?: number; + }> = {}, +) { + const enabled = input.enabled ?? true; + const intervalMs = input.intervalMs ?? LIVE_DATA_REFRESH_INTERVAL_MS; + const [refreshKey, setRefreshKey] = useState(0); + const lastRefreshAt = useRef(0); + + useEffect(() => { + if (!enabled) return; + lastRefreshAt.current = Date.now(); + + function refreshIfDue() { + const now = Date.now(); + if ( + !shouldRefreshLiveData({ + visibilityState: document.visibilityState, + lastRefreshAt: lastRefreshAt.current, + now, + intervalMs, + }) + ) { + return; + } + lastRefreshAt.current = now; + setRefreshKey((value) => value + 1); + } + + const interval = window.setInterval(refreshIfDue, intervalMs); + document.addEventListener("visibilitychange", refreshIfDue); + window.addEventListener("focus", refreshIfDue); + return () => { + window.clearInterval(interval); + document.removeEventListener("visibilitychange", refreshIfDue); + window.removeEventListener("focus", refreshIfDue); + }; + }, [enabled, intervalMs]); + + return refreshKey; +} diff --git a/config/data-pipeline-bootstrap.v1.json b/config/data-pipeline-bootstrap.v1.json new file mode 100644 index 00000000..fd6d9e46 --- /dev/null +++ b/config/data-pipeline-bootstrap.v1.json @@ -0,0 +1,192 @@ +{ + "schemaVersion": 1, + "chainId": 1, + "sourceGroup": "core", + "catalogVersion": "programmable-mainnet-bootstrap-v1", + "createdAt": "2026-08-01T09:00:00.000Z", + "releaseBindingPath": "config/data-pipeline-release.v1.json", + "dynamicBindingSpecs": { + "classic-reward-vault-v1": { + "factoryConfigurationField": "configurationHash", + "normalizedRuntimeCodeHash": "0x2acc1dab5def24dc4bf08058e78c50387f639eb37b3098a98c9a20c7f8ed2794", + "immutableReferencesCommitment": "0x907697f82d7893c5208d35481bff0be82526809b29f52b3e83a1bcda15be583d", + "runtimeCodeLength": "6543", + "bindings": [ + { "ordinal": "0", "offset": "362", "length": "32", "source": "constant", "encoding": "address", "value": "0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90" }, + { "ordinal": "1", "offset": "510", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "configurationHash" }, + { "ordinal": "2", "offset": "664", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "poolId" }, + { "ordinal": "3", "offset": "884", "length": "32", "source": "constant", "encoding": "address", "value": "0x0000000000000000000000009746469cd79fddc5aa7218e7dd51c829ee518c0c" }, + { "ordinal": "4", "offset": "1092", "length": "32", "source": "constant", "encoding": "address", "value": "0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90" }, + { "ordinal": "5", "offset": "1195", "length": "32", "source": "factory_event", "encoding": "address", "field": "feeHook" }, + { "ordinal": "6", "offset": "1941", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "poolId" }, + { "ordinal": "7", "offset": "2469", "length": "32", "source": "constant", "encoding": "address", "value": "0x0000000000000000000000009746469cd79fddc5aa7218e7dd51c829ee518c0c" }, + { "ordinal": "8", "offset": "3048", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "poolId" }, + { "ordinal": "9", "offset": "3601", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "poolId" }, + { "ordinal": "10", "offset": "3645", "length": "32", "source": "factory_event", "encoding": "address", "field": "feeHook" }, + { "ordinal": "11", "offset": "3910", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "poolId" }, + { "ordinal": "12", "offset": "4012", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "configurationHash" } + ] + }, + "quote-asset-fee-split-vault-v1": { + "factoryConfigurationField": null, + "normalizedRuntimeCodeHash": "0x4bdda3a916596d3643f87f95ec401122586759b29e360891565754b629b2c846", + "immutableReferencesCommitment": "0xb7a44c4e10798e9027247e4d5e7ac191d3f7b50b8d81a4746ffbd6a337a42ec0", + "runtimeCodeLength": "3352", + "bindings": [ + { "ordinal": "0", "offset": "286", "length": "32", "source": "deferred_allocation_evidence", "encoding": "bytes", "evidenceRole": "configuration_hash" }, + { "ordinal": "1", "offset": "404", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "poolId" }, + { "ordinal": "2", "offset": "479", "length": "32", "source": "deferred_allocation_evidence", "encoding": "bytes", "evidenceRole": "beneficiary_count" }, + { "ordinal": "3", "offset": "610", "length": "32", "source": "constant", "encoding": "address", "value": "0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90" }, + { "ordinal": "4", "offset": "712", "length": "32", "source": "factory_event", "encoding": "address", "field": "feeHook" }, + { "ordinal": "5", "offset": "751", "length": "32", "source": "factory_event", "encoding": "address", "field": "quoteAsset" }, + { "ordinal": "6", "offset": "1258", "length": "32", "source": "factory_event", "encoding": "address", "field": "quoteAsset" }, + { "ordinal": "7", "offset": "1411", "length": "32", "source": "factory_event", "encoding": "bytes", "field": "poolId" }, + { "ordinal": "8", "offset": "1462", "length": "32", "source": "factory_event", "encoding": "address", "field": "feeHook" }, + { "ordinal": "9", "offset": "1604", "length": "32", "source": "factory_event", "encoding": "address", "field": "quoteAsset" }, + { "ordinal": "10", "offset": "2018", "length": "32", "source": "factory_event", "encoding": "address", "field": "quoteAsset" }, + { "ordinal": "11", "offset": "2059", "length": "32", "source": "factory_event", "encoding": "address", "field": "quoteAsset" }, + { "ordinal": "12", "offset": "2441", "length": "32", "source": "deferred_allocation_evidence", "encoding": "bytes", "evidenceRole": "beneficiary_count" } + ] + } + }, + "releases": [ + { + "releaseId": "classic-v2", + "modelId": "classic", + "activation": { "epochNumber": 1, "expectedGeneration": 0, "nextGeneration": 1 }, + "deploymentManifestPath": "contracts/deployments/mainnet-classic-v2.json", + "sources": [ + { "contractName": "ClassicV2Hook", "sourceRole": "hook", "sourceType": "ethereum_contract", "artifact": "EthCreatorFeeHookV2", "deploymentKey": "feeHook", "recoverySelector": null }, + { "contractName": "ClassicV2Launcher", "sourceRole": "launcher", "sourceType": "ethereum_contract", "artifact": "MemeLaunchV1", "deploymentKey": "memeLauncher", "recoverySelector": "0x19b3284b" } + ], + "dynamicSources": [], + "launchRequirements": [ + { "occurrenceRole": "launcher", "eventType": "MemeTokenLaunched", "requiredWhen": "always" } + ] + }, + { + "releaseId": "classic-v3", + "modelId": "classic", + "activation": { "epochNumber": 1, "expectedGeneration": 0, "nextGeneration": 1 }, + "deploymentManifestPath": "contracts/deployments/mainnet-classic-v3.json", + "sources": [ + { "contractName": "ClassicV3RewardVaultFactory", "sourceRole": "vault_factory", "sourceType": "ethereum_contract", "artifact": "ClassicRewardVaultFactoryV1", "deploymentKey": "rewardVaultFactory", "recoverySelector": null }, + { "contractName": "ClassicV3VestingWalletFactory", "sourceRole": "vesting_factory", "sourceType": "ethereum_contract", "artifact": "ClassicInitialBuyVestingWalletFactoryV1", "deploymentKey": "initialBuyVestingWalletFactory", "recoverySelector": null }, + { "contractName": "ClassicV3Hook", "sourceRole": "hook", "sourceType": "ethereum_contract", "artifact": "EthCreatorFeeHookV3", "deploymentKey": "feeHook", "recoverySelector": null }, + { "contractName": "ClassicV3Launcher", "sourceRole": "launcher", "sourceType": "ethereum_contract", "artifact": "MemeLaunchV2", "deploymentKey": "launcher", "recoverySelector": "0xbf388406" } + ], + "dynamicSources": [ + { + "contractName": "ClassicV3RewardVault", + "artifact": "ClassicRewardVaultV1", + "parentContractName": "ClassicV3RewardVaultFactory", + "parentSourceRole": "vault_factory", + "factoryEventType": "ClassicRewardVaultDeployed", + "deployedAddressField": "vault", + "deployedSourceRole": "reward_vault", + "bindingSpec": "classic-reward-vault-v1", + "factoryConfigurationField": "configurationHash", + "bindingPolicy": "factory-event-and-constants" + } + ], + "launchRequirements": [ + { "occurrenceRole": "launcher", "eventType": "MemeTokenLaunchedV2", "requiredWhen": "always" }, + { "occurrenceRole": "vault_factory", "eventType": "ClassicRewardVaultDeployed", "requiredWhen": "reward_vault" }, + { "occurrenceRole": "vesting_factory", "eventType": "ClassicInitialBuyVestingWalletDeployed", "requiredWhen": "locked_custody" } + ] + }, + { + "releaseId": "stock-paired-v1", + "modelId": "stock-paired", + "activation": { "epochNumber": 1, "expectedGeneration": 0, "nextGeneration": 1 }, + "deploymentManifestPath": "contracts/deployments/mainnet-stock-paired-v1.json", + "sources": [ + { "contractName": "StockV1Launcher", "sourceRole": "launcher", "sourceType": "ethereum_contract", "artifact": "StockPairedLaunchV1", "deploymentKey": "launcher", "recoverySelector": "0x0f6d2003" }, + { "contractName": "StockV1EthCoordinator", "sourceRole": "coordinator", "sourceType": "ethereum_contract", "artifact": "StockPairedEthLaunchCoordinatorV1", "deploymentKey": "ethLaunchCoordinator", "recoverySelector": "0xdfd98d51" }, + { "contractName": "StockV1Hook", "sourceRole": "hook", "sourceType": "ethereum_contract", "artifact": "QuoteAssetCreatorFeeHookV1", "deploymentKey": "feeHook", "recoverySelector": null }, + { "contractName": "StockV1RewardVaultFactory", "sourceRole": "vault_factory", "sourceType": "ethereum_contract", "artifact": "QuoteAssetFeeSplitVaultFactoryV1", "deploymentKey": "feeSplitVaultFactory", "recoverySelector": null } + ], + "dynamicSources": [ + { + "contractName": "StockV1RewardVault", + "artifact": "QuoteAssetFeeSplitVaultV1", + "parentContractName": "StockV1RewardVaultFactory", + "parentSourceRole": "vault_factory", + "factoryEventType": "QuoteAssetFeeSplitVaultDeployed", + "deployedAddressField": "vault", + "deployedSourceRole": "reward_vault", + "bindingSpec": "quote-asset-fee-split-vault-v1", + "factoryConfigurationField": null, + "bindingPolicy": "factory-event-constants-and-deferred-allocation" + } + ], + "launchRequirements": [ + { "occurrenceRole": "launcher", "eventType": "StockPairedTokenLaunched", "requiredWhen": "always" }, + { "occurrenceRole": "vault_factory", "eventType": "QuoteAssetFeeSplitVaultDeployed", "requiredWhen": "reward_vault" }, + { "occurrenceRole": "coordinator", "eventType": "StockPairedEthTokenLaunched", "requiredWhen": "eth_funded" } + ] + }, + { + "releaseId": "stock-paired-v2", + "modelId": "stock-paired", + "activation": { "epochNumber": 1, "expectedGeneration": 0, "nextGeneration": 1 }, + "deploymentManifestPath": "contracts/deployments/mainnet-stock-paired-v2.json", + "sources": [ + { "contractName": "StockV2Launcher", "sourceRole": "launcher", "sourceType": "ethereum_contract", "artifact": "StockPairedLaunchV1", "deploymentKey": "launcher", "recoverySelector": "0x0f6d2003" }, + { "contractName": "StockV2EthCoordinator", "sourceRole": "coordinator", "sourceType": "ethereum_contract", "artifact": "StockPairedEthLaunchCoordinatorV1", "deploymentKey": "ethLaunchCoordinator", "recoverySelector": "0xdfd98d51" }, + { "contractName": "StockV2V3Hook", "sourceRole": "hook", "sourceType": "ethereum_contract", "artifact": "QuoteAssetCreatorFeeHookV1", "deploymentKey": "feeHook", "recoverySelector": null }, + { "contractName": "StockV2V3RewardVaultFactory", "sourceRole": "vault_factory", "sourceType": "ethereum_contract", "artifact": "QuoteAssetFeeSplitVaultFactoryV1", "deploymentKey": "feeSplitVaultFactory", "recoverySelector": null } + ], + "dynamicSources": [ + { + "contractName": "StockV2V3RewardVault", + "artifact": "QuoteAssetFeeSplitVaultV1", + "parentContractName": "StockV2V3RewardVaultFactory", + "parentSourceRole": "vault_factory", + "factoryEventType": "QuoteAssetFeeSplitVaultDeployed", + "deployedAddressField": "vault", + "deployedSourceRole": "reward_vault", + "bindingSpec": "quote-asset-fee-split-vault-v1", + "factoryConfigurationField": null, + "bindingPolicy": "factory-event-constants-and-deferred-allocation" + } + ], + "launchRequirements": [ + { "occurrenceRole": "launcher", "eventType": "StockPairedTokenLaunched", "requiredWhen": "always" }, + { "occurrenceRole": "vault_factory", "eventType": "QuoteAssetFeeSplitVaultDeployed", "requiredWhen": "reward_vault" }, + { "occurrenceRole": "coordinator", "eventType": "StockPairedEthTokenLaunched", "requiredWhen": "eth_funded" } + ] + }, + { + "releaseId": "stock-paired-v3", + "modelId": "stock-paired", + "activation": { "epochNumber": 1, "expectedGeneration": 0, "nextGeneration": 1 }, + "deploymentManifestPath": "contracts/deployments/mainnet-stock-paired-v3.json", + "sources": [ + { "contractName": "StockV3Launcher", "sourceRole": "launcher", "sourceType": "ethereum_contract", "artifact": "StockPairedLaunchV3", "deploymentKey": "launcher", "recoverySelector": "0x0f6d2003" }, + { "contractName": "StockV3EthCoordinator", "sourceRole": "coordinator", "sourceType": "ethereum_contract", "artifact": "StockPairedEthLaunchCoordinatorV3", "deploymentKey": "ethLaunchCoordinator", "recoverySelector": "0xdfd98d51" }, + { "contractName": "StockV2V3Hook", "sourceRole": "hook", "sourceType": "ethereum_contract", "artifact": "QuoteAssetCreatorFeeHookV1", "deploymentKey": "feeHook", "recoverySelector": null }, + { "contractName": "StockV2V3RewardVaultFactory", "sourceRole": "vault_factory", "sourceType": "ethereum_contract", "artifact": "QuoteAssetFeeSplitVaultFactoryV1", "deploymentKey": "feeSplitVaultFactory", "recoverySelector": null } + ], + "dynamicSources": [ + { + "contractName": "StockV2V3RewardVault", + "artifact": "QuoteAssetFeeSplitVaultV1", + "parentContractName": "StockV2V3RewardVaultFactory", + "parentSourceRole": "vault_factory", + "factoryEventType": "QuoteAssetFeeSplitVaultDeployed", + "deployedAddressField": "vault", + "deployedSourceRole": "reward_vault", + "bindingSpec": "quote-asset-fee-split-vault-v1", + "factoryConfigurationField": null, + "bindingPolicy": "factory-event-constants-and-deferred-allocation" + } + ], + "launchRequirements": [ + { "occurrenceRole": "launcher", "eventType": "StockPairedTokenLaunched", "requiredWhen": "always" }, + { "occurrenceRole": "vault_factory", "eventType": "QuoteAssetFeeSplitVaultDeployed", "requiredWhen": "reward_vault" }, + { "occurrenceRole": "coordinator", "eventType": "StockPairedEthTokenLaunched", "requiredWhen": "eth_funded" } + ] + } + ] +} diff --git a/config/data-pipeline-canonical-fingerprint.v1.json b/config/data-pipeline-canonical-fingerprint.v1.json new file mode 100644 index 00000000..164fec7a --- /dev/null +++ b/config/data-pipeline-canonical-fingerprint.v1.json @@ -0,0 +1,577 @@ +{ + "format": "programmable-canonical-fingerprint-fixture", + "encoding_version": 1, + "hash": "ethereum-keccak-256", + "integer_encoding": "unsigned fixed-width big-endian", + "variable_encoding": "uint32 big-endian byte length followed by exact bytes", + "nullable_encoding": "0x00 for null; 0x01 followed by ordinary encoding for present", + "array_encoding": "uint32 big-endian count followed by elements in original order", + "json_encoding": "RFC 8785 JCS; uint256 values are canonical decimal strings", + "field_schemas": { + "occurrence": [ + [ + "chain_id", + "u64" + ], + [ + "transaction_hash", + "bytes32" + ], + [ + "receipt_log_ordinal", + "u32" + ], + [ + "block_number", + "u64" + ], + [ + "block_hash", + "bytes32" + ], + [ + "transaction_index", + "u32" + ], + [ + "block_global_log_index", + "u32" + ], + [ + "source_address", + "bytes20" + ], + [ + "event_signature", + "bytes32" + ], + [ + "ordered_topics", + "array" + ], + [ + "raw_data", + "varbytes" + ], + [ + "decoded_payload", + "varutf8" + ], + [ + "payload_hash", + "bytes32" + ], + [ + "decoder_version", + "varutf8" + ], + [ + "abi_event_set_commitment", + "bytes32" + ], + [ + "release_id", + "varutf8" + ], + [ + "model_id", + "varutf8" + ], + [ + "envio_candidate_id", + "varutf8" + ], + [ + "provider_cursor", + "varutf8" + ], + [ + "block_timestamp_unix", + "u64" + ] + ], + "allocation": [ + [ + "chain_id", + "u64" + ], + [ + "release_id", + "varutf8" + ], + [ + "model_id", + "varutf8" + ], + [ + "vault", + "bytes20" + ], + [ + "factory_transaction_hash", + "bytes32" + ], + [ + "factory_receipt_log_ordinal", + "u32" + ], + [ + "factory_block_hash", + "bytes32" + ], + [ + "creation_block_number", + "u64" + ], + [ + "creation_transaction_index", + "u32" + ], + [ + "ordered_beneficiaries", + "array" + ], + [ + "ordered_shares_bps", + "array" + ], + [ + "allocation_hash", + "bytes32" + ], + [ + "configuration_hash", + "bytes32" + ], + [ + "active_configuration_hash", + "nullable" + ], + [ + "artifact_creation_code_commitment", + "bytes32" + ], + [ + "required_occurrences", + "array" + ] + ], + "evidence": [ + [ + "allocation_fingerprint", + "bytes32" + ], + [ + "recovery_method", + "varutf8" + ], + [ + "evidence_version", + "varutf8" + ], + [ + "top_level_destination", + "nullable" + ], + [ + "method_selector", + "nullable" + ], + [ + "transaction_input_hash", + "nullable" + ], + [ + "constructor_arguments_commitment", + "bytes32" + ], + [ + "local_init_code_hash", + "bytes32" + ], + [ + "create2_salt", + "bytes32" + ], + [ + "local_create2_address", + "bytes20" + ], + [ + "historical_enrichment_status", + "varutf8" + ], + [ + "getter_block_hash", + "nullable" + ], + [ + "getter_result_hash_a", + "nullable" + ], + [ + "getter_result_hash_b", + "nullable" + ], + [ + "predict_result_hash_a", + "nullable" + ], + [ + "predict_result_hash_b", + "nullable" + ], + [ + "predicted_vault_a", + "nullable" + ], + [ + "predicted_vault_b", + "nullable" + ], + [ + "selected_rpc_result_hash_a", + "bytes32" + ], + [ + "selected_rpc_result_hash_b", + "bytes32" + ], + [ + "selected_rpc_transaction_receipt_hash_a", + "nullable" + ], + [ + "selected_rpc_transaction_receipt_hash_b", + "nullable" + ], + [ + "extra_note", + "nullable" + ], + [ + "required_occurrence_fingerprints", + "array" + ] + ] + }, + "sentinel_vectors": [ + { + "name": "evidence_null_marker", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a76310000", + "expected_keccak256": "0x945cecbc3714b84a54cebb706446ed17b531a8e53703a90774a51797989f2f63" + }, + { + "name": "evidence_present_empty", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a7631000100000000", + "expected_keccak256": "0x080646fe0f81fe8dc11056eec4e4690a9351a30b435968db0571673a18efb934" + }, + { + "name": "allocation_order_ab", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000002111111111111111111111111111111111111111122222222222222222222222222222222222222220000000217700fa0", + "expected_keccak256": "0xe9937a36b49028672572c67a28640c69473ebb5dfafed3eb04e33867e177243c" + }, + { + "name": "allocation_order_ba", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a7631000000000222222222222222222222222222222222222222221111111111111111111111111111111111111111000000020fa01770", + "expected_keccak256": "0xf5a3b18d2b3d8c9ae9a520109fd0f4dac92ff19f1ef55fca8aa91fce98259059" + }, + { + "name": "occurrence_two_topics_indexed_only", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a6f6363757272656e63653a76310000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000", + "expected_keccak256": "0x262dc3ea7b1dac050cfca3aa67f6881de42706ea35349b88b7d14e860d64ff74" + } + ], + "vectors": [ + { + "name": "occurrence_all_fields_v1", + "domain": "occurrence", + "input": { + "chain_id": "1", + "transaction_hash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "receipt_log_ordinal": "0", + "block_number": "25639596", + "block_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "transaction_index": "3", + "block_global_log_index": "7", + "source_address": "0x3333333333333333333333333333333333333333", + "event_signature": "0x4444444444444444444444444444444444444444444444444444444444444444", + "ordered_topics": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "raw_data": "0x010203", + "decoded_payload": { + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "creator": "0x3333333333333333333333333333333333333333", + "flags": [ + true, + false + ], + "nested": { + "b": "two", + "a": "one" + } + }, + "payload_hash": "0x5555555555555555555555555555555555555555555555555555555555555555", + "decoder_version": "projector-v1.0.0", + "abi_event_set_commitment": "0x6666666666666666666666666666666666666666666666666666666666666666", + "release_id": "classic-v3", + "model_id": "classic-v3", + "envio_candidate_id": "1:22:11:7", + "provider_cursor": "25639596:7", + "block_timestamp_unix": "1785463200" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a6f6363757272656e63653a76310000000000000000011111111111111111111111111111111111111111111111111111111111111111000000000000000001873aac222222222222222222222222222222222222222222222222222222222222222200000003000000073333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444400000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000003010203000000c67b22616d6f756e74223a22313135373932303839323337333136313935343233353730393835303038363837393037383533323639393834363635363430353634303339343537353834303037393133313239363339393335222c2263726561746f72223a22307833333333333333333333333333333333333333333333333333333333333333333333333333333333222c22666c616773223a5b747275652c66616c73655d2c226e6573746564223a7b2261223a226f6e65222c2262223a2274776f227d7d55555555555555555555555555555555555555555555555555555555555555550000001070726f6a6563746f722d76312e302e3066666666666666666666666666666666666666666666666666666666666666660000000a636c61737369632d76330000000a636c61737369632d763300000009313a32323a31313a370000000a32353633393539363a37000000006a6c01a0", + "expected_keccak256": "0x6fe25eb0a62ea86736aa134ada719976b6166844b98d83b56d478ae409956955" + }, + { + "name": "occurrence_indexed_only_empty_data_v1", + "domain": "occurrence", + "input": { + "chain_id": "1", + "transaction_hash": "0x1212121212121212121212121212121212121212121212121212121212121212", + "receipt_log_ordinal": "0", + "block_number": "25639596", + "block_hash": "0x2323232323232323232323232323232323232323232323232323232323232323", + "transaction_index": "3", + "block_global_log_index": "7", + "source_address": "0x3333333333333333333333333333333333333333", + "event_signature": "0x4444444444444444444444444444444444444444444444444444444444444444", + "ordered_topics": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "raw_data": "0x", + "decoded_payload": { + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "creator": "0x3333333333333333333333333333333333333333", + "flags": [ + true, + false + ], + "nested": { + "b": "two", + "a": "one" + } + }, + "payload_hash": "0x5656565656565656565656565656565656565656565656565656565656565656", + "decoder_version": "projector-v1.0.0", + "abi_event_set_commitment": "0x6666666666666666666666666666666666666666666666666666666666666666", + "release_id": "classic-v3", + "model_id": "classic-v3", + "envio_candidate_id": "1:23:12:7", + "provider_cursor": "25639596:7", + "block_timestamp_unix": "1785463200" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a6f6363757272656e63653a76310000000000000000011212121212121212121212121212121212121212121212121212121212121212000000000000000001873aac232323232323232323232323232323232323232323232323232323232323232300000003000000073333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444400000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000c67b22616d6f756e74223a22313135373932303839323337333136313935343233353730393835303038363837393037383533323639393834363635363430353634303339343537353834303037393133313239363339393335222c2263726561746f72223a22307833333333333333333333333333333333333333333333333333333333333333333333333333333333222c22666c616773223a5b747275652c66616c73655d2c226e6573746564223a7b2261223a226f6e65222c2262223a2274776f227d7d56565656565656565656565656565656565656565656565656565656565656560000001070726f6a6563746f722d76312e302e3066666666666666666666666666666666666666666666666666666666666666660000000a636c61737369632d76330000000a636c61737369632d763300000009313a32333a31323a370000000a32353633393539363a37000000006a6c01a0", + "expected_keccak256": "0xef8ea5dee7777948f98130615b8f4ab9fd1d316e243b6c3a745c88043ef5d2d8" + }, + { + "name": "allocation_order_ab_v1", + "domain": "allocation", + "input": { + "chain_id": "1", + "release_id": "classic-v3", + "model_id": "classic-v3", + "vault": "0x7777777777777777777777777777777777777777", + "factory_transaction_hash": "0x8888888888888888888888888888888888888888888888888888888888888888", + "factory_receipt_log_ordinal": "2", + "factory_block_hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "creation_block_number": "25639600", + "creation_transaction_index": "4", + "ordered_beneficiaries": [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222" + ], + "ordered_shares_bps": [ + "6000", + "4000" + ], + "allocation_hash": "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "configuration_hash": "0xa2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2", + "active_configuration_hash": "0xa3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "artifact_creation_code_commitment": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "required_occurrences": [ + { + "transaction_hash": "0xa5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + "receipt_log_ordinal": "1", + "block_hash": "0xa6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6", + "role": "launcher" + }, + { + "transaction_hash": "0xa7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7", + "receipt_log_ordinal": "1", + "block_hash": "0xa8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8", + "role": "factory" + }, + { + "transaction_hash": "0xa9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9", + "receipt_log_ordinal": "1", + "block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "role": "hook" + } + ] + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000000000000010000000a636c61737369632d76330000000a636c61737369632d7633777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000299999999999999999999999999999999999999999999999999999999999999990000000001873ab00000000400000002111111111111111111111111111111111111111122222222222222222222222222222222222222220000000217700fa0a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a201a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a400000003a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a500000001a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6000000086c61756e63686572a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a700000001a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a800000007666163746f7279a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a900000001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000004686f6f6b", + "expected_keccak256": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0" + }, + { + "name": "allocation_order_ba_v1", + "domain": "allocation", + "input": { + "chain_id": "1", + "release_id": "classic-v3", + "model_id": "classic-v3", + "vault": "0x7777777777777777777777777777777777777777", + "factory_transaction_hash": "0x8888888888888888888888888888888888888888888888888888888888888888", + "factory_receipt_log_ordinal": "2", + "factory_block_hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "creation_block_number": "25639600", + "creation_transaction_index": "4", + "ordered_beneficiaries": [ + "0x2222222222222222222222222222222222222222", + "0x1111111111111111111111111111111111111111" + ], + "ordered_shares_bps": [ + "4000", + "6000" + ], + "allocation_hash": "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "configuration_hash": "0xa2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2", + "active_configuration_hash": "0xa3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "artifact_creation_code_commitment": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "required_occurrences": [ + { + "transaction_hash": "0xa5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + "receipt_log_ordinal": "1", + "block_hash": "0xa6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6", + "role": "launcher" + }, + { + "transaction_hash": "0xa7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7", + "receipt_log_ordinal": "1", + "block_hash": "0xa8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8", + "role": "factory" + }, + { + "transaction_hash": "0xa9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9", + "receipt_log_ordinal": "1", + "block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "role": "hook" + } + ] + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000000000000010000000a636c61737369632d76330000000a636c61737369632d7633777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000299999999999999999999999999999999999999999999999999999999999999990000000001873ab0000000040000000222222222222222222222222222222222222222221111111111111111111111111111111111111111000000020fa01770a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a201a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a400000003a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a500000001a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6000000086c61756e63686572a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a700000001a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a800000007666163746f7279a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a900000001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000004686f6f6b", + "expected_keccak256": "0x1578cf70b13f4382f5b68923f4034573bcaf233970306c835db13c28c746887e" + }, + { + "name": "evidence_all_fields_v1", + "domain": "evidence", + "input": { + "allocation_fingerprint": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0", + "recovery_method": "launcher_calldata", + "evidence_version": "seed-verifier-v1.0.1", + "top_level_destination": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "method_selector": "0xbf388406", + "transaction_input_hash": "0xb3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3", + "constructor_arguments_commitment": "0xc0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "local_init_code_hash": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "create2_salt": "0xc2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2", + "local_create2_address": "0x7777777777777777777777777777777777777777", + "historical_enrichment_status": "matched", + "getter_block_hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "getter_result_hash_a": "0xb5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5", + "getter_result_hash_b": "0xb5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5", + "predict_result_hash_a": "0xb6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6", + "predict_result_hash_b": "0xb6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6", + "selected_rpc_result_hash_a": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_result_hash_b": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_transaction_receipt_hash_a": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "selected_rpc_transaction_receipt_hash_b": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "extra_note": "complete", + "required_occurrence_fingerprints": [ + "0x5252525252525252525252525252525252525252525252525252525252525252", + "0x5454545454545454545454545454545454545454545454545454545454545454", + "0x5656565656565656565656565656565656565656565656565656565656565656" + ], + "predicted_vault_a": "0x7777777777777777777777777777777777777777", + "predicted_vault_b": "0x7777777777777777777777777777777777777777" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3101b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c27777777777777777777777777777777777777777000000076d61746368656401999999999999999999999999999999999999999999999999999999999999999901b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b501b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b501b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b601b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6017777777777777777777777777777777777777777017777777777777777777777777777777777777777b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b80100000008636f6d706c65746500000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656", + "expected_keccak256": "0xd5eefc2d52e51f7e1b058450f49d5d153f56e1f646233699aa92e5edc15e335e" + }, + { + "name": "evidence_null_optional_v1", + "domain": "evidence", + "input": { + "allocation_fingerprint": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0", + "recovery_method": "launcher_calldata", + "evidence_version": "seed-verifier-v1.0.0", + "top_level_destination": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "method_selector": "0xbf388406", + "transaction_input_hash": "0xb3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3", + "constructor_arguments_commitment": "0xc0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "local_init_code_hash": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "create2_salt": "0xc2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2", + "local_create2_address": "0x7777777777777777777777777777777777777777", + "historical_enrichment_status": "unavailable", + "getter_block_hash": null, + "getter_result_hash_a": null, + "getter_result_hash_b": null, + "predict_result_hash_a": null, + "predict_result_hash_b": null, + "selected_rpc_result_hash_a": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_result_hash_b": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_transaction_receipt_hash_a": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "selected_rpc_transaction_receipt_hash_b": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "extra_note": null, + "required_occurrence_fingerprints": [ + "0x5252525252525252525252525252525252525252525252525252525252525252", + "0x5454545454545454545454545454545454545454545454545454545454545454", + "0x5656565656565656565656565656565656565656565656565656565656565656" + ], + "predicted_vault_a": null, + "predicted_vault_b": null + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3001b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c277777777777777777777777777777777777777770000000b756e617661696c61626c6500000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b80000000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656", + "expected_keccak256": "0x27996b52c56c657c4a8ef3894a50dceb3348f299afc64adcfc0a202f5713b914" + }, + { + "name": "evidence_present_empty_v1", + "domain": "evidence", + "input": { + "allocation_fingerprint": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0", + "recovery_method": "launcher_calldata", + "evidence_version": "seed-verifier-v1.0.0", + "top_level_destination": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "method_selector": "0xbf388406", + "transaction_input_hash": "0xb3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3", + "constructor_arguments_commitment": "0xc0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "local_init_code_hash": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "create2_salt": "0xc2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2", + "local_create2_address": "0x7777777777777777777777777777777777777777", + "historical_enrichment_status": "unavailable", + "getter_block_hash": null, + "getter_result_hash_a": null, + "getter_result_hash_b": null, + "predict_result_hash_a": null, + "predict_result_hash_b": null, + "selected_rpc_result_hash_a": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_result_hash_b": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_transaction_receipt_hash_a": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "selected_rpc_transaction_receipt_hash_b": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "extra_note": "", + "required_occurrence_fingerprints": [ + "0x5252525252525252525252525252525252525252525252525252525252525252", + "0x5454545454545454545454545454545454545454545454545454545454545454", + "0x5656565656565656565656565656565656565656565656565656565656565656" + ], + "predicted_vault_a": null, + "predicted_vault_b": null + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3001b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c277777777777777777777777777777777777777770000000b756e617661696c61626c6500000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8010000000000000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656", + "expected_keccak256": "0xbd4b5c41c44f9d85419a04918edb205b72c0a49a794cf43664c9509e803579fa" + } + ] +} diff --git a/config/data-pipeline-envio-candidate.v1.json b/config/data-pipeline-envio-candidate.v1.json new file mode 100644 index 00000000..2781bf52 --- /dev/null +++ b/config/data-pipeline-envio-candidate.v1.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "status": "deployed-synced-audited-not-promoted", + "deploymentLabel": "production-7f24e63", + "graphqlEndpoint": "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + "sourceCommit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + "handlerSha256": "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + "sourceRegistrySha256": "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51, + "redactedIdentity": "envio:production-7f24e63", + "deploymentCommitment": "0xa4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259", + "schemaCommitment": "0x5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1", + "audit": { + "entityCount": 265, + "entityCounts": { + "ClassicLaunch": 27, + "ClassicPool": 186, + "StockLaunch": 1, + "StockPool": 8, + "Token": 43 + }, + "coordinatorRepairCount": 52, + "baselineEvidenceSha256": "0x337edaae07f61790dabebfffe2b7b1edb6c5144736386fa0604ebf58c856dfdc", + "candidateAuditEvidenceSha256": "0x331a7b8fe6d2fc57b5a2fed322f11b6bce40c7f3c3cef227086e1262934b9a7a" + }, + "policy": { + "databaseMode": "candidate-only", + "legacyProductionDeploymentRegistered": false, + "publicationAllowedBeforePromotion": false, + "promotion": "atomic-attestation-required" + } +} diff --git a/config/data-pipeline-release.v1.json b/config/data-pipeline-release.v1.json new file mode 100644 index 00000000..36631ce0 --- /dev/null +++ b/config/data-pipeline-release.v1.json @@ -0,0 +1,177 @@ +{ + "schemaVersion": 1, + "chainId": 1, + "startBlock": 25624130, + "confirmations": 12, + "envio": { + "deploymentLabel": "production-7f24e63", + "graphqlEndpoint": "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + "schemaVersion": "1", + "sourceCommit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + "handlerSha256": "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + "sourceRegistrySha256": "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51 + }, + "uniswapV4Subgraph": { + "subgraphId": "DiYPVdygkfjDWhbxGSqAQxwBKmfKnkWQojqeM2rkLb3G", + "deployment": "QmZsgJLiLQKpb8hxTmQ5LWyrFVvfWzVaL4WK8dfFBn7EeK" + }, + "sources": [ + { + "contractName": "ClassicV2Hook", + "address": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "startBlock": 25624130, + "runtimeCodeHash": "0x274e29fb8d19f0607533ac7582827db0236ab546bb393d52049229b2ffe74381" + }, + { + "contractName": "ClassicV2Launcher", + "address": "0xd240d06f8586eb799f20056054e5b527405e6bad", + "startBlock": 25624131, + "runtimeCodeHash": "0xd229555c79c61874549a1991c43df172104e1db3087ba8fca8804675b7440d36" + }, + { + "contractName": "ClassicV3RewardVaultFactory", + "address": "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + "startBlock": 25639538, + "runtimeCodeHash": "0x874ec76f396807bfcbbdd88cc2fd534f10201242ad0479a05fe5d2ee937616ee" + }, + { + "contractName": "ClassicV3VestingWalletFactory", + "address": "0xde21b9c0cc0afdb9be20e8236113f066bb8c66f4", + "startBlock": 25639564, + "runtimeCodeHash": "0x13b7578a8abd0bc0ba724b5815d9bd0aff0d07c2677c00d2577004e8c1f6d5f4" + }, + { + "contractName": "ClassicV3Hook", + "address": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "startBlock": 25639591, + "runtimeCodeHash": "0x3eba781023d3146ed9b502ac5b402d39cea4c34a14f64c878cb9ea62149590f1" + }, + { + "contractName": "ClassicV3Launcher", + "address": "0xc3bd04aac2fb2ba58efd7eb673e544e0b80de770", + "startBlock": 25639596, + "runtimeCodeHash": "0x9cc9723456c471d90ac838c02fa4fc47ed4b7e82c85358e71deec978c48d2dc8" + }, + { + "contractName": "StockV1Launcher", + "address": "0x195750f33cad5ef2df857a53226b421297a1e79e", + "startBlock": 25637469, + "runtimeCodeHash": "0xbd6f60760341db3d4ed31118676ab4342d0868cff42cc6f8205d877086fbce65" + }, + { + "contractName": "StockV1EthCoordinator", + "address": "0xfa5f17389ca28d071781d59750b32c842ab6a54b", + "startBlock": 25637469, + "runtimeCodeHash": "0xe6cff454f284798017864eefdb9c510c159a1185479d49ba1ad7f0a2d3c7b645" + }, + { + "contractName": "StockV1Hook", + "address": "0x7773d183fe7b60d4f1885047fa42b815a62fe0cc", + "startBlock": 25637469, + "runtimeCodeHash": "0x4da04b13565c195132988b3b96e3c43b9f199c0324f18fee616f888b775a2230" + }, + { + "contractName": "StockV1RewardVaultFactory", + "address": "0xd430d9162c153afdf9e4caca6d2317e72a044441", + "startBlock": 25637469, + "runtimeCodeHash": "0x14a1e46cbb829712c7ba64ce018537aed9385d6116b939ef3d355fa2fdc0f2b6" + }, + { + "contractName": "StockV2Launcher", + "address": "0x5ea6be24838061ba45dbe8d82de1b267dc240daf", + "startBlock": 25640338, + "runtimeCodeHash": "0xeee5fbf395266fade461c8d3af1316bb2ce7efa1fb16e64c8d05edf71af0986d" + }, + { + "contractName": "StockV2EthCoordinator", + "address": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "startBlock": 25640338, + "runtimeCodeHash": "0xd8ce52c763197fe20bc992aa2d4a78bf77063905d1e18fba38d014c2e77683ad" + }, + { + "contractName": "StockV2V3Hook", + "address": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "startBlock": 25640338, + "runtimeCodeHash": "0x3e292c9ddc64cc3a9c45f79d9d239ab2b8196f10efbdbc74b4f9b37dba53981d" + }, + { + "contractName": "StockV2V3RewardVaultFactory", + "address": "0x52d70971d6653a754c29385a2a6f241a481952d4", + "startBlock": 25640338, + "runtimeCodeHash": "0x14a1e46cbb829712c7ba64ce018537aed9385d6116b939ef3d355fa2fdc0f2b6" + }, + { + "contractName": "StockV3Launcher", + "address": "0x0573879f72d8ee8b0e5a4ec5e8bcdb2fcab9e51c", + "startBlock": 25642745, + "runtimeCodeHash": "0xa392a2a24b0a2ac124b9f132eb1488589bbabe91e82323da12151ee42af87573" + }, + { + "contractName": "StockV3EthCoordinator", + "address": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "startBlock": 25642745, + "runtimeCodeHash": "0xc687b0c5680a31dd27b2265f60a0aa4992bd30fab9aea3acf6b0bd3842c49d63" + } + ], + "releases": [ + { + "model": "classic", + "releaseVersion": "classic-v2", + "activationBlock": 25624131, + "sourceContracts": ["ClassicV2Hook", "ClassicV2Launcher"], + "dynamicContracts": [] + }, + { + "model": "classic", + "releaseVersion": "classic-v3", + "activationBlock": 25639596, + "sourceContracts": [ + "ClassicV3RewardVaultFactory", + "ClassicV3VestingWalletFactory", + "ClassicV3Hook", + "ClassicV3Launcher" + ], + "dynamicContracts": ["ClassicV3RewardVault"] + }, + { + "model": "stock-paired", + "releaseVersion": "stock-paired-v1", + "activationBlock": 25637469, + "sourceContracts": [ + "StockV1Launcher", + "StockV1EthCoordinator", + "StockV1Hook", + "StockV1RewardVaultFactory" + ], + "dynamicContracts": ["StockV1RewardVault"] + }, + { + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "activationBlock": 25640338, + "sourceContracts": [ + "StockV2Launcher", + "StockV2EthCoordinator", + "StockV2V3Hook", + "StockV2V3RewardVaultFactory" + ], + "dynamicContracts": ["StockV2V3RewardVault"] + }, + { + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "activationBlock": 25642745, + "sourceContracts": [ + "StockV3Launcher", + "StockV3EthCoordinator", + "StockV2V3Hook", + "StockV2V3RewardVaultFactory" + ], + "dynamicContracts": ["StockV2V3RewardVault"] + } + ] +} diff --git a/config/read-model-load-profile.v1.json b/config/read-model-load-profile.v1.json new file mode 100644 index 00000000..8bb6eac0 --- /dev/null +++ b/config/read-model-load-profile.v1.json @@ -0,0 +1,164 @@ +{ + "schemaVersion": 1, + "profileId": "read-model-smoke-v1", + "scope": { + "models": ["classic", "stock-paired"], + "excludedModels": ["adaptive", "deep"] + }, + "evidence": { + "maximumAgeSeconds": 1800, + "requiredKinds": ["preview", "production-canary"], + "requireExactGitHead": true, + "requireLiveVercelBinding": true, + "requiredArtifactDigests": ["datasetManifest", "httpSamples", "rpcTrace"] + }, + "dataset": { + "launches": 200, + "chainEvents": 600, + "marketSnapshots": 200, + "marketCandles": 200, + "accounts": 100, + "rewardRows": 200 + }, + "datasetCoverage": { + "minimumEligibleLaunches": 200, + "maximumEligibleLaunches": 400, + "maximumClassicLookupLaunches": 300, + "maximumStockLookupLaunches": 100, + "minimumRowsPerLaunch": { + "chainEvents": 3, + "marketSnapshots": 1, + "marketCandles": 1, + "rewardRows": 1 + }, + "requiredReleaseVersions": [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3" + ], + "minimumClassicLookupLaunches": 32, + "minimumStockLookupLaunches": 32, + "tokenSampleCount": 100, + "accountSampleCount": 100, + "classicLaunchSampleCount": 32, + "stockLaunchSampleCount": 32, + "candidateSampleCount": 8 + }, + "load": { + "concurrency": 20, + "durationSeconds": 60, + "minimumCompletedRequests": 1000, + "probeTimeoutMs": 30000, + "maximumErrorRateBps": 0, + "maximumCacheHitRateBps": 0, + "minimumDistinctTokenKeys": 100, + "minimumDistinctAccountKeys": 100, + "minimumDistinctClassicLaunchKeys": 32, + "minimumDistinctStockLaunchKeys": 32, + "probeCacheControl": "private, no-store", + "requiredVercelCacheStatuses": ["MISS", "BYPASS"], + "routeMixBps": { + "exploreList": 1000, + "tokenDetail": 1000, + "tokenChart": 1000, + "creatorProfile": 1000, + "classicProfile": 1000, + "stockProfile": 1000, + "classicLaunchLookup": 1800, + "stockLaunchLookup": 1000, + "publicIndexer": 800, + "health": 400 + }, + "maximumRouteP95Ms": { + "exploreList": 750, + "tokenDetail": 750, + "tokenChart": 1500, + "creatorProfile": 750, + "classicProfile": 750, + "stockProfile": 1500, + "classicLaunchLookup": 750, + "stockLaunchLookup": 1500, + "publicIndexer": 500, + "health": 1500 + }, + "maximumRouteP99Ms": { + "exploreList": 1500, + "tokenDetail": 1500, + "tokenChart": 2500, + "creatorProfile": 1500, + "classicProfile": 1500, + "stockProfile": 2500, + "classicLaunchLookup": 1500, + "stockLaunchLookup": 2500, + "publicIndexer": 1000, + "health": 2500 + } + }, + "projector": { + "hostingDeadlineMs": 90000, + "hardDeadlineMs": 75000, + "minimumReserveMs": 15000, + "smokeCandidateBatchSize": 8, + "maximumCandidateBatchSize": 8, + "rpc": { + "providerCount": 2, + "perCallTimeoutMs": 5000, + "maxAttemptsPerCall": 3, + "baseBackoffMs": 50, + "maxConcurrencyPerProvider": 4, + "fixedCallsPerProviderPerAttempt": 3, + "callsPerCandidatePerProviderPerAttempt": 3, + "smokeFirstAttemptCallsPerProvider": 27, + "theoreticalWorstCaseCallsPerProvider": 81, + "theoreticalWorstCaseDurationMs": 121200, + "globalRetryAllowancePerProvider": 15, + "maxCallsPerProviderPerRun": 42, + "maxAggregateCallsPerRun": 84 + } + }, + "providerLimits": { + "envio": { + "planQueriesPerMinute": 1000, + "steadyQueriesPerMinute": 500, + "burstQueriesPerMinute": 750 + } + }, + "shadow": { + "requiredRoutes": [ + "exploreList", + "tokenDetail", + "tokenChart", + "creatorProfile", + "classicProfile", + "stockProfile", + "classicLaunchLookup", + "stockLaunchLookup" + ], + "minimumSamples": 880, + "maximumP50Ms": 10, + "maximumP95Ms": 25, + "maximumP99Ms": 50, + "maximumLiveComparisonP50Ms": 5000, + "maximumLiveComparisonP95Ms": 15000, + "maximumLiveComparisonP99Ms": 25000, + "maximumParityMismatches": 0, + "maximumFallbacks": 0 + }, + "cacheContracts": { + "exploreList": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "tokenDetail": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "tokenChart": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "creatorProfile": "private, max-age=0, s-maxage=15", + "classicProfile": "no-store", + "stockProfile": "no-store", + "classicLaunchLookup": "no-store", + "stockLaunchLookup": "no-store", + "publicIndexer": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "tokenList": "public, max-age=0, s-maxage=60, stale-while-revalidate=300", + "health": "public, max-age=0, s-maxage=30", + "accountMutation": "private, no-store", + "transactionPreparation": "private, no-store" + } +} diff --git a/config/read-model-operations.v1.json b/config/read-model-operations.v1.json new file mode 100644 index 00000000..704fb8f1 --- /dev/null +++ b/config/read-model-operations.v1.json @@ -0,0 +1,133 @@ +{ + "schemaVersion": 1, + "legacyIndexer": { + "path": "/api/ops/index-v2", + "schedule": "*/5 * * * *", + "retainedUntil": "indexed-read-cutover", + "route": "app/api/ops/index-v2/route.ts", + "sha256": "9638ec482ff66c5f3b1377c60b946e6348fb895769b6f8596fca2dc8cfbac535", + "closedAlias": { + "path": "/api/ops/index", + "route": "app/api/ops/index/route.ts", + "status": 410, + "sha256": "bb498b00334df908029a588bec552516f281fdc0dfc3185bc5cd820984a9ee1f" + } + }, + "workers": [ + { + "id": "source-projector", + "path": "/api/ops/projector", + "schedule": "* * * * *", + "activationEnvironment": "PROGRAMMABLE_PROJECTOR_ACTIVE", + "route": { + "path": "app/api/ops/projector/route.ts", + "sha256": "9b12168cbbadf0addac351c45f71931f3c04370bcd6cabe6174d21daeb00a94d" + }, + "runtime": { + "path": "lib/data-pipeline/projector-runtime-config.server.ts", + "sha256": "f54859e55f35b99784eebd6cef58a40a5848904be21417249f3bff5bf1c88637" + }, + "dependencies": [ + { + "path": "lib/data-pipeline/candidate-projector-runtime-binding.server.ts", + "sha256": "32efa13d740614f7e66fd20a0158edf3383f4f6643a7fe34268fabda6261931c" + } + ], + "migrations": [ + { + "path": "supabase/migrations/20260731203900_projector_runtime_singleton_lease.sql", + "sha256": "068f27a70ec6df57b84bf336fc2c46b316a7d10d40b9d489fc47e95acb6f74b0" + }, + { + "path": "supabase/migrations/20260731224000_projector_provider_evidence_binding.sql", + "sha256": "0404f7c610a34af23fe536f021927efec4e0aede235068b70be04331c58f03af" + }, + { + "path": "supabase/migrations/20260801090000_bootstrap_dynamic_evidence_and_launch_requirements.sql", + "sha256": "e095d128feb12c8962c81be003e693dd67417cfed209144c998ab57d5e8786aa" + }, + { + "path": "supabase/migrations/20260801091000_candidate_projector_unpromoted_gate.sql", + "sha256": "cd8b5a4aa4801ca773cb84047edbf05349288cada47d671bd47e7d997902c91f" + }, + { + "path": "supabase/migrations/20260801092000_verify_candidate_database_promoted.sql", + "sha256": "ed5f54a374ad8178393e88a3948281ad9acba10aebbbd5209ea6793691b8c677" + }, + { + "path": "supabase/migrations/20260801093000_bind_candidate_promotion_to_product.sql", + "sha256": "c6a032ef371b2211004c8d72c0a8c4eec4ba630776210aed48d2d054e642dbbe" + }, + { + "path": "supabase/migrations/20260801125441_reuse_safe_head_observations.sql", + "sha256": "afbeea7bcf60e492e51bfd0c56517613f32a6f87a0182af00c48bdaef6569e74" + }, + { + "path": "supabase/migrations/20260801144403_accept_uuid_v8_dynamic_source_lineage.sql", + "sha256": "85e0509d2a4fa49062a18d891e51cd0c64c1015926c3c3ef47a83ce16edb4170" + }, + { + "path": "supabase/migrations/20260801155212_reuse_dual_rpc_block_evidence.sql", + "sha256": "51142370cf7fdf2bd60c2812978fe2cbbacf99f42b87c72f0ad1ac61b303cf51" + }, + { + "path": "supabase/migrations/20260801204500_reuse_dual_rpc_block_evidence_constraint.sql", + "sha256": "92cc63189b41eda613ba9da21b7ef21bee650a93f1825f5ee063727ee6c06b11" + } + ] + }, + { + "id": "market-projector", + "path": "/api/ops/market-projector", + "schedule": "* * * * *", + "activationEnvironment": "PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE", + "route": { + "path": "app/api/ops/market-projector/route.ts", + "sha256": "73bf9299095cfdf75d5452513ee818e161297a83c6355760ab2f79a22a13edbd" + }, + "runtime": { + "path": "lib/data-pipeline/market-projector-runtime.server.ts", + "sha256": "ed1c55148d05a47d747616a4bc8250996780be65d053b989d51db21b4519109b" + }, + "migrations": [ + { + "path": "supabase/migrations/20260731223000_market_projector_contract.sql", + "sha256": "ea73f4112a53b25e72aa697d3fc0679bf9c6e7f93a496edd167803d6a7f81a24" + } + ] + } + ], + "eventTriggers": [ + { + "id": "quicknode-stream-projector-wake", + "path": "/api/ops/projector-wake", + "provider": "quicknode-streams", + "mode": "wake-only", + "secretEnvironment": "PROGRAMMABLE_QUICKNODE_STREAM_SECRET", + "route": { + "path": "app/api/ops/projector-wake/route.ts", + "sha256": "cdea2e18ebdb545e0f4de7bdd54da181c5cf6fa715e5abe1ac32c0bae66d138b" + }, + "verifier": { + "path": "lib/data-pipeline/quicknode-stream-wake.server.ts", + "sha256": "9c452c2ae94b62d31ed2ffdaaf974b1acd4c7a856493ac02013e43f89eb4bc65" + } + } + ], + "unscheduled": [ + { + "id": "reconciler-preparity", + "path": "/api/ops/reconcile-preparity", + "reason": "full-release-route-matrix-not-yet-proven" + } + ], + "postPromotion": { + "publicRoutes": [ + "/", + "/api/ops/health", + "/api/explore?limit=6&page=1&sort=market-cap", + "/api/indexers/v1/token-list" + ], + "evidenceRequiredForIndexedRoutes": true + } +} diff --git a/config/read-model-release-profile.v1.json b/config/read-model-release-profile.v1.json new file mode 100644 index 00000000..1a11cf1d --- /dev/null +++ b/config/read-model-release-profile.v1.json @@ -0,0 +1,164 @@ +{ + "schemaVersion": 1, + "profileId": "read-model-release-v1", + "scope": { + "models": ["classic", "stock-paired"], + "excludedModels": ["adaptive", "deep"] + }, + "evidence": { + "maximumAgeSeconds": 1800, + "requiredKinds": ["preview", "production-canary"], + "requireExactGitHead": true, + "requireLiveVercelBinding": true, + "requiredArtifactDigests": ["datasetManifest", "httpSamples", "rpcTrace"] + }, + "dataset": { + "launches": 264, + "chainEvents": 792, + "marketSnapshots": 264, + "marketCandles": 264, + "accounts": 100, + "rewardRows": 264 + }, + "datasetCoverage": { + "minimumEligibleLaunches": 264, + "maximumEligibleLaunches": 400, + "maximumClassicLookupLaunches": 300, + "maximumStockLookupLaunches": 100, + "minimumRowsPerLaunch": { + "chainEvents": 3, + "marketSnapshots": 1, + "marketCandles": 1, + "rewardRows": 1 + }, + "requiredReleaseVersions": [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3" + ], + "minimumClassicLookupLaunches": 32, + "minimumStockLookupLaunches": 32, + "tokenSampleCount": 264, + "accountSampleCount": 100, + "classicLaunchSampleCount": 32, + "stockLaunchSampleCount": 32, + "candidateSampleCount": 32 + }, + "load": { + "concurrency": 20, + "durationSeconds": 60, + "minimumCompletedRequests": 1000, + "probeTimeoutMs": 30000, + "maximumErrorRateBps": 0, + "maximumCacheHitRateBps": 0, + "minimumDistinctTokenKeys": 264, + "minimumDistinctAccountKeys": 100, + "minimumDistinctClassicLaunchKeys": 32, + "minimumDistinctStockLaunchKeys": 32, + "probeCacheControl": "private, no-store", + "requiredVercelCacheStatuses": ["MISS", "BYPASS"], + "routeMixBps": { + "exploreList": 1000, + "tokenDetail": 1000, + "tokenChart": 1000, + "creatorProfile": 1000, + "classicProfile": 1000, + "stockProfile": 1000, + "classicLaunchLookup": 1800, + "stockLaunchLookup": 1000, + "publicIndexer": 800, + "health": 400 + }, + "maximumRouteP95Ms": { + "exploreList": 750, + "tokenDetail": 750, + "tokenChart": 1500, + "creatorProfile": 750, + "classicProfile": 750, + "stockProfile": 1500, + "classicLaunchLookup": 750, + "stockLaunchLookup": 1500, + "publicIndexer": 500, + "health": 1500 + }, + "maximumRouteP99Ms": { + "exploreList": 1500, + "tokenDetail": 1500, + "tokenChart": 2500, + "creatorProfile": 1500, + "classicProfile": 1500, + "stockProfile": 2500, + "classicLaunchLookup": 1500, + "stockLaunchLookup": 2500, + "publicIndexer": 1000, + "health": 2500 + } + }, + "projector": { + "hostingDeadlineMs": 90000, + "hardDeadlineMs": 75000, + "minimumReserveMs": 15000, + "smokeCandidateBatchSize": 8, + "maximumCandidateBatchSize": 32, + "rpc": { + "providerCount": 2, + "perCallTimeoutMs": 5000, + "maxAttemptsPerCall": 3, + "baseBackoffMs": 50, + "maxConcurrencyPerProvider": 4, + "fixedCallsPerProviderPerAttempt": 3, + "callsPerCandidatePerProviderPerAttempt": 3, + "smokeFirstAttemptCallsPerProvider": 27, + "theoreticalWorstCaseCallsPerProvider": 297, + "theoreticalWorstCaseDurationMs": 393900, + "globalRetryAllowancePerProvider": 29, + "maxCallsPerProviderPerRun": 128, + "maxAggregateCallsPerRun": 256 + } + }, + "providerLimits": { + "envio": { + "planQueriesPerMinute": 1000, + "steadyQueriesPerMinute": 500, + "burstQueriesPerMinute": 750 + } + }, + "shadow": { + "requiredRoutes": [ + "exploreList", + "tokenDetail", + "tokenChart", + "creatorProfile", + "classicProfile", + "stockProfile", + "classicLaunchLookup", + "stockLaunchLookup" + ], + "minimumSamples": 880, + "maximumP50Ms": 10, + "maximumP95Ms": 25, + "maximumP99Ms": 50, + "maximumLiveComparisonP50Ms": 5000, + "maximumLiveComparisonP95Ms": 15000, + "maximumLiveComparisonP99Ms": 25000, + "maximumParityMismatches": 0, + "maximumFallbacks": 0 + }, + "cacheContracts": { + "exploreList": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "tokenDetail": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "tokenChart": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "creatorProfile": "private, max-age=0, s-maxage=15", + "classicProfile": "no-store", + "stockProfile": "no-store", + "classicLaunchLookup": "no-store", + "stockLaunchLookup": "no-store", + "publicIndexer": "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + "tokenList": "public, max-age=0, s-maxage=60, stale-while-revalidate=300", + "health": "public, max-age=0, s-maxage=30", + "accountMutation": "private, no-store", + "transactionPreparation": "private, no-store" + } +} diff --git a/docs/data-pipeline/DATABASE.md b/docs/data-pipeline/DATABASE.md new file mode 100644 index 00000000..bbf41514 --- /dev/null +++ b/docs/data-pipeline/DATABASE.md @@ -0,0 +1,427 @@ +# Private read-model database + +The Programmable read model is a server-only Postgres subsystem. It stores +candidate provenance, dual-RPC canonicality evidence, immutable event +occurrences, verified projections, profiles, reconciliation evidence and +generation-fenced checkpoints. It does not authorize transaction calldata, +hold signing credentials, or expose a browser database surface. + +The application schema is `programmable_private`. The local Data API is +disabled, and its schema allowlist contains only `public` and +`graphql_public`; `programmable_private` is deliberately absent. No browser +role or `service_role` grant reaches the private schema. + +## Current deployment state + +The implementation brief contains owner-supplied, point-in-time setup evidence +for a hosted Supabase project named `programmable-read-model` with project ref +`mnnvlrqwhfoppogslsje`, a Central EU (Frankfurt) region, and the provider +settings listed there. This worktree did not independently verify that the +project, region, compute, backup, billing or spend-control settings are still +current. Treat every one of those details as unverified-current provider +evidence, not as a live-state assertion. + +This implementation does **not** link the local directory, apply a remote +migration, change compute or billing, configure Vercel, or activate +application reads. The integration owner must review the exact migration +commit and run every local gate below before any hosted migration. + +## Local setup + +Requirements: + +- Docker-compatible local container runtime +- Supabase CLI +- Node.js 24.14.0, matching `supabase/tests/codec/.node-version`, and the + repository lockfile dependencies +- `psql` for the two-session concurrency harness +- `gitleaks` for the secret gate + +From the repository root: + +```sh +supabase start +supabase db reset +supabase test db supabase/tests/database +npm run db:test:pglite +(cd supabase/tests/codec && npm ci --ignore-scripts) +node supabase/tests/codec/verify-reference-canonical-fingerprint-v1.mjs +node supabase/tests/codec/verify-production-canonical-fingerprint-v1.ts +node supabase/tests/codec/verify-reference-provider-evidence-v2.mjs +node supabase/tests/codec/verify-production-provider-evidence-v2.ts +supabase db lint --local --level warning +``` + +`supabase db reset` must begin from an empty local database and apply every +versioned migration in order. The explicit test path keeps the real +two-session scripts out of the pgTAP runner; it runs only the suites in +`supabase/tests/database/`. + +`npm run db:test:pglite` is a fast supplementary gate. It replays all +migrations and then runs each pgTAP file against a separate fresh in-memory +database. It catches SQL, function-shape and privilege regressions without a +local container, but it does not replace a native PostgreSQL 17 reset, lint or +the real two-session concurrency harness. + +The local direct Postgres port is `54322` and the local transaction-pooler +port is `54329`, as declared in `supabase/config.toml`. Set +`PROGRAMMABLE_DATABASE_URL` to the local direct database URL reported by +`supabase status`, then run: + +```sh +supabase/tests/concurrency/run.sh +``` + +Never place that URL in a tracked file or command transcript. The harness uses +two real `psql` sessions. When it exits successfully against a reset local +database, it exercises exact-scope pointer, lease and checkpoint CAS; a +higher-pointer, higher-lease, higher-checkpoint-generation reorg; independent +scopes; an injected failure after staged projection and checkpoint writes; +same-wallet and same-alias first binding; rekey/rekey, +rekey/tombstone, rekey/alias-claim, recovery/recovery, +recovery/profile-mutation and profile-revision races; and case-insensitive +username collision. The checked-in harness is test design, not evidence that +those real-session races passed until this command has actually completed. + +`supabase/seed.sql` is intentionally comment-only. pgTAP tests create +deterministic fixtures inside transactions and roll them back. + +## Connection modes + +If a hosted operation is separately authorized, first verify the current +project ref and endpoint from the provider control plane. Migrations, schema +inspection, backup and restore then use the verified direct Postgres endpoint +on port `5432`; they must not run through a transaction pooler. + +A later, separately reviewed Vercel integration may use Supavisor transaction +mode on port `6543`. Prepared statements must be disabled for that runtime +connection. The browser receives no Postgres, Supabase, migrator, +`service_role`, or capability-role credential. + +No runtime connection is enabled by this migration set. + +## Ownership and capabilities + +`programmable_migrator` is the sole owner of every private application schema +object. It is `NOLOGIN`, non-superuser, has no `BYPASSRLS`, and is absent from +runtime. Every base table enables and forces RLS; its only policy targets the +migrator owner. + +| Role | Capability | +| --- | --- | +| `programmable_projector` | Exact audited epoch, provider, ingestion, evidence, staging, promotion and rewind functions | +| `programmable_reconciler` | Exact audited health, reconciliation, parity and market append functions; named reconciliation views | +| `programmable_api_reader` | Named public evidence views plus the frozen public route and indexer-feed read functions | +| `programmable_profile_binder` | Insert-if-absent first profile binding only | +| `programmable_profile_recovery` | Hash-version rotation, alias rekey, tombstone and recovery | +| `programmable_profile_writer` | Revision-fenced profile mutation only | +| `programmable_maintenance` | Bounded telemetry, market and parity retention functions only | + +Runtime roles receive schema `USAGE` only where an exact function or view +requires it. They receive no base-table DML, sequence access, ownership, +membership in another capability role, `CREATE`, `BYPASSRLS`, or broad +function grant. `PUBLIC`, `anon`, `authenticated` and `service_role` have no +private-schema access. Default privileges repeat the deny posture for future +objects. + +Every callable function is owned by the migrator, uses `SECURITY DEFINER`, +fixes `search_path` to the empty string, schema-qualifies its object access, +and checks the active capability role. Stable views are explicit +definer-mode, security-barrier views with declared columns. + +## Canonicality and publication + +Release scope is exactly: + +```text +(chain_id, release_id, model_id, source_group) +``` + +Epoch and source-binding rows are immutable. `release_epoch_current` is the +only current pointer and moves by expected-generation compare-and-swap. Runs +capture both epoch ID and pointer generation. Before activation, each +contract source is bound to its exact address, inclusive start block, +semantic source role, recovery selector where applicable, ABI/event-set +commitment and release-wide artifact creation-code commitment. Global +occurrence rows own only physical chain identity and placement. The exact +release binding or dynamic attestation, event type, decoder, ABI, decoded +payload and provider evidence live in the immutable +`chain_event_occurrence_materializations` ledger keyed by occurrence, epoch +and pointer generation. The same raw event can therefore materialize in more +than one valid release without duplicating its chain identity or authorizing a +projection through another release's first-seen snapshot. Ingestion rejects +an address, block or ABI outside the exact manifest, and an active epoch +cannot acquire a late binding. + +Factory-created sources are not admitted by address alone. Each release pins +an immutable dynamic-source template containing the parent factory role and +event, the exact decoded address field (`vault` or `wallet`), deployed role, +deployed artifact creation-code commitment, normalized runtime-code hash, +runtime length, immutable-reference commitment and ABI commitment. The +template never assumes every factory instance has one exact runtime hash. +Registration requires the parent factory occurrence's exact materialization +to be current canonical and the emitted address to match that exact field. A +separate immutable record proves equal exact instance runtime hashes from both +pinned RPC deployments at the deployment block, then checks the agreed +normalized hash, runtime length and immutable references against the template. +That record stores both providers' complete runtime bytes and the complete +locally reconstructed runtime, not hashes and lengths alone. A second +immutable binding ties each authorized dynamic address to current-canonical +factory, launch and pool occurrences plus the exact token, pool ID, hook and +quote asset. Unbound or orphaned attestations never enter projector log +filters. +The per-instance attestation also records constructor-argument and local +init-code commitments, and rejects an init-code hash that is merely the +release template's artifact creation-code commitment. +Two valid factory instances may consequently have different exact runtime +hashes while matching the same immutable-aware template. A release-neutral +Envio inbox preserves raw candidate evidence once; append-only resolutions +associate the same candidate with an exact static binding or dynamic +attestation in each epoch without rewriting the raw row. + +The release-neutral Envio cursor has one explicit dual-RPC-attested genesis. +Each bounded page independently compares the ordered Alchemy and QuickNode log +commitments with the exact durable inbox interval. One atomic capability then +appends candidates, coverage evidence and the succeeded outcome before its +last-step cursor CAS. The projector cannot execute the standalone cursor +advance. Omission, extra-log, provider disagreement, stale generation or +partial failure therefore leaves no advanced cursor. + +RPC deployments use a dedicated registration capability; the generic provider +writer rejects `rpc_provider`. The specialized path is fixed to Ethereum +mainnet (`chain_id = 1`) and derives the only accepted order: Alchemy first, +QuickNode second. It retains constructor version, endpoint URL commitment, +endpoint origin commitment and a nonzero commitment linked to the allowlisted +`rpc-endpoint-commitments-v1` evidence domain. Raw endpoint URLs and origins +are never database fields. An accepted safe head requires those two different +immutable deployments in that fixed order, the expected chain ID from both, +heads of at least twelve, finality depth twelve, the exact safe block +`least(head_a, head_b) - 12`, and equal hashes at that block. Every promoted +source occurrence and checkpoint target also carries same-observation, +same-epoch agreeing block evidence. + +Logical event identity is `(chain_id, transaction_hash, +receipt_log_ordinal)`. Fork placement adds `block_hash`. Block-global log +index remains provenance and never becomes identity. Transaction indexes, +block-global log indexes and receipt log ordinals preserve the full unsigned +32-bit domain in `bigint`-backed checked domains and read models; no `int4` +narrowing is permitted. + +Projection rows are staged under a nonterminal run. One fenced promotion +validates the current epoch, lease generation, safe-head evidence, occurrence +evidence, verified seed evidence and checkpoint CAS, then atomically appends +the deterministic ordered fold manifest (including every typed projection row +reference, selected occurrence, allocation fact/evidence pair and route), +outcome, publication, route +eligibility and checkpoint. Every staged projection must bind the exact run, +epoch/generation, target block/hash and a source occurrence present in the +ordered promotion set. Every complete launch requires exactly one token-bound +PoolKey row and one fee-configuration row; selected reward facts require +complete vault and beneficiary-allocation rows. Dedicated, +audited typed writers cover PoolKey/fees, accruals/totals, vault/allocation, +beneficiary/creator/launcher claims, payout changes, account balances and +initial-buy custody/vesting. Rewind is a +separate entry point and requires strictly higher pointer, lease, checkpoint +and reorg generations; orphaning and rebuildable-row deletion are restricted +to the same `(chain_id, release_id, model_id, source_group)`. Append functions +cannot make data current. + +`checkpoint_summary_v1` exposes the exact canonical `checkpoint_id` together +with `source_group` and `projector_version`; consumers bind readiness to that +identity, not merely an epoch/generation/block tuple. Projector-only readers +expose the exact current release manifest, only current asset-bound dynamic +attestations, and a lease-fenced terminal-disposition stream so a restart can +reconstruct the ordered decision IDs required by promotion. + +Every typed writer is fenced by the release's immutable +`(projection_kind, source_role, event_type)` allowlist. Publication also +requires a non-empty ordered launch-completeness manifest and exact staged +occurrence roles. `always`, `reward_vault`, `locked_custody`, and `eth_funded` +conditions are evaluated per launch. Reward-vault launches require a complete +vault projection and selected verified seed; locked custody requires a +matching vesting projection and factory occurrence; ETH-funded launches +require the coordinator occurrence. Classic V3 fee disclosure stores +`buy_creator_fee_bps` and `sell_creator_fee_bps` separately. Hook creator and +launcher claims, `CreatorFeesCheckpointed`, and +`CtoRewardConfigurationActivated` are distinct immutable event facts rather +than being forced into the beneficiary-claim or initial-seed shape. + +Reward-allocation facts require the complete ordered launcher, factory and +hook occurrence set from the same current release epoch, and every role is +checked against the occurrence's immutable release-binding commitment. +Promotion accepts only evidence with explicit recomputation attestations whose +allocation, configuration and active-configuration commitments equal the +fact. Calldata evidence must bind the exact manifest destination and selector, +the factory transaction and release artifact. The release-wide artifact +creation-code commitment is distinct from, and is never compared for equality +with, the per-instance constructor-arguments commitment, local init-code hash, +CREATE2 salt or computed CREATE2 address. Getter and prediction provider pairs +must agree; matched predictions must both equal the emitted vault. Structurally +retained legacy evidence with +no recomputation attestation is never promotion-eligible. Every later evidence +append is re-evaluated against the exact fact epoch even when recomputation +fields are absent. Any contradiction in binding, CREATE2 address, selected RPC +results, receipt or enrichment evidence is retained in immutable mismatch +evidence, appends a quarantine decision, removes any verified pointer and +quarantines only the exact epoch-generation routes rather than disappearing in +a rolled-back exception. If two independently valid facts prove different +allocation or configuration content +for the same canonical factory occurrence and vault, the audited conflict +transition records both decisions, removes the verified pointer and +quarantines affected routes atomically. + +The server-only launch list, token-detail and creator-profile views expose the +same current published launch together with its canonical PoolKey +(`currency0`, `currency1`, hook, fee and tick spacing), audited fee disclosure, +launch timestamp, and latest immutable project-metadata revision with ordered +HTTPS links. Project metadata remains descriptive and cannot make an otherwise +ineligible launch visible. + +Recent-launch pagination uses the complete ordered cursor +`(promoted_block_number, launch_transaction_hash, token)`; all three values +must be absent for the first page or present for later pages. Account reward +function rows return their authoritative `chain_id` and `account` alongside +the reward data, so an adapter never relabels returned scope from request +parameters. + +Dependency circuit state is stored with the exact database enum values +`closed`, `open`, `half_open`, and `frozen`. JSON adapters may present a +different display label, but must map explicitly; `half-open` is not a valid +database enum value and `frozen` must not be dropped. + +Named market snapshot and candle views are available only to +`programmable_api_reader`. Every returned row belongs to a successful, +zero-mismatch reconciliation run in the same current release/source epoch, +uses an immutable `uniswap_subgraph` deployment record, and binds its source +block number/hash to exact dual-RPC block evidence. These views join through +the independently gated token-detail launch view; absent or stale market data +stays absent and never removes or fabricates the underlying launch. + +## Fingerprint trust boundary + +Postgres does not canonicalize JSON, serialize a fingerprint preimage, or +calculate Keccak/SHA3. The reviewed server-boundary TypeScript codec owns +normalization, JCS, v1 framing and Ethereum Keccak-256. The independent +JavaScript reference verifier and the checked-in static JSON vectors pin the +same complete bytes and digests. Its exact Node runtime is recorded in +`supabase/tests/codec/.node-version`; its independent `canonicalize` and +`@noble/hashes` implementations are exact-pinned with registry integrity +values in the codec-local lockfile. + +Provider evidence uses a separate normative v2 frame. The domain prefix is +`programmable:provider-evidence:v2\0`, followed by the immutable one-byte +subtype tag: safe head `1`, block `2`, runtime code `3`, dynamic attestation +`4`, or bounded log coverage `5`. The checked-in v2 fixture fixes every field +order and width, UUID bytes, nullable markers, length framing, ordered arrays, +complete runtime bytes, complete preimages, Keccak digests and one-field +mutation checks. Its per-subtype and aggregate definition commitments exactly +match the SQL allowlist; changing a field requires a new encoding version, +not an in-place reinterpretation. + +The database treats `encoding_version`, `canonical_preimage` and +`content_fingerprint` as opaque immutable provenance. Audited functions: + +- admit only immutable, migration-allowlisted encoding versions and their + exact domain prefixes; +- validate structured address, hash, integer, array and release relations; +- store the supplied bytes verbatim; +- return an existing row only when exact replay byte-compares every immutable + member, including preimage and digest; +- reject a changed preimage, digest, ordering or provenance member. + +The evidence v1 frame commits separately, in order, to constructor arguments, +per-instance init-code hash, CREATE2 salt and computed address. None is an +alias for the release artifact creation-code commitment. + +Capability fencing plus the reviewed codec establish correctness for a new +digest. SQL does not pretend to authenticate an unseen preimage/hash pair. +Release-binding, current-canonical role, recomputed configuration, dual-provider +agreement and projection-fold checks are relational promotion gates in +addition to that opaque fingerprint provenance; they are not inferred merely +from the existence of a fingerprint row. + +## Backup, restore and migration gate + +Migrations are additive and append-only. Never edit a migration already +applied to any environment. Before a reviewed hosted migration: + +1. Reset an empty local database and run all pgTAP suites. +2. Run all four codec conformance verifiers: independent reference and + production implementations for v1 and provider-evidence v2. +3. Run the real two-session concurrency harness. +4. Run local lint and migration dry-run inspection without linking production. +5. Run the secret scan over the exact diff. +6. Take a direct-port backup and record the reviewed migration commit. + +The deterministic hosted operator and its exact plan, dry-run, apply and +verification sequence are documented in +[`HOSTED-DATABASE-OPERATOR.md`](./HOSTED-DATABASE-OPERATOR.md). Its plan scans +the complete ordered `supabase/migrations/*.sql` set; no hand-maintained +worker manifest is an authority for migration completeness. Migration and +release bootstrap remain separate operations. + +Backup and restore must copy the stored fingerprint triple byte-for-byte. +Compare ordered exports of: + +```text +(primary key, encoding_version, encode(canonical_preimage, 'hex'), + encode(content_fingerprint, 'hex')) +``` + +for safe-head evidence, block evidence, event occurrences, allocation facts +and allocation evidence before and after restore. Do not recanonicalize, +rehash, backfill, silently upgrade an encoding version, or rewrite an old +pair. A new codec version is a new immutable row in +`fingerprint_encoding_versions`; it never updates an old definition or fact. +The replay gate verifies that a stored v1 key remains byte-identical and +idempotent after v2 is allowlisted. Rerun relationship/replay pgTAP tests +after restore and rerun all four codec verifiers from the checked-in constants. + +After independently verifying and authorizing the hosted target, use its +direct endpoint on port `5432` for `pg_dump`, `pg_restore`, migrations and +restore validation. A Supavisor connection is not a restore path. + +## Retention + +No scheduler or `pg_cron` job is installed. Maintenance is an explicit +server-side operation, and each call deletes at most 10,000 eligible rows. + +| Data class | Default | +| --- | --- | +| Canonical launches, assets, claims and reward accounting | Indefinite | +| Profiles | Until verified deletion | +| Canonical block evidence | At least 4,096 blocks | +| Orphan/reorg evidence | 400 days | +| Raw market snapshots | 7 days | +| Hourly candles | 90 days | +| Daily candles | Indefinite | +| Five-minute portfolio points | 400 days, then daily aggregates | +| Successful-run telemetry | 30 days | +| Failed/reorg-run telemetry | 180 days | +| Matching parity records | 30 days | +| Resolved mismatches | 180 days after resolution | + +Run headers, terminal outcomes, epochs, occurrence/fingerprint evidence, +selected seed history, checkpoints, publications, audits and reconciliation +records are not retention targets. Foreign keys use `RESTRICT`/`NO ACTION`; +retention cannot cascade or null provenance. + +## Explicit compatibility boundary + +This P0 schema supports the reviewed Classic V3 and Stock-Paired V3 release +families only. Deep-only releases, events, projection rules, routes and reward +semantics are deliberately unsupported and excluded. A Deep integration needs +its own reviewed manifests, event rules, lifecycle tests and release evidence; +it must not reuse these capabilities by relabeling a supported model. + +## Troubleshooting + +- If `supabase start` reports that Docker is unavailable, start a compatible + local container runtime. Do not work around this by linking or migrating the + hosted project. +- If the concurrency harness reports that `psql` is missing, install the + PostgreSQL client and rerun against the local direct database. +- A passing codec test does not prove migrations, RLS or concurrency. +- A passing local reset does not prove hosted migration, runtime + connectivity, production activation, provider health or application + availability. diff --git a/docs/data-pipeline/ENVIO-CANDIDATE-RUNBOOK.md b/docs/data-pipeline/ENVIO-CANDIDATE-RUNBOOK.md new file mode 100644 index 00000000..773cfe7e --- /dev/null +++ b/docs/data-pipeline/ENVIO-CANDIDATE-RUNBOOK.md @@ -0,0 +1,149 @@ +# Envio release candidate + +## Current provider state + +Candidate `7f24e63` is deployed at mirror commit +`7ffd15c2a28c481a2d3632e30b315262c2471b2e`, fully synced and audited, but +it is not promoted. The active production deployment remains +`production-1e7c381` at mirror commit +`2cb1c35c7738fea63e656ad11589664dc93d785d`. + +The paired baseline and candidate audit were captured at the same provider checkpoint and are recorded in: + +- [`envio-candidate-7f24e63-baseline-20260801T042058Z.json`](./envio-candidate-7f24e63-baseline-20260801T042058Z.json) +- [`envio-candidate-7f24e63-audit-20260801T042059Z.json`](./envio-candidate-7f24e63-audit-20260801T042059Z.json) +- [`envio-candidate-7f24e63-deployment-7ffd15c.json`](./envio-candidate-7f24e63-deployment-7ffd15c.json) + +The deployment evidence also records the rejected +`6f2f408e137ce3c01450a13ed11f477ae4ac7240` deployment and the direct push to +the mirror's unprotected `production` branch. Neither deployment changes the +product binding. Promotion remains blocked by the Postgres, reconciliation, +performance and staged-deployment gates below. + +## Historical preparation record + +Candidate `7f24e63` was originally prepared but not deployed. It retains the +existing Classic V2/V3 and Stock-Paired V1/V2/V3 history. It does not add or +activate a new Stock launch path. + +The candidate fixes the authenticated Stock coordinator creator transition, +adds the complete runtime artifact identity and widens log placement fields to +their exact uint32 domain. Its source and rollback identities are pinned in +[`envio-candidate-7f24e63.json`](./envio-candidate-7f24e63.json). + +## Local source gate + +Use a clean checkout at the exact source commit. Do not generate the mirror +from a later working tree. + +```bash +git worktree add --detach /private/tmp/programmable-envio-source-7f24e63 \ + 7f24e6380d5cf17092f5ade7cbad678465e3ef95 +cd /private/tmp/programmable-envio-source-7f24e63/indexer +pnpm install --frozen-lockfile +pnpm codegen +pnpm typecheck +pnpm test +node scripts/release-candidate.mjs identity \ + --source-commit 7f24e6380d5cf17092f5ade7cbad678465e3ef95 +``` + +The identity output must match the candidate JSON byte for byte at every +identity field. + +## Deployment mirror + +The private `0xprogrammable/programmable-indexer` repository is the only Envio +deployment mirror. Create a review branch there, replace only `indexer/` with +the tree from source commit `7f24e63`, and set its root `SOURCE_COMMIT` to the +full source SHA. Keep the existing deployment manifests unchanged. Review the +tree diff before committing and pushing that branch. + +Record the resulting mirror commit in a new evidence manifest. Do not edit this +prepared manifest in place, and do not push the candidate directly to the +mirror's `production` branch. + +Before deploying the mirror commit, set the eight exact `ENVIO_*` identity +values from the candidate JSON. Envio environment changes apply to the next +deployment. Confirm the values with `indexer env list` before continuing. + +```bash +npx --yes envio-cloud@0.10.0 indexer env set \ + programmable-indexer 0xprogrammable \ + ENVIO_DEPLOYMENT_LABEL=production-7f24e63 \ + ENVIO_SOURCE_COMMIT=7f24e6380d5cf17092f5ade7cbad678465e3ef95 \ + ENVIO_CONFIG_SHA256=0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2 \ + ENVIO_SCHEMA_SHA256=0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0 \ + ENVIO_HANDLER_SHA256=0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491 \ + ENVIO_SOURCE_REGISTRY_SHA256=0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a \ + ENVIO_EVENT_SET_SHA256=0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243 \ + ENVIO_EVENT_COUNT=51 +``` + +The following command is intentionally not run during candidate preparation: + +```bash +npx --yes envio-cloud@0.10.0 deployment deploy \ + programmable-indexer 0xprogrammable --yes +``` + +## Replay inventory gate + +Immediately before replay, freeze the complete production inventory. The file +contains every launch identity and a digest; its count is derived from the live +inventory rather than hard-coded in application logic. + +```bash +node indexer/scripts/release-candidate.mjs snapshot \ + --endpoint https://indexer.hyperindex.xyz/f6714ef/v1/graphql \ + --output /secure/release/envio-baseline.json +``` + +After the candidate reports fully synced, obtain its deployment-specific +endpoint and run the audit against the frozen baseline: + +```bash +npx --yes envio-cloud@0.10.0 deployment status \ + programmable-indexer 0xprogrammable --watch-till-synced + +npx --yes envio-cloud@0.10.0 deployment endpoint \ + programmable-indexer 0xprogrammable + +node indexer/scripts/release-candidate.mjs audit \ + --endpoint \ + --identity /secure/release/envio-candidate-identity.json \ + --baseline /secure/release/envio-baseline.json \ + --output /secure/release/envio-candidate-inventory.json +``` + +The audit fails if any frozen launch is missing or changed, any supported +release is absent, any candidate is incomplete, provenance is invalid, the +runtime identity differs, or duplicate launch identities appear. New eligible +launches are accepted only when they pass the same checks. + +## Promotion boundary + +Do not promote from Envio sync status alone. Promotion still requires the +Postgres backfill, dual-RPC reconciliation, release parity, reorg, performance, +monitoring and staged website gates in +[`READ-MODEL-RELEASE-GATE.md`](./READ-MODEL-RELEASE-GATE.md). + +Only after those gates pass may the integration owner promote the exact mirror +commit and update `config/data-pipeline-release.v1.json` to the new identity. +The website release must bind to that exact product commit. + +## Rollback + +The active rollback deployment is mirror commit +`2cb1c35c7738fea63e656ad11589664dc93d785d`. Before rollback, stop the +projector and public-route cutover, restore the pre-cutover database snapshot +and product binding, then promote the old deployment: + +```bash +npx --yes envio-cloud@0.10.0 deployment promote \ + programmable-indexer 2cb1c35 0xprogrammable --yes +``` + +Verify that the endpoint reports deployment `production-1e7c381` and the exact +rollback identity in the candidate JSON before restoring website traffic. A +promotion command by itself is not complete rollback evidence. diff --git a/docs/data-pipeline/HOSTED-DATABASE-OPERATOR.md b/docs/data-pipeline/HOSTED-DATABASE-OPERATOR.md new file mode 100644 index 00000000..bf3b35d1 --- /dev/null +++ b/docs/data-pipeline/HOSTED-DATABASE-OPERATOR.md @@ -0,0 +1,147 @@ +# Hosted database operator + +This operator prepares a fresh hosted Supabase database without linking the +repository or placing a database credential on a command line. It discovers +every canonical SQL file under `supabase/migrations`, including migrations +added after this document, and fails if the directory differs from the exact +Git commit. + +It does not configure Vercel, set role passwords, backfill data, start a +projector, enable a public read flag or promote a release. + +## Safety boundary + +The database URL is read only from +`PROGRAMMABLE_MIGRATOR_DATABASE_URL`. The operator accepts exactly: + +```text +postgresql://postgres:@db..supabase.co:5432/postgres?sslmode=verify-full +``` + +It rejects pooler hosts, port `6543`, a different project ref, an implicit +port, a different database, extra URL parameters and any SSL mode other than +`verify-full`. The expected project ref is a required explicit argument. The +CA is read only from `PROGRAMMABLE_POSTGRES_SSL_CA_PEM`. Neither value is +printed, written to a plan or accepted as a CLI flag. + +Load both secrets into the current operator session from the approved secret +manager. Do not paste either value into a tracked file, ticket, shell command +or transcript. + +## 1. Produce the reviewed migration plan + +Run this from the exact committed checkout that will be reviewed: + +```sh +node scripts/data-pipeline/hosted-db-operator.mjs plan > /secure/operator/migrations.json +``` + +The plan contains the full commit hash, every ordered filename, version, byte +length and SHA-256 digest, plus commitments to the order and the complete +plan. A noncanonical, empty, duplicate, untracked, modified or symlinked SQL +file is rejected. + +Review the plan and retain it with the release evidence. Do not regenerate it +after review. A changed or later migration produces a different plan and must +be reviewed as a new input. + +## 2. Inspect the hosted target without writing + +```sh +node scripts/data-pipeline/hosted-db-operator.mjs dry-run \ + --plan /secure/operator/migrations.json \ + --expected-project-ref +``` + +`dry-run` opens the verified direct endpoint, checks the server database, +port and minimum PostgreSQL version, then compares the remote migration +history with the exact local order. It does not create a schema or table. + +The remote history must be an exact prefix of the local plan. An out-of-order +version, remote-only version, renamed migration, missing statement record or +missing Programmable file evidence is a hard failure. The operator does not +offer a repair shortcut because that would assert file provenance it cannot +prove. + +## 3. Apply after separate authorization + +Only after the backup, local database gates, secret scan, plan review and +explicit production-change authorization, repeat the exact plan commitment as +the apply confirmation: + +```sh +node scripts/data-pipeline/hosted-db-operator.mjs apply \ + --plan /secure/operator/migrations.json \ + --expected-project-ref \ + --confirm-apply +``` + +Each pending file runs in its own transaction. Its canonical Supabase history +row and the Programmable evidence row are committed in the same transaction. +The evidence row records the version, name, order, filename, file SHA-256, +reviewed plan commitment and repository commit. A failed file is not recorded +as applied. Successfully committed earlier files remain an exact prefix and +can be resumed with the same plan. + +Apply also holds one database advisory lock for the full run. A concurrent +operator fails instead of racing the history table. + +The operator uses Supabase's canonical +`supabase_migrations.schema_migrations(version, name, statements)` shape. Its +additional `supabase_migrations.programmable_migration_evidence` table is +private operator evidence and contains no credentials. + +## 4. Verify the complete history + +```sh +node scripts/data-pipeline/hosted-db-operator.mjs verify \ + --plan /secure/operator/migrations.json \ + --expected-project-ref +``` + +`verify` exits with status `2` while any planned migration remains pending. +It is not proof of a successful backfill, release bootstrap, provider health, +projector run, parity result or public activation. + +## Separate bootstrap plan + +With the reviewed Alchemy and QuickNode URLs loaded only in their server-side +environment variables, generate the data-pipeline bootstrap plan separately: + +```sh +node scripts/data-pipeline/hosted-db-operator.mjs bootstrap-plan \ + > /secure/operator/bootstrap.json +``` + +This plan reads `config/data-pipeline-release.v1.json` through the production +release parser and computes the exact Envio, dual-RPC and official Uniswap v4 +subgraph commitments with the same production modules used by the workers. +Its output includes only redacted identities and commitments, never RPC URLs, +API keys or database credentials. + +The current release binding does not contain semantic source roles, recovery +selectors, ABI/event-set commitments, artifact creation-code commitments, +dynamic-source template evidence, RPC endpoint-evidence attestations or the +current database generation required for activation. The bootstrap plan lists +those missing values per release and source and therefore reports +`execution.ready=false`. That is intentional. Do not convert runtime-code +hashes into creation-code commitments, infer roles from contract names or +invent activation receipts. + +Bootstrap execution needs a separately reviewed manifest that supplies those +exact inputs, a database-state read at execution time and a dedicated +operator. Migration completion alone must never activate a release epoch or a +worker. + +## Failure handling + +- Operator failures are credential-safe and deliberately terse. Use the + reviewed plan, database provider logs and SQLSTATE code for diagnosis. +- Never use `migration repair` or manually insert history rows to make the + check green without independently proving the original file bytes. +- Never edit an applied migration. Add a later canonical migration and produce + a new plan. +- If a direct endpoint is unavailable, stop. Do not fall back to Supavisor or + weaken TLS validation. +- Keep both projector activation values and every indexed public-read flag + `false` until their independent release gates pass. diff --git a/docs/data-pipeline/PRODUCTION-CUTOVER-OPERATOR.md b/docs/data-pipeline/PRODUCTION-CUTOVER-OPERATOR.md new file mode 100644 index 00000000..cea0db02 --- /dev/null +++ b/docs/data-pipeline/PRODUCTION-CUTOVER-OPERATOR.md @@ -0,0 +1,305 @@ +# Production data cutover + +This runbook moves the reviewed read-model candidate into production without +letting candidate data reach public reads before its exact Envio deployment, +database state and staged website have been attested. + +The operator is fail-closed. It does not promote Envio, promote Vercel, create +the isolated restore database or change provider secrets. Those actions remain +explicit control-plane steps. It records and verifies the evidence around them. + +## Fixed identities + +| Role | Endpoint | Mirror commit | +| --- | --- | --- | +| Candidate | `https://indexer.hyperindex.xyz/d7a39a2/v1/graphql` | `7ffd15c2a28c481a2d3632e30b315262c2471b2e` | +| Rollback | `https://indexer.hyperindex.xyz/f6714ef/v1/graphql` | `2cb1c35c7738fea63e656ad11589664dc93d785d` | + +The checked-in candidate evidence is the source of truth. Do not substitute a +deployment alias, mutable URL or different mirror commit. + +## Safety rules + +1. Run from a clean checkout of the exact reviewed `production` commit. +2. Enable the reviewed indexed route flags only on the unaliased staged + deployment. The closed database publication fence keeps those routes on + legacy data until database attestation succeeds. +3. Keep the production domain on the previous Vercel deployment until the + final promotion step. +4. Use only the direct Supabase TLS endpoint on port 5432 for migrations, + credential rotation, backup and promotion attestation. +5. Never put a password, database URL, CA, provider token or RPC URL in an + argument, file committed to Git, terminal transcript or evidence output. +6. Stop on any identity, inventory, checkpoint, parity, restore or staged + deployment mismatch. Do not repair evidence manually. + +## Required environment + +The approved secret manager must inject these values into the operator process: + +```text +PROGRAMMABLE_MIGRATOR_DATABASE_URL +PROGRAMMABLE_POSTGRES_SSL_CA_PEM +PROGRAMMABLE_API_READER_DATABASE_PASSWORD +PROGRAMMABLE_PROJECTOR_DATABASE_PASSWORD +PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_PASSWORD +PROGRAMMABLE_RECONCILER_DATABASE_PASSWORD +PROGRAMMABLE_RELEASE_PROBE_DATABASE_PASSWORD +PROGRAMMABLE_PROJECTOR_DATABASE_URL +PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_URL +PROGRAMMABLE_API_READER_DATABASE_URL +PROGRAMMABLE_RECONCILER_DATABASE_URL +PROGRAMMABLE_RELEASE_PROBE_DATABASE_URL +PROGRAMMABLE_CUTOVER_RESTORE_DATABASE_URL +PROGRAMMABLE_CUTOVER_RESTORE_SSL_CA_PEM +PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL +PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL +PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN +PROGRAMMABLE_SHADOW_PROBE_TOKEN +PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN +CRON_SECRET +VERCEL_AUTOMATION_BYPASS_SECRET +VERCEL_TOKEN +VERCEL_ORG_ID +VERCEL_PROJECT_ID +``` + +The five generated role passwords must be distinct printable values of at +least 32 characters. The pooler URLs must correspond to those same passwords. + +Every evidence path below is an absolute path outside the repository. The +operator creates output files with mode `0600` and refuses to overwrite them. + +## 1. Database and credentials + +Run the reviewed migration plan and candidate bootstrap with +`hosted-db-operator.mjs` first. The candidate database must remain +`candidate-only`, unpromoted and have zero projection publications. + +Provision and verify the five login roles: + +```sh +node scripts/data-pipeline/cutover-operator.mjs roles-provision \ + --expected-project-ref \ + --output /secure/cutover/roles-provisioned.json + +node scripts/data-pipeline/cutover-operator.mjs roles-verify \ + --expected-project-ref \ + --pooler-host .pooler.supabase.com \ + --output /secure/cutover/roles-verified.json +``` + +Pooler verification uses port 6543, `prepare: false`, the exact login identity +and `SET LOCAL ROLE` for every capability role. + +## 2. Backup with tested restore + +Install PostgreSQL 17 client tools. Before starting, create an empty, +TLS-verified loopback database named +`programmable_restore_`. It must not be a production or +remote database. + +```sh +node scripts/data-pipeline/cutover-operator.mjs backup-restore \ + --expected-project-ref \ + --operation-id cutover-20260801-a \ + --restore-isolation-id cutover_20260801 \ + --backup /secure/cutover/pre-attestation.dump \ + --evidence /secure/cutover/pre-attestation-restore.json +``` + +The command rejects source drift during the backup window and accepts the +archive only when the isolated restore has the exact same manifest. + +## 3. Fenced candidate ingestion + +Run raw candidate ingestion while the publication fence is still closed: + +```sh +node scripts/data-pipeline/cutover-operator.mjs raw-backfill \ + --expected-project-ref \ + --backup-evidence /secure/cutover/pre-attestation-restore.json \ + --maximum-cycles 512 \ + --output /secure/cutover/raw-backfill.json +``` + +The run must reach an idle boundary. It fails if a projection publication +appears, the provider UUID changes or the database becomes promoted. + +## 4. Exact staged website + +Build one production-configured Vercel deployment with `--skip-domain` and +record its immutable `dpl_...` ID and `.vercel.app` URL. The reviewed indexed +route flags are enabled, shadow comparison remains disabled, and the database +publication fence remains closed. Auto-assignment of the production domain +must be disabled. + +The staged deployment must bind the reviewed candidate endpoint and the exact +product commit. Do not use a branch alias for any following check. +It must not depend on promotion values created later in the cutover. Its +immutable `VERCEL_GIT_COMMIT_SHA` and `VERCEL_DEPLOYMENT_ID` are the runtime +identity inputs. + +Create a release-gate evidence file from the reviewed staged binding and raw +backfill checks. It must carry one non-zero `evidenceSha256` or +`releaseEvidenceSha256` commitment. + +The exact staged deployment may have both projector workers enabled, but it +must remain unassigned to every production domain and scheduler. Prove its +control-plane identity, the unchanged live production binding and that every +source and market lease has drained before touching Envio: + +```sh +node scripts/data-pipeline/cutover-operator.mjs projector-drain \ + --expected-project-ref \ + --target-url https://.vercel.app/ \ + --deployment-id \ + --release-gate /secure/cutover/pre-promotion-release-gate.json \ + --output /secure/cutover/projector-drain.json +``` + +Leave that deployment unassigned through Envio and database attestation. The +gate never invokes either worker. It verifies through the Vercel control plane +that the production domain still resolves to a different deployment, then +waits for both singleton leases to be released or expired. Lease state is +observed using the database clock without exposing holder or token values. + +## 5. Promote and attest Envio + +Promote only the fixed candidate mirror commit through the Envio control +plane. Immediately capture a private observation file: + +```json +{ + "observedAt": "2026-08-01T12:00:00.000Z", + "controlPlane": { + "owner": "0xprogrammable", + "project": "programmable-indexer", + "status": "prod", + "mirrorCommit": "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + "deploymentLabel": "production-7f24e63" + }, + "runtime": { + "endpoint": "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + "endpointId": "d7a39a2", + "deploymentLabel": "production-7f24e63", + "identity": { "copy": "the exact live runtime identity object" } + } +} +``` + +The runtime identity must be copied from the live endpoint response, not from +the expected file. The operator compares it to the checked-in identity and +inventory: + +```sh +node scripts/data-pipeline/cutover-operator.mjs envio-attest \ + --observation /secure/cutover/envio-post-promotion-observation.json \ + --drain-evidence /secure/cutover/projector-drain.json \ + --output /secure/cutover/envio-promotion-attestation.json +``` + +If this command fails, immediately promote the fixed rollback mirror and leave +the Vercel production deployment unchanged. + +## 6. Attest the candidate database + +First create a deterministic database plan: + +```sh +node scripts/data-pipeline/cutover-operator.mjs database-plan \ + --expected-project-ref \ + --envio-attestation /secure/cutover/envio-promotion-attestation.json \ + --drain-evidence /secure/cutover/projector-drain.json \ + --staged-deployment-id \ + --output /secure/cutover/database-promotion-plan.json +``` + +Review every identity and commitment. Apply only by repeating the exact +`inputCommitment` from that plan: + +```sh +node scripts/data-pipeline/cutover-operator.mjs database-apply \ + --expected-project-ref \ + --envio-attestation /secure/cutover/envio-promotion-attestation.json \ + --drain-evidence /secure/cutover/projector-drain.json \ + --plan /secure/cutover/database-promotion-plan.json \ + --confirm-apply \ + --output /secure/cutover/database-promotion-result.json +``` + +This is the step that opens the database publication fence. The operator holds +an advisory transaction lock, rechecks both lease rows using database time +inside the same transaction, and atomically stores the exact product commit and +staged `dpl_...` ID with the promotion attestation. It then reads those values +back before accepting the result. A stale drain receipt cannot authorize +promotion after a lease was reacquired. + +## 7. Staged projectors, reconciliation and load gate + +Use the exact same staged deployment and `dpl_...` ID from the drain evidence. +Its source and market workers may now be invoked directly while the production +domain still points to the previous deployment. No second deployment can replace it between +drain, database attestation, gates and final promotion. +Every worker request must prove that the deployment's immutable Vercel commit +and deployment ID equal the database-bound values. No dynamically generated +promotion environment variables are part of this gate. + +```sh +node scripts/data-pipeline/cutover-operator.mjs staged-gates \ + --expected-project-ref \ + --target-url https://.vercel.app/ \ + --deployment-id \ + --drain-evidence /secure/cutover/projector-drain.json \ + --output-directory /secure/cutover/read-model-capture \ + --maximum-cycles 512 \ + --output /secure/cutover/staged-gates.json +``` + +The command requires source and market projector catch-up, one exact +checkpoint for each supported release, zero reconciliation mismatches and an +accepted load/release gate. `CRON_SECRET` and the Vercel automation bypass are +sent only in request headers and are never written to release evidence. + +Only after this file exists and has been independently reviewed may the exact +staged Vercel deployment be promoted to the production domain. Run the existing +post-promotion deployment and runtime binding checks immediately afterward. + +## 8. Rollback evidence + +Before Envio promotion, record the currently live Vercel deployment ID and Git +commit. They are deliberately allowed to differ from the candidate product +commit because production remains untouched during the staged cutover. + +```sh +node scripts/data-pipeline/cutover-operator.mjs rollback-plan \ + --envio-attestation /secure/cutover/envio-promotion-attestation.json \ + --backup-evidence /secure/cutover/pre-attestation-restore.json \ + --vercel-deployment-id \ + --vercel-product-commit \ + --output /secure/cutover/rollback-plan.json +``` + +On any failure after Envio promotion and before final Vercel promotion: + +1. Keep all public-read flags false and stop source, market and reconciliation + workers. +2. Promote the fixed rollback Envio mirror. +3. Restore the pre-attestation database snapshot, or discard all + post-attestation candidate state under an independently reviewed recovery. +4. Verify the rollback runtime identity and inventory. +5. Verify that the previous Vercel deployment and commit never changed. + +Record one committed receipt for each ordered step in a private observation +file, then validate it: + +```sh +node scripts/data-pipeline/cutover-operator.mjs rollback-verify \ + --plan /secure/cutover/rollback-plan.json \ + --observation /secure/cutover/rollback-observation.json \ + --output /secure/cutover/rollback-evidence.json +``` + +If failure occurs after Vercel promotion, first disable the public-read flags, +then restore the previous Vercel deployment in addition to the Envio and +database rollback. Reopen traffic only after all three exact identities pass. diff --git a/docs/data-pipeline/PUBLIC-ROUTE-ACTIVATION.md b/docs/data-pipeline/PUBLIC-ROUTE-ACTIVATION.md new file mode 100644 index 00000000..a8db037e --- /dev/null +++ b/docs/data-pipeline/PUBLIC-ROUTE-ACTIVATION.md @@ -0,0 +1,44 @@ +# Public route activation + +The indexed public route adapters are wired to checkpoint-bound Postgres reader +functions. Their production flags remain off. Legacy reads stay authoritative +until the database migration, projector population, same-checkpoint parity, +load and runtime gates all pass. + +## Reader contract + +Each reader returns one raw `IndexedRouteEnvelopeV2` plus independently checked +boundary evidence: + +- the exact route scope and publication records +- the route checkpoint block number and hash +- record count and record-scope evidence derived from the returned records +- parity, reconciliation and binding commitments for every requested release +- the expected HTTP status for ready, missing and pending outcomes + +The API adapter rejects missing, duplicate, incomplete, stale or differently +scoped rows. It never fills absent fields, converts an incomplete row into an +empty result, or combines data from different checkpoints. + +The current reviewed scope is Classic V2, Classic V3 and Stock-Paired V1 to V3. +Deep, Adaptive and unknown releases remain outside this boundary. + +## Activation gate + +Activate one public route at a time only after all of the following are true: + +1. The schema migration is applied and the API-reader role can execute only the + intended reader functions and readiness view. +2. The projector has populated every requested release at one immutable safe + checkpoint. +3. Authorized release probes show same-checkpoint parity for ready, empty, + missing, pending, pagination and error outcomes. +4. Stale parity, mismatch, reorg, database failure and cache-isolation tests pass. +5. The production load gate passes without an unacceptable latency or provider + regression. +6. The integration owner records the reviewed production commit, migration, + runtime evidence and rollback flag before enabling the route. + +An enabled indexed route that cannot prove a current exact snapshot must use the +configured private no-store Legacy fallback or return a private no-store `503`. +The route must never serve an unverified indexed payload. diff --git a/docs/data-pipeline/READ-MODEL-RELEASE-GATE.md b/docs/data-pipeline/READ-MODEL-RELEASE-GATE.md new file mode 100644 index 00000000..3191e7ef --- /dev/null +++ b/docs/data-pipeline/READ-MODEL-RELEASE-GATE.md @@ -0,0 +1,167 @@ +# Read model release gate + +This gate decides whether a reviewed Vercel deployment has enough current evidence to enable indexed reads. It does not deploy, promote, or change a production flag. + +## What the evidence proves + +The release bundle keeps two questions separate. + +### Dataset cardinality + +The runtime capture must contain every eligible launch returned by the private performance dataset. The gate requires at least 200 unique launches and accepts at most 400 for this profile. Do not hard-code the current production count: freeze the complete inventory immediately before each release, record its anchor and digest, and require every frozen launch plus any later eligible arrivals. + +Every launch must belong to exactly one of these releases, and every release must have at least one launch: + +- `classic-v2` +- `classic-v3` +- `stock-paired-v1` +- `stock-paired-v2` +- `stock-paired-v3` + +Token addresses and transaction hashes must be unique. Projection row counts must meet the per-launch ratios in `config/read-model-load-profile.v1.json`. + +### Throughput corpus + +The load run uses deterministic samples selected from the real cardinality dataset: + +- 100 unique eligible token addresses +- 100 unique accounts with profile or reward rows +- 32 unique Classic v3 launches +- 32 unique Stock-Paired launches +- 8 unique mainnet projector candidates + +These lists are repeated during the load run. They are not padded to match the full launch count. A repeated load sample and a complete launch inventory are different evidence. + +### Complete Explore activation matrix + +The 1,000-request throughput corpus remains a latency and capacity sample. It is +not accepted as proof that Explore pagination is complete. After that load run, +the capture therefore records a separate aggregate matrix in: + +- `explore-matrix-evidence.v1.json` +- `explore-matrix-pages.v1.jsonl` + +The matrix is derived from the complete `eligibleLaunches` inventory returned by +the same protected runtime capture. It never repeats a sampled token to make a +coverage count look complete. The manifest binds the release profile, capture +nonce, staged Vercel URL and deployment id, exact Git SHA, dataset file digest, +dataset timestamp, per-release counts, canonical inventory digest, one snapshot +block/hash commitment, query-case commitments, page artifact digest, and final +corpus digest. + +For the normalized empty query, the capture walks every real six-token page and +one `Number.MAX_SAFE_INTEGER` clamp for each supported sort: + +- `newest` +- `oldest` +- `market-cap` +- `market-cap-asc` + +Those page calls exercise every adjacent indexed cursor through the public route. +The route adapter validates each internal start/end cursor before returning, and +the signed shadow comparison must still match the independently produced legacy +response. The gate additionally reconstructs each traversal from the page +evidence and rejects any gap, duplicate token, missing token, unexpected token, +wrong page count, or clamp that does not resolve to the final real page. + +The capture also commits exactly eight unique cases per query kind for real token +names, symbols, and addresses. Cases are selected deterministically from the frozen +inventory, use trimmed mixed-case input (and `$`-prefixed symbol input), and are +accepted only when their normalized query matches their committed source token. +Each selected case is bounded to one real page, then exercised under all four +sorts plus the same maximum-page clamp. A corpus with fewer than eight distinct +bounded real cases in any query kind is rejected; synthetic values and duplicate +padding are rejected. + +The empty-query traversal must reproduce every token from each of these release +families under every sort: + +- `classic-v2` +- `classic-v3` +- `stock-paired-v1` +- `stock-paired-v2` +- `stock-paired-v3` + +All matrix pages must share one public snapshot checkpoint. If the projector +advances during capture, the checkpoint commitments differ and the release gate +fails; operators must take a fresh coherent capture instead of combining pages +from different snapshots. + +## Database boundary + +The corpus comes from `programmable_private.get_read_model_performance_dataset_v1(bigint)` and its private view. The capture must record the database identity checks from the same runtime transaction. + +The accepted evidence proves all of the following: + +- session login `programmable_projector_login` +- active role `programmable_projector` +- API reader login `programmable_api_reader_login` +- active API reader role `programmable_api_reader` +- API reader call denied with SQLSTATE `42501` +- API reader has no function execute privilege +- API reader has no view select privilege + +The public API reader cannot manufacture or retrieve this corpus. + +## Indexer replay boundary + +The Envio deployment used for a release must be the exact handler, schema and +source registry committed by the release manifest. A deployment being marked +ready is not enough. Before database backfill, enumerate the complete launch +inventory and require every eligible launch to be both complete and provenance +valid for its declared release. Raw launch counts are never promotable evidence. + +The replay must also prove release-specific identity transitions. In +Stock-Paired ETH launches, the launcher may first record the authenticated +coordinator as the provisional deployer. The coordinator event may replace that +value only when it comes from the manifest-bound coordinator and names the +actual creator. A replay that treats this transition as a conflict, accepts an +unbound coordinator, or leaves any supported release incomplete must remain +outside the projector and public route activation. + +## Signed route probes + +Each parity request carries a unique route-bound HMAC. The secret in `PROGRAMMABLE_SHADOW_PROBE_TOKEN` remains on the capture runner and server. It is never sent in an HTTP header. + +The signed payload is: + +```text +programmable-release-probe-v1 + + +``` + +The deployment validates freshness, route binding, signature, and replay before returning private probe headers. Probe responses must use `Cache-Control: private, no-store`. + +## Load and parity contract + +The profile runs 1,000 distributed real requests over at least 60 seconds with observed concurrency of at least 20. Every request must be unique, reach the origin, return a successful status, and match its requested dataset key. + +The release is rejected if any of these conditions occur: + +- any HTTP error +- any cache hit or stale response +- any missing real sample key +- any route p95 or p99 above its configured budget +- any parity result other than `match` +- any missing or true `x-programmable-live-fallback` value +- fewer than 880 signed comparison samples +- any projector deadline, retry, provider, candidate, or commitment mismatch +- any missing Explore matrix sidecar or digest mismatch +- any matrix response that is cached, non-200, non-`match`, missing a signed + probe measurement, or reports a fallback +- any missing sort, query case, real page, internal-cursor traversal, clamp, token, + release family, or single-checkpoint binding + +## Release sequence + +1. Build a clean integration commit. +2. Deploy that exact commit to a deployment-specific Vercel URL without assigning the production domain. +3. Capture the private runtime dataset, raw dual-RPC trace, signed throughput + samples, and the separately committed complete Explore matrix. +4. Verify artifact digests, Git SHA, deployment ID, Vercel project, response + identities, cache contracts, latency, parity, fallback state, inventory/page + completeness, clamping, and the single matrix checkpoint. +5. Promote only when every check passes and the integration owner has approved publication. + +A local test, simulation, preview build, or successful provider request is not production activation evidence. diff --git a/docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z.json b/docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z.json new file mode 100644 index 00000000..f7618a3b --- /dev/null +++ b/docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z.json @@ -0,0 +1,497 @@ +{ + "schemaVersion": 2, + "kind": "envio-release-inventory", + "endpoint": "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + "capturedAt": "2026-08-01T04:20:59.886Z", + "deployment": { + "provider": "envio-cloud", + "owner": "0xprogrammable", + "project": "programmable-indexer", + "mirrorCommit": "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + "deploymentLabel": "production-7f24e63", + "endpointId": "d7a39a2" + }, + "identity": { + "deployment": "production-7f24e63", + "sourceCommit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + "handlerSha256": "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + "sourceRegistrySha256": "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51 + }, + "baseline": { + "digest": "0xc826c240381a57295707e138bd49ceddef18abd81e5e19731dffb4a160d8e09f", + "deployment": { + "provider": "envio-cloud", + "host": "indexer.hyperindex.xyz", + "endpointId": "f6714ef", + "deploymentLabel": "production-1e7c381", + "chainId": 1 + }, + "anchor": { + "progressBlock": "25657578", + "bufferBlock": "25657578", + "sourceBlock": "25657590", + "eventsProcessed": "56002", + "stateProgressBlock": "25657571", + "stateProgressBlockHash": "0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716", + "stateProgressTimestamp": "1785557819", + "stateProgressTransactionHash": "0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1", + "stateProgressOccurrenceId": "1:0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716:0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1:778" + }, + "inventory": { + "count": 265, + "perRelease": { + "classic-v2": 27, + "classic-v3": 186, + "stock-paired-v1": 1, + "stock-paired-v2": 8, + "stock-paired-v3": 43 + }, + "sha256": "0x5a388ae00ff52fd63abf45560cdb456cafe883c17249cabce83ca31286104c6d" + } + }, + "anchor": { + "progressBlock": "25657578", + "bufferBlock": "25657578", + "sourceBlock": "25657590", + "eventsProcessed": "56002", + "stateProgressBlock": "25657571", + "stateProgressBlockHash": "0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716", + "stateProgressTimestamp": "1785557819", + "stateProgressTransactionHash": "0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1", + "stateProgressOccurrenceId": "1:0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716:0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1:778" + }, + "inventory": { + "count": 265, + "perRelease": { + "classic-v2": 27, + "classic-v3": 186, + "stock-paired-v1": 1, + "stock-paired-v2": 8, + "stock-paired-v3": 43 + }, + "sha256": "0xa63d33aaee6065a612cd2a318496e9379c20389d3ca8fe0663919d25e6f2ad04" + }, + "authenticatedCoordinatorCreatorRepairs": [ + { + "id": "1:stock-paired-v1:0x5700d903e959f41e091dab41c1e5582cd9400a1193dad48b1f8141b0cc29ac3b", + "releaseVersion": "stock-paired-v1", + "priorCoordinatorSource": "0xfa5f17389ca28d071781d59750b32c842ab6a54b", + "authenticatedCreator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "launchOccurrenceId": "1:0x8172ac34f638aa3a35728f233f2a380a96d36006e9c6591b4098a7ff4791003f:0x02a71e6e1854143e3e298edb64af7e5486465829a8e7f33078b75a32aa06ed90:316", + "coordinatorOccurrenceId": "1:0x8172ac34f638aa3a35728f233f2a380a96d36006e9c6591b4098a7ff4791003f:0x02a71e6e1854143e3e298edb64af7e5486465829a8e7f33078b75a32aa06ed90:321" + }, + { + "id": "1:stock-paired-v2:0x2f5dc5c369e5e1f33e664e15c96cf2d33a00bc55f1acf2b14788f14602604cb4", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0xb38ef2aa4306c8a2ce97d8718aa3dda7a0ba331c", + "launchOccurrenceId": "1:0x85cb7872a3a86d009687ff028c645c5d2cbcc447b3d224fdffc2076352f8ef22:0x72a5155df768dbd68c5bc75672872ae94ea3a44f3c18abd819a31b6cd98c3bd5:81", + "coordinatorOccurrenceId": "1:0x85cb7872a3a86d009687ff028c645c5d2cbcc447b3d224fdffc2076352f8ef22:0x72a5155df768dbd68c5bc75672872ae94ea3a44f3c18abd819a31b6cd98c3bd5:86" + }, + { + "id": "1:stock-paired-v2:0x49e441f535f82be6234dea14f23e52154669b2ca0c8f98747dc95f6a6e099312", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "launchOccurrenceId": "1:0xa16a0d36cde8f79db93e31e98337c1898e2c6fb7e8b5dbc36d3752a375a60287:0xc45d348083c53afaf79f056f1ea5529e9410ac3faa954a5c8ef7272a6371ec83:948", + "coordinatorOccurrenceId": "1:0xa16a0d36cde8f79db93e31e98337c1898e2c6fb7e8b5dbc36d3752a375a60287:0xc45d348083c53afaf79f056f1ea5529e9410ac3faa954a5c8ef7272a6371ec83:953" + }, + { + "id": "1:stock-paired-v2:0x4f3a4a1d87d06dafb214d3cdc03b6157abfeeaf9cd3110a5500230fff247b35a", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0xa8cac4b2d5078b0eee808d5b2bee638784f4ae73", + "launchOccurrenceId": "1:0x6ae6b0daa49763f5aa574fef5dff3ed8cd07574f0cfac09afa7692dcdc6ebe6f:0xd49746082b301c1dff6222ffb9e6a08628c3837d111c3cf538bec3ec7b747259:132", + "coordinatorOccurrenceId": "1:0x6ae6b0daa49763f5aa574fef5dff3ed8cd07574f0cfac09afa7692dcdc6ebe6f:0xd49746082b301c1dff6222ffb9e6a08628c3837d111c3cf538bec3ec7b747259:137" + }, + { + "id": "1:stock-paired-v2:0x653457e36314263c4641240f9c05a57509edc6d9b5358385f96dec1c3a905339", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0x3fe3fc76538777442787a38256e19aeaf49f65c0", + "launchOccurrenceId": "1:0x5066b7abf547d9d256ea2afd267d489144825dd2f7eb51a8a0cf4c5151c08cf9:0xdfe9b68b48a7d8f2a06df9eb2436f573cc04cd94e93bf0655055bc2073147fbc:515", + "coordinatorOccurrenceId": "1:0x5066b7abf547d9d256ea2afd267d489144825dd2f7eb51a8a0cf4c5151c08cf9:0xdfe9b68b48a7d8f2a06df9eb2436f573cc04cd94e93bf0655055bc2073147fbc:520" + }, + { + "id": "1:stock-paired-v2:0x8f7cf6d844d4aaa85312456b3a3f2b74a9c84e4817acf733196f6c4099eb7e36", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0xb0095c3e6ac1a4f936df3914e8cc46783b9b5287", + "launchOccurrenceId": "1:0x175df0b7427e18e392a039c85f348c74f1c9f2e239479ca60dbc6abfbf4002c4:0x8c26d38ae601fc607e7d9822e09e6019451dff3a470647266a2b1783dc5a2090:352", + "coordinatorOccurrenceId": "1:0x175df0b7427e18e392a039c85f348c74f1c9f2e239479ca60dbc6abfbf4002c4:0x8c26d38ae601fc607e7d9822e09e6019451dff3a470647266a2b1783dc5a2090:357" + }, + { + "id": "1:stock-paired-v2:0xdb6bcd8648aab014ec3121a9090b4af9b3a46a2bfa81b0ac50c3b6e26ba09e08", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0xa55d439f9ad4d1e2a043aed538d4c771a3e394bb", + "launchOccurrenceId": "1:0x9817c330a494baf1ccd4f07f99a041338e7bc357ea69ceeabc339f45af2cb8b1:0x440a08fb7e00ecd039c2594faf7ce085fd4019803b5b90cd3c2b8aad6cf014a5:327", + "coordinatorOccurrenceId": "1:0x9817c330a494baf1ccd4f07f99a041338e7bc357ea69ceeabc339f45af2cb8b1:0x440a08fb7e00ecd039c2594faf7ce085fd4019803b5b90cd3c2b8aad6cf014a5:332" + }, + { + "id": "1:stock-paired-v2:0xf03e343145583e84e6c2eb413d19e8b7674dd2a50817ade8212165e0970aaeaa", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0x5a237f89d1db282e40188a041b142dcde9ebe548", + "launchOccurrenceId": "1:0x1e9ec9537c0926f649d131290c828d6810bf2ca6efcf1f583df45a98d092da24:0xd243eb7d5e2591e282829429bcfd4714c19f9adf94de15a496f83388597014a6:674", + "coordinatorOccurrenceId": "1:0x1e9ec9537c0926f649d131290c828d6810bf2ca6efcf1f583df45a98d092da24:0xd243eb7d5e2591e282829429bcfd4714c19f9adf94de15a496f83388597014a6:679" + }, + { + "id": "1:stock-paired-v2:0xf7081331212fb2d81242ee8afa941d4ef774f8b17260ed109b30712a783080af", + "releaseVersion": "stock-paired-v2", + "priorCoordinatorSource": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "authenticatedCreator": "0x5094e658f06a90058abdbbd97b09c95856c81534", + "launchOccurrenceId": "1:0xc4d1eb4f4fd4f19cbf7ffc182b237ad3349e88c5eeb06bd6ed782b3597e3b773:0xc47e44a3c813b1904a2d00f5f93268a8ddcaf98ca2416d5a5e2cb40aa1e106dc:1007", + "coordinatorOccurrenceId": "1:0xc4d1eb4f4fd4f19cbf7ffc182b237ad3349e88c5eeb06bd6ed782b3597e3b773:0xc47e44a3c813b1904a2d00f5f93268a8ddcaf98ca2416d5a5e2cb40aa1e106dc:1012" + }, + { + "id": "1:stock-paired-v3:0x0329b08cc21cfd2e61e7c80c21bf69adf05d41b141494592c0e12cd0f06dba6f", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "launchOccurrenceId": "1:0x474014ff30cb4b0a06493198548343eef4c67dbece578b7d573479940a9470fa:0x3c91042b70e7804145b57a43d3b1b6b4fda8ebbc64713c1a9b18274dec0838bc:32", + "coordinatorOccurrenceId": "1:0x474014ff30cb4b0a06493198548343eef4c67dbece578b7d573479940a9470fa:0x3c91042b70e7804145b57a43d3b1b6b4fda8ebbc64713c1a9b18274dec0838bc:37" + }, + { + "id": "1:stock-paired-v3:0x0677820a202c395b72da3f60022cf9e318ad86d2c9680ab3663fd4dab140a8ae", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xdea99a37194a83a0825eb608b9f00c64630fee1c", + "launchOccurrenceId": "1:0x3a509c29aaef823668725157cfab95fba31bbd994bc2f5bcfc1050d46eccf6a6:0x8fb609c1511099a99bccbb9a8eb42a54c3c4142e60ef29442f104ca1568d628e:121", + "coordinatorOccurrenceId": "1:0x3a509c29aaef823668725157cfab95fba31bbd994bc2f5bcfc1050d46eccf6a6:0x8fb609c1511099a99bccbb9a8eb42a54c3c4142e60ef29442f104ca1568d628e:126" + }, + { + "id": "1:stock-paired-v3:0x0e23f2508a69495a00eaff11f8d33d213a054da585df710758fa7d0899c49569", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x70ab92f4dbff5270830e61bf8358a64614a01817", + "launchOccurrenceId": "1:0x10fb6c2520d643ad04ec384ff5e022993c3c76d1a601bea67c4f9d000eecaf8e:0xe426631e76642c942205c3708b71daf3dad3b876666ac9d31129ce9c9bb322e6:294", + "coordinatorOccurrenceId": "1:0x10fb6c2520d643ad04ec384ff5e022993c3c76d1a601bea67c4f9d000eecaf8e:0xe426631e76642c942205c3708b71daf3dad3b876666ac9d31129ce9c9bb322e6:299" + }, + { + "id": "1:stock-paired-v3:0x125293294c7d8add7c30b4082103ef825a8e75a9068a8fcecdd24e0f3a57ed52", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xc6bc971163f3916bcfde1c583524b31cd2c2c1c3", + "launchOccurrenceId": "1:0x121e059996726f2017eec251624ab01ea2a30c0bd269ecbda895347371654ac5:0xf01261aae6cb3cbf8a5a9456689f4e9fa8c77a5069e406312f6c362eefee1785:477", + "coordinatorOccurrenceId": "1:0x121e059996726f2017eec251624ab01ea2a30c0bd269ecbda895347371654ac5:0xf01261aae6cb3cbf8a5a9456689f4e9fa8c77a5069e406312f6c362eefee1785:482" + }, + { + "id": "1:stock-paired-v3:0x156c8836a0e4236f05510e432a2c99272d1681fab982ad7da684220fd85f25f1", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xf3e3ac45ac46ec098fa9f8a812c578cff7d348a1", + "launchOccurrenceId": "1:0x9b66a1366edca1cc239748ce4f74e644944ccee5816e04603ceb8d6bad5c5ef8:0x57269c513e6aa1dadcbe8e09a8e24f3364093339cc0ee0aab40ab43c9c4a6403:523", + "coordinatorOccurrenceId": "1:0x9b66a1366edca1cc239748ce4f74e644944ccee5816e04603ceb8d6bad5c5ef8:0x57269c513e6aa1dadcbe8e09a8e24f3364093339cc0ee0aab40ab43c9c4a6403:528" + }, + { + "id": "1:stock-paired-v3:0x26442d0657af85780239c3fba19674e02bf8f391dce5011ac54fbf3c9ae667d7", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "launchOccurrenceId": "1:0xa6d84ea28a2e0acecdc14a68ff8994e979c764b4d39318c743f9a66349370731:0xe1a7a0ee4f46507cb6fd89437dd717663d2a3ebcebcb3b240baad5bafa6d2147:44", + "coordinatorOccurrenceId": "1:0xa6d84ea28a2e0acecdc14a68ff8994e979c764b4d39318c743f9a66349370731:0xe1a7a0ee4f46507cb6fd89437dd717663d2a3ebcebcb3b240baad5bafa6d2147:49" + }, + { + "id": "1:stock-paired-v3:0x27f49ec995bd6e2449a846a00a378bd6e0dc021f4ac2c3cc7d725c5357eccf77", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "launchOccurrenceId": "1:0x7402cd76c3280f4e210576ab71dc08a08cf73d1c326c6526c7690b9274938176:0x2c4af90d1e1d20da9be2c9decb8fffce74e1bbc9fb136d448fb1fce2077b86c9:29", + "coordinatorOccurrenceId": "1:0x7402cd76c3280f4e210576ab71dc08a08cf73d1c326c6526c7690b9274938176:0x2c4af90d1e1d20da9be2c9decb8fffce74e1bbc9fb136d448fb1fce2077b86c9:34" + }, + { + "id": "1:stock-paired-v3:0x2d056c0b921e087947c29cc65ebb7d3c2eedae0d144271db05380ee1fbb5e703", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x7d21623327f3047aa3b1e71ba226c573c37afcdd", + "launchOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xb5fef54843b095c231dc746f90795069f934e1966e89c41eef6b0f63bb7b1a16:149", + "coordinatorOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xb5fef54843b095c231dc746f90795069f934e1966e89c41eef6b0f63bb7b1a16:154" + }, + { + "id": "1:stock-paired-v3:0x2ee3a3d02bfd3286276511c8b814e8118aee4fa0c9c624a158fc738974c5b721", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xfa50de214e13302e01bdf635d5204ff26b84ea60", + "launchOccurrenceId": "1:0x333869646bdd5df10a9f5abc434bb722579a0d26baffca69f9320b3293a8fe0a:0xd4a7cd5b0c22cbe4595d9840c23cea7b24202915bf39e7fdbd835f50fac14ae4:1265", + "coordinatorOccurrenceId": "1:0x333869646bdd5df10a9f5abc434bb722579a0d26baffca69f9320b3293a8fe0a:0xd4a7cd5b0c22cbe4595d9840c23cea7b24202915bf39e7fdbd835f50fac14ae4:1270" + }, + { + "id": "1:stock-paired-v3:0x352df2e41e33728dbda751dfe2752e747dc21580fdade6eaed3f81a78db1c17a", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x4a09dbe91d6b74a44f8bdbcd61459d8d51bee25b", + "launchOccurrenceId": "1:0xd3bbe6d051400b72d4a64597abeec532ee017a85babdc5a1fdaf73d907984a3c:0x3521acd3736309d435233b586e080854bfeef9b59730b86a067f311658a42bff:320", + "coordinatorOccurrenceId": "1:0xd3bbe6d051400b72d4a64597abeec532ee017a85babdc5a1fdaf73d907984a3c:0x3521acd3736309d435233b586e080854bfeef9b59730b86a067f311658a42bff:325" + }, + { + "id": "1:stock-paired-v3:0x356dc81234f3f4c8c5535be0fc2df6aabfebef6808c23d4ae82dbdf8ffcfe316", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x1e3add1956238d739353b5f7949d7b21ef6efcb0", + "launchOccurrenceId": "1:0xbe755c8ba25796f80aaef0847c691578db4c0ebc007b16417273888812a8a82b:0x7f6e8c2a03a9102dbb177f90e6a166df3cbb0ca20c210644301856e82c4994a3:29", + "coordinatorOccurrenceId": "1:0xbe755c8ba25796f80aaef0847c691578db4c0ebc007b16417273888812a8a82b:0x7f6e8c2a03a9102dbb177f90e6a166df3cbb0ca20c210644301856e82c4994a3:34" + }, + { + "id": "1:stock-paired-v3:0x47a4e2f097fa40a211b9b114c958a7efd21ff2baa319b1689cc75a0005cde4bf", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x70ab92f4dbff5270830e61bf8358a64614a01817", + "launchOccurrenceId": "1:0x891ea663f66342102582bf10482a465f7efec40f46515c7abb99aba768697e65:0x7fcd975c784b3b705957d7cc897b46ed499e4b74317bc49e9cb2840ad92ac14a:46", + "coordinatorOccurrenceId": "1:0x891ea663f66342102582bf10482a465f7efec40f46515c7abb99aba768697e65:0x7fcd975c784b3b705957d7cc897b46ed499e4b74317bc49e9cb2840ad92ac14a:51" + }, + { + "id": "1:stock-paired-v3:0x4c9dab0a2ce086adb44efe400bde649f2e9e851ebcf9f7843ce35ea573f6fd3a", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xdea99a37194a83a0825eb608b9f00c64630fee1c", + "launchOccurrenceId": "1:0x010a9ff11ec84bd80c6e2ead02ca2e4990c1b75f8779074ca8d6a8f2307bd02f:0x066d1e1e22129e0b30d0f35f47e1cbcfcd367b7fdae8431a31b16cdfe132c514:198", + "coordinatorOccurrenceId": "1:0x010a9ff11ec84bd80c6e2ead02ca2e4990c1b75f8779074ca8d6a8f2307bd02f:0x066d1e1e22129e0b30d0f35f47e1cbcfcd367b7fdae8431a31b16cdfe132c514:203" + }, + { + "id": "1:stock-paired-v3:0x4ca78fdac94e0059e491f9cd49150bdba20ca1d6b67283062d19668190a40127", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xf3e3ac45ac46ec098fa9f8a812c578cff7d348a1", + "launchOccurrenceId": "1:0x8dae32726dbb785f4521e12a76b9d6772c2a734a71a478886cc0deb197c48464:0x480705f1b4a2534833dc56888a8c9f8f1b638260366016a220b58340a66ab133:565", + "coordinatorOccurrenceId": "1:0x8dae32726dbb785f4521e12a76b9d6772c2a734a71a478886cc0deb197c48464:0x480705f1b4a2534833dc56888a8c9f8f1b638260366016a220b58340a66ab133:570" + }, + { + "id": "1:stock-paired-v3:0x4e159f23625e10606cebe7c7267a77c07fb727ebb551c39f38f66c252a70dcc1", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x793a5e8b8ff431cc2d8ee41e8ec2d9ad70247e60", + "launchOccurrenceId": "1:0xa9b35f50fd75862f62e7d01699ff4e428371956a5c5f61c342eed563e2c9c941:0x32950b2eed832c7bcc54fc72f1c263a89035a566f85aee07d53fdc7220f14c55:358", + "coordinatorOccurrenceId": "1:0xa9b35f50fd75862f62e7d01699ff4e428371956a5c5f61c342eed563e2c9c941:0x32950b2eed832c7bcc54fc72f1c263a89035a566f85aee07d53fdc7220f14c55:363" + }, + { + "id": "1:stock-paired-v3:0x565eee3e69ef553665845451f70b506c4591ac39b91fdfa2dde7b5a6f75d2f84", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xe0e4cba1f6d9aeffe30189c124f39fd3d131093d", + "launchOccurrenceId": "1:0x067d3e8f739fdb98163f78333f3cb30f83ae6814fb622877e9ede212e307940e:0xd0df87fb583b413eb656cfd0379ae162fbaf01a344c684b463461bab9d55c004:575", + "coordinatorOccurrenceId": "1:0x067d3e8f739fdb98163f78333f3cb30f83ae6814fb622877e9ede212e307940e:0xd0df87fb583b413eb656cfd0379ae162fbaf01a344c684b463461bab9d55c004:580" + }, + { + "id": "1:stock-paired-v3:0x5ac643e9d116b7650216029bda6bd494bf81b26acf19cf5554548e12dabf1e1c", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x46fdf1633a42b792b362c2a2e247f606beda6b10", + "launchOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0x804781f3d3b789e8a8b47a85892b2ff1ca4d507a8232594a6e373f280fddbedc:67", + "coordinatorOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0x804781f3d3b789e8a8b47a85892b2ff1ca4d507a8232594a6e373f280fddbedc:72" + }, + { + "id": "1:stock-paired-v3:0x6309907bb3942da175ba911450029343e016f4a400c2d2cb14af881a35589a6e", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x66ed6a79e084a99982b6cb861981c98ac7d0d468", + "launchOccurrenceId": "1:0xeee1c4f24eb4200199691abfb62e0e36bef0e784bdf177125f83712b040085ca:0xfe1d0d3dd380f3e2506b15c3d302491e23579d057903edd8c99ab4649c62cc6b:450", + "coordinatorOccurrenceId": "1:0xeee1c4f24eb4200199691abfb62e0e36bef0e784bdf177125f83712b040085ca:0xfe1d0d3dd380f3e2506b15c3d302491e23579d057903edd8c99ab4649c62cc6b:455" + }, + { + "id": "1:stock-paired-v3:0x63883df2fbc586a0d8bbeee123e97e25135d47d4de9a822fddb43fe3d7242365", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x70ab92f4dbff5270830e61bf8358a64614a01817", + "launchOccurrenceId": "1:0xb3e610525f9fa81fe2900a4812270065496f31a7e53bab4350f6e9110dbc3b7d:0x2de336e707ee73a72e4461104b8a712e14a87d9eb761971e70306cfffcc11fed:370", + "coordinatorOccurrenceId": "1:0xb3e610525f9fa81fe2900a4812270065496f31a7e53bab4350f6e9110dbc3b7d:0x2de336e707ee73a72e4461104b8a712e14a87d9eb761971e70306cfffcc11fed:375" + }, + { + "id": "1:stock-paired-v3:0x852d8f5e60a18ef544fcba49595e4ddf57937c3c1319714cbed4476413b425d4", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "launchOccurrenceId": "1:0x12149525c9d6b5dc90ad12f8514a3c7df463f8321e565b1c6b0e164e1e516a8a:0xef9656a84204becbbff4481e13d2ae688bd77dc1e8db9b1241910cff07559533:591", + "coordinatorOccurrenceId": "1:0x12149525c9d6b5dc90ad12f8514a3c7df463f8321e565b1c6b0e164e1e516a8a:0xef9656a84204becbbff4481e13d2ae688bd77dc1e8db9b1241910cff07559533:596" + }, + { + "id": "1:stock-paired-v3:0x9028fbb0a68bf629e6db35f21a45a372496fcbdc3fdd1f145470883e820c6d60", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x195efa8b60470d9db33446b28b88e2572407b2e3", + "launchOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0xbfc859a0ba5b79a7a9bdfeadfe69565fca513295cb8226b4aece086db4aa6584:472", + "coordinatorOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0xbfc859a0ba5b79a7a9bdfeadfe69565fca513295cb8226b4aece086db4aa6584:477" + }, + { + "id": "1:stock-paired-v3:0x9705591c7060367133cdccf539659d2f21b320e9d2731d4132c1909da6ab9b39", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x5e3cdb002e48c1391138abca7c4bd5f2998d7485", + "launchOccurrenceId": "1:0xed5ae2c389827818d3788d09398f73a548502c5f0d3d580fd6c325254c728d0d:0xc4733edff036e2efd6f7c578a12ac258bfecf7d56f285444c9b351d32c0fee16:263", + "coordinatorOccurrenceId": "1:0xed5ae2c389827818d3788d09398f73a548502c5f0d3d580fd6c325254c728d0d:0xc4733edff036e2efd6f7c578a12ac258bfecf7d56f285444c9b351d32c0fee16:268" + }, + { + "id": "1:stock-paired-v3:0x991b1c6dec03a051e906215bd5ce7f0a9342fb9c5c0e0b07e14d6bc719d6945a", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xb3940904537334e359246e327235e5db25d175e4", + "launchOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xd8559a2fe164dcb17a63a81b45eee21a85d45ee24e2fc1350e3b3dbd5e9469bf:213", + "coordinatorOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xd8559a2fe164dcb17a63a81b45eee21a85d45ee24e2fc1350e3b3dbd5e9469bf:218" + }, + { + "id": "1:stock-paired-v3:0x9dcdb5ef3c66f3495c58f36078e32787489c23edfa9f2aa161100232fedbe4dd", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "launchOccurrenceId": "1:0x944b988d33908da3a3d2dc623d83d6fab7fdae1edfabe7bb1bb59c1b7cecbabf:0x62b0af876ee6a2b6b96e947036b57f2a2a70ce46d5a29e8a5589b6be87188f2f:182", + "coordinatorOccurrenceId": "1:0x944b988d33908da3a3d2dc623d83d6fab7fdae1edfabe7bb1bb59c1b7cecbabf:0x62b0af876ee6a2b6b96e947036b57f2a2a70ce46d5a29e8a5589b6be87188f2f:187" + }, + { + "id": "1:stock-paired-v3:0xa12cecc9365e610c28c689e060afda473f8f6c663f7fb6038c810fbba93c8ddc", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xc112fdf5dc4d896a3e93e1ab75ea7fa1e60b407b", + "launchOccurrenceId": "1:0xbd8a197b72a542fef88169d23bcd122add9512194cf0f96ea830436f3ea6e919:0x562aa9493ef108261df9dfe9a794ff1ecbfc83d8b2b60108d3f2bd4cbb473c9e:94", + "coordinatorOccurrenceId": "1:0xbd8a197b72a542fef88169d23bcd122add9512194cf0f96ea830436f3ea6e919:0x562aa9493ef108261df9dfe9a794ff1ecbfc83d8b2b60108d3f2bd4cbb473c9e:99" + }, + { + "id": "1:stock-paired-v3:0xa9563564fc0cd284a0772cfd17e7a9958ed107b885e385def7f8df055ae0166f", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x5cb4a95b28a524e9260ceddd861bb1f2a73fc43a", + "launchOccurrenceId": "1:0xe12447b4136dd6478dbf2c3afcb17a595479b67b7dc089167be2466132bb8af9:0x6b250aa99fdc3717785dd5c5b8c882d27a1962487e54b42710234b4c6ced3670:357", + "coordinatorOccurrenceId": "1:0xe12447b4136dd6478dbf2c3afcb17a595479b67b7dc089167be2466132bb8af9:0x6b250aa99fdc3717785dd5c5b8c882d27a1962487e54b42710234b4c6ced3670:362" + }, + { + "id": "1:stock-paired-v3:0xa9924bf955de80ca5906ce23f0da346623e58f55a21a10280ac5c79c2cdf1712", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x4321dd0a9fcc4ae18202bdf734df40dd959156df", + "launchOccurrenceId": "1:0xb372148134e6e2017f79ad04d59e98301e2d22285d63578f9354488ea0d1aee4:0x6297a7d6fb27cd8c1dd6d713fd5c25f63378b748bec074263ef47b90060fdde1:347", + "coordinatorOccurrenceId": "1:0xb372148134e6e2017f79ad04d59e98301e2d22285d63578f9354488ea0d1aee4:0x6297a7d6fb27cd8c1dd6d713fd5c25f63378b748bec074263ef47b90060fdde1:352" + }, + { + "id": "1:stock-paired-v3:0xaae2328c84cc26ee531ebcefe8d725842908e07d89b17d33cc2d7d705d2d47b6", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "launchOccurrenceId": "1:0xcfe9f755e0ed2030a8e57d06da534b62ca48e1abdb2f1ad4ab2f5f29d479116e:0xbb4b839550a80136b568c8be530ca1e61fec51ff9b780d85558676ccf2393d07:266", + "coordinatorOccurrenceId": "1:0xcfe9f755e0ed2030a8e57d06da534b62ca48e1abdb2f1ad4ab2f5f29d479116e:0xbb4b839550a80136b568c8be530ca1e61fec51ff9b780d85558676ccf2393d07:271" + }, + { + "id": "1:stock-paired-v3:0xb36b2ea3dc8e386c919eaae08d44614a82454daea59d833eaab7a4d6e0152947", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "launchOccurrenceId": "1:0x5515e0ca689db90886f7f68784e2e2564dc0dab175020bb9dfdd3051e6d07149:0x2ab24d4368831fc64bc22f518fb625e321a550bb461137aec2911758eb160af3:312", + "coordinatorOccurrenceId": "1:0x5515e0ca689db90886f7f68784e2e2564dc0dab175020bb9dfdd3051e6d07149:0x2ab24d4368831fc64bc22f518fb625e321a550bb461137aec2911758eb160af3:317" + }, + { + "id": "1:stock-paired-v3:0xb411f6d0d3f4131a2b4269dbc2b01c0ac586d3c9ba8ee11bf753b60878da024f", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x7d21623327f3047aa3b1e71ba226c573c37afcdd", + "launchOccurrenceId": "1:0x380b88e8cde3a26eb57cb27fd541865b89262180a5e1d1fca2d16ade5198db17:0x32e81a5eecc6b42a004062cecd329b366d209d7bb85143adf43fed4112dd797e:358", + "coordinatorOccurrenceId": "1:0x380b88e8cde3a26eb57cb27fd541865b89262180a5e1d1fca2d16ade5198db17:0x32e81a5eecc6b42a004062cecd329b366d209d7bb85143adf43fed4112dd797e:363" + }, + { + "id": "1:stock-paired-v3:0xb5fc0272b7e574f61e9b4f69fc502ae2a51c640fb0eaba8e3b7be8729bbbb99f", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xd0d18801e55c24793d062989661ef219b8821e25", + "launchOccurrenceId": "1:0x6e27661507b76ff66615b88102ecf69e23a94e328b63afd70129ba7116f9580f:0x0d65f767382e5fc4dde403d55037d748da3f3e42aef17a19c34f40b103277852:230", + "coordinatorOccurrenceId": "1:0x6e27661507b76ff66615b88102ecf69e23a94e328b63afd70129ba7116f9580f:0x0d65f767382e5fc4dde403d55037d748da3f3e42aef17a19c34f40b103277852:235" + }, + { + "id": "1:stock-paired-v3:0xbb7d2e0af0925b41f510005229982a86b327d2fdc6df0ec01521beddef2b54f4", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x70ab92f4dbff5270830e61bf8358a64614a01817", + "launchOccurrenceId": "1:0x8f24866db686474ae3ca518ed198b20ea64434654f9afbf0590fd2c62cdd65fa:0x6d183f2ede2bed4d4c9ff850a5737a2934168f6bad2bb8b0ff625cd8fc08b7d8:472", + "coordinatorOccurrenceId": "1:0x8f24866db686474ae3ca518ed198b20ea64434654f9afbf0590fd2c62cdd65fa:0x6d183f2ede2bed4d4c9ff850a5737a2934168f6bad2bb8b0ff625cd8fc08b7d8:477" + }, + { + "id": "1:stock-paired-v3:0xbfce9d391b98ca42478566735471cee3105eac0bc5f2e98816066191cad0bce4", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x70ab92f4dbff5270830e61bf8358a64614a01817", + "launchOccurrenceId": "1:0x83ed4cf100feab4e36e588d0436eb46a4c8e0dcbbdf1a69cb5a42f0b7dcf7e4e:0x2deeefb14f794287849c68f152bd4a32b9f5ff42b97a13ee9a1b1ec8613c8ef0:348", + "coordinatorOccurrenceId": "1:0x83ed4cf100feab4e36e588d0436eb46a4c8e0dcbbdf1a69cb5a42f0b7dcf7e4e:0x2deeefb14f794287849c68f152bd4a32b9f5ff42b97a13ee9a1b1ec8613c8ef0:353" + }, + { + "id": "1:stock-paired-v3:0xc95aae95148a9f10ed0340a7528246634e926459c09bcadf615aeea21d017e9e", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "launchOccurrenceId": "1:0xc027186d70dc796f3add3a1a6d508cb454ac8222e23859fe8692e7ba571e67bd:0xbe52bd2bb71159a1f6ab085cb7c7d5eb4f0583608b8adaf7d62f283be6693b19:921", + "coordinatorOccurrenceId": "1:0xc027186d70dc796f3add3a1a6d508cb454ac8222e23859fe8692e7ba571e67bd:0xbe52bd2bb71159a1f6ab085cb7c7d5eb4f0583608b8adaf7d62f283be6693b19:926" + }, + { + "id": "1:stock-paired-v3:0xd3bf87a197d72a0beaca1b605f73f15b84db3c754f6b6c25e40ff0b04ba8bc08", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xfa50de214e13302e01bdf635d5204ff26b84ea60", + "launchOccurrenceId": "1:0xea35ed5ca4e1d3eff2395cc39bffe1acf7a5e0adc08d2f6a5db62bc658c1a8bf:0xbe41f565ca216725d15c49de8644b857189c1027e6147425e124976826f6ab2b:532", + "coordinatorOccurrenceId": "1:0xea35ed5ca4e1d3eff2395cc39bffe1acf7a5e0adc08d2f6a5db62bc658c1a8bf:0xbe41f565ca216725d15c49de8644b857189c1027e6147425e124976826f6ab2b:537" + }, + { + "id": "1:stock-paired-v3:0xe317c724a274f7e51c6b330b95a21b3f8d9708dd6c760a3b20816ca26af453b7", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xc112fdf5dc4d896a3e93e1ab75ea7fa1e60b407b", + "launchOccurrenceId": "1:0xae2fc5adc70352a091b122311dc3dfe737c9991187c8a07fff53749b59259690:0x92997af2820c02d7f84b8dd6f12049164c788de7bdc5203af0e77b8e2fd011c5:238", + "coordinatorOccurrenceId": "1:0xae2fc5adc70352a091b122311dc3dfe737c9991187c8a07fff53749b59259690:0x92997af2820c02d7f84b8dd6f12049164c788de7bdc5203af0e77b8e2fd011c5:243" + }, + { + "id": "1:stock-paired-v3:0xe6e9b57719a1cbad4d6ea15c8ddd00bd43036a8c543d52fd546da0c2eee734c5", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x347ffc6db9acc54ac2019795173b9599e8b82bd9", + "launchOccurrenceId": "1:0x1a5b3b2a142489e91cfdf92adfef03dc9ea01cf8355181f329f46eb338de2dc7:0x9ae052204f0a22ab7918b481b32e920cfd92efcfcfcbf2d99d1b87c9b0130c26:711", + "coordinatorOccurrenceId": "1:0x1a5b3b2a142489e91cfdf92adfef03dc9ea01cf8355181f329f46eb338de2dc7:0x9ae052204f0a22ab7918b481b32e920cfd92efcfcfcbf2d99d1b87c9b0130c26:716" + }, + { + "id": "1:stock-paired-v3:0xe800739e29723439475cdf77c4d178980c112582ae36283fe6668cdb030911cf", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x0aeda175d27d2208092a01aab9fbfb2da7d72b32", + "launchOccurrenceId": "1:0x2dd642f8805e5a8f020945f4373821c7b3876b9cfc8f2dc1e9d5e2a852d0ede0:0xa95de243bcec62423e9e9347af490af8f796c4a6623719ef4d03f304765ec494:38", + "coordinatorOccurrenceId": "1:0x2dd642f8805e5a8f020945f4373821c7b3876b9cfc8f2dc1e9d5e2a852d0ede0:0xa95de243bcec62423e9e9347af490af8f796c4a6623719ef4d03f304765ec494:43" + }, + { + "id": "1:stock-paired-v3:0xe86221fdfaa14f1f1e679e65c72f4dc9fa8144818e5822cd6198379f4a2c0cac", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x31d39253587c5d2310c7aea1bb83ffbfa76df8eb", + "launchOccurrenceId": "1:0x0bc54b592905e6920102d4f6bba6ce52040c34d215aa981141999b74622994ae:0x05316203467fb7d8fe81b2793a245933a9457e62df2199e8511851738cb38f64:438", + "coordinatorOccurrenceId": "1:0x0bc54b592905e6920102d4f6bba6ce52040c34d215aa981141999b74622994ae:0x05316203467fb7d8fe81b2793a245933a9457e62df2199e8511851738cb38f64:443" + }, + { + "id": "1:stock-paired-v3:0xea4535bb2ef44a5e00ce2e5d7b562e2c0c573deeb37769f0e8e0b70ed37c97d4", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xb0095c3e6ac1a4f936df3914e8cc46783b9b5287", + "launchOccurrenceId": "1:0xade89ae5ea6b8771a8ee1e90b015ecd62e4f197bc910a3047a3a59bc2f2d6c1c:0x442bf8ccce84b6c83d26d6daa001c220eeb4e57bdf908e41287cfaac0f234a64:197", + "coordinatorOccurrenceId": "1:0xade89ae5ea6b8771a8ee1e90b015ecd62e4f197bc910a3047a3a59bc2f2d6c1c:0x442bf8ccce84b6c83d26d6daa001c220eeb4e57bdf908e41287cfaac0f234a64:202" + }, + { + "id": "1:stock-paired-v3:0xec1235c81b8d4008ab3db4fb3e591438ed9e34313573f2157be46e8ed612e61d", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0xb0095c3e6ac1a4f936df3914e8cc46783b9b5287", + "launchOccurrenceId": "1:0x15713a1cc43c53f1a8219f5ad5005d68014583c288c19d77448cfe5aaff7b7b9:0x3c93ba04f33d51512ee3156b93405b5c0ba3100027e23231e9b87043b710633b:234", + "coordinatorOccurrenceId": "1:0x15713a1cc43c53f1a8219f5ad5005d68014583c288c19d77448cfe5aaff7b7b9:0x3c93ba04f33d51512ee3156b93405b5c0ba3100027e23231e9b87043b710633b:239" + }, + { + "id": "1:stock-paired-v3:0xf0468aace6ee478944fd323c37b94cf62fd0ef1aa0ac2e8cbd9a1b38d14d4b12", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x0aeda175d27d2208092a01aab9fbfb2da7d72b32", + "launchOccurrenceId": "1:0x0f4f3548fa9981ee9aa5c90552c6564b7ba9c807786fe8cc926775273d646f34:0xde92436a374181f77ccfd6c9d2723e0527f5050c7a1bfb194b8f85356011131f:29", + "coordinatorOccurrenceId": "1:0x0f4f3548fa9981ee9aa5c90552c6564b7ba9c807786fe8cc926775273d646f34:0xde92436a374181f77ccfd6c9d2723e0527f5050c7a1bfb194b8f85356011131f:34" + }, + { + "id": "1:stock-paired-v3:0xf0eaf9a4e1e51b32869fe5ac724c434e9d93a28564f0421cc066f396961a3fc2", + "releaseVersion": "stock-paired-v3", + "priorCoordinatorSource": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "authenticatedCreator": "0x239a9c5b6e5760180341ce77ada4645fabf78d8a", + "launchOccurrenceId": "1:0x7da87b204383123192b653c16f47335f5c5e295b6afd0c56cf3d35312f0e3910:0xb26b3b9c6635d80aa6c581685d726394279720b5cf0185a3e4b2b99587ea36a1:337", + "coordinatorOccurrenceId": "1:0x7da87b204383123192b653c16f47335f5c5e295b6afd0c56cf3d35312f0e3910:0xb26b3b9c6635d80aa6c581685d726394279720b5cf0185a3e4b2b99587ea36a1:342" + } + ], + "digest": "0x42199d74de38991b95324b937a3765932a612999260371c9d9f9e631da9ded97" +} diff --git a/docs/data-pipeline/envio-candidate-7f24e63-baseline-20260801T042058Z.json b/docs/data-pipeline/envio-candidate-7f24e63-baseline-20260801T042058Z.json new file mode 100644 index 00000000..a74f52b7 --- /dev/null +++ b/docs/data-pipeline/envio-candidate-7f24e63-baseline-20260801T042058Z.json @@ -0,0 +1,12228 @@ +{ + "schemaVersion": 2, + "kind": "envio-launch-inventory-baseline", + "endpoint": "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + "capturedAt": "2026-08-01T04:20:58.618Z", + "deployment": { + "provider": "envio-cloud", + "host": "indexer.hyperindex.xyz", + "endpointId": "f6714ef", + "deploymentLabel": "production-1e7c381", + "chainId": 1 + }, + "anchor": { + "progressBlock": "25657578", + "bufferBlock": "25657578", + "sourceBlock": "25657590", + "eventsProcessed": "56002", + "stateProgressBlock": "25657571", + "stateProgressBlockHash": "0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716", + "stateProgressTimestamp": "1785557819", + "stateProgressTransactionHash": "0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1", + "stateProgressOccurrenceId": "1:0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716:0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1:778" + }, + "inventory": { + "count": 265, + "perRelease": { + "classic-v2": 27, + "classic-v3": 186, + "stock-paired-v1": 1, + "stock-paired-v2": 8, + "stock-paired-v3": 43 + }, + "sha256": "0x5a388ae00ff52fd63abf45560cdb456cafe883c17249cabce83ca31286104c6d" + }, + "entries": [ + { + "id": "1:classic-v2:0x017db92d647d727962863d1d52f99a0fbec099795c63660faa5654af48bdb0d8", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x017db92d647d727962863d1d52f99a0fbec099795c63660faa5654af48bdb0d8", + "token": "0xaabad60ff766bb27680b2208ad095041630de21f", + "creator": "0x222a62ec22898f304e0c821030c845eb90ddfa5b", + "quoteAsset": null, + "poolId": "0x726ab914ca51a113e58ab0df05ab4840428440cf7a086348c2a23d6e0b31caef", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x7d5a436b39294958a2ba78ae54f73c8c461d47da", + "positionTokenId": "354488", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x3223d7317eaa77cdca73fda1a730426483bcf004a1f8fe40913c52f9bbd16b2e:0xac67a0e23b5053886f06a1fa23b155449ae6072f9a7ae8bdf8ac8d128401f6b9:169", + "liquidityOccurrenceId": "1:0x3223d7317eaa77cdca73fda1a730426483bcf004a1f8fe40913c52f9bbd16b2e:0xac67a0e23b5053886f06a1fa23b155449ae6072f9a7ae8bdf8ac8d128401f6b9:170", + "initialBuyOccurrenceId": "1:0x3223d7317eaa77cdca73fda1a730426483bcf004a1f8fe40913c52f9bbd16b2e:0xac67a0e23b5053886f06a1fa23b155449ae6072f9a7ae8bdf8ac8d128401f6b9:171", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25639726" + }, + { + "id": "1:classic-v2:0x1b25b54483711c47e8be12b0359ee6e0ad6cd444b096bf504d57791682636925", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x1b25b54483711c47e8be12b0359ee6e0ad6cd444b096bf504d57791682636925", + "token": "0x21580898a93102fd644e7bc4a5eb1073a8cdb7c5", + "creator": "0xb6e866e738e2f112273c11cee965c47f1c49f781", + "quoteAsset": null, + "poolId": "0x68eba2c502fe84dde3bc19a7db6106d4bd723221a88fd9cb599c9d218fa87880", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xba3048071aa7c66ae2049882dc1942adde9c6a5c", + "positionTokenId": "354670", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7c6ccd35ec746e3d27815179b94b69010e535f752d777b81a12e2d474b9e78d8:0xf9262f33a5fbe9dc23bd7927f8779d1faacef41cbf8cf497aa59ca569b4920c2:607", + "liquidityOccurrenceId": "1:0x7c6ccd35ec746e3d27815179b94b69010e535f752d777b81a12e2d474b9e78d8:0xf9262f33a5fbe9dc23bd7927f8779d1faacef41cbf8cf497aa59ca569b4920c2:608", + "initialBuyOccurrenceId": "1:0x7c6ccd35ec746e3d27815179b94b69010e535f752d777b81a12e2d474b9e78d8:0xf9262f33a5fbe9dc23bd7927f8779d1faacef41cbf8cf497aa59ca569b4920c2:609", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640700" + }, + { + "id": "1:classic-v2:0x246ab39e51b0e6fb7a0d46c768466afbeae73cbbd8d1b416fe111282c650f7aa", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x246ab39e51b0e6fb7a0d46c768466afbeae73cbbd8d1b416fe111282c650f7aa", + "token": "0xe8bee0918e80d87dd190c2478dd713ccf08e93cd", + "creator": "0xf0f925f165a0ec7e48d2cf90f0928af3c32fe422", + "quoteAsset": null, + "poolId": "0xe662721a3c0039a98125a22b3c8fa1ceed2460d2087f4422e4bf4573c25470eb", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x265c7161c1adca2a4df11fb461b784d6a0243507", + "positionTokenId": "352281", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x66694e4742003e862783417d0750401a0e65653763b0b9d7e0a3055476bf9de6:0xb5bf5a8c0e336f97089cd133752e4979984a18f44ad161f0b50fa03f5b781850:991", + "liquidityOccurrenceId": "1:0x66694e4742003e862783417d0750401a0e65653763b0b9d7e0a3055476bf9de6:0xb5bf5a8c0e336f97089cd133752e4979984a18f44ad161f0b50fa03f5b781850:992", + "initialBuyOccurrenceId": "1:0x66694e4742003e862783417d0750401a0e65653763b0b9d7e0a3055476bf9de6:0xb5bf5a8c0e336f97089cd133752e4979984a18f44ad161f0b50fa03f5b781850:993", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627210" + }, + { + "id": "1:classic-v2:0x27d6640fc5a852fe351230ae6aafc331b40d6d7e1e53341357a67d1a2f550a83", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x27d6640fc5a852fe351230ae6aafc331b40d6d7e1e53341357a67d1a2f550a83", + "token": "0x163baaa9fc5553766a904e8a99c5f940dcec98ed", + "creator": "0xec6ba66a6d19fc4b6e123fbdba59e310d6d767e0", + "quoteAsset": null, + "poolId": "0x766c2d930f171c4bf4c502ac446b9cc13cb492a0bb49bfe69b44582475a576ed", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x91dd5b66dc1dbc89ac48fd2269992977fc75d5bb", + "positionTokenId": "355021", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7d7bbff9b1a9e4cd73739595d5b5ac70b1bd23de35dc5e79c875dae414a769ae:0x749ae04dffe2cbb9d3d60dc88a77fbf1658fb5fcf1afa05ee9347ef8d7195db8:78", + "liquidityOccurrenceId": "1:0x7d7bbff9b1a9e4cd73739595d5b5ac70b1bd23de35dc5e79c875dae414a769ae:0x749ae04dffe2cbb9d3d60dc88a77fbf1658fb5fcf1afa05ee9347ef8d7195db8:79", + "initialBuyOccurrenceId": "1:0x7d7bbff9b1a9e4cd73739595d5b5ac70b1bd23de35dc5e79c875dae414a769ae:0x749ae04dffe2cbb9d3d60dc88a77fbf1658fb5fcf1afa05ee9347ef8d7195db8:80", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25643040" + }, + { + "id": "1:classic-v2:0x34ac4e740fe98bf4e9f9f2f51163cfd77ad8e754d537c36df20cc1b8ac612919", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x34ac4e740fe98bf4e9f9f2f51163cfd77ad8e754d537c36df20cc1b8ac612919", + "token": "0xf8d0ec5a77cc955ff58033ce54b4fa2e17db3f5a", + "creator": "0xb38ef2aa4306c8a2ce97d8718aa3dda7a0ba331c", + "quoteAsset": null, + "poolId": "0x7ad5e4d2c84902179140799d6c296bbc9c0e2c6d905b56d55024a7ca100079fe", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x428da07db1408480658f3d8b6e734e380120e971", + "positionTokenId": "352291", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x0382114b20cc1d786e24d7ec64197a814336266823b95fce5a3f8f74fcd8bfe7:0xaa27e2aa4d73a1fb4f371b945c367835622a1e706743c4bcd751b9cbf409bd91:951", + "liquidityOccurrenceId": "1:0x0382114b20cc1d786e24d7ec64197a814336266823b95fce5a3f8f74fcd8bfe7:0xaa27e2aa4d73a1fb4f371b945c367835622a1e706743c4bcd751b9cbf409bd91:952", + "initialBuyOccurrenceId": "1:0x0382114b20cc1d786e24d7ec64197a814336266823b95fce5a3f8f74fcd8bfe7:0xaa27e2aa4d73a1fb4f371b945c367835622a1e706743c4bcd751b9cbf409bd91:953", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627284" + }, + { + "id": "1:classic-v2:0x407af1a04877e6c4d3b74311c6d6a725e4b4b874b11e29cfb24d80b73508d8a8", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x407af1a04877e6c4d3b74311c6d6a725e4b4b874b11e29cfb24d80b73508d8a8", + "token": "0x26a77c5f275551e5f96ad97f555933d944ee1740", + "creator": "0xe1f7280de84b84f91be3830ca437ee6fc548bb7a", + "quoteAsset": null, + "poolId": "0x6b8c66fbe00b5c4701853d53993306febc1954b9772aed77c0f6b1f9261ae827", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xc81bf0f0a0bba35bef5cfd74f0b740164931d232", + "positionTokenId": "354727", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "30000000000000000", + "initialBuyTokenAmount": "21438505518229829458161070", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xdb2132b2eb675b25a37339438cb38d7dc8c6e95348cfc19904293fcf6d26b5b1:0xf41460d007ebadf328ea02f955f6f89f3b64c33939122562d539e8dde74f3582:164", + "liquidityOccurrenceId": "1:0xdb2132b2eb675b25a37339438cb38d7dc8c6e95348cfc19904293fcf6d26b5b1:0xf41460d007ebadf328ea02f955f6f89f3b64c33939122562d539e8dde74f3582:165", + "initialBuyOccurrenceId": "1:0xdb2132b2eb675b25a37339438cb38d7dc8c6e95348cfc19904293fcf6d26b5b1:0xf41460d007ebadf328ea02f955f6f89f3b64c33939122562d539e8dde74f3582:166", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25641181" + }, + { + "id": "1:classic-v2:0x46bd7403609b59313b2982261ad805ac00c807eb3421b4d43ec002e72a544cf3", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x46bd7403609b59313b2982261ad805ac00c807eb3421b4d43ec002e72a544cf3", + "token": "0x95daaa54836d871ef4a99ce3a59ae4ec5ad2f27a", + "creator": "0xb38ef2aa4306c8a2ce97d8718aa3dda7a0ba331c", + "quoteAsset": null, + "poolId": "0x324f273d586b5d7a2906ba46890d0aa45181694b755207a31f7e2026f9a5fbf3", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xee27cf48c1ef21b2c39011b5e8850e15f9e041d7", + "positionTokenId": "352269", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xdd01ae0d896304d5c7e9959364444a52f04204da2b5d20e26e5c4edf58ed5628:0x76d10d603b9525961c7e0008b57fa395f1d34881a82d5b744707b899e26366f2:1005", + "liquidityOccurrenceId": "1:0xdd01ae0d896304d5c7e9959364444a52f04204da2b5d20e26e5c4edf58ed5628:0x76d10d603b9525961c7e0008b57fa395f1d34881a82d5b744707b899e26366f2:1006", + "initialBuyOccurrenceId": "1:0xdd01ae0d896304d5c7e9959364444a52f04204da2b5d20e26e5c4edf58ed5628:0x76d10d603b9525961c7e0008b57fa395f1d34881a82d5b744707b899e26366f2:1007", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627167" + }, + { + "id": "1:classic-v2:0x52930ce388ebef624cb69433b15b837be6847508595a95c9839b8497befae67a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x52930ce388ebef624cb69433b15b837be6847508595a95c9839b8497befae67a", + "token": "0x7f25a2734e72a65bc0fd9ec6280a84998e61e3ea", + "creator": "0x753a036cacd44ca5e8d446aa574d0094ceec767c", + "quoteAsset": null, + "poolId": "0x182ea058dd5ddd6d7ba89d9a68255c4676a95b61f9b8582a2d107e38a144f551", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xa0e4fa79c6e4217aeac80b0dbe99a5a4a7201c75", + "positionTokenId": "357578", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf19b6a4679d654f783aa6d00bbd5ecec54e9aa8c32160eb29829c8139c9ef9d1:0xb9e539a82eb3fab7f67537a7939c2f8fd09f40735829562715b1ae9039f3e5ec:269", + "liquidityOccurrenceId": "1:0xf19b6a4679d654f783aa6d00bbd5ecec54e9aa8c32160eb29829c8139c9ef9d1:0xb9e539a82eb3fab7f67537a7939c2f8fd09f40735829562715b1ae9039f3e5ec:270", + "initialBuyOccurrenceId": "1:0xf19b6a4679d654f783aa6d00bbd5ecec54e9aa8c32160eb29829c8139c9ef9d1:0xb9e539a82eb3fab7f67537a7939c2f8fd09f40735829562715b1ae9039f3e5ec:271", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25653685" + }, + { + "id": "1:classic-v2:0x554cdec87d813ec4fcf144aa9a11242c1aba0bafe8d585bd4e75f3efea0a6ca2", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x554cdec87d813ec4fcf144aa9a11242c1aba0bafe8d585bd4e75f3efea0a6ca2", + "token": "0xfce7c90cf19b847690a7b7267e8817b8cc9822e6", + "creator": "0xf6b3029077e7d4b5e6ff46fd8e40fe2f28cf4242", + "quoteAsset": null, + "poolId": "0x8be3f3c07ae261db04b805ac53a67f94e15078e36adc88a931ce90aaa34912cf", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xbb5231031deaaaf13ee46712dc1521e36bc7aade", + "positionTokenId": "352256", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x5afb12194bcde592c3f601bb351eebeaf0ad01a1eea3b784eb42b8c11336e9d9:0x8c85a3e78c3b8c0191f0c73a3e15fc58c1e99b93e28da80849f8c562395f9150:1337", + "liquidityOccurrenceId": "1:0x5afb12194bcde592c3f601bb351eebeaf0ad01a1eea3b784eb42b8c11336e9d9:0x8c85a3e78c3b8c0191f0c73a3e15fc58c1e99b93e28da80849f8c562395f9150:1338", + "initialBuyOccurrenceId": "1:0x5afb12194bcde592c3f601bb351eebeaf0ad01a1eea3b784eb42b8c11336e9d9:0x8c85a3e78c3b8c0191f0c73a3e15fc58c1e99b93e28da80849f8c562395f9150:1339", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627101" + }, + { + "id": "1:classic-v2:0x60c643f4a406f70be6e15ea6482620df0d1a5b88d0b34432ab8b8123113de608", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x60c643f4a406f70be6e15ea6482620df0d1a5b88d0b34432ab8b8123113de608", + "token": "0x05204a4ce651452892a620950bdc2adedbf63b0a", + "creator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "quoteAsset": null, + "poolId": "0xb12253d75eb143edcb6aab74f543802c6fa72998e092bc7bd1acf27a42adc2ea", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x9020eef40e36546bf34f15070a8d9bca2ebf4bb8", + "positionTokenId": "351734", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc6aeca512415f637db1e9c93bcb3f350cf6d0ac1ef03e5507cc0ad5f669d5df2:0x44a480caaac8b937e7ccc31e45e13bd725253e231fcc12f7795bc5358a0a5d4c:510", + "liquidityOccurrenceId": "1:0xc6aeca512415f637db1e9c93bcb3f350cf6d0ac1ef03e5507cc0ad5f669d5df2:0x44a480caaac8b937e7ccc31e45e13bd725253e231fcc12f7795bc5358a0a5d4c:511", + "initialBuyOccurrenceId": "1:0xc6aeca512415f637db1e9c93bcb3f350cf6d0ac1ef03e5507cc0ad5f669d5df2:0x44a480caaac8b937e7ccc31e45e13bd725253e231fcc12f7795bc5358a0a5d4c:512", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25624511" + }, + { + "id": "1:classic-v2:0x63ada31fcd2ea3257bd062f1b0c02fb75c94980219b27aa23e0e2a9a3f424525", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x63ada31fcd2ea3257bd062f1b0c02fb75c94980219b27aa23e0e2a9a3f424525", + "token": "0xdcb41ff4ed05d3a31e04febdbb893cbc0c9f469a", + "creator": "0xf0f925f165a0ec7e48d2cf90f0928af3c32fe422", + "quoteAsset": null, + "poolId": "0xec216394f6bcf28ee474934bfea092e62c0931f5918f7f24321589f17f4b7305", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x2fe6bbcf634eb1fe399f7d5425f5fef6f87df60f", + "positionTokenId": "352288", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xd0707ea0199fd4fbd6e9d16ec69ad04293a8d299cc0325ff1660382b1967e136:0xadd38913e0398c634e58317c88c3218ecc526837144502a884a472d2bfa6ed33:380", + "liquidityOccurrenceId": "1:0xd0707ea0199fd4fbd6e9d16ec69ad04293a8d299cc0325ff1660382b1967e136:0xadd38913e0398c634e58317c88c3218ecc526837144502a884a472d2bfa6ed33:381", + "initialBuyOccurrenceId": "1:0xd0707ea0199fd4fbd6e9d16ec69ad04293a8d299cc0325ff1660382b1967e136:0xadd38913e0398c634e58317c88c3218ecc526837144502a884a472d2bfa6ed33:382", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627257" + }, + { + "id": "1:classic-v2:0x663cbacd1460898b6de6df629e06bd477f2749416bb95da1c0b399943f1192c1", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x663cbacd1460898b6de6df629e06bd477f2749416bb95da1c0b399943f1192c1", + "token": "0x3971bd304b3be0ce5412c3da3d9c3f7ef4389030", + "creator": "0x83b8500bd6fe33c658db88717bfb60a89b7d1ddb", + "quoteAsset": null, + "poolId": "0x4669476c40d12715aca4659def5f4659b34990ffd54e2a2bfd6d97fb1f42797f", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x0abb19e6b3f53f2ac1f2bb3de035ef269a0f9e0a", + "positionTokenId": "356133", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x144fd7e77ad318188b02494539dc6136e33d9a688e3f2fdcfe6efa7fcae26a27:0xfb807634883572c420b45c62bf8aeb73b37562deda7522ae0e66a2f0b0000189:459", + "liquidityOccurrenceId": "1:0x144fd7e77ad318188b02494539dc6136e33d9a688e3f2fdcfe6efa7fcae26a27:0xfb807634883572c420b45c62bf8aeb73b37562deda7522ae0e66a2f0b0000189:460", + "initialBuyOccurrenceId": "1:0x144fd7e77ad318188b02494539dc6136e33d9a688e3f2fdcfe6efa7fcae26a27:0xfb807634883572c420b45c62bf8aeb73b37562deda7522ae0e66a2f0b0000189:461", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646967" + }, + { + "id": "1:classic-v2:0x6f84f318451cf1575ee80c408913db6331a9b80f7db67fc395df743c2d12f7a1", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x6f84f318451cf1575ee80c408913db6331a9b80f7db67fc395df743c2d12f7a1", + "token": "0x7ede594bc87e52ef846852fe45aee10bb83dcce2", + "creator": "0x6dfb0c84775585a4e8c181848358f274aa2f2234", + "quoteAsset": null, + "poolId": "0x3b8313f2502816cb902309a66b3e8f9e3ac9f92c020378cb63490bd38b9a1166", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x29b71d0314fdb48b6ae3963e689bdb97f7f21e6f", + "positionTokenId": "352369", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x47e113f8e15ea3979d0b599dd67b4d7e1927d513306d1e1cb54a9de263cd8b0e:0x67587703edc7ff3c8feee4bcaee36a3c44af22339c137650b59cb56e6c82e445:236", + "liquidityOccurrenceId": "1:0x47e113f8e15ea3979d0b599dd67b4d7e1927d513306d1e1cb54a9de263cd8b0e:0x67587703edc7ff3c8feee4bcaee36a3c44af22339c137650b59cb56e6c82e445:237", + "initialBuyOccurrenceId": "1:0x47e113f8e15ea3979d0b599dd67b4d7e1927d513306d1e1cb54a9de263cd8b0e:0x67587703edc7ff3c8feee4bcaee36a3c44af22339c137650b59cb56e6c82e445:238", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627869" + }, + { + "id": "1:classic-v2:0x80ca5407975a9d6f26c04d0f9a13ef0df4d7febe52e97fab320605fc818db60d", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x80ca5407975a9d6f26c04d0f9a13ef0df4d7febe52e97fab320605fc818db60d", + "token": "0x1bd9133ddfd826ccf933fcff7424c4674a0443c3", + "creator": "0x6dfb0c84775585a4e8c181848358f274aa2f2234", + "quoteAsset": null, + "poolId": "0x8017f0a6843370a2712d95be364d37875f52c1f65fcfbb95c2eb708abb196de8", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xed98f15a0700f4f76240539c20abb7a8173ecac0", + "positionTokenId": "352380", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x34a9f78a2e4c8c2bed4597c4aa4bd6f9895b93857758556c5ba524b52217285c:0x86f98a6f521ad3fc364bb84c775a6530c46773d8f14d228132ec5208577c2d26:421", + "liquidityOccurrenceId": "1:0x34a9f78a2e4c8c2bed4597c4aa4bd6f9895b93857758556c5ba524b52217285c:0x86f98a6f521ad3fc364bb84c775a6530c46773d8f14d228132ec5208577c2d26:422", + "initialBuyOccurrenceId": "1:0x34a9f78a2e4c8c2bed4597c4aa4bd6f9895b93857758556c5ba524b52217285c:0x86f98a6f521ad3fc364bb84c775a6530c46773d8f14d228132ec5208577c2d26:423", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627928" + }, + { + "id": "1:classic-v2:0x8ce8d7f0d15b89b6380e601b03440acf44ebab4b453fa792e791f55bd16fc0b7", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0x8ce8d7f0d15b89b6380e601b03440acf44ebab4b453fa792e791f55bd16fc0b7", + "token": "0x884687f8403e7c45e2c5f33318c5317b0ce9cfa0", + "creator": "0x8376eb17ee9dfb388da0cc9c026b2d5882dbefa4", + "quoteAsset": null, + "poolId": "0xeb1d89a18177dfaa4f333e563495a39478f0e52fd22224d716a1de20684fdd1f", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x22831130f595922edc2ebe5316f81115520910dc", + "positionTokenId": "354282", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "729739899031511876349884", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1ffb2ac6c57aa4a285b1777e7345ab687780b1b3e55b5247e210aaba1d2b0b25:0x13e80af5698abd244664de01413f7955e4b2cf038fe2ab39d7e650cf89d05496:907", + "liquidityOccurrenceId": "1:0x1ffb2ac6c57aa4a285b1777e7345ab687780b1b3e55b5247e210aaba1d2b0b25:0x13e80af5698abd244664de01413f7955e4b2cf038fe2ab39d7e650cf89d05496:908", + "initialBuyOccurrenceId": "1:0x1ffb2ac6c57aa4a285b1777e7345ab687780b1b3e55b5247e210aaba1d2b0b25:0x13e80af5698abd244664de01413f7955e4b2cf038fe2ab39d7e650cf89d05496:909", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25638359" + }, + { + "id": "1:classic-v2:0xa108551b77a450d6ee7831e1b9813eacd3bb4ca929c4a33e1cb0bfae2014868c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xa108551b77a450d6ee7831e1b9813eacd3bb4ca929c4a33e1cb0bfae2014868c", + "token": "0x90ca46dab93ce65036cae2417f769d184837dd0b", + "creator": "0x6dfb0c84775585a4e8c181848358f274aa2f2234", + "quoteAsset": null, + "poolId": "0x6692a4d4ea9409680fc97566489bae6d5a30a1b22923e858991a67ec2e890f24", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x80a8d883df85f9dd7f05e92ff480c304cd59238c", + "positionTokenId": "352366", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7c905d497f67f26c7b59ac35348c95468941a2821bc38b3691b96c8c50051b42:0xf89b337857f0e51ff8c6d61c5b67737c6d61a784ec8f4d17b5f46f37ace1f7cb:240", + "liquidityOccurrenceId": "1:0x7c905d497f67f26c7b59ac35348c95468941a2821bc38b3691b96c8c50051b42:0xf89b337857f0e51ff8c6d61c5b67737c6d61a784ec8f4d17b5f46f37ace1f7cb:241", + "initialBuyOccurrenceId": "1:0x7c905d497f67f26c7b59ac35348c95468941a2821bc38b3691b96c8c50051b42:0xf89b337857f0e51ff8c6d61c5b67737c6d61a784ec8f4d17b5f46f37ace1f7cb:242", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627824" + }, + { + "id": "1:classic-v2:0xa78ab2f00080d424c383422af952e34f76d5eb557f3cb9504dcbed72873a4fa5", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xa78ab2f00080d424c383422af952e34f76d5eb557f3cb9504dcbed72873a4fa5", + "token": "0x2fe8e06580888181d9792c7a05b969088ea740a4", + "creator": "0x9b71dd66e7e8be4de110fc8ad324092bf71ae103", + "quoteAsset": null, + "poolId": "0xaed63303d0c68f6d100c00aa21ecc54816e47250b925bff135ccd3cc04897e77", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x19b0e78de477302db83856de4b160b5566faffd3", + "positionTokenId": "352268", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9c7e01139ba57019d6da8924afe53763340baf90e5cae2056db7bf33d4ef4c44:0xa951f89710530d1efbbbf9ad7d9ef59f5a22ffe94f5c65500797ff2bf0e491e9:212", + "liquidityOccurrenceId": "1:0x9c7e01139ba57019d6da8924afe53763340baf90e5cae2056db7bf33d4ef4c44:0xa951f89710530d1efbbbf9ad7d9ef59f5a22ffe94f5c65500797ff2bf0e491e9:213", + "initialBuyOccurrenceId": "1:0x9c7e01139ba57019d6da8924afe53763340baf90e5cae2056db7bf33d4ef4c44:0xa951f89710530d1efbbbf9ad7d9ef59f5a22ffe94f5c65500797ff2bf0e491e9:214", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627159" + }, + { + "id": "1:classic-v2:0xaedf3d087229f5b4a763a5cacaa55f7070795296971228bc49ef1c091662722e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xaedf3d087229f5b4a763a5cacaa55f7070795296971228bc49ef1c091662722e", + "token": "0x3c940d0fd4fa4e45d8884d16e19a27ece5e23e1f", + "creator": "0xf0f925f165a0ec7e48d2cf90f0928af3c32fe422", + "quoteAsset": null, + "poolId": "0x87e45d11d16572230dde8704c73abf5f71a0791738b7a21a8c3d01a237a3fed1", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x06a4a9beb52f3b61f4fdf17eee6368aa7dbf05d1", + "positionTokenId": "352290", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x091da22665679737dec5851796ac85c4495a9e1ed75c72778e2cd369a1ba9b70:0xca18cb51d095daa787b7c7f14c676905ddbb18f18ac54e6c5ff442bd314c7590:565", + "liquidityOccurrenceId": "1:0x091da22665679737dec5851796ac85c4495a9e1ed75c72778e2cd369a1ba9b70:0xca18cb51d095daa787b7c7f14c676905ddbb18f18ac54e6c5ff442bd314c7590:566", + "initialBuyOccurrenceId": "1:0x091da22665679737dec5851796ac85c4495a9e1ed75c72778e2cd369a1ba9b70:0xca18cb51d095daa787b7c7f14c676905ddbb18f18ac54e6c5ff442bd314c7590:567", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627279" + }, + { + "id": "1:classic-v2:0xb6941c6fe818506d9b27bce2c1d84845cf0b96406b7af8600270f9c9a831607b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xb6941c6fe818506d9b27bce2c1d84845cf0b96406b7af8600270f9c9a831607b", + "token": "0x7db4f2c2807f84c6d6f37d6cdd1218ae144036f5", + "creator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "quoteAsset": null, + "poolId": "0x641ffc86b34543cfb48d84b66a927a75fa494d579c40bd2a873d6eba1add48b1", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x8a3e9b9965f09831061d1612149c23355ddc9308", + "positionTokenId": "352117", + "totalSwapFeeBps": 200, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "3000000000000000", + "initialBuyTokenAmount": "2163995911660998705227421", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe70dde19908bbc789bb879555025c66113565a98730613da3a7527b3e1f1aa30:0xbf369cd6039cacf7a7cf3da86f891697a5a853dbfb382a7cb340fceb37f054ab:72", + "liquidityOccurrenceId": "1:0xe70dde19908bbc789bb879555025c66113565a98730613da3a7527b3e1f1aa30:0xbf369cd6039cacf7a7cf3da86f891697a5a853dbfb382a7cb340fceb37f054ab:73", + "initialBuyOccurrenceId": "1:0xe70dde19908bbc789bb879555025c66113565a98730613da3a7527b3e1f1aa30:0xbf369cd6039cacf7a7cf3da86f891697a5a853dbfb382a7cb340fceb37f054ab:74", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25626329" + }, + { + "id": "1:classic-v2:0xba2c8ea5a3a0d4856124b28c9e78a3491dd78caaee17c1910ce4009fdf58b826", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xba2c8ea5a3a0d4856124b28c9e78a3491dd78caaee17c1910ce4009fdf58b826", + "token": "0xe4e52c8c60b49d501d14f4b6feee937e1c954e80", + "creator": "0x195efa8b60470d9db33446b28b88e2572407b2e3", + "quoteAsset": null, + "poolId": "0x1621088a11c81f5bfbe00f111f6c99bc3836d220942184432283b067088216f2", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x44cd3fbafb8d8fe7334a134dec03ea3e5cafb974", + "positionTokenId": "354592", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9aa8209dc7d7b18a2cf678b4e2f6e15be15516999c130d08e40f5608d6d715ba:0x38aa09a4795b3d72808d2a60f340d3fbbf3138e503bc57054cfc9d99efb0e07e:351", + "liquidityOccurrenceId": "1:0x9aa8209dc7d7b18a2cf678b4e2f6e15be15516999c130d08e40f5608d6d715ba:0x38aa09a4795b3d72808d2a60f340d3fbbf3138e503bc57054cfc9d99efb0e07e:352", + "initialBuyOccurrenceId": "1:0x9aa8209dc7d7b18a2cf678b4e2f6e15be15516999c130d08e40f5608d6d715ba:0x38aa09a4795b3d72808d2a60f340d3fbbf3138e503bc57054cfc9d99efb0e07e:353", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640324" + }, + { + "id": "1:classic-v2:0xbde69ed27482551142eb99aad264f0a07817fef17956d7c976fd57b5e61bf383", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xbde69ed27482551142eb99aad264f0a07817fef17956d7c976fd57b5e61bf383", + "token": "0xc8598465b517e07e704a99393e273bfe6dc7e07c", + "creator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "quoteAsset": null, + "poolId": "0xa8ce574c8a215d80d99edc2d24bac5ebf2c3de8a8558e3ef970f255de5f01eea", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x6eb57cd6ac381d023f5c9cadb691b4275f6ebe2b", + "positionTokenId": "352145", + "totalSwapFeeBps": 300, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "715008219657663549241071", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x3c961c2d80c141155d946a450146339e0b49c0ecdbe8b2f8af6d21656bfdf48d:0xa9936406a5af249784065aacfa2d12ad5b5884a4c6c787a3b65150c44d8cb82c:1264", + "liquidityOccurrenceId": "1:0x3c961c2d80c141155d946a450146339e0b49c0ecdbe8b2f8af6d21656bfdf48d:0xa9936406a5af249784065aacfa2d12ad5b5884a4c6c787a3b65150c44d8cb82c:1265", + "initialBuyOccurrenceId": "1:0x3c961c2d80c141155d946a450146339e0b49c0ecdbe8b2f8af6d21656bfdf48d:0xa9936406a5af249784065aacfa2d12ad5b5884a4c6c787a3b65150c44d8cb82c:1266", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25626489" + }, + { + "id": "1:classic-v2:0xbdeb126ff65e424f7294a5df464e2474409b2e42ce2068550a9b333014487a8f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xbdeb126ff65e424f7294a5df464e2474409b2e42ce2068550a9b333014487a8f", + "token": "0x3dee42295a9f6fdc9bb258dd0aa61c5f7a8208b8", + "creator": "0x4321dd0a9fcc4ae18202bdf734df40dd959156df", + "quoteAsset": null, + "poolId": "0x64779a23cba44400cc16fd082bee086a36cc52a541c3066bbaa38075cee78ca3", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x5b7cb57b1e7fc711f672d20cd2e9e2c906c5f037", + "positionTokenId": "354679", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc641f1fd0c0a4825ef9fab89840c3a960750001ad0847c618a85bdd98f4901ed:0x8fc432c0572ff1be590c825a641633034dab32d2bf3069601c68635da42ae40b:106", + "liquidityOccurrenceId": "1:0xc641f1fd0c0a4825ef9fab89840c3a960750001ad0847c618a85bdd98f4901ed:0x8fc432c0572ff1be590c825a641633034dab32d2bf3069601c68635da42ae40b:107", + "initialBuyOccurrenceId": "1:0xc641f1fd0c0a4825ef9fab89840c3a960750001ad0847c618a85bdd98f4901ed:0x8fc432c0572ff1be590c825a641633034dab32d2bf3069601c68635da42ae40b:108", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640816" + }, + { + "id": "1:classic-v2:0xbe1a81c08ee52219c22e2f4431715497fa2353f999c83935a4d83f64c5c35f97", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xbe1a81c08ee52219c22e2f4431715497fa2353f999c83935a4d83f64c5c35f97", + "token": "0x697dd2d368e038de7ad1f7726d5ef9dcfb995bd4", + "creator": "0x80cc63a9282a6fa9ea0ad70b3aa0c619f6f25a9c", + "quoteAsset": null, + "poolId": "0x5dd9813e20f3fecc244d7b52c7dcbaa2905fcee008143ec837bf60e4ddbe5846", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xc2e4fd79639015f843bb5ad60d2d7581c760b7a7", + "positionTokenId": "352283", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "25000000000000000", + "initialBuyTokenAmount": "17929484825085125456919282", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xd17c614a295decd83f3b4226321b0c72ddc3d3f5d690287551eccaffbaf55e5c:0x17f4e0c79233888f7ea85fd635ddb20bf5be39a0f42acba80023a150c180a0dc:1327", + "liquidityOccurrenceId": "1:0xd17c614a295decd83f3b4226321b0c72ddc3d3f5d690287551eccaffbaf55e5c:0x17f4e0c79233888f7ea85fd635ddb20bf5be39a0f42acba80023a150c180a0dc:1328", + "initialBuyOccurrenceId": "1:0xd17c614a295decd83f3b4226321b0c72ddc3d3f5d690287551eccaffbaf55e5c:0x17f4e0c79233888f7ea85fd635ddb20bf5be39a0f42acba80023a150c180a0dc:1329", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627223" + }, + { + "id": "1:classic-v2:0xd666b57423269fca9b67dcb7ec33fb22da852d9a57b6731a0a14a856c8a185eb", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xd666b57423269fca9b67dcb7ec33fb22da852d9a57b6731a0a14a856c8a185eb", + "token": "0x6d7ae59eb1fbef5bcb7d52064279b1e003caba1a", + "creator": "0x99205545b36cfdb42691ad8438e9fcd037aff132", + "quoteAsset": null, + "poolId": "0x2db5e9c19e259615bbfc8aed6d5b9a00a15591196b09dd0c87e5f805403a90e9", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xee5d2779e1d6f228076716e3e46fd7b4a69af3e1", + "positionTokenId": "354685", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xef514e8142dfb18751e3a840313cef74a733ee117ebdfa3f9a9ea05f2fddf721:0xae255de9134f3b92d7b0e81ba4517096e701188b570eeb2b867a11dbab64fca0:977", + "liquidityOccurrenceId": "1:0xef514e8142dfb18751e3a840313cef74a733ee117ebdfa3f9a9ea05f2fddf721:0xae255de9134f3b92d7b0e81ba4517096e701188b570eeb2b867a11dbab64fca0:978", + "initialBuyOccurrenceId": "1:0xef514e8142dfb18751e3a840313cef74a733ee117ebdfa3f9a9ea05f2fddf721:0xae255de9134f3b92d7b0e81ba4517096e701188b570eeb2b867a11dbab64fca0:979", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640870" + }, + { + "id": "1:classic-v2:0xf4c4fd3813c8b82533b576259146545ab1b844843082bdde03062c5ac613648b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xf4c4fd3813c8b82533b576259146545ab1b844843082bdde03062c5ac613648b", + "token": "0xd036f17315d159b48cc154bbc70aa4fa06210de9", + "creator": "0xc3bfae1e3cac87f39bc4c054343127a397358e03", + "quoteAsset": null, + "poolId": "0x661d6b80b7c1d223218c78e1474d6ce540c70f504ec214f45595335a8406694e", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x169ad075246147302246b527972901c069791537", + "positionTokenId": "354715", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xabcb3f0c166c5dd327df1cc59d1ed391aca8e6d61530eeed79f36dab5e713cf1:0x9e5cb02f7cd7ada9013b5e288c61954a658a6cfb3b1c2e52c256f468623d40d0:357", + "liquidityOccurrenceId": "1:0xabcb3f0c166c5dd327df1cc59d1ed391aca8e6d61530eeed79f36dab5e713cf1:0x9e5cb02f7cd7ada9013b5e288c61954a658a6cfb3b1c2e52c256f468623d40d0:358", + "initialBuyOccurrenceId": "1:0xabcb3f0c166c5dd327df1cc59d1ed391aca8e6d61530eeed79f36dab5e713cf1:0x9e5cb02f7cd7ada9013b5e288c61954a658a6cfb3b1c2e52c256f468623d40d0:359", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25641075" + }, + { + "id": "1:classic-v2:0xf62bfccb2c0e3832607d8e6c48c00b0411d1d9bf12337fd039c4821d25e8cd20", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xf62bfccb2c0e3832607d8e6c48c00b0411d1d9bf12337fd039c4821d25e8cd20", + "token": "0x7987f03462200b3d8a072e02c89a8a41dcb124ee", + "creator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "quoteAsset": null, + "poolId": "0xd9ca22573437a06a12d5c757b151aa1a76265c1dfdde4b76507233d7ad2b6df0", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0xe68da18043623c31a93426b084c0ad1ca494c566", + "positionTokenId": "352224", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "2000000000000000", + "initialBuyTokenAmount": "1458415534058453948045650", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x17e7e16d94fdf07c3d06586080c68264a39756b326ecf9d55d5170542d8b733d:0x47668b99d392ba82fc82d2a38413bd679e6ec8a04e5cf9535bff2c558259732a:976", + "liquidityOccurrenceId": "1:0x17e7e16d94fdf07c3d06586080c68264a39756b326ecf9d55d5170542d8b733d:0x47668b99d392ba82fc82d2a38413bd679e6ec8a04e5cf9535bff2c558259732a:977", + "initialBuyOccurrenceId": "1:0x17e7e16d94fdf07c3d06586080c68264a39756b326ecf9d55d5170542d8b733d:0x47668b99d392ba82fc82d2a38413bd679e6ec8a04e5cf9535bff2c558259732a:978", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25627056" + }, + { + "id": "1:classic-v2:0xf93bafb146a2037ceb38ebd4755a3434a648baedea44ed99e68fa1c136173faa", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v2", + "launchHash": "0xf93bafb146a2037ceb38ebd4755a3434a648baedea44ed99e68fa1c136173faa", + "token": "0xea01e06ec5138ae8ed1cd2f333a0ae81e4a0dcb2", + "creator": "0x8376eb17ee9dfb388da0cc9c026b2d5882dbefa4", + "quoteAsset": null, + "poolId": "0xb33bae0eea3bba694cf3071af0061b2324fab5768bccaa0943d91a933cbe941a", + "hook": "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + "rewardVault": null, + "positionRecipient": "0x092c1f837d13534520e681c861b2fda200a50b15", + "positionTokenId": "354565", + "totalSwapFeeBps": 100, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": null, + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xdf0a3d8de7b7e97a49d53fa902129fc178ab536ae9bece80b70e9eaaca5c71f5:0xed75eb6538db6c98453211d69eba31f237435ad5002586e130478c64b7144d45:572", + "liquidityOccurrenceId": "1:0xdf0a3d8de7b7e97a49d53fa902129fc178ab536ae9bece80b70e9eaaca5c71f5:0xed75eb6538db6c98453211d69eba31f237435ad5002586e130478c64b7144d45:573", + "initialBuyOccurrenceId": "1:0xdf0a3d8de7b7e97a49d53fa902129fc178ab536ae9bece80b70e9eaaca5c71f5:0xed75eb6538db6c98453211d69eba31f237435ad5002586e130478c64b7144d45:574", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": false, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640184" + }, + { + "id": "1:classic-v3:0x01e443e129cad3e834ea58513298b58a68aea0e1a16f9d0188266cd430bfd3fb", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x01e443e129cad3e834ea58513298b58a68aea0e1a16f9d0188266cd430bfd3fb", + "token": "0xf3fb92520450a9dfbc0298bded0f572f052a994c", + "creator": "0x7d49972deb8d8d4178202c2db2025fe3e4e61ef3", + "quoteAsset": null, + "poolId": "0x63fb600958a6413bd42fd20d9d4b414fefdd4cfe18f275e45b8341e02de6568a", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd37fdff14cbb0a196d0e4d5c6385e8ec11b31036", + "positionRecipient": "0xb5674d881d1153942ebb05547cb7f6dae6d2ee11", + "positionTokenId": "356338", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x616b3d90b309235494afb22bd1c8a7cee4ddbdf49b9d0f077993d9c97a2f6cdd", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe0cedb2864d93c4f903b138d9dee4860601f1a58b0448e4047f14df2899ca39e:0xac4b906a277ca91f1ab8053fe14e44759200a2379c7ebc1f3736e15f6ceeda0e:542", + "liquidityOccurrenceId": "1:0xe0cedb2864d93c4f903b138d9dee4860601f1a58b0448e4047f14df2899ca39e:0xac4b906a277ca91f1ab8053fe14e44759200a2379c7ebc1f3736e15f6ceeda0e:543", + "initialBuyOccurrenceId": "1:0xe0cedb2864d93c4f903b138d9dee4860601f1a58b0448e4047f14df2899ca39e:0xac4b906a277ca91f1ab8053fe14e44759200a2379c7ebc1f3736e15f6ceeda0e:544", + "custodyOccurrenceId": "1:0xe0cedb2864d93c4f903b138d9dee4860601f1a58b0448e4047f14df2899ca39e:0xac4b906a277ca91f1ab8053fe14e44759200a2379c7ebc1f3736e15f6ceeda0e:545", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647667" + }, + { + "id": "1:classic-v3:0x0330b72c0d448c2528c3cc36d9914511050c3f033337950d2a8480407ce731b7", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x0330b72c0d448c2528c3cc36d9914511050c3f033337950d2a8480407ce731b7", + "token": "0x38b68388cd17ed56e57318d1124f84a14f81ba74", + "creator": "0xb3940904537334e359246e327235e5db25d175e4", + "quoteAsset": null, + "poolId": "0x6572ee690940929ffcbe9e45035031908f0ffc77c64500250ca28a09f873c595", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd061cb7baf9a9c0b952cef4e7c489d3a983937e8", + "positionRecipient": "0x73e50d6dc39b7cc8a8a0c11af053544a8fe4e049", + "positionTokenId": "355858", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xe2c9affad56c59ddd3566efa93ebea228190c00faa651aba3db53b3321dd9d5b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x936ba9ab1b7663b71f67d99592146a0e6c988bbcc052065bae88b4c702e9c08a:0xc9df784afbf7467b3a9fd3e2c140e327e9f8212b586b9e12d4394d057dc107af:477", + "liquidityOccurrenceId": "1:0x936ba9ab1b7663b71f67d99592146a0e6c988bbcc052065bae88b4c702e9c08a:0xc9df784afbf7467b3a9fd3e2c140e327e9f8212b586b9e12d4394d057dc107af:478", + "initialBuyOccurrenceId": "1:0x936ba9ab1b7663b71f67d99592146a0e6c988bbcc052065bae88b4c702e9c08a:0xc9df784afbf7467b3a9fd3e2c140e327e9f8212b586b9e12d4394d057dc107af:479", + "custodyOccurrenceId": "1:0x936ba9ab1b7663b71f67d99592146a0e6c988bbcc052065bae88b4c702e9c08a:0xc9df784afbf7467b3a9fd3e2c140e327e9f8212b586b9e12d4394d057dc107af:480", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646018" + }, + { + "id": "1:classic-v3:0x03572ad605253faf8050f629faf83dabeded077434640b27a2127c334fdc5c85", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x03572ad605253faf8050f629faf83dabeded077434640b27a2127c334fdc5c85", + "token": "0xfda92d6260ee16dcc6a51a7c886231c5bcf90d27", + "creator": "0x6cab818daaf4040476e2489a7ed29ca685ad0623", + "quoteAsset": null, + "poolId": "0x409688281c60b8ad24da493e755e804c5241c4034b474e45114c7b47431897ff", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x3e494d31274f66b1017a62f89422b5b2a0bee4ab", + "positionRecipient": "0xc0f9c64d11332d26561b8c62093f9bdd5931bea1", + "positionTokenId": "355816", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x02ca5e6d8987c098a44693c9ba470480ff9d4f4ce5d336a91fd0ae5efcc0a94d", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf71d8cad2c55096d36a519367fa83d34a180b8e8ad2622de6ef5e1194afb9126:0x812367d4f97fa347815ca2a7c134a19b2ad7a54f5f4a7645cd77ac0b67412fdb:513", + "liquidityOccurrenceId": "1:0xf71d8cad2c55096d36a519367fa83d34a180b8e8ad2622de6ef5e1194afb9126:0x812367d4f97fa347815ca2a7c134a19b2ad7a54f5f4a7645cd77ac0b67412fdb:514", + "initialBuyOccurrenceId": "1:0xf71d8cad2c55096d36a519367fa83d34a180b8e8ad2622de6ef5e1194afb9126:0x812367d4f97fa347815ca2a7c134a19b2ad7a54f5f4a7645cd77ac0b67412fdb:515", + "custodyOccurrenceId": "1:0xf71d8cad2c55096d36a519367fa83d34a180b8e8ad2622de6ef5e1194afb9126:0x812367d4f97fa347815ca2a7c134a19b2ad7a54f5f4a7645cd77ac0b67412fdb:516", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645971" + }, + { + "id": "1:classic-v3:0x0378b82410b011c0c1de99b67494a16e228b8a7a94920280d3ca1329881b71c0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x0378b82410b011c0c1de99b67494a16e228b8a7a94920280d3ca1329881b71c0", + "token": "0x1d6a9b4db4e93babf67d0093e6e954b5358a21de", + "creator": "0x9e9bf7495369e73e76e0f41c00481b16a270711c", + "quoteAsset": null, + "poolId": "0xfea4988937d428355b9e07118227ea92060eb03413d8009c0607a2a78b36bef0", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xbc2145f6911ce70b54df411a7e54199f7a92720b", + "positionRecipient": "0x0cd30885597d679c32a5767ba0d8427cddaa373c", + "positionTokenId": "357036", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x80341c0108e2dff7acf59420ddece22beb9ef7e2ae61383f2e4fce4d94a3831a", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x3a678347b6838ba954679e84203a6fcb83b7d0bfdc4a5558016d73c6c1e54ffc:0x852b2503dc95a20594bb1f016d55c12877d9c559aa2312b73354c8cb222ee574:1075", + "liquidityOccurrenceId": "1:0x3a678347b6838ba954679e84203a6fcb83b7d0bfdc4a5558016d73c6c1e54ffc:0x852b2503dc95a20594bb1f016d55c12877d9c559aa2312b73354c8cb222ee574:1076", + "initialBuyOccurrenceId": "1:0x3a678347b6838ba954679e84203a6fcb83b7d0bfdc4a5558016d73c6c1e54ffc:0x852b2503dc95a20594bb1f016d55c12877d9c559aa2312b73354c8cb222ee574:1077", + "custodyOccurrenceId": "1:0x3a678347b6838ba954679e84203a6fcb83b7d0bfdc4a5558016d73c6c1e54ffc:0x852b2503dc95a20594bb1f016d55c12877d9c559aa2312b73354c8cb222ee574:1078", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25650840" + }, + { + "id": "1:classic-v3:0x03f7697602709a4a7e6200b3d94bc22cdc347dadfbba2cf79ecfc63cde6e46b6", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x03f7697602709a4a7e6200b3d94bc22cdc347dadfbba2cf79ecfc63cde6e46b6", + "token": "0x75e649a60e2c79879d70f56eab043f8835e5b1b0", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0xb0516622f0cfbdf6fc4a01c778188a04e5ea1494d0d9c3ec23d86da1288f190c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x4bba06cd7e00a5d756b5e4a0d09279cb12034944", + "positionRecipient": "0x90462fb5540b227e79c1d372007ea9600bf1c13e", + "positionTokenId": "355745", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9340c17c04e33232a8630d3c02d0995accc7e72ea222884eae52f7eaab45e737", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x06ae556141a7d519333a118721ab05c19181f75474b3fb678f2faf0c82673df7:0x3fa3f62603bc3bc5ddaa7391ae3c14124c09098764ec4c46ac770d91532e3514:152", + "liquidityOccurrenceId": "1:0x06ae556141a7d519333a118721ab05c19181f75474b3fb678f2faf0c82673df7:0x3fa3f62603bc3bc5ddaa7391ae3c14124c09098764ec4c46ac770d91532e3514:153", + "initialBuyOccurrenceId": "1:0x06ae556141a7d519333a118721ab05c19181f75474b3fb678f2faf0c82673df7:0x3fa3f62603bc3bc5ddaa7391ae3c14124c09098764ec4c46ac770d91532e3514:154", + "custodyOccurrenceId": "1:0x06ae556141a7d519333a118721ab05c19181f75474b3fb678f2faf0c82673df7:0x3fa3f62603bc3bc5ddaa7391ae3c14124c09098764ec4c46ac770d91532e3514:155", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645792" + }, + { + "id": "1:classic-v3:0x04234ada9b87f9a713954a82b7136be242fc601e4ae18d25aba37f7cb62633a6", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x04234ada9b87f9a713954a82b7136be242fc601e4ae18d25aba37f7cb62633a6", + "token": "0xc5d2b11bc305be08e963983e9f848fd744fb5f47", + "creator": "0xaa01c01fba173f68895a4e7af95803de7b40636a", + "quoteAsset": null, + "poolId": "0xdd2ac781ba27681904bfdd34e05fc49a5e19ea055e2195a99c2f9fd086258cce", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xfedf9db2b9fe1d6562ad50f1ba8bdc81b9ea6060", + "positionRecipient": "0x0edb6e2efed192fc96ee445f7205e6375b7cb368", + "positionTokenId": "355727", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xb7ea1d1eaba1400510c9e01d68d85eae876f531dc35db6f04fbeb5e09695f0b9", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "30000000000000000", + "initialBuyTokenAmount": "21438505518229829458161070", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x5b4b16aeead0b9e0e760054711ac32f85d9c85841ae783fc322d335fcae2b54f:0x961a060318a46aecee380e62cceb091861e35a5463087a12484aad1ccd26948c:601", + "liquidityOccurrenceId": "1:0x5b4b16aeead0b9e0e760054711ac32f85d9c85841ae783fc322d335fcae2b54f:0x961a060318a46aecee380e62cceb091861e35a5463087a12484aad1ccd26948c:602", + "initialBuyOccurrenceId": "1:0x5b4b16aeead0b9e0e760054711ac32f85d9c85841ae783fc322d335fcae2b54f:0x961a060318a46aecee380e62cceb091861e35a5463087a12484aad1ccd26948c:603", + "custodyOccurrenceId": "1:0x5b4b16aeead0b9e0e760054711ac32f85d9c85841ae783fc322d335fcae2b54f:0x961a060318a46aecee380e62cceb091861e35a5463087a12484aad1ccd26948c:604", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645744" + }, + { + "id": "1:classic-v3:0x05e6b8843a2c1077b97f851f09f8ec3b86e687d92aaafb50c55e34a10f2badf0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x05e6b8843a2c1077b97f851f09f8ec3b86e687d92aaafb50c55e34a10f2badf0", + "token": "0x1030aae11559636faada304c3d7d45fa730972b1", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0xb7798b17e0b53649fa15977ede6d57441b14e641bbd5cf71ec0806e5b1b92f5d", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa31e6000f17af5435af6530d6538c42357d37f23", + "positionRecipient": "0xe1c7208f9e7e19ba99415e82fd06be4fd5da401b", + "positionTokenId": "355878", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x73fcdc1b666b82dae3fd48afb9d321180bdb11c770d4268bf5e98fc7d23930ff", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7dc705be68d22c60542b2f9df6a13e0742c66bfd18712e7670aefd1465f95bbf:0x5acefc655be226bf38d9c59dcfda11799c6d3e6abf3a9d321f96ea05d3d9def7:529", + "liquidityOccurrenceId": "1:0x7dc705be68d22c60542b2f9df6a13e0742c66bfd18712e7670aefd1465f95bbf:0x5acefc655be226bf38d9c59dcfda11799c6d3e6abf3a9d321f96ea05d3d9def7:530", + "initialBuyOccurrenceId": "1:0x7dc705be68d22c60542b2f9df6a13e0742c66bfd18712e7670aefd1465f95bbf:0x5acefc655be226bf38d9c59dcfda11799c6d3e6abf3a9d321f96ea05d3d9def7:531", + "custodyOccurrenceId": "1:0x7dc705be68d22c60542b2f9df6a13e0742c66bfd18712e7670aefd1465f95bbf:0x5acefc655be226bf38d9c59dcfda11799c6d3e6abf3a9d321f96ea05d3d9def7:532", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646082" + }, + { + "id": "1:classic-v3:0x05f0b74e77104236a69a9c257bfe5963b77834d0fefcb2f7161b53304013264a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x05f0b74e77104236a69a9c257bfe5963b77834d0fefcb2f7161b53304013264a", + "token": "0x758f7155f133d688393029217f97f330fa66ada4", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x7472e8c3939fddd271abe76fa1e2e766975b89bd9106b8b442d47180d4088ba1", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x90dd595defe7a1034df69f672e8dca512d315459", + "positionRecipient": "0x1334af23981a4eca5e393f07aa46df12041cbc3c", + "positionTokenId": "355839", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xbbda4485f0a38320ffcaad56e92fec2f4bebeb5978b41c94b6c2f89efa865acd", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x6ccd7f2c4aa554641bd229c4c66afd1befd4ce783e10f866f46635b133ecd74d:0x868477db4036a74b2aeaea706b2798ab38e80c9bd1bd1135a8b3a44c608d9ba3:370", + "liquidityOccurrenceId": "1:0x6ccd7f2c4aa554641bd229c4c66afd1befd4ce783e10f866f46635b133ecd74d:0x868477db4036a74b2aeaea706b2798ab38e80c9bd1bd1135a8b3a44c608d9ba3:371", + "initialBuyOccurrenceId": "1:0x6ccd7f2c4aa554641bd229c4c66afd1befd4ce783e10f866f46635b133ecd74d:0x868477db4036a74b2aeaea706b2798ab38e80c9bd1bd1135a8b3a44c608d9ba3:372", + "custodyOccurrenceId": "1:0x6ccd7f2c4aa554641bd229c4c66afd1befd4ce783e10f866f46635b133ecd74d:0x868477db4036a74b2aeaea706b2798ab38e80c9bd1bd1135a8b3a44c608d9ba3:373", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645993" + }, + { + "id": "1:classic-v3:0x0780b0cdd914c414c0fc3d7acbfdc52bcef7e404a93e2351da3cf00948939e1f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x0780b0cdd914c414c0fc3d7acbfdc52bcef7e404a93e2351da3cf00948939e1f", + "token": "0xbf4755aa8a2c00f6316edc3bdad531e0dc265361", + "creator": "0x9170813cc45caffc24e21a72ccfad2fcd180ba89", + "quoteAsset": null, + "poolId": "0xa1bcb88c51292490ba458efd9eb99e8689aadc46bfc8bd397239e7b00f413130", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x057c522becac204bd8c8a24ba5dfeff2741123ec", + "positionRecipient": "0x97321ec64a34b7557462d341959776ee2195d90e", + "positionTokenId": "355690", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x1b912beefea243de61d62976bae88f55d07c7801f08163ac3d352db5c8ffb9fe", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc52912af0c2a3f09e67c65dd5146ac6a13fa90867016ddb2add503175e5db69d:0x6d95097202473175d10e705ee4cf8d566bf2f03e03e501817e45f64bea8cd508:487", + "liquidityOccurrenceId": "1:0xc52912af0c2a3f09e67c65dd5146ac6a13fa90867016ddb2add503175e5db69d:0x6d95097202473175d10e705ee4cf8d566bf2f03e03e501817e45f64bea8cd508:488", + "initialBuyOccurrenceId": "1:0xc52912af0c2a3f09e67c65dd5146ac6a13fa90867016ddb2add503175e5db69d:0x6d95097202473175d10e705ee4cf8d566bf2f03e03e501817e45f64bea8cd508:489", + "custodyOccurrenceId": "1:0xc52912af0c2a3f09e67c65dd5146ac6a13fa90867016ddb2add503175e5db69d:0x6d95097202473175d10e705ee4cf8d566bf2f03e03e501817e45f64bea8cd508:490", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645686" + }, + { + "id": "1:classic-v3:0x0ae2363120ea68a81eb08a50df306a2ca491d2acb525062beda08bd4a1eeaff8", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x0ae2363120ea68a81eb08a50df306a2ca491d2acb525062beda08bd4a1eeaff8", + "token": "0xf4e92fb2a2d1d4111e88217971e2ff8abc12e0db", + "creator": "0xe51faf16f4ee8cc168949077ac624e745bb93720", + "quoteAsset": null, + "poolId": "0x64f3e1b931be156299f58afd65fda2bfa6a46c323ab5657d7dfd0adade42c437", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x41fc123a8edcfb3c8b187f3902d24809637220e0", + "positionRecipient": "0x51dc2543fb4aca8b5d1162a1d82861adc2aeb580", + "positionTokenId": "355413", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x2bb484e4a2c6b4a2a2ec7a688612daa198d50d15bb521670506320c96653dd4a", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x849235d548ccf7a5467a8eb82fb409528114939f5000936db3c22b2dcec84728:0x0b47bab511940f74ec7d79a22407623330afa392513c82bdabe7a722ea7c3bc1:71", + "liquidityOccurrenceId": "1:0x849235d548ccf7a5467a8eb82fb409528114939f5000936db3c22b2dcec84728:0x0b47bab511940f74ec7d79a22407623330afa392513c82bdabe7a722ea7c3bc1:72", + "initialBuyOccurrenceId": "1:0x849235d548ccf7a5467a8eb82fb409528114939f5000936db3c22b2dcec84728:0x0b47bab511940f74ec7d79a22407623330afa392513c82bdabe7a722ea7c3bc1:73", + "custodyOccurrenceId": "1:0x849235d548ccf7a5467a8eb82fb409528114939f5000936db3c22b2dcec84728:0x0b47bab511940f74ec7d79a22407623330afa392513c82bdabe7a722ea7c3bc1:74", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645062" + }, + { + "id": "1:classic-v3:0x0b05c25184f684f22cbc79f82851e6edb6b79d4f7789e5a3cdb7fbd06b758bab", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x0b05c25184f684f22cbc79f82851e6edb6b79d4f7789e5a3cdb7fbd06b758bab", + "token": "0x748fddf6ccf2a6a7433c52b7f687b2773936eb90", + "creator": "0xb29bf1629da12af5678882bd4caffe7edc690f5a", + "quoteAsset": null, + "poolId": "0xf17ad30f0d0b4c2f687058d2e5273848780614ab18bc1b75b6dad45b6a403512", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xf48c0b9d7354625ad2aa94f34d4f270d0f5c94fb", + "positionRecipient": "0x84a6dad862146f592d1f0892dd56e86eb890bc23", + "positionTokenId": "356084", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0xcd0ed91096490ec7aa6d2a37d1624aee42951224fbeb41519eaf4696fbdecbb4", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "15000000000000000", + "initialBuyTokenAmount": "10727125733381350585769746", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x959af5609e67d94c941d319c65dcd8672896a443f0f1cd8a0c86c0e190db0dba:0x4eb52dca9e63f03b3f5e2d43e47485e9e9dc180ec0156360b7370932aca7279b:290", + "liquidityOccurrenceId": "1:0x959af5609e67d94c941d319c65dcd8672896a443f0f1cd8a0c86c0e190db0dba:0x4eb52dca9e63f03b3f5e2d43e47485e9e9dc180ec0156360b7370932aca7279b:291", + "initialBuyOccurrenceId": "1:0x959af5609e67d94c941d319c65dcd8672896a443f0f1cd8a0c86c0e190db0dba:0x4eb52dca9e63f03b3f5e2d43e47485e9e9dc180ec0156360b7370932aca7279b:292", + "custodyOccurrenceId": "1:0x959af5609e67d94c941d319c65dcd8672896a443f0f1cd8a0c86c0e190db0dba:0x4eb52dca9e63f03b3f5e2d43e47485e9e9dc180ec0156360b7370932aca7279b:293", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646766" + }, + { + "id": "1:classic-v3:0x0b84371fccf198d16b3eb2a47e59b9a70ca0e643afdf88ab11854053e7de9525", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x0b84371fccf198d16b3eb2a47e59b9a70ca0e643afdf88ab11854053e7de9525", + "token": "0x7ea8e8590fb12e81a828fcb121eb9324d5cfda44", + "creator": "0xa7e7462b9c974aec733e1b3bc7a9c8e9cc291372", + "quoteAsset": null, + "poolId": "0xafc58fb587d8ce913f66d2c7d01946ad77f9b36dc78288068352c168e074f59b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1bd606eb7964d30d1d6db19f1b7dfce800c1cbec", + "positionRecipient": "0xadad15318993328923f36ef5e6237475e48453b2", + "positionTokenId": "354637", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x435115ed98a17c554caa3c0437f86bcb76d8fef4fdb70ce7fd6a00a7d7210324", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "500000000000000000", + "initialBuyTokenAmount": "267472468697533373101230033", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x11a51ea4ead533dead9c47518c7bb5a6b655f138e73068e4f638878f5d62d3ac:0x7c4c2c1d2dd485da408399f8733fc5cb7957d09b91b437a60b907435323f2d7f:632", + "liquidityOccurrenceId": "1:0x11a51ea4ead533dead9c47518c7bb5a6b655f138e73068e4f638878f5d62d3ac:0x7c4c2c1d2dd485da408399f8733fc5cb7957d09b91b437a60b907435323f2d7f:633", + "initialBuyOccurrenceId": "1:0x11a51ea4ead533dead9c47518c7bb5a6b655f138e73068e4f638878f5d62d3ac:0x7c4c2c1d2dd485da408399f8733fc5cb7957d09b91b437a60b907435323f2d7f:634", + "custodyOccurrenceId": "1:0x11a51ea4ead533dead9c47518c7bb5a6b655f138e73068e4f638878f5d62d3ac:0x7c4c2c1d2dd485da408399f8733fc5cb7957d09b91b437a60b907435323f2d7f:635", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640482" + }, + { + "id": "1:classic-v3:0x1018e8f534766dc795d181eb3894cfc527e5a303403041b9ccc87970b09e57d9", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x1018e8f534766dc795d181eb3894cfc527e5a303403041b9ccc87970b09e57d9", + "token": "0x299f11367b16a21dbeaf716d75186d6a2357aae4", + "creator": "0x64d65e1c4f2e70dc1114f14c6505678c048da7ec", + "quoteAsset": null, + "poolId": "0x04b7eebb68942f67b33118cd6ef6d071c46525e6acde67b9327c4bf6f8e87849", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x38a97350698b182193f9037eea42def8f3819c02", + "positionRecipient": "0xf255c61e57c546269f094cd1676dc41e08da8e21", + "positionTokenId": "356045", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0xf2a93b898c6eba81ce00251bc1a8db7bce234408b02ec78ffdf5422fe1eb25f7", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "34883942100954326694409764", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x76e9ed8df9175713f2a399f41ad96e1da699b44ac2c98ba8a0d62979f95f9515:0x53fe356bb2f65eba6bcb04f4c45bac01cff9a660e22f4f9b40896015a697c511:84", + "liquidityOccurrenceId": "1:0x76e9ed8df9175713f2a399f41ad96e1da699b44ac2c98ba8a0d62979f95f9515:0x53fe356bb2f65eba6bcb04f4c45bac01cff9a660e22f4f9b40896015a697c511:85", + "initialBuyOccurrenceId": "1:0x76e9ed8df9175713f2a399f41ad96e1da699b44ac2c98ba8a0d62979f95f9515:0x53fe356bb2f65eba6bcb04f4c45bac01cff9a660e22f4f9b40896015a697c511:86", + "custodyOccurrenceId": "1:0x76e9ed8df9175713f2a399f41ad96e1da699b44ac2c98ba8a0d62979f95f9515:0x53fe356bb2f65eba6bcb04f4c45bac01cff9a660e22f4f9b40896015a697c511:87", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646524" + }, + { + "id": "1:classic-v3:0x11ba1e286dd5495ee0cec5adb6be5d85758dc7f8835426ad398f165d10b56aaa", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x11ba1e286dd5495ee0cec5adb6be5d85758dc7f8835426ad398f165d10b56aaa", + "token": "0xd5e46da3718b5e433f26dc01313c842681a1a35f", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0x5b87133b8cf8d5556c57cc5e46e7b1bd39a662d7c6cfd2654c7518ed2e019cca", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x9b2c8aee835af49924fa7b802f6c5c65e0aba623", + "positionRecipient": "0x9a268c5ae287948c3248eb2605bd34116f7c30ab", + "positionTokenId": "355790", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x7a95ff3917ee5601d0fbaf04d33dda75aaeca85ee0a9b21b9b4022de5ee1ed2b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xbb12132247293dbeb2a5a72ec19c8236e45854196007429e3d727ecb20d3c84b:0xf96672338172ebad2b998b76dca43f33818d27442360ddd108878df07fda6ab3:677", + "liquidityOccurrenceId": "1:0xbb12132247293dbeb2a5a72ec19c8236e45854196007429e3d727ecb20d3c84b:0xf96672338172ebad2b998b76dca43f33818d27442360ddd108878df07fda6ab3:678", + "initialBuyOccurrenceId": "1:0xbb12132247293dbeb2a5a72ec19c8236e45854196007429e3d727ecb20d3c84b:0xf96672338172ebad2b998b76dca43f33818d27442360ddd108878df07fda6ab3:679", + "custodyOccurrenceId": "1:0xbb12132247293dbeb2a5a72ec19c8236e45854196007429e3d727ecb20d3c84b:0xf96672338172ebad2b998b76dca43f33818d27442360ddd108878df07fda6ab3:680", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645913" + }, + { + "id": "1:classic-v3:0x1224fd610f83c103cc2775d2ee1fc3e0be1a82ce78e7283c969aa9cb12350298", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x1224fd610f83c103cc2775d2ee1fc3e0be1a82ce78e7283c969aa9cb12350298", + "token": "0x1d9774fae531ade66184e59a52a95b208fd93669", + "creator": "0xa6587d6a21a2086dd4fde6326ef72006acff0df1", + "quoteAsset": null, + "poolId": "0x998289c687b3d152ea7262b5a79cfe981b74c18500092e47684ccc41d7fee7bd", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x3797fc1ff350a68acdc085b0cd72ad566311cfde", + "positionRecipient": "0xa4e566635df2124db7a33fa8c950c99f508b3318", + "positionTokenId": "355637", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x617a5ba4c694aca6fa6e6a4965c9dfd4c8891b90a0c04cb962d5fcacb732791e", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x82437962d2e9ed23a68df38376d3e77d5b4511c042c89dbf034a7b7228726815:0xbc537f4a141d336cec894a927e7822a7f7b26cde4158bec082749f8b35a2fc95:592", + "liquidityOccurrenceId": "1:0x82437962d2e9ed23a68df38376d3e77d5b4511c042c89dbf034a7b7228726815:0xbc537f4a141d336cec894a927e7822a7f7b26cde4158bec082749f8b35a2fc95:593", + "initialBuyOccurrenceId": "1:0x82437962d2e9ed23a68df38376d3e77d5b4511c042c89dbf034a7b7228726815:0xbc537f4a141d336cec894a927e7822a7f7b26cde4158bec082749f8b35a2fc95:594", + "custodyOccurrenceId": "1:0x82437962d2e9ed23a68df38376d3e77d5b4511c042c89dbf034a7b7228726815:0xbc537f4a141d336cec894a927e7822a7f7b26cde4158bec082749f8b35a2fc95:595", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645602" + }, + { + "id": "1:classic-v3:0x155193aa9ed9edec5ddd71ae9a447598e96cdb523a5ed5b33327537d872fc99c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x155193aa9ed9edec5ddd71ae9a447598e96cdb523a5ed5b33327537d872fc99c", + "token": "0xbac99009a36e72f0e99e2da0bae4697169e71cfb", + "creator": "0x5ffa822ae9e83777be4468bfed1d2fad5d305df5", + "quoteAsset": null, + "poolId": "0x090f37c1d6a4e56b738f62949b0f1090c8d453622faffaf0e0bc081b8caccd63", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x93db5fe3488a219d6425f881e5422a14dc959c36", + "positionRecipient": "0x1831c04b3182cc686ccf49c4bed6be74120db028", + "positionTokenId": "354614", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xe046c61a1f3aec77d7951f8d90b440267d64d5df84edb8b28c486f86cff2f95b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x555b420e20215d164877cfc9a7d414480625c93885b3fe0767ad165540b7da2b:0x579bb6078bec11b32d4a2d2100d2bb5a5c1d5f8abac17e8fdb866d03f5a9e923:18", + "liquidityOccurrenceId": "1:0x555b420e20215d164877cfc9a7d414480625c93885b3fe0767ad165540b7da2b:0x579bb6078bec11b32d4a2d2100d2bb5a5c1d5f8abac17e8fdb866d03f5a9e923:19", + "initialBuyOccurrenceId": "1:0x555b420e20215d164877cfc9a7d414480625c93885b3fe0767ad165540b7da2b:0x579bb6078bec11b32d4a2d2100d2bb5a5c1d5f8abac17e8fdb866d03f5a9e923:20", + "custodyOccurrenceId": "1:0x555b420e20215d164877cfc9a7d414480625c93885b3fe0767ad165540b7da2b:0x579bb6078bec11b32d4a2d2100d2bb5a5c1d5f8abac17e8fdb866d03f5a9e923:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640390" + }, + { + "id": "1:classic-v3:0x169f643f23f9aa12d2846a3ad88567b99ada13513912910d6d3e33c40b0794a8", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x169f643f23f9aa12d2846a3ad88567b99ada13513912910d6d3e33c40b0794a8", + "token": "0x193c47a17da2cfee5b7542c40a77f7f51663c4e6", + "creator": "0xe1a4c93150085929e928a164bd828a2a10cae37b", + "quoteAsset": null, + "poolId": "0x57f6135fe61cfda23f06091de56c92600bc455b4cd37e123fceef883843dde12", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x93422b1d7f4617caafc4c31c0862550018b0b83c", + "positionRecipient": "0x9af7e7dfb30cf97b099e454bf7972470c731c7b4", + "positionTokenId": "355749", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xde2428a19b14cda168bbae803811643f37727f80387e3e80626f0dc84932963b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xba2aadfd362e6a2a4201fac6d8f976c122d45b5984d37dacd62ca23df5a2345a:0xf6196649fe5a1b67560d0de1fe075a333f6e9912b173e440bcca7931ae3d5465:782", + "liquidityOccurrenceId": "1:0xba2aadfd362e6a2a4201fac6d8f976c122d45b5984d37dacd62ca23df5a2345a:0xf6196649fe5a1b67560d0de1fe075a333f6e9912b173e440bcca7931ae3d5465:783", + "initialBuyOccurrenceId": "1:0xba2aadfd362e6a2a4201fac6d8f976c122d45b5984d37dacd62ca23df5a2345a:0xf6196649fe5a1b67560d0de1fe075a333f6e9912b173e440bcca7931ae3d5465:784", + "custodyOccurrenceId": "1:0xba2aadfd362e6a2a4201fac6d8f976c122d45b5984d37dacd62ca23df5a2345a:0xf6196649fe5a1b67560d0de1fe075a333f6e9912b173e440bcca7931ae3d5465:785", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645805" + }, + { + "id": "1:classic-v3:0x17b2db503125ada977875dc9a451041051f2752bffd11e3d6ef38936d72c3fc7", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x17b2db503125ada977875dc9a451041051f2752bffd11e3d6ef38936d72c3fc7", + "token": "0x20b4e00423ad9cf9c077133a152363257fa24d3b", + "creator": "0x12e3a550a6bfa0083dd8e366e287887084c4a5a9", + "quoteAsset": null, + "poolId": "0x24670de1aaa0d12876a2f06d07ee2d2e4b9b03d4c21d63577819e31fd0289047", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x3b6b633ffb6c13103d1c798267fb17e66145583e", + "positionRecipient": "0x2e227d6e6c79530cebb0b5cbe97305462a5e237b", + "positionTokenId": "356904", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xb9cd01b862bdd545c1ccfd6ada0cb646c44d0da22ae965b61eaa51263aa2077a", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa5645e21473cac3fc1e3f1707a9073596a5a72426a69d1027d74d79417e57727:0x8cf7c8b025d94cdb7810b27724bc683de365677574b102eb74f1b47cafd9be83:69", + "liquidityOccurrenceId": "1:0xa5645e21473cac3fc1e3f1707a9073596a5a72426a69d1027d74d79417e57727:0x8cf7c8b025d94cdb7810b27724bc683de365677574b102eb74f1b47cafd9be83:70", + "initialBuyOccurrenceId": "1:0xa5645e21473cac3fc1e3f1707a9073596a5a72426a69d1027d74d79417e57727:0x8cf7c8b025d94cdb7810b27724bc683de365677574b102eb74f1b47cafd9be83:71", + "custodyOccurrenceId": "1:0xa5645e21473cac3fc1e3f1707a9073596a5a72426a69d1027d74d79417e57727:0x8cf7c8b025d94cdb7810b27724bc683de365677574b102eb74f1b47cafd9be83:72", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25650195" + }, + { + "id": "1:classic-v3:0x18c2dbb6e427b400078c384ac0307ed3abe04039700bfb7e5d0064cbc14476d4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x18c2dbb6e427b400078c384ac0307ed3abe04039700bfb7e5d0064cbc14476d4", + "token": "0x45d5cfb8b0994f8bfd7b9d7183906378dcbee530", + "creator": "0x0565f267d5b5ae98b22447184703011375a6d1b0", + "quoteAsset": null, + "poolId": "0x458ff30e80fd573939f9fe6b749b880d88688658a09090eac1b22c0f64a9df61", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x4e48fd44bbf93c2079ee47cf0ab4afe7edc80b6f", + "positionRecipient": "0x7c496ff1849ad9eea58f2cb23ab400b6da3ed96c", + "positionTokenId": "355653", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xeadde142340806bc16449f1b28df79db8944d155433b9c8fd875b8fc4480815c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x71b4d79448e2170767cc76d807794ed82897d378d525fb887a58d2b7b3a2938a:0x188f210b96d15cf41f4f67e8891a08ae6671748069b8d859f6bb3d2b145ec1a5:816", + "liquidityOccurrenceId": "1:0x71b4d79448e2170767cc76d807794ed82897d378d525fb887a58d2b7b3a2938a:0x188f210b96d15cf41f4f67e8891a08ae6671748069b8d859f6bb3d2b145ec1a5:817", + "initialBuyOccurrenceId": "1:0x71b4d79448e2170767cc76d807794ed82897d378d525fb887a58d2b7b3a2938a:0x188f210b96d15cf41f4f67e8891a08ae6671748069b8d859f6bb3d2b145ec1a5:818", + "custodyOccurrenceId": "1:0x71b4d79448e2170767cc76d807794ed82897d378d525fb887a58d2b7b3a2938a:0x188f210b96d15cf41f4f67e8891a08ae6671748069b8d859f6bb3d2b145ec1a5:819", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645627" + }, + { + "id": "1:classic-v3:0x1917736a3fdd6daad8cdeeabca9cbe2d93f325169578db8c83e64cc973f8e06f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x1917736a3fdd6daad8cdeeabca9cbe2d93f325169578db8c83e64cc973f8e06f", + "token": "0xf9edf7126ebc5141e0e421e1b50c07ff9401cb51", + "creator": "0xd7005c031b2c6c24e76a93df5823122bb210f3d4", + "quoteAsset": null, + "poolId": "0x1f77ee6e6d7560797b4af11e31a2f1b9a7d873f44890e189d13a6cdfa8042a7e", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xc849d9c957d99dd39d92f237945420e1e0195014", + "positionRecipient": "0x2e290f4dfe69e85299dab62ae24bd768c36292d9", + "positionTokenId": "354633", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x774d32793f7af01d21e7afe3cdee102da8624d626dc036ebdb901104092e3231", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf01794fe6b1e34bb4ff99736c2e6e99d3b122e633009cd7c28c3cf15f58f3074:0xd61c38627f792f5cb009d0e80ec796236b0578cf1ddab314faba3d51c715dda2:40", + "liquidityOccurrenceId": "1:0xf01794fe6b1e34bb4ff99736c2e6e99d3b122e633009cd7c28c3cf15f58f3074:0xd61c38627f792f5cb009d0e80ec796236b0578cf1ddab314faba3d51c715dda2:41", + "initialBuyOccurrenceId": "1:0xf01794fe6b1e34bb4ff99736c2e6e99d3b122e633009cd7c28c3cf15f58f3074:0xd61c38627f792f5cb009d0e80ec796236b0578cf1ddab314faba3d51c715dda2:42", + "custodyOccurrenceId": "1:0xf01794fe6b1e34bb4ff99736c2e6e99d3b122e633009cd7c28c3cf15f58f3074:0xd61c38627f792f5cb009d0e80ec796236b0578cf1ddab314faba3d51c715dda2:43", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640462" + }, + { + "id": "1:classic-v3:0x19a0250364709f21bcd341bed2a87789e6f1f39308cb18f00d5d82c44ff37a26", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x19a0250364709f21bcd341bed2a87789e6f1f39308cb18f00d5d82c44ff37a26", + "token": "0xcdfcd3f8ed711b8fe27619798ac6ec8de2cde821", + "creator": "0xee33695ef2831f8d9f7b5ec04af91c65d9c0ffee", + "quoteAsset": null, + "poolId": "0xb82dde7f1c00620ae8df2e062a47d7dee041d7aeb343bf1a14832d4c24906792", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xdb577ed62a1d7de066a268993fafcd7304467732", + "positionRecipient": "0x788b1c62a778a0933ce92ad2131b2a39491272bf", + "positionTokenId": "355959", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x5317cc90aafbb93e51d5bfc8f3ef5edd52c77ccf2595af6e18d97bae0d428837", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x2270e9e3bad6ea962d19ea92f920485364c5b81af8f516dbf552a3a473309160:0x42247988daf5c40d1f1dd0001f41a0c38a52d29091222c8d7dda4edf06af4e6f:456", + "liquidityOccurrenceId": "1:0x2270e9e3bad6ea962d19ea92f920485364c5b81af8f516dbf552a3a473309160:0x42247988daf5c40d1f1dd0001f41a0c38a52d29091222c8d7dda4edf06af4e6f:457", + "initialBuyOccurrenceId": "1:0x2270e9e3bad6ea962d19ea92f920485364c5b81af8f516dbf552a3a473309160:0x42247988daf5c40d1f1dd0001f41a0c38a52d29091222c8d7dda4edf06af4e6f:458", + "custodyOccurrenceId": "1:0x2270e9e3bad6ea962d19ea92f920485364c5b81af8f516dbf552a3a473309160:0x42247988daf5c40d1f1dd0001f41a0c38a52d29091222c8d7dda4edf06af4e6f:459", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646270" + }, + { + "id": "1:classic-v3:0x19b5919cbcf5d65e8f05ccb09186d9e701008b18844a8ebcc3cf6367af3cf542", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x19b5919cbcf5d65e8f05ccb09186d9e701008b18844a8ebcc3cf6367af3cf542", + "token": "0x47dff944a615ef6b701682ff850e99ce2fddd39a", + "creator": "0x9e9bf7495369e73e76e0f41c00481b16a270711c", + "quoteAsset": null, + "poolId": "0x851f5066cafa1366716ee9ac215d81ca36e3a132d64992c7f7de734f1918a025", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x11035fb91cea52d3616de0437bfff9b34f4b8881", + "positionRecipient": "0x6b8464a1cfe16bf1e0c759e4a1febb34d6d6b6c1", + "positionTokenId": "357281", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xcf7c6ec8aa704b558c91c591e4f2821d8f686ef5ac455b8317f3f305a0715f22", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "2000000000000000", + "initialBuyTokenAmount": "1458415534058453948045650", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x461e13cc729f978a31de87aa4bd7e9aac7e5a6a304d560a1eee061b75e63f628:0x65bd429b24bfc3088cfc1742f785767089c050b287ad2ec8442b17d81eb9050a:89", + "liquidityOccurrenceId": "1:0x461e13cc729f978a31de87aa4bd7e9aac7e5a6a304d560a1eee061b75e63f628:0x65bd429b24bfc3088cfc1742f785767089c050b287ad2ec8442b17d81eb9050a:90", + "initialBuyOccurrenceId": "1:0x461e13cc729f978a31de87aa4bd7e9aac7e5a6a304d560a1eee061b75e63f628:0x65bd429b24bfc3088cfc1742f785767089c050b287ad2ec8442b17d81eb9050a:91", + "custodyOccurrenceId": "1:0x461e13cc729f978a31de87aa4bd7e9aac7e5a6a304d560a1eee061b75e63f628:0x65bd429b24bfc3088cfc1742f785767089c050b287ad2ec8442b17d81eb9050a:92", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25651960" + }, + { + "id": "1:classic-v3:0x1de0f9c143a91cb30881c7f10bf72498e433eada8cce7c38cdc72fd10697759e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x1de0f9c143a91cb30881c7f10bf72498e433eada8cce7c38cdc72fd10697759e", + "token": "0x10fc26d295d6564c6f5855d0f714130244ba72eb", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x7247ade885580679f57ba637334706476a1d7cb6ae6b5f92bd0dcbae94e432db", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x33fa3ba86045bdb8a3a326453fcf0373a1d7a090", + "positionRecipient": "0x50c49b89f602e2d3484251090cb62f8d3073ca37", + "positionTokenId": "355788", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xafed9578b8739207fb67f83c1c5f7835fc2279cbffdba237bad5afcf2fb636b5", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x75c07f6e72ababc6d779fd6f34da61b6936ab12cb06327651c653aafeb55fbbe:0xd204845e6580fed790857a75678a99915fb4af24b0e28f16507ed56006945d93:18", + "liquidityOccurrenceId": "1:0x75c07f6e72ababc6d779fd6f34da61b6936ab12cb06327651c653aafeb55fbbe:0xd204845e6580fed790857a75678a99915fb4af24b0e28f16507ed56006945d93:19", + "initialBuyOccurrenceId": "1:0x75c07f6e72ababc6d779fd6f34da61b6936ab12cb06327651c653aafeb55fbbe:0xd204845e6580fed790857a75678a99915fb4af24b0e28f16507ed56006945d93:20", + "custodyOccurrenceId": "1:0x75c07f6e72ababc6d779fd6f34da61b6936ab12cb06327651c653aafeb55fbbe:0xd204845e6580fed790857a75678a99915fb4af24b0e28f16507ed56006945d93:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645907" + }, + { + "id": "1:classic-v3:0x22944da11b3048509b0dc5964599fddd3f5d69fb401a5bbad1931ffb9c896a64", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x22944da11b3048509b0dc5964599fddd3f5d69fb401a5bbad1931ffb9c896a64", + "token": "0xfeba7e390a30e568cd79c050c8caeb2a22de90ce", + "creator": "0x5da441739614ea06af94b48afb9a7af906d4132d", + "quoteAsset": null, + "poolId": "0x0592081845eb5b7495d470d15c0eb7e34f1f67c2185598e46abf9e748ff68c14", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x31e1a67acd47c030b828ec450a3f43c8120e3e90", + "positionRecipient": "0x743e4a13892e5c97ab6a609eb358a30ab1a6973d", + "positionTokenId": "355625", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x4097b7abccf611e0e2e5803d516bcc584e5d3b2061e4f8a5c76f0a4b3e7e0722", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb1d896a329c63221590ce5f1f5a5341e009e5b30db9c73540c9425e41ca0e98e:0x55adfaf48dcbf63052748bec508bc3e3bef87f316c125f4233761f8e895531c7:127", + "liquidityOccurrenceId": "1:0xb1d896a329c63221590ce5f1f5a5341e009e5b30db9c73540c9425e41ca0e98e:0x55adfaf48dcbf63052748bec508bc3e3bef87f316c125f4233761f8e895531c7:128", + "initialBuyOccurrenceId": "1:0xb1d896a329c63221590ce5f1f5a5341e009e5b30db9c73540c9425e41ca0e98e:0x55adfaf48dcbf63052748bec508bc3e3bef87f316c125f4233761f8e895531c7:129", + "custodyOccurrenceId": "1:0xb1d896a329c63221590ce5f1f5a5341e009e5b30db9c73540c9425e41ca0e98e:0x55adfaf48dcbf63052748bec508bc3e3bef87f316c125f4233761f8e895531c7:130", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645583" + }, + { + "id": "1:classic-v3:0x23ed41983a8c9e6537f47ede1272208e38855634df7775820d4f06060cd5943c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x23ed41983a8c9e6537f47ede1272208e38855634df7775820d4f06060cd5943c", + "token": "0x222e834018a74c11ca105f8addfded879f12da4e", + "creator": "0x5c7917ab56f13a0aada0765559b1e53593900231", + "quoteAsset": null, + "poolId": "0x09a0625ca679d47a4ec1d0c6723ff460cb47423a4ac931e977bbc5edf3ce6b1b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x0825e0cc8f16ba43be4c738d15a6e42890ff2fcf", + "positionRecipient": "0xc8ee35bf056ec3f086d914cc27bf2ef9910133cc", + "positionTokenId": "354606", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9c3002c9d1cacf5c6d6e4924f99d513e519b3f90fac8f424be110810e04b92f5", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x21d534b8ca64f30d2ecc0f94792ec94c1652f5661875723117d37fc34e61c1ec:0x57c1275f4ce23d233c0dd759a82c4ddbf886c2175ab5c50648c403842418cb42:555", + "liquidityOccurrenceId": "1:0x21d534b8ca64f30d2ecc0f94792ec94c1652f5661875723117d37fc34e61c1ec:0x57c1275f4ce23d233c0dd759a82c4ddbf886c2175ab5c50648c403842418cb42:556", + "initialBuyOccurrenceId": "1:0x21d534b8ca64f30d2ecc0f94792ec94c1652f5661875723117d37fc34e61c1ec:0x57c1275f4ce23d233c0dd759a82c4ddbf886c2175ab5c50648c403842418cb42:557", + "custodyOccurrenceId": "1:0x21d534b8ca64f30d2ecc0f94792ec94c1652f5661875723117d37fc34e61c1ec:0x57c1275f4ce23d233c0dd759a82c4ddbf886c2175ab5c50648c403842418cb42:558", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640374" + }, + { + "id": "1:classic-v3:0x263fb7deed1160eda354a68369c342a2a16fcd873fe7d8fcf5f4f28f6870857f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x263fb7deed1160eda354a68369c342a2a16fcd873fe7d8fcf5f4f28f6870857f", + "token": "0xe38aa84de7f9ee5922c40806d147b97d921e8675", + "creator": "0x5cb4a95b28a524e9260ceddd861bb1f2a73fc43a", + "quoteAsset": null, + "poolId": "0x2e5f98805ed7da4316479221874cada7dfc94277e8b8f90f918341c57cf16053", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xcea6eb9baaa27c3cb28f6eaaefa61e5c4f6755e7", + "positionRecipient": "0xf9e0ebc85fca547af1674679fc93e0b899d07d6d", + "positionTokenId": "356307", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x033691aed5ab8ea251240add2e0d1db2602e5beabfef6f512ac459478e479ac3", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9e5e708a03c3afe898eeedbe030e6bd0312993cd3731c2b5fc7c3af5acbe9c8b:0x3594ce01017ecd02a2d6fc8bec4b3f7a3e80dca7ff8a9d72657425a49ac8d97e:137", + "liquidityOccurrenceId": "1:0x9e5e708a03c3afe898eeedbe030e6bd0312993cd3731c2b5fc7c3af5acbe9c8b:0x3594ce01017ecd02a2d6fc8bec4b3f7a3e80dca7ff8a9d72657425a49ac8d97e:138", + "initialBuyOccurrenceId": "1:0x9e5e708a03c3afe898eeedbe030e6bd0312993cd3731c2b5fc7c3af5acbe9c8b:0x3594ce01017ecd02a2d6fc8bec4b3f7a3e80dca7ff8a9d72657425a49ac8d97e:139", + "custodyOccurrenceId": "1:0x9e5e708a03c3afe898eeedbe030e6bd0312993cd3731c2b5fc7c3af5acbe9c8b:0x3594ce01017ecd02a2d6fc8bec4b3f7a3e80dca7ff8a9d72657425a49ac8d97e:140", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647565" + }, + { + "id": "1:classic-v3:0x2a7f60f8c745d69c381d835ca03f55e57cbe2792ce760615532fea628ebe520c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x2a7f60f8c745d69c381d835ca03f55e57cbe2792ce760615532fea628ebe520c", + "token": "0x9160aa5cbfc50e4bc2aea5a1207cb222fee40921", + "creator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "quoteAsset": null, + "poolId": "0x1eca20b1110bab53431f64c13ca4070a7f1cfac98987c1ecad1b777dc178aa71", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1b6679c996f05b654c8ff4076369f6c51c7d3660", + "positionRecipient": "0xec2e118f5ecb4896fe370fa78e85dbdf91d6e5fe", + "positionTokenId": "355146", + "totalSwapFeeBps": null, + "buySwapFeeBps": 300, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x1e90508e4c5f08875c828c62969b90c32c524aa7584f3e859e842ba0a97ddf8f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "429127663717334283035658", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb025ed3f47798535ac4025970e75b2d6fafc0a32b4c0d9435621f47ab75bd8d0:0x90854dba3f333a1d25d59bbf22bd456ae207f5edff607913610755ddcab52e8d:399", + "liquidityOccurrenceId": "1:0xb025ed3f47798535ac4025970e75b2d6fafc0a32b4c0d9435621f47ab75bd8d0:0x90854dba3f333a1d25d59bbf22bd456ae207f5edff607913610755ddcab52e8d:400", + "initialBuyOccurrenceId": "1:0xb025ed3f47798535ac4025970e75b2d6fafc0a32b4c0d9435621f47ab75bd8d0:0x90854dba3f333a1d25d59bbf22bd456ae207f5edff607913610755ddcab52e8d:401", + "custodyOccurrenceId": "1:0xb025ed3f47798535ac4025970e75b2d6fafc0a32b4c0d9435621f47ab75bd8d0:0x90854dba3f333a1d25d59bbf22bd456ae207f5edff607913610755ddcab52e8d:402", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25643580" + }, + { + "id": "1:classic-v3:0x2bda1cf8955af3dc436790fa1a539cda7b7a4ae171c60ad192cfd36e5031230b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x2bda1cf8955af3dc436790fa1a539cda7b7a4ae171c60ad192cfd36e5031230b", + "token": "0x8c1a9cae5afb6c0c3e59127a57dabb50e9fa97cc", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0xc923201405c9aa26b55a69a79830ca6ae330265aa889509e2cc79a91080f8fb8", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x59824398d8bcb70e51633426094fccb16ca3af69", + "positionRecipient": "0x62491134accb24fa6d57ea20124727b11828441d", + "positionTokenId": "355985", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x0caef1aad1c3b7966751154ecf256d507ff0d5ecd4bb20860aef87be8800fd4b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x0d9b275822526d4b68afcf797f7c0ecaa50aa1c59daf30937391786783be77cd:0x39a2a23a8dcc338cc1d07896beb11d1b7fb08398a62bd4acda650c38d0cf80da:283", + "liquidityOccurrenceId": "1:0x0d9b275822526d4b68afcf797f7c0ecaa50aa1c59daf30937391786783be77cd:0x39a2a23a8dcc338cc1d07896beb11d1b7fb08398a62bd4acda650c38d0cf80da:284", + "initialBuyOccurrenceId": "1:0x0d9b275822526d4b68afcf797f7c0ecaa50aa1c59daf30937391786783be77cd:0x39a2a23a8dcc338cc1d07896beb11d1b7fb08398a62bd4acda650c38d0cf80da:285", + "custodyOccurrenceId": "1:0x0d9b275822526d4b68afcf797f7c0ecaa50aa1c59daf30937391786783be77cd:0x39a2a23a8dcc338cc1d07896beb11d1b7fb08398a62bd4acda650c38d0cf80da:286", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646320" + }, + { + "id": "1:classic-v3:0x2d9cc77e071ee4f31c9519091f79412e2c92617f5a1f2c9dc0551be753ed1e40", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x2d9cc77e071ee4f31c9519091f79412e2c92617f5a1f2c9dc0551be753ed1e40", + "token": "0x76264e35f523b687992399c155df33024b232801", + "creator": "0xc79891f64faf5aee3bce89a7b1514b0370b02814", + "quoteAsset": null, + "poolId": "0x608e1e4ef2f512fbf27af934251339c0f263307311c1d1e16547fe798a450889", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1904b94937f5866d6fa181b349f06f31531fa8ff", + "positionRecipient": "0xc9c354ac0317faf21fd893ba6bf6d2fbbc8cd3e5", + "positionTokenId": "356822", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x64e907407d1b921137159d25cdb541ab0c1043c56f1c42583b6f9f0f56994feb", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "729739899031511876349884", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb10c9ce8dcb483124cdd1d9fd0b6a69815edf3727faa082d696ea5e21af59525:0x0f0a2add84f6dd63e9b84519312394041d4de54f33282ef5a7bfff8f2d6c8085:481", + "liquidityOccurrenceId": "1:0xb10c9ce8dcb483124cdd1d9fd0b6a69815edf3727faa082d696ea5e21af59525:0x0f0a2add84f6dd63e9b84519312394041d4de54f33282ef5a7bfff8f2d6c8085:482", + "initialBuyOccurrenceId": "1:0xb10c9ce8dcb483124cdd1d9fd0b6a69815edf3727faa082d696ea5e21af59525:0x0f0a2add84f6dd63e9b84519312394041d4de54f33282ef5a7bfff8f2d6c8085:483", + "custodyOccurrenceId": "1:0xb10c9ce8dcb483124cdd1d9fd0b6a69815edf3727faa082d696ea5e21af59525:0x0f0a2add84f6dd63e9b84519312394041d4de54f33282ef5a7bfff8f2d6c8085:484", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25649786" + }, + { + "id": "1:classic-v3:0x2e079a871bef0d25c87e1cd2769cbc650197709dc555056a54e849520b96c525", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x2e079a871bef0d25c87e1cd2769cbc650197709dc555056a54e849520b96c525", + "token": "0xe781bf5e3433580332492dee668bea59c2616f3c", + "creator": "0xbe8af7e12b536ab55fbaf92edbb512972e0504da", + "quoteAsset": null, + "poolId": "0x4f8c88b02cc4639df4209f74102080cecc8b3a5675ad5d20294f4e2b3d2bd90b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd780c5723f659d07e6fe89bb456a8ab585189d8a", + "positionRecipient": "0x1a843e224e9b3d9460c7e41c0eb7e25a7ea8b749", + "positionTokenId": "354624", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x079ea030c33df7b9cd0c84d9cbf1b52d55a02d875a377d9efe91fd3903a69a5a", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x0359a38d0287cbe3a58a913ba36ac9bf38cde794a07290f31991378983810aef:0xc985f6fb38e4617d26468dfc2ce976f80154d435786a62378eb2593924983194:840", + "liquidityOccurrenceId": "1:0x0359a38d0287cbe3a58a913ba36ac9bf38cde794a07290f31991378983810aef:0xc985f6fb38e4617d26468dfc2ce976f80154d435786a62378eb2593924983194:841", + "initialBuyOccurrenceId": "1:0x0359a38d0287cbe3a58a913ba36ac9bf38cde794a07290f31991378983810aef:0xc985f6fb38e4617d26468dfc2ce976f80154d435786a62378eb2593924983194:842", + "custodyOccurrenceId": "1:0x0359a38d0287cbe3a58a913ba36ac9bf38cde794a07290f31991378983810aef:0xc985f6fb38e4617d26468dfc2ce976f80154d435786a62378eb2593924983194:843", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640421" + }, + { + "id": "1:classic-v3:0x2f3ff504eaa02f44b7d4873ecd686dfc44758a6a50f9db1e3885a684191ad05c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x2f3ff504eaa02f44b7d4873ecd686dfc44758a6a50f9db1e3885a684191ad05c", + "token": "0xa5b7456eae3649d23f87cf6e30612d44918d2e95", + "creator": "0x8424c4b7b7105b06a6a6b59d6f527f19b70e0320", + "quoteAsset": null, + "poolId": "0xa81153c4867bdb71ad53821de9d10feeb16f38da51f534885c9c8edb10159b1f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xee6923ca0ac08514bb51aa653c8eb2bb431f0313", + "positionRecipient": "0x5e6d99b717dc0f66f2177500a0ecf2ab15d7bc1b", + "positionTokenId": "355650", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x333f18dd3c8a0a1cfa6a52785259cc62a4dd3e05da242c04fc9e281780e13a27", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x87b6a15d418ef93305d03c07e7e98a9d4b391f52ec522efabddea6845d6fc916:0x4b51c6b112e53c2b25d75e0b5e4cf898273936ff5274b468ce5eb939ea45def2:459", + "liquidityOccurrenceId": "1:0x87b6a15d418ef93305d03c07e7e98a9d4b391f52ec522efabddea6845d6fc916:0x4b51c6b112e53c2b25d75e0b5e4cf898273936ff5274b468ce5eb939ea45def2:460", + "initialBuyOccurrenceId": "1:0x87b6a15d418ef93305d03c07e7e98a9d4b391f52ec522efabddea6845d6fc916:0x4b51c6b112e53c2b25d75e0b5e4cf898273936ff5274b468ce5eb939ea45def2:461", + "custodyOccurrenceId": "1:0x87b6a15d418ef93305d03c07e7e98a9d4b391f52ec522efabddea6845d6fc916:0x4b51c6b112e53c2b25d75e0b5e4cf898273936ff5274b468ce5eb939ea45def2:462", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645617" + }, + { + "id": "1:classic-v3:0x3068252bdc0db7d199df2a48a942cf263048958928e539083e07683cf0cb892b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3068252bdc0db7d199df2a48a942cf263048958928e539083e07683cf0cb892b", + "token": "0xf749e87b7964bc873e42288d34e8272c2a9db171", + "creator": "0x7afdb3171e878df8dc6467b16e6ca33e23653997", + "quoteAsset": null, + "poolId": "0x18937f61126be8c2a1f822bfcab42dfef3d8238cc6a12e23325919074de38065", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd9d0b3bc92b6f6fc5d34de69e96cfb6f9bd03937", + "positionRecipient": "0xc272d7710ca0f2810a7ac0f2c04b46f4592d7303", + "positionTokenId": "355668", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x60963d9c8119ef05bdf4de754964461f2cfb3121768f4246f8dcf243ea47ef72", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa8a044333d6a86321ba4ef069621955013ea89661628cc97671335450544bcac:0xfbd4ba9ee9d1115997956cc52972afc87373795958f149f1cf1915867a711b7b:336", + "liquidityOccurrenceId": "1:0xa8a044333d6a86321ba4ef069621955013ea89661628cc97671335450544bcac:0xfbd4ba9ee9d1115997956cc52972afc87373795958f149f1cf1915867a711b7b:337", + "initialBuyOccurrenceId": "1:0xa8a044333d6a86321ba4ef069621955013ea89661628cc97671335450544bcac:0xfbd4ba9ee9d1115997956cc52972afc87373795958f149f1cf1915867a711b7b:338", + "custodyOccurrenceId": "1:0xa8a044333d6a86321ba4ef069621955013ea89661628cc97671335450544bcac:0xfbd4ba9ee9d1115997956cc52972afc87373795958f149f1cf1915867a711b7b:339", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645654" + }, + { + "id": "1:classic-v3:0x320eeae1098e87004eabf129bb4438dd32eb53007f03edf1325c1bf55aa4f27e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x320eeae1098e87004eabf129bb4438dd32eb53007f03edf1325c1bf55aa4f27e", + "token": "0x17d29230697a803806bc18b38470ffdd2444d2ed", + "creator": "0x104db318d27fd8894b13f8de2154d79cac2ca46a", + "quoteAsset": null, + "poolId": "0xaa26b843d0c8f51b795815dd397f7ffcbd431044bf65a9f86472da5876864a07", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6233a10e285d4d2c3e74099a2ba41bb7b06c80f7", + "positionRecipient": "0xf2860c5c0954860f10d88203c286636890ce88b0", + "positionTokenId": "355710", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x4e6897b7ce31781f9367438adbbab1da621a3d2d6dc728e8f4442f0e6137a215", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0xa9b2c853e2b864214b8f6f2cb88f8480cb428089ff97101e42a0c07f2ba8a17c:934", + "liquidityOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0xa9b2c853e2b864214b8f6f2cb88f8480cb428089ff97101e42a0c07f2ba8a17c:935", + "initialBuyOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0xa9b2c853e2b864214b8f6f2cb88f8480cb428089ff97101e42a0c07f2ba8a17c:936", + "custodyOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0xa9b2c853e2b864214b8f6f2cb88f8480cb428089ff97101e42a0c07f2ba8a17c:937", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645718" + }, + { + "id": "1:classic-v3:0x32cd261076b44a627701eb27453a756c35ec123ea55d6eaa4ed9f3b89fdbf049", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x32cd261076b44a627701eb27453a756c35ec123ea55d6eaa4ed9f3b89fdbf049", + "token": "0xf56e0210586d98adbe90076bf65b0bbf3f103c31", + "creator": "0x5cb4a95b28a524e9260ceddd861bb1f2a73fc43a", + "quoteAsset": null, + "poolId": "0x533301aa33677fcc82f74e57ec57ed7e4432aec42d51b0386e4080e1d07ee79b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x461ee5390df03bfae8685fae50c8ed0941d95ebd", + "positionRecipient": "0xf339c4b002fad399be91a6f5585947dc18ddcf5c", + "positionTokenId": "357234", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x913eb799a93cf6de08f337075a8d2a78f4c1bf0a973ad7da2e298ef080effc5f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x2fa07b0fb8288e504d0ec4c69bb32bc3f35463cc1f9e83613d346796c703c1a8:0x269b6ae46f06fd6a4bb8dd0313362a3f4fb3262883a5532edd573e2a8223dd5d:547", + "liquidityOccurrenceId": "1:0x2fa07b0fb8288e504d0ec4c69bb32bc3f35463cc1f9e83613d346796c703c1a8:0x269b6ae46f06fd6a4bb8dd0313362a3f4fb3262883a5532edd573e2a8223dd5d:548", + "initialBuyOccurrenceId": "1:0x2fa07b0fb8288e504d0ec4c69bb32bc3f35463cc1f9e83613d346796c703c1a8:0x269b6ae46f06fd6a4bb8dd0313362a3f4fb3262883a5532edd573e2a8223dd5d:549", + "custodyOccurrenceId": "1:0x2fa07b0fb8288e504d0ec4c69bb32bc3f35463cc1f9e83613d346796c703c1a8:0x269b6ae46f06fd6a4bb8dd0313362a3f4fb3262883a5532edd573e2a8223dd5d:550", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25651809" + }, + { + "id": "1:classic-v3:0x349e3186d61f0faa1eb5abdf68a64ecc61fe686d7d0d4746e969dc3a856c2b1a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x349e3186d61f0faa1eb5abdf68a64ecc61fe686d7d0d4746e969dc3a856c2b1a", + "token": "0xf12f5db9a31a021e906b68efd12507463df3fa17", + "creator": "0x27f07ded1e5794fcc572feded8b232a5d31480d7", + "quoteAsset": null, + "poolId": "0x40bc3958629e60b2a3ea8b8b6ddfa7ce3edc884b393a1ba2b86be3d178600207", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6f5b42c90c98620678d737adf97b7fd05a1f6d52", + "positionRecipient": "0x17edf8baacf5454d57070d9bc86ca2d1c963a07a", + "positionTokenId": "354630", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xe61df14f2eee2035329bdc52f908282ec4896dada63f3c3a8b6696bff57738c1", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xfb43eb908be75e141ee838b79abd87459f475485cbb5692cf2184e749ccceb0b:0x085c2f53b09233419acca17287751cc4ab02a76348d64aebaa9efade979aeb3c:210", + "liquidityOccurrenceId": "1:0xfb43eb908be75e141ee838b79abd87459f475485cbb5692cf2184e749ccceb0b:0x085c2f53b09233419acca17287751cc4ab02a76348d64aebaa9efade979aeb3c:211", + "initialBuyOccurrenceId": "1:0xfb43eb908be75e141ee838b79abd87459f475485cbb5692cf2184e749ccceb0b:0x085c2f53b09233419acca17287751cc4ab02a76348d64aebaa9efade979aeb3c:212", + "custodyOccurrenceId": "1:0xfb43eb908be75e141ee838b79abd87459f475485cbb5692cf2184e749ccceb0b:0x085c2f53b09233419acca17287751cc4ab02a76348d64aebaa9efade979aeb3c:213", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640444" + }, + { + "id": "1:classic-v3:0x3799dce4e24ac4e2000bfccf620e29c0e02e698f631b57bcc3b8d067c653e510", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3799dce4e24ac4e2000bfccf620e29c0e02e698f631b57bcc3b8d067c653e510", + "token": "0x21af1922c3d1250b6174764c16eb2ec90f8c18b1", + "creator": "0x0e6576edb1a828acbca1670c16f9630611ad4ae6", + "quoteAsset": null, + "poolId": "0x1ef47f446995ac27fdf69eb388543bfc425bed64a6d1c5720dcc8fa6b6df7d70", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x140616654a8cb05f9402a1806a79786abc0593c9", + "positionRecipient": "0xbf11e86a309d491e1f5e92ad510603e1e3677cde", + "positionTokenId": "355578", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xeb4ede729c59cdee085e5d3ac6e6b0c5f645e2a3d9724d6a15b1e820be8ef796", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "42300000000000000", + "initialBuyTokenAmount": "29964907617407355334153800", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xfc123df9619f9040e1aa4872c18bf2bbe91a484d2dad85ac1e633c27ceb98066:0x61f7c5f4d0c92065449fea7c67e0718243059e49801a087f4d1ce2f2cf4b7c55:161", + "liquidityOccurrenceId": "1:0xfc123df9619f9040e1aa4872c18bf2bbe91a484d2dad85ac1e633c27ceb98066:0x61f7c5f4d0c92065449fea7c67e0718243059e49801a087f4d1ce2f2cf4b7c55:162", + "initialBuyOccurrenceId": "1:0xfc123df9619f9040e1aa4872c18bf2bbe91a484d2dad85ac1e633c27ceb98066:0x61f7c5f4d0c92065449fea7c67e0718243059e49801a087f4d1ce2f2cf4b7c55:163", + "custodyOccurrenceId": "1:0xfc123df9619f9040e1aa4872c18bf2bbe91a484d2dad85ac1e633c27ceb98066:0x61f7c5f4d0c92065449fea7c67e0718243059e49801a087f4d1ce2f2cf4b7c55:164", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645503" + }, + { + "id": "1:classic-v3:0x3845293a03634ac22119c72686d23a8df173cc744a4f7877c7ec7d76907d9cf0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3845293a03634ac22119c72686d23a8df173cc744a4f7877c7ec7d76907d9cf0", + "token": "0xacc13ac145e0c4cc5efc81676cdbd653cb41ce47", + "creator": "0x46fdf1633a42b792b362c2a2e247f606beda6b10", + "quoteAsset": null, + "poolId": "0x59bc50d61e80ebea379dd8517d037022ee51d5dbd6ddca9d1192bdc882f8077f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xfc16d5a49ba7c86b3bc5c8e381624316110a71d0", + "positionRecipient": "0x06ab224638df0ffb7c2a8e29b062d297ba5ab20f", + "positionTokenId": "355545", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xa6d56a838cf694f8697fb17a19af3c3aad3e992edaafba11ee91dcc510ef454e", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf727538aea19f9ca7ee0ca8e33f875cac965149b66403cfa16d257c36391ca94:0x4782604e70ea8662151cb0b926c52c13ba3e88cb57ef795b573b67e278fd14db:79", + "liquidityOccurrenceId": "1:0xf727538aea19f9ca7ee0ca8e33f875cac965149b66403cfa16d257c36391ca94:0x4782604e70ea8662151cb0b926c52c13ba3e88cb57ef795b573b67e278fd14db:80", + "initialBuyOccurrenceId": "1:0xf727538aea19f9ca7ee0ca8e33f875cac965149b66403cfa16d257c36391ca94:0x4782604e70ea8662151cb0b926c52c13ba3e88cb57ef795b573b67e278fd14db:81", + "custodyOccurrenceId": "1:0xf727538aea19f9ca7ee0ca8e33f875cac965149b66403cfa16d257c36391ca94:0x4782604e70ea8662151cb0b926c52c13ba3e88cb57ef795b573b67e278fd14db:82", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645465" + }, + { + "id": "1:classic-v3:0x39b06fa918dc374bdfcf2792fdc6fb07dc87bf36d622397f2fd6973ab975ac00", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x39b06fa918dc374bdfcf2792fdc6fb07dc87bf36d622397f2fd6973ab975ac00", + "token": "0x16ae7d33bc4077e1e346f351cec5cc24f5bfc816", + "creator": "0x4762b949c1341572ea4f68fc1157c917774504d5", + "quoteAsset": null, + "poolId": "0x2a684b161314ff1fb8fcd1737da41f6072f13cf4641870cadd545a42688d1c76", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x88b943735141c4cecfaffe7d8307525bc9ffa47f", + "positionRecipient": "0x85069e578073355e4b1c63cbf14c50c7fa0384c0", + "positionTokenId": "355540", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x4950a14488511b340bc2f2242c5558012ecc09f70952ddee79e7b077f176fea5", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc71a68e01a93e8febe1c9508ac9435babc090a4f306aca58241a3992044e8ba5:0x71a6aeb27d9633e327c65ece10cfe37bdee0b46e8a6b5cbeda7c38f84245d68d:799", + "liquidityOccurrenceId": "1:0xc71a68e01a93e8febe1c9508ac9435babc090a4f306aca58241a3992044e8ba5:0x71a6aeb27d9633e327c65ece10cfe37bdee0b46e8a6b5cbeda7c38f84245d68d:800", + "initialBuyOccurrenceId": "1:0xc71a68e01a93e8febe1c9508ac9435babc090a4f306aca58241a3992044e8ba5:0x71a6aeb27d9633e327c65ece10cfe37bdee0b46e8a6b5cbeda7c38f84245d68d:801", + "custodyOccurrenceId": "1:0xc71a68e01a93e8febe1c9508ac9435babc090a4f306aca58241a3992044e8ba5:0x71a6aeb27d9633e327c65ece10cfe37bdee0b46e8a6b5cbeda7c38f84245d68d:802", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645456" + }, + { + "id": "1:classic-v3:0x3ac0585a2a323185eda2064166926705021a8d4290c6c5919bffb28a3679633b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3ac0585a2a323185eda2064166926705021a8d4290c6c5919bffb28a3679633b", + "token": "0x00e7bf87408628b0477d3547ac8970523d9d576a", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x0ac87c3dea1fa9289e10fc573b3e5e675afd14dbafd47e6664e97d424646b99a", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7078699bd2ec47ca6307462276c61f161e76b2bf", + "positionRecipient": "0x5ccfdb7bd931b5b6ace4f1afe08e71014785684e", + "positionTokenId": "355888", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x729193fd9d2f75568b4b630910aeb575519e29b56a9f90a07fb23228a322c55e", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xca03df06347e3f6ae2f10b40a36334c5278a12f45fd9d938ef48400469be8bc4:0xb0dbb1e1294f718d15b80593a3a5c2c087eb6355afba048b2ec79c27d8ec0a33:339", + "liquidityOccurrenceId": "1:0xca03df06347e3f6ae2f10b40a36334c5278a12f45fd9d938ef48400469be8bc4:0xb0dbb1e1294f718d15b80593a3a5c2c087eb6355afba048b2ec79c27d8ec0a33:340", + "initialBuyOccurrenceId": "1:0xca03df06347e3f6ae2f10b40a36334c5278a12f45fd9d938ef48400469be8bc4:0xb0dbb1e1294f718d15b80593a3a5c2c087eb6355afba048b2ec79c27d8ec0a33:341", + "custodyOccurrenceId": "1:0xca03df06347e3f6ae2f10b40a36334c5278a12f45fd9d938ef48400469be8bc4:0xb0dbb1e1294f718d15b80593a3a5c2c087eb6355afba048b2ec79c27d8ec0a33:342", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646115" + }, + { + "id": "1:classic-v3:0x3af6c8ceed7264fe4a1459d5ac69495f777d0dc4c47ef30c9abb154fd0bf1b6f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3af6c8ceed7264fe4a1459d5ac69495f777d0dc4c47ef30c9abb154fd0bf1b6f", + "token": "0x1b17246bda9c1ac65ab6e414750fa01380523382", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0x29813e9a7f32725ea9cab0b0d822fbf9e8d74e630d36cfed0bffc86bfd10f67c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x44f07c3eac07efe5f14436f2573322f498aafb4b", + "positionRecipient": "0x0fded5cc4ae400328d0966cce028be1f4734cc70", + "positionTokenId": "355889", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xcd94765acae59e64b45852a3f25a7f892bdcfe196726d58586eb5f54f1e8032c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xd5557a562fa3016b0b3370d4d69ddc43e1870150625059aeccfd81a8048b2a58:0xf5e8d964da1ebd9f95900c3532e0f2349c19853d83ab4a31ba1de3a0d45f0471:310", + "liquidityOccurrenceId": "1:0xd5557a562fa3016b0b3370d4d69ddc43e1870150625059aeccfd81a8048b2a58:0xf5e8d964da1ebd9f95900c3532e0f2349c19853d83ab4a31ba1de3a0d45f0471:311", + "initialBuyOccurrenceId": "1:0xd5557a562fa3016b0b3370d4d69ddc43e1870150625059aeccfd81a8048b2a58:0xf5e8d964da1ebd9f95900c3532e0f2349c19853d83ab4a31ba1de3a0d45f0471:312", + "custodyOccurrenceId": "1:0xd5557a562fa3016b0b3370d4d69ddc43e1870150625059aeccfd81a8048b2a58:0xf5e8d964da1ebd9f95900c3532e0f2349c19853d83ab4a31ba1de3a0d45f0471:313", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646116" + }, + { + "id": "1:classic-v3:0x3cb6e1f60688709f2aa32cf763cfc7bf6b71becf0da7c37c68966ba2f88b09a3", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3cb6e1f60688709f2aa32cf763cfc7bf6b71becf0da7c37c68966ba2f88b09a3", + "token": "0xc9e9d3b9529d522ff38f3d7e111ca6291272b1ce", + "creator": "0x334350efbd1f1e5a30b137485096daee3025e21a", + "quoteAsset": null, + "poolId": "0xda7bed9ff0121bdfc5c8be7b7227fb7c9c9fb147c72abe64d6985418353ec2d3", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa4dc517c6e5495475bab876b5ad4512cbfabaad4", + "positionRecipient": "0x711493c9fab20b01abd4b89d33f4b113d40cc7d3", + "positionTokenId": "355827", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x266929246a74f499398ce4200e8dc8ccc7411504cb151695b5083d5b2cf2f5e8", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc4b5ca45772af59fdfbae545605a1db01d4bd7ba69fc7fe2aa64ce129f152a06:0x90b9413e5ca30df4695fab511427f4daaa4326ed0ba442eda185de3e681c2568:491", + "liquidityOccurrenceId": "1:0xc4b5ca45772af59fdfbae545605a1db01d4bd7ba69fc7fe2aa64ce129f152a06:0x90b9413e5ca30df4695fab511427f4daaa4326ed0ba442eda185de3e681c2568:492", + "initialBuyOccurrenceId": "1:0xc4b5ca45772af59fdfbae545605a1db01d4bd7ba69fc7fe2aa64ce129f152a06:0x90b9413e5ca30df4695fab511427f4daaa4326ed0ba442eda185de3e681c2568:493", + "custodyOccurrenceId": "1:0xc4b5ca45772af59fdfbae545605a1db01d4bd7ba69fc7fe2aa64ce129f152a06:0x90b9413e5ca30df4695fab511427f4daaa4326ed0ba442eda185de3e681c2568:494", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645978" + }, + { + "id": "1:classic-v3:0x3cd1e80298683b18866080d58e15ac806c50907be5cc0362ab1d1bd50d188746", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3cd1e80298683b18866080d58e15ac806c50907be5cc0362ab1d1bd50d188746", + "token": "0x22ce6ab9076a82fda4bfbb1fed211182e4548701", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x4aef041d188ba220691289b51a39bb290aa1a594dc15b8940a8d03a91d1d8366", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x8e5f3da16a25939faf16d67033cde9b6eb189f60", + "positionRecipient": "0x0e2f1b54a88b3ba3a6d7da20ab06f245052653aa", + "positionTokenId": "355886", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x6f0d35ccdb9b47ec4fe4bbb964880d9fa773baa4aa58bd909940fec6791d1d76", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x243bf261aca91e2f9d27235663daa9cc0c1ca4124eefa4d34e88ca076f057ef3:0xf09715d8c68d620b03eedc355862e081ac2f365ab6aecf2aa78e14ccbc1aa527:202", + "liquidityOccurrenceId": "1:0x243bf261aca91e2f9d27235663daa9cc0c1ca4124eefa4d34e88ca076f057ef3:0xf09715d8c68d620b03eedc355862e081ac2f365ab6aecf2aa78e14ccbc1aa527:203", + "initialBuyOccurrenceId": "1:0x243bf261aca91e2f9d27235663daa9cc0c1ca4124eefa4d34e88ca076f057ef3:0xf09715d8c68d620b03eedc355862e081ac2f365ab6aecf2aa78e14ccbc1aa527:204", + "custodyOccurrenceId": "1:0x243bf261aca91e2f9d27235663daa9cc0c1ca4124eefa4d34e88ca076f057ef3:0xf09715d8c68d620b03eedc355862e081ac2f365ab6aecf2aa78e14ccbc1aa527:205", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646108" + }, + { + "id": "1:classic-v3:0x3dbe92e80abd193ffc1f1e6aada581466946a8270b0eea7f0f53f43483d7c664", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3dbe92e80abd193ffc1f1e6aada581466946a8270b0eea7f0f53f43483d7c664", + "token": "0xe46525ef19454ff99b1fb93f8ada9bfff00f4d6d", + "creator": "0x6cab818daaf4040476e2489a7ed29ca685ad0623", + "quoteAsset": null, + "poolId": "0x828143dadf01aeb1be56bf85fb4dffac49e1ef0a1216f08c46aa530611c8f6b4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x8ef4769650c9005d0c12d64ec17039b8d89c1ee7", + "positionRecipient": "0x281f9998a6c1a0ec92edc5d118e55c092b1f5f59", + "positionTokenId": "355956", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x3a9ec6652c2a4d71c08ff8cf8d35732c4b851c7be11e1b70a03a55e9573e6c80", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x3c48c474edae7ec927404a5c6882771c6e95c1e21bdcf966172ddfce5e860ed9:0x40f3ceb3fedb05e56f4fc6d61039e65e4ec4480c891566e01d439805d62435bf:374", + "liquidityOccurrenceId": "1:0x3c48c474edae7ec927404a5c6882771c6e95c1e21bdcf966172ddfce5e860ed9:0x40f3ceb3fedb05e56f4fc6d61039e65e4ec4480c891566e01d439805d62435bf:375", + "initialBuyOccurrenceId": "1:0x3c48c474edae7ec927404a5c6882771c6e95c1e21bdcf966172ddfce5e860ed9:0x40f3ceb3fedb05e56f4fc6d61039e65e4ec4480c891566e01d439805d62435bf:376", + "custodyOccurrenceId": "1:0x3c48c474edae7ec927404a5c6882771c6e95c1e21bdcf966172ddfce5e860ed9:0x40f3ceb3fedb05e56f4fc6d61039e65e4ec4480c891566e01d439805d62435bf:377", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646263" + }, + { + "id": "1:classic-v3:0x3fa0d0a7c0ab34f466a81fae1b5a90361ca0c95a6b55c4c80822a9ba8397d8f0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x3fa0d0a7c0ab34f466a81fae1b5a90361ca0c95a6b55c4c80822a9ba8397d8f0", + "token": "0xdc80d8bc171038a9d48f4d9f551a557c027eb119", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0xed05ea8bb92b6d6e0523b7e73b32fc087a1aa0e0a271c135bfd5d10fe02aac0b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xfe175b01727f63a451f9f257c9d9a5951c8feee5", + "positionRecipient": "0x84987ac2b118d795e37e1d4b2ef6cde20baebd47", + "positionTokenId": "355868", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x5dfc0e02d0f29a697e3be43f3519582f175be3726fbe3d7a79894b62d181cf2a", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe4c2da23ded514db99512f6559df5a5aecb5efe129cf79daf825ade5e305bffe:0x7fca268501cb3c1ccd046726ced7e52beeec5107883971f8e62c7c7b979cce3a:195", + "liquidityOccurrenceId": "1:0xe4c2da23ded514db99512f6559df5a5aecb5efe129cf79daf825ade5e305bffe:0x7fca268501cb3c1ccd046726ced7e52beeec5107883971f8e62c7c7b979cce3a:196", + "initialBuyOccurrenceId": "1:0xe4c2da23ded514db99512f6559df5a5aecb5efe129cf79daf825ade5e305bffe:0x7fca268501cb3c1ccd046726ced7e52beeec5107883971f8e62c7c7b979cce3a:197", + "custodyOccurrenceId": "1:0xe4c2da23ded514db99512f6559df5a5aecb5efe129cf79daf825ade5e305bffe:0x7fca268501cb3c1ccd046726ced7e52beeec5107883971f8e62c7c7b979cce3a:198", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646061" + }, + { + "id": "1:classic-v3:0x405587a84ede2300ff6c56548bb59b8152a4b2ddf394b16e1f9278ba2f2caa65", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x405587a84ede2300ff6c56548bb59b8152a4b2ddf394b16e1f9278ba2f2caa65", + "token": "0x78289a10d07ae46194929e828125e80dcc0097d6", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x51028574675407cf55dc822aff4e3a106e32aab2973939e0b3f368035a6e6264", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x29dffaa030795497ced9ca1e76e1ed107470dcb0", + "positionRecipient": "0xe030247919cecf5527e7a4323e390d422ec03184", + "positionTokenId": "355882", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xfc34ed83f7431cc97686e33caebafdc15594a29b24053ffcf9cad9ad949446b0", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x119a2d646ecd9d8526763b9f77f6f877dad67138ba2551ffdfb62869c9a81353:0x985871e11c950be09ec7e9c26b6977b3ac00fe809a1a5a0c74369aeaf22d5275:42", + "liquidityOccurrenceId": "1:0x119a2d646ecd9d8526763b9f77f6f877dad67138ba2551ffdfb62869c9a81353:0x985871e11c950be09ec7e9c26b6977b3ac00fe809a1a5a0c74369aeaf22d5275:43", + "initialBuyOccurrenceId": "1:0x119a2d646ecd9d8526763b9f77f6f877dad67138ba2551ffdfb62869c9a81353:0x985871e11c950be09ec7e9c26b6977b3ac00fe809a1a5a0c74369aeaf22d5275:44", + "custodyOccurrenceId": "1:0x119a2d646ecd9d8526763b9f77f6f877dad67138ba2551ffdfb62869c9a81353:0x985871e11c950be09ec7e9c26b6977b3ac00fe809a1a5a0c74369aeaf22d5275:45", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646096" + }, + { + "id": "1:classic-v3:0x432fb91d1e0dae75e0906f01530311932a74a6c0378df614586804aaa479e497", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x432fb91d1e0dae75e0906f01530311932a74a6c0378df614586804aaa479e497", + "token": "0xe26d93552abedfaf6063609e00c4442e97b49f8a", + "creator": "0xfa50de214e13302e01bdf635d5204ff26b84ea60", + "quoteAsset": null, + "poolId": "0x63eaec312c53e32b9f16707e5c3496048cf8d589e28c953d8cc819cc110fa0c4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb568b36870a17be285f851a4554d72309cbfaee4", + "positionRecipient": "0xef0e3a60d45c104b886321a75dfdb5faf261307f", + "positionTokenId": "356520", + "totalSwapFeeBps": null, + "buySwapFeeBps": 500, + "sellSwapFeeBps": 500, + "rewardConfigurationHash": "0x2e28e60110eab84393e49d1c8aed8862e991e445d78be953500161f081a57bea", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "420283389315512053870821", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x365a95ac8c39c761408627a882e888c390a58de03c175e92e41761cddb1fdb58:0x5f5486249edde1588b1a7ed0c14493c6437e0b6691862dd9d625bb9b71cb5b62:830", + "liquidityOccurrenceId": "1:0x365a95ac8c39c761408627a882e888c390a58de03c175e92e41761cddb1fdb58:0x5f5486249edde1588b1a7ed0c14493c6437e0b6691862dd9d625bb9b71cb5b62:831", + "initialBuyOccurrenceId": "1:0x365a95ac8c39c761408627a882e888c390a58de03c175e92e41761cddb1fdb58:0x5f5486249edde1588b1a7ed0c14493c6437e0b6691862dd9d625bb9b71cb5b62:832", + "custodyOccurrenceId": "1:0x365a95ac8c39c761408627a882e888c390a58de03c175e92e41761cddb1fdb58:0x5f5486249edde1588b1a7ed0c14493c6437e0b6691862dd9d625bb9b71cb5b62:833", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25648359" + }, + { + "id": "1:classic-v3:0x4359ec03a185280d07c874927112c947da3b510e84d74f57571a8f5f6bfed9c5", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4359ec03a185280d07c874927112c947da3b510e84d74f57571a8f5f6bfed9c5", + "token": "0xc0120e7f180ad4104cecf1edd69fc0ec958910a6", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x2d233b89f32c7ef4d67e7620ffb1c1dfface38beccc91dc7cf5eaf372ecde7d5", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x883f4f8e50fdb9c95616d7ee7a7042de3e7e2497", + "positionRecipient": "0x9395e992490482ba1672d0a23005254a140452f2", + "positionTokenId": "355781", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xa5c7070811505c3cd3637f1d0471c2a1fe312d1d066442f1d4d460f25c9dd78f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xee2ab5dad4c5151b1be532bc82d675127eb78dd828704d88089b30b7975b75b4:0x822e6855188e649d040b8db1d57ab2b376236e75f2a18ce11b784432992b5fb1:199", + "liquidityOccurrenceId": "1:0xee2ab5dad4c5151b1be532bc82d675127eb78dd828704d88089b30b7975b75b4:0x822e6855188e649d040b8db1d57ab2b376236e75f2a18ce11b784432992b5fb1:200", + "initialBuyOccurrenceId": "1:0xee2ab5dad4c5151b1be532bc82d675127eb78dd828704d88089b30b7975b75b4:0x822e6855188e649d040b8db1d57ab2b376236e75f2a18ce11b784432992b5fb1:201", + "custodyOccurrenceId": "1:0xee2ab5dad4c5151b1be532bc82d675127eb78dd828704d88089b30b7975b75b4:0x822e6855188e649d040b8db1d57ab2b376236e75f2a18ce11b784432992b5fb1:202", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645899" + }, + { + "id": "1:classic-v3:0x43f05d2614b05102e26ede41431e22c237caa808600795672f68a81a2d016a8e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x43f05d2614b05102e26ede41431e22c237caa808600795672f68a81a2d016a8e", + "token": "0x65a2365e89b596d4b4c17153a75d700cd5f0ee04", + "creator": "0x75d4830a424836b8dc9c95e4fb7357109b653eb8", + "quoteAsset": null, + "poolId": "0xed916458e5ddbdd1646f3a29e5f82a82849946183db98f5351389b01967a69ae", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x323f8c24f45953c76fee0069d98dda87ca72b9bd", + "positionRecipient": "0x354de4f2aa9714e7e5f61210c7a6bede437b274a", + "positionTokenId": "355742", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x5a61ebed70fa73cfbe049bddfecb7c550a25c0e2c74fb63d7f358a664ae5080b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x351fe959812f82697b96fe8a478c377043c8eb1c99b0496b54789d533fb20c52:0xad031c2a89c2741632f419d96ebc88a20aaf668ac828d300c24b84d53ce122c7:457", + "liquidityOccurrenceId": "1:0x351fe959812f82697b96fe8a478c377043c8eb1c99b0496b54789d533fb20c52:0xad031c2a89c2741632f419d96ebc88a20aaf668ac828d300c24b84d53ce122c7:458", + "initialBuyOccurrenceId": "1:0x351fe959812f82697b96fe8a478c377043c8eb1c99b0496b54789d533fb20c52:0xad031c2a89c2741632f419d96ebc88a20aaf668ac828d300c24b84d53ce122c7:459", + "custodyOccurrenceId": "1:0x351fe959812f82697b96fe8a478c377043c8eb1c99b0496b54789d533fb20c52:0xad031c2a89c2741632f419d96ebc88a20aaf668ac828d300c24b84d53ce122c7:460", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645785" + }, + { + "id": "1:classic-v3:0x4424be4cee56938ef1429ec7a698f9ff81b44b22c3f34b4e82ae664144c9e73d", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4424be4cee56938ef1429ec7a698f9ff81b44b22c3f34b4e82ae664144c9e73d", + "token": "0x0fb86acea7ee712b2e1cf40ab00630c89df56d3c", + "creator": "0xca5dc6ed0cab4a1c8c65c34a4aaa6fedc64355bb", + "quoteAsset": null, + "poolId": "0x628f0c4abcc9090061820dad69869e7a7c5c06965878e23701da7646f0483cea", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1180e7da7260e86a01abf5b2b5475fd3267d7ef5", + "positionRecipient": "0x76fa2d117c40735b4e961a9de314fe3059a17e26", + "positionTokenId": "355557", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xf09d8076e7fa19928af1cdd8007feafc6368d61b0536258505363e13ab8bf64c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x16b44e495fe794094e53c6eae20f3b739f259d57c820bcfbfb3240a3cf3b3866:0x8e33cc19cc9d4eaa02c2fb3699c09f364b7fcb2a6cf256d357edb87e7278820a:761", + "liquidityOccurrenceId": "1:0x16b44e495fe794094e53c6eae20f3b739f259d57c820bcfbfb3240a3cf3b3866:0x8e33cc19cc9d4eaa02c2fb3699c09f364b7fcb2a6cf256d357edb87e7278820a:762", + "initialBuyOccurrenceId": "1:0x16b44e495fe794094e53c6eae20f3b739f259d57c820bcfbfb3240a3cf3b3866:0x8e33cc19cc9d4eaa02c2fb3699c09f364b7fcb2a6cf256d357edb87e7278820a:763", + "custodyOccurrenceId": "1:0x16b44e495fe794094e53c6eae20f3b739f259d57c820bcfbfb3240a3cf3b3866:0x8e33cc19cc9d4eaa02c2fb3699c09f364b7fcb2a6cf256d357edb87e7278820a:764", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645480" + }, + { + "id": "1:classic-v3:0x4578f61acfb1e812309278b2b21fee8973e7ebbf4b4b5e174df77e5ee601b325", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4578f61acfb1e812309278b2b21fee8973e7ebbf4b4b5e174df77e5ee601b325", + "token": "0x70ecb7006e99b829517b324d471cffc7aa2af574", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0xb4a9dd5c40cfb55b6faa9b0f368ed06c4de42f6516192237e64f78c9290acd6e", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x898bb81d8f1b8cb6323228efdca284e1acf12a3b", + "positionRecipient": "0xc7c6e8f298a01903da49f052728347a3176f1612", + "positionTokenId": "355738", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x683ce6718676f96e1ea5e299fba2063467fc34f6c5ad49e8f2f4e9db55e0ef01", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x3b2ef30cfe88cd696a47cf468c70ae1be1fc8047dd343c278ebb9ddf9ea4e862:0x1f9b1cefe78ada1ee447d3664c36824f8a9d0520605f4c279195e91a2dad6c81:150", + "liquidityOccurrenceId": "1:0x3b2ef30cfe88cd696a47cf468c70ae1be1fc8047dd343c278ebb9ddf9ea4e862:0x1f9b1cefe78ada1ee447d3664c36824f8a9d0520605f4c279195e91a2dad6c81:151", + "initialBuyOccurrenceId": "1:0x3b2ef30cfe88cd696a47cf468c70ae1be1fc8047dd343c278ebb9ddf9ea4e862:0x1f9b1cefe78ada1ee447d3664c36824f8a9d0520605f4c279195e91a2dad6c81:152", + "custodyOccurrenceId": "1:0x3b2ef30cfe88cd696a47cf468c70ae1be1fc8047dd343c278ebb9ddf9ea4e862:0x1f9b1cefe78ada1ee447d3664c36824f8a9d0520605f4c279195e91a2dad6c81:153", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645775" + }, + { + "id": "1:classic-v3:0x4768e43f408d7662294077a8bb2ef0117fac46fd87da1a12bb5bc00d0a00306b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4768e43f408d7662294077a8bb2ef0117fac46fd87da1a12bb5bc00d0a00306b", + "token": "0xd0f4d0f69808d28e9fac26a7a887e7484ce13a42", + "creator": "0xc09bcfd8e654681d3d8bc5ef6b8cb00cb5363fb0", + "quoteAsset": null, + "poolId": "0xb1228f96185bb81285650ab6546a211e3bd219eccd534a643d46917ffeaa6cf9", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x2ec323ed0c21a4f9a2642357e3a82d554dd114f2", + "positionRecipient": "0x93ab99e29c9eae2f61bbfd754673bfdefe7e3753", + "positionTokenId": "355678", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9cd143889860aae368916ff78ae08f2c3aeb616b587890d9a83d4e6344c31641", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "30000000000000000", + "initialBuyTokenAmount": "21438505518229829458161070", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xfee8d4dd6040242f0a789fdd2108909c4a7d9647e70f4b7b7e84ab1771b5e129:0xd017d95edb4097520e40b486843e3403bbcd2399bc29062724448848820bc29e:829", + "liquidityOccurrenceId": "1:0xfee8d4dd6040242f0a789fdd2108909c4a7d9647e70f4b7b7e84ab1771b5e129:0xd017d95edb4097520e40b486843e3403bbcd2399bc29062724448848820bc29e:830", + "initialBuyOccurrenceId": "1:0xfee8d4dd6040242f0a789fdd2108909c4a7d9647e70f4b7b7e84ab1771b5e129:0xd017d95edb4097520e40b486843e3403bbcd2399bc29062724448848820bc29e:831", + "custodyOccurrenceId": "1:0xfee8d4dd6040242f0a789fdd2108909c4a7d9647e70f4b7b7e84ab1771b5e129:0xd017d95edb4097520e40b486843e3403bbcd2399bc29062724448848820bc29e:832", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645674" + }, + { + "id": "1:classic-v3:0x48d6e12b26d0044269c5affd6fa1523153f7a08bf58633336d7432deffb0c830", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x48d6e12b26d0044269c5affd6fa1523153f7a08bf58633336d7432deffb0c830", + "token": "0x1f0f23bb9cd69fc8e7ce6bfe2499d815e5ab1b1e", + "creator": "0x6cab818daaf4040476e2489a7ed29ca685ad0623", + "quoteAsset": null, + "poolId": "0xb235ea0c84bdbf8f67dcfe4cbbab263ae487cb3a4a8c5409560e11ef9e394341", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7fac61280c532ce8981abf95edd956ab9f830fed", + "positionRecipient": "0x3620210f6c9d78d6d8fb5b55df446a5b2f6268e9", + "positionTokenId": "355925", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x61d260a14047cb088b4f09b7cd16997f820856f25010687971abf4870b6a7535", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xdc774d1459aedae2b2cd57ac1b6ccc258df37f3ebfb54244828d5c5cf4b9c51a:0x92a1c606b25be0ea3adbefc34c428d64696d3c4aced49715087d4319f295342e:300", + "liquidityOccurrenceId": "1:0xdc774d1459aedae2b2cd57ac1b6ccc258df37f3ebfb54244828d5c5cf4b9c51a:0x92a1c606b25be0ea3adbefc34c428d64696d3c4aced49715087d4319f295342e:301", + "initialBuyOccurrenceId": "1:0xdc774d1459aedae2b2cd57ac1b6ccc258df37f3ebfb54244828d5c5cf4b9c51a:0x92a1c606b25be0ea3adbefc34c428d64696d3c4aced49715087d4319f295342e:302", + "custodyOccurrenceId": "1:0xdc774d1459aedae2b2cd57ac1b6ccc258df37f3ebfb54244828d5c5cf4b9c51a:0x92a1c606b25be0ea3adbefc34c428d64696d3c4aced49715087d4319f295342e:303", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646180" + }, + { + "id": "1:classic-v3:0x4a53a8d47e1e10e3ffacde4d30ebc4be84d48fb9406a12327ee07226e32b9511", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4a53a8d47e1e10e3ffacde4d30ebc4be84d48fb9406a12327ee07226e32b9511", + "token": "0x355cbec654f308badca305a319938d3a23c236dc", + "creator": "0x5cb4a95b28a524e9260ceddd861bb1f2a73fc43a", + "quoteAsset": null, + "poolId": "0xe04d86b4998dbfaac28eeabaf1628fc7a84f3c61d410754e57da0e47dd786408", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xf1eaef5117da10864db8fc00f5ab7f1a6dfaf672", + "positionRecipient": "0x4620390cfbd1493513f051d2b4015cc596909001", + "positionTokenId": "356260", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xd43ff7d2f23611ce8dc2205a4810fd326297c8c7d5c09e3c46099730f4035651", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x00535bf4912c87ad26923d74491046a899e2724d27782f7ea9c063ed999a1cff:0xe642aec68b74ff97f3ee55a903c0923e7f2d09217e6913578106a3b787e40028:480", + "liquidityOccurrenceId": "1:0x00535bf4912c87ad26923d74491046a899e2724d27782f7ea9c063ed999a1cff:0xe642aec68b74ff97f3ee55a903c0923e7f2d09217e6913578106a3b787e40028:481", + "initialBuyOccurrenceId": "1:0x00535bf4912c87ad26923d74491046a899e2724d27782f7ea9c063ed999a1cff:0xe642aec68b74ff97f3ee55a903c0923e7f2d09217e6913578106a3b787e40028:482", + "custodyOccurrenceId": "1:0x00535bf4912c87ad26923d74491046a899e2724d27782f7ea9c063ed999a1cff:0xe642aec68b74ff97f3ee55a903c0923e7f2d09217e6913578106a3b787e40028:483", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647478" + }, + { + "id": "1:classic-v3:0x4b1a8b8846afbd054d37c121e603222ebffc21b3cf0c7d27d073d2e60b1760ec", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4b1a8b8846afbd054d37c121e603222ebffc21b3cf0c7d27d073d2e60b1760ec", + "token": "0xc477afe67d1f7f89a1ea4e099ca224a574db3dee", + "creator": "0x3224bc16e533a65f96f32c7c66250e526d3cd4f4", + "quoteAsset": null, + "poolId": "0x0b39b1afb408fd8166d080302f899784e6c82a5e48bb727d47696b0f625172a3", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa55dd5c92f363c2d1afec45293bf85839068a175", + "positionRecipient": "0xb561ac78358cc27de3a163a502e46ea833a25402", + "positionTokenId": "354921", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x6c65ebe41599767a0a1c0f87a0bbdde5e3b191da154390a9185353d7071e1da6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "28000000000000000", + "initialBuyTokenAmount": "20037910674252090817728455", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa05a852367ed5e7eece3b616ed56029557e8873788650ca371c1343581113df0:0x484c6df3740348d8b758fbebdb3560f18885651bc068b9f17e1db31787d56b1d:650", + "liquidityOccurrenceId": "1:0xa05a852367ed5e7eece3b616ed56029557e8873788650ca371c1343581113df0:0x484c6df3740348d8b758fbebdb3560f18885651bc068b9f17e1db31787d56b1d:651", + "initialBuyOccurrenceId": "1:0xa05a852367ed5e7eece3b616ed56029557e8873788650ca371c1343581113df0:0x484c6df3740348d8b758fbebdb3560f18885651bc068b9f17e1db31787d56b1d:652", + "custodyOccurrenceId": "1:0xa05a852367ed5e7eece3b616ed56029557e8873788650ca371c1343581113df0:0x484c6df3740348d8b758fbebdb3560f18885651bc068b9f17e1db31787d56b1d:653", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25642338" + }, + { + "id": "1:classic-v3:0x4bfa23ac4dc79372c18be756e3ab95c71e8e2cb1aa8ed0e2ddef261fdf251f41", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4bfa23ac4dc79372c18be756e3ab95c71e8e2cb1aa8ed0e2ddef261fdf251f41", + "token": "0x37c6afa56c7cd01b45f8a7472e5bf3fb248da64f", + "creator": "0x60bc5323c0e0e9144af0b35ce8de119fc743e345", + "quoteAsset": null, + "poolId": "0xb9b966c27ea6d56edde3f3479e8c336afda7e2030d9a9cbe8c71908f00f2074b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xe23925ee6c08e83b30ac2f662c493806d9ae10a6", + "positionRecipient": "0x7400c0a0a141120cf85789020395fe1c78e65803", + "positionTokenId": "355769", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x3203821b34fd08ab268d461315e7b1edbe54874f57c00d33a1ff0cb71d7443de", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7177080303191202649091171", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7acc5ce5f18d8a5ca063c87a9fc0ced76dae63298f6f8448b2181e1120ee3bd4:0xcd986a869c7fc77088e9004cac6e4f2b247d0fa107309d01e3b58528f59795ef:233", + "liquidityOccurrenceId": "1:0x7acc5ce5f18d8a5ca063c87a9fc0ced76dae63298f6f8448b2181e1120ee3bd4:0xcd986a869c7fc77088e9004cac6e4f2b247d0fa107309d01e3b58528f59795ef:234", + "initialBuyOccurrenceId": "1:0x7acc5ce5f18d8a5ca063c87a9fc0ced76dae63298f6f8448b2181e1120ee3bd4:0xcd986a869c7fc77088e9004cac6e4f2b247d0fa107309d01e3b58528f59795ef:235", + "custodyOccurrenceId": "1:0x7acc5ce5f18d8a5ca063c87a9fc0ced76dae63298f6f8448b2181e1120ee3bd4:0xcd986a869c7fc77088e9004cac6e4f2b247d0fa107309d01e3b58528f59795ef:236", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645870" + }, + { + "id": "1:classic-v3:0x4c2c04f0f4d0c48cb0ee3d1c130bb13b0d241590ce3f44dde7a9177a128be4cd", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4c2c04f0f4d0c48cb0ee3d1c130bb13b0d241590ce3f44dde7a9177a128be4cd", + "token": "0x8c35f593c0d40fed36756570f4ea9b633528756d", + "creator": "0xca5dc6ed0cab4a1c8c65c34a4aaa6fedc64355bb", + "quoteAsset": null, + "poolId": "0x7bfaecffb2034884372814ad7e2a82b4ebce97af74727d8454dd4720d02d5c7c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xbc3e146d8c1695f26fb3db0e5a3d51504105b3ec", + "positionRecipient": "0x7e474ee87bca5d313b03d7fb973bb66cca5613a6", + "positionTokenId": "355599", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x098a46fa2054676fdb2b37ce6631a9d021263356f390e62c0dfc89bcd464d45f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x88843d948ba3e511b5ada83343ff0ba8682da01e956162f1e8ca4af85cad627a:0x93e981c51f4776abadc7461f8f9edd82ac2ef3d3db019e17474914a4bb3e0f15:411", + "liquidityOccurrenceId": "1:0x88843d948ba3e511b5ada83343ff0ba8682da01e956162f1e8ca4af85cad627a:0x93e981c51f4776abadc7461f8f9edd82ac2ef3d3db019e17474914a4bb3e0f15:412", + "initialBuyOccurrenceId": "1:0x88843d948ba3e511b5ada83343ff0ba8682da01e956162f1e8ca4af85cad627a:0x93e981c51f4776abadc7461f8f9edd82ac2ef3d3db019e17474914a4bb3e0f15:413", + "custodyOccurrenceId": "1:0x88843d948ba3e511b5ada83343ff0ba8682da01e956162f1e8ca4af85cad627a:0x93e981c51f4776abadc7461f8f9edd82ac2ef3d3db019e17474914a4bb3e0f15:414", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645540" + }, + { + "id": "1:classic-v3:0x4c45d4980126b498cb55cb83737ba65098430737d42804413950d88d9d54631c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x4c45d4980126b498cb55cb83737ba65098430737d42804413950d88d9d54631c", + "token": "0xf6e900b4051c14d48e56d801c8b41759d8ce50dc", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x970d6d6ec0327dd2b4b2a1b701065cd5e0fe33bbb23f79c21a2be4c40f81e334", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6f83cbd4a5f27c3631e1c2ebfd2c248c6201ed28", + "positionRecipient": "0x99d37e5a5d051c07204abf2b134d53cb56f1617f", + "positionTokenId": "355777", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x736a3ec8b0a6d626ca66fc1c85785d445b94aeacaaa6d34aeb72f6941b21340c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x6ae8ec28f62d6a67a3bd7a9cf4f4ff8eface9e7ee9d03f3813f7313e28bd1ce8:0x8b1264ed10feb206dac0a960d75342207956577d8c5875e000971e0ee7a9d5b6:561", + "liquidityOccurrenceId": "1:0x6ae8ec28f62d6a67a3bd7a9cf4f4ff8eface9e7ee9d03f3813f7313e28bd1ce8:0x8b1264ed10feb206dac0a960d75342207956577d8c5875e000971e0ee7a9d5b6:562", + "initialBuyOccurrenceId": "1:0x6ae8ec28f62d6a67a3bd7a9cf4f4ff8eface9e7ee9d03f3813f7313e28bd1ce8:0x8b1264ed10feb206dac0a960d75342207956577d8c5875e000971e0ee7a9d5b6:563", + "custodyOccurrenceId": "1:0x6ae8ec28f62d6a67a3bd7a9cf4f4ff8eface9e7ee9d03f3813f7313e28bd1ce8:0x8b1264ed10feb206dac0a960d75342207956577d8c5875e000971e0ee7a9d5b6:564", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645890" + }, + { + "id": "1:classic-v3:0x506f7e60102b5b689f94cb00618ae44ebee94947d8c874305e3750d7ad5ac16f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x506f7e60102b5b689f94cb00618ae44ebee94947d8c874305e3750d7ad5ac16f", + "token": "0xfb07132be7ec77c59a19e0c31f1ebd03ff8ba371", + "creator": "0x4b165b124c10b0276d56148663036bf6f32d83f3", + "quoteAsset": null, + "poolId": "0xcff665e3663936a51be4a0c88468e536d2c9130feb4296fc4ccee4ab2acea3fc", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6dadb46beef260160b2fb7ce88ade4bfc5b39e3d", + "positionRecipient": "0x392b1ada4cceaedbd17cb24a05fcf9a80802580e", + "positionTokenId": "357048", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xbcff5c18f59db240f3eac5d080f0820c9a1c62a3f14557b38c8d55a4ef8fb9b8", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x88492608a344e16328f955a8a1441733affec22591cc6d57d861bfb79c5924e4:0xce05d5d2aff8fcf9d66e8874b4db53ee24551e7216679ad2b48f6462c27eb537:759", + "liquidityOccurrenceId": "1:0x88492608a344e16328f955a8a1441733affec22591cc6d57d861bfb79c5924e4:0xce05d5d2aff8fcf9d66e8874b4db53ee24551e7216679ad2b48f6462c27eb537:760", + "initialBuyOccurrenceId": "1:0x88492608a344e16328f955a8a1441733affec22591cc6d57d861bfb79c5924e4:0xce05d5d2aff8fcf9d66e8874b4db53ee24551e7216679ad2b48f6462c27eb537:761", + "custodyOccurrenceId": "1:0x88492608a344e16328f955a8a1441733affec22591cc6d57d861bfb79c5924e4:0xce05d5d2aff8fcf9d66e8874b4db53ee24551e7216679ad2b48f6462c27eb537:762", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25650941" + }, + { + "id": "1:classic-v3:0x5510df3e077dfb94f4bda396530c10eee73359d8ce1b2c3b8b388de999881fe4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5510df3e077dfb94f4bda396530c10eee73359d8ce1b2c3b8b388de999881fe4", + "token": "0xc4f5833a7d575cd4d52bb01de0ec0151fc6a5e00", + "creator": "0x8376eb17ee9dfb388da0cc9c026b2d5882dbefa4", + "quoteAsset": null, + "poolId": "0x9e03b822df2b46442e24dc05d518ddb42a621c32ec07d0b065102aac921ea5c0", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x5cf269386328b1233220efc9d814720cf84f2053", + "positionRecipient": "0x54d85a2058512afa7810da7971938d4712798bf8", + "positionTokenId": "354654", + "totalSwapFeeBps": null, + "buySwapFeeBps": 300, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x91fe597ae254f85db4012fde217a9a2c2737119626c59e7a2e423a1f396dfd4c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "2000000000000000", + "initialBuyTokenAmount": "1428994696361581360689556", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x17437ec4756ff7ddc253317e97fbd9fc15f53da7d1bcf053a642086000c6503f:0x91b51cc37c135d8a058bdebd862f1c9d033b2e04b93edf6c0240970c4a0fe31e:1073", + "liquidityOccurrenceId": "1:0x17437ec4756ff7ddc253317e97fbd9fc15f53da7d1bcf053a642086000c6503f:0x91b51cc37c135d8a058bdebd862f1c9d033b2e04b93edf6c0240970c4a0fe31e:1074", + "initialBuyOccurrenceId": "1:0x17437ec4756ff7ddc253317e97fbd9fc15f53da7d1bcf053a642086000c6503f:0x91b51cc37c135d8a058bdebd862f1c9d033b2e04b93edf6c0240970c4a0fe31e:1075", + "custodyOccurrenceId": "1:0x17437ec4756ff7ddc253317e97fbd9fc15f53da7d1bcf053a642086000c6503f:0x91b51cc37c135d8a058bdebd862f1c9d033b2e04b93edf6c0240970c4a0fe31e:1076", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640571" + }, + { + "id": "1:classic-v3:0x56bd8b6492e3b01596423cdeead0fc14247d11bd9ce3824320d505570321422c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x56bd8b6492e3b01596423cdeead0fc14247d11bd9ce3824320d505570321422c", + "token": "0xa6ff815162178bba593cdb0075cd8d552e093243", + "creator": "0x46fdf1633a42b792b362c2a2e247f606beda6b10", + "quoteAsset": null, + "poolId": "0xfa41709eff9712cf94b9faf3184b0b69786e8d679682099380a787664f480a1e", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x308009273e67a9b0af2474abc1fedaabe4363fa3", + "positionRecipient": "0xdf523cdc896428ff2f8f7c3b01dc5ac6fd310ffd", + "positionTokenId": "355615", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x25c56ced08a1adb0bd870922589241a6813ee74ce7b11f73e7da8dfff0d8dfc4", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x07f4cbb7756d328287ad36707470e1977632072c4695d433a723a6d46f875346:0x8ea6206b2814f071e4bb3a42c8bb4f5c124480498507918ca22cc965c3cfa885:94", + "liquidityOccurrenceId": "1:0x07f4cbb7756d328287ad36707470e1977632072c4695d433a723a6d46f875346:0x8ea6206b2814f071e4bb3a42c8bb4f5c124480498507918ca22cc965c3cfa885:95", + "initialBuyOccurrenceId": "1:0x07f4cbb7756d328287ad36707470e1977632072c4695d433a723a6d46f875346:0x8ea6206b2814f071e4bb3a42c8bb4f5c124480498507918ca22cc965c3cfa885:96", + "custodyOccurrenceId": "1:0x07f4cbb7756d328287ad36707470e1977632072c4695d433a723a6d46f875346:0x8ea6206b2814f071e4bb3a42c8bb4f5c124480498507918ca22cc965c3cfa885:97", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645576" + }, + { + "id": "1:classic-v3:0x589fb8f50bd5a2167ceffe44cec661ee94f85415d0afdaa1bd0ee3b683495307", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x589fb8f50bd5a2167ceffe44cec661ee94f85415d0afdaa1bd0ee3b683495307", + "token": "0x4776cc910a039fef68e21f3293b9a97691a3d60b", + "creator": "0x5cb4a95b28a524e9260ceddd861bb1f2a73fc43a", + "quoteAsset": null, + "poolId": "0x6030dee393984298c501424cb280f2fc3e017f1062e0d1f31f311d0b7bb27a01", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd8c93f62f2b16442cae98013d226eb4570f7c3d8", + "positionRecipient": "0x602ba9d9757bdfd773fa3d0796d1146e7d0320c2", + "positionTokenId": "356458", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xf0af6556e1430a4439c6c2eb84fdcad4f45fbc4d001c8a4333c6b3de24b95735", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9f6cac16095aabd392b1d5070d83845c9801dbd8f62423f85a4bc63b602deca1:0xd487f8cf77e523b551317b60194c7135f177333743c4392f52ea8d51a7356329:588", + "liquidityOccurrenceId": "1:0x9f6cac16095aabd392b1d5070d83845c9801dbd8f62423f85a4bc63b602deca1:0xd487f8cf77e523b551317b60194c7135f177333743c4392f52ea8d51a7356329:589", + "initialBuyOccurrenceId": "1:0x9f6cac16095aabd392b1d5070d83845c9801dbd8f62423f85a4bc63b602deca1:0xd487f8cf77e523b551317b60194c7135f177333743c4392f52ea8d51a7356329:590", + "custodyOccurrenceId": "1:0x9f6cac16095aabd392b1d5070d83845c9801dbd8f62423f85a4bc63b602deca1:0xd487f8cf77e523b551317b60194c7135f177333743c4392f52ea8d51a7356329:591", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25648123" + }, + { + "id": "1:classic-v3:0x59084d9001e6136467871c4c9e979b763f30233bb5c023e66c074babcdf3bb54", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x59084d9001e6136467871c4c9e979b763f30233bb5c023e66c074babcdf3bb54", + "token": "0xfd8485f9c98573e3d7404ffcfd0d420ce0fb9ff2", + "creator": "0x8376eb17ee9dfb388da0cc9c026b2d5882dbefa4", + "quoteAsset": null, + "poolId": "0xf4ed98ea6f042b11e57ef7ab44902be64fa9c920604a88e875f471b1d7fc8798", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xdd25b4680e2b9df6eb9f79f56752de28d8d913d2", + "positionRecipient": "0x9a3e7bf4c5dac67c6508951a854cdef3c1a9a1fc", + "positionTokenId": "354662", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xe799fb972814ac4a7149c711bb6221aeb23eaa730d5e1121bf7db8a533ce3b13", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe61c992bba25a682e49c0ded3eb84992ac0c080c50c41f83208a658b3d9e0c8c:0x607acef87bbd2572a3c3796b1a9d7f696e083ded0bfc28e1f6eb5241a3adaac7:1343", + "liquidityOccurrenceId": "1:0xe61c992bba25a682e49c0ded3eb84992ac0c080c50c41f83208a658b3d9e0c8c:0x607acef87bbd2572a3c3796b1a9d7f696e083ded0bfc28e1f6eb5241a3adaac7:1344", + "initialBuyOccurrenceId": "1:0xe61c992bba25a682e49c0ded3eb84992ac0c080c50c41f83208a658b3d9e0c8c:0x607acef87bbd2572a3c3796b1a9d7f696e083ded0bfc28e1f6eb5241a3adaac7:1345", + "custodyOccurrenceId": "1:0xe61c992bba25a682e49c0ded3eb84992ac0c080c50c41f83208a658b3d9e0c8c:0x607acef87bbd2572a3c3796b1a9d7f696e083ded0bfc28e1f6eb5241a3adaac7:1346", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640626" + }, + { + "id": "1:classic-v3:0x5ad0a899fdcaaeb2df9bab153be4a44991c6e9f0c2548f1816e5a957a307ffff", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5ad0a899fdcaaeb2df9bab153be4a44991c6e9f0c2548f1816e5a957a307ffff", + "token": "0xfa5d9694d9f8fa47b8a6c15df4510b76cb844e2c", + "creator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "quoteAsset": null, + "poolId": "0x6f5bf5f01d4d1e41d28d0447f8c40f50e66a110077ace434e924a7dbeb8755f8", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x4cfeef5c38f9236f360e5aac67f5096db307ef3e", + "positionRecipient": "0x07af388b834fd9695ab9f013680a171d9f74c33e", + "positionTokenId": "354484", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0x4c656f75459499898c43f7360279fb0b14a84c6a390ec9e28fc4f14c4945814c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xaa566895419dbea51f8aaf2fdf33cdb5f6edf42789431d9bd71bd8caa5f2423a:0x6297f67812ca78198cc69182a3443f8cf3ca766bf7813b470ce8d0bde663a96e:428", + "liquidityOccurrenceId": "1:0xaa566895419dbea51f8aaf2fdf33cdb5f6edf42789431d9bd71bd8caa5f2423a:0x6297f67812ca78198cc69182a3443f8cf3ca766bf7813b470ce8d0bde663a96e:429", + "initialBuyOccurrenceId": "1:0xaa566895419dbea51f8aaf2fdf33cdb5f6edf42789431d9bd71bd8caa5f2423a:0x6297f67812ca78198cc69182a3443f8cf3ca766bf7813b470ce8d0bde663a96e:430", + "custodyOccurrenceId": "1:0xaa566895419dbea51f8aaf2fdf33cdb5f6edf42789431d9bd71bd8caa5f2423a:0x6297f67812ca78198cc69182a3443f8cf3ca766bf7813b470ce8d0bde663a96e:431", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25639700" + }, + { + "id": "1:classic-v3:0x5baace9860d1dbad9b5eebff0668c32b6c826ee8d0359f73e7cc25b1b445d833", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5baace9860d1dbad9b5eebff0668c32b6c826ee8d0359f73e7cc25b1b445d833", + "token": "0x27a229adba3d9a412d6722c2d2c040496d751a06", + "creator": "0x561b564e511e190e24a1ca43a3f2336e2f60e0f5", + "quoteAsset": null, + "poolId": "0xe524dfbb91d9cff941b7e1d0ed7c7adbd3237d33be79d2ac4d38e9f2994260f4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb6e69d0820f8c0011ad63d46048b82e46d3fcc6e", + "positionRecipient": "0x2505e4ecaf399c215a9e1d706b09a2ac5a0086a8", + "positionTokenId": "356061", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0x59a4f6ca23afc6e72ef44add69d9b8cd2d480d58b0a4225172016a3a918df95f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14251873763907894480382812", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9d3de708b28867ed18ceb1e50c19581e42a141ee93dcafdb177ac6a811122802:0x9836e6a8945fc736f6683be2e24376738026a18c1b13fb195ba440478082b5bf:377", + "liquidityOccurrenceId": "1:0x9d3de708b28867ed18ceb1e50c19581e42a141ee93dcafdb177ac6a811122802:0x9836e6a8945fc736f6683be2e24376738026a18c1b13fb195ba440478082b5bf:378", + "initialBuyOccurrenceId": "1:0x9d3de708b28867ed18ceb1e50c19581e42a141ee93dcafdb177ac6a811122802:0x9836e6a8945fc736f6683be2e24376738026a18c1b13fb195ba440478082b5bf:379", + "custodyOccurrenceId": "1:0x9d3de708b28867ed18ceb1e50c19581e42a141ee93dcafdb177ac6a811122802:0x9836e6a8945fc736f6683be2e24376738026a18c1b13fb195ba440478082b5bf:380", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646628" + }, + { + "id": "1:classic-v3:0x5c54eb429752d3da875d8cafc4d832dbb1c3e099dbab81323e225face1bfb91d", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5c54eb429752d3da875d8cafc4d832dbb1c3e099dbab81323e225face1bfb91d", + "token": "0x7bb352a067891fba9612fd0f58bd147163c035f6", + "creator": "0xb3729c6d8924863327c35aa396073120eb76686f", + "quoteAsset": null, + "poolId": "0x18277f7c187552a4121b8fd54003571142a0999bdc717937068d828a859b7358", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x259af01e4ee14777ed6c3038138c4a1dea90f426", + "positionRecipient": "0xf607ae455c7c8ddb85f4dd440dfadc7864685223", + "positionTokenId": "354625", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x6a0463b4b505bdcbfeab9033083ad65c1e28d30db5a4a56ae91e457b3477248b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "42000000000000000", + "initialBuyTokenAmount": "29758714777135740385512446", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa45506a5c1195f9f99065452235f6a27578c6f1a3455fd76e53792f9d406db4b:0x8ea434acd5463f711d3dbf156fd67e42613ec0f091e0214ba13246ef0645053e:241", + "liquidityOccurrenceId": "1:0xa45506a5c1195f9f99065452235f6a27578c6f1a3455fd76e53792f9d406db4b:0x8ea434acd5463f711d3dbf156fd67e42613ec0f091e0214ba13246ef0645053e:242", + "initialBuyOccurrenceId": "1:0xa45506a5c1195f9f99065452235f6a27578c6f1a3455fd76e53792f9d406db4b:0x8ea434acd5463f711d3dbf156fd67e42613ec0f091e0214ba13246ef0645053e:243", + "custodyOccurrenceId": "1:0xa45506a5c1195f9f99065452235f6a27578c6f1a3455fd76e53792f9d406db4b:0x8ea434acd5463f711d3dbf156fd67e42613ec0f091e0214ba13246ef0645053e:244", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640422" + }, + { + "id": "1:classic-v3:0x5ccf15ba916d9cd28188fbd4db41be49b9064d01014bbf60856b08cd93e289f4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5ccf15ba916d9cd28188fbd4db41be49b9064d01014bbf60856b08cd93e289f4", + "token": "0x77c86b400624812f3fc45315e2515c26b652bc43", + "creator": "0x54af2de879d03e322004f66143ba62b10e9c439c", + "quoteAsset": null, + "poolId": "0x041c0a3541ed4c77d49324ecf5de8bd6ddd365af884be7123590a2da03186dbc", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xdc4b3d35d9bc2db603f12196caeae68df23d96fa", + "positionRecipient": "0xc768240478332abf8a912d5a46488cbb10a7df82", + "positionTokenId": "354588", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xd45ad77423fd9ff79d03c077f21c384a646eb80bad2d043712d8dd98d541c406", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x8d5e814eba1b0bbf952e5052aa161e6f1055da92472524de9b97a318842202b7:0x4010b164e282604622aeb24fc014eb7cf51ca80202dae7837ff0e8f8cd8edaca:425", + "liquidityOccurrenceId": "1:0x8d5e814eba1b0bbf952e5052aa161e6f1055da92472524de9b97a318842202b7:0x4010b164e282604622aeb24fc014eb7cf51ca80202dae7837ff0e8f8cd8edaca:426", + "initialBuyOccurrenceId": "1:0x8d5e814eba1b0bbf952e5052aa161e6f1055da92472524de9b97a318842202b7:0x4010b164e282604622aeb24fc014eb7cf51ca80202dae7837ff0e8f8cd8edaca:427", + "custodyOccurrenceId": "1:0x8d5e814eba1b0bbf952e5052aa161e6f1055da92472524de9b97a318842202b7:0x4010b164e282604622aeb24fc014eb7cf51ca80202dae7837ff0e8f8cd8edaca:428", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640315" + }, + { + "id": "1:classic-v3:0x5cdb4018e6d63211fe6688e49c9d8a7c498e65077bcd52bc68ba677d30d274c3", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5cdb4018e6d63211fe6688e49c9d8a7c498e65077bcd52bc68ba677d30d274c3", + "token": "0x69fd07343edb17a752f47ad27d070313c11e8b78", + "creator": "0xe51faf16f4ee8cc168949077ac624e745bb93720", + "quoteAsset": null, + "poolId": "0xb5a047597f67eae932ca65de6414eeb0a3387ef869f52761135927063e1c68b2", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x8c79276ac61961164bb01bfae059dfbd2deb92a9", + "positionRecipient": "0xa6046e7f924104bbaf759d19d5233f466829adca", + "positionTokenId": "357185", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9faff80ec365ac70d70dccb67965515e51f8fa9d539d5c78fb73a9c7ec729591", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x8bb72f353d0a4d7094ea07dc50f0889f5f932026ac4bbfd6ae3b2aaf8d79af0b:0x6864d041f928122529ebc3882aaa4f159a6811d244649398a72c30c936cf99c9:475", + "liquidityOccurrenceId": "1:0x8bb72f353d0a4d7094ea07dc50f0889f5f932026ac4bbfd6ae3b2aaf8d79af0b:0x6864d041f928122529ebc3882aaa4f159a6811d244649398a72c30c936cf99c9:476", + "initialBuyOccurrenceId": "1:0x8bb72f353d0a4d7094ea07dc50f0889f5f932026ac4bbfd6ae3b2aaf8d79af0b:0x6864d041f928122529ebc3882aaa4f159a6811d244649398a72c30c936cf99c9:477", + "custodyOccurrenceId": "1:0x8bb72f353d0a4d7094ea07dc50f0889f5f932026ac4bbfd6ae3b2aaf8d79af0b:0x6864d041f928122529ebc3882aaa4f159a6811d244649398a72c30c936cf99c9:478", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25651557" + }, + { + "id": "1:classic-v3:0x5d286cc7907b910c3867718b20ccf5fc6f65a830a65f3fa059209a2c4a8e848b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5d286cc7907b910c3867718b20ccf5fc6f65a830a65f3fa059209a2c4a8e848b", + "token": "0x066a991e09740564381cd06f371eb9da0707531e", + "creator": "0x5ffa822ae9e83777be4468bfed1d2fad5d305df5", + "quoteAsset": null, + "poolId": "0x2e68a6d4908dc61228d2713ca1a7636096f948e4a202cc7613235c2d6463e957", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x3cb30fb83c6f01017296d97a570e719016fa468b", + "positionRecipient": "0xd091c87b1f780b2efcac22fa257b44752f96a904", + "positionTokenId": "354619", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xaf9674e7e643c7842fb11b4f4252189669642c889f68f8e56b3981d61d6b150c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe7953a2b1a17e3d49d2fb9f9ed206c55806b3e1301e9e06ae78242c1ac277b56:0x853ba6a9687704cb01657e118227835a109ceb689ebbbadca14a77269fcc9d03:196", + "liquidityOccurrenceId": "1:0xe7953a2b1a17e3d49d2fb9f9ed206c55806b3e1301e9e06ae78242c1ac277b56:0x853ba6a9687704cb01657e118227835a109ceb689ebbbadca14a77269fcc9d03:197", + "initialBuyOccurrenceId": "1:0xe7953a2b1a17e3d49d2fb9f9ed206c55806b3e1301e9e06ae78242c1ac277b56:0x853ba6a9687704cb01657e118227835a109ceb689ebbbadca14a77269fcc9d03:198", + "custodyOccurrenceId": "1:0xe7953a2b1a17e3d49d2fb9f9ed206c55806b3e1301e9e06ae78242c1ac277b56:0x853ba6a9687704cb01657e118227835a109ceb689ebbbadca14a77269fcc9d03:199", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640401" + }, + { + "id": "1:classic-v3:0x5e61012bec1c204de76e4f4449a30c33a2ed05ccff0ad2f6f63ef6e99035c4c0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x5e61012bec1c204de76e4f4449a30c33a2ed05ccff0ad2f6f63ef6e99035c4c0", + "token": "0xcd42ef990878528701e51b2f93043dff2110dd04", + "creator": "0x32767f0e97d711819853930f17b61ae6e9dfbaeb", + "quoteAsset": null, + "poolId": "0x3d749cfcf41f07ced8b88620ff2d69f0cf2b755698fa4221a212398b565aedc2", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb23da26a8d8952e20be37da9d7a84e0c6ce29743", + "positionRecipient": "0xa08fc398302bf5b36cb8f32f9428d278b14f704c", + "positionTokenId": "356176", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x39bda415054280e608a2fed13c716df56cf85ca0a370955bd882f671ae8b016c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x14507928125e015d6c1a01219e641fc6b6afcadce2db440c990e11aece9cd920:0xf9e6d9d7de81e5a1771ac301da24c9b7205424af1d970d5c9eca45fbb77b6a1f:18", + "liquidityOccurrenceId": "1:0x14507928125e015d6c1a01219e641fc6b6afcadce2db440c990e11aece9cd920:0xf9e6d9d7de81e5a1771ac301da24c9b7205424af1d970d5c9eca45fbb77b6a1f:19", + "initialBuyOccurrenceId": "1:0x14507928125e015d6c1a01219e641fc6b6afcadce2db440c990e11aece9cd920:0xf9e6d9d7de81e5a1771ac301da24c9b7205424af1d970d5c9eca45fbb77b6a1f:20", + "custodyOccurrenceId": "1:0x14507928125e015d6c1a01219e641fc6b6afcadce2db440c990e11aece9cd920:0xf9e6d9d7de81e5a1771ac301da24c9b7205424af1d970d5c9eca45fbb77b6a1f:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647131" + }, + { + "id": "1:classic-v3:0x60214230ed43d27e61b5752806edaa00d7daeed7576e9eaa23a9ba2756ef284a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x60214230ed43d27e61b5752806edaa00d7daeed7576e9eaa23a9ba2756ef284a", + "token": "0x0bcacd52070bdb7a9b732b075b563de496e02f43", + "creator": "0xb47895343d3ae603454a5f1b4358c2fd9da90caa", + "quoteAsset": null, + "poolId": "0xd073b15f1012bf2ef636229555e698eef1e944ca0e2bd7b7c7a4e69b073ee396", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x2933996802065150b04d7fef030b827c99a03912", + "positionRecipient": "0xbd277bbb8a25bc7da3e53037e8bd4066bfc1b1ad", + "positionTokenId": "356141", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x006d46bda106825cbb9aeacb51e6c035b4a0df43fb7a9d1134d414e00462db17", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x33a1c8963b7ba6e0df855efab0d35e676ba8cdd8a54ba2ee30c0f4e84fa2382c:0x7d94784bb6f03ae9fc6570484c3e56242065b97cdb8e59b5fac096feca2e4789:522", + "liquidityOccurrenceId": "1:0x33a1c8963b7ba6e0df855efab0d35e676ba8cdd8a54ba2ee30c0f4e84fa2382c:0x7d94784bb6f03ae9fc6570484c3e56242065b97cdb8e59b5fac096feca2e4789:523", + "initialBuyOccurrenceId": "1:0x33a1c8963b7ba6e0df855efab0d35e676ba8cdd8a54ba2ee30c0f4e84fa2382c:0x7d94784bb6f03ae9fc6570484c3e56242065b97cdb8e59b5fac096feca2e4789:524", + "custodyOccurrenceId": "1:0x33a1c8963b7ba6e0df855efab0d35e676ba8cdd8a54ba2ee30c0f4e84fa2382c:0x7d94784bb6f03ae9fc6570484c3e56242065b97cdb8e59b5fac096feca2e4789:525", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647001" + }, + { + "id": "1:classic-v3:0x60fbd39802fd6f573c09ad929da2c4666e0fbfedc7aca538dfef56cf92c555ec", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x60fbd39802fd6f573c09ad929da2c4666e0fbfedc7aca538dfef56cf92c555ec", + "token": "0xe8a0115d36a8dcf18417ece830b3585ad7a27e78", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0x70f7ab3f0612d7c226ff98ae1869c621261fc7eb644eed0e2dad5344d9207201", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xc903821f027f0407b945ed7507c36fdaee19baa8", + "positionRecipient": "0xc546447409901b349ad1d3115ab8e7282fcd18f6", + "positionTokenId": "356509", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x100c1ce929e4763fb96d9396177834cf0e3c9a5f27d8981a64215d52380042c2", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x5136ae9340c3eae4f0a310c217e9633c90067173161939738b7f3d386f2928eb:0x11b3f8a997996c31fa240a5aec297da05a33575e4dcd1f7d6d881ee9cbbce198:821", + "liquidityOccurrenceId": "1:0x5136ae9340c3eae4f0a310c217e9633c90067173161939738b7f3d386f2928eb:0x11b3f8a997996c31fa240a5aec297da05a33575e4dcd1f7d6d881ee9cbbce198:822", + "initialBuyOccurrenceId": "1:0x5136ae9340c3eae4f0a310c217e9633c90067173161939738b7f3d386f2928eb:0x11b3f8a997996c31fa240a5aec297da05a33575e4dcd1f7d6d881ee9cbbce198:823", + "custodyOccurrenceId": "1:0x5136ae9340c3eae4f0a310c217e9633c90067173161939738b7f3d386f2928eb:0x11b3f8a997996c31fa240a5aec297da05a33575e4dcd1f7d6d881ee9cbbce198:824", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25648312" + }, + { + "id": "1:classic-v3:0x620b4f6579b5e329baa96d14e42bfb02c9ac781102972c5554ba0db2c354b77b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x620b4f6579b5e329baa96d14e42bfb02c9ac781102972c5554ba0db2c354b77b", + "token": "0x79753eb04c4d951065fd985a005b19868fbb7891", + "creator": "0xf3e3ac45ac46ec098fa9f8a812c578cff7d348a1", + "quoteAsset": null, + "poolId": "0x2eefbcc23cb6fb1b4bae2e31accce5416d2aa483b00fafd5ddcdd048b133a6ee", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xc4843962d47cdcc31c01a224300e782f774509a1", + "positionRecipient": "0xc4b9fb18917283858cbe7ed665e5aa01d9831e74", + "positionTokenId": "355514", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x224872738a29054b9cd1679c5b8f17ef5046999d6c367487a1b0d49a09cc4043", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x42c89c8f55fcb55a350cd02b4bbdb86322eb7735eba8124afdb06b63d0c2e4c8:0x1337474075133372405958d8fedfa7c6dfbd3d2a2969b2ee0c059dac2cfd4486:896", + "liquidityOccurrenceId": "1:0x42c89c8f55fcb55a350cd02b4bbdb86322eb7735eba8124afdb06b63d0c2e4c8:0x1337474075133372405958d8fedfa7c6dfbd3d2a2969b2ee0c059dac2cfd4486:897", + "initialBuyOccurrenceId": "1:0x42c89c8f55fcb55a350cd02b4bbdb86322eb7735eba8124afdb06b63d0c2e4c8:0x1337474075133372405958d8fedfa7c6dfbd3d2a2969b2ee0c059dac2cfd4486:898", + "custodyOccurrenceId": "1:0x42c89c8f55fcb55a350cd02b4bbdb86322eb7735eba8124afdb06b63d0c2e4c8:0x1337474075133372405958d8fedfa7c6dfbd3d2a2969b2ee0c059dac2cfd4486:899", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645379" + }, + { + "id": "1:classic-v3:0x639037048d2cc3923be4104b9db0cff1f6a596acaf44988d3707d2edb8b1dc17", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x639037048d2cc3923be4104b9db0cff1f6a596acaf44988d3707d2edb8b1dc17", + "token": "0x8e513cd0b1a701f6d98f4eb586d4b10e53d7cc03", + "creator": "0x90142730403c87b503c7398e8e1be447a45bf48b", + "quoteAsset": null, + "poolId": "0x2e114033fd05154156c349ca87be1990ed124dd57fd77f63ce389b1c833677e3", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x339ab784d82c117cc2c13ff35091c6c59e3a4109", + "positionRecipient": "0xb198813eeb142d4bee5b1109c23f02448bf3164f", + "positionTokenId": "355546", + "totalSwapFeeBps": null, + "buySwapFeeBps": 300, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x41b3110ab8b0fef3120194b11e03851ac7a39c94049b1b8a31a942ca39c90bc7", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "4000000000000000", + "initialBuyTokenAmount": "2853911168799061780321555", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1e499b7324e55b703adba44a1ec5dadd3bdad975a4840b5b4aefa0a7eabdb0fd:0x4d0c511ac92df926fd6785544a8239502d87e91fdbb81cc6f2f11ef8787b8ce6:602", + "liquidityOccurrenceId": "1:0x1e499b7324e55b703adba44a1ec5dadd3bdad975a4840b5b4aefa0a7eabdb0fd:0x4d0c511ac92df926fd6785544a8239502d87e91fdbb81cc6f2f11ef8787b8ce6:603", + "initialBuyOccurrenceId": "1:0x1e499b7324e55b703adba44a1ec5dadd3bdad975a4840b5b4aefa0a7eabdb0fd:0x4d0c511ac92df926fd6785544a8239502d87e91fdbb81cc6f2f11ef8787b8ce6:604", + "custodyOccurrenceId": "1:0x1e499b7324e55b703adba44a1ec5dadd3bdad975a4840b5b4aefa0a7eabdb0fd:0x4d0c511ac92df926fd6785544a8239502d87e91fdbb81cc6f2f11ef8787b8ce6:605", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645466" + }, + { + "id": "1:classic-v3:0x639cf38884abe1f94f5f503cb5e7ff589598b31eb75ab5c2bf38e86ffe55a337", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x639cf38884abe1f94f5f503cb5e7ff589598b31eb75ab5c2bf38e86ffe55a337", + "token": "0xd97c51a02135e0dca2ebd1d4a232b956c308dbee", + "creator": "0x2e1c62353efb3743729e1af6c234b32cc4efaf85", + "quoteAsset": null, + "poolId": "0xeda653d65230fd91b0b5f45a0dcebc90ee553bf989a94782d65eeeae05540ae9", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7203096e3d29ad9385550de29ea0ed810b6df5ae", + "positionRecipient": "0xae196723cc450cc194ea9f233465e54c10837e84", + "positionTokenId": "355929", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xb8744aa49fa3a9fb8fcd4d73ab7a0ec5408dcf7634564804edc05c8a49f218d9", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "200000000000000000", + "initialBuyTokenAmount": "127441193931839478169123110", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc863cb5f36c3cdf134ca2383ec8f935c761cf5316131f8eefa3d91fc6d5284f3:0xdefb92e7b3bac03d91d223db65f142609a0302d8efa96591bdb5d2236839fbfe:527", + "liquidityOccurrenceId": "1:0xc863cb5f36c3cdf134ca2383ec8f935c761cf5316131f8eefa3d91fc6d5284f3:0xdefb92e7b3bac03d91d223db65f142609a0302d8efa96591bdb5d2236839fbfe:528", + "initialBuyOccurrenceId": "1:0xc863cb5f36c3cdf134ca2383ec8f935c761cf5316131f8eefa3d91fc6d5284f3:0xdefb92e7b3bac03d91d223db65f142609a0302d8efa96591bdb5d2236839fbfe:529", + "custodyOccurrenceId": "1:0xc863cb5f36c3cdf134ca2383ec8f935c761cf5316131f8eefa3d91fc6d5284f3:0xdefb92e7b3bac03d91d223db65f142609a0302d8efa96591bdb5d2236839fbfe:530", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646187" + }, + { + "id": "1:classic-v3:0x6484626ac99e9e9a5fdabb4255df0ab5fc39f3bc50869121c1cade076861b009", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x6484626ac99e9e9a5fdabb4255df0ab5fc39f3bc50869121c1cade076861b009", + "token": "0xdf76fdcd1f9198117f3cc857c4d329e6a2b3844c", + "creator": "0xfc3bf72e5353ca8c419dff08db344f92c232d437", + "quoteAsset": null, + "poolId": "0xdb5959de5a01301f8ff59e67fd13a2b8e203c648eca6e569284111fd484bc576", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x417c937943d046ff205fe4163cd999b49245cdea", + "positionRecipient": "0x1416012f893db2aa7d4ec6973c93bb15da58f836", + "positionTokenId": "356034", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x290814deddbfba11505ff004d1c46e11119104c5fb13b7bfbcb9e82edf53c168", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x0bc42df18e5dc4ef0b11d986ae4b2c69e860b2412db2bf3e4cdad038a8444c8a:0x9e1c1cfc7581c5604cb0a17c5302c358b9710d2601ceaf696db817b20713a48e:18", + "liquidityOccurrenceId": "1:0x0bc42df18e5dc4ef0b11d986ae4b2c69e860b2412db2bf3e4cdad038a8444c8a:0x9e1c1cfc7581c5604cb0a17c5302c358b9710d2601ceaf696db817b20713a48e:19", + "initialBuyOccurrenceId": "1:0x0bc42df18e5dc4ef0b11d986ae4b2c69e860b2412db2bf3e4cdad038a8444c8a:0x9e1c1cfc7581c5604cb0a17c5302c358b9710d2601ceaf696db817b20713a48e:20", + "custodyOccurrenceId": "1:0x0bc42df18e5dc4ef0b11d986ae4b2c69e860b2412db2bf3e4cdad038a8444c8a:0x9e1c1cfc7581c5604cb0a17c5302c358b9710d2601ceaf696db817b20713a48e:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646463" + }, + { + "id": "1:classic-v3:0x65166638ab04564d9b9fc45d5a9f4b722d1ccae9a7aa33350af3f2010d75f68f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x65166638ab04564d9b9fc45d5a9f4b722d1ccae9a7aa33350af3f2010d75f68f", + "token": "0x432036000f807e96391ec78dc0a5db86f5b1f068", + "creator": "0xb96a9d9b39bef2478f8d1a3ca476458b7c687955", + "quoteAsset": null, + "poolId": "0xdbc2437cd63a142339005c8c7bfbbf28c88adf8cebc35e2f2206005e6a90b3da", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x24a69f221b4e2b8de05e35009d8782266c01d687", + "positionRecipient": "0x1cf73a59787c59fdd85ab8b61c3ee11a0909e6a2", + "positionTokenId": "355795", + "totalSwapFeeBps": null, + "buySwapFeeBps": 300, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x1e30c9cf5cb5d422bd3a08046e9988a3db29a266ad3f8e1ccbfe5d3834e5f195", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "15000000000000000", + "initialBuyTokenAmount": "10618827608539682787730387", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa0f3c2b6527e63d147fe52cdbc7ea3b3c5071f729e266919e2301bab1ce89e3a:0x949e4510a06454d083f4c047849ad415d598de896f661c47e2b1dc8964d7cd60:309", + "liquidityOccurrenceId": "1:0xa0f3c2b6527e63d147fe52cdbc7ea3b3c5071f729e266919e2301bab1ce89e3a:0x949e4510a06454d083f4c047849ad415d598de896f661c47e2b1dc8964d7cd60:310", + "initialBuyOccurrenceId": "1:0xa0f3c2b6527e63d147fe52cdbc7ea3b3c5071f729e266919e2301bab1ce89e3a:0x949e4510a06454d083f4c047849ad415d598de896f661c47e2b1dc8964d7cd60:311", + "custodyOccurrenceId": "1:0xa0f3c2b6527e63d147fe52cdbc7ea3b3c5071f729e266919e2301bab1ce89e3a:0x949e4510a06454d083f4c047849ad415d598de896f661c47e2b1dc8964d7cd60:312", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645924" + }, + { + "id": "1:classic-v3:0x681d9bb168c4c1a803de85b349f9bf7e671c28eadbabf496af9351c1cdf058d8", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x681d9bb168c4c1a803de85b349f9bf7e671c28eadbabf496af9351c1cdf058d8", + "token": "0x43cb4c0b06d89a866f6bed7c1e9074a5014fdafd", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x3720d8363f00cbbecee330b7ef6881a2f3594b98a80b494e8d1b4e3f343eb43b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb72738f85f1dfd7fa170b1c40b51a37c6e5134eb", + "positionRecipient": "0x6acddf90700be7de3d3546409e9742be383cad4f", + "positionTokenId": "355757", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xae1fd7e4a0c62d9fbb25ceaeafab301d4d75d5dda8f5a5f24b731804ac947707", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xce8ef5d864b0766b74834e8f444068bd47fb9697d5e7e900fa1676b755e0a5d9:0xd833e5a657b559e4914320ac1051555e0df6ca36cf59f2b3be0ebaa0a08293d0:158", + "liquidityOccurrenceId": "1:0xce8ef5d864b0766b74834e8f444068bd47fb9697d5e7e900fa1676b755e0a5d9:0xd833e5a657b559e4914320ac1051555e0df6ca36cf59f2b3be0ebaa0a08293d0:159", + "initialBuyOccurrenceId": "1:0xce8ef5d864b0766b74834e8f444068bd47fb9697d5e7e900fa1676b755e0a5d9:0xd833e5a657b559e4914320ac1051555e0df6ca36cf59f2b3be0ebaa0a08293d0:160", + "custodyOccurrenceId": "1:0xce8ef5d864b0766b74834e8f444068bd47fb9697d5e7e900fa1676b755e0a5d9:0xd833e5a657b559e4914320ac1051555e0df6ca36cf59f2b3be0ebaa0a08293d0:161", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645827" + }, + { + "id": "1:classic-v3:0x685a0e81cbd16283a1aba0af975a5f92a963873c7c4824157e50ea27ada61b76", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x685a0e81cbd16283a1aba0af975a5f92a963873c7c4824157e50ea27ada61b76", + "token": "0xdc0e5196f6f2d2f6cec8521a7b9e8d04eddca90f", + "creator": "0xabc94d1a928e0c747517045e8208448ae460946f", + "quoteAsset": null, + "poolId": "0x326f8d9546b733ab1b90f05290721b6ccbb42ee9b1cb36361494c086c41b2ea0", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x25df67ac0e3c9e2b698ea9d321b2983a9ca1d6f1", + "positionRecipient": "0xa031ccc5bc55f3003e2e02a6dec9e2b3a02dcf3c", + "positionTokenId": "354651", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xc9dceb1eaa7222e2359013e5c44fa3dca9310196421ffaef2e1870a8e24048b6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xea98fda9708f0e2fe2b9fff9776640e3decd07c94997c56b93117dc75db50932:0x745975f1357c438f779e089206e2bc465f0b2fe6dc703c32baf72019f57a669b:1041", + "liquidityOccurrenceId": "1:0xea98fda9708f0e2fe2b9fff9776640e3decd07c94997c56b93117dc75db50932:0x745975f1357c438f779e089206e2bc465f0b2fe6dc703c32baf72019f57a669b:1042", + "initialBuyOccurrenceId": "1:0xea98fda9708f0e2fe2b9fff9776640e3decd07c94997c56b93117dc75db50932:0x745975f1357c438f779e089206e2bc465f0b2fe6dc703c32baf72019f57a669b:1043", + "custodyOccurrenceId": "1:0xea98fda9708f0e2fe2b9fff9776640e3decd07c94997c56b93117dc75db50932:0x745975f1357c438f779e089206e2bc465f0b2fe6dc703c32baf72019f57a669b:1044", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640550" + }, + { + "id": "1:classic-v3:0x68bb7a00b90d9fed5fdb1d6cd6912384b928ff8af267649a5db1d9bd2ddef84d", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x68bb7a00b90d9fed5fdb1d6cd6912384b928ff8af267649a5db1d9bd2ddef84d", + "token": "0xd91bfa64a95b74f3316edfe9479c1392b73b6e6a", + "creator": "0x76cf9605818799ce7e7381c69c0e42c804d8f0f2", + "quoteAsset": null, + "poolId": "0xf99a2cc12b850187ec6fba101b5d1f4fe6866414b1e22bcb5ef1603f29cf5b9f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x9449f19d823d99ab29e50574fbe3ba95fd65e4d6", + "positionRecipient": "0xd9dcf83b21468b5d3ee908403bfdec92e32a966f", + "positionTokenId": "357928", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x6ffd7afab2685581eb6fda36b41a03bd231e360cbd158577eb7f8b23aa83265c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x48f8793f229839556ed127eb1e356cd97fbb8574b6328ca3bc508d7f16416e97:0x82f96d7e4c9a2fcf6fb8918fe2698357332c8ce4aeb1f22d1597e51ce92ad51e:159", + "liquidityOccurrenceId": "1:0x48f8793f229839556ed127eb1e356cd97fbb8574b6328ca3bc508d7f16416e97:0x82f96d7e4c9a2fcf6fb8918fe2698357332c8ce4aeb1f22d1597e51ce92ad51e:160", + "initialBuyOccurrenceId": "1:0x48f8793f229839556ed127eb1e356cd97fbb8574b6328ca3bc508d7f16416e97:0x82f96d7e4c9a2fcf6fb8918fe2698357332c8ce4aeb1f22d1597e51ce92ad51e:161", + "custodyOccurrenceId": "1:0x48f8793f229839556ed127eb1e356cd97fbb8574b6328ca3bc508d7f16416e97:0x82f96d7e4c9a2fcf6fb8918fe2698357332c8ce4aeb1f22d1597e51ce92ad51e:162", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25655306" + }, + { + "id": "1:classic-v3:0x699e5b5e0cafc4b5770254a65755cb094f578b4730baabac529e9103191fbb11", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x699e5b5e0cafc4b5770254a65755cb094f578b4730baabac529e9103191fbb11", + "token": "0xd81bccc476c3ee6009130b674f1472aba757041f", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0xcbbe6481a82c21a22648e5d352671237f1503efafe72663ad2c210005f9def55", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1f9e05ff20debd66d66f9a66b847fad8ded43b77", + "positionRecipient": "0x81de9b035e1f89591c012795bcada26b49155095", + "positionTokenId": "355536", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x95fce9823af79ca3e928f53557b4234b54064a7a2b41dc3c58c16794c5fb56ec", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf7b6180536f13e8d29b8cfaa23667b9d71bb9f1b8c37ad147eef6f89f7e7c7d9:0x97282967d56d64970a16a31b162dd789312d080a05a49e496efe892fb7a411d3:332", + "liquidityOccurrenceId": "1:0xf7b6180536f13e8d29b8cfaa23667b9d71bb9f1b8c37ad147eef6f89f7e7c7d9:0x97282967d56d64970a16a31b162dd789312d080a05a49e496efe892fb7a411d3:333", + "initialBuyOccurrenceId": "1:0xf7b6180536f13e8d29b8cfaa23667b9d71bb9f1b8c37ad147eef6f89f7e7c7d9:0x97282967d56d64970a16a31b162dd789312d080a05a49e496efe892fb7a411d3:334", + "custodyOccurrenceId": "1:0xf7b6180536f13e8d29b8cfaa23667b9d71bb9f1b8c37ad147eef6f89f7e7c7d9:0x97282967d56d64970a16a31b162dd789312d080a05a49e496efe892fb7a411d3:335", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645453" + }, + { + "id": "1:classic-v3:0x6a1c0a289e15b8a39fe3721998e93ca94641f7bd27829e57f987c126e4c460e6", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x6a1c0a289e15b8a39fe3721998e93ca94641f7bd27829e57f987c126e4c460e6", + "token": "0x0b4aec0706c731bd6b51f767ce711453b1a3a2da", + "creator": "0x7ad3ebe4a9da5654cbb3281dc66a5573105c750c", + "quoteAsset": null, + "poolId": "0xe93dbb190d1d832ba178676770cc470adcfa739ce519cda0c02fe54b4909f23e", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd4ff0627d7ffea28773f95fbb252308721dd4dfb", + "positionRecipient": "0x7534fa3fffb627d74d28d097131710bce5c3afd0", + "positionTokenId": "355694", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x7ff3b0d9ebc7c84c77699aa5de9011b4f5bce382dab56f8807c07909c7abd0d6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x164e295616039745d58196b3cccec2183b04ecfd2a5a85926cae0c4f711ff515:0x50dfc302ddce471c070c68d8d9b68000ba9337cc01040b18ea8016d5eeaab8b0:372", + "liquidityOccurrenceId": "1:0x164e295616039745d58196b3cccec2183b04ecfd2a5a85926cae0c4f711ff515:0x50dfc302ddce471c070c68d8d9b68000ba9337cc01040b18ea8016d5eeaab8b0:373", + "initialBuyOccurrenceId": "1:0x164e295616039745d58196b3cccec2183b04ecfd2a5a85926cae0c4f711ff515:0x50dfc302ddce471c070c68d8d9b68000ba9337cc01040b18ea8016d5eeaab8b0:374", + "custodyOccurrenceId": "1:0x164e295616039745d58196b3cccec2183b04ecfd2a5a85926cae0c4f711ff515:0x50dfc302ddce471c070c68d8d9b68000ba9337cc01040b18ea8016d5eeaab8b0:375", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645689" + }, + { + "id": "1:classic-v3:0x6aaf3e0d207430e80d7a12021f2e30043b428f34f301f625669b1e920eb7b490", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x6aaf3e0d207430e80d7a12021f2e30043b428f34f301f625669b1e920eb7b490", + "token": "0xb095ef6bc2ca4e18da2ffe7e847a46f390ac936a", + "creator": "0x380b8685dd98b66ff2f1bf02f938ada40580fc3e", + "quoteAsset": null, + "poolId": "0x1d651d64fc1ba2680d451112bf6c2e820660b7e97b915c2ed166b6a58b3a8716", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x8867f08a1e805206b1688188d13b676418035d45", + "positionRecipient": "0xd48f9c846fe1f2785786fbfd8bbcfa70607e3833", + "positionTokenId": "356464", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9bdd84f8b33afe8a714a63796e92d36174ca09dfcd19c5c36c96ef54dcf3f7d7", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x3bac20ec84289ccfcf1d93106998ec12d4a6b450d6f26e7c8891d49941c1fcf5:0xdf1cdbdc7b1e032775fbf5dc9d1cf88153c2bf383154e32844ecc310d11a42a7:248", + "liquidityOccurrenceId": "1:0x3bac20ec84289ccfcf1d93106998ec12d4a6b450d6f26e7c8891d49941c1fcf5:0xdf1cdbdc7b1e032775fbf5dc9d1cf88153c2bf383154e32844ecc310d11a42a7:249", + "initialBuyOccurrenceId": "1:0x3bac20ec84289ccfcf1d93106998ec12d4a6b450d6f26e7c8891d49941c1fcf5:0xdf1cdbdc7b1e032775fbf5dc9d1cf88153c2bf383154e32844ecc310d11a42a7:250", + "custodyOccurrenceId": "1:0x3bac20ec84289ccfcf1d93106998ec12d4a6b450d6f26e7c8891d49941c1fcf5:0xdf1cdbdc7b1e032775fbf5dc9d1cf88153c2bf383154e32844ecc310d11a42a7:251", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25648156" + }, + { + "id": "1:classic-v3:0x6b206e19eb482e01972c13fa86da7f4dffaa8511b839fc3d4ec2716fe198555f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x6b206e19eb482e01972c13fa86da7f4dffaa8511b839fc3d4ec2716fe198555f", + "token": "0x57b8e3a2eb94e684b1f92582a6be8a9d54039956", + "creator": "0x75d4830a424836b8dc9c95e4fb7357109b653eb8", + "quoteAsset": null, + "poolId": "0x396c2ab70d4aa7ea7e6b92d12f597c00c7eaefd327219157bb82331c45c18032", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x14f4b4ad3030ca335ab3570650dd9bb250d90b36", + "positionRecipient": "0x5104089f97c7248014e4d4d2536cb02183baf357", + "positionTokenId": "355751", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x2ffc6e690eae67784503a812bba670f54b9700029058d16bdc883574153a8ead", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x77f673ad764f5689dbc2ffdab41e3dd98e1971b375c02770d5accac2d9af7874:0xdeb5279eba7b56b2d1652a028907199ffc9f03feb8ad7924f0c472085d48b30f:563", + "liquidityOccurrenceId": "1:0x77f673ad764f5689dbc2ffdab41e3dd98e1971b375c02770d5accac2d9af7874:0xdeb5279eba7b56b2d1652a028907199ffc9f03feb8ad7924f0c472085d48b30f:564", + "initialBuyOccurrenceId": "1:0x77f673ad764f5689dbc2ffdab41e3dd98e1971b375c02770d5accac2d9af7874:0xdeb5279eba7b56b2d1652a028907199ffc9f03feb8ad7924f0c472085d48b30f:565", + "custodyOccurrenceId": "1:0x77f673ad764f5689dbc2ffdab41e3dd98e1971b375c02770d5accac2d9af7874:0xdeb5279eba7b56b2d1652a028907199ffc9f03feb8ad7924f0c472085d48b30f:566", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645811" + }, + { + "id": "1:classic-v3:0x6e11ad79d6da416b80f721c039898038390b42360de14db7bd8f24c4600fa445", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x6e11ad79d6da416b80f721c039898038390b42360de14db7bd8f24c4600fa445", + "token": "0xf52cba4d2704463fc77ed522e24f790dac562f18", + "creator": "0x4762b949c1341572ea4f68fc1157c917774504d5", + "quoteAsset": null, + "poolId": "0x986b81ce317cc7fbc38c304d0d0a65f7a378c065721ea4be67aa69d549b9cc4a", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x4da37c792c7c157e3aaea1f0a89afe903065fe4f", + "positionRecipient": "0xde9ee47391197c55285ad4ab0139beabe108f796", + "positionTokenId": "355664", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x26b762730c93ad5e1594795d54694d87f61633e8f3513b17e31d7a0bfac4e082", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "30000000000000000", + "initialBuyTokenAmount": "21438505518229829458161070", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x5dc5c7203d2aee9213ffaec1d8f54e1576184a47dd3997f163c06cfb24f53a5f:0x9af438462c2d18fdf2a700160fd1c9d1eddeb80afd5c07cc69f1014bc32ff37b:720", + "liquidityOccurrenceId": "1:0x5dc5c7203d2aee9213ffaec1d8f54e1576184a47dd3997f163c06cfb24f53a5f:0x9af438462c2d18fdf2a700160fd1c9d1eddeb80afd5c07cc69f1014bc32ff37b:721", + "initialBuyOccurrenceId": "1:0x5dc5c7203d2aee9213ffaec1d8f54e1576184a47dd3997f163c06cfb24f53a5f:0x9af438462c2d18fdf2a700160fd1c9d1eddeb80afd5c07cc69f1014bc32ff37b:722", + "custodyOccurrenceId": "1:0x5dc5c7203d2aee9213ffaec1d8f54e1576184a47dd3997f163c06cfb24f53a5f:0x9af438462c2d18fdf2a700160fd1c9d1eddeb80afd5c07cc69f1014bc32ff37b:723", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645642" + }, + { + "id": "1:classic-v3:0x6e3f66852e482e9c7d0c1d839cd9e4c35cab7566df3a8e9abc57a2a2e9be2d75", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x6e3f66852e482e9c7d0c1d839cd9e4c35cab7566df3a8e9abc57a2a2e9be2d75", + "token": "0x04c82d148f3570b90432c9ccb908b20555a6f369", + "creator": "0x14495123d050005036fcdabdb3114ab243a60f2d", + "quoteAsset": null, + "poolId": "0x38e661dc4e93c5f1be921c5bf1edc6b6730c2a8f995380e5d95e19f109318727", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x205c992c6093b4739787b7b8edc2eec7e0f31a9d", + "positionRecipient": "0x6442604ffe9d57e129edf72d99f13cf157106896", + "positionTokenId": "356734", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x715680752fcfd822c4616d8f4541d4ba3c099eac2e74fd502fbbb2d0b7014edd", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "4500000000000000", + "initialBuyTokenAmount": "3275463717672648078015375", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe26cfa000ba6508282415940859b9574a134cff68e043d61c88335c0f0370e93:0x8291d5489dfc40b56db9212cd6d34a460ea245892208c3351606bf5533dfe81c:206", + "liquidityOccurrenceId": "1:0xe26cfa000ba6508282415940859b9574a134cff68e043d61c88335c0f0370e93:0x8291d5489dfc40b56db9212cd6d34a460ea245892208c3351606bf5533dfe81c:207", + "initialBuyOccurrenceId": "1:0xe26cfa000ba6508282415940859b9574a134cff68e043d61c88335c0f0370e93:0x8291d5489dfc40b56db9212cd6d34a460ea245892208c3351606bf5533dfe81c:208", + "custodyOccurrenceId": "1:0xe26cfa000ba6508282415940859b9574a134cff68e043d61c88335c0f0370e93:0x8291d5489dfc40b56db9212cd6d34a460ea245892208c3351606bf5533dfe81c:209", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25649210" + }, + { + "id": "1:classic-v3:0x70934ce502f76c87610f5bd1675cb7e37425993c340fb6f8cd55b1f9296b89c6", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x70934ce502f76c87610f5bd1675cb7e37425993c340fb6f8cd55b1f9296b89c6", + "token": "0x735cc71c5440dae57cbe2494b7084761dd5e7974", + "creator": "0xc3574b859693105db80400287f3782dd196e93c6", + "quoteAsset": null, + "poolId": "0xda1c5792b5f34aa922f3cf01fddd661b7d9cc7c3ce1e8c598709cdef63fed8a6", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xec4c2eb0825f7e190aef0b47d229c5e6bbe51ba4", + "positionRecipient": "0xbd55f2dfcaa0a09d8ffa5b9fdccee08d66bd36bd", + "positionTokenId": "355609", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x0a71b1d662b58024bb9139c334043f9faf750e95d62c3557675592b9879a9e03", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "4000000000000000", + "initialBuyTokenAmount": "2912583311371364487286705", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1e6693b5fec3df72f203d6445662c3dc0a3ce2f06f592b8940b72a6b7ec49de6:0x2735fe0819ebfa75242c8f79bd4d0b1d352824d0393ff6e458eef9ee88644feb:235", + "liquidityOccurrenceId": "1:0x1e6693b5fec3df72f203d6445662c3dc0a3ce2f06f592b8940b72a6b7ec49de6:0x2735fe0819ebfa75242c8f79bd4d0b1d352824d0393ff6e458eef9ee88644feb:236", + "initialBuyOccurrenceId": "1:0x1e6693b5fec3df72f203d6445662c3dc0a3ce2f06f592b8940b72a6b7ec49de6:0x2735fe0819ebfa75242c8f79bd4d0b1d352824d0393ff6e458eef9ee88644feb:237", + "custodyOccurrenceId": "1:0x1e6693b5fec3df72f203d6445662c3dc0a3ce2f06f592b8940b72a6b7ec49de6:0x2735fe0819ebfa75242c8f79bd4d0b1d352824d0393ff6e458eef9ee88644feb:238", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645570" + }, + { + "id": "1:classic-v3:0x72bb54b304fe98bf212685fc0c7152cfec269de87a5e4fcd05a8c849c9633eaa", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x72bb54b304fe98bf212685fc0c7152cfec269de87a5e4fcd05a8c849c9633eaa", + "token": "0x0707b51c176f95715703241abdacfd136a522b77", + "creator": "0x239cdc761ae531a39a5dbe552e60d5ad69f4a22a", + "quoteAsset": null, + "poolId": "0x26857ae7d95e86e94d6d605c3fb2426a56e561edb0c2079d15e00e90b30f3c5d", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6573f34e667078914f59266ed1b01d810a9386aa", + "positionRecipient": "0xf9d77f1749c561166837ae57d29cf8cf6d6be30c", + "positionTokenId": "355521", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xa1b3f36781ccd123990fe57e3f290c608826d21e670f60ceddac0db42b94f63f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x0eacf110ed8c7f18a8b25d3b735baf90970a019beb7b3d61e677489fe09a63aa:171", + "liquidityOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x0eacf110ed8c7f18a8b25d3b735baf90970a019beb7b3d61e677489fe09a63aa:172", + "initialBuyOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x0eacf110ed8c7f18a8b25d3b735baf90970a019beb7b3d61e677489fe09a63aa:173", + "custodyOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x0eacf110ed8c7f18a8b25d3b735baf90970a019beb7b3d61e677489fe09a63aa:174", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645401" + }, + { + "id": "1:classic-v3:0x74d25373ae5f48886e7bebee776e46e4b34070cd0f1d913788456dcbcc6c14f4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x74d25373ae5f48886e7bebee776e46e4b34070cd0f1d913788456dcbcc6c14f4", + "token": "0x78a8c628956deddc65252a07465dd5cd3fd03cf2", + "creator": "0xc79891f64faf5aee3bce89a7b1514b0370b02814", + "quoteAsset": null, + "poolId": "0xed800e80ed614fd98123e38f4aa0bc302ef0460ea95030b52a6063e68c40e405", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb43ab762cd97e6a93ab5529e33f65b1daa97d3e1", + "positionRecipient": "0xaff0fd3f2e3646821e90a99f7ba6deb72e1e10c0", + "positionTokenId": "355110", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x7c58aeed006aa126822bef3c8ef5f50f4727d06218095a15ed9b1111b911eb5d", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "729739899031511876349884", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x2b3f36523071d60a02d0facaa68e1f9f2c7ad97e15aa81c5abd223b11cce83c7:0x0aab74704c4aff6fbbb6cae301f03ceb7f4c7716044d56af15fe4a87e39091df:186", + "liquidityOccurrenceId": "1:0x2b3f36523071d60a02d0facaa68e1f9f2c7ad97e15aa81c5abd223b11cce83c7:0x0aab74704c4aff6fbbb6cae301f03ceb7f4c7716044d56af15fe4a87e39091df:187", + "initialBuyOccurrenceId": "1:0x2b3f36523071d60a02d0facaa68e1f9f2c7ad97e15aa81c5abd223b11cce83c7:0x0aab74704c4aff6fbbb6cae301f03ceb7f4c7716044d56af15fe4a87e39091df:188", + "custodyOccurrenceId": "1:0x2b3f36523071d60a02d0facaa68e1f9f2c7ad97e15aa81c5abd223b11cce83c7:0x0aab74704c4aff6fbbb6cae301f03ceb7f4c7716044d56af15fe4a87e39091df:189", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25643453" + }, + { + "id": "1:classic-v3:0x7877e264b62c6c76de34339b84c868f019fdbbe689804f5891dc903c4aa44dce", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7877e264b62c6c76de34339b84c868f019fdbbe689804f5891dc903c4aa44dce", + "token": "0x5a3a4e701bb253f8b1ffb3655e713ae9d063187a", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x59239ec61d4541d4d5abc1bc6c179e24a0660911ca0f7b41c7a12f948150a2a6", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x3bb26197ad65ac35237122edb037dd59eaf4e148", + "positionRecipient": "0x2883fdab6f630033ae7ae0f5775f71efab907514", + "positionTokenId": "355740", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x54963582f5a6d61dc5348470dc4658d8446c04b9dfb34600060686e93203007e", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x5b76a744c237a2992ea92fa35a6c242b6589332653d5a430f13385d4b5cc5efd:0x39e69eab35c1c07a422a92135a2d60d2848b32f95014e38f43f0a3287772eaee:172", + "liquidityOccurrenceId": "1:0x5b76a744c237a2992ea92fa35a6c242b6589332653d5a430f13385d4b5cc5efd:0x39e69eab35c1c07a422a92135a2d60d2848b32f95014e38f43f0a3287772eaee:173", + "initialBuyOccurrenceId": "1:0x5b76a744c237a2992ea92fa35a6c242b6589332653d5a430f13385d4b5cc5efd:0x39e69eab35c1c07a422a92135a2d60d2848b32f95014e38f43f0a3287772eaee:174", + "custodyOccurrenceId": "1:0x5b76a744c237a2992ea92fa35a6c242b6589332653d5a430f13385d4b5cc5efd:0x39e69eab35c1c07a422a92135a2d60d2848b32f95014e38f43f0a3287772eaee:175", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645782" + }, + { + "id": "1:classic-v3:0x7c1721c684a2d25cd28ad752b8f6f8bfddd97cfb875c27d9d9586bc30a2130d1", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7c1721c684a2d25cd28ad752b8f6f8bfddd97cfb875c27d9d9586bc30a2130d1", + "token": "0xb937970fdf688abcce4bc10f16a2c6bc621c7299", + "creator": "0xe51faf16f4ee8cc168949077ac624e745bb93720", + "quoteAsset": null, + "poolId": "0x1d06bcf9086312b37b39354455afbd1909a366232c4859009453cfc69b247008", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x9a81e0835260543d64017f00d101d6eff512d99e", + "positionRecipient": "0x593077792bcc1be0f718535a37434447a0880a74", + "positionTokenId": "356165", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x8bfa632f63b660ac20b0501dfb0c6a2f4f489be14411b6830efd9433e9efb4fa", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "666000000000000", + "initialBuyTokenAmount": "486125257583779785273649", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x52b66e4690cd221e5db82e17da3a9a7f205a803f51d3f5d33577d30151f642af:0xf1a3f8fd864ddbe314203fa8f1aa9890b8a352e1a9a98f936056f3b8852c3687:368", + "liquidityOccurrenceId": "1:0x52b66e4690cd221e5db82e17da3a9a7f205a803f51d3f5d33577d30151f642af:0xf1a3f8fd864ddbe314203fa8f1aa9890b8a352e1a9a98f936056f3b8852c3687:369", + "initialBuyOccurrenceId": "1:0x52b66e4690cd221e5db82e17da3a9a7f205a803f51d3f5d33577d30151f642af:0xf1a3f8fd864ddbe314203fa8f1aa9890b8a352e1a9a98f936056f3b8852c3687:370", + "custodyOccurrenceId": "1:0x52b66e4690cd221e5db82e17da3a9a7f205a803f51d3f5d33577d30151f642af:0xf1a3f8fd864ddbe314203fa8f1aa9890b8a352e1a9a98f936056f3b8852c3687:371", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647104" + }, + { + "id": "1:classic-v3:0x7c216f5754c5ca422cb8783f102b7e10d6ef8ba3a3e953d384661ddfc954e385", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7c216f5754c5ca422cb8783f102b7e10d6ef8ba3a3e953d384661ddfc954e385", + "token": "0x41809d8038e71058122527ead94668fa06c3d284", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0xc611f90fd8c2b61817e02a92adbf93dfd3cb15dd5ace19dc1a63f804c25c5be4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x8757d38dd5550532132015d4b73b477cb759f319", + "positionRecipient": "0xc9855f07321f879c14bc721dfca5605a30b4249b", + "positionTokenId": "355723", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x1faf94df4a3889a7452e50c950d38a1b2fadb623790a9bfcc2ceae3cc3e68851", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xd9318c490ab9cdbb1cc06f1ca1403f0a732197125582c4024620a5853a95ac6e:0x6d1843a6ce8a47d1544073f1d80c58415e287531361c75946ab0035109f528aa:78", + "liquidityOccurrenceId": "1:0xd9318c490ab9cdbb1cc06f1ca1403f0a732197125582c4024620a5853a95ac6e:0x6d1843a6ce8a47d1544073f1d80c58415e287531361c75946ab0035109f528aa:79", + "initialBuyOccurrenceId": "1:0xd9318c490ab9cdbb1cc06f1ca1403f0a732197125582c4024620a5853a95ac6e:0x6d1843a6ce8a47d1544073f1d80c58415e287531361c75946ab0035109f528aa:80", + "custodyOccurrenceId": "1:0xd9318c490ab9cdbb1cc06f1ca1403f0a732197125582c4024620a5853a95ac6e:0x6d1843a6ce8a47d1544073f1d80c58415e287531361c75946ab0035109f528aa:81", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645733" + }, + { + "id": "1:classic-v3:0x7c311f84a8212c73410b09de61a23641998ff51232fa6d1b2ead86064fc54343", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7c311f84a8212c73410b09de61a23641998ff51232fa6d1b2ead86064fc54343", + "token": "0x114cd5524fe9d413d4ade9f6c72cff06cb86bffe", + "creator": "0x5ffa822ae9e83777be4468bfed1d2fad5d305df5", + "quoteAsset": null, + "poolId": "0xde04e107a7088ffd1c9075ad4e7be69475e9b765f8731496d1326c42657359cf", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x2fe70daf5f37497d4f733df22a2c237ec4e87229", + "positionRecipient": "0xb514aa64c5cc306498d624afa150c46d197b7fe7", + "positionTokenId": "354587", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x96316409c1c7d6b6a0b6407470c51eccc222e7339a3280e50c36db615aa9da92", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7c33b1449d0c2e22c2875d52d3be3d2ec5fb7fc43576b2c2409a673cd3d4c1dd:0x0a98eaaabb0a1bf4eef2fa4ca35924c62a47072af5040ef854f8d2f0e898d90c:276", + "liquidityOccurrenceId": "1:0x7c33b1449d0c2e22c2875d52d3be3d2ec5fb7fc43576b2c2409a673cd3d4c1dd:0x0a98eaaabb0a1bf4eef2fa4ca35924c62a47072af5040ef854f8d2f0e898d90c:277", + "initialBuyOccurrenceId": "1:0x7c33b1449d0c2e22c2875d52d3be3d2ec5fb7fc43576b2c2409a673cd3d4c1dd:0x0a98eaaabb0a1bf4eef2fa4ca35924c62a47072af5040ef854f8d2f0e898d90c:278", + "custodyOccurrenceId": "1:0x7c33b1449d0c2e22c2875d52d3be3d2ec5fb7fc43576b2c2409a673cd3d4c1dd:0x0a98eaaabb0a1bf4eef2fa4ca35924c62a47072af5040ef854f8d2f0e898d90c:279", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640309" + }, + { + "id": "1:classic-v3:0x7c924afedf43eb329c3dbdd567318e3d89d3007ba0804159886b19e01dbf366f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7c924afedf43eb329c3dbdd567318e3d89d3007ba0804159886b19e01dbf366f", + "token": "0xb8e4d439b7f4951d7b14cd9dd030a2faddf2d47a", + "creator": "0x75d4830a424836b8dc9c95e4fb7357109b653eb8", + "quoteAsset": null, + "poolId": "0x7b487355623a4c2f9a945bfbec359b02f01cfd12d511993d36b26e6b716d4bb9", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x9d79cf6ef6d9dd6e1e6dc737095abaf7d3494517", + "positionRecipient": "0xf53c8331939ccb1cfd7ddec178bdd8164f41528d", + "positionTokenId": "355794", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xb9e373974dbab492f6243bc4284778166ba3a1199695a5ce16ea7751ae077d6a", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1e0128218722d06088e9b09b168dd4d592c0bbedf1f044c87e814921144a2702:0x7434864fd582b4544705481850701a8e1a590859242e79ffe4cb561f2a59ce72:559", + "liquidityOccurrenceId": "1:0x1e0128218722d06088e9b09b168dd4d592c0bbedf1f044c87e814921144a2702:0x7434864fd582b4544705481850701a8e1a590859242e79ffe4cb561f2a59ce72:560", + "initialBuyOccurrenceId": "1:0x1e0128218722d06088e9b09b168dd4d592c0bbedf1f044c87e814921144a2702:0x7434864fd582b4544705481850701a8e1a590859242e79ffe4cb561f2a59ce72:561", + "custodyOccurrenceId": "1:0x1e0128218722d06088e9b09b168dd4d592c0bbedf1f044c87e814921144a2702:0x7434864fd582b4544705481850701a8e1a590859242e79ffe4cb561f2a59ce72:562", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645923" + }, + { + "id": "1:classic-v3:0x7eb9e7a9772994084062b815b9bd53e18febdd0551af140a023af33f976aaf35", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7eb9e7a9772994084062b815b9bd53e18febdd0551af140a023af33f976aaf35", + "token": "0xf8a9f580d0019c900074f88c0d94b1fee71fa2a3", + "creator": "0xbe8af7e12b536ab55fbaf92edbb512972e0504da", + "quoteAsset": null, + "poolId": "0x645dc466ebbdc90ec108c852aeac88b947a16ef26729c594ff8692da3bd05db7", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x3e1f554f02179f586fd495e4dbe941696e474aaa", + "positionRecipient": "0x2363e03dd8b827527807a1f19eb5cefb7d08f5cd", + "positionTokenId": "354627", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x890b83d5543a1248e703d47ca9e5314197ff9ac21fb789115c1b1239b5689b09", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x007de019c0fd781c2556672fdf08427619f40d560bedcc77dd2a22f1013b7a0f:0xebfe0e77b90a308adf00b51b34de6b4efcd756edae0be8b9731ed2f8670329fb:1464", + "liquidityOccurrenceId": "1:0x007de019c0fd781c2556672fdf08427619f40d560bedcc77dd2a22f1013b7a0f:0xebfe0e77b90a308adf00b51b34de6b4efcd756edae0be8b9731ed2f8670329fb:1465", + "initialBuyOccurrenceId": "1:0x007de019c0fd781c2556672fdf08427619f40d560bedcc77dd2a22f1013b7a0f:0xebfe0e77b90a308adf00b51b34de6b4efcd756edae0be8b9731ed2f8670329fb:1466", + "custodyOccurrenceId": "1:0x007de019c0fd781c2556672fdf08427619f40d560bedcc77dd2a22f1013b7a0f:0xebfe0e77b90a308adf00b51b34de6b4efcd756edae0be8b9731ed2f8670329fb:1467", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640435" + }, + { + "id": "1:classic-v3:0x7f6ab23776b0b6013cfea9500290db0f42cdb12635c74e1037b4ea3c52cdc615", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7f6ab23776b0b6013cfea9500290db0f42cdb12635c74e1037b4ea3c52cdc615", + "token": "0x47d816cdddf88601d84786bee739ed8c74e2f86b", + "creator": "0xc457b4a3eab11c88ad157081cd17e524e11fce04", + "quoteAsset": null, + "poolId": "0x4ac2e33c6a518cdff9e3d504ba3cbbdd868fea6068d4f361cac0e5486960ceaf", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x50d080d3a575666cd90c58343a1ca20e00532217", + "positionRecipient": "0x1046dc0de25d6bb528a76ffc677c6804cf98bccd", + "positionTokenId": "356699", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x3baa237ec1e367e6ef1abb9d251ec9a841547776260bb2d8f1c47defa5a4a382", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x15dd0cd1e70788a67f4cd05b0bbbab2746194279488e97a1682575a7484997bb:0xad3d8c2256d3b93ece9506694156ba0d704b0a1e3a447bf2bc423792f81a3010:103", + "liquidityOccurrenceId": "1:0x15dd0cd1e70788a67f4cd05b0bbbab2746194279488e97a1682575a7484997bb:0xad3d8c2256d3b93ece9506694156ba0d704b0a1e3a447bf2bc423792f81a3010:104", + "initialBuyOccurrenceId": "1:0x15dd0cd1e70788a67f4cd05b0bbbab2746194279488e97a1682575a7484997bb:0xad3d8c2256d3b93ece9506694156ba0d704b0a1e3a447bf2bc423792f81a3010:105", + "custodyOccurrenceId": "1:0x15dd0cd1e70788a67f4cd05b0bbbab2746194279488e97a1682575a7484997bb:0xad3d8c2256d3b93ece9506694156ba0d704b0a1e3a447bf2bc423792f81a3010:106", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25649051" + }, + { + "id": "1:classic-v3:0x7fe16657632efab0f2c355abbf60f79ff7cfd236afe4c3edd03c5f7d2ab880f0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x7fe16657632efab0f2c355abbf60f79ff7cfd236afe4c3edd03c5f7d2ab880f0", + "token": "0xf86ead51c210670635afd66827b508904fe76618", + "creator": "0xb47895343d3ae603454a5f1b4358c2fd9da90caa", + "quoteAsset": null, + "poolId": "0xbeda3e7838efc883c8e6ae86b6d860dabb7d6ecc00e479349dec09b4f91e4dbc", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd83b995a948c24e5dce514eab1871c0389b4f522", + "positionRecipient": "0x8db41bbefed6f1e857bc81e96b010b13833477d9", + "positionTokenId": "356831", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x7af413832337791de5659af0c10c573d4578c27a18cf4edf437ac70d0ada2f21", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x51df26d4a1dabebe318f85b07844b74773c9755a9f867a060675638411b6d1ab:0xcfb3e52a41b60ea1554a4523a9805980b3894b063bd3ef16613eca27c96bb4d8:206", + "liquidityOccurrenceId": "1:0x51df26d4a1dabebe318f85b07844b74773c9755a9f867a060675638411b6d1ab:0xcfb3e52a41b60ea1554a4523a9805980b3894b063bd3ef16613eca27c96bb4d8:207", + "initialBuyOccurrenceId": "1:0x51df26d4a1dabebe318f85b07844b74773c9755a9f867a060675638411b6d1ab:0xcfb3e52a41b60ea1554a4523a9805980b3894b063bd3ef16613eca27c96bb4d8:208", + "custodyOccurrenceId": "1:0x51df26d4a1dabebe318f85b07844b74773c9755a9f867a060675638411b6d1ab:0xcfb3e52a41b60ea1554a4523a9805980b3894b063bd3ef16613eca27c96bb4d8:209", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25649822" + }, + { + "id": "1:classic-v3:0x80f8880452e5534ad4a207f57229c00d4c08993fe053ed429eb67fe764fb5056", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x80f8880452e5534ad4a207f57229c00d4c08993fe053ed429eb67fe764fb5056", + "token": "0x44c74adbd07f776643fcb6ca99c7d031e5c571fd", + "creator": "0xb3940904537334e359246e327235e5db25d175e4", + "quoteAsset": null, + "poolId": "0x4a601813570506547acab483783188e0a72f160190c2e8271310e5264f279c61", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb0a9c3cf2ee92d6d09c0d559ec5c7e8dad1d530b", + "positionRecipient": "0x4ddc5528569b8d532257f06d1f8f084ceee22483", + "positionTokenId": "355760", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x44b3f76cb4e9ae2468f986061399d11c9a5f66ac980bf3d6fdb7769cf0817251", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x0308f154f9d8f3d535be2586b62e71ee6bea35a381bdd69e36324ee739116be0:0x6f80ea90be3f40cd8c1f045667b7fddde05e42d2c0d603dcd20b0d36113bb0af:112", + "liquidityOccurrenceId": "1:0x0308f154f9d8f3d535be2586b62e71ee6bea35a381bdd69e36324ee739116be0:0x6f80ea90be3f40cd8c1f045667b7fddde05e42d2c0d603dcd20b0d36113bb0af:113", + "initialBuyOccurrenceId": "1:0x0308f154f9d8f3d535be2586b62e71ee6bea35a381bdd69e36324ee739116be0:0x6f80ea90be3f40cd8c1f045667b7fddde05e42d2c0d603dcd20b0d36113bb0af:114", + "custodyOccurrenceId": "1:0x0308f154f9d8f3d535be2586b62e71ee6bea35a381bdd69e36324ee739116be0:0x6f80ea90be3f40cd8c1f045667b7fddde05e42d2c0d603dcd20b0d36113bb0af:115", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645841" + }, + { + "id": "1:classic-v3:0x81a147b934cda4c70fbf9f3eeba0ae048440946356815fa38fe05616e036f870", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x81a147b934cda4c70fbf9f3eeba0ae048440946356815fa38fe05616e036f870", + "token": "0x0da45a2aff55bba4c5c542c6b9436fe401c74897", + "creator": "0x2e1c62353efb3743729e1af6c234b32cc4efaf85", + "quoteAsset": null, + "poolId": "0xdce4fdc68013a8caf52485b38736acc594044be7727959b049e07ba06fd436fe", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x73ba8ee8d26ab929264c9ca2c5843b69938b5db4", + "positionRecipient": "0x1c89cfe49d4b32021fdd78af66dc1c8d3e838432", + "positionTokenId": "356023", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x8daca5c25322f93839743419a469a80ef712bdba292bceacf7220e3dedcabb66", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4fa30b4643be69208fcc4c16c01d2b2e195967057758e2c797bd39c7f99866fa:0xbb6141879ce9a459327ecb5207bf209ee74cb35510a6e3fd96574683dd2d3f4d:464", + "liquidityOccurrenceId": "1:0x4fa30b4643be69208fcc4c16c01d2b2e195967057758e2c797bd39c7f99866fa:0xbb6141879ce9a459327ecb5207bf209ee74cb35510a6e3fd96574683dd2d3f4d:465", + "initialBuyOccurrenceId": "1:0x4fa30b4643be69208fcc4c16c01d2b2e195967057758e2c797bd39c7f99866fa:0xbb6141879ce9a459327ecb5207bf209ee74cb35510a6e3fd96574683dd2d3f4d:466", + "custodyOccurrenceId": "1:0x4fa30b4643be69208fcc4c16c01d2b2e195967057758e2c797bd39c7f99866fa:0xbb6141879ce9a459327ecb5207bf209ee74cb35510a6e3fd96574683dd2d3f4d:467", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646427" + }, + { + "id": "1:classic-v3:0x81c4a8b4965cfdea03934be9a2ed992ee27ce97008e0268191044e46504bc469", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x81c4a8b4965cfdea03934be9a2ed992ee27ce97008e0268191044e46504bc469", + "token": "0x86cb79562185d27c41a714a2d3e785d7a1eaf095", + "creator": "0xf79780543e90afd2bef0325f2573a283f05fb55b", + "quoteAsset": null, + "poolId": "0x4c1d4ee870520c705880f7be2e7bb876ba73e413ae85a34a91e865e27a900cc4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x0dafc85d46c6bdb4919f498739dafd0de85beb4b", + "positionRecipient": "0x52b57c3775db540f5cd3da02b078ad9b425e281e", + "positionTokenId": "354568", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x650d581eb3acb3af1211a9d6c473899be2b416642b47b2fbc31a78c4582b389c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "433549742227946105221397", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb2680dc937970e9a89578027cccb2e7358214ca0fee29e83dc8cbea8092d159b:0x50a7305114ce8021352d5f09e1e8175ac1e500ea8e2d7a08f4e52df6daba860b:138", + "liquidityOccurrenceId": "1:0xb2680dc937970e9a89578027cccb2e7358214ca0fee29e83dc8cbea8092d159b:0x50a7305114ce8021352d5f09e1e8175ac1e500ea8e2d7a08f4e52df6daba860b:139", + "initialBuyOccurrenceId": "1:0xb2680dc937970e9a89578027cccb2e7358214ca0fee29e83dc8cbea8092d159b:0x50a7305114ce8021352d5f09e1e8175ac1e500ea8e2d7a08f4e52df6daba860b:140", + "custodyOccurrenceId": "1:0xb2680dc937970e9a89578027cccb2e7358214ca0fee29e83dc8cbea8092d159b:0x50a7305114ce8021352d5f09e1e8175ac1e500ea8e2d7a08f4e52df6daba860b:141", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640201" + }, + { + "id": "1:classic-v3:0x827dbb16e0060fe820142e3a2807b612c35d7f435419ce5a772b8689e40969e5", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x827dbb16e0060fe820142e3a2807b612c35d7f435419ce5a772b8689e40969e5", + "token": "0xe8dea615ab5579a18648bf666314b3a7c21e177a", + "creator": "0x25911bad5471fa668e081635f15648cc924f5e2e", + "quoteAsset": null, + "poolId": "0x38a36e14986d56c66411d5321bdf5e3f431de00f1331ac9c02f614b01c636b3e", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xe165dc0807023d84101e49e513f26241f5afd0ff", + "positionRecipient": "0x771d4c7375bbcb0630458d35d63c9c0ae0c70760", + "positionTokenId": "355862", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x47a7d3a88d4e19bb6fbef899d85693c310b66d557ff4711c4d65b228fe25b8a6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x6025224e809934ef717b49e53401326c1976669604047114875fbc291ae79004:0x79d62596ba0ac01bbf1361cd0efb80c36761c2bb137a5729ef9aab43502ea777:116", + "liquidityOccurrenceId": "1:0x6025224e809934ef717b49e53401326c1976669604047114875fbc291ae79004:0x79d62596ba0ac01bbf1361cd0efb80c36761c2bb137a5729ef9aab43502ea777:117", + "initialBuyOccurrenceId": "1:0x6025224e809934ef717b49e53401326c1976669604047114875fbc291ae79004:0x79d62596ba0ac01bbf1361cd0efb80c36761c2bb137a5729ef9aab43502ea777:118", + "custodyOccurrenceId": "1:0x6025224e809934ef717b49e53401326c1976669604047114875fbc291ae79004:0x79d62596ba0ac01bbf1361cd0efb80c36761c2bb137a5729ef9aab43502ea777:119", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646031" + }, + { + "id": "1:classic-v3:0x8328670402b4fcb68d36cadec9a52ce82e93ed320807da4152f842adcc148815", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x8328670402b4fcb68d36cadec9a52ce82e93ed320807da4152f842adcc148815", + "token": "0x51da1f81412ca84a32bf8b910f117a729991e810", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x61931cccdc4217d819c475b06cb989533563828c51c17915c3d0e4a9580aedb9", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x59af5ac3cead3b5d586b09bbdd00102d2d930b7f", + "positionRecipient": "0xd04bb680d61a59f81b62ecef5de6a32999ba1b53", + "positionTokenId": "355776", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x087fe76d8e6c1a8b88dbbcc0646464c91196bc69b1b3eb7a48cd04d1e6ddd6b8", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4321e3cd90d47dae9fe0588b2abef707bf387a955604cc0d60a3dbd69ff69601:0x553e56cdf4af46cebb35a9edbccc8ecdc883b8cc3d7cdbea681ed2a60abadb2b:620", + "liquidityOccurrenceId": "1:0x4321e3cd90d47dae9fe0588b2abef707bf387a955604cc0d60a3dbd69ff69601:0x553e56cdf4af46cebb35a9edbccc8ecdc883b8cc3d7cdbea681ed2a60abadb2b:621", + "initialBuyOccurrenceId": "1:0x4321e3cd90d47dae9fe0588b2abef707bf387a955604cc0d60a3dbd69ff69601:0x553e56cdf4af46cebb35a9edbccc8ecdc883b8cc3d7cdbea681ed2a60abadb2b:622", + "custodyOccurrenceId": "1:0x4321e3cd90d47dae9fe0588b2abef707bf387a955604cc0d60a3dbd69ff69601:0x553e56cdf4af46cebb35a9edbccc8ecdc883b8cc3d7cdbea681ed2a60abadb2b:623", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645885" + }, + { + "id": "1:classic-v3:0x848fe8403c40ecf4dc56104faa4f60c668a6af09e1ad3dc8cd872ac9c265a8b9", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x848fe8403c40ecf4dc56104faa4f60c668a6af09e1ad3dc8cd872ac9c265a8b9", + "token": "0xd7fa71a8beb3b3858232da2170681595d3c8f1dd", + "creator": "0xe9555be3df0b4f0e125f6d5b88bb8194d674ca7e", + "quoteAsset": null, + "poolId": "0x8f86c83db25dd87799bb44394ce8c62380cd99e8fc1fa5043ac579b9a1eed0af", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xec3483d193aa33168b2d3f95cb01c11882a9b194", + "positionRecipient": "0x0bd292f67278fb391a596156466157692f2af555", + "positionTokenId": "356003", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x47e88d2fa572941c64340e5d96a358a8bb01ea5b1cf92ac8366861fd72bca3ee", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1ffd8e3c0d4fb687e1180ab297e9b229fc9a36728a35ec7f892a3d5f2dcb6da3:0xf1ba0d9efefc5fc9e4749f33fd9273dc67c54f6673f80b3e3c650aa9aad7c74e:464", + "liquidityOccurrenceId": "1:0x1ffd8e3c0d4fb687e1180ab297e9b229fc9a36728a35ec7f892a3d5f2dcb6da3:0xf1ba0d9efefc5fc9e4749f33fd9273dc67c54f6673f80b3e3c650aa9aad7c74e:465", + "initialBuyOccurrenceId": "1:0x1ffd8e3c0d4fb687e1180ab297e9b229fc9a36728a35ec7f892a3d5f2dcb6da3:0xf1ba0d9efefc5fc9e4749f33fd9273dc67c54f6673f80b3e3c650aa9aad7c74e:466", + "custodyOccurrenceId": "1:0x1ffd8e3c0d4fb687e1180ab297e9b229fc9a36728a35ec7f892a3d5f2dcb6da3:0xf1ba0d9efefc5fc9e4749f33fd9273dc67c54f6673f80b3e3c650aa9aad7c74e:467", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646368" + }, + { + "id": "1:classic-v3:0x8639ba350368e2ff42d232d6114421397dc365ab59b624310fba7da927a79fc0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x8639ba350368e2ff42d232d6114421397dc365ab59b624310fba7da927a79fc0", + "token": "0x8a7773c9922ad003bee4eefa4e9e924c80a25a2f", + "creator": "0x5c7917ab56f13a0aada0765559b1e53593900231", + "quoteAsset": null, + "poolId": "0xe1582e2f0ccf31a9fd27fc413f68f3f7eb8e7d81756188444b2f1c828fcfc8b6", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x4ab51d06747771c1a6e408230d7a95676e64bf4a", + "positionRecipient": "0xa5d7b945e8a5f720bccee712ffa490a2ff092141", + "positionTokenId": "354612", + "totalSwapFeeBps": null, + "buySwapFeeBps": 1000, + "sellSwapFeeBps": 1000, + "rewardConfigurationHash": "0x8fda245cebfb1fa593b7f1eeea1af39c13dc323d03ee527e62df420b122c6428", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "398172018566563722319445", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7a971b4603fe3c083d2d4b05f88467544433772b7698e4eda1e8680a68656db2:0x7f31a78dab500250af32c9ca626266d90f06eee7fae425166088eb76e18ed0c4:1276", + "liquidityOccurrenceId": "1:0x7a971b4603fe3c083d2d4b05f88467544433772b7698e4eda1e8680a68656db2:0x7f31a78dab500250af32c9ca626266d90f06eee7fae425166088eb76e18ed0c4:1277", + "initialBuyOccurrenceId": "1:0x7a971b4603fe3c083d2d4b05f88467544433772b7698e4eda1e8680a68656db2:0x7f31a78dab500250af32c9ca626266d90f06eee7fae425166088eb76e18ed0c4:1278", + "custodyOccurrenceId": "1:0x7a971b4603fe3c083d2d4b05f88467544433772b7698e4eda1e8680a68656db2:0x7f31a78dab500250af32c9ca626266d90f06eee7fae425166088eb76e18ed0c4:1279", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640387" + }, + { + "id": "1:classic-v3:0x89a2b4df2ef7b40ffad38b41803eccbc0de503f508e4118288d5f4515f0427e6", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x89a2b4df2ef7b40ffad38b41803eccbc0de503f508e4118288d5f4515f0427e6", + "token": "0x701413bb67689e2e6befec83af461660d9a7e9a1", + "creator": "0xc7bcb2eee9bbfbf875499960746bc52b2e1a75c6", + "quoteAsset": null, + "poolId": "0xce2f7819ab67e0812f467c718623ebf928fa2a20074e936010ab49211ec6ff73", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb606144a90d9b1332f41821b641540f151dd2654", + "positionRecipient": "0x572e2f26ff74f97f50b414ae1d7c396423e2f0d0", + "positionTokenId": "357554", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 500, + "rewardConfigurationHash": "0xd33581416e61e887204be8baf7499e8045df37b144814a4f342c81d9f44ea4f4", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "12500000000000000", + "initialBuyTokenAmount": "9045836002208465417516040", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc9d932187bb0293e1aa16cab4e1afc7cc5075f9d3c4711cafc9f3cf5daf4b427:0x0cbde02d9526857154359c40aec9a4e24c975a2a93f2a994c2fa197f32dc70c1:510", + "liquidityOccurrenceId": "1:0xc9d932187bb0293e1aa16cab4e1afc7cc5075f9d3c4711cafc9f3cf5daf4b427:0x0cbde02d9526857154359c40aec9a4e24c975a2a93f2a994c2fa197f32dc70c1:511", + "initialBuyOccurrenceId": "1:0xc9d932187bb0293e1aa16cab4e1afc7cc5075f9d3c4711cafc9f3cf5daf4b427:0x0cbde02d9526857154359c40aec9a4e24c975a2a93f2a994c2fa197f32dc70c1:512", + "custodyOccurrenceId": "1:0xc9d932187bb0293e1aa16cab4e1afc7cc5075f9d3c4711cafc9f3cf5daf4b427:0x0cbde02d9526857154359c40aec9a4e24c975a2a93f2a994c2fa197f32dc70c1:513", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25653547" + }, + { + "id": "1:classic-v3:0x8b03c84506bf66da2a9680fd1ae4b8f46d5d749446bb5bdc5f4ade03075f859b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x8b03c84506bf66da2a9680fd1ae4b8f46d5d749446bb5bdc5f4ade03075f859b", + "token": "0xbb899f7f9a9d1ca7f8e7218dad737d45014dcda5", + "creator": "0x2f8022c2870bd9f1473e7020aeaed93acb005126", + "quoteAsset": null, + "poolId": "0xb206136776cebc2b974becb13b573d1c64be57e2004008b3d4e2196450a88aa5", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x43c6acc91c5892f91157f035ca8027365afe9c8c", + "positionRecipient": "0x81307ddf9291ad50ea914b658817e35dbe8f72a3", + "positionTokenId": "355600", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9e35bca19735dc281ce4f5ba0477d794b7a492ca6c9d82506094d1991824dd4c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9fe49b01d256f84b14c119481dd85d9b4028b25e194d7fba55f752d70fb57a34:0x9710160f16ba6bcbe2016f91d46ae502c7767a73061dcd6f54eab166e5e75b5c:629", + "liquidityOccurrenceId": "1:0x9fe49b01d256f84b14c119481dd85d9b4028b25e194d7fba55f752d70fb57a34:0x9710160f16ba6bcbe2016f91d46ae502c7767a73061dcd6f54eab166e5e75b5c:630", + "initialBuyOccurrenceId": "1:0x9fe49b01d256f84b14c119481dd85d9b4028b25e194d7fba55f752d70fb57a34:0x9710160f16ba6bcbe2016f91d46ae502c7767a73061dcd6f54eab166e5e75b5c:631", + "custodyOccurrenceId": "1:0x9fe49b01d256f84b14c119481dd85d9b4028b25e194d7fba55f752d70fb57a34:0x9710160f16ba6bcbe2016f91d46ae502c7767a73061dcd6f54eab166e5e75b5c:632", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645544" + }, + { + "id": "1:classic-v3:0x8cca2bf2400a9f086fa6fb73d4a4990a0819446a062eb6821abd32a92b54cd9c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x8cca2bf2400a9f086fa6fb73d4a4990a0819446a062eb6821abd32a92b54cd9c", + "token": "0xae8b2cc60766a5ad36d9ce3d3fdec341d82f51df", + "creator": "0xf3e3ac45ac46ec098fa9f8a812c578cff7d348a1", + "quoteAsset": null, + "poolId": "0x1a55873b7a0cafb7d8d94303c528d60ed79afd8559d6b02d94c78727040f6649", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x5e6ae4e7ce90e498e292938e8e21ed23f537304b", + "positionRecipient": "0x3664043f0649c9e40d60e013f2a393128433eb3e", + "positionTokenId": "355506", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x0c01e3c408aabc30ecc19f1a3c135684179761b95bae7ddf142237d565fabc16", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x6ce13b77c34f65c15ff2a1fb554757823323d3ccd5384978baf6664df73966fa:0x4bb767a07ee0ab126d635fdf451ccced73ef6e044a3f4bff6c2a91fadcfb43e9:294", + "liquidityOccurrenceId": "1:0x6ce13b77c34f65c15ff2a1fb554757823323d3ccd5384978baf6664df73966fa:0x4bb767a07ee0ab126d635fdf451ccced73ef6e044a3f4bff6c2a91fadcfb43e9:295", + "initialBuyOccurrenceId": "1:0x6ce13b77c34f65c15ff2a1fb554757823323d3ccd5384978baf6664df73966fa:0x4bb767a07ee0ab126d635fdf451ccced73ef6e044a3f4bff6c2a91fadcfb43e9:296", + "custodyOccurrenceId": "1:0x6ce13b77c34f65c15ff2a1fb554757823323d3ccd5384978baf6664df73966fa:0x4bb767a07ee0ab126d635fdf451ccced73ef6e044a3f4bff6c2a91fadcfb43e9:297", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645361" + }, + { + "id": "1:classic-v3:0x8dac5dd66690deceae06d30006366ec0244abeabdc9eb35b1fd93683fa8c94a7", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x8dac5dd66690deceae06d30006366ec0244abeabdc9eb35b1fd93683fa8c94a7", + "token": "0x1471a10dd9a731fb663d84dc203be563ef705c00", + "creator": "0xb3940904537334e359246e327235e5db25d175e4", + "quoteAsset": null, + "poolId": "0x6ef3944e7e50a3c33e60fb8d365d6c218f06fd7dde31c0082f471d20730eeb65", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xeb7559b71a39a03045e9b36b0397268e1de71c15", + "positionRecipient": "0xd0816c32d9c441daff9dc02afd6e4825cc2d5577", + "positionTokenId": "355793", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x6e12925bd0835b76ad91a423a5862996599d4291f8cc6a30dc363542a0843044", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x251acb69f08def384fd5a06852c5c2e8fa33373e3cea0e52ab64de2e580eee28:0x0e375454da9d165c0cbc0bcd51d03cd33ee2dd8dcd797ac2272e8bc6e3f4a714:161", + "liquidityOccurrenceId": "1:0x251acb69f08def384fd5a06852c5c2e8fa33373e3cea0e52ab64de2e580eee28:0x0e375454da9d165c0cbc0bcd51d03cd33ee2dd8dcd797ac2272e8bc6e3f4a714:162", + "initialBuyOccurrenceId": "1:0x251acb69f08def384fd5a06852c5c2e8fa33373e3cea0e52ab64de2e580eee28:0x0e375454da9d165c0cbc0bcd51d03cd33ee2dd8dcd797ac2272e8bc6e3f4a714:163", + "custodyOccurrenceId": "1:0x251acb69f08def384fd5a06852c5c2e8fa33373e3cea0e52ab64de2e580eee28:0x0e375454da9d165c0cbc0bcd51d03cd33ee2dd8dcd797ac2272e8bc6e3f4a714:164", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645921" + }, + { + "id": "1:classic-v3:0x8de24818285ffd47f3817a40d4ece07f7fee08d1bd29cb3fd08260d163714483", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x8de24818285ffd47f3817a40d4ece07f7fee08d1bd29cb3fd08260d163714483", + "token": "0x11c95e63648e9d2959df11bc77a9775460381cbb", + "creator": "0x9f97749468e99d299e56fc611e9c2b172ecd9f19", + "quoteAsset": null, + "poolId": "0x26248a5793e07b5af9719d44bfb9b98331def0cd0dfe56d8fd38b7214176cd7a", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xad213f8a9b0ff6dd87ea1bd7fce8cd3fbc0b4fb5", + "positionRecipient": "0x5e96f28f519d4fea4279fcb8e0857d68bd766f46", + "positionTokenId": "357816", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x0eff8dcd4ed6070691d82f7c5dfeb213d844d676ccf8fc5135af9982fc0e79a5", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "5000000000000000", + "initialBuyTokenAmount": "3638080086377951380764388", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xfe9563c97af2540acb9903350358a09773dcbd7c60f5a164d986197452ba1a28:0x696de29761465de5c09570f2312af4f25dea723165dad75710fd027d6d3bda37:1033", + "liquidityOccurrenceId": "1:0xfe9563c97af2540acb9903350358a09773dcbd7c60f5a164d986197452ba1a28:0x696de29761465de5c09570f2312af4f25dea723165dad75710fd027d6d3bda37:1034", + "initialBuyOccurrenceId": "1:0xfe9563c97af2540acb9903350358a09773dcbd7c60f5a164d986197452ba1a28:0x696de29761465de5c09570f2312af4f25dea723165dad75710fd027d6d3bda37:1035", + "custodyOccurrenceId": "1:0xfe9563c97af2540acb9903350358a09773dcbd7c60f5a164d986197452ba1a28:0x696de29761465de5c09570f2312af4f25dea723165dad75710fd027d6d3bda37:1036", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25654502" + }, + { + "id": "1:classic-v3:0x8ed0e9f7b1f6a4f51fd10fe24e26da96c5ff03c69ad8f16541e615f64c927fd9", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x8ed0e9f7b1f6a4f51fd10fe24e26da96c5ff03c69ad8f16541e615f64c927fd9", + "token": "0x7d8cc9c29eb5ef86bd18721958646c8bc7bc3dbc", + "creator": "0x4b08916a6d14ef89324c827c1fa26138a00470f2", + "quoteAsset": null, + "poolId": "0xdb24f819a48d8d1bf3e8dca639ac0e2b4ad55a86f1f42e8e668c6639373a2c52", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7ea3b3bfeda66eaf0cceb1239816e15fd2e352f4", + "positionRecipient": "0x76b3b3124e4795af46a5d90846eeb72a13d0f6a3", + "positionTokenId": "355640", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xaeb63b05b76d6a21320d94182271c677dd10414213e4710dcd670fdb790a860b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9fdcff5e5f7321bd878da248ad38869e79b22e551e43a6f519e17d01f560e209:0x946d693c7240c9e22682f73e2cfca6a85adb5d86d47d0c1e08c2ddad516b898d:317", + "liquidityOccurrenceId": "1:0x9fdcff5e5f7321bd878da248ad38869e79b22e551e43a6f519e17d01f560e209:0x946d693c7240c9e22682f73e2cfca6a85adb5d86d47d0c1e08c2ddad516b898d:318", + "initialBuyOccurrenceId": "1:0x9fdcff5e5f7321bd878da248ad38869e79b22e551e43a6f519e17d01f560e209:0x946d693c7240c9e22682f73e2cfca6a85adb5d86d47d0c1e08c2ddad516b898d:319", + "custodyOccurrenceId": "1:0x9fdcff5e5f7321bd878da248ad38869e79b22e551e43a6f519e17d01f560e209:0x946d693c7240c9e22682f73e2cfca6a85adb5d86d47d0c1e08c2ddad516b898d:320", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645606" + }, + { + "id": "1:classic-v3:0x905b404d7301260b9eb54b9754ec4d6bd87af19f229a3ace06b083c23c45c00a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x905b404d7301260b9eb54b9754ec4d6bd87af19f229a3ace06b083c23c45c00a", + "token": "0x8f98494036694bc6ee6a245ad70583776bc3d058", + "creator": "0x5cb4a95b28a524e9260ceddd861bb1f2a73fc43a", + "quoteAsset": null, + "poolId": "0xb12f8f3742c4a69d9274544a753fb973b052d04ea754c14bb36b01b00949030c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xd053ac06589b9e63d0bb0001d0c5361ee3302ef1", + "positionRecipient": "0xe1cfc5a75fada3e708b991ff4b0297f69c0e1398", + "positionTokenId": "357227", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xa7259c031d3d1131b35094090b95cc428b37d403850d061ef007e9d33d4b6030", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9eb56f92b74389363a1516352eab91ffc5e4bd8ccf990d5293691a59661e3f6d:0xf0b76fcb3aa8acf18f8ca5552f836941ea1023272bc875a3daaebb74c5a124a5:427", + "liquidityOccurrenceId": "1:0x9eb56f92b74389363a1516352eab91ffc5e4bd8ccf990d5293691a59661e3f6d:0xf0b76fcb3aa8acf18f8ca5552f836941ea1023272bc875a3daaebb74c5a124a5:428", + "initialBuyOccurrenceId": "1:0x9eb56f92b74389363a1516352eab91ffc5e4bd8ccf990d5293691a59661e3f6d:0xf0b76fcb3aa8acf18f8ca5552f836941ea1023272bc875a3daaebb74c5a124a5:429", + "custodyOccurrenceId": "1:0x9eb56f92b74389363a1516352eab91ffc5e4bd8ccf990d5293691a59661e3f6d:0xf0b76fcb3aa8acf18f8ca5552f836941ea1023272bc875a3daaebb74c5a124a5:430", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25651784" + }, + { + "id": "1:classic-v3:0x934890970a742ae6c0dda1d323bc23c99cfeb647ae28950f2caf0b8dbdb39d30", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x934890970a742ae6c0dda1d323bc23c99cfeb647ae28950f2caf0b8dbdb39d30", + "token": "0xe60845e1672ec5157aed16cf574de4bfe75a54a4", + "creator": "0xc3574b859693105db80400287f3782dd196e93c6", + "quoteAsset": null, + "poolId": "0x195300e41d1a8807a50605ba22dc328562f1f952725e8790c441e8645b64a633", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x00e9fb18a230dc0989720d2d4b63026279aad1d8", + "positionRecipient": "0x095fb8c4c82419078c1ad15f44821661160e96c3", + "positionTokenId": "356025", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x49c77f78b8c57ca5d00cc94254646fdce8679dbeefbb76afce40e3c249c506e6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xfb139dc600d58f7721a480edd2c02f96350acd72fe4f54a971b06dfcd50abc9f:0xa7252baeaccb6fa409c840116111f36cee6c29860541194e1d96b151acb4da9a:518", + "liquidityOccurrenceId": "1:0xfb139dc600d58f7721a480edd2c02f96350acd72fe4f54a971b06dfcd50abc9f:0xa7252baeaccb6fa409c840116111f36cee6c29860541194e1d96b151acb4da9a:519", + "initialBuyOccurrenceId": "1:0xfb139dc600d58f7721a480edd2c02f96350acd72fe4f54a971b06dfcd50abc9f:0xa7252baeaccb6fa409c840116111f36cee6c29860541194e1d96b151acb4da9a:520", + "custodyOccurrenceId": "1:0xfb139dc600d58f7721a480edd2c02f96350acd72fe4f54a971b06dfcd50abc9f:0xa7252baeaccb6fa409c840116111f36cee6c29860541194e1d96b151acb4da9a:521", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646443" + }, + { + "id": "1:classic-v3:0x969b539e8457d5b32832a53aba6fdbf16a02edf7fb5758203fb5dd0ac2a0df2e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x969b539e8457d5b32832a53aba6fdbf16a02edf7fb5758203fb5dd0ac2a0df2e", + "token": "0xfa11c84fc46b9ee78c655064c4b6ffdd7af7283f", + "creator": "0x968672a4b47be63a76efebccc80dde4829db151f", + "quoteAsset": null, + "poolId": "0xc89e0de6bece154e42f1fc5663e85373efb3fe9653d2c3c7edbc26df95296546", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xdaed1866d6553868fda4916e35107e5f76fdd534", + "positionRecipient": "0xc94b03cde647f13a56849df978b7a94736003940", + "positionTokenId": "354579", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x961f9260408578f8bb293279358512fb356f720544149c93961fc7ccad8f57bb", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "729739899031511876349884", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x527c18b1332e021bedef1b99098ea9b21ff94f7c97b83a61360edf44818bc138:0xa2d563bc9dd5ebac9d1af1734fdf16fa9de58cd5d80da16574bff182860c7800:478", + "liquidityOccurrenceId": "1:0x527c18b1332e021bedef1b99098ea9b21ff94f7c97b83a61360edf44818bc138:0xa2d563bc9dd5ebac9d1af1734fdf16fa9de58cd5d80da16574bff182860c7800:479", + "initialBuyOccurrenceId": "1:0x527c18b1332e021bedef1b99098ea9b21ff94f7c97b83a61360edf44818bc138:0xa2d563bc9dd5ebac9d1af1734fdf16fa9de58cd5d80da16574bff182860c7800:480", + "custodyOccurrenceId": "1:0x527c18b1332e021bedef1b99098ea9b21ff94f7c97b83a61360edf44818bc138:0xa2d563bc9dd5ebac9d1af1734fdf16fa9de58cd5d80da16574bff182860c7800:481", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640254" + }, + { + "id": "1:classic-v3:0x96b61d71d94d3dae83006821b125d045ba23eee57a1d156015dfc48baa76d275", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x96b61d71d94d3dae83006821b125d045ba23eee57a1d156015dfc48baa76d275", + "token": "0x0bc95cc31e5ce7e0de5de8579facb5f1314142fa", + "creator": "0x12e54bc919ba8cb3bf9ef18926d8667ede2f436f", + "quoteAsset": null, + "poolId": "0xcab266a8059ff182148b198f8aa52df6499c3b08abcae9160a9ebd4e4727739d", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xe23b76e122e617abdd3b2c5c92da68cf7c2db673", + "positionRecipient": "0xd688702ea3cf0fbb5b1e27eb016f65c527011090", + "positionTokenId": "355515", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x41ceebe870d6aaacbf845f57ff4fef292321d4a5d43f1c829a6974143d721e39", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x83a2cf7144750f8ccd00c60cee5cbc5e194636462185f30bf7f9ae43d416d5ab:0xadfe336109acce1519d7b3feb71d0153585ba00acee9283bcdd9156accaa11dc:694", + "liquidityOccurrenceId": "1:0x83a2cf7144750f8ccd00c60cee5cbc5e194636462185f30bf7f9ae43d416d5ab:0xadfe336109acce1519d7b3feb71d0153585ba00acee9283bcdd9156accaa11dc:695", + "initialBuyOccurrenceId": "1:0x83a2cf7144750f8ccd00c60cee5cbc5e194636462185f30bf7f9ae43d416d5ab:0xadfe336109acce1519d7b3feb71d0153585ba00acee9283bcdd9156accaa11dc:696", + "custodyOccurrenceId": "1:0x83a2cf7144750f8ccd00c60cee5cbc5e194636462185f30bf7f9ae43d416d5ab:0xadfe336109acce1519d7b3feb71d0153585ba00acee9283bcdd9156accaa11dc:697", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645383" + }, + { + "id": "1:classic-v3:0x978c875d7a47dcaffc5acb66c7109d71ceb9db138eafbf356c7fe113c5e93df5", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x978c875d7a47dcaffc5acb66c7109d71ceb9db138eafbf356c7fe113c5e93df5", + "token": "0xfeac77b1650c58ee007309743e9059c0991f3f5c", + "creator": "0x4b08916a6d14ef89324c827c1fa26138a00470f2", + "quoteAsset": null, + "poolId": "0x6ed178ea18036f2cb39ee068b34b1946628857c2665526264d71f132a0a75881", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xed6700d8083c8142594551f4a8c3b5613758b723", + "positionRecipient": "0xb62f7bb4def3be0cf346c79607a2bf7fea7f077e", + "positionTokenId": "356320", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xdf898f251e5f7d8384e94b3ec69d496d62d6d300f7a2de7b718defe8cfc240f6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "120000000000000000", + "initialBuyTokenAmount": "80571992740005192597773019", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x116cdf856c3fef7dbd1db618db423291894305071ba287e3d42759a2ebbff2da:0x7eb513dd22d5f88ee137d9e894eca22f59eb50b500a5fe710ee0c5506efd52bc:208", + "liquidityOccurrenceId": "1:0x116cdf856c3fef7dbd1db618db423291894305071ba287e3d42759a2ebbff2da:0x7eb513dd22d5f88ee137d9e894eca22f59eb50b500a5fe710ee0c5506efd52bc:209", + "initialBuyOccurrenceId": "1:0x116cdf856c3fef7dbd1db618db423291894305071ba287e3d42759a2ebbff2da:0x7eb513dd22d5f88ee137d9e894eca22f59eb50b500a5fe710ee0c5506efd52bc:210", + "custodyOccurrenceId": "1:0x116cdf856c3fef7dbd1db618db423291894305071ba287e3d42759a2ebbff2da:0x7eb513dd22d5f88ee137d9e894eca22f59eb50b500a5fe710ee0c5506efd52bc:211", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647611" + }, + { + "id": "1:classic-v3:0x97f2dc8114ff47e9c5223bc480ab38fdc5a68e26768fa77062cacdcf1513bdbf", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x97f2dc8114ff47e9c5223bc480ab38fdc5a68e26768fa77062cacdcf1513bdbf", + "token": "0x22cb1d3d89155e6eb1b8c50428194fd2975a66f8", + "creator": "0xc657f1c6555aa745d623d1c1d69d2d27b53ef9a6", + "quoteAsset": null, + "poolId": "0x40b4fe3113611c3acf5fc7ecb381dc9202a22d1e02c7f5d3c5982eb0a73050c9", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x2f5f85f87f30a133d9e6a8ce76d6f336dfcc8ea6", + "positionRecipient": "0xfe2a5fe91269e9c34f1af5be9261c641a6222e14", + "positionTokenId": "357930", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x79bbd91e432855aed61f1a885353d7c726f1cb09ae86134f9968398c11840a2d", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf6c99a2acf98f4149d046079354500898bb1428dea4cd7d53af9053e6851c747:0xfa32b6592bc09dd2c126d6676062141663726962aec7654fee4756b9f7ee25b3:702", + "liquidityOccurrenceId": "1:0xf6c99a2acf98f4149d046079354500898bb1428dea4cd7d53af9053e6851c747:0xfa32b6592bc09dd2c126d6676062141663726962aec7654fee4756b9f7ee25b3:703", + "initialBuyOccurrenceId": "1:0xf6c99a2acf98f4149d046079354500898bb1428dea4cd7d53af9053e6851c747:0xfa32b6592bc09dd2c126d6676062141663726962aec7654fee4756b9f7ee25b3:704", + "custodyOccurrenceId": "1:0xf6c99a2acf98f4149d046079354500898bb1428dea4cd7d53af9053e6851c747:0xfa32b6592bc09dd2c126d6676062141663726962aec7654fee4756b9f7ee25b3:705", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25655323" + }, + { + "id": "1:classic-v3:0x99f98a892a028d5abf566c5074bad9566fd3e4c779d3866dddfe7b7710e08c12", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x99f98a892a028d5abf566c5074bad9566fd3e4c779d3866dddfe7b7710e08c12", + "token": "0x0f914304464ec79a416a0554202416c564e70b81", + "creator": "0x32c88ff1dc5189a634ee7759f929e58b5b9c241e", + "quoteAsset": null, + "poolId": "0x95b6e2d64a8042cbdca37f8126e176608f68ed464dccb498b735d899667cd763", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x9f865c26c39973700195af38a5fc94fe2679eb61", + "positionRecipient": "0xc2ab85987174001cc4cd6e14e5f9c0d01d27305e", + "positionTokenId": "356009", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x49ab83e4966745c51649176dc141fea54d41bb8c0362e9005e88234dfc5f3ecb", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "25000000000000000", + "initialBuyTokenAmount": "17929484825085125456919282", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe69c85c6977f5f8b07b8886a7e8543eaa1f35804731265003a13d76aed737030:0x61c99ec8494565774d05abc959ad992faf340e7dd56c5ab63f301d716161a85b:100", + "liquidityOccurrenceId": "1:0xe69c85c6977f5f8b07b8886a7e8543eaa1f35804731265003a13d76aed737030:0x61c99ec8494565774d05abc959ad992faf340e7dd56c5ab63f301d716161a85b:101", + "initialBuyOccurrenceId": "1:0xe69c85c6977f5f8b07b8886a7e8543eaa1f35804731265003a13d76aed737030:0x61c99ec8494565774d05abc959ad992faf340e7dd56c5ab63f301d716161a85b:102", + "custodyOccurrenceId": "1:0xe69c85c6977f5f8b07b8886a7e8543eaa1f35804731265003a13d76aed737030:0x61c99ec8494565774d05abc959ad992faf340e7dd56c5ab63f301d716161a85b:103", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646382" + }, + { + "id": "1:classic-v3:0x9c4a5abf24c86b8ce6cb7d4e7be83daafe6d94ded1903a1b05acceaefb091dd9", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x9c4a5abf24c86b8ce6cb7d4e7be83daafe6d94ded1903a1b05acceaefb091dd9", + "token": "0x0148174a1bae35be8324ea3ea0e4fc3d778ea2ce", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0x9949e8d9dee9e6a7c96742066ec2a18f8e4d287c795e7b7c40f182a705f55583", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1ad9c2417e105650f1e69ae7e8d76fd518367188", + "positionRecipient": "0xb47e59e0601d8f32603dd9ee8ec5b49918fb7011", + "positionTokenId": "355522", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xe42bc28dde1043ee10c305184ec4273ba91718082a5caf4b46150580a2c7bdd9", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x3715772eb14b5b8d312b4e72f0b7cc4b58effe555ec0d3126776fcb51615315f:673", + "liquidityOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x3715772eb14b5b8d312b4e72f0b7cc4b58effe555ec0d3126776fcb51615315f:674", + "initialBuyOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x3715772eb14b5b8d312b4e72f0b7cc4b58effe555ec0d3126776fcb51615315f:675", + "custodyOccurrenceId": "1:0x7579c6291529d0696b40349343455902d0c98a7509c530ed78f2d4f1af3f5396:0x3715772eb14b5b8d312b4e72f0b7cc4b58effe555ec0d3126776fcb51615315f:676", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645401" + }, + { + "id": "1:classic-v3:0x9cc8be9bfd9c4ca32e838b8aa94c3458f51e641b23fcf91e8a89ae6bb3199aa4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x9cc8be9bfd9c4ca32e838b8aa94c3458f51e641b23fcf91e8a89ae6bb3199aa4", + "token": "0xfa751839645015c36e4756e842353711777f730d", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0xf3716aa83d3f3a8a772c3ef542cb26539af07ff4bc868c3c49fd7b0911304b89", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6bb687c453f73ec3af9dc8d564ae4f428b4cfd64", + "positionRecipient": "0x710d387a266f23b276df5e216b69fc387a2defe9", + "positionTokenId": "355865", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x05008f38c0fe2f8d73f9f66a3519f0dd24b5e0fbd6bc6dc7683162ebac84922d", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xbf695ed96ea13823c0312e9eb0186a6893b78a6a494c206e207419c3f9cc033f:0xa50ffd32c07631b07e4e7f445017332a3c17634be1b10e00c074b5dda3cfc0d3:706", + "liquidityOccurrenceId": "1:0xbf695ed96ea13823c0312e9eb0186a6893b78a6a494c206e207419c3f9cc033f:0xa50ffd32c07631b07e4e7f445017332a3c17634be1b10e00c074b5dda3cfc0d3:707", + "initialBuyOccurrenceId": "1:0xbf695ed96ea13823c0312e9eb0186a6893b78a6a494c206e207419c3f9cc033f:0xa50ffd32c07631b07e4e7f445017332a3c17634be1b10e00c074b5dda3cfc0d3:708", + "custodyOccurrenceId": "1:0xbf695ed96ea13823c0312e9eb0186a6893b78a6a494c206e207419c3f9cc033f:0xa50ffd32c07631b07e4e7f445017332a3c17634be1b10e00c074b5dda3cfc0d3:709", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646051" + }, + { + "id": "1:classic-v3:0x9e097d6d9e2946660966155da96cd524d7bcad84c04c0371716f0ddcae000745", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0x9e097d6d9e2946660966155da96cd524d7bcad84c04c0371716f0ddcae000745", + "token": "0xbf2f0b622eb668b069e84407d761d6af337ceb03", + "creator": "0xea573d1f12ffda1191ead5dfdd5705d36f077777", + "quoteAsset": null, + "poolId": "0x65c53a643cd84d4ba6de24518999ea39045b6117ed8755429de13825a345d1d5", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xfea82c2f273bcf6349d464e64ada745c94c5a3b7", + "positionRecipient": "0x6e7dcfbcf1bfe64c8d53148fe83b8df007b99438", + "positionTokenId": "356116", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xd23c26dce5004db101401a9c7c1fe6ff629ab170b00a2331c2193016314f5e63", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x646a5a5de59d9e2f14eb44a1167b2de129cec36c70cc90721eba92fe71414bcd:0x827aa9caf33256c78d79f3ed81d263f68ab516fb6e8fe2485041c0f37800dcf0:662", + "liquidityOccurrenceId": "1:0x646a5a5de59d9e2f14eb44a1167b2de129cec36c70cc90721eba92fe71414bcd:0x827aa9caf33256c78d79f3ed81d263f68ab516fb6e8fe2485041c0f37800dcf0:663", + "initialBuyOccurrenceId": "1:0x646a5a5de59d9e2f14eb44a1167b2de129cec36c70cc90721eba92fe71414bcd:0x827aa9caf33256c78d79f3ed81d263f68ab516fb6e8fe2485041c0f37800dcf0:664", + "custodyOccurrenceId": "1:0x646a5a5de59d9e2f14eb44a1167b2de129cec36c70cc90721eba92fe71414bcd:0x827aa9caf33256c78d79f3ed81d263f68ab516fb6e8fe2485041c0f37800dcf0:665", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646939" + }, + { + "id": "1:classic-v3:0xa1b040d06a14477c66bf96ce1e63d190852bf2ecec90ed058be4927c2c828e0e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xa1b040d06a14477c66bf96ce1e63d190852bf2ecec90ed058be4927c2c828e0e", + "token": "0x2e6816a1fe02e1b8c147f78f747ee04600f33de5", + "creator": "0xbfb97bdbeca8b9c58c882f5ab226d919a35b0a76", + "quoteAsset": null, + "poolId": "0x42a642af421c72df5bdd3acfdf5dc8c454b63b269715280d46676b43a836a7d8", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x5a75172c075f5ab381a23003d1c382fbce1d5217", + "positionRecipient": "0xfaf75e9500907d93603f54d1e76a38b5bea5a07a", + "positionTokenId": "355763", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x1e0d6484db14ae44e305d16a840116cf7dc10fad30b80a3c9cbfb36d4ad056f3", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x481a30a07dfd60742a3b2b875b2b10029d56d93c71e742e9b23e5e4cbe8cb542:0x40e328372e04a3d9110ae852f25e27dc650ebd2cc209e31cfd6f05a7da83872e:18", + "liquidityOccurrenceId": "1:0x481a30a07dfd60742a3b2b875b2b10029d56d93c71e742e9b23e5e4cbe8cb542:0x40e328372e04a3d9110ae852f25e27dc650ebd2cc209e31cfd6f05a7da83872e:19", + "initialBuyOccurrenceId": "1:0x481a30a07dfd60742a3b2b875b2b10029d56d93c71e742e9b23e5e4cbe8cb542:0x40e328372e04a3d9110ae852f25e27dc650ebd2cc209e31cfd6f05a7da83872e:20", + "custodyOccurrenceId": "1:0x481a30a07dfd60742a3b2b875b2b10029d56d93c71e742e9b23e5e4cbe8cb542:0x40e328372e04a3d9110ae852f25e27dc650ebd2cc209e31cfd6f05a7da83872e:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645855" + }, + { + "id": "1:classic-v3:0xa2e0c1edae5c56a759d7465c544e942b8a3f72b36c7595e34784d1fcba302374", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xa2e0c1edae5c56a759d7465c544e942b8a3f72b36c7595e34784d1fcba302374", + "token": "0x445d983acc9e04dcfb7498289f019f158b161b08", + "creator": "0x57b1bc0f4e811b29262b4cceb276c77c1b78e45e", + "quoteAsset": null, + "poolId": "0x217431736eca2c6f3ca6568fda66bdc4500719133d0824b670d4fc95e5e80d5c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x23c8f6ed13f3ced42b86e6cc3f0ba38fb37e0879", + "positionRecipient": "0xe58e7d0e2d3b2264d51cce2c354906e005f87d75", + "positionTokenId": "357102", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x24bfb9bd6b54d72b4d09f3dd60bad034c5da0f663c82a0d7726ba3ee76212b9b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x92c30c721d806624d6d721a1c78fa03dd8ce6c302f63a6a5c04aa37c4e9e25c4:0x98bc41ee36c7d19519ba1c0bee0f84613ddbaabb515d5dfc46338a6197b8abb6:199", + "liquidityOccurrenceId": "1:0x92c30c721d806624d6d721a1c78fa03dd8ce6c302f63a6a5c04aa37c4e9e25c4:0x98bc41ee36c7d19519ba1c0bee0f84613ddbaabb515d5dfc46338a6197b8abb6:200", + "initialBuyOccurrenceId": "1:0x92c30c721d806624d6d721a1c78fa03dd8ce6c302f63a6a5c04aa37c4e9e25c4:0x98bc41ee36c7d19519ba1c0bee0f84613ddbaabb515d5dfc46338a6197b8abb6:201", + "custodyOccurrenceId": "1:0x92c30c721d806624d6d721a1c78fa03dd8ce6c302f63a6a5c04aa37c4e9e25c4:0x98bc41ee36c7d19519ba1c0bee0f84613ddbaabb515d5dfc46338a6197b8abb6:202", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25651179" + }, + { + "id": "1:classic-v3:0xa4d6270fa2df64d527f76e7d1a3b08d6d749581a162b28ac8169c4690b88f3bd", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xa4d6270fa2df64d527f76e7d1a3b08d6d749581a162b28ac8169c4690b88f3bd", + "token": "0x74b2439f72b14aefccefb20b53d9cb35b58d8adf", + "creator": "0x111b15200390ab8744bf7d398e0689bcff9b2b9f", + "quoteAsset": null, + "poolId": "0xe77a80329b4b2d3adaf1ae17abf61235bb13c2dd8592d4474a79a7ac292663e6", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x56cf1885659fde5f6a8d21bb9401f5f1a1545760", + "positionRecipient": "0x4a7ae1a37a41ce00f4bec719e5960351f38dcf19", + "positionTokenId": "357910", + "totalSwapFeeBps": null, + "buySwapFeeBps": 500, + "sellSwapFeeBps": 500, + "rewardConfigurationHash": "0x44f23a54ffcdd046b8626329a83e31e5628ec81ea80372b6dcb69f4e2d16aa04", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "6958902679726813541206176", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x3aa43e9e14c27337b7043c4a940e50190883aa68f5fe86d50a24142a35fedb04:0x62ea0f7212bbc1c96485445c9118adf93b95d0e9141b058a0747422af49e8ecb:597", + "liquidityOccurrenceId": "1:0x3aa43e9e14c27337b7043c4a940e50190883aa68f5fe86d50a24142a35fedb04:0x62ea0f7212bbc1c96485445c9118adf93b95d0e9141b058a0747422af49e8ecb:598", + "initialBuyOccurrenceId": "1:0x3aa43e9e14c27337b7043c4a940e50190883aa68f5fe86d50a24142a35fedb04:0x62ea0f7212bbc1c96485445c9118adf93b95d0e9141b058a0747422af49e8ecb:599", + "custodyOccurrenceId": "1:0x3aa43e9e14c27337b7043c4a940e50190883aa68f5fe86d50a24142a35fedb04:0x62ea0f7212bbc1c96485445c9118adf93b95d0e9141b058a0747422af49e8ecb:600", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25655236" + }, + { + "id": "1:classic-v3:0xa7ab017d8a4a164f36fe2f9226032307eccd8e62d1e625f2e47eb78cd6d67ca6", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xa7ab017d8a4a164f36fe2f9226032307eccd8e62d1e625f2e47eb78cd6d67ca6", + "token": "0x8e51721b8b28117d7f4e13d525c04e270beda13b", + "creator": "0x6cab818daaf4040476e2489a7ed29ca685ad0623", + "quoteAsset": null, + "poolId": "0x378e30558def5c2fb5ececea091ebc801699bd5e63e68edc7166e2990199b176", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7c839f86ab8b99467671c591008f73eeb555fc45", + "positionRecipient": "0xea19a2aec6ec22071827fb18f1c1c41888232c82", + "positionTokenId": "356140", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x6ca6aca7d87cee44dd5b8e18b8cfc05cbf6b44e76d1b856913716109031103fb", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x6af3e11b72d467f1c9c79bc08759cc78404e919beddbc94679d7d9e64f28e216:0xb2da67cc7673d0bb93709a7d8ee19e638ff54e894ba23292d9338d85c5c924e9:74", + "liquidityOccurrenceId": "1:0x6af3e11b72d467f1c9c79bc08759cc78404e919beddbc94679d7d9e64f28e216:0xb2da67cc7673d0bb93709a7d8ee19e638ff54e894ba23292d9338d85c5c924e9:75", + "initialBuyOccurrenceId": "1:0x6af3e11b72d467f1c9c79bc08759cc78404e919beddbc94679d7d9e64f28e216:0xb2da67cc7673d0bb93709a7d8ee19e638ff54e894ba23292d9338d85c5c924e9:76", + "custodyOccurrenceId": "1:0x6af3e11b72d467f1c9c79bc08759cc78404e919beddbc94679d7d9e64f28e216:0xb2da67cc7673d0bb93709a7d8ee19e638ff54e894ba23292d9338d85c5c924e9:77", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646979" + }, + { + "id": "1:classic-v3:0xa9cebe472e1711928ce3c40cbeeb3985493361095d4468a5e3682617041b6bb2", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xa9cebe472e1711928ce3c40cbeeb3985493361095d4468a5e3682617041b6bb2", + "token": "0xd04e0d583abb59ea286f68a06768fb410c2f2801", + "creator": "0xe3bde8d07b156660970c6bd36294024a6df3a734", + "quoteAsset": null, + "poolId": "0x4f7ee92d8fb9d12b7e981658e72d31bd9c5e0448d676ac8aa8c1ba576f7dd7f0", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x40f1ad8ee521eafb2cc778045afe17b2a57bbe50", + "positionRecipient": "0x277d31f0c953d1cb1717587b9d5ec53acf98689b", + "positionTokenId": "355676", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x4f7615bac5ecf5c21c160f39c0341a31f53b016757dc04c9b5744366e4dd1a33", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "70000000000000000", + "initialBuyTokenAmount": "48633020504595009332144161", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa431057d4d18339f5117929108cb664ef46c20a7371b6c6a4e7b50e6c2ee7001:0x76d707802d70d56d66bd7cc298420060cd656855fb266214b485f52796bc7931:46", + "liquidityOccurrenceId": "1:0xa431057d4d18339f5117929108cb664ef46c20a7371b6c6a4e7b50e6c2ee7001:0x76d707802d70d56d66bd7cc298420060cd656855fb266214b485f52796bc7931:47", + "initialBuyOccurrenceId": "1:0xa431057d4d18339f5117929108cb664ef46c20a7371b6c6a4e7b50e6c2ee7001:0x76d707802d70d56d66bd7cc298420060cd656855fb266214b485f52796bc7931:48", + "custodyOccurrenceId": "1:0xa431057d4d18339f5117929108cb664ef46c20a7371b6c6a4e7b50e6c2ee7001:0x76d707802d70d56d66bd7cc298420060cd656855fb266214b485f52796bc7931:49", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645669" + }, + { + "id": "1:classic-v3:0xac28f6879ce64064449c059b3666187814afdcf1fc110e1afb9945fc34865dfb", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xac28f6879ce64064449c059b3666187814afdcf1fc110e1afb9945fc34865dfb", + "token": "0xadd92b46b90c93d682e0d9e9bb1cda0a7b4d46e7", + "creator": "0xd49b8997ad2247a6b9059e3f7f0f95536c2aa92f", + "quoteAsset": null, + "poolId": "0xe0dad81d54d64dee4aa68e2c0fd2b9ab4f4b028708967f4adcf02cd0fe212f6f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xf91dd5c5a0e33ff3a86b966cfde9c32e806fba66", + "positionRecipient": "0x32b54aed6749fa8a4a49b44f07bb49fa26b0d6be", + "positionTokenId": "357575", + "totalSwapFeeBps": null, + "buySwapFeeBps": 300, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x016b1a2abb12dce2387f2bbed636f449d701d201742080b6113668d1d5c93259", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "3000000000000000", + "initialBuyTokenAmount": "2141961618645944479654358", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x58f82583a05b753c377718358cfd77458226c4e04a15ddc1e50ebcee2ce35c11:0x965d903400bf291dd37916efc5fb8965fed751b561d6d0fbd303c819511d3ece:85", + "liquidityOccurrenceId": "1:0x58f82583a05b753c377718358cfd77458226c4e04a15ddc1e50ebcee2ce35c11:0x965d903400bf291dd37916efc5fb8965fed751b561d6d0fbd303c819511d3ece:86", + "initialBuyOccurrenceId": "1:0x58f82583a05b753c377718358cfd77458226c4e04a15ddc1e50ebcee2ce35c11:0x965d903400bf291dd37916efc5fb8965fed751b561d6d0fbd303c819511d3ece:87", + "custodyOccurrenceId": "1:0x58f82583a05b753c377718358cfd77458226c4e04a15ddc1e50ebcee2ce35c11:0x965d903400bf291dd37916efc5fb8965fed751b561d6d0fbd303c819511d3ece:88", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25653666" + }, + { + "id": "1:classic-v3:0xaccdf83a7871f892452ae5d34746db339e9725e88327356940f2b3501db46734", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xaccdf83a7871f892452ae5d34746db339e9725e88327356940f2b3501db46734", + "token": "0x7bd2e6aac44c84f56bfa4d15b05c164ffd8bf90a", + "creator": "0x5c7917ab56f13a0aada0765559b1e53593900231", + "quoteAsset": null, + "poolId": "0xefd3ad2830a1c13e347b0865c477c512f384adeb0918a73e133393f9c109629f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xae1517931c8c6d0d29dbff6d5ffcc62b29e2afe9", + "positionRecipient": "0x5b9407ab720e4238cf3b75c0b6c5d555da47b296", + "positionTokenId": "354599", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x561d5632a1e26729c37a7d60747c7fcc36d94ffbdd5dcacfd44e175bb0c6d60b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xca0c84804770622c90ff4e1fcee25857daf99539a2995bc3c9b3527cecee217a:0x1c50f00dcce62ccd762574ff46e6eba1becec2bd7e4fff567b4bf618e2609700:1221", + "liquidityOccurrenceId": "1:0xca0c84804770622c90ff4e1fcee25857daf99539a2995bc3c9b3527cecee217a:0x1c50f00dcce62ccd762574ff46e6eba1becec2bd7e4fff567b4bf618e2609700:1222", + "initialBuyOccurrenceId": "1:0xca0c84804770622c90ff4e1fcee25857daf99539a2995bc3c9b3527cecee217a:0x1c50f00dcce62ccd762574ff46e6eba1becec2bd7e4fff567b4bf618e2609700:1223", + "custodyOccurrenceId": "1:0xca0c84804770622c90ff4e1fcee25857daf99539a2995bc3c9b3527cecee217a:0x1c50f00dcce62ccd762574ff46e6eba1becec2bd7e4fff567b4bf618e2609700:1224", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640357" + }, + { + "id": "1:classic-v3:0xae0ddc0ab3788f502daa7ffa92922a76ba21f3886915293f64ebb838f1638bc3", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xae0ddc0ab3788f502daa7ffa92922a76ba21f3886915293f64ebb838f1638bc3", + "token": "0x7881aff839332c1b1de31108a65f08a8cbacbd2d", + "creator": "0x5c7917ab56f13a0aada0765559b1e53593900231", + "quoteAsset": null, + "poolId": "0x9a2189d3cbac41579657fe96d68b775c192674c2fef8a55cf3da27995177f49d", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6db936abc9beff1b0418e2a8ab1b3905f2ff4b8f", + "positionRecipient": "0xdddc889117719caa1c1cd1652ece0c6b058086ba", + "positionTokenId": "354644", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x13a6df94198d97546f19ad69c3ff24f6cceec79199a7d006c3d2c562b678ec64", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe8b0c6381e9dda125750eddf9191db81be44aacf426329cb1bfed59866cf75c4:0x8076b99151ba6f8e0ccbb78100bb19ef3e7062e302e070ed5699e3646abbf4fd:746", + "liquidityOccurrenceId": "1:0xe8b0c6381e9dda125750eddf9191db81be44aacf426329cb1bfed59866cf75c4:0x8076b99151ba6f8e0ccbb78100bb19ef3e7062e302e070ed5699e3646abbf4fd:747", + "initialBuyOccurrenceId": "1:0xe8b0c6381e9dda125750eddf9191db81be44aacf426329cb1bfed59866cf75c4:0x8076b99151ba6f8e0ccbb78100bb19ef3e7062e302e070ed5699e3646abbf4fd:748", + "custodyOccurrenceId": "1:0xe8b0c6381e9dda125750eddf9191db81be44aacf426329cb1bfed59866cf75c4:0x8076b99151ba6f8e0ccbb78100bb19ef3e7062e302e070ed5699e3646abbf4fd:749", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640525" + }, + { + "id": "1:classic-v3:0xae4bbedf0a0d3bb1de3c70ec0e0a40d6ae2d8524a8c43e9a79b1ec01230241f4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xae4bbedf0a0d3bb1de3c70ec0e0a40d6ae2d8524a8c43e9a79b1ec01230241f4", + "token": "0x26e9034815bf1bcc5e383a7c49b810becaf28755", + "creator": "0x48eb4042667b193894030372b69a69653ce605d7", + "quoteAsset": null, + "poolId": "0xa5c37600e47b98032066f139cc445edcae63e79fdbf05d4ab8346a53982e1203", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa47ffa50128c07dce35d446dac4891e732f30767", + "positionRecipient": "0x7eeae0576ac0da10f901e391c19edbbd183a4650", + "positionTokenId": "355711", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x61665f1cc2f4fbdb847848c0376cdf702d1e1c1643f0cfbb36ea9661edf8d043", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "30000000000000000", + "initialBuyTokenAmount": "21438505518229829458161070", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4c9ed5f6c7339f3d8f09c3bf9045fefa529414174872dfff4203e967dc29191d:0xdb021dbcb91f3067ab45e08ba46add694c7f4f9e0c4635bbe50f2636a002919e:312", + "liquidityOccurrenceId": "1:0x4c9ed5f6c7339f3d8f09c3bf9045fefa529414174872dfff4203e967dc29191d:0xdb021dbcb91f3067ab45e08ba46add694c7f4f9e0c4635bbe50f2636a002919e:313", + "initialBuyOccurrenceId": "1:0x4c9ed5f6c7339f3d8f09c3bf9045fefa529414174872dfff4203e967dc29191d:0xdb021dbcb91f3067ab45e08ba46add694c7f4f9e0c4635bbe50f2636a002919e:314", + "custodyOccurrenceId": "1:0x4c9ed5f6c7339f3d8f09c3bf9045fefa529414174872dfff4203e967dc29191d:0xdb021dbcb91f3067ab45e08ba46add694c7f4f9e0c4635bbe50f2636a002919e:315", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645719" + }, + { + "id": "1:classic-v3:0xae741bd0d616e47b3c3d06b6928f66c20f01fcfb0738b6aa07caaea9f3f6e5ee", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xae741bd0d616e47b3c3d06b6928f66c20f01fcfb0738b6aa07caaea9f3f6e5ee", + "token": "0xa1aa5a6594e9d07f342e5be70f8db998f38ea590", + "creator": "0x2e1c62353efb3743729e1af6c234b32cc4efaf85", + "quoteAsset": null, + "poolId": "0x46b10156add0c717b2dd445ff552732b5bc8326e15c57e3d4a4e685916d2330a", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x8de5bb7baa9a5d2dd21465208ecc8d44c68bb233", + "positionRecipient": "0x203e90c56ad760558543529442555d97dfc84442", + "positionTokenId": "355855", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xdae6f961491341ab1b79fa993d487391c50e2d0cf7af22c15928fe5d85944e6c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x568853963396951e5cbd2968eb3241c4f59438c727eb52cfc716d8a7cfd7d65c:0x9a9e7882fb91ebd11c0e6ad6743591167a7b26ec9c2b97c0073c255843232a44:250", + "liquidityOccurrenceId": "1:0x568853963396951e5cbd2968eb3241c4f59438c727eb52cfc716d8a7cfd7d65c:0x9a9e7882fb91ebd11c0e6ad6743591167a7b26ec9c2b97c0073c255843232a44:251", + "initialBuyOccurrenceId": "1:0x568853963396951e5cbd2968eb3241c4f59438c727eb52cfc716d8a7cfd7d65c:0x9a9e7882fb91ebd11c0e6ad6743591167a7b26ec9c2b97c0073c255843232a44:252", + "custodyOccurrenceId": "1:0x568853963396951e5cbd2968eb3241c4f59438c727eb52cfc716d8a7cfd7d65c:0x9a9e7882fb91ebd11c0e6ad6743591167a7b26ec9c2b97c0073c255843232a44:253", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646012" + }, + { + "id": "1:classic-v3:0xaf072ee04c4fc522551047a6f334d8cd7be5c51b88d022e9e8a583a01c7ee582", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xaf072ee04c4fc522551047a6f334d8cd7be5c51b88d022e9e8a583a01c7ee582", + "token": "0x54632bb6da224526a067941486126515420c2a05", + "creator": "0x4762b949c1341572ea4f68fc1157c917774504d5", + "quoteAsset": null, + "poolId": "0xfd549144095906311e209eba8e9613d3e4721c109732210c9bf95b5f202882de", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xe93624aebb786a97627ca1bdfbc998d5df530482", + "positionRecipient": "0xb6fea2e5e1e7125f9d9cbd6b81887ee98cb33f5d", + "positionTokenId": "355588", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xed6aa78bd6379b71d82976c6f5f829d6c1dd532a848fc2ce3cb367a225c1589a", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "30000000000000000", + "initialBuyTokenAmount": "21438505518229829458161070", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa73171bd83caae81ee311aa4f84d5b4fa2c01dbe7db57a1b702465b49ca71293:0x820d15bbb540f9cd89d2cb5b97bda6da00bf1fd3dbb25cefce449c2b10587436:128", + "liquidityOccurrenceId": "1:0xa73171bd83caae81ee311aa4f84d5b4fa2c01dbe7db57a1b702465b49ca71293:0x820d15bbb540f9cd89d2cb5b97bda6da00bf1fd3dbb25cefce449c2b10587436:129", + "initialBuyOccurrenceId": "1:0xa73171bd83caae81ee311aa4f84d5b4fa2c01dbe7db57a1b702465b49ca71293:0x820d15bbb540f9cd89d2cb5b97bda6da00bf1fd3dbb25cefce449c2b10587436:130", + "custodyOccurrenceId": "1:0xa73171bd83caae81ee311aa4f84d5b4fa2c01dbe7db57a1b702465b49ca71293:0x820d15bbb540f9cd89d2cb5b97bda6da00bf1fd3dbb25cefce449c2b10587436:131", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645529" + }, + { + "id": "1:classic-v3:0xb023d584e9426e5f25163f8ee25d87ed35ba896c6fb3d3038cb2146d91650276", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xb023d584e9426e5f25163f8ee25d87ed35ba896c6fb3d3038cb2146d91650276", + "token": "0x73f40b594ae47479e4fdc479894f27fdc9091356", + "creator": "0x90142730403c87b503c7398e8e1be447a45bf48b", + "quoteAsset": null, + "poolId": "0xa3bc0d0cd1dcc6684294c5653544eac48dbb5df1f88d0b6ab41c960dd528426f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x99e029e7778e81be65334495330c42099c7f43d6", + "positionRecipient": "0x639d4bfdcc187a78ab49a8cb3b78a2df82fa1e35", + "positionTokenId": "355513", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0x81379084985b91cdedee06607ba4bb7be0c127edb2b3bee966ddf5ef89e9749c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "5000000000000000", + "initialBuyTokenAmount": "3601464150303497561197899", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb3be4b5d668e4372c72fe570d85f7f8bc7dfb314c97fc955102d656afb2c59d3:0x9a9fcb16b7bcb1f6e2942b26b1ffa04be4720895784445d0f271aab19a271d74:280", + "liquidityOccurrenceId": "1:0xb3be4b5d668e4372c72fe570d85f7f8bc7dfb314c97fc955102d656afb2c59d3:0x9a9fcb16b7bcb1f6e2942b26b1ffa04be4720895784445d0f271aab19a271d74:281", + "initialBuyOccurrenceId": "1:0xb3be4b5d668e4372c72fe570d85f7f8bc7dfb314c97fc955102d656afb2c59d3:0x9a9fcb16b7bcb1f6e2942b26b1ffa04be4720895784445d0f271aab19a271d74:282", + "custodyOccurrenceId": "1:0xb3be4b5d668e4372c72fe570d85f7f8bc7dfb314c97fc955102d656afb2c59d3:0x9a9fcb16b7bcb1f6e2942b26b1ffa04be4720895784445d0f271aab19a271d74:283", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645375" + }, + { + "id": "1:classic-v3:0xb14cb0cd14d5629565d185da5aa030798fdcebbf78e1ee7c56d17ed0318cfcc9", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xb14cb0cd14d5629565d185da5aa030798fdcebbf78e1ee7c56d17ed0318cfcc9", + "token": "0xa36685f8ba01d91b75b34e0982626cdc7598024c", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0xe78afd22f8252f4d522c4e28c0f0b395a342c4fad8930b116f1c9b5179a802cb", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa7ef190eca95c75ac401605ffb3f35c734217f4d", + "positionRecipient": "0xfe2fdb7d042ba64db5b995f53a357203a4b461c3", + "positionTokenId": "355587", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xb9c2bb4edacff862ff84f28a5da2917a70f3b78e97bc62b22a61fd6000099b20", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb81c9213e5ef42a27f159d2e2a3bce6015e9b49c3e5f2e24bcafa6d4c3e388d8:0xf26624ff444f2459c00b84f3dad2bde3563e067604b6a4f8a5b989c262f2c301:829", + "liquidityOccurrenceId": "1:0xb81c9213e5ef42a27f159d2e2a3bce6015e9b49c3e5f2e24bcafa6d4c3e388d8:0xf26624ff444f2459c00b84f3dad2bde3563e067604b6a4f8a5b989c262f2c301:830", + "initialBuyOccurrenceId": "1:0xb81c9213e5ef42a27f159d2e2a3bce6015e9b49c3e5f2e24bcafa6d4c3e388d8:0xf26624ff444f2459c00b84f3dad2bde3563e067604b6a4f8a5b989c262f2c301:831", + "custodyOccurrenceId": "1:0xb81c9213e5ef42a27f159d2e2a3bce6015e9b49c3e5f2e24bcafa6d4c3e388d8:0xf26624ff444f2459c00b84f3dad2bde3563e067604b6a4f8a5b989c262f2c301:832", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645528" + }, + { + "id": "1:classic-v3:0xb2f2aaed9bade3f0aacf2887c5c928806b2ec146fa07be00e40b156b3e60ca72", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xb2f2aaed9bade3f0aacf2887c5c928806b2ec146fa07be00e40b156b3e60ca72", + "token": "0xcfd14befdda10f6d6f0180f50645aef7cce83844", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x2bcb2c4cf53496285c13f5bbf41f5df6a8278f1d95dcd9cc089343edb527498f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x56ba2cf8a91abb196f7b5dcb504cf719bf885e6b", + "positionRecipient": "0x810458a8d1059326021b9b2109196473237f5631", + "positionTokenId": "355912", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x7c27b1b057aa2d9c19efdc4c176521f0e34c5c2adf343c178e5471f643f49b8e", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4b3cf4cc8b14d57d52404e6c089c66f44cb0e668672a5eb755da35ddf2ac9235:0xa270663fbc3fec0b662263a402afd0000943733c4e4431de70448258911d0799:357", + "liquidityOccurrenceId": "1:0x4b3cf4cc8b14d57d52404e6c089c66f44cb0e668672a5eb755da35ddf2ac9235:0xa270663fbc3fec0b662263a402afd0000943733c4e4431de70448258911d0799:358", + "initialBuyOccurrenceId": "1:0x4b3cf4cc8b14d57d52404e6c089c66f44cb0e668672a5eb755da35ddf2ac9235:0xa270663fbc3fec0b662263a402afd0000943733c4e4431de70448258911d0799:359", + "custodyOccurrenceId": "1:0x4b3cf4cc8b14d57d52404e6c089c66f44cb0e668672a5eb755da35ddf2ac9235:0xa270663fbc3fec0b662263a402afd0000943733c4e4431de70448258911d0799:360", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646148" + }, + { + "id": "1:classic-v3:0xb36f1ac8cd916f2727f0309d3f53f9454afdf1f3fec3cff2c9fae0f7c2a68ead", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xb36f1ac8cd916f2727f0309d3f53f9454afdf1f3fec3cff2c9fae0f7c2a68ead", + "token": "0x2674d3e7755c9a890e07392eebd6f9cec9881f64", + "creator": "0xa5a715cb75281e6e6dc07c3de37f4f8f867f6fbb", + "quoteAsset": null, + "poolId": "0x7c0dc44e2303f78eba0695752743dc5ff6d0cbdd535dbf8264202fa58002a1a4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x9cd1c1a4d2b156989a87f0d802c44811283f7909", + "positionRecipient": "0xdcb769e989b5d5e4a92655e03b9813a7a90ded16", + "positionTokenId": "355700", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x1ab2b969766bfb0c25b68d462c45b724a3d509d5d6864c9c34d46b1372510f71", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x34424fb9b014e4c16c680039db499c9c3cb8fc25afb7cdfbe74b53ecfd05af4a:0x8fe7e8b688fb49ead916f9bac689986b6685c9e30b9c869fa32f35f90b2bc392:551", + "liquidityOccurrenceId": "1:0x34424fb9b014e4c16c680039db499c9c3cb8fc25afb7cdfbe74b53ecfd05af4a:0x8fe7e8b688fb49ead916f9bac689986b6685c9e30b9c869fa32f35f90b2bc392:552", + "initialBuyOccurrenceId": "1:0x34424fb9b014e4c16c680039db499c9c3cb8fc25afb7cdfbe74b53ecfd05af4a:0x8fe7e8b688fb49ead916f9bac689986b6685c9e30b9c869fa32f35f90b2bc392:553", + "custodyOccurrenceId": "1:0x34424fb9b014e4c16c680039db499c9c3cb8fc25afb7cdfbe74b53ecfd05af4a:0x8fe7e8b688fb49ead916f9bac689986b6685c9e30b9c869fa32f35f90b2bc392:554", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645699" + }, + { + "id": "1:classic-v3:0xb825cc7dffe05ee0df3e2024a8f311fe4d8cee0e25a67389339c1b7c9b316371", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xb825cc7dffe05ee0df3e2024a8f311fe4d8cee0e25a67389339c1b7c9b316371", + "token": "0xaeb0086762a6875f57649e476bc98cbe97fcb41b", + "creator": "0x90142730403c87b503c7398e8e1be447a45bf48b", + "quoteAsset": null, + "poolId": "0x79f72d422d3d10a7ca5e655c69232d82dc3fbdeb0319511872e69e037fe2f6e1", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1824b34b9a1299f309b806927ec67418894f09a9", + "positionRecipient": "0xa7c82cbc7575dd12480d8dc1a78cde6f8e80a0c0", + "positionTokenId": "355500", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0x6de6e01d76a2453d9f06241e44de4bbec9c8f953c50c262a0dfc4c8f9f1079b7", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7177080303191202649091171", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xe3a2857ed6b5987c9eb66b978c7dfc0dc39df1ea3ad1f690d6d693d2594f4df4:0x0d1bba2b874e908645fcc3acf023fd1f45fca9b36031f750a588a28342c51bcd:18", + "liquidityOccurrenceId": "1:0xe3a2857ed6b5987c9eb66b978c7dfc0dc39df1ea3ad1f690d6d693d2594f4df4:0x0d1bba2b874e908645fcc3acf023fd1f45fca9b36031f750a588a28342c51bcd:19", + "initialBuyOccurrenceId": "1:0xe3a2857ed6b5987c9eb66b978c7dfc0dc39df1ea3ad1f690d6d693d2594f4df4:0x0d1bba2b874e908645fcc3acf023fd1f45fca9b36031f750a588a28342c51bcd:20", + "custodyOccurrenceId": "1:0xe3a2857ed6b5987c9eb66b978c7dfc0dc39df1ea3ad1f690d6d693d2594f4df4:0x0d1bba2b874e908645fcc3acf023fd1f45fca9b36031f750a588a28342c51bcd:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645345" + }, + { + "id": "1:classic-v3:0xb8da64776cbb9b188f460ffec323d4ddece0d58313bc4820ee634f69b90ef02c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xb8da64776cbb9b188f460ffec323d4ddece0d58313bc4820ee634f69b90ef02c", + "token": "0xaa5d84f9930e646822600c548a6652d87498671e", + "creator": "0xb38ef2aa4306c8a2ce97d8718aa3dda7a0ba331c", + "quoteAsset": null, + "poolId": "0x3ad19871aacbb4d6a35c58dcd590d5b552065a0e3a120e90424b0ae92bcdaee1", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x99454c56efd508edaad585ff5705e3a1c7c0a3b8", + "positionRecipient": "0x81954cff9532cbbb36deca870fa600634e30d34b", + "positionTokenId": "354740", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x6ed908e9f1abbcaa85d5b054dfb64d0faa81cc189e9ce3087aa030dbaea2794e", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x8a55dc0345008ab6846c9f3801717fb1ab521043eaf4155185970b61d241ee96:0x280c0824a5a539cb143880a06d26010d751199cfc0abdc804957b3679a85c6cd:1449", + "liquidityOccurrenceId": "1:0x8a55dc0345008ab6846c9f3801717fb1ab521043eaf4155185970b61d241ee96:0x280c0824a5a539cb143880a06d26010d751199cfc0abdc804957b3679a85c6cd:1450", + "initialBuyOccurrenceId": "1:0x8a55dc0345008ab6846c9f3801717fb1ab521043eaf4155185970b61d241ee96:0x280c0824a5a539cb143880a06d26010d751199cfc0abdc804957b3679a85c6cd:1451", + "custodyOccurrenceId": "1:0x8a55dc0345008ab6846c9f3801717fb1ab521043eaf4155185970b61d241ee96:0x280c0824a5a539cb143880a06d26010d751199cfc0abdc804957b3679a85c6cd:1452", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25641349" + }, + { + "id": "1:classic-v3:0xbf07e7c3f5ef8252afaed4c614237e714d6f5f8d2df3ee7e78a66671fed6363c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xbf07e7c3f5ef8252afaed4c614237e714d6f5f8d2df3ee7e78a66671fed6363c", + "token": "0xb4a25860ac1f9ebecf48b08fb0139df217c72524", + "creator": "0xc3574b859693105db80400287f3782dd196e93c6", + "quoteAsset": null, + "poolId": "0x923484aa6f3a628f8a87e4ec5e7b74e97b51a417d9bff2d7a89e7178ec404688", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x78851630293b30eff52285f14b398c9d96823a47", + "positionRecipient": "0x10608d8492604f3d0c4c3b6951ad7d3c82cf6d7a", + "positionTokenId": "355590", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xc25af8fa4b37bbd0a4eb0660e8bce78f3fe65808a9fbca5e7b6416da803a62cd", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1d2640d3232e3d5a8d45a79e5a27fdfca89a2f7f036c719c15a88f978b7ec1cb:0xe6d249f50673e7300034d5f3245217566b500c36ca521c6b6de1d93f94b4b902:245", + "liquidityOccurrenceId": "1:0x1d2640d3232e3d5a8d45a79e5a27fdfca89a2f7f036c719c15a88f978b7ec1cb:0xe6d249f50673e7300034d5f3245217566b500c36ca521c6b6de1d93f94b4b902:246", + "initialBuyOccurrenceId": "1:0x1d2640d3232e3d5a8d45a79e5a27fdfca89a2f7f036c719c15a88f978b7ec1cb:0xe6d249f50673e7300034d5f3245217566b500c36ca521c6b6de1d93f94b4b902:247", + "custodyOccurrenceId": "1:0x1d2640d3232e3d5a8d45a79e5a27fdfca89a2f7f036c719c15a88f978b7ec1cb:0xe6d249f50673e7300034d5f3245217566b500c36ca521c6b6de1d93f94b4b902:248", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645534" + }, + { + "id": "1:classic-v3:0xbfb0cf978cc4e26266b96aa49d048521db184034abfc1aa54cd78df5973643d5", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xbfb0cf978cc4e26266b96aa49d048521db184034abfc1aa54cd78df5973643d5", + "token": "0x967815dae5f449bed6176d0b6c334dd1417d2ce7", + "creator": "0x63608970d148491dd494b2529c3cd233eb955229", + "quoteAsset": null, + "poolId": "0x6de0ab9ac2ac74242d21429ebb98aae7cca30290a68b4bee81dd1f03c2a2a023", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xf43fe682bb8cb370b847f5ad386d504688c7e90e", + "positionRecipient": "0xe4e324254e2b7d8e97d167db5554b3bde2b7b466", + "positionTokenId": "354621", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x1fb247f7b94cb641dfcfc6e14871b19ece3bf6f3c14e3c74dc2332fefcacfff5", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "80000000000000000", + "initialBuyTokenAmount": "55197108844362546910510708", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xc888055c73b9aa6f927546f150c1f891e86d824f6ee0549b6616a0585b5f095b:0x5b6e97cdda91d93b107d1489753e201228a01bbccc731e662297c3ddb76e0570:312", + "liquidityOccurrenceId": "1:0xc888055c73b9aa6f927546f150c1f891e86d824f6ee0549b6616a0585b5f095b:0x5b6e97cdda91d93b107d1489753e201228a01bbccc731e662297c3ddb76e0570:313", + "initialBuyOccurrenceId": "1:0xc888055c73b9aa6f927546f150c1f891e86d824f6ee0549b6616a0585b5f095b:0x5b6e97cdda91d93b107d1489753e201228a01bbccc731e662297c3ddb76e0570:314", + "custodyOccurrenceId": "1:0xc888055c73b9aa6f927546f150c1f891e86d824f6ee0549b6616a0585b5f095b:0x5b6e97cdda91d93b107d1489753e201228a01bbccc731e662297c3ddb76e0570:315", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640409" + }, + { + "id": "1:classic-v3:0xc0237da55a4a4685c7a588c80af40b5aeb96e640dfcf0367031d33b949ef3d84", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc0237da55a4a4685c7a588c80af40b5aeb96e640dfcf0367031d33b949ef3d84", + "token": "0xaa346ba40e01de46650e9ca2fb984fbb2be14fa1", + "creator": "0x2bb333d48dfaf1596d9036671d2e43168994249e", + "quoteAsset": null, + "poolId": "0x077582098072469a90b075a27f083c6cabaee36cbda5fd8ba1296c43b11c4ab2", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xf89632873841319ed992a9d4220dd850a9e48f8a", + "positionRecipient": "0x20643bd9bcf1de12f8b873ad7e90b2d4f165e922", + "positionTokenId": "358014", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xb3ab0d64b7d2d69487c66992619d00fd611cf5576514bf8f55ecec067275c1f6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x689c89436dbc7c1254ceb451b689f1cebb60e3ad17c219094f1e0066a295fb72:0x02ac7a0af67760913c84453d31ee67cc01009ed0c7a0f8e65b60b2bd79ff2845:1664", + "liquidityOccurrenceId": "1:0x689c89436dbc7c1254ceb451b689f1cebb60e3ad17c219094f1e0066a295fb72:0x02ac7a0af67760913c84453d31ee67cc01009ed0c7a0f8e65b60b2bd79ff2845:1665", + "initialBuyOccurrenceId": "1:0x689c89436dbc7c1254ceb451b689f1cebb60e3ad17c219094f1e0066a295fb72:0x02ac7a0af67760913c84453d31ee67cc01009ed0c7a0f8e65b60b2bd79ff2845:1666", + "custodyOccurrenceId": "1:0x689c89436dbc7c1254ceb451b689f1cebb60e3ad17c219094f1e0066a295fb72:0x02ac7a0af67760913c84453d31ee67cc01009ed0c7a0f8e65b60b2bd79ff2845:1667", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25655950" + }, + { + "id": "1:classic-v3:0xc0b1070a3f5bccdf6379d10a1d580f7733d69b8b46cd0ffa8bec1f473e8ea2b4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc0b1070a3f5bccdf6379d10a1d580f7733d69b8b46cd0ffa8bec1f473e8ea2b4", + "token": "0xcb6c74a3ebf6fa10b3255e2787f709d4c6014e76", + "creator": "0x90142730403c87b503c7398e8e1be447a45bf48b", + "quoteAsset": null, + "poolId": "0xca4588d36afa188fd35c2744e9288161445a6a4f126832e582b29fd6af3b2e58", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xc3e2726e637e81706ac77a539273a514020e95a9", + "positionRecipient": "0x701559983226738a46f106f2a00b39effee8a4bb", + "positionTokenId": "355585", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0x70e961b52501e5ac46bb6283d5c83e0694eee00572107f03d45b6156c38d9bef", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "4000000000000000", + "initialBuyTokenAmount": "2883248103178803853335382", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xdce8dc98f8a8547c1473dc2d05d02b50b740dcdd3316a4fc89d9ace88057728a:0xf3d9cc3ff6d4b212340598fbae9b2a0bbaddf8cb8b31a914e47c09ad9faca0cc:383", + "liquidityOccurrenceId": "1:0xdce8dc98f8a8547c1473dc2d05d02b50b740dcdd3316a4fc89d9ace88057728a:0xf3d9cc3ff6d4b212340598fbae9b2a0bbaddf8cb8b31a914e47c09ad9faca0cc:384", + "initialBuyOccurrenceId": "1:0xdce8dc98f8a8547c1473dc2d05d02b50b740dcdd3316a4fc89d9ace88057728a:0xf3d9cc3ff6d4b212340598fbae9b2a0bbaddf8cb8b31a914e47c09ad9faca0cc:385", + "custodyOccurrenceId": "1:0xdce8dc98f8a8547c1473dc2d05d02b50b740dcdd3316a4fc89d9ace88057728a:0xf3d9cc3ff6d4b212340598fbae9b2a0bbaddf8cb8b31a914e47c09ad9faca0cc:386", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645518" + }, + { + "id": "1:classic-v3:0xc14de5a7b242154b2ecf802719bba24c4452e4de86a6f9e8cd5e2eef14965b29", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc14de5a7b242154b2ecf802719bba24c4452e4de86a6f9e8cd5e2eef14965b29", + "token": "0x19b9cdbee6d9a05c5a0b147bb530fcace0c3244f", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0xf291ecd7108f07a6e12b1ab4d4c320fad794e6fa5e535886dbea11bd2dcb0c5b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7185c609e8f4bdcf1de62d2d0b2c6867893f1a5c", + "positionRecipient": "0xe4e21d1cb98ffadbb496f49a74d0db3eccea5173", + "positionTokenId": "355731", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xab9910d019a126e055305aa86a7a3a430c15c027bb4fa38ae28ae24a33dd2928", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x394f32b07b4e6a0a46047c6b2dca268fa0dae7d73e67e950b7c159e2ff7e30c7:0xe220c256c4dacc11e3853b487a3a2553284dc7bc73a91cea5789a1889a04c623:78", + "liquidityOccurrenceId": "1:0x394f32b07b4e6a0a46047c6b2dca268fa0dae7d73e67e950b7c159e2ff7e30c7:0xe220c256c4dacc11e3853b487a3a2553284dc7bc73a91cea5789a1889a04c623:79", + "initialBuyOccurrenceId": "1:0x394f32b07b4e6a0a46047c6b2dca268fa0dae7d73e67e950b7c159e2ff7e30c7:0xe220c256c4dacc11e3853b487a3a2553284dc7bc73a91cea5789a1889a04c623:80", + "custodyOccurrenceId": "1:0x394f32b07b4e6a0a46047c6b2dca268fa0dae7d73e67e950b7c159e2ff7e30c7:0xe220c256c4dacc11e3853b487a3a2553284dc7bc73a91cea5789a1889a04c623:81", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645757" + }, + { + "id": "1:classic-v3:0xc45e70335be268cc2320cda853a2039eee161939b50ba49a73a5a020705c10a1", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc45e70335be268cc2320cda853a2039eee161939b50ba49a73a5a020705c10a1", + "token": "0x5a7d5e35697cb341944a899b2c498b063b02c4f9", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0xc6b7926fa7db1fa1de493d9c7e8c88af1cb5e63dff74d9ccba8592d78dabfeda", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6f6d645107a2c3d50f845d1a78dee39cfa666658", + "positionRecipient": "0xdcf631e56463bfe3498267dda2ca5eca3c50c24d", + "positionTokenId": "355796", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xe1acc112fa5158d405a24195bbc6396da5373b583f972e7ef583795aea0f8188", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x780acaad8ac71679d1dc7ebf724348655a5279b177a30fac70f07227978538a8:0x4721500258c749c4afa0aee18985c4bfce0610d81db6db6036d883d03d72bf7b:281", + "liquidityOccurrenceId": "1:0x780acaad8ac71679d1dc7ebf724348655a5279b177a30fac70f07227978538a8:0x4721500258c749c4afa0aee18985c4bfce0610d81db6db6036d883d03d72bf7b:282", + "initialBuyOccurrenceId": "1:0x780acaad8ac71679d1dc7ebf724348655a5279b177a30fac70f07227978538a8:0x4721500258c749c4afa0aee18985c4bfce0610d81db6db6036d883d03d72bf7b:283", + "custodyOccurrenceId": "1:0x780acaad8ac71679d1dc7ebf724348655a5279b177a30fac70f07227978538a8:0x4721500258c749c4afa0aee18985c4bfce0610d81db6db6036d883d03d72bf7b:284", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645929" + }, + { + "id": "1:classic-v3:0xc53f8d24c23e741e69f05f35e78557a9c21115fd87142423792cb21d3d3af6bc", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc53f8d24c23e741e69f05f35e78557a9c21115fd87142423792cb21d3d3af6bc", + "token": "0xd29cee911cf10771ed3af46889cc4dde3fb36a6e", + "creator": "0xda5964ee61e9a764b3e5e25059f8275ae8c2d492", + "quoteAsset": null, + "poolId": "0xfa9f26ffe52c94d03e8b2ef43204122fa7ac84f68dd9dd01083cb794ae5e98b1", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xf1e2e3397f954426d1cfddb520a51954ae54145e", + "positionRecipient": "0x67e66c21c51998a4ffba8d447f00850509663014", + "positionTokenId": "355560", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x27a368a7954d5519faa28360d1096efb037fba5918e95bb3c6f3b5a80614c3e6", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x014fd21ab0b24a75766e33941f8af53100099fb415706eec579de691b360cf1a:0xd7887e614f2616df1491e445bc37350cab04cc8cbaa628701018fa5ab952347c:866", + "liquidityOccurrenceId": "1:0x014fd21ab0b24a75766e33941f8af53100099fb415706eec579de691b360cf1a:0xd7887e614f2616df1491e445bc37350cab04cc8cbaa628701018fa5ab952347c:867", + "initialBuyOccurrenceId": "1:0x014fd21ab0b24a75766e33941f8af53100099fb415706eec579de691b360cf1a:0xd7887e614f2616df1491e445bc37350cab04cc8cbaa628701018fa5ab952347c:868", + "custodyOccurrenceId": "1:0x014fd21ab0b24a75766e33941f8af53100099fb415706eec579de691b360cf1a:0xd7887e614f2616df1491e445bc37350cab04cc8cbaa628701018fa5ab952347c:869", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645483" + }, + { + "id": "1:classic-v3:0xc54cb06ce4ff09713b08d46129107708fcb43e24571dd04efc330f3eb5fd1647", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc54cb06ce4ff09713b08d46129107708fcb43e24571dd04efc330f3eb5fd1647", + "token": "0xaec07aeceeacb07eb37dfd9d50e08463f08f6718", + "creator": "0x3224bc16e533a65f96f32c7c66250e526d3cd4f4", + "quoteAsset": null, + "poolId": "0x1cb95624ed056a63a3e096adae32295d6085e9413e71e0c5a33e9644153f9f90", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7c48200db689acd6f6383ba642c303c652b0b4ca", + "positionRecipient": "0x41af172bd78a861d793196cdaf84190a814bbc91", + "positionTokenId": "354905", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x4a6c38b9700635d9f3fedd13e90d352a258639e601517ed606fd6006df37b47b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x57f1643fcf2192588f42d3eb0629c675d0c4b523bfac67ab346dc01ed9f00225:0xc2a4582abc8828ff286077a4979b613af7149494cbb8f2230af45edfda6449bd:180", + "liquidityOccurrenceId": "1:0x57f1643fcf2192588f42d3eb0629c675d0c4b523bfac67ab346dc01ed9f00225:0xc2a4582abc8828ff286077a4979b613af7149494cbb8f2230af45edfda6449bd:181", + "initialBuyOccurrenceId": "1:0x57f1643fcf2192588f42d3eb0629c675d0c4b523bfac67ab346dc01ed9f00225:0xc2a4582abc8828ff286077a4979b613af7149494cbb8f2230af45edfda6449bd:182", + "custodyOccurrenceId": "1:0x57f1643fcf2192588f42d3eb0629c675d0c4b523bfac67ab346dc01ed9f00225:0xc2a4582abc8828ff286077a4979b613af7149494cbb8f2230af45edfda6449bd:183", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25642259" + }, + { + "id": "1:classic-v3:0xc5c1460d96ad7e4186028aff829bc21ae15873dac32d266c9b74b3f41dd0361e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc5c1460d96ad7e4186028aff829bc21ae15873dac32d266c9b74b3f41dd0361e", + "token": "0x2fc5a1ac03e739e9a7840221062e6a0aaeb2c586", + "creator": "0xf0f925f165a0ec7e48d2cf90f0928af3c32fe422", + "quoteAsset": null, + "poolId": "0x2a74084919b153edc88a2ea93f663a560229cfb1b2173657040f05ddaf65916d", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xe00b147e73302e8547753442b931c937ca00ef7a", + "positionRecipient": "0xab41a56ed66e33e8ffd735415fd24033fc483ede", + "positionTokenId": "355510", + "totalSwapFeeBps": null, + "buySwapFeeBps": 300, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x42c791d8e72211dcb5f37aef8e920db559b309f24326f65fb1bc33df9d754f1c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "429127663717334283035658", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x08ae14e3c3975303b8e7d0d0acb45c363608e3c464ef5c664fe4aeea910eb672:0x64a52d31c45d6d66c85d5a26d82235bc9d7b505909961936f7415bc7fc14627a:952", + "liquidityOccurrenceId": "1:0x08ae14e3c3975303b8e7d0d0acb45c363608e3c464ef5c664fe4aeea910eb672:0x64a52d31c45d6d66c85d5a26d82235bc9d7b505909961936f7415bc7fc14627a:953", + "initialBuyOccurrenceId": "1:0x08ae14e3c3975303b8e7d0d0acb45c363608e3c464ef5c664fe4aeea910eb672:0x64a52d31c45d6d66c85d5a26d82235bc9d7b505909961936f7415bc7fc14627a:954", + "custodyOccurrenceId": "1:0x08ae14e3c3975303b8e7d0d0acb45c363608e3c464ef5c664fe4aeea910eb672:0x64a52d31c45d6d66c85d5a26d82235bc9d7b505909961936f7415bc7fc14627a:955", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645369" + }, + { + "id": "1:classic-v3:0xc6a67a9afe8722b020a8f946951669d018addceac6059c7343f56dea6057e170", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc6a67a9afe8722b020a8f946951669d018addceac6059c7343f56dea6057e170", + "token": "0x1098262be4da26a495ef7bdfbb83a8897e0a8ccb", + "creator": "0x6d747fe7d5774e3f3ebe6803fc256ff8a79bc3a2", + "quoteAsset": null, + "poolId": "0x95c751a8f4a9f573657106b894cbf3825a886859fea4375b66bcd2b4fc5a328d", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xde6791d35f1cb7e8e49fdcac5c8fc18364807e3a", + "positionRecipient": "0x4ea8a1010b54c7bfeba7d06d534aecbaa52460f6", + "positionTokenId": "354641", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9c2da9cdd8dbbcb134a123d63f8d0b332c47e869865857258c6717a794ba655f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xd862061c0ddef62a1e312c07068809bd591a7094447bf911a745c2ee12120944:0xe11d1f3a68f9b11e12f29b09a32c8afd10e1e95d58b97eec4147390899361d3b:389", + "liquidityOccurrenceId": "1:0xd862061c0ddef62a1e312c07068809bd591a7094447bf911a745c2ee12120944:0xe11d1f3a68f9b11e12f29b09a32c8afd10e1e95d58b97eec4147390899361d3b:390", + "initialBuyOccurrenceId": "1:0xd862061c0ddef62a1e312c07068809bd591a7094447bf911a745c2ee12120944:0xe11d1f3a68f9b11e12f29b09a32c8afd10e1e95d58b97eec4147390899361d3b:391", + "custodyOccurrenceId": "1:0xd862061c0ddef62a1e312c07068809bd591a7094447bf911a745c2ee12120944:0xe11d1f3a68f9b11e12f29b09a32c8afd10e1e95d58b97eec4147390899361d3b:392", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640506" + }, + { + "id": "1:classic-v3:0xc6e2a73175df97a309530c4cd57eb7fe1220614e1197d95e37227700121e4e04", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc6e2a73175df97a309530c4cd57eb7fe1220614e1197d95e37227700121e4e04", + "token": "0x64b6b3ea3bb12abd54331a112655569c7da8b942", + "creator": "0x702ba46435d1e55b18440100bc81eb055574875e", + "quoteAsset": null, + "poolId": "0x4f8f4770a3d1fe430fbf22a614fd3eb068abb2796d2f4b73d9b99c4d7ea16ee0", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa8973ad3c61c4b6c63f6f95de1711393a90fdce0", + "positionRecipient": "0x66b66be0b731ce99a645e17c7759a972c85bc68c", + "positionTokenId": "354787", + "totalSwapFeeBps": null, + "buySwapFeeBps": 1000, + "sellSwapFeeBps": 1000, + "rewardConfigurationHash": "0x8553cd49f18946194a0a8c99eedd68e939e8ecb6c6c35aaceba11935d8cc4902", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "6595060137723579360320860", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x783c084ab1b04d99427fb097f589dfc71f20409dc64bf8f8a146bac3d2336eb4:0x716857de0efcb62c0cdb3b963da6b3b82ce70d96e20048ad1ae91b4f2890bb4a:537", + "liquidityOccurrenceId": "1:0x783c084ab1b04d99427fb097f589dfc71f20409dc64bf8f8a146bac3d2336eb4:0x716857de0efcb62c0cdb3b963da6b3b82ce70d96e20048ad1ae91b4f2890bb4a:538", + "initialBuyOccurrenceId": "1:0x783c084ab1b04d99427fb097f589dfc71f20409dc64bf8f8a146bac3d2336eb4:0x716857de0efcb62c0cdb3b963da6b3b82ce70d96e20048ad1ae91b4f2890bb4a:539", + "custodyOccurrenceId": "1:0x783c084ab1b04d99427fb097f589dfc71f20409dc64bf8f8a146bac3d2336eb4:0x716857de0efcb62c0cdb3b963da6b3b82ce70d96e20048ad1ae91b4f2890bb4a:540", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25641480" + }, + { + "id": "1:classic-v3:0xc7ccdb87824ea31dab33c6654d0484f9a29ce196178fdf67224327737325394b", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc7ccdb87824ea31dab33c6654d0484f9a29ce196178fdf67224327737325394b", + "token": "0x1095dc372ffd728dbf6d485feaa0d72570a6a10c", + "creator": "0x29301f367cbc3ebf39b5ec2347b3f87e3d2d40da", + "quoteAsset": null, + "poolId": "0xcf3f487ac75431f143a7d5edbaba60a6c88ef1d695b43560ecf1112e6736a7e0", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xc358a8306fe426400809c783494579e7e6fda10a", + "positionRecipient": "0x0f7f374725929e27c6e7f7023b0845ea5e635903", + "positionTokenId": "355647", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0xfb24759ce29af537fc4b133d0e1420dcdbc96fcdcdec242fbbc7bedcd929aca2", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb4f2483761408e6951483b62acc81a1b44c61ce5135b58ed054af81b781570fe:0x2ef80d2f846e1b3f28c5f3a39fcf0b246d289109cc5e980f6a350773223373b7:806", + "liquidityOccurrenceId": "1:0xb4f2483761408e6951483b62acc81a1b44c61ce5135b58ed054af81b781570fe:0x2ef80d2f846e1b3f28c5f3a39fcf0b246d289109cc5e980f6a350773223373b7:807", + "initialBuyOccurrenceId": "1:0xb4f2483761408e6951483b62acc81a1b44c61ce5135b58ed054af81b781570fe:0x2ef80d2f846e1b3f28c5f3a39fcf0b246d289109cc5e980f6a350773223373b7:808", + "custodyOccurrenceId": "1:0xb4f2483761408e6951483b62acc81a1b44c61ce5135b58ed054af81b781570fe:0x2ef80d2f846e1b3f28c5f3a39fcf0b246d289109cc5e980f6a350773223373b7:809", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645611" + }, + { + "id": "1:classic-v3:0xc7ff7be476a9537573dd81cca8b6615a2e28f5aeb681e3ed84c173fcede80388", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc7ff7be476a9537573dd81cca8b6615a2e28f5aeb681e3ed84c173fcede80388", + "token": "0xcc914e50c894c3c90260c5e40c5264eb4b2b2c2b", + "creator": "0xc1cf1b3e2e68f5178f4d0bec7c691ba57d8d462f", + "quoteAsset": null, + "poolId": "0xd4a307ca3f862e62768a7f0eda37431c5d7e193ab3fa6dceb79300e63757d2c7", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x9022bc2dcdcf8fad1160d77866006f9d07b8c40d", + "positionRecipient": "0xd0682f8638918edc8dd9d9028f52b97cac5721a5", + "positionTokenId": "354638", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x01bb65d3eb3701db419251c1a105b004d38d02e83392a906a520e2dedb48f1af", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "729739899031511876349884", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf93b9619f4a0dc4d017c3336994f18c42ea663762f1e417530f59e2a9f48e8c5:0xb40af24c340f4ca0ba7974c6768747491a2b77375c7d51de5ba3c4f749b73296:195", + "liquidityOccurrenceId": "1:0xf93b9619f4a0dc4d017c3336994f18c42ea663762f1e417530f59e2a9f48e8c5:0xb40af24c340f4ca0ba7974c6768747491a2b77375c7d51de5ba3c4f749b73296:196", + "initialBuyOccurrenceId": "1:0xf93b9619f4a0dc4d017c3336994f18c42ea663762f1e417530f59e2a9f48e8c5:0xb40af24c340f4ca0ba7974c6768747491a2b77375c7d51de5ba3c4f749b73296:197", + "custodyOccurrenceId": "1:0xf93b9619f4a0dc4d017c3336994f18c42ea663762f1e417530f59e2a9f48e8c5:0xb40af24c340f4ca0ba7974c6768747491a2b77375c7d51de5ba3c4f749b73296:198", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640491" + }, + { + "id": "1:classic-v3:0xc8d31e17044faea377383b0fb08a1094a728411c602ffd9cc024ee1c55d08352", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xc8d31e17044faea377383b0fb08a1094a728411c602ffd9cc024ee1c55d08352", + "token": "0xd97fa698fdf5e77be54e182a481546627b90c6ce", + "creator": "0xf8af8b05323c61c6c48e4df67f49f6ca4cedc066", + "quoteAsset": null, + "poolId": "0xd00d24b6d9b1f5e877d136c531fe2e34865754f4f146cd721575be549dfb49eb", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x2cd09c792775b02f2b59687990235f5970226587", + "positionRecipient": "0xa32167d924de165d252ff162561c426921a8a640", + "positionTokenId": "355670", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xf9c6e2906121846a2789fdbe3c7745cd38f5bb75d2fff92113b1bfb45a061458", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x28ade29d9d1ff1964e898f384f291d9b671cc1e0f39089608f9a62391b851e22:0x41d26afafc36558c150315d3718caeb71f84223fafe1b4fb26688c5159f47a3b:527", + "liquidityOccurrenceId": "1:0x28ade29d9d1ff1964e898f384f291d9b671cc1e0f39089608f9a62391b851e22:0x41d26afafc36558c150315d3718caeb71f84223fafe1b4fb26688c5159f47a3b:528", + "initialBuyOccurrenceId": "1:0x28ade29d9d1ff1964e898f384f291d9b671cc1e0f39089608f9a62391b851e22:0x41d26afafc36558c150315d3718caeb71f84223fafe1b4fb26688c5159f47a3b:529", + "custodyOccurrenceId": "1:0x28ade29d9d1ff1964e898f384f291d9b671cc1e0f39089608f9a62391b851e22:0x41d26afafc36558c150315d3718caeb71f84223fafe1b4fb26688c5159f47a3b:530", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645658" + }, + { + "id": "1:classic-v3:0xcbf0ed37542d2baa77fdf953902a7bfe5e587adea6f3ae4e96f6c4da82c94f3f", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xcbf0ed37542d2baa77fdf953902a7bfe5e587adea6f3ae4e96f6c4da82c94f3f", + "token": "0xc9422790ea8aba5e65424744e97439e0e1d3f827", + "creator": "0x051a53d095d277e22e3d905b10d3bbe75f0ac508", + "quoteAsset": null, + "poolId": "0xf16525b5516bea2651d41552b312898f1c4d4b8a6320ce6167b1de70faaa7182", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x7887321709d54b27d0a9d690d44c6b92196523a2", + "positionRecipient": "0x56ce34eb3f66534e0d649782df4fe7242e0150c7", + "positionTokenId": "355701", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xcffa0a5b6be43ab2e28927db88debdcad895e8d09d498da111254e3f3c67cab1", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "200000000000000000", + "initialBuyTokenAmount": "127441193931839478169123110", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xec43f0febc59ec784983e73cced8161064c1512b97dc6f280af56d748a301874:0x31cb0102ca9a7e4afe53b85bb330f6d8a2adb9b17fda8979d9c4d22f38cc07b3:465", + "liquidityOccurrenceId": "1:0xec43f0febc59ec784983e73cced8161064c1512b97dc6f280af56d748a301874:0x31cb0102ca9a7e4afe53b85bb330f6d8a2adb9b17fda8979d9c4d22f38cc07b3:466", + "initialBuyOccurrenceId": "1:0xec43f0febc59ec784983e73cced8161064c1512b97dc6f280af56d748a301874:0x31cb0102ca9a7e4afe53b85bb330f6d8a2adb9b17fda8979d9c4d22f38cc07b3:467", + "custodyOccurrenceId": "1:0xec43f0febc59ec784983e73cced8161064c1512b97dc6f280af56d748a301874:0x31cb0102ca9a7e4afe53b85bb330f6d8a2adb9b17fda8979d9c4d22f38cc07b3:468", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645701" + }, + { + "id": "1:classic-v3:0xd2a3ac8ca14a1fc9730a1ae0c0810c97ca0d0e6b9d1c61461932e64f64d04a74", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xd2a3ac8ca14a1fc9730a1ae0c0810c97ca0d0e6b9d1c61461932e64f64d04a74", + "token": "0x5d2746629f089bfb3b3f4838bf725fa662115c0d", + "creator": "0x46fdf1633a42b792b362c2a2e247f606beda6b10", + "quoteAsset": null, + "poolId": "0x5a217faa3899ea366a078cd2103c1aaf744405dfb138a63449d76ce70d627a6a", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x10db899b3ebbd9e431bf7ca8b4bd2484595aff76", + "positionRecipient": "0x992e97abbc34dde8f7e0ad56aa2c45f69fef3a2f", + "positionTokenId": "355361", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x5c83b0c4aa44a13d2623301516205bd500a851c8090eac1c572e9dc246863de3", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x22a0554e4844fe71360732bf743064b0b1cb4002de2f8549ce4fd83a7600eefe:0xa3a81f3463a2366c44a64bc10e62bdb67b2161e0b4a30e00c55ca0da9c7a9e59:674", + "liquidityOccurrenceId": "1:0x22a0554e4844fe71360732bf743064b0b1cb4002de2f8549ce4fd83a7600eefe:0xa3a81f3463a2366c44a64bc10e62bdb67b2161e0b4a30e00c55ca0da9c7a9e59:675", + "initialBuyOccurrenceId": "1:0x22a0554e4844fe71360732bf743064b0b1cb4002de2f8549ce4fd83a7600eefe:0xa3a81f3463a2366c44a64bc10e62bdb67b2161e0b4a30e00c55ca0da9c7a9e59:676", + "custodyOccurrenceId": "1:0x22a0554e4844fe71360732bf743064b0b1cb4002de2f8549ce4fd83a7600eefe:0xa3a81f3463a2366c44a64bc10e62bdb67b2161e0b4a30e00c55ca0da9c7a9e59:677", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25644837" + }, + { + "id": "1:classic-v3:0xd39c3055fda0d9393e2b3e294e1f5ad52e99fae9bb351c2dda41fa4394702287", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xd39c3055fda0d9393e2b3e294e1f5ad52e99fae9bb351c2dda41fa4394702287", + "token": "0x5d786ee4834e431dbb486e06a058e3cb91288b72", + "creator": "0xd0d18801e55c24793d062989661ef219b8821e25", + "quoteAsset": null, + "poolId": "0x1a0dd0d57dda0cf39a1d3e15226a98c66b479a51e87a76a3903391341b1cf939", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x1d13277760be0f9fa25047f169dfd0c663d7a48b", + "positionRecipient": "0xd38fbd35b25aa7d8859e81c81b6ae105fafec370", + "positionTokenId": "355541", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x026dc2115f6ad0b3ba7367c6a4ecee4ede7350086bad786c21c29966a75b178b", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf1fe263a0d8078643a9e8783a87f1cbedb82a66e22d30d39044fa444f6263c56:0x8587be39ee794a9e74c7575123db1ac77057166a028599c4583df3a3690ec862:514", + "liquidityOccurrenceId": "1:0xf1fe263a0d8078643a9e8783a87f1cbedb82a66e22d30d39044fa444f6263c56:0x8587be39ee794a9e74c7575123db1ac77057166a028599c4583df3a3690ec862:515", + "initialBuyOccurrenceId": "1:0xf1fe263a0d8078643a9e8783a87f1cbedb82a66e22d30d39044fa444f6263c56:0x8587be39ee794a9e74c7575123db1ac77057166a028599c4583df3a3690ec862:516", + "custodyOccurrenceId": "1:0xf1fe263a0d8078643a9e8783a87f1cbedb82a66e22d30d39044fa444f6263c56:0x8587be39ee794a9e74c7575123db1ac77057166a028599c4583df3a3690ec862:517", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645459" + }, + { + "id": "1:classic-v3:0xd404901102ed772a230facf0c3402f6a26d0070ae47b7624b18d7feabc214ee4", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xd404901102ed772a230facf0c3402f6a26d0070ae47b7624b18d7feabc214ee4", + "token": "0xdf57f34e6484687172817119e3e0f3ae6a844511", + "creator": "0x9170813cc45caffc24e21a72ccfad2fcd180ba89", + "quoteAsset": null, + "poolId": "0xc2687f3f7514ecc119af2f8491762a2eb83bd512befb157056f5b8f4e7f312c4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xe129fe0f6bb0d1575d7697319e8d970f0072cec5", + "positionRecipient": "0x36be93207d66f977885aa30c79d346597316ef32", + "positionTokenId": "355754", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xedc65505040a1606189d443d9119b6ec116ee039df04857cc416add15c53322d", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x946b2da520c3058b750096bbf037640b2e31913dc377c56bb024117591592b99:0xd2a1c539c51a07acf4e0ee3aeb3a17eb0c6ef99677b3df61df3451a98509266a:631", + "liquidityOccurrenceId": "1:0x946b2da520c3058b750096bbf037640b2e31913dc377c56bb024117591592b99:0xd2a1c539c51a07acf4e0ee3aeb3a17eb0c6ef99677b3df61df3451a98509266a:632", + "initialBuyOccurrenceId": "1:0x946b2da520c3058b750096bbf037640b2e31913dc377c56bb024117591592b99:0xd2a1c539c51a07acf4e0ee3aeb3a17eb0c6ef99677b3df61df3451a98509266a:633", + "custodyOccurrenceId": "1:0x946b2da520c3058b750096bbf037640b2e31913dc377c56bb024117591592b99:0xd2a1c539c51a07acf4e0ee3aeb3a17eb0c6ef99677b3df61df3451a98509266a:634", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645822" + }, + { + "id": "1:classic-v3:0xd534ffae1d491331936615907929a073bb192ca4f5e7246ba8eebdfb82d4427e", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xd534ffae1d491331936615907929a073bb192ca4f5e7246ba8eebdfb82d4427e", + "token": "0x5e58195c92d642b2e718bf1197e4a632fdf59f5f", + "creator": "0x5da441739614ea06af94b48afb9a7af906d4132d", + "quoteAsset": null, + "poolId": "0x4ad40f8048d848990659c77978443684a3212fabd3fce374b080ba8234c7257f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x0ce32c2f0e4571408366848ffc2960962e17a37f", + "positionRecipient": "0xc0465a3ec66ea75d27c1dac8842f9c166d666738", + "positionTokenId": "357742", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x3a3ba1507d2d831a91ca826df8488aa60048aadb6ac57611f0aef03749a06274", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x44050c8a655c7f664a1c77817d73c6b1c635c6e8006f7adebb8c7640905a84a8:0xc59c641e33dc7dc2e2b62a173b1d56b82461983979cdc9ea252162eacce4c03c:307", + "liquidityOccurrenceId": "1:0x44050c8a655c7f664a1c77817d73c6b1c635c6e8006f7adebb8c7640905a84a8:0xc59c641e33dc7dc2e2b62a173b1d56b82461983979cdc9ea252162eacce4c03c:308", + "initialBuyOccurrenceId": "1:0x44050c8a655c7f664a1c77817d73c6b1c635c6e8006f7adebb8c7640905a84a8:0xc59c641e33dc7dc2e2b62a173b1d56b82461983979cdc9ea252162eacce4c03c:309", + "custodyOccurrenceId": "1:0x44050c8a655c7f664a1c77817d73c6b1c635c6e8006f7adebb8c7640905a84a8:0xc59c641e33dc7dc2e2b62a173b1d56b82461983979cdc9ea252162eacce4c03c:310", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25654225" + }, + { + "id": "1:classic-v3:0xd851b8571581ffe78d9e11a0ab4e5ebbf6db26ce1795e8e398c507e22a6a00c0", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xd851b8571581ffe78d9e11a0ab4e5ebbf6db26ce1795e8e398c507e22a6a00c0", + "token": "0xab84d068aa67a3a85acfe8568fdb439ddebc22f4", + "creator": "0x60bc5323c0e0e9144af0b35ce8de119fc743e345", + "quoteAsset": null, + "poolId": "0xe8307e82de99792fc56a4edf318094c92bd2c871fb4d9732051d1a10ba497d60", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xfdb798a30d9c102d688e13280f59b332c6360447", + "positionRecipient": "0xaa8a1aaa85bf43e0b4cb5e6fea2eeefcd72583e5", + "positionTokenId": "356403", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 300, + "rewardConfigurationHash": "0x0f58d211e98980484ba59648ff25250b0a7e7ca4c650c1fbf6822a64f7756ec3", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14251873763907894480382812", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4fe122ecbbee6a21febae477862557ea129e3299c27e7ad6711a4a2a21393a23:0x5ebd77718d5a10fc1387050a093389fa0f26dc09500047952a9754e873eb6bdf:1872", + "liquidityOccurrenceId": "1:0x4fe122ecbbee6a21febae477862557ea129e3299c27e7ad6711a4a2a21393a23:0x5ebd77718d5a10fc1387050a093389fa0f26dc09500047952a9754e873eb6bdf:1873", + "initialBuyOccurrenceId": "1:0x4fe122ecbbee6a21febae477862557ea129e3299c27e7ad6711a4a2a21393a23:0x5ebd77718d5a10fc1387050a093389fa0f26dc09500047952a9754e873eb6bdf:1874", + "custodyOccurrenceId": "1:0x4fe122ecbbee6a21febae477862557ea129e3299c27e7ad6711a4a2a21393a23:0x5ebd77718d5a10fc1387050a093389fa0f26dc09500047952a9754e873eb6bdf:1875", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647875" + }, + { + "id": "1:classic-v3:0xda621e2cf622ad2ee30bec8ae770105340c85255833fe62c6132f2df2a7ed367", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xda621e2cf622ad2ee30bec8ae770105340c85255833fe62c6132f2df2a7ed367", + "token": "0xc21161e03bfd07ceb82c84591339dc91d74872fe", + "creator": "0x78667855f74bbe1e64718a97f33a824fab79ff09", + "quoteAsset": null, + "poolId": "0x3e7ed0570e307f57f3c23fa74e2f1d42ef31732d20fcc2495c5b0da3873b730a", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x90900a4b78b8df48af20a599c13dc3d84b44a7ef", + "positionRecipient": "0xae41fd6059d33a64090145fe78c4f3717072911d", + "positionTokenId": "355748", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x3a5b5a0dbff34500fed221c3baf653a564ca542609cc0918123b942b08280499", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "60000000000000000", + "initialBuyTokenAmount": "41977085066619728890186168", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x2d5caf93bd71148adafef9c91e575aea9bdffb3e9eea6650cc71638de492c263:0xf97866ac9585334ee8a9668c1f0eafd0857fd7e9ca94f082e21e2421a2641b35:18", + "liquidityOccurrenceId": "1:0x2d5caf93bd71148adafef9c91e575aea9bdffb3e9eea6650cc71638de492c263:0xf97866ac9585334ee8a9668c1f0eafd0857fd7e9ca94f082e21e2421a2641b35:19", + "initialBuyOccurrenceId": "1:0x2d5caf93bd71148adafef9c91e575aea9bdffb3e9eea6650cc71638de492c263:0xf97866ac9585334ee8a9668c1f0eafd0857fd7e9ca94f082e21e2421a2641b35:20", + "custodyOccurrenceId": "1:0x2d5caf93bd71148adafef9c91e575aea9bdffb3e9eea6650cc71638de492c263:0xf97866ac9585334ee8a9668c1f0eafd0857fd7e9ca94f082e21e2421a2641b35:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645802" + }, + { + "id": "1:classic-v3:0xda90f435b148eb59797ab4edcd04a21dab6e0dc0ab2839926805a75828d107dd", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xda90f435b148eb59797ab4edcd04a21dab6e0dc0ab2839926805a75828d107dd", + "token": "0x84a527737d8323b2e3652cecc4d5f884d8c7750c", + "creator": "0x5ffa822ae9e83777be4468bfed1d2fad5d305df5", + "quoteAsset": null, + "poolId": "0xf0754bb2285a18327be49b58b4560019860c8430ea5d373766a1be58b012a527", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x586a620ffefea693fab7bee64e1eaf0e934627fa", + "positionRecipient": "0x482cdf12fa324d895ff3965286a5b28a6ce49fc5", + "positionTokenId": "354593", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x0004ff453a3c63f50f301acbf70f1c0e9a2f981e532f754436ac6f6dafc3f147", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x76347fdfded86fad0d2230dbb7529917550807b48b2c7f0d8f6b1953043651fa:0xf65022d264cd05ddbef691f21d776548f3d06b9da6aa7410fa606fa16d4f67c5:18", + "liquidityOccurrenceId": "1:0x76347fdfded86fad0d2230dbb7529917550807b48b2c7f0d8f6b1953043651fa:0xf65022d264cd05ddbef691f21d776548f3d06b9da6aa7410fa606fa16d4f67c5:19", + "initialBuyOccurrenceId": "1:0x76347fdfded86fad0d2230dbb7529917550807b48b2c7f0d8f6b1953043651fa:0xf65022d264cd05ddbef691f21d776548f3d06b9da6aa7410fa606fa16d4f67c5:20", + "custodyOccurrenceId": "1:0x76347fdfded86fad0d2230dbb7529917550807b48b2c7f0d8f6b1953043651fa:0xf65022d264cd05ddbef691f21d776548f3d06b9da6aa7410fa606fa16d4f67c5:21", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640328" + }, + { + "id": "1:classic-v3:0xdc6c5c9d1ec5baccf543c45cb604cd6aebeb7a2ab49b63e6579ddddf713b1fa5", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xdc6c5c9d1ec5baccf543c45cb604cd6aebeb7a2ab49b63e6579ddddf713b1fa5", + "token": "0x4921b1931f1f4aab8720f8617855dac1b3c2b408", + "creator": "0x7ab6996b5a8efa4bb35e6f339dbdb6f050b057d2", + "quoteAsset": null, + "poolId": "0x7107cce3186343a0615d853f8f774c08bfb20b074fb974dece39a905238c7bec", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x283cecf8fb6772ddeb2ce0c72adc78ed0e04f388", + "positionRecipient": "0x06ffee565b2aeb3b2c4210a6b903fcf34bc178ad", + "positionTokenId": "356832", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x1494aeaa5cd2984a4ed797ac93784cabe5d34970ce784cf5497e0aca1377d5e5", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "729739899031511876349884", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x79b0f9557ab7bc7dcd5d42a309a1365e183e363fd624e454b1b421dccb2600d6:0x42df9031e5454aafeec6164d868f06ddf61cc1d445faa576c3df9990c69a4726:345", + "liquidityOccurrenceId": "1:0x79b0f9557ab7bc7dcd5d42a309a1365e183e363fd624e454b1b421dccb2600d6:0x42df9031e5454aafeec6164d868f06ddf61cc1d445faa576c3df9990c69a4726:346", + "initialBuyOccurrenceId": "1:0x79b0f9557ab7bc7dcd5d42a309a1365e183e363fd624e454b1b421dccb2600d6:0x42df9031e5454aafeec6164d868f06ddf61cc1d445faa576c3df9990c69a4726:347", + "custodyOccurrenceId": "1:0x79b0f9557ab7bc7dcd5d42a309a1365e183e363fd624e454b1b421dccb2600d6:0x42df9031e5454aafeec6164d868f06ddf61cc1d445faa576c3df9990c69a4726:348", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25649844" + }, + { + "id": "1:classic-v3:0xdd576ddcfd4d42b71c0ac92170af49ce0b807908b6fd1094ab25f74733618ced", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xdd576ddcfd4d42b71c0ac92170af49ce0b807908b6fd1094ab25f74733618ced", + "token": "0x555304655b1e1bb4a097fb66f4f52c80f9263df5", + "creator": "0xf00e1844903586a83a7a2d8ec28f4dcb5e31deca", + "quoteAsset": null, + "poolId": "0x4826bd10d426fd262b6dc8658cd5c0bd6bf02182ebe2532ea982b8027962dd9f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xbee6e44a2b2491b8e573d1cbb879c8a36c994a42", + "positionRecipient": "0xc67013faa99a25312da1b1bb39dd0aa20a7c170c", + "positionTokenId": "357902", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9fc478bed3bcda1ce1c45a7bcf782dbdf6ed3609d6d8157daf46f56a355c30c3", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "433549742227946105221397", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x087bb29440dc802654070492a0fce81add7945d07285fa115966c84d80358cc8:0x95102031811f32306c33b2660ab23767315a82de856622aa5b3d0eaeb863c1ef:458", + "liquidityOccurrenceId": "1:0x087bb29440dc802654070492a0fce81add7945d07285fa115966c84d80358cc8:0x95102031811f32306c33b2660ab23767315a82de856622aa5b3d0eaeb863c1ef:459", + "initialBuyOccurrenceId": "1:0x087bb29440dc802654070492a0fce81add7945d07285fa115966c84d80358cc8:0x95102031811f32306c33b2660ab23767315a82de856622aa5b3d0eaeb863c1ef:460", + "custodyOccurrenceId": "1:0x087bb29440dc802654070492a0fce81add7945d07285fa115966c84d80358cc8:0x95102031811f32306c33b2660ab23767315a82de856622aa5b3d0eaeb863c1ef:461", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25655183" + }, + { + "id": "1:classic-v3:0xdd85be9a0e4dfe9534f49e1e36ea619afeafe5c3709588def04a4ee4b5036b83", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xdd85be9a0e4dfe9534f49e1e36ea619afeafe5c3709588def04a4ee4b5036b83", + "token": "0xe9cfb1980dc8d1c5932577bf3be92b254d4b305b", + "creator": "0x5c1b2bf3e255850d061a1e379f525fffcfa615c7", + "quoteAsset": null, + "poolId": "0xfed6cb6ef2fedf6ab3d1601a7f1c4909a0e0d0bdd1a69f9037ef72b71521ff07", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x6206c4b87d4a784af53c2b72ecf2d7dabe337ec3", + "positionRecipient": "0xc38b5a4d836a3dac7fe6782ded844e97efe5ad35", + "positionTokenId": "356063", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xc8e3d38ecdb37a8cfb6381215296338a2343e5af7a45091f4c5ffff0dc4ce733", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "15000000000000000", + "initialBuyTokenAmount": "10835400152091333926970158", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x089e5fb3ad02e8ad5301239bcd38ad45e94ab8410eb62966c57f53f1152f3af5:0x4eb9aaee831d354c3b64453f145fad7804127a9d3853238674677acd0aa5eace:412", + "liquidityOccurrenceId": "1:0x089e5fb3ad02e8ad5301239bcd38ad45e94ab8410eb62966c57f53f1152f3af5:0x4eb9aaee831d354c3b64453f145fad7804127a9d3853238674677acd0aa5eace:413", + "initialBuyOccurrenceId": "1:0x089e5fb3ad02e8ad5301239bcd38ad45e94ab8410eb62966c57f53f1152f3af5:0x4eb9aaee831d354c3b64453f145fad7804127a9d3853238674677acd0aa5eace:414", + "custodyOccurrenceId": "1:0x089e5fb3ad02e8ad5301239bcd38ad45e94ab8410eb62966c57f53f1152f3af5:0x4eb9aaee831d354c3b64453f145fad7804127a9d3853238674677acd0aa5eace:415", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646639" + }, + { + "id": "1:classic-v3:0xe189d3a641ea6853462f3c13aed12012bb03e9d3d80f3f80cae675bd76ca7124", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe189d3a641ea6853462f3c13aed12012bb03e9d3d80f3f80cae675bd76ca7124", + "token": "0x1152cb508500595e1d52a885bcb0f802ac46b53a", + "creator": "0x7a0cc03aa1ef4c84ef3155e3d6d82594a893bdc1", + "quoteAsset": null, + "poolId": "0xe1b9e29fb5db8a12a7d9fcb083d0877d7986ddba32c02a747d322dc629081144", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x5d2e2e0b7c6778fbe138d912d5e69cdd9607baec", + "positionRecipient": "0xd4b11481545366d33895289e43dd4a21e60682cf", + "positionTokenId": "355139", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xe592ed30a546919fb56e09a061ff4304b1f66691760082b9270993017e32ea82", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb460de56dfd688387ed51b2bcde01065efd782572e83013a4dd03fbb303ba1a4:0x9a4dbf386aff015fe77d7286cdcbacfa23a67d0b58aa7ff34569311c2117a1f7:85", + "liquidityOccurrenceId": "1:0xb460de56dfd688387ed51b2bcde01065efd782572e83013a4dd03fbb303ba1a4:0x9a4dbf386aff015fe77d7286cdcbacfa23a67d0b58aa7ff34569311c2117a1f7:86", + "initialBuyOccurrenceId": "1:0xb460de56dfd688387ed51b2bcde01065efd782572e83013a4dd03fbb303ba1a4:0x9a4dbf386aff015fe77d7286cdcbacfa23a67d0b58aa7ff34569311c2117a1f7:87", + "custodyOccurrenceId": "1:0xb460de56dfd688387ed51b2bcde01065efd782572e83013a4dd03fbb303ba1a4:0x9a4dbf386aff015fe77d7286cdcbacfa23a67d0b58aa7ff34569311c2117a1f7:88", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25643559" + }, + { + "id": "1:classic-v3:0xe1c4b8e54d123f50cb02abc3baacca46dd936fcc1342bab4b9eb4f4205fb7bf9", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe1c4b8e54d123f50cb02abc3baacca46dd936fcc1342bab4b9eb4f4205fb7bf9", + "token": "0x3e37d67c8ec69a58a16ccd89b9f868815e7ab7b8", + "creator": "0x36b3664afefec73b7358a11d69308b3198d7fc09", + "quoteAsset": null, + "poolId": "0x26eca2ead6dd79a4935f5cefa3df44544cb14a466951d09997790aa00502b6d3", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xc340306284d2caa8cd223260eb34904a54aee7c7", + "positionRecipient": "0xde580a3b56e4dfc20d45c81b65708413913087f8", + "positionTokenId": "356994", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xaaf3c01ae6257c5f2c991d8523acbe12c78e17e71a5ea4b2b7ae91d784c26e8e", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb15b91db5cb634a596fde5a597d37e100d45bda5bd102c787a2bf83a56c78477:0xd8dcd1410bbaf918e6bd9e89093f3e73b9b2c717f7668f0a395633a3f4c967ca:22", + "liquidityOccurrenceId": "1:0xb15b91db5cb634a596fde5a597d37e100d45bda5bd102c787a2bf83a56c78477:0xd8dcd1410bbaf918e6bd9e89093f3e73b9b2c717f7668f0a395633a3f4c967ca:23", + "initialBuyOccurrenceId": "1:0xb15b91db5cb634a596fde5a597d37e100d45bda5bd102c787a2bf83a56c78477:0xd8dcd1410bbaf918e6bd9e89093f3e73b9b2c717f7668f0a395633a3f4c967ca:24", + "custodyOccurrenceId": "1:0xb15b91db5cb634a596fde5a597d37e100d45bda5bd102c787a2bf83a56c78477:0xd8dcd1410bbaf918e6bd9e89093f3e73b9b2c717f7668f0a395633a3f4c967ca:25", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25650569" + }, + { + "id": "1:classic-v3:0xe1c571dd844010979c6e7e355e9b0d06126750de5bd5ffcb536783aa78483fba", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe1c571dd844010979c6e7e355e9b0d06126750de5bd5ffcb536783aa78483fba", + "token": "0x88e4e668429ba2cdf79469d72b0fb9718bc8f568", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0xbcb66b4dfc1571babf630dce7a182d64be103ffd04f5e6363ef2231283fe0a06", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xea5fcd58ea20ba2a67af9b0887829b6423675c1a", + "positionRecipient": "0x4211b204f48973670cda6f7f1ef1d4073f02a0a4", + "positionTokenId": "355927", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x0106e76c00f61aa0b4b2903e84a5611e286df98f5b818d5c933f8206fb4cee83", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x7afcad2cc6d0c3a34eb77fa038c18881288f6ee12dfb507e44604c16f0ce6160:0x38d6920000b14a9bf25daca1d32d6a2e4ac3812ccb13674030a314c1f8e0eae2:322", + "liquidityOccurrenceId": "1:0x7afcad2cc6d0c3a34eb77fa038c18881288f6ee12dfb507e44604c16f0ce6160:0x38d6920000b14a9bf25daca1d32d6a2e4ac3812ccb13674030a314c1f8e0eae2:323", + "initialBuyOccurrenceId": "1:0x7afcad2cc6d0c3a34eb77fa038c18881288f6ee12dfb507e44604c16f0ce6160:0x38d6920000b14a9bf25daca1d32d6a2e4ac3812ccb13674030a314c1f8e0eae2:324", + "custodyOccurrenceId": "1:0x7afcad2cc6d0c3a34eb77fa038c18881288f6ee12dfb507e44604c16f0ce6160:0x38d6920000b14a9bf25daca1d32d6a2e4ac3812ccb13674030a314c1f8e0eae2:325", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646185" + }, + { + "id": "1:classic-v3:0xe32c06e1b27e1162df7fd3292d3f7a64ee538dacc0867c79414968ec26dba272", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe32c06e1b27e1162df7fd3292d3f7a64ee538dacc0867c79414968ec26dba272", + "token": "0xcb0058440ec7ed8859c7660167a4e60dc83b5e9b", + "creator": "0x5ffa822ae9e83777be4468bfed1d2fad5d305df5", + "quoteAsset": null, + "poolId": "0xf96981530d2dcb4a8450656cc35cbbe8a348c0761453d714ee797262dd11f07b", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x945f5918676882f2f29f7a777eaa486eba059114", + "positionRecipient": "0x452861c17133fb62c97a89f3bcafcbc525c9504a", + "positionTokenId": "354594", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x51d06ca7ac4cf84e417fa35d5a9afde479ccdf6b86b11df9aa2c299fdaa5b877", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xb24d44e60b6c0868e52b6691f61516a6499caf455aa9d09b5bdcc1eb2ff9d541:0xbdcf0a3e00c0b4ade4601b4000accaf04245703bfa481e187cf5463a4eefdc24:134", + "liquidityOccurrenceId": "1:0xb24d44e60b6c0868e52b6691f61516a6499caf455aa9d09b5bdcc1eb2ff9d541:0xbdcf0a3e00c0b4ade4601b4000accaf04245703bfa481e187cf5463a4eefdc24:135", + "initialBuyOccurrenceId": "1:0xb24d44e60b6c0868e52b6691f61516a6499caf455aa9d09b5bdcc1eb2ff9d541:0xbdcf0a3e00c0b4ade4601b4000accaf04245703bfa481e187cf5463a4eefdc24:136", + "custodyOccurrenceId": "1:0xb24d44e60b6c0868e52b6691f61516a6499caf455aa9d09b5bdcc1eb2ff9d541:0xbdcf0a3e00c0b4ade4601b4000accaf04245703bfa481e187cf5463a4eefdc24:137", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25640330" + }, + { + "id": "1:classic-v3:0xe4601de4bcc9721b31cddd2ae4af663a65130510a5cd88937484be43d6487e3c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe4601de4bcc9721b31cddd2ae4af663a65130510a5cd88937484be43d6487e3c", + "token": "0x653fd633637d82274051e706af8f61873f02c72b", + "creator": "0x5c1b2bf3e255850d061a1e379f525fffcfa615c7", + "quoteAsset": null, + "poolId": "0x62c4200e70df52a3226a6a750a0a371b3590be03384594fecbe053afaa1339ae", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xe5b0e03ad5dbe44df756be2d2a80372499573291", + "positionRecipient": "0xc879fb19c9bb958cafc53c1235b2b5570596dc30", + "positionTokenId": "356067", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x98b412150a8ba3c252b22a12381d918439ef3691cbd099c73db251e37afbc011", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xbee61746e4c643ede0b4ea5a29e09658e5880e99d7d369e359a6ec9ed5a3c550:0xbdb09e359fd2b327060e4597a873cc79fa60e84f0b0139975adb2303a50572e8:350", + "liquidityOccurrenceId": "1:0xbee61746e4c643ede0b4ea5a29e09658e5880e99d7d369e359a6ec9ed5a3c550:0xbdb09e359fd2b327060e4597a873cc79fa60e84f0b0139975adb2303a50572e8:351", + "initialBuyOccurrenceId": "1:0xbee61746e4c643ede0b4ea5a29e09658e5880e99d7d369e359a6ec9ed5a3c550:0xbdb09e359fd2b327060e4597a873cc79fa60e84f0b0139975adb2303a50572e8:352", + "custodyOccurrenceId": "1:0xbee61746e4c643ede0b4ea5a29e09658e5880e99d7d369e359a6ec9ed5a3c550:0xbdb09e359fd2b327060e4597a873cc79fa60e84f0b0139975adb2303a50572e8:353", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646652" + }, + { + "id": "1:classic-v3:0xe4c4621f7a50bc5a32b03127bac39393e5272d95321e4994048d62a68fd2790c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe4c4621f7a50bc5a32b03127bac39393e5272d95321e4994048d62a68fd2790c", + "token": "0x700ffbf0c8c96d2106bf3883937b8eb3a4c6521e", + "creator": "0xb1f27de1ce349bb9031882bc44d102d6c57a723c", + "quoteAsset": null, + "poolId": "0xfcd8a4dfbbdc1ade8e3d83219d26675aa0d218dc1f9cf58c7b6c0f12446fa6ed", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x3434c9af33bbd35caf6c2c0106dc248968edd341", + "positionRecipient": "0x18bdf0dc2923603bba28b656e5021dfc4614a5d1", + "positionTokenId": "355871", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x55c02ae91684a4c04fc7222825ae23c12152f9eeb35f826ed1280a2073d029fc", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "68057245261861571047346184", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1d8f607fb44402885449613eea233ba76d5d9ebc518e53043fc5030c338a93f9:0x6be7e316edf8e82544008e7487eac7151ab6a5fad43c9fd9489b66ac1a8eaaa2:32", + "liquidityOccurrenceId": "1:0x1d8f607fb44402885449613eea233ba76d5d9ebc518e53043fc5030c338a93f9:0x6be7e316edf8e82544008e7487eac7151ab6a5fad43c9fd9489b66ac1a8eaaa2:33", + "initialBuyOccurrenceId": "1:0x1d8f607fb44402885449613eea233ba76d5d9ebc518e53043fc5030c338a93f9:0x6be7e316edf8e82544008e7487eac7151ab6a5fad43c9fd9489b66ac1a8eaaa2:34", + "custodyOccurrenceId": "1:0x1d8f607fb44402885449613eea233ba76d5d9ebc518e53043fc5030c338a93f9:0x6be7e316edf8e82544008e7487eac7151ab6a5fad43c9fd9489b66ac1a8eaaa2:35", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646075" + }, + { + "id": "1:classic-v3:0xe4fe4f03c758c0708d7b5cf0c967c56f6e91150b48ddb015d503599c08f5004c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe4fe4f03c758c0708d7b5cf0c967c56f6e91150b48ddb015d503599c08f5004c", + "token": "0xa479151a9d1632993e8c47d15d89a77df17e97a2", + "creator": "0x2e0464a449dca53a183955fd3146316c9a5c9d9c", + "quoteAsset": null, + "poolId": "0x92ca60a06d1e4979b1ba6d08b8fb6a49b27df45fb00490e66ccef4191ffe77fc", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xaf0b4944a6249dabbcec4d27f6c6c0d71b3bf21b", + "positionRecipient": "0x48a73e1867a792e1f788835b7a93d0ea99884ec3", + "positionTokenId": "356754", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x878209721f111e7701c92f0cdf1dcb145949f1ad0e5510ce3550fd405e6f6391", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "20000000000000000", + "initialBuyTokenAmount": "14395207591280463018060415", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xd19507ec66742cfb450cad19a28c67d7f1c5fe45d2e717a8230ca000822572f4:0xf90b1e6367304de0cb69b0fd3e6d4d00e3fc7156c8780ebef8f069475790ab8c:124", + "liquidityOccurrenceId": "1:0xd19507ec66742cfb450cad19a28c67d7f1c5fe45d2e717a8230ca000822572f4:0xf90b1e6367304de0cb69b0fd3e6d4d00e3fc7156c8780ebef8f069475790ab8c:125", + "initialBuyOccurrenceId": "1:0xd19507ec66742cfb450cad19a28c67d7f1c5fe45d2e717a8230ca000822572f4:0xf90b1e6367304de0cb69b0fd3e6d4d00e3fc7156c8780ebef8f069475790ab8c:126", + "custodyOccurrenceId": "1:0xd19507ec66742cfb450cad19a28c67d7f1c5fe45d2e717a8230ca000822572f4:0xf90b1e6367304de0cb69b0fd3e6d4d00e3fc7156c8780ebef8f069475790ab8c:127", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25649316" + }, + { + "id": "1:classic-v3:0xe52be9ecca06a9a6ea705d913c44cbf0278f30d63a228f1e47066603a4f4769c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe52be9ecca06a9a6ea705d913c44cbf0278f30d63a228f1e47066603a4f4769c", + "token": "0x497ea05b38f6eba5c72fa599e0c548343ae89051", + "creator": "0xe51faf16f4ee8cc168949077ac624e745bb93720", + "quoteAsset": null, + "poolId": "0x7afc3857877eb27c480674c857dbd5c0439923a46e89626105934e8ba5c5c6dc", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xea42d3261c155190fb8181fd657d9e4a07ff1bdf", + "positionRecipient": "0x2e88244b8aebd936e5c1cd41154423d20ded3df7", + "positionTokenId": "357070", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x02ec2003c66ccde65f0056b33781b8cf5d2ee0a3bf2ebeea0ae51aaa46385e1d", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xeb7a17b8993813e5c3faed2b771e8968237b9aaa19456db38b30655c67582712:0x7e883e3cbb0d10ba5f0905c1b99641ad6d21992f20891953de6912f708a4dadc:72", + "liquidityOccurrenceId": "1:0xeb7a17b8993813e5c3faed2b771e8968237b9aaa19456db38b30655c67582712:0x7e883e3cbb0d10ba5f0905c1b99641ad6d21992f20891953de6912f708a4dadc:73", + "initialBuyOccurrenceId": "1:0xeb7a17b8993813e5c3faed2b771e8968237b9aaa19456db38b30655c67582712:0x7e883e3cbb0d10ba5f0905c1b99641ad6d21992f20891953de6912f708a4dadc:74", + "custodyOccurrenceId": "1:0xeb7a17b8993813e5c3faed2b771e8968237b9aaa19456db38b30655c67582712:0x7e883e3cbb0d10ba5f0905c1b99641ad6d21992f20891953de6912f708a4dadc:75", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25651063" + }, + { + "id": "1:classic-v3:0xe75904ca0fe54ae7bf574672f32d76d7c13c94aa8a5ff740337d82565666ec1c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe75904ca0fe54ae7bf574672f32d76d7c13c94aa8a5ff740337d82565666ec1c", + "token": "0xfbb5a9312be28f3b0191e2c813593c293ec18072", + "creator": "0x6e2946c675c57efb1e193b9be8230cccd4ce3e50", + "quoteAsset": null, + "poolId": "0x6290365115dc150fbd5854e12d0c619ffbd32c8347d5a987c5f3f420b8325721", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x8457177f9e17dba0185456a50018c12145d6fdd9", + "positionRecipient": "0xea6c39eb7a5027aee9ab1e0aad78fd1976d318d3", + "positionTokenId": "356258", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xb94f3e87bc94bae6926983ec3c004d54f9e4648f382e64ccad6e676c9d7374cb", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4337023bed775dc9a105131051c3feb3782ac5b7cba58df7af84b27cb09b7d1c:0x72e473419c88d49ea141cd1ff67e8d05cdd7ea3a0ae8f1e9e53e6f7f084480f5:335", + "liquidityOccurrenceId": "1:0x4337023bed775dc9a105131051c3feb3782ac5b7cba58df7af84b27cb09b7d1c:0x72e473419c88d49ea141cd1ff67e8d05cdd7ea3a0ae8f1e9e53e6f7f084480f5:336", + "initialBuyOccurrenceId": "1:0x4337023bed775dc9a105131051c3feb3782ac5b7cba58df7af84b27cb09b7d1c:0x72e473419c88d49ea141cd1ff67e8d05cdd7ea3a0ae8f1e9e53e6f7f084480f5:337", + "custodyOccurrenceId": "1:0x4337023bed775dc9a105131051c3feb3782ac5b7cba58df7af84b27cb09b7d1c:0x72e473419c88d49ea141cd1ff67e8d05cdd7ea3a0ae8f1e9e53e6f7f084480f5:338", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647472" + }, + { + "id": "1:classic-v3:0xe7a3be5a84e1c05faaffed5dcf89755e084a8acc6b9e96450cd30d43dd6c31de", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe7a3be5a84e1c05faaffed5dcf89755e084a8acc6b9e96450cd30d43dd6c31de", + "token": "0x988b8262173b0824aa26d7f7b1a9695907e50b97", + "creator": "0x9170813cc45caffc24e21a72ccfad2fcd180ba89", + "quoteAsset": null, + "poolId": "0xac05556a44409a5b4536170bfb2512c3251a44ecad50b45001ccfc3e932ce73c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x38efdc7ddee2f51074d357f0c38e75acbf2d8e7b", + "positionRecipient": "0x378a91fe11d5ae5ce0ae22044a519549ee526a0c", + "positionTokenId": "355407", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x07b1912120b1794b92b5edaef52e970e20fcf7c1886e7f902be498af4edf1a89", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xa930502a4bb32fe14d103a6a0420cc311bab2f62e5bf45c22f80f4d7420a7bca:0x9aea9d6c06d46e9c31f215824c6fdc669ae1587024ad9bb298448b011ba5341e:90", + "liquidityOccurrenceId": "1:0xa930502a4bb32fe14d103a6a0420cc311bab2f62e5bf45c22f80f4d7420a7bca:0x9aea9d6c06d46e9c31f215824c6fdc669ae1587024ad9bb298448b011ba5341e:91", + "initialBuyOccurrenceId": "1:0xa930502a4bb32fe14d103a6a0420cc311bab2f62e5bf45c22f80f4d7420a7bca:0x9aea9d6c06d46e9c31f215824c6fdc669ae1587024ad9bb298448b011ba5341e:92", + "custodyOccurrenceId": "1:0xa930502a4bb32fe14d103a6a0420cc311bab2f62e5bf45c22f80f4d7420a7bca:0x9aea9d6c06d46e9c31f215824c6fdc669ae1587024ad9bb298448b011ba5341e:93", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645020" + }, + { + "id": "1:classic-v3:0xe918fc8573228a3e5341d35aca4f295e22ac3d30df3cf687d4069b5b33c98daf", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe918fc8573228a3e5341d35aca4f295e22ac3d30df3cf687d4069b5b33c98daf", + "token": "0x22426c0a689f5ddcc57e5248ee434f50978ec33a", + "creator": "0x74ae384376eebce74624df5954a8cced13b41e6a", + "quoteAsset": null, + "poolId": "0x635ad8ee301e37131c1879dc40a992186b44c0c00b5359716651bb1793863b2c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa54096223b3fec1058819ab71220522076d68914", + "positionRecipient": "0x3516df38d41ce448628c512565f6dd14fa524475", + "positionTokenId": "355543", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xaba253700ca2b76ea445284bdd813bb1ba378e92baa6199358def13edd61a103", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x0cf527471ff9cc18a01ef63bc898c4bbf3815ae6ea6d3919f345a0c3356da693:0xe2ffb57a104f36602c3528471882ec755ee6469a9ec408b28be890d057bbcfcd:554", + "liquidityOccurrenceId": "1:0x0cf527471ff9cc18a01ef63bc898c4bbf3815ae6ea6d3919f345a0c3356da693:0xe2ffb57a104f36602c3528471882ec755ee6469a9ec408b28be890d057bbcfcd:555", + "initialBuyOccurrenceId": "1:0x0cf527471ff9cc18a01ef63bc898c4bbf3815ae6ea6d3919f345a0c3356da693:0xe2ffb57a104f36602c3528471882ec755ee6469a9ec408b28be890d057bbcfcd:556", + "custodyOccurrenceId": "1:0x0cf527471ff9cc18a01ef63bc898c4bbf3815ae6ea6d3919f345a0c3356da693:0xe2ffb57a104f36602c3528471882ec755ee6469a9ec408b28be890d057bbcfcd:557", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645460" + }, + { + "id": "1:classic-v3:0xe9d837dd926cadb738b2394d5d9e26e97f24c03e8f8a764f25784938040bb689", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xe9d837dd926cadb738b2394d5d9e26e97f24c03e8f8a764f25784938040bb689", + "token": "0x81a1c27e3dfc2fdaf9dd9e1bbf2a051f6556a489", + "creator": "0x76a72e24b4b6fa5bad47d6cc5668c22d5494f21c", + "quoteAsset": null, + "poolId": "0x1d5aa32a32b2ce6e3b1d076c4d9a6f92ee8cdbd29cb94ee7d941e198b62e3ea4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xf56ce5b76207ed94d6d3c07713124bde1ff45e14", + "positionRecipient": "0x90b45516bc76faa37801361ff7db31ded8736b34", + "positionTokenId": "355116", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x5b86fd54d153954bcbe1d75cfc4429af63d5c2076f67afbf1de548b8f89e9df5", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x59b64d210141dd6061e4900bf5bd83639b2edc7bbc2276625c92a2ffd34c97ee:0x09bef04ca9a9a7f106a06a7d95fe70184b7ddd08ff86657538dee8e894c2627a:58", + "liquidityOccurrenceId": "1:0x59b64d210141dd6061e4900bf5bd83639b2edc7bbc2276625c92a2ffd34c97ee:0x09bef04ca9a9a7f106a06a7d95fe70184b7ddd08ff86657538dee8e894c2627a:59", + "initialBuyOccurrenceId": "1:0x59b64d210141dd6061e4900bf5bd83639b2edc7bbc2276625c92a2ffd34c97ee:0x09bef04ca9a9a7f106a06a7d95fe70184b7ddd08ff86657538dee8e894c2627a:60", + "custodyOccurrenceId": "1:0x59b64d210141dd6061e4900bf5bd83639b2edc7bbc2276625c92a2ffd34c97ee:0x09bef04ca9a9a7f106a06a7d95fe70184b7ddd08ff86657538dee8e894c2627a:61", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25643478" + }, + { + "id": "1:classic-v3:0xea94d84c0c2c7e615eb2f5fbda964f0cadc736e6df7e3f60f7b9dd6bc15a7b46", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xea94d84c0c2c7e615eb2f5fbda964f0cadc736e6df7e3f60f7b9dd6bc15a7b46", + "token": "0xeec42ec597518c0a87031754d9036250ea0d77cb", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0x4f620c47b2e21839bb995ad18548195781c9976e8bfe65543117fd3c60b553a5", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xc32995e1b66c5f57d20ecc105ef1bc37ebaf19a3", + "positionRecipient": "0x7e9c00028e85a14f520acdaced694b6601a325e5", + "positionTokenId": "355949", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x660453231586f17a227c9c3c9078acfeb45aa562556eca01681e2439a281ed6f", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x5154401bffb4880683818a7789c04e1f70040cc674e55aa572f0972409f4ed89:0xb8f303a76e90e94d49706da4387776b50549f631e517d953094ca5274a419e2f:686", + "liquidityOccurrenceId": "1:0x5154401bffb4880683818a7789c04e1f70040cc674e55aa572f0972409f4ed89:0xb8f303a76e90e94d49706da4387776b50549f631e517d953094ca5274a419e2f:687", + "initialBuyOccurrenceId": "1:0x5154401bffb4880683818a7789c04e1f70040cc674e55aa572f0972409f4ed89:0xb8f303a76e90e94d49706da4387776b50549f631e517d953094ca5274a419e2f:688", + "custodyOccurrenceId": "1:0x5154401bffb4880683818a7789c04e1f70040cc674e55aa572f0972409f4ed89:0xb8f303a76e90e94d49706da4387776b50549f631e517d953094ca5274a419e2f:689", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646236" + }, + { + "id": "1:classic-v3:0xeaf920504bac8ae9e1328e37f302342bc49b6ba07393f0b08ecea1522a3b611a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xeaf920504bac8ae9e1328e37f302342bc49b6ba07393f0b08ecea1522a3b611a", + "token": "0x0282bece9ae5814c82e6939642b47d516e056a58", + "creator": "0x4b08916a6d14ef89324c827c1fa26138a00470f2", + "quoteAsset": null, + "poolId": "0xe4bddc3d7101d00bd9fb4a5096827004435abd56f4e3a755bfa6ccfd61dac1df", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x625f41aaa6d2c2d3c440d5afe714e2c77f5cf8e6", + "positionRecipient": "0xcbf09c7b0de6b572e58af0b29109b342a405e3c6", + "positionTokenId": "355783", + "totalSwapFeeBps": null, + "buySwapFeeBps": 200, + "sellSwapFeeBps": 200, + "rewardConfigurationHash": "0xaf1b2834795e61ffd0a4dfa3ce6cc27815b58ffa37a23e2ceeb447c99c429382", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "100000000000000000", + "initialBuyTokenAmount": "67416143360259716956310334", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4575af5e1ae95cc496a5a037fde44a1105df0fb4d3be05dc022f19d60d78740e:0x7a9d767aaf8b22b359bde7161a4b98c96ed063230fcbae8a97e267b5fc3a45f9:628", + "liquidityOccurrenceId": "1:0x4575af5e1ae95cc496a5a037fde44a1105df0fb4d3be05dc022f19d60d78740e:0x7a9d767aaf8b22b359bde7161a4b98c96ed063230fcbae8a97e267b5fc3a45f9:629", + "initialBuyOccurrenceId": "1:0x4575af5e1ae95cc496a5a037fde44a1105df0fb4d3be05dc022f19d60d78740e:0x7a9d767aaf8b22b359bde7161a4b98c96ed063230fcbae8a97e267b5fc3a45f9:630", + "custodyOccurrenceId": "1:0x4575af5e1ae95cc496a5a037fde44a1105df0fb4d3be05dc022f19d60d78740e:0x7a9d767aaf8b22b359bde7161a4b98c96ed063230fcbae8a97e267b5fc3a45f9:631", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645900" + }, + { + "id": "1:classic-v3:0xebe049d1ec014407e1cbfd4e45ee8ff661717246cfa688aed245614319e5c29a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xebe049d1ec014407e1cbfd4e45ee8ff661717246cfa688aed245614319e5c29a", + "token": "0x14de93a713ee21d46cab86e8b84892d10bb26707", + "creator": "0x6cab818daaf4040476e2489a7ed29ca685ad0623", + "quoteAsset": null, + "poolId": "0x1f6680f6b273fa9c487e74d1323b9c03253618516267b3bdd80d14805c3f9951", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x09f92dfe91d4d41ed4d7deb95f1e247e77b5c2fb", + "positionRecipient": "0xf7ee348582435a8c3ca722ac5b561250f9fd0f1b", + "positionTokenId": "355940", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xabcb87c2cbcfaa9fec7bf9e085e2d2186eee101c498c6d3c986c45086796a74d", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "50000000000000000", + "initialBuyTokenAmount": "35227361211893808519261776", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x000db21ad6b67a0b65a05395aafe50259bdc63053f9cc2f7cb489c9006d948c2:0x75500da9ab619acc0809cafa76876568b70c042be83ecec2ca0912d51317fd30:537", + "liquidityOccurrenceId": "1:0x000db21ad6b67a0b65a05395aafe50259bdc63053f9cc2f7cb489c9006d948c2:0x75500da9ab619acc0809cafa76876568b70c042be83ecec2ca0912d51317fd30:538", + "initialBuyOccurrenceId": "1:0x000db21ad6b67a0b65a05395aafe50259bdc63053f9cc2f7cb489c9006d948c2:0x75500da9ab619acc0809cafa76876568b70c042be83ecec2ca0912d51317fd30:539", + "custodyOccurrenceId": "1:0x000db21ad6b67a0b65a05395aafe50259bdc63053f9cc2f7cb489c9006d948c2:0x75500da9ab619acc0809cafa76876568b70c042be83ecec2ca0912d51317fd30:540", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646205" + }, + { + "id": "1:classic-v3:0xeccaf4e218a2ca5d2fe54e76ce1ce2a2981e6bb90ac8a5d18fcf0506d8c4bfb9", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xeccaf4e218a2ca5d2fe54e76ce1ce2a2981e6bb90ac8a5d18fcf0506d8c4bfb9", + "token": "0x4f5446498300168524db609d608b173354a5b9de", + "creator": "0xe51faf16f4ee8cc168949077ac624e745bb93720", + "quoteAsset": null, + "poolId": "0x4921c224a7aa210e7833071b0d71ea5166cc576280b0603afe62b5abb90eaa85", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xbe0808da0f66ba59efc6773ab4f01b3bb4f409de", + "positionRecipient": "0x9bed92d568d957f4b38f2a775a8cc8b0317ada56", + "positionTokenId": "355463", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x6553ff0a823c31e9d989557bca7cbb859e3e15d80eee1eddf4c673c00460c743", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x513f48cd84a0ef4d7fc10d70037499c020c4bc60eeb063128c3990e1e4df9a38:0x8b1dec528f4c1ac0882a4eff071a65c1ed44ae025fed524e041fadef3b322b35:170", + "liquidityOccurrenceId": "1:0x513f48cd84a0ef4d7fc10d70037499c020c4bc60eeb063128c3990e1e4df9a38:0x8b1dec528f4c1ac0882a4eff071a65c1ed44ae025fed524e041fadef3b322b35:171", + "initialBuyOccurrenceId": "1:0x513f48cd84a0ef4d7fc10d70037499c020c4bc60eeb063128c3990e1e4df9a38:0x8b1dec528f4c1ac0882a4eff071a65c1ed44ae025fed524e041fadef3b322b35:172", + "custodyOccurrenceId": "1:0x513f48cd84a0ef4d7fc10d70037499c020c4bc60eeb063128c3990e1e4df9a38:0x8b1dec528f4c1ac0882a4eff071a65c1ed44ae025fed524e041fadef3b322b35:173", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645140" + }, + { + "id": "1:classic-v3:0xede9f7591a75d8d6dea23acbe9d1a715701af3dfd626886fc26e909100e4d0be", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xede9f7591a75d8d6dea23acbe9d1a715701af3dfd626886fc26e909100e4d0be", + "token": "0xdd606e420c9f45fff4a27742a9a6af0e6370cbed", + "creator": "0x0a5ad9642e0ceb5ef6b6bf623072967ceca86876", + "quoteAsset": null, + "poolId": "0x26d632631a40e97b95867b3f0c31fa5652bb8e418af96e66b21121fbfb245777", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x95b1d3af8a62bfed9b13a005b842a1da052b565e", + "positionRecipient": "0xc051442f5c09605b2523ed531c10a3852dcbd7fe", + "positionTokenId": "356840", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x8aca3c677003f0d712eaa7133a880ec315d47e938c1cab2f85f32164a49df96c", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "1000000000000000", + "initialBuyTokenAmount": "729739899031511876349884", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x11a38774d47ef7e4c6a7d7b32a21b2da1d93eff94eaf088bbfc37b756b17c7a2:0x2450559eaca663af922a21885bd623562250c984c124a236d741149a6b87d141:102", + "liquidityOccurrenceId": "1:0x11a38774d47ef7e4c6a7d7b32a21b2da1d93eff94eaf088bbfc37b756b17c7a2:0x2450559eaca663af922a21885bd623562250c984c124a236d741149a6b87d141:103", + "initialBuyOccurrenceId": "1:0x11a38774d47ef7e4c6a7d7b32a21b2da1d93eff94eaf088bbfc37b756b17c7a2:0x2450559eaca663af922a21885bd623562250c984c124a236d741149a6b87d141:104", + "custodyOccurrenceId": "1:0x11a38774d47ef7e4c6a7d7b32a21b2da1d93eff94eaf088bbfc37b756b17c7a2:0x2450559eaca663af922a21885bd623562250c984c124a236d741149a6b87d141:105", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25649877" + }, + { + "id": "1:classic-v3:0xf1d55444224bcf35597cff805ef0071b533b53813b47c93a56748e4a4bfe27d1", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xf1d55444224bcf35597cff805ef0071b533b53813b47c93a56748e4a4bfe27d1", + "token": "0xdccd98d3211767f73d130a28d42f537dcf100236", + "creator": "0x51da7fd4ad2e60eb9773ab1252a9ca5db95de814", + "quoteAsset": null, + "poolId": "0x76f4e443e8858bcd7b3408429070677e6de0ef863965ad12333134919bfd2384", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xb18c715f0e9085ea2803f23516387a22a227913f", + "positionRecipient": "0xb1ce6c99a7487dcaa8a462446e56ce7a23a8c319", + "positionTokenId": "355708", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xfd0e92357e44c53e5ef67865d089b0c4be47490253474dcc1494b4878076b3c3", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0x9116c5295a51f734422ea4d07bf24bcea3242038601c5ba0d2bb0a7fffe5f32d:175", + "liquidityOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0x9116c5295a51f734422ea4d07bf24bcea3242038601c5ba0d2bb0a7fffe5f32d:176", + "initialBuyOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0x9116c5295a51f734422ea4d07bf24bcea3242038601c5ba0d2bb0a7fffe5f32d:177", + "custodyOccurrenceId": "1:0x9b16ad1925e1395476d7804f1aa64891fb3576f96ac4324294dca6188fddfb78:0x9116c5295a51f734422ea4d07bf24bcea3242038601c5ba0d2bb0a7fffe5f32d:178", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645718" + }, + { + "id": "1:classic-v3:0xf35cf79956262fbd3a3da43892502094461da8dd08ada78221284fb0260b17fc", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xf35cf79956262fbd3a3da43892502094461da8dd08ada78221284fb0260b17fc", + "token": "0xa4cb465ec8a9627404fbafe3facafff0ed80487d", + "creator": "0x195efa8b60470d9db33446b28b88e2572407b2e3", + "quoteAsset": null, + "poolId": "0x0a18460ef0366dd2a7c17edb27e69b14cd89f5a20788d1bebc18fd82eacce5f4", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa73a2e4c00bc386d3f921120b21d3bd62eb8b711", + "positionRecipient": "0x637a27eeceb1bc08980bfbba3c20fa284321b263", + "positionTokenId": "356044", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x7dcf5523334caff9cb39d81b2c28e86a2410f6bea3e6f4cc4b598b0519f786ad", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xf6fda33f9e05ede755d29191b995e4ce751cb8b9b963abbdaf63fc467552543c:0x7560244dda1a4eb76d708d8331fbcfebc0a1b96555a580ccc5dba8a412296bbe:100", + "liquidityOccurrenceId": "1:0xf6fda33f9e05ede755d29191b995e4ce751cb8b9b963abbdaf63fc467552543c:0x7560244dda1a4eb76d708d8331fbcfebc0a1b96555a580ccc5dba8a412296bbe:101", + "initialBuyOccurrenceId": "1:0xf6fda33f9e05ede755d29191b995e4ce751cb8b9b963abbdaf63fc467552543c:0x7560244dda1a4eb76d708d8331fbcfebc0a1b96555a580ccc5dba8a412296bbe:102", + "custodyOccurrenceId": "1:0xf6fda33f9e05ede755d29191b995e4ce751cb8b9b963abbdaf63fc467552543c:0x7560244dda1a4eb76d708d8331fbcfebc0a1b96555a580ccc5dba8a412296bbe:103", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646518" + }, + { + "id": "1:classic-v3:0xf7be8540afbfab612a0f301c02c6042d64b44b6b032f6fd722b4ea2a1b12e6fe", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xf7be8540afbfab612a0f301c02c6042d64b44b6b032f6fd722b4ea2a1b12e6fe", + "token": "0xdfc572ea4a605b503c19e4cb436c1b3c6d03fc9b", + "creator": "0x25d61a0a6ce4010d1c5a3b73e9ca8c9af5054589", + "quoteAsset": null, + "poolId": "0xddb9c5a164d213610a85b8accc78f59b16beeac184f3a37d8aaaa3ba920655d3", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x02aa0d07ae5b7dc9a266d628cd74254ef4de3390", + "positionRecipient": "0x6dfadd40661ec32fe44315ad58e55d70a5a721a9", + "positionTokenId": "356064", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0xacae459bf3f873b537eb26260ee662bd33283f4c1ca4c4df980082c1c4cb8154", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "47210000000000000", + "initialBuyTokenAmount": "33327185262943718980715477", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x462406ab2603b593e8caec9300abb9e85f833d6da80b524e76f878f3cbe4ed8d:0x477bbbd7dfcd6082daafaea8408e521dbed22bec545c31cc8ed77612f1787f55:233", + "liquidityOccurrenceId": "1:0x462406ab2603b593e8caec9300abb9e85f833d6da80b524e76f878f3cbe4ed8d:0x477bbbd7dfcd6082daafaea8408e521dbed22bec545c31cc8ed77612f1787f55:234", + "initialBuyOccurrenceId": "1:0x462406ab2603b593e8caec9300abb9e85f833d6da80b524e76f878f3cbe4ed8d:0x477bbbd7dfcd6082daafaea8408e521dbed22bec545c31cc8ed77612f1787f55:235", + "custodyOccurrenceId": "1:0x462406ab2603b593e8caec9300abb9e85f833d6da80b524e76f878f3cbe4ed8d:0x477bbbd7dfcd6082daafaea8408e521dbed22bec545c31cc8ed77612f1787f55:236", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646643" + }, + { + "id": "1:classic-v3:0xf861d945c7cdb3ebe9130992d87ba3423414165ee436133941e5b4e17db19fa8", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xf861d945c7cdb3ebe9130992d87ba3423414165ee436133941e5b4e17db19fa8", + "token": "0x329f892f2973939f90c6d031be4e8554aee7a032", + "creator": "0x856c1a92c879fe90e16a72c3d49fbf4b7402bb8a", + "quoteAsset": null, + "poolId": "0x09827abf0f15f6d8960b20575995570996eabbfd5cbff0eec710432ec995288e", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x20c8d375958360cbd96ee1ca62eb5ee84d5e210f", + "positionRecipient": "0xc44de89854088e1a76a0fd58af9411ced1a3ebd4", + "positionTokenId": "355808", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x78a9c6fbb75a541bb4e58d2452d7a4c2e6facea09011c3785dd5a1288c17a134", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x5bcc5f420bc0b7ad91607725b8a4a585f85cd14a4fe4eb354c789b8c0124774a:0x245e05b36a3f4c27e719242fb84c3f6f63d0d0007c3a05ae38fa13301fa8dd63:207", + "liquidityOccurrenceId": "1:0x5bcc5f420bc0b7ad91607725b8a4a585f85cd14a4fe4eb354c789b8c0124774a:0x245e05b36a3f4c27e719242fb84c3f6f63d0d0007c3a05ae38fa13301fa8dd63:208", + "initialBuyOccurrenceId": "1:0x5bcc5f420bc0b7ad91607725b8a4a585f85cd14a4fe4eb354c789b8c0124774a:0x245e05b36a3f4c27e719242fb84c3f6f63d0d0007c3a05ae38fa13301fa8dd63:209", + "custodyOccurrenceId": "1:0x5bcc5f420bc0b7ad91607725b8a4a585f85cd14a4fe4eb354c789b8c0124774a:0x245e05b36a3f4c27e719242fb84c3f6f63d0d0007c3a05ae38fa13301fa8dd63:210", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645963" + }, + { + "id": "1:classic-v3:0xfc5c21125907d27649ca4400429cb4c28058b7f34a188ac262fef3f0591ced35", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xfc5c21125907d27649ca4400429cb4c28058b7f34a188ac262fef3f0591ced35", + "token": "0xda666ad90f34d42735bd660bfeca139d38cffda3", + "creator": "0x03d0a35f7ead5d143a14b93d08027ad3be17f3f0", + "quoteAsset": null, + "poolId": "0x3693769210bf48fdbdae8f2da0c9519fe0f5c19953035f4c133def8570aa660c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x16f039fc308aba69097f9b2487418fb5aa77f07b", + "positionRecipient": "0x0c0c25be4e6401e93f2516590e630536a927c53a", + "positionTokenId": "356235", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x63b16ec9a7380d0ff3558eca8400859d3cb33f89e9eae1755afb8b8342b5be66", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x94fe6de2c0cd41139f84e96b10428a02cfda1871fcedaff4ec7d1fdd8392861f:0xe02e43476644712f980603ebe14660a88a2e70060f7be15337562bf83c6a1680:204", + "liquidityOccurrenceId": "1:0x94fe6de2c0cd41139f84e96b10428a02cfda1871fcedaff4ec7d1fdd8392861f:0xe02e43476644712f980603ebe14660a88a2e70060f7be15337562bf83c6a1680:205", + "initialBuyOccurrenceId": "1:0x94fe6de2c0cd41139f84e96b10428a02cfda1871fcedaff4ec7d1fdd8392861f:0xe02e43476644712f980603ebe14660a88a2e70060f7be15337562bf83c6a1680:206", + "custodyOccurrenceId": "1:0x94fe6de2c0cd41139f84e96b10428a02cfda1871fcedaff4ec7d1fdd8392861f:0xe02e43476644712f980603ebe14660a88a2e70060f7be15337562bf83c6a1680:207", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25647431" + }, + { + "id": "1:classic-v3:0xfcb947cbab7cf00f05be128238b1a3964b972991ba4f969a6e261448ba3bb28a", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xfcb947cbab7cf00f05be128238b1a3964b972991ba4f969a6e261448ba3bb28a", + "token": "0x94aea2c7653786623d7c89180411fa25de166600", + "creator": "0x38d7dd967a516883e49d24b884ea7784f88c1fa0", + "quoteAsset": null, + "poolId": "0xdd0304f3001b6a0543ab4b5cc2e8ae2bff246a03cf82de31db19bd6243682c44", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0x2a1398ca4ea8122e340763352f5f942f0a413a16", + "positionRecipient": "0x9fb09ab3e762a29893e2e26fb074c91909d0dd4c", + "positionTokenId": "355902", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x29525718cd43169b6e8a8b2a105b5dcec1b0fdf772fef0bc90e952a4e60ed267", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "2000000000000000", + "initialBuyTokenAmount": "1458415534058453948045650", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x1d8199f83a0611bd6a211fdf1762270742ad0313b46ed162449062009e63114f:0x96d74a7ef48299b5d8aceb9a541e8ac239810e256b9e5cf3621f44db60dc2627:1302", + "liquidityOccurrenceId": "1:0x1d8199f83a0611bd6a211fdf1762270742ad0313b46ed162449062009e63114f:0x96d74a7ef48299b5d8aceb9a541e8ac239810e256b9e5cf3621f44db60dc2627:1303", + "initialBuyOccurrenceId": "1:0x1d8199f83a0611bd6a211fdf1762270742ad0313b46ed162449062009e63114f:0x96d74a7ef48299b5d8aceb9a541e8ac239810e256b9e5cf3621f44db60dc2627:1304", + "custodyOccurrenceId": "1:0x1d8199f83a0611bd6a211fdf1762270742ad0313b46ed162449062009e63114f:0x96d74a7ef48299b5d8aceb9a541e8ac239810e256b9e5cf3621f44db60dc2627:1305", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25646134" + }, + { + "id": "1:classic-v3:0xfdf3f515c972effe7df1c1a3f382321217b6a92d804a8aad93fc753d60c932a2", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xfdf3f515c972effe7df1c1a3f382321217b6a92d804a8aad93fc753d60c932a2", + "token": "0xc3776d0a685aa5d78f81cad90c5f518c677f6089", + "creator": "0xb38ef2aa4306c8a2ce97d8718aa3dda7a0ba331c", + "quoteAsset": null, + "poolId": "0x3618ad11825cb0d5ddc498e6a1ee56467308891a097c35c603e8c616ab95188f", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xfc35655c076d6e951a5fa366a83dbdd14f278dc9", + "positionRecipient": "0x7c304c990dfdb8352350ee858e0eeab9f225d46a", + "positionTokenId": "354738", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x9a29b427918396dc602cc877a53d0e9a55652b6db6589e306b8bb7e8e9271dfa", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "600000000000000", + "initialBuyTokenAmount": "437971781612384114831424", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0x4083983c0a29fe5660bf36fe38c15b7be1259f77dcec497291e3e9c3b6f170e5:0xe5b67235e3d161524bb2851d6dd2cc401abe4533d16183c019901effb5f52259:102", + "liquidityOccurrenceId": "1:0x4083983c0a29fe5660bf36fe38c15b7be1259f77dcec497291e3e9c3b6f170e5:0xe5b67235e3d161524bb2851d6dd2cc401abe4533d16183c019901effb5f52259:103", + "initialBuyOccurrenceId": "1:0x4083983c0a29fe5660bf36fe38c15b7be1259f77dcec497291e3e9c3b6f170e5:0xe5b67235e3d161524bb2851d6dd2cc401abe4533d16183c019901effb5f52259:104", + "custodyOccurrenceId": "1:0x4083983c0a29fe5660bf36fe38c15b7be1259f77dcec497291e3e9c3b6f170e5:0xe5b67235e3d161524bb2851d6dd2cc401abe4533d16183c019901effb5f52259:105", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25641333" + }, + { + "id": "1:classic-v3:0xfedad61bc703d0ed8c1b77b76d77fb22fa1e24f0be372377830541e503fc7b4c", + "chainId": 1, + "model": "classic", + "releaseVersion": "classic-v3", + "launchHash": "0xfedad61bc703d0ed8c1b77b76d77fb22fa1e24f0be372377830541e503fc7b4c", + "token": "0xa7e9031375c5cbec57341e9f02b9f77d189250c9", + "creator": "0x4b08916a6d14ef89324c827c1fa26138a00470f2", + "quoteAsset": null, + "poolId": "0x81c96297809b34782f1c6e44a43b465332ef9b5c58bfe5510a61c1682030ae9c", + "hook": "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + "rewardVault": "0xa9ec35e18e05ec2d553db9745624a3558ffc4ab6", + "positionRecipient": "0xb2915732490a07bbf66fd28e5942c5620565eb2a", + "positionTokenId": "355554", + "totalSwapFeeBps": null, + "buySwapFeeBps": 100, + "sellSwapFeeBps": 100, + "rewardConfigurationHash": "0x61328c3219c61aa380550d1ea7d568ff8c98c7c5bbe0a69940a11eada4317820", + "quoteConfigurationHash": null, + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999987736", + "lockedTokenDust": "12264", + "initialTick": 204200, + "tickLower": -887200, + "tickUpper": 204200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "10000000000000000", + "initialBuyTokenAmount": "7249784874772468972176384", + "initialBuyEthAmount": null, + "launchOccurrenceId": "1:0xd41f6bd5f7fc5b2658fff26bf7cc612a12df966aa1055344630c73bd3cc47d03:0x8604f52294809bd21a628160e59e2d75f6ca5d88d0e4a35c72f5038f5a1a98ea:508", + "liquidityOccurrenceId": "1:0xd41f6bd5f7fc5b2658fff26bf7cc612a12df966aa1055344630c73bd3cc47d03:0x8604f52294809bd21a628160e59e2d75f6ca5d88d0e4a35c72f5038f5a1a98ea:509", + "initialBuyOccurrenceId": "1:0xd41f6bd5f7fc5b2658fff26bf7cc612a12df966aa1055344630c73bd3cc47d03:0x8604f52294809bd21a628160e59e2d75f6ca5d88d0e4a35c72f5038f5a1a98ea:510", + "custodyOccurrenceId": "1:0xd41f6bd5f7fc5b2658fff26bf7cc612a12df966aa1055344630c73bd3cc47d03:0x8604f52294809bd21a628160e59e2d75f6ca5d88d0e4a35c72f5038f5a1a98ea:511", + "coordinatorOccurrenceId": null, + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": true, + "hasCoordinatorEvent": false, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": true, + "isComplete": true, + "updatedBlock": "25645478" + }, + { + "id": "1:stock-paired-v1:0x5700d903e959f41e091dab41c1e5582cd9400a1193dad48b1f8141b0cc29ac3b", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v1", + "launchHash": "0x5700d903e959f41e091dab41c1e5582cd9400a1193dad48b1f8141b0cc29ac3b", + "token": "0x3c82787014931bd11b9edb789e42f92d792dd07f", + "creator": "0xfa5f17389ca28d071781d59750b32c842ab6a54b", + "quoteAsset": "0xf3e4872e6a4cf365888d93b6146a2baa7348f1a4", + "poolId": "0x96422749017b39aa837a3c1757076afe4c3ea35a04097455e9b2e52a83195bac", + "hook": "0x7773d183fe7b60d4f1885047fa42b815a62fe0cc", + "rewardVault": "0x3fe2b69cde6e981365f0f3989dde4ea397b1a657", + "positionRecipient": "0x51f9e203d60415a8db230bb740701ad00343b106", + "positionTokenId": "354306", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xce5c094aa486b0e135e3ac2c1c65b1e2934e635f8c59a85d3d19eb2f1c78e021", + "quoteConfigurationHash": "0x5afb0aeb9f69d08716d71417c86f86447064486b8a8b4dce954fc7c369e4bcba", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994664", + "lockedTokenDust": "5336", + "initialTick": -191200, + "tickLower": -191200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "21767614500788826", + "initialBuyTokenAmount": "4313838078413982337443858", + "initialBuyEthAmount": "600000000000000", + "launchOccurrenceId": "1:0x8172ac34f638aa3a35728f233f2a380a96d36006e9c6591b4098a7ff4791003f:0x02a71e6e1854143e3e298edb64af7e5486465829a8e7f33078b75a32aa06ed90:316", + "liquidityOccurrenceId": "1:0x8172ac34f638aa3a35728f233f2a380a96d36006e9c6591b4098a7ff4791003f:0x02a71e6e1854143e3e298edb64af7e5486465829a8e7f33078b75a32aa06ed90:317", + "initialBuyOccurrenceId": "1:0x8172ac34f638aa3a35728f233f2a380a96d36006e9c6591b4098a7ff4791003f:0x02a71e6e1854143e3e298edb64af7e5486465829a8e7f33078b75a32aa06ed90:318", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x8172ac34f638aa3a35728f233f2a380a96d36006e9c6591b4098a7ff4791003f:0x02a71e6e1854143e3e298edb64af7e5486465829a8e7f33078b75a32aa06ed90:321", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25638549" + }, + { + "id": "1:stock-paired-v2:0x2f5dc5c369e5e1f33e664e15c96cf2d33a00bc55f1acf2b14788f14602604cb4", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0x2f5dc5c369e5e1f33e664e15c96cf2d33a00bc55f1acf2b14788f14602604cb4", + "token": "0x13e3a7d6af491dba367dcdf5cb9ef544ad1dc513", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0xb7c5a711b29329e0f4798fd51dec21483fa319178a905e70138028e269e3698d", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xc1389a0813420be81292245f9ba98bdcd329c307", + "positionRecipient": "0x2941def51b4a0c0e72823f4914a28531fbe4ab30", + "positionTokenId": "354946", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x51dcd794e2a354941ad76526c410dd8b81ae4eee8e39fec3d2e6095ed8e5343a", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994664", + "lockedTokenDust": "5336", + "initialTick": -191200, + "tickLower": -191200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "98871502129207208", + "initialBuyTokenAmount": "19299151445021512650002314", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x85cb7872a3a86d009687ff028c645c5d2cbcc447b3d224fdffc2076352f8ef22:0x72a5155df768dbd68c5bc75672872ae94ea3a44f3c18abd819a31b6cd98c3bd5:81", + "liquidityOccurrenceId": "1:0x85cb7872a3a86d009687ff028c645c5d2cbcc447b3d224fdffc2076352f8ef22:0x72a5155df768dbd68c5bc75672872ae94ea3a44f3c18abd819a31b6cd98c3bd5:82", + "initialBuyOccurrenceId": "1:0x85cb7872a3a86d009687ff028c645c5d2cbcc447b3d224fdffc2076352f8ef22:0x72a5155df768dbd68c5bc75672872ae94ea3a44f3c18abd819a31b6cd98c3bd5:83", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x85cb7872a3a86d009687ff028c645c5d2cbcc447b3d224fdffc2076352f8ef22:0x72a5155df768dbd68c5bc75672872ae94ea3a44f3c18abd819a31b6cd98c3bd5:86", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25642485" + }, + { + "id": "1:stock-paired-v2:0x49e441f535f82be6234dea14f23e52154669b2ca0c8f98747dc95f6a6e099312", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0x49e441f535f82be6234dea14f23e52154669b2ca0c8f98747dc95f6a6e099312", + "token": "0x369f5fa21942560c42ba9fdb8a156f5c962bd2ec", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0xf3e4872e6a4cf365888d93b6146a2baa7348f1a4", + "poolId": "0x4c58ab386fb3a362f5734f50bc3e4660624805a60e94a7a2aa5b3bfd0b9b0242", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xedba500f363015e287d01f54f2521938a530188e", + "positionRecipient": "0xef0165cfb29579969cd7bb48967206ac528dbf29", + "positionTokenId": "354667", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xb8dd9c9192b374dce432c911f115f937de9017dbed0746b3b0d0e2e72faf8853", + "quoteConfigurationHash": "0xdb9c04a4d6149d2a9eedbad638381942097a29be33ef2d70647e0c6edb4fda51", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994664", + "lockedTokenDust": "5336", + "initialTick": -191200, + "tickLower": -191200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "21267443759664973", + "initialBuyTokenAmount": "4215133611643120124250951", + "initialBuyEthAmount": "600000000000000", + "launchOccurrenceId": "1:0xa16a0d36cde8f79db93e31e98337c1898e2c6fb7e8b5dbc36d3752a375a60287:0xc45d348083c53afaf79f056f1ea5529e9410ac3faa954a5c8ef7272a6371ec83:948", + "liquidityOccurrenceId": "1:0xa16a0d36cde8f79db93e31e98337c1898e2c6fb7e8b5dbc36d3752a375a60287:0xc45d348083c53afaf79f056f1ea5529e9410ac3faa954a5c8ef7272a6371ec83:949", + "initialBuyOccurrenceId": "1:0xa16a0d36cde8f79db93e31e98337c1898e2c6fb7e8b5dbc36d3752a375a60287:0xc45d348083c53afaf79f056f1ea5529e9410ac3faa954a5c8ef7272a6371ec83:950", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xa16a0d36cde8f79db93e31e98337c1898e2c6fb7e8b5dbc36d3752a375a60287:0xc45d348083c53afaf79f056f1ea5529e9410ac3faa954a5c8ef7272a6371ec83:953", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25640676" + }, + { + "id": "1:stock-paired-v2:0x4f3a4a1d87d06dafb214d3cdc03b6157abfeeaf9cd3110a5500230fff247b35a", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0x4f3a4a1d87d06dafb214d3cdc03b6157abfeeaf9cd3110a5500230fff247b35a", + "token": "0xf255daaa1927f8c1d3999c718e61ca5825fa1306", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0x2ea5177263b76ed7d14650863cf62f0be25ad76470e809cc6eba777bfe82e8e6", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xa983fbbb4c7d54f106c3b7d3751e86cf70507540", + "positionRecipient": "0xb7fd41cfeaf032dc1bcda51cacf27c7de64c1753", + "positionTokenId": "354906", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x0c36f5d7ebbe8d657c076a6c76447a5caf056b40b350cda3415b0c1092ba5891", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994664", + "lockedTokenDust": "5336", + "initialTick": -191200, + "tickLower": -191200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "186712722767474969", + "initialBuyTokenAmount": "35830894826036498059849959", + "initialBuyEthAmount": "30000000000000000", + "launchOccurrenceId": "1:0x6ae6b0daa49763f5aa574fef5dff3ed8cd07574f0cfac09afa7692dcdc6ebe6f:0xd49746082b301c1dff6222ffb9e6a08628c3837d111c3cf538bec3ec7b747259:132", + "liquidityOccurrenceId": "1:0x6ae6b0daa49763f5aa574fef5dff3ed8cd07574f0cfac09afa7692dcdc6ebe6f:0xd49746082b301c1dff6222ffb9e6a08628c3837d111c3cf538bec3ec7b747259:133", + "initialBuyOccurrenceId": "1:0x6ae6b0daa49763f5aa574fef5dff3ed8cd07574f0cfac09afa7692dcdc6ebe6f:0xd49746082b301c1dff6222ffb9e6a08628c3837d111c3cf538bec3ec7b747259:134", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x6ae6b0daa49763f5aa574fef5dff3ed8cd07574f0cfac09afa7692dcdc6ebe6f:0xd49746082b301c1dff6222ffb9e6a08628c3837d111c3cf538bec3ec7b747259:137", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25642266" + }, + { + "id": "1:stock-paired-v2:0x653457e36314263c4641240f9c05a57509edc6d9b5358385f96dec1c3a905339", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0x653457e36314263c4641240f9c05a57509edc6d9b5358385f96dec1c3a905339", + "token": "0xa85b0843cf7fb272130e4c369e5cc1708a5a423c", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0xb1ac4dc8fba22284c3f1ae51a4af428005cc39defa699c01ed378177312e1021", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xb174bb16d74f4677e55b889a7001e1167e737049", + "positionRecipient": "0x695148b1eb553131987b8af21a2c3037cb3cacaf", + "positionTokenId": "355720", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x6b330d5ae91e6a144b4ea1bb47ab22a643e089c72ec25321b34180136dd428de", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994684", + "lockedTokenDust": "5316", + "initialTick": 191200, + "tickLower": -887200, + "tickUpper": 191200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "98557959483142554", + "initialBuyTokenAmount": "19239127182562736130697505", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x5066b7abf547d9d256ea2afd267d489144825dd2f7eb51a8a0cf4c5151c08cf9:0xdfe9b68b48a7d8f2a06df9eb2436f573cc04cd94e93bf0655055bc2073147fbc:515", + "liquidityOccurrenceId": "1:0x5066b7abf547d9d256ea2afd267d489144825dd2f7eb51a8a0cf4c5151c08cf9:0xdfe9b68b48a7d8f2a06df9eb2436f573cc04cd94e93bf0655055bc2073147fbc:516", + "initialBuyOccurrenceId": "1:0x5066b7abf547d9d256ea2afd267d489144825dd2f7eb51a8a0cf4c5151c08cf9:0xdfe9b68b48a7d8f2a06df9eb2436f573cc04cd94e93bf0655055bc2073147fbc:517", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x5066b7abf547d9d256ea2afd267d489144825dd2f7eb51a8a0cf4c5151c08cf9:0xdfe9b68b48a7d8f2a06df9eb2436f573cc04cd94e93bf0655055bc2073147fbc:520", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25645727" + }, + { + "id": "1:stock-paired-v2:0x8f7cf6d844d4aaa85312456b3a3f2b74a9c84e4817acf733196f6c4099eb7e36", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0x8f7cf6d844d4aaa85312456b3a3f2b74a9c84e4817acf733196f6c4099eb7e36", + "token": "0x65cbe55386e4bb35fca4365df64179b1e07bb6ab", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0x1f5fc5c3c8b0f15c7e21af623936ff2b210b6415", + "poolId": "0x5686df948e60159d539018ae19d2ff244a5088d0109b74db24caef3f95523b91", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x98cc763084112656478c0c3ee1e51baed99ab2c6", + "positionRecipient": "0x815ff68db06097744bc75f1c6b626fe60a948ebd", + "positionTokenId": "354846", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x7741eb96e8b5b60d46828a8d382241d89a273c40e5238f998be23cd701eea7f5", + "quoteConfigurationHash": "0x2b6d004d4f484b709c475d4a408858948e039c231ff72ce331b663ff6310eca1", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994684", + "lockedTokenDust": "5316", + "initialTick": 191200, + "tickLower": -887200, + "tickUpper": 191200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "724439552073836488", + "initialBuyTokenAmount": "126018656731602658439119113", + "initialBuyEthAmount": "50000000000000000", + "launchOccurrenceId": "1:0x175df0b7427e18e392a039c85f348c74f1c9f2e239479ca60dbc6abfbf4002c4:0x8c26d38ae601fc607e7d9822e09e6019451dff3a470647266a2b1783dc5a2090:352", + "liquidityOccurrenceId": "1:0x175df0b7427e18e392a039c85f348c74f1c9f2e239479ca60dbc6abfbf4002c4:0x8c26d38ae601fc607e7d9822e09e6019451dff3a470647266a2b1783dc5a2090:353", + "initialBuyOccurrenceId": "1:0x175df0b7427e18e392a039c85f348c74f1c9f2e239479ca60dbc6abfbf4002c4:0x8c26d38ae601fc607e7d9822e09e6019451dff3a470647266a2b1783dc5a2090:354", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x175df0b7427e18e392a039c85f348c74f1c9f2e239479ca60dbc6abfbf4002c4:0x8c26d38ae601fc607e7d9822e09e6019451dff3a470647266a2b1783dc5a2090:357", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25641953" + }, + { + "id": "1:stock-paired-v2:0xdb6bcd8648aab014ec3121a9090b4af9b3a46a2bfa81b0ac50c3b6e26ba09e08", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0xdb6bcd8648aab014ec3121a9090b4af9b3a46a2bfa81b0ac50c3b6e26ba09e08", + "token": "0x3553f3caf5d4ecb593df2656bbb8cdd118890e45", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0xf3e4872e6a4cf365888d93b6146a2baa7348f1a4", + "poolId": "0x976cadf0c00970def3229dccc6bab9c2b0ecf275c867641ab74064074a8ca2a4", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x733e44b9185ba6a0693cf091dd7ffa0adfc56ed9", + "positionRecipient": "0x2071896c7dd9032bd5b556a43d75c8de052cdf8f", + "positionTokenId": "355954", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x4708373198131eddfb88e1aa43bc20b13450114cec6eeb23740dbfa4b6f8a7dc", + "quoteConfigurationHash": "0xdb9c04a4d6149d2a9eedbad638381942097a29be33ef2d70647e0c6edb4fda51", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994664", + "lockedTokenDust": "5336", + "initialTick": -191200, + "tickLower": -191200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "362020358219834042", + "initialBuyTokenAmount": "67211950369201229915092847", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x9817c330a494baf1ccd4f07f99a041338e7bc357ea69ceeabc339f45af2cb8b1:0x440a08fb7e00ecd039c2594faf7ce085fd4019803b5b90cd3c2b8aad6cf014a5:327", + "liquidityOccurrenceId": "1:0x9817c330a494baf1ccd4f07f99a041338e7bc357ea69ceeabc339f45af2cb8b1:0x440a08fb7e00ecd039c2594faf7ce085fd4019803b5b90cd3c2b8aad6cf014a5:328", + "initialBuyOccurrenceId": "1:0x9817c330a494baf1ccd4f07f99a041338e7bc357ea69ceeabc339f45af2cb8b1:0x440a08fb7e00ecd039c2594faf7ce085fd4019803b5b90cd3c2b8aad6cf014a5:329", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x9817c330a494baf1ccd4f07f99a041338e7bc357ea69ceeabc339f45af2cb8b1:0x440a08fb7e00ecd039c2594faf7ce085fd4019803b5b90cd3c2b8aad6cf014a5:332", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646261" + }, + { + "id": "1:stock-paired-v2:0xf03e343145583e84e6c2eb413d19e8b7674dd2a50817ade8212165e0970aaeaa", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0xf03e343145583e84e6c2eb413d19e8b7674dd2a50817ade8212165e0970aaeaa", + "token": "0xa7c9c2ebd906a3f0836c564e3f25a7a1ae7ea02c", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x67a098700a1948824aa46b20cc70a6a303c5400c9db20ae0ee68f22dcfe5e2a8", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x66b891ec069129b3fb053669e36b459fc314dbce", + "positionRecipient": "0x786191b5a5a7dccc4eccfadc98cfce7b99eb4d93", + "positionTokenId": "355608", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x7bac0fa755e410be8c6ab1586c74a0439ebf8a2427c8b6cdeb5a5ea19b7a67ab", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994684", + "lockedTokenDust": "5316", + "initialTick": 191200, + "tickLower": -887200, + "tickUpper": 191200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "210851994095052123", + "initialBuyTokenAmount": "40276735766026130260984632", + "initialBuyEthAmount": "21600000000000000", + "launchOccurrenceId": "1:0x1e9ec9537c0926f649d131290c828d6810bf2ca6efcf1f583df45a98d092da24:0xd243eb7d5e2591e282829429bcfd4714c19f9adf94de15a496f83388597014a6:674", + "liquidityOccurrenceId": "1:0x1e9ec9537c0926f649d131290c828d6810bf2ca6efcf1f583df45a98d092da24:0xd243eb7d5e2591e282829429bcfd4714c19f9adf94de15a496f83388597014a6:675", + "initialBuyOccurrenceId": "1:0x1e9ec9537c0926f649d131290c828d6810bf2ca6efcf1f583df45a98d092da24:0xd243eb7d5e2591e282829429bcfd4714c19f9adf94de15a496f83388597014a6:676", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x1e9ec9537c0926f649d131290c828d6810bf2ca6efcf1f583df45a98d092da24:0xd243eb7d5e2591e282829429bcfd4714c19f9adf94de15a496f83388597014a6:679", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25645566" + }, + { + "id": "1:stock-paired-v2:0xf7081331212fb2d81242ee8afa941d4ef774f8b17260ed109b30712a783080af", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v2", + "launchHash": "0xf7081331212fb2d81242ee8afa941d4ef774f8b17260ed109b30712a783080af", + "token": "0xff7bd587109ed8e022a510c5e29dd3bcdeb6b1fe", + "creator": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x3f7c47068d7f8c6dbea94d6a1dd827bc0451c16e286c5db323091c2b8e65119e", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xfe79fe68e713e9414ce1ceec7f5ccb2c944fcc23", + "positionRecipient": "0xed2173a23a64e7839073d2e0bb87204a923abf7d", + "positionTokenId": "355856", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x8a0fe6893bdf25eb11e5a933b8bbd09bb9c4c40f36e052bf51b87c4cdb22fa74", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994684", + "lockedTokenDust": "5316", + "initialTick": 191200, + "tickLower": -887200, + "tickUpper": 191200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "51554097903743893", + "initialBuyTokenAmount": "10156874677241341917353897", + "initialBuyEthAmount": "20000000000000000", + "launchOccurrenceId": "1:0xc4d1eb4f4fd4f19cbf7ffc182b237ad3349e88c5eeb06bd6ed782b3597e3b773:0xc47e44a3c813b1904a2d00f5f93268a8ddcaf98ca2416d5a5e2cb40aa1e106dc:1007", + "liquidityOccurrenceId": "1:0xc4d1eb4f4fd4f19cbf7ffc182b237ad3349e88c5eeb06bd6ed782b3597e3b773:0xc47e44a3c813b1904a2d00f5f93268a8ddcaf98ca2416d5a5e2cb40aa1e106dc:1008", + "initialBuyOccurrenceId": "1:0xc4d1eb4f4fd4f19cbf7ffc182b237ad3349e88c5eeb06bd6ed782b3597e3b773:0xc47e44a3c813b1904a2d00f5f93268a8ddcaf98ca2416d5a5e2cb40aa1e106dc:1009", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xc4d1eb4f4fd4f19cbf7ffc182b237ad3349e88c5eeb06bd6ed782b3597e3b773:0xc47e44a3c813b1904a2d00f5f93268a8ddcaf98ca2416d5a5e2cb40aa1e106dc:1012", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646014" + }, + { + "id": "1:stock-paired-v3:0x0329b08cc21cfd2e61e7c80c21bf69adf05d41b141494592c0e12cd0f06dba6f", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x0329b08cc21cfd2e61e7c80c21bf69adf05d41b141494592c0e12cd0f06dba6f", + "token": "0x0c42cc855ad919717e1bb3618a440f6a59a5ca16", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0xed7b4375a0539d98bb70f0ba50bfea59d8d74a286e25d6be43c64da419ad3194", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x0d5d44d20bf281e93ea1b1f278253b054afc66af", + "positionRecipient": "0xdf701b67ec700caccd982eba492913dd7b417e5d", + "positionTokenId": "356018", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xd6cf98f5de1deda70091a34c0330a027f94d56dbbb94e55c7cc24b0edea6cf25", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "729432303821568158", + "initialBuyTokenAmount": "169415158214291526792417247", + "initialBuyEthAmount": "300000000000000000", + "launchOccurrenceId": "1:0x474014ff30cb4b0a06493198548343eef4c67dbece578b7d573479940a9470fa:0x3c91042b70e7804145b57a43d3b1b6b4fda8ebbc64713c1a9b18274dec0838bc:32", + "liquidityOccurrenceId": "1:0x474014ff30cb4b0a06493198548343eef4c67dbece578b7d573479940a9470fa:0x3c91042b70e7804145b57a43d3b1b6b4fda8ebbc64713c1a9b18274dec0838bc:33", + "initialBuyOccurrenceId": "1:0x474014ff30cb4b0a06493198548343eef4c67dbece578b7d573479940a9470fa:0x3c91042b70e7804145b57a43d3b1b6b4fda8ebbc64713c1a9b18274dec0838bc:34", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x474014ff30cb4b0a06493198548343eef4c67dbece578b7d573479940a9470fa:0x3c91042b70e7804145b57a43d3b1b6b4fda8ebbc64713c1a9b18274dec0838bc:37", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646407" + }, + { + "id": "1:stock-paired-v3:0x0677820a202c395b72da3f60022cf9e318ad86d2c9680ab3663fd4dab140a8ae", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x0677820a202c395b72da3f60022cf9e318ad86d2c9680ab3663fd4dab140a8ae", + "token": "0x13a8df8a098343cc920e5b35af0d763f1a6d9c27", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x2140801a9ef46614b63ad384a6c11b62deb708a64f794c2086bbb0acedeb1959", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x66ea68d6f09a8d859e7817cb0fd2d051ff9c9eaa", + "positionRecipient": "0x34acaebe03badf64e67763b39a768e0edd2012f1", + "positionTokenId": "357041", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x3615eedb9d2d6c17ef066c5df5dd9ddee7aaf20d103bafd23517f9dd3ccd5750", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "95755546987690407", + "initialBuyTokenAmount": "6962852550858291750477469", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x3a509c29aaef823668725157cfab95fba31bbd994bc2f5bcfc1050d46eccf6a6:0x8fb609c1511099a99bccbb9a8eb42a54c3c4142e60ef29442f104ca1568d628e:121", + "liquidityOccurrenceId": "1:0x3a509c29aaef823668725157cfab95fba31bbd994bc2f5bcfc1050d46eccf6a6:0x8fb609c1511099a99bccbb9a8eb42a54c3c4142e60ef29442f104ca1568d628e:122", + "initialBuyOccurrenceId": "1:0x3a509c29aaef823668725157cfab95fba31bbd994bc2f5bcfc1050d46eccf6a6:0x8fb609c1511099a99bccbb9a8eb42a54c3c4142e60ef29442f104ca1568d628e:123", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x3a509c29aaef823668725157cfab95fba31bbd994bc2f5bcfc1050d46eccf6a6:0x8fb609c1511099a99bccbb9a8eb42a54c3c4142e60ef29442f104ca1568d628e:126", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25650885" + }, + { + "id": "1:stock-paired-v3:0x0e23f2508a69495a00eaff11f8d33d213a054da585df710758fa7d0899c49569", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x0e23f2508a69495a00eaff11f8d33d213a054da585df710758fa7d0899c49569", + "token": "0x0e154944d68d210ed70cf55c9fa45fd862e0086a", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x7e611afe1ed195a7735de70dc47d370cf3d61701bfa53b0abf8b9a581fbf24cb", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xa4134d572eb38972363bd2dd2c3f4341d69f1d1e", + "positionRecipient": "0x7d32dcd16a1bb59fe2c889a659ae914cebf7a2b5", + "positionTokenId": "356026", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x012fac53979bb28893842fb29405ef3ba96f6aec49d43e4ba4fbec0837d48e09", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "48154937452610984", + "initialBuyTokenAmount": "3513742236605908289554944", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x10fb6c2520d643ad04ec384ff5e022993c3c76d1a601bea67c4f9d000eecaf8e:0xe426631e76642c942205c3708b71daf3dad3b876666ac9d31129ce9c9bb322e6:294", + "liquidityOccurrenceId": "1:0x10fb6c2520d643ad04ec384ff5e022993c3c76d1a601bea67c4f9d000eecaf8e:0xe426631e76642c942205c3708b71daf3dad3b876666ac9d31129ce9c9bb322e6:295", + "initialBuyOccurrenceId": "1:0x10fb6c2520d643ad04ec384ff5e022993c3c76d1a601bea67c4f9d000eecaf8e:0xe426631e76642c942205c3708b71daf3dad3b876666ac9d31129ce9c9bb322e6:296", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x10fb6c2520d643ad04ec384ff5e022993c3c76d1a601bea67c4f9d000eecaf8e:0xe426631e76642c942205c3708b71daf3dad3b876666ac9d31129ce9c9bb322e6:299", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646444" + }, + { + "id": "1:stock-paired-v3:0x125293294c7d8add7c30b4082103ef825a8e75a9068a8fcecdd24e0f3a57ed52", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x125293294c7d8add7c30b4082103ef825a8e75a9068a8fcecdd24e0f3a57ed52", + "token": "0x0b142287759bdf24e5ad9ee461eea118677773fb", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x14c3abf95cb9c93a8b82c1cdcb76d72cb87b2d4c", + "poolId": "0xf9deaddaa86e292aeaa9fe1c1178332d90fa8ebdd9298e36f17f04c2ea0123d7", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x307cc2813ea78c0877e9bc6757b05766ad246320", + "positionRecipient": "0xda85f0bfe1ef34266e0606fca12e1e6f3dab7b91", + "positionTokenId": "355967", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x78faad4a07596f153fa8ac22abc5dff6467bc126433f6cc2edfb4e94adcf3c2a", + "quoteConfigurationHash": "0xb0e7d1e939706011d11b9c67e14f9c0862e9749f5d710f193708f719137b7a62", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999992417", + "lockedTokenDust": "7583", + "initialTick": -187000, + "tickLower": -187000, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "236574398292020538", + "initialBuyTokenAmount": "30010333369573672773916288", + "initialBuyEthAmount": "40000000000000000", + "launchOccurrenceId": "1:0x121e059996726f2017eec251624ab01ea2a30c0bd269ecbda895347371654ac5:0xf01261aae6cb3cbf8a5a9456689f4e9fa8c77a5069e406312f6c362eefee1785:477", + "liquidityOccurrenceId": "1:0x121e059996726f2017eec251624ab01ea2a30c0bd269ecbda895347371654ac5:0xf01261aae6cb3cbf8a5a9456689f4e9fa8c77a5069e406312f6c362eefee1785:478", + "initialBuyOccurrenceId": "1:0x121e059996726f2017eec251624ab01ea2a30c0bd269ecbda895347371654ac5:0xf01261aae6cb3cbf8a5a9456689f4e9fa8c77a5069e406312f6c362eefee1785:479", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x121e059996726f2017eec251624ab01ea2a30c0bd269ecbda895347371654ac5:0xf01261aae6cb3cbf8a5a9456689f4e9fa8c77a5069e406312f6c362eefee1785:482", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646284" + }, + { + "id": "1:stock-paired-v3:0x156c8836a0e4236f05510e432a2c99272d1681fab982ad7da684220fd85f25f1", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x156c8836a0e4236f05510e432a2c99272d1681fab982ad7da684220fd85f25f1", + "token": "0x226827526a022b65d44bd136bbb4d5df1c248ffc", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0xa69ad7dae87f0aedfecf491fe7e65c90eeaa937cb61109249905bf8582bcab48", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x261a9a464b19872b3b5b2faa2bfdb868cefc9dd4", + "positionRecipient": "0xbd9099832ffad208c1fefbaa22505fd0c744e7a0", + "positionTokenId": "356010", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xf87323a68fa535ac6045c577f8615f7e8100c3eb1ba449c188c2aa1f1980cdc5", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "481542226923674154", + "initialBuyTokenAmount": "34059821681364712773859434", + "initialBuyEthAmount": "50000000000000000", + "launchOccurrenceId": "1:0x9b66a1366edca1cc239748ce4f74e644944ccee5816e04603ceb8d6bad5c5ef8:0x57269c513e6aa1dadcbe8e09a8e24f3364093339cc0ee0aab40ab43c9c4a6403:523", + "liquidityOccurrenceId": "1:0x9b66a1366edca1cc239748ce4f74e644944ccee5816e04603ceb8d6bad5c5ef8:0x57269c513e6aa1dadcbe8e09a8e24f3364093339cc0ee0aab40ab43c9c4a6403:524", + "initialBuyOccurrenceId": "1:0x9b66a1366edca1cc239748ce4f74e644944ccee5816e04603ceb8d6bad5c5ef8:0x57269c513e6aa1dadcbe8e09a8e24f3364093339cc0ee0aab40ab43c9c4a6403:525", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x9b66a1366edca1cc239748ce4f74e644944ccee5816e04603ceb8d6bad5c5ef8:0x57269c513e6aa1dadcbe8e09a8e24f3364093339cc0ee0aab40ab43c9c4a6403:528", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646384" + }, + { + "id": "1:stock-paired-v3:0x26442d0657af85780239c3fba19674e02bf8f391dce5011ac54fbf3c9ae667d7", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x26442d0657af85780239c3fba19674e02bf8f391dce5011ac54fbf3c9ae667d7", + "token": "0x0ffd8add68ed4d1c3305baa2bf66b5d6440206f7", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x90ce3686a9d9de5c95566eee991eee11fb13c1175c7c49e239f8899181b2bccd", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xb20c142c9bde9e37093def04c10611b670bdaffd", + "positionRecipient": "0xc49946b2e320a3740f7d7670b5b3041aff7cd9b6", + "positionTokenId": "356041", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x447c64f3e1daa996ecf2fe8bafca81cc6eb8e11b30a173cb470a538b4833c7a2", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "257005050215639401", + "initialBuyTokenAmount": "67047769819959269238141925", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0xa6d84ea28a2e0acecdc14a68ff8994e979c764b4d39318c743f9a66349370731:0xe1a7a0ee4f46507cb6fd89437dd717663d2a3ebcebcb3b240baad5bafa6d2147:44", + "liquidityOccurrenceId": "1:0xa6d84ea28a2e0acecdc14a68ff8994e979c764b4d39318c743f9a66349370731:0xe1a7a0ee4f46507cb6fd89437dd717663d2a3ebcebcb3b240baad5bafa6d2147:45", + "initialBuyOccurrenceId": "1:0xa6d84ea28a2e0acecdc14a68ff8994e979c764b4d39318c743f9a66349370731:0xe1a7a0ee4f46507cb6fd89437dd717663d2a3ebcebcb3b240baad5bafa6d2147:46", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xa6d84ea28a2e0acecdc14a68ff8994e979c764b4d39318c743f9a66349370731:0xe1a7a0ee4f46507cb6fd89437dd717663d2a3ebcebcb3b240baad5bafa6d2147:49", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646494" + }, + { + "id": "1:stock-paired-v3:0x27f49ec995bd6e2449a846a00a378bd6e0dc021f4ac2c3cc7d725c5357eccf77", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x27f49ec995bd6e2449a846a00a378bd6e0dc021f4ac2c3cc7d725c5357eccf77", + "token": "0x7075ddc6b9d265a372b697296a9114ed1af3f9d7", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x5a7b63709b93ffefa41e27af07c0bc3f8caf860ebbd18d75e079ba910e03cdc4", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x7689d349bfb6098f705084733d08d8780ff31f26", + "positionRecipient": "0x93655a65bfb7cfd0b11b411982ac6fe80dfa68d0", + "positionTokenId": "356035", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x4e4425d4949acf4db984e13f836f6e491f7206654556005a68b605a25ae09109", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "255317315223008101", + "initialBuyTokenAmount": "66636811665301669544900102", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0x7402cd76c3280f4e210576ab71dc08a08cf73d1c326c6526c7690b9274938176:0x2c4af90d1e1d20da9be2c9decb8fffce74e1bbc9fb136d448fb1fce2077b86c9:29", + "liquidityOccurrenceId": "1:0x7402cd76c3280f4e210576ab71dc08a08cf73d1c326c6526c7690b9274938176:0x2c4af90d1e1d20da9be2c9decb8fffce74e1bbc9fb136d448fb1fce2077b86c9:30", + "initialBuyOccurrenceId": "1:0x7402cd76c3280f4e210576ab71dc08a08cf73d1c326c6526c7690b9274938176:0x2c4af90d1e1d20da9be2c9decb8fffce74e1bbc9fb136d448fb1fce2077b86c9:31", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x7402cd76c3280f4e210576ab71dc08a08cf73d1c326c6526c7690b9274938176:0x2c4af90d1e1d20da9be2c9decb8fffce74e1bbc9fb136d448fb1fce2077b86c9:34", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646471" + }, + { + "id": "1:stock-paired-v3:0x2d056c0b921e087947c29cc65ebb7d3c2eedae0d144271db05380ee1fbb5e703", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x2d056c0b921e087947c29cc65ebb7d3c2eedae0d144271db05380ee1fbb5e703", + "token": "0x038f8351d445414a9dd6ccbace1808cc446c667d", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x14c3abf95cb9c93a8b82c1cdcb76d72cb87b2d4c", + "poolId": "0xd3860bd11c048b908db57e5f22ddca7c4dfe75e1dcf82b3d1df883b2ae79c1e9", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x4a4bc7e35c5d4acddcccec598905ee672561b4e1", + "positionRecipient": "0x125abe23dbdcbbe48eec65f3ed09d94caa66197f", + "positionTokenId": "355978", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x6ebdb56b70b07d576b277344f0d63c99a62780cf98c10c467a069014d5e539d0", + "quoteConfigurationHash": "0xb0e7d1e939706011d11b9c67e14f9c0862e9749f5d710f193708f719137b7a62", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999992417", + "lockedTokenDust": "7583", + "initialTick": -187000, + "tickLower": -187000, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "222621898146715706", + "initialBuyTokenAmount": "28290479415491387237066104", + "initialBuyEthAmount": "39000000000000000", + "launchOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xb5fef54843b095c231dc746f90795069f934e1966e89c41eef6b0f63bb7b1a16:149", + "liquidityOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xb5fef54843b095c231dc746f90795069f934e1966e89c41eef6b0f63bb7b1a16:150", + "initialBuyOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xb5fef54843b095c231dc746f90795069f934e1966e89c41eef6b0f63bb7b1a16:151", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xb5fef54843b095c231dc746f90795069f934e1966e89c41eef6b0f63bb7b1a16:154", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646305" + }, + { + "id": "1:stock-paired-v3:0x2ee3a3d02bfd3286276511c8b814e8118aee4fa0c9c624a158fc738974c5b721", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x2ee3a3d02bfd3286276511c8b814e8118aee4fa0c9c624a158fc738974c5b721", + "token": "0xd0d5bca9eaa78a5056a28d4064438af3fa1352a9", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0xcb92348c4eabd21a0a45a89606b9fa6fc671f532e367e285122b0fa88cff5fee", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xbab34d788f96d928a10a6d515494a58cd850d8d4", + "positionRecipient": "0xffab4503791b8f1a2f25a4700d269d910a295334", + "positionTokenId": "356837", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x6a1a36a3609356905899bbe15a7c4bac13e37e4db6a12e4378316a3cda68a76d", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "12676596501183404", + "initialBuyTokenAmount": "3532231787050348509558622", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x333869646bdd5df10a9f5abc434bb722579a0d26baffca69f9320b3293a8fe0a:0xd4a7cd5b0c22cbe4595d9840c23cea7b24202915bf39e7fdbd835f50fac14ae4:1265", + "liquidityOccurrenceId": "1:0x333869646bdd5df10a9f5abc434bb722579a0d26baffca69f9320b3293a8fe0a:0xd4a7cd5b0c22cbe4595d9840c23cea7b24202915bf39e7fdbd835f50fac14ae4:1266", + "initialBuyOccurrenceId": "1:0x333869646bdd5df10a9f5abc434bb722579a0d26baffca69f9320b3293a8fe0a:0xd4a7cd5b0c22cbe4595d9840c23cea7b24202915bf39e7fdbd835f50fac14ae4:1267", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x333869646bdd5df10a9f5abc434bb722579a0d26baffca69f9320b3293a8fe0a:0xd4a7cd5b0c22cbe4595d9840c23cea7b24202915bf39e7fdbd835f50fac14ae4:1270", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25649864" + }, + { + "id": "1:stock-paired-v3:0x352df2e41e33728dbda751dfe2752e747dc21580fdade6eaed3f81a78db1c17a", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x352df2e41e33728dbda751dfe2752e747dc21580fdade6eaed3f81a78db1c17a", + "token": "0x2aaa42c492ef434dcde149a9321402bf2c7537c5", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0xfaf099bce8fb926c52766c4b0099469a0dcceb3bbaee718b96a1332d9a05f753", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x7a448a6a67ec642c23e7e123414970ae3d8b237c", + "positionRecipient": "0x771c7e5f7d433dbd702f19da65d42d06883b87bb", + "positionTokenId": "355995", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x8d2bfa65596ad55b0ce8f3b6829541cd0192dd8614f3ff7cfc9ddb93059bea55", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "48144849017426173", + "initialBuyTokenAmount": "3513008695395357824975710", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0xd3bbe6d051400b72d4a64597abeec532ee017a85babdc5a1fdaf73d907984a3c:0x3521acd3736309d435233b586e080854bfeef9b59730b86a067f311658a42bff:320", + "liquidityOccurrenceId": "1:0xd3bbe6d051400b72d4a64597abeec532ee017a85babdc5a1fdaf73d907984a3c:0x3521acd3736309d435233b586e080854bfeef9b59730b86a067f311658a42bff:321", + "initialBuyOccurrenceId": "1:0xd3bbe6d051400b72d4a64597abeec532ee017a85babdc5a1fdaf73d907984a3c:0x3521acd3736309d435233b586e080854bfeef9b59730b86a067f311658a42bff:322", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xd3bbe6d051400b72d4a64597abeec532ee017a85babdc5a1fdaf73d907984a3c:0x3521acd3736309d435233b586e080854bfeef9b59730b86a067f311658a42bff:325", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646352" + }, + { + "id": "1:stock-paired-v3:0x356dc81234f3f4c8c5535be0fc2df6aabfebef6808c23d4ae82dbdf8ffcfe316", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x356dc81234f3f4c8c5535be0fc2df6aabfebef6808c23d4ae82dbdf8ffcfe316", + "token": "0x2940446b9612df1b73cef7df1b5a98192368d15a", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x1f31d759508b17670a33fd465fabbd0f67b90bbceae7ddb507d2007da2bb7bd9", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xa0665ab5b7b0a994f9925df27b6cf7a52ed622cc", + "positionRecipient": "0xd4abca2dcf20afd49e3e390a2775077a429205fa", + "positionTokenId": "355976", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x3382e144c57e690cbfc654bf2494d828b4767f75fba0360a7864d585bd11db22", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "194422578401190823", + "initialBuyTokenAmount": "14036706124241751081447992", + "initialBuyEthAmount": "20000000000000000", + "launchOccurrenceId": "1:0xbe755c8ba25796f80aaef0847c691578db4c0ebc007b16417273888812a8a82b:0x7f6e8c2a03a9102dbb177f90e6a166df3cbb0ca20c210644301856e82c4994a3:29", + "liquidityOccurrenceId": "1:0xbe755c8ba25796f80aaef0847c691578db4c0ebc007b16417273888812a8a82b:0x7f6e8c2a03a9102dbb177f90e6a166df3cbb0ca20c210644301856e82c4994a3:30", + "initialBuyOccurrenceId": "1:0xbe755c8ba25796f80aaef0847c691578db4c0ebc007b16417273888812a8a82b:0x7f6e8c2a03a9102dbb177f90e6a166df3cbb0ca20c210644301856e82c4994a3:31", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xbe755c8ba25796f80aaef0847c691578db4c0ebc007b16417273888812a8a82b:0x7f6e8c2a03a9102dbb177f90e6a166df3cbb0ca20c210644301856e82c4994a3:34", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646303" + }, + { + "id": "1:stock-paired-v3:0x47a4e2f097fa40a211b9b114c958a7efd21ff2baa319b1689cc75a0005cde4bf", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x47a4e2f097fa40a211b9b114c958a7efd21ff2baa319b1689cc75a0005cde4bf", + "token": "0x53e7c84188e47b220e15add5c83b450474f73c89", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0xda2e778d66dc3541b9b2d3e7784d1a233c386f9e212314f74bdd94c11a759473", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xc0b64578a3f0bb4b6ad6af54669a7da3b39c2a9e", + "positionRecipient": "0x82dd5cf69558ff5cc9d00ae4f4528707e4f6767c", + "positionTokenId": "355962", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x99b0f619a51b2659bf35db2154565fe051df16583ccf984defaf9863903a2e21", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "12987921885448784", + "initialBuyTokenAmount": "3618666191371317257648838", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x891ea663f66342102582bf10482a465f7efec40f46515c7abb99aba768697e65:0x7fcd975c784b3b705957d7cc897b46ed499e4b74317bc49e9cb2840ad92ac14a:46", + "liquidityOccurrenceId": "1:0x891ea663f66342102582bf10482a465f7efec40f46515c7abb99aba768697e65:0x7fcd975c784b3b705957d7cc897b46ed499e4b74317bc49e9cb2840ad92ac14a:47", + "initialBuyOccurrenceId": "1:0x891ea663f66342102582bf10482a465f7efec40f46515c7abb99aba768697e65:0x7fcd975c784b3b705957d7cc897b46ed499e4b74317bc49e9cb2840ad92ac14a:48", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x891ea663f66342102582bf10482a465f7efec40f46515c7abb99aba768697e65:0x7fcd975c784b3b705957d7cc897b46ed499e4b74317bc49e9cb2840ad92ac14a:51", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646276" + }, + { + "id": "1:stock-paired-v3:0x4c9dab0a2ce086adb44efe400bde649f2e9e851ebcf9f7843ce35ea573f6fd3a", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x4c9dab0a2ce086adb44efe400bde649f2e9e851ebcf9f7843ce35ea573f6fd3a", + "token": "0x168f3f3a832b6ab375ec7b2f8df2c34e031f5783", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x33a1ecae29441dcffd58cc67c97d55e588400d0e948b18cc04da2e0ed48cb487", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x97349dbab31b389e63f8dc64aa152b854bf79891", + "positionRecipient": "0xd3dd76069122c714954c5fc1b8695d3b73563a62", + "positionTokenId": "357031", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x14d5db7721badaf88a4e983c832710f094351c7dd324e83172a6f305ee33c9fd", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "95695546903397030", + "initialBuyTokenAmount": "6958520011595430083960637", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x010a9ff11ec84bd80c6e2ead02ca2e4990c1b75f8779074ca8d6a8f2307bd02f:0x066d1e1e22129e0b30d0f35f47e1cbcfcd367b7fdae8431a31b16cdfe132c514:198", + "liquidityOccurrenceId": "1:0x010a9ff11ec84bd80c6e2ead02ca2e4990c1b75f8779074ca8d6a8f2307bd02f:0x066d1e1e22129e0b30d0f35f47e1cbcfcd367b7fdae8431a31b16cdfe132c514:199", + "initialBuyOccurrenceId": "1:0x010a9ff11ec84bd80c6e2ead02ca2e4990c1b75f8779074ca8d6a8f2307bd02f:0x066d1e1e22129e0b30d0f35f47e1cbcfcd367b7fdae8431a31b16cdfe132c514:200", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x010a9ff11ec84bd80c6e2ead02ca2e4990c1b75f8779074ca8d6a8f2307bd02f:0x066d1e1e22129e0b30d0f35f47e1cbcfcd367b7fdae8431a31b16cdfe132c514:203", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25650805" + }, + { + "id": "1:stock-paired-v3:0x4ca78fdac94e0059e491f9cd49150bdba20ca1d6b67283062d19668190a40127", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x4ca78fdac94e0059e491f9cd49150bdba20ca1d6b67283062d19668190a40127", + "token": "0xc56c898542842793d60476471b865d339bc3b7fc", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0x57819d16155dcd5bff856931504a577485c21a72af62ab10ba9740c5be4ec625", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xe7f93b2b47660c471d54d0def7b5543ba07b887c", + "positionRecipient": "0x4621107986c895bf2526726cb37938188e93c817", + "positionTokenId": "356039", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x498edf20b236a74434c4dc6515782ea144b230cd608d35f40746c918b08db4ec", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "307973486449875339", + "initialBuyTokenAmount": "33830200224510957924205663", + "initialBuyEthAmount": "50000000000000000", + "launchOccurrenceId": "1:0x8dae32726dbb785f4521e12a76b9d6772c2a734a71a478886cc0deb197c48464:0x480705f1b4a2534833dc56888a8c9f8f1b638260366016a220b58340a66ab133:565", + "liquidityOccurrenceId": "1:0x8dae32726dbb785f4521e12a76b9d6772c2a734a71a478886cc0deb197c48464:0x480705f1b4a2534833dc56888a8c9f8f1b638260366016a220b58340a66ab133:566", + "initialBuyOccurrenceId": "1:0x8dae32726dbb785f4521e12a76b9d6772c2a734a71a478886cc0deb197c48464:0x480705f1b4a2534833dc56888a8c9f8f1b638260366016a220b58340a66ab133:567", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x8dae32726dbb785f4521e12a76b9d6772c2a734a71a478886cc0deb197c48464:0x480705f1b4a2534833dc56888a8c9f8f1b638260366016a220b58340a66ab133:570", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646492" + }, + { + "id": "1:stock-paired-v3:0x4e159f23625e10606cebe7c7267a77c07fb727ebb551c39f38f66c252a70dcc1", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x4e159f23625e10606cebe7c7267a77c07fb727ebb551c39f38f66c252a70dcc1", + "token": "0xc6d885890dd81b7a4bfd3c137fbc3480f19a84b1", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf3e4872e6a4cf365888d93b6146a2baa7348f1a4", + "poolId": "0x45e8d5e01242bf91fa69aaf29189b35065ae84369bb3769887d28c7eb09e8956", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xa06d9ada4bb52e64ec74d384bce12b5acb365cf7", + "positionRecipient": "0x69a0b0d3329463bf4f9a102d6573a2718254db3c", + "positionTokenId": "357112", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x5c5fd20e3ed305b4a8dfb76453a9fafa17b656c4fa42a7c0d9d284ad85f2bebe", + "quoteConfigurationHash": "0xdb9c04a4d6149d2a9eedbad638381942097a29be33ef2d70647e0c6edb4fda51", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999999888", + "lockedTokenDust": "112", + "initialTick": -168200, + "tickLower": -168200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "3217357828303189817", + "initialBuyTokenAmount": "60335817429593767379578076", + "initialBuyEthAmount": "90000000000000000", + "launchOccurrenceId": "1:0xa9b35f50fd75862f62e7d01699ff4e428371956a5c5f61c342eed563e2c9c941:0x32950b2eed832c7bcc54fc72f1c263a89035a566f85aee07d53fdc7220f14c55:358", + "liquidityOccurrenceId": "1:0xa9b35f50fd75862f62e7d01699ff4e428371956a5c5f61c342eed563e2c9c941:0x32950b2eed832c7bcc54fc72f1c263a89035a566f85aee07d53fdc7220f14c55:359", + "initialBuyOccurrenceId": "1:0xa9b35f50fd75862f62e7d01699ff4e428371956a5c5f61c342eed563e2c9c941:0x32950b2eed832c7bcc54fc72f1c263a89035a566f85aee07d53fdc7220f14c55:360", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xa9b35f50fd75862f62e7d01699ff4e428371956a5c5f61c342eed563e2c9c941:0x32950b2eed832c7bcc54fc72f1c263a89035a566f85aee07d53fdc7220f14c55:363", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25651223" + }, + { + "id": "1:stock-paired-v3:0x565eee3e69ef553665845451f70b506c4591ac39b91fdfa2dde7b5a6f75d2f84", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x565eee3e69ef553665845451f70b506c4591ac39b91fdfa2dde7b5a6f75d2f84", + "token": "0x49569692a2c9b8ce84e09897f7767f56e5b9e959", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0x5a045693cf4552f9cb757357337648120aabea0f6d4523c3e6806e2ee05450c0", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x36f4f0f46b814251256888c36a2ec84b5db4fa36", + "positionRecipient": "0x720d192f3b55e2b3ba2f8764d66c681ba9dd38da", + "positionTokenId": "355972", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xa454c8254ef22d903bd6d08a03c4c47ce953e3bcc0178f72a6b22b15695c0ee2", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "498845345030150353", + "initialBuyTokenAmount": "53671721142905468144840774", + "initialBuyEthAmount": "80000000000000000", + "launchOccurrenceId": "1:0x067d3e8f739fdb98163f78333f3cb30f83ae6814fb622877e9ede212e307940e:0xd0df87fb583b413eb656cfd0379ae162fbaf01a344c684b463461bab9d55c004:575", + "liquidityOccurrenceId": "1:0x067d3e8f739fdb98163f78333f3cb30f83ae6814fb622877e9ede212e307940e:0xd0df87fb583b413eb656cfd0379ae162fbaf01a344c684b463461bab9d55c004:576", + "initialBuyOccurrenceId": "1:0x067d3e8f739fdb98163f78333f3cb30f83ae6814fb622877e9ede212e307940e:0xd0df87fb583b413eb656cfd0379ae162fbaf01a344c684b463461bab9d55c004:577", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x067d3e8f739fdb98163f78333f3cb30f83ae6814fb622877e9ede212e307940e:0xd0df87fb583b413eb656cfd0379ae162fbaf01a344c684b463461bab9d55c004:580", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646290" + }, + { + "id": "1:stock-paired-v3:0x5ac643e9d116b7650216029bda6bd494bf81b26acf19cf5554548e12dabf1e1c", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x5ac643e9d116b7650216029bda6bd494bf81b26acf19cf5554548e12dabf1e1c", + "token": "0x150e6a78d5459562d8a3f1458039b570b5845d33", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0x413f9b85ca198c5ed88060be524def13c73fadab78caa6c41487e94c7797a324", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x1a38c7a01df0573faa064052c0c960233e3acba2", + "positionRecipient": "0x3df11634733d6a1eb3441c2cdcafe8f634e2a37c", + "positionTokenId": "355981", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x5267c54939faab5c2f1c506a5da109eeb58ad5d81149e330c75fec1c5aac0f9a", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "61716592082189210", + "initialBuyTokenAmount": "6967917450288842806225740", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0x804781f3d3b789e8a8b47a85892b2ff1ca4d507a8232594a6e373f280fddbedc:67", + "liquidityOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0x804781f3d3b789e8a8b47a85892b2ff1ca4d507a8232594a6e373f280fddbedc:68", + "initialBuyOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0x804781f3d3b789e8a8b47a85892b2ff1ca4d507a8232594a6e373f280fddbedc:69", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0x804781f3d3b789e8a8b47a85892b2ff1ca4d507a8232594a6e373f280fddbedc:72", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646312" + }, + { + "id": "1:stock-paired-v3:0x6309907bb3942da175ba911450029343e016f4a400c2d2cb14af881a35589a6e", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x6309907bb3942da175ba911450029343e016f4a400c2d2cb14af881a35589a6e", + "token": "0x5f1763f4edca4e946288e9a0b7c4b5d89ef2e8b8", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf3e4872e6a4cf365888d93b6146a2baa7348f1a4", + "poolId": "0xe21ae5a59a173ad6c987dc47e64ad6da0d466f0b1f1481844ea3510fabc9fb61", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xa5fea9cbc1b691d4a6520bd828f40222f0e8eded", + "positionRecipient": "0x78b6bd968b484595bcade5ff43d547e2bae375e4", + "positionTokenId": "355964", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xf66d8a5c07594ece5aa8f60f9dc938bb1d0e8a057145488121497aeabb87aa79", + "quoteConfigurationHash": "0xdb9c04a4d6149d2a9eedbad638381942097a29be33ef2d70647e0c6edb4fda51", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999999888", + "lockedTokenDust": "112", + "initialTick": -168200, + "tickLower": -168200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "362201003895874376", + "initialBuyTokenAmount": "7176699690766722398266492", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0xeee1c4f24eb4200199691abfb62e0e36bef0e784bdf177125f83712b040085ca:0xfe1d0d3dd380f3e2506b15c3d302491e23579d057903edd8c99ab4649c62cc6b:450", + "liquidityOccurrenceId": "1:0xeee1c4f24eb4200199691abfb62e0e36bef0e784bdf177125f83712b040085ca:0xfe1d0d3dd380f3e2506b15c3d302491e23579d057903edd8c99ab4649c62cc6b:451", + "initialBuyOccurrenceId": "1:0xeee1c4f24eb4200199691abfb62e0e36bef0e784bdf177125f83712b040085ca:0xfe1d0d3dd380f3e2506b15c3d302491e23579d057903edd8c99ab4649c62cc6b:452", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xeee1c4f24eb4200199691abfb62e0e36bef0e784bdf177125f83712b040085ca:0xfe1d0d3dd380f3e2506b15c3d302491e23579d057903edd8c99ab4649c62cc6b:455", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646279" + }, + { + "id": "1:stock-paired-v3:0x63883df2fbc586a0d8bbeee123e97e25135d47d4de9a822fddb43fe3d7242365", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x63883df2fbc586a0d8bbeee123e97e25135d47d4de9a822fddb43fe3d7242365", + "token": "0x01bfb9d2469ecf2f5324b25d8a18701a58599fc8", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0xf832105d8ed1ac385ce5ba405e611a567915c9be21219ffe94f6c5e7bf548473", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x3652801b9095e06829161d514ab95ccaf28b91c1", + "positionRecipient": "0x7dc250adbb6a2fc3eb3a28fd1554b0133b2ab6df", + "positionTokenId": "356022", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xeb16ca5a693b20a2329bdabd420ee6901a03f5bd30fe6954e098f82ff1fddbff", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "30814357805186329", + "initialBuyTokenAmount": "3491178457369320308115696", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0xb3e610525f9fa81fe2900a4812270065496f31a7e53bab4350f6e9110dbc3b7d:0x2de336e707ee73a72e4461104b8a712e14a87d9eb761971e70306cfffcc11fed:370", + "liquidityOccurrenceId": "1:0xb3e610525f9fa81fe2900a4812270065496f31a7e53bab4350f6e9110dbc3b7d:0x2de336e707ee73a72e4461104b8a712e14a87d9eb761971e70306cfffcc11fed:371", + "initialBuyOccurrenceId": "1:0xb3e610525f9fa81fe2900a4812270065496f31a7e53bab4350f6e9110dbc3b7d:0x2de336e707ee73a72e4461104b8a712e14a87d9eb761971e70306cfffcc11fed:372", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xb3e610525f9fa81fe2900a4812270065496f31a7e53bab4350f6e9110dbc3b7d:0x2de336e707ee73a72e4461104b8a712e14a87d9eb761971e70306cfffcc11fed:375", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646420" + }, + { + "id": "1:stock-paired-v3:0x852d8f5e60a18ef544fcba49595e4ddf57937c3c1319714cbed4476413b425d4", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x852d8f5e60a18ef544fcba49595e4ddf57937c3c1319714cbed4476413b425d4", + "token": "0x2c821f5560c440a9c51999ecf870657f18d5cc4a", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0xc167bc302dc2de9f67b4578d62bc254d93ca7d3db647765678bd837f3e925517", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x542f3d7838764647217fc261dc327ddb618f64cb", + "positionRecipient": "0x11a763ce54556ed6057998a23d27c84ec11b2198", + "positionTokenId": "356017", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xf17fd73bb8218d7d0a2e1e11b005deb067d0cdb20ac26c88b868a4c0409ebbaa", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "48200590391167088", + "initialBuyTokenAmount": "3517061698444482043025802", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x12149525c9d6b5dc90ad12f8514a3c7df463f8321e565b1c6b0e164e1e516a8a:0xef9656a84204becbbff4481e13d2ae688bd77dc1e8db9b1241910cff07559533:591", + "liquidityOccurrenceId": "1:0x12149525c9d6b5dc90ad12f8514a3c7df463f8321e565b1c6b0e164e1e516a8a:0xef9656a84204becbbff4481e13d2ae688bd77dc1e8db9b1241910cff07559533:592", + "initialBuyOccurrenceId": "1:0x12149525c9d6b5dc90ad12f8514a3c7df463f8321e565b1c6b0e164e1e516a8a:0xef9656a84204becbbff4481e13d2ae688bd77dc1e8db9b1241910cff07559533:593", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x12149525c9d6b5dc90ad12f8514a3c7df463f8321e565b1c6b0e164e1e516a8a:0xef9656a84204becbbff4481e13d2ae688bd77dc1e8db9b1241910cff07559533:596", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646401" + }, + { + "id": "1:stock-paired-v3:0x9028fbb0a68bf629e6db35f21a45a372496fcbdc3fdd1f145470883e820c6d60", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x9028fbb0a68bf629e6db35f21a45a372496fcbdc3fdd1f145470883e820c6d60", + "token": "0x1bae0ade24de13a044be1c7641ace8054d1a395c", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x386770f021ed8f22aadd1a08cbdfd36f623e97ed412e9d41fb6e83e49475eb81", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x75e5fb4ac5ad6a98ee980f8130a6990f8db12c91", + "positionRecipient": "0x2af5217d91ea57876201f6a6f200a7b483f06e4f", + "positionTokenId": "355982", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x099357af18e848df195600612dd1949b60342879546d28d1f0acf21e555bf473", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "47761141376431553", + "initialBuyTokenAmount": "3485108089379060787936886", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0xbfc859a0ba5b79a7a9bdfeadfe69565fca513295cb8226b4aece086db4aa6584:472", + "liquidityOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0xbfc859a0ba5b79a7a9bdfeadfe69565fca513295cb8226b4aece086db4aa6584:473", + "initialBuyOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0xbfc859a0ba5b79a7a9bdfeadfe69565fca513295cb8226b4aece086db4aa6584:474", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x7317d4688b1e0b84978070ccd0a15438182bc5c22e6c513ba064a5456ef47289:0xbfc859a0ba5b79a7a9bdfeadfe69565fca513295cb8226b4aece086db4aa6584:477", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646312" + }, + { + "id": "1:stock-paired-v3:0x9705591c7060367133cdccf539659d2f21b320e9d2731d4132c1909da6ab9b39", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x9705591c7060367133cdccf539659d2f21b320e9d2731d4132c1909da6ab9b39", + "token": "0x31fcdee0aea658e0f7a3d275fd126f6faf3b6d82", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x139a552b6590ff4912dc3c31b6c0acc969c642db6c953f14b998afce6f3af621", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x9821840ad7b72ba89ae0b6ddf9d38221371d36ea", + "positionRecipient": "0x7de30e6ca8fd6d2cc66308cf59fdcb63e3d8da58", + "positionTokenId": "355990", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x7aea110fa53b47aa65dc2c97261a6b3e472324a389e932ec100612c0536f0836", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "25811422808447289", + "initialBuyTokenAmount": "7165918982644038681968476", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0xed5ae2c389827818d3788d09398f73a548502c5f0d3d580fd6c325254c728d0d:0xc4733edff036e2efd6f7c578a12ac258bfecf7d56f285444c9b351d32c0fee16:263", + "liquidityOccurrenceId": "1:0xed5ae2c389827818d3788d09398f73a548502c5f0d3d580fd6c325254c728d0d:0xc4733edff036e2efd6f7c578a12ac258bfecf7d56f285444c9b351d32c0fee16:264", + "initialBuyOccurrenceId": "1:0xed5ae2c389827818d3788d09398f73a548502c5f0d3d580fd6c325254c728d0d:0xc4733edff036e2efd6f7c578a12ac258bfecf7d56f285444c9b351d32c0fee16:265", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xed5ae2c389827818d3788d09398f73a548502c5f0d3d580fd6c325254c728d0d:0xc4733edff036e2efd6f7c578a12ac258bfecf7d56f285444c9b351d32c0fee16:268", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646338" + }, + { + "id": "1:stock-paired-v3:0x991b1c6dec03a051e906215bd5ce7f0a9342fb9c5c0e0b07e14d6bc719d6945a", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x991b1c6dec03a051e906215bd5ce7f0a9342fb9c5c0e0b07e14d6bc719d6945a", + "token": "0x02ea9d17eafc745c9fc1109a37e48411808060b5", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x14c3abf95cb9c93a8b82c1cdcb76d72cb87b2d4c", + "poolId": "0xf26f2775fa4a48faed979adfd02f53cac28aa32c9829acfb0c9c5f0707a57e62", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xfe086d71b9fad778f21ee799ab965e19626afa5c", + "positionRecipient": "0xaf71f964b90d4069b9fb19ab6c90b9654a9f53af", + "positionTokenId": "355979", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x5e0521b2f77d736c745f950fe62bf922189cd8d01b3cb9333547980d108930fb", + "quoteConfigurationHash": "0xb0e7d1e939706011d11b9c67e14f9c0862e9749f5d710f193708f719137b7a62", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999992417", + "lockedTokenDust": "7583", + "initialTick": -187000, + "tickLower": -187000, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "557971066970227074", + "initialBuyTokenAmount": "68007975021968955301852206", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xd8559a2fe164dcb17a63a81b45eee21a85d45ee24e2fc1350e3b3dbd5e9469bf:213", + "liquidityOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xd8559a2fe164dcb17a63a81b45eee21a85d45ee24e2fc1350e3b3dbd5e9469bf:214", + "initialBuyOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xd8559a2fe164dcb17a63a81b45eee21a85d45ee24e2fc1350e3b3dbd5e9469bf:215", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x76d2a01bb0dd0129d30fccf64412b6182e221327725d4db240d6447aa3d2c6bf:0xd8559a2fe164dcb17a63a81b45eee21a85d45ee24e2fc1350e3b3dbd5e9469bf:218", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646305" + }, + { + "id": "1:stock-paired-v3:0x9dcdb5ef3c66f3495c58f36078e32787489c23edfa9f2aa161100232fedbe4dd", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0x9dcdb5ef3c66f3495c58f36078e32787489c23edfa9f2aa161100232fedbe4dd", + "token": "0x761a2c1c91d80ac54be3daccc2ae47be2e75a369", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x9c862e8ddb353832dfe555c650c3008da72ff862255ea89c243c8bc1333fd353", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x026a5663fc101825f65b72fb5841b674ee8db254", + "positionRecipient": "0xb6b2685d13e79387c50499943827a0710351f015", + "positionTokenId": "356033", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xd8e385d7aafe0fd28fffd5fe174aaf88e206b9b94d05154dbed7525b3bbb2510", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "499772232588518920", + "initialBuyTokenAmount": "122615494913534784722646111", + "initialBuyEthAmount": "200000000000000000", + "launchOccurrenceId": "1:0x944b988d33908da3a3d2dc623d83d6fab7fdae1edfabe7bb1bb59c1b7cecbabf:0x62b0af876ee6a2b6b96e947036b57f2a2a70ce46d5a29e8a5589b6be87188f2f:182", + "liquidityOccurrenceId": "1:0x944b988d33908da3a3d2dc623d83d6fab7fdae1edfabe7bb1bb59c1b7cecbabf:0x62b0af876ee6a2b6b96e947036b57f2a2a70ce46d5a29e8a5589b6be87188f2f:183", + "initialBuyOccurrenceId": "1:0x944b988d33908da3a3d2dc623d83d6fab7fdae1edfabe7bb1bb59c1b7cecbabf:0x62b0af876ee6a2b6b96e947036b57f2a2a70ce46d5a29e8a5589b6be87188f2f:184", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x944b988d33908da3a3d2dc623d83d6fab7fdae1edfabe7bb1bb59c1b7cecbabf:0x62b0af876ee6a2b6b96e947036b57f2a2a70ce46d5a29e8a5589b6be87188f2f:187", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646456" + }, + { + "id": "1:stock-paired-v3:0xa12cecc9365e610c28c689e060afda473f8f6c663f7fb6038c810fbba93c8ddc", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xa12cecc9365e610c28c689e060afda473f8f6c663f7fb6038c810fbba93c8ddc", + "token": "0x6a4015081888e32de4d2e877614a189e6ac41421", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0xca9cc3d2900ce7a13d4a53d2558857f8eb1e41b476ce739a91f2440c3f8670ee", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x36cb2239d07fab236828f659b6acdbecd2498ae6", + "positionRecipient": "0xca0b4c3ebd66ed79e9fcecfac3731804b462cca8", + "positionTokenId": "356218", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x76cdd754aa6b00de53db7060c451dd11b0287dce5c9fd36501ab572e1c98b3dd", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "617579742082641898", + "initialBuyTokenAmount": "65608442483343044284455617", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0xbd8a197b72a542fef88169d23bcd122add9512194cf0f96ea830436f3ea6e919:0x562aa9493ef108261df9dfe9a794ff1ecbfc83d8b2b60108d3f2bd4cbb473c9e:94", + "liquidityOccurrenceId": "1:0xbd8a197b72a542fef88169d23bcd122add9512194cf0f96ea830436f3ea6e919:0x562aa9493ef108261df9dfe9a794ff1ecbfc83d8b2b60108d3f2bd4cbb473c9e:95", + "initialBuyOccurrenceId": "1:0xbd8a197b72a542fef88169d23bcd122add9512194cf0f96ea830436f3ea6e919:0x562aa9493ef108261df9dfe9a794ff1ecbfc83d8b2b60108d3f2bd4cbb473c9e:96", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xbd8a197b72a542fef88169d23bcd122add9512194cf0f96ea830436f3ea6e919:0x562aa9493ef108261df9dfe9a794ff1ecbfc83d8b2b60108d3f2bd4cbb473c9e:99", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25647399" + }, + { + "id": "1:stock-paired-v3:0xa9563564fc0cd284a0772cfd17e7a9958ed107b885e385def7f8df055ae0166f", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xa9563564fc0cd284a0772cfd17e7a9958ed107b885e385def7f8df055ae0166f", + "token": "0x12f1dd5fe425adc31b589f7562ffe2a37e92051d", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x0f69c01740afa9ac907c351ebd3ef0e6545800bf4a8b47b9c56344303aec516d", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xc8ed6707d1fe674e8ee5b3648cced4d358272c25", + "positionRecipient": "0xc6bf32edeec2ad2a5f58495830182528907a2699", + "positionTokenId": "356220", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x028536df775ae86d0141eea7dcef7adadf40723f936a8e2ed51e51257e1dbd41", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "487259486646361718", + "initialBuyTokenAmount": "34450276294850867334660472", + "initialBuyEthAmount": "50000000000000000", + "launchOccurrenceId": "1:0xe12447b4136dd6478dbf2c3afcb17a595479b67b7dc089167be2466132bb8af9:0x6b250aa99fdc3717785dd5c5b8c882d27a1962487e54b42710234b4c6ced3670:357", + "liquidityOccurrenceId": "1:0xe12447b4136dd6478dbf2c3afcb17a595479b67b7dc089167be2466132bb8af9:0x6b250aa99fdc3717785dd5c5b8c882d27a1962487e54b42710234b4c6ced3670:358", + "initialBuyOccurrenceId": "1:0xe12447b4136dd6478dbf2c3afcb17a595479b67b7dc089167be2466132bb8af9:0x6b250aa99fdc3717785dd5c5b8c882d27a1962487e54b42710234b4c6ced3670:359", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xe12447b4136dd6478dbf2c3afcb17a595479b67b7dc089167be2466132bb8af9:0x6b250aa99fdc3717785dd5c5b8c882d27a1962487e54b42710234b4c6ced3670:362", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25647406" + }, + { + "id": "1:stock-paired-v3:0xa9924bf955de80ca5906ce23f0da346623e58f55a21a10280ac5c79c2cdf1712", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xa9924bf955de80ca5906ce23f0da346623e58f55a21a10280ac5c79c2cdf1712", + "token": "0x1b8ec2f1e6602af953e99132abc4a47b51719cf4", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xba47214edd2bb43099611b208f75e4b42fdcfedc", + "poolId": "0x4d7eca98adc955a4c2e2636c5d8d1846e44f479eb62cc1d2edb3e54e1a129f6d", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xdcaad93159dc9bce67b21491f33ce4eed6d65fd6", + "positionRecipient": "0x683aa7acf541a441c6a0ca6f0db65a8b1108f11a", + "positionTokenId": "356636", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xb2d8900ddfea970e01b9ff93addeb35ab933bb2bfea4b491a27ca99435f3936f", + "quoteConfigurationHash": "0x633a5dd90d58002e0f4ef91d13b3fab773a46a4e39ff4f76d5d50f0ba0eaa343", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994028", + "lockedTokenDust": "5972", + "initialTick": -186800, + "tickLower": -186800, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "56946171683365134", + "initialBuyTokenAmount": "7246966743481851514024390", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0xb372148134e6e2017f79ad04d59e98301e2d22285d63578f9354488ea0d1aee4:0x6297a7d6fb27cd8c1dd6d713fd5c25f63378b748bec074263ef47b90060fdde1:347", + "liquidityOccurrenceId": "1:0xb372148134e6e2017f79ad04d59e98301e2d22285d63578f9354488ea0d1aee4:0x6297a7d6fb27cd8c1dd6d713fd5c25f63378b748bec074263ef47b90060fdde1:348", + "initialBuyOccurrenceId": "1:0xb372148134e6e2017f79ad04d59e98301e2d22285d63578f9354488ea0d1aee4:0x6297a7d6fb27cd8c1dd6d713fd5c25f63378b748bec074263ef47b90060fdde1:349", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xb372148134e6e2017f79ad04d59e98301e2d22285d63578f9354488ea0d1aee4:0x6297a7d6fb27cd8c1dd6d713fd5c25f63378b748bec074263ef47b90060fdde1:352", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25648780" + }, + { + "id": "1:stock-paired-v3:0xaae2328c84cc26ee531ebcefe8d725842908e07d89b17d33cc2d7d705d2d47b6", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xaae2328c84cc26ee531ebcefe8d725842908e07d89b17d33cc2d7d705d2d47b6", + "token": "0x295f49ca9dfdfaec45f60005203d43cd75f102cb", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0xb594f08012d81a76458dbf0a5efa94977f533306577c5e093e7cb97b9d973d0e", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xba2dd7d28f698a4b35ab34f5d89a7998903f2591", + "positionRecipient": "0x5cb659ad2aeb640f3fbe7bb849e54885c3afac76", + "positionTokenId": "356037", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xb138c3957153a868c31edb3e9125073e92a660b6e50b4d33d6c5efa62b893862", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "61722486165392971", + "initialBuyTokenAmount": "6968578265928705310053921", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0xcfe9f755e0ed2030a8e57d06da534b62ca48e1abdb2f1ad4ab2f5f29d479116e:0xbb4b839550a80136b568c8be530ca1e61fec51ff9b780d85558676ccf2393d07:266", + "liquidityOccurrenceId": "1:0xcfe9f755e0ed2030a8e57d06da534b62ca48e1abdb2f1ad4ab2f5f29d479116e:0xbb4b839550a80136b568c8be530ca1e61fec51ff9b780d85558676ccf2393d07:267", + "initialBuyOccurrenceId": "1:0xcfe9f755e0ed2030a8e57d06da534b62ca48e1abdb2f1ad4ab2f5f29d479116e:0xbb4b839550a80136b568c8be530ca1e61fec51ff9b780d85558676ccf2393d07:268", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xcfe9f755e0ed2030a8e57d06da534b62ca48e1abdb2f1ad4ab2f5f29d479116e:0xbb4b839550a80136b568c8be530ca1e61fec51ff9b780d85558676ccf2393d07:271", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646477" + }, + { + "id": "1:stock-paired-v3:0xb36b2ea3dc8e386c919eaae08d44614a82454daea59d833eaab7a4d6e0152947", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xb36b2ea3dc8e386c919eaae08d44614a82454daea59d833eaab7a4d6e0152947", + "token": "0x01ed0b479e77f4a335b54cffcb63a1f1682079d3", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x14c3abf95cb9c93a8b82c1cdcb76d72cb87b2d4c", + "poolId": "0x20c0d9fa7abc61eceba8f096b95920c471c40699aad78f95f206958583d58b9d", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x1d8c3cd329336b30200e5beac26b028dcc065b21", + "positionRecipient": "0xddc019d16d174b653cebf0f20e3dd9ae14c675e6", + "positionTokenId": "356001", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x0351a42ca33b19ddcc86a381bd9d32146de210cf1def1d192ad0d31997471443", + "quoteConfigurationHash": "0xb0e7d1e939706011d11b9c67e14f9c0862e9749f5d710f193708f719137b7a62", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999992417", + "lockedTokenDust": "7583", + "initialTick": -187000, + "tickLower": -187000, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "55900367796908432", + "initialBuyTokenAmount": "7257503375688028612930291", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x5515e0ca689db90886f7f68784e2e2564dc0dab175020bb9dfdd3051e6d07149:0x2ab24d4368831fc64bc22f518fb625e321a550bb461137aec2911758eb160af3:312", + "liquidityOccurrenceId": "1:0x5515e0ca689db90886f7f68784e2e2564dc0dab175020bb9dfdd3051e6d07149:0x2ab24d4368831fc64bc22f518fb625e321a550bb461137aec2911758eb160af3:313", + "initialBuyOccurrenceId": "1:0x5515e0ca689db90886f7f68784e2e2564dc0dab175020bb9dfdd3051e6d07149:0x2ab24d4368831fc64bc22f518fb625e321a550bb461137aec2911758eb160af3:314", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x5515e0ca689db90886f7f68784e2e2564dc0dab175020bb9dfdd3051e6d07149:0x2ab24d4368831fc64bc22f518fb625e321a550bb461137aec2911758eb160af3:317", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646365" + }, + { + "id": "1:stock-paired-v3:0xb411f6d0d3f4131a2b4269dbc2b01c0ac586d3c9ba8ee11bf753b60878da024f", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xb411f6d0d3f4131a2b4269dbc2b01c0ac586d3c9ba8ee11bf753b60878da024f", + "token": "0x291becd9e89e9c40c678735e7a584c0ef67c4bc9", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x123894d3a32ac9d9e37110d73660b3e6099461b8482f51b5169ebdef3a98521a", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x62a3cc855eba3b33624b1a876385bdc422f47700", + "positionRecipient": "0x9faad836b5da35483112d79e1d5e7096fbd2960b", + "positionTokenId": "355963", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x24251252b1aa35f6b00625352c66cf4b61a050fee5aa46732939fafed7896225", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "97354121564032955", + "initialBuyTokenAmount": "7078269920886360629115218", + "initialBuyEthAmount": "10000000000000000", + "launchOccurrenceId": "1:0x380b88e8cde3a26eb57cb27fd541865b89262180a5e1d1fca2d16ade5198db17:0x32e81a5eecc6b42a004062cecd329b366d209d7bb85143adf43fed4112dd797e:358", + "liquidityOccurrenceId": "1:0x380b88e8cde3a26eb57cb27fd541865b89262180a5e1d1fca2d16ade5198db17:0x32e81a5eecc6b42a004062cecd329b366d209d7bb85143adf43fed4112dd797e:359", + "initialBuyOccurrenceId": "1:0x380b88e8cde3a26eb57cb27fd541865b89262180a5e1d1fca2d16ade5198db17:0x32e81a5eecc6b42a004062cecd329b366d209d7bb85143adf43fed4112dd797e:360", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x380b88e8cde3a26eb57cb27fd541865b89262180a5e1d1fca2d16ade5198db17:0x32e81a5eecc6b42a004062cecd329b366d209d7bb85143adf43fed4112dd797e:363", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646277" + }, + { + "id": "1:stock-paired-v3:0xb5fc0272b7e574f61e9b4f69fc502ae2a51c640fb0eaba8e3b7be8729bbbb99f", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xb5fc0272b7e574f61e9b4f69fc502ae2a51c640fb0eaba8e3b7be8729bbbb99f", + "token": "0x291c3de665a15c4b01aad786551bde6425097e07", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xba47214edd2bb43099611b208f75e4b42fdcfedc", + "poolId": "0xf15856bd374827b0b2d1e685cc12774101b39980f466560526d9e622c088241a", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x366aa2f0f96debeaff58f324d2e3ef09e773e919", + "positionRecipient": "0xcb67ffaf9b8f09a9c73406c66a442a29be73e652", + "positionTokenId": "356062", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x04b389015ebe23f5a6071f49f57a9ff55598a9d180c8b6b72f6c504209541bad", + "quoteConfigurationHash": "0x633a5dd90d58002e0f4ef91d13b3fab773a46a4e39ff4f76d5d50f0ba0eaa343", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999994028", + "lockedTokenDust": "5972", + "initialTick": -186800, + "tickLower": -186800, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "28260685681925225", + "initialBuyTokenAmount": "3609630342794121724158160", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x6e27661507b76ff66615b88102ecf69e23a94e328b63afd70129ba7116f9580f:0x0d65f767382e5fc4dde403d55037d748da3f3e42aef17a19c34f40b103277852:230", + "liquidityOccurrenceId": "1:0x6e27661507b76ff66615b88102ecf69e23a94e328b63afd70129ba7116f9580f:0x0d65f767382e5fc4dde403d55037d748da3f3e42aef17a19c34f40b103277852:231", + "initialBuyOccurrenceId": "1:0x6e27661507b76ff66615b88102ecf69e23a94e328b63afd70129ba7116f9580f:0x0d65f767382e5fc4dde403d55037d748da3f3e42aef17a19c34f40b103277852:232", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x6e27661507b76ff66615b88102ecf69e23a94e328b63afd70129ba7116f9580f:0x0d65f767382e5fc4dde403d55037d748da3f3e42aef17a19c34f40b103277852:235", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646630" + }, + { + "id": "1:stock-paired-v3:0xbb7d2e0af0925b41f510005229982a86b327d2fdc6df0ec01521beddef2b54f4", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xbb7d2e0af0925b41f510005229982a86b327d2fdc6df0ec01521beddef2b54f4", + "token": "0x0edb8408248a450357110aad8d56b188944fa1a4", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x9bbc529de2d44b3a2a4f1bad7e63e07f437248a17ad6e8cb386099d4cd5e068b", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x6c9346a5b761461a3f4738361cbf5821b5fc5132", + "positionRecipient": "0xfb26ac1c9e820fa0a25607aab97626eb1332a0cc", + "positionTokenId": "355965", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xacfba537adac0c126f2428a66948a5bd74311d6a47f461e288905ef04cbc88dc", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "47765788026135373", + "initialBuyTokenAmount": "3485445971440243268161791", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x8f24866db686474ae3ca518ed198b20ea64434654f9afbf0590fd2c62cdd65fa:0x6d183f2ede2bed4d4c9ff850a5737a2934168f6bad2bb8b0ff625cd8fc08b7d8:472", + "liquidityOccurrenceId": "1:0x8f24866db686474ae3ca518ed198b20ea64434654f9afbf0590fd2c62cdd65fa:0x6d183f2ede2bed4d4c9ff850a5737a2934168f6bad2bb8b0ff625cd8fc08b7d8:473", + "initialBuyOccurrenceId": "1:0x8f24866db686474ae3ca518ed198b20ea64434654f9afbf0590fd2c62cdd65fa:0x6d183f2ede2bed4d4c9ff850a5737a2934168f6bad2bb8b0ff625cd8fc08b7d8:474", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x8f24866db686474ae3ca518ed198b20ea64434654f9afbf0590fd2c62cdd65fa:0x6d183f2ede2bed4d4c9ff850a5737a2934168f6bad2bb8b0ff625cd8fc08b7d8:477", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646280" + }, + { + "id": "1:stock-paired-v3:0xbfce9d391b98ca42478566735471cee3105eac0bc5f2e98816066191cad0bce4", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xbfce9d391b98ca42478566735471cee3105eac0bc5f2e98816066191cad0bce4", + "token": "0x06ea65e4ea4e75bf5fb7ca00da77b4b87b16d4cf", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0xab00dcffb6a6bfe1c0eb93370ad328a357bef812621b747562972e747976b0f7", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x75265358c5f1b76f341a11b1955c9117a42a2788", + "positionRecipient": "0x0479c3b8e6b79237b4774d1e67d7c3b3bc73d0df", + "positionTokenId": "356024", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x0022db02ba998e684a8385bf7f3fac77b926dcdddbe9edbb28f2fb0003bd75a6", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "48107418188454077", + "initialBuyTokenAmount": "3510287049243412422300456", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x83ed4cf100feab4e36e588d0436eb46a4c8e0dcbbdf1a69cb5a42f0b7dcf7e4e:0x2deeefb14f794287849c68f152bd4a32b9f5ff42b97a13ee9a1b1ec8613c8ef0:348", + "liquidityOccurrenceId": "1:0x83ed4cf100feab4e36e588d0436eb46a4c8e0dcbbdf1a69cb5a42f0b7dcf7e4e:0x2deeefb14f794287849c68f152bd4a32b9f5ff42b97a13ee9a1b1ec8613c8ef0:349", + "initialBuyOccurrenceId": "1:0x83ed4cf100feab4e36e588d0436eb46a4c8e0dcbbdf1a69cb5a42f0b7dcf7e4e:0x2deeefb14f794287849c68f152bd4a32b9f5ff42b97a13ee9a1b1ec8613c8ef0:350", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x83ed4cf100feab4e36e588d0436eb46a4c8e0dcbbdf1a69cb5a42f0b7dcf7e4e:0x2deeefb14f794287849c68f152bd4a32b9f5ff42b97a13ee9a1b1ec8613c8ef0:353", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646435" + }, + { + "id": "1:stock-paired-v3:0xc95aae95148a9f10ed0340a7528246634e926459c09bcadf615aeea21d017e9e", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xc95aae95148a9f10ed0340a7528246634e926459c09bcadf615aeea21d017e9e", + "token": "0x2c348590cb56fcc5984f035d57bdb01e32c945d5", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf3e4872e6a4cf365888d93b6146a2baa7348f1a4", + "poolId": "0xedbc1872069e415b9b4eeed0a6a367c1318ba09694f26f5ab99b514fb3ffe77a", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x83a0057a2936952f70f922b82e957351b41003be", + "positionRecipient": "0x8dde76ece7b6a028b870d50641eb0cdb28a72393", + "positionTokenId": "355004", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x0b36a16b08a64cc52da571703eeaca3892f08b3c5fddaac4bd3abeb3b9c6bd5c", + "quoteConfigurationHash": "0xdb9c04a4d6149d2a9eedbad638381942097a29be33ef2d70647e0c6edb4fda51", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999999888", + "lockedTokenDust": "112", + "initialTick": -168200, + "tickLower": -168200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "21649485434553699", + "initialBuyTokenAmount": "431880044804460094772331", + "initialBuyEthAmount": "600000000000000", + "launchOccurrenceId": "1:0xc027186d70dc796f3add3a1a6d508cb454ac8222e23859fe8692e7ba571e67bd:0xbe52bd2bb71159a1f6ab085cb7c7d5eb4f0583608b8adaf7d62f283be6693b19:921", + "liquidityOccurrenceId": "1:0xc027186d70dc796f3add3a1a6d508cb454ac8222e23859fe8692e7ba571e67bd:0xbe52bd2bb71159a1f6ab085cb7c7d5eb4f0583608b8adaf7d62f283be6693b19:922", + "initialBuyOccurrenceId": "1:0xc027186d70dc796f3add3a1a6d508cb454ac8222e23859fe8692e7ba571e67bd:0xbe52bd2bb71159a1f6ab085cb7c7d5eb4f0583608b8adaf7d62f283be6693b19:923", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xc027186d70dc796f3add3a1a6d508cb454ac8222e23859fe8692e7ba571e67bd:0xbe52bd2bb71159a1f6ab085cb7c7d5eb4f0583608b8adaf7d62f283be6693b19:926", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25642871" + }, + { + "id": "1:stock-paired-v3:0xd3bf87a197d72a0beaca1b605f73f15b84db3c754f6b6c25e40ff0b04ba8bc08", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xd3bf87a197d72a0beaca1b605f73f15b84db3c754f6b6c25e40ff0b04ba8bc08", + "token": "0x1d71b4c72365953a5e13666bbf8768c3192468ad", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf3e4872e6a4cf365888d93b6146a2baa7348f1a4", + "poolId": "0x321a259e88d63b6989f7ad273d07528e8e66f0bd5220a47efb81feef08165637", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x114e83ac760a05e15c47bd8a3dc90af86a7653d5", + "positionRecipient": "0xaa5311aefcc0e748b3fb902aaa8a3b1e98f86e81", + "positionTokenId": "356858", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xedf28c637171bd2521c468902e33860e14c96f594b6f4c304cdb525d0dffb931", + "quoteConfigurationHash": "0xdb9c04a4d6149d2a9eedbad638381942097a29be33ef2d70647e0c6edb4fda51", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999999888", + "lockedTokenDust": "112", + "initialTick": -168200, + "tickLower": -168200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "177757969853578097", + "initialBuyTokenAmount": "3535039544369292772026751", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0xea35ed5ca4e1d3eff2395cc39bffe1acf7a5e0adc08d2f6a5db62bc658c1a8bf:0xbe41f565ca216725d15c49de8644b857189c1027e6147425e124976826f6ab2b:532", + "liquidityOccurrenceId": "1:0xea35ed5ca4e1d3eff2395cc39bffe1acf7a5e0adc08d2f6a5db62bc658c1a8bf:0xbe41f565ca216725d15c49de8644b857189c1027e6147425e124976826f6ab2b:533", + "initialBuyOccurrenceId": "1:0xea35ed5ca4e1d3eff2395cc39bffe1acf7a5e0adc08d2f6a5db62bc658c1a8bf:0xbe41f565ca216725d15c49de8644b857189c1027e6147425e124976826f6ab2b:534", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xea35ed5ca4e1d3eff2395cc39bffe1acf7a5e0adc08d2f6a5db62bc658c1a8bf:0xbe41f565ca216725d15c49de8644b857189c1027e6147425e124976826f6ab2b:537", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25650001" + }, + { + "id": "1:stock-paired-v3:0xe317c724a274f7e51c6b330b95a21b3f8d9708dd6c760a3b20816ca26af453b7", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xe317c724a274f7e51c6b330b95a21b3f8d9708dd6c760a3b20816ca26af453b7", + "token": "0x717a56475fddc1ac788e5d315e44d351136e35ad", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0x27078d3e160e5aa816d2fb7688c8262bac8090a8e50991c050ec40491427ef6c", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xd45bcdb69a5d19b2d7ad2f23ed6eb26ffdf19637", + "positionRecipient": "0xd79448745d9c26d8f0dbd618c6d52afab4926057", + "positionTokenId": "356238", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xe9b0654e0f40b2ada655bfa0ae53a5e20c2d3ac4381a8863113ae17bd5fb4a5c", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "617073155444157229", + "initialBuyTokenAmount": "65558153518527012720539065", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0xae2fc5adc70352a091b122311dc3dfe737c9991187c8a07fff53749b59259690:0x92997af2820c02d7f84b8dd6f12049164c788de7bdc5203af0e77b8e2fd011c5:238", + "liquidityOccurrenceId": "1:0xae2fc5adc70352a091b122311dc3dfe737c9991187c8a07fff53749b59259690:0x92997af2820c02d7f84b8dd6f12049164c788de7bdc5203af0e77b8e2fd011c5:239", + "initialBuyOccurrenceId": "1:0xae2fc5adc70352a091b122311dc3dfe737c9991187c8a07fff53749b59259690:0x92997af2820c02d7f84b8dd6f12049164c788de7bdc5203af0e77b8e2fd011c5:240", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xae2fc5adc70352a091b122311dc3dfe737c9991187c8a07fff53749b59259690:0x92997af2820c02d7f84b8dd6f12049164c788de7bdc5203af0e77b8e2fd011c5:243", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25647436" + }, + { + "id": "1:stock-paired-v3:0xe6e9b57719a1cbad4d6ea15c8ddd00bd43036a8c543d52fd546da0c2eee734c5", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xe6e9b57719a1cbad4d6ea15c8ddd00bd43036a8c543d52fd546da0c2eee734c5", + "token": "0xc19caef8f179cdf7ee77423868b1677a572db5cd", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x332e2e7ec71930f543ad828d304d8b7a6ce7f29dd1fcbc8c2b0ac463e01c410b", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xedcbb80562a729bf3c7ff33629d70ed76e531941", + "positionRecipient": "0x70c70a798a3f38a8f1469ec450d196730a1eb286", + "positionTokenId": "355973", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x1dd27bdbae41f901caa9787c354cd35bd73908f0532a294b571c2741cac371a6", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "252940949556188064", + "initialBuyTokenAmount": "66057559960536784284929468", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0x1a5b3b2a142489e91cfdf92adfef03dc9ea01cf8355181f329f46eb338de2dc7:0x9ae052204f0a22ab7918b481b32e920cfd92efcfcfcbf2d99d1b87c9b0130c26:711", + "liquidityOccurrenceId": "1:0x1a5b3b2a142489e91cfdf92adfef03dc9ea01cf8355181f329f46eb338de2dc7:0x9ae052204f0a22ab7918b481b32e920cfd92efcfcfcbf2d99d1b87c9b0130c26:712", + "initialBuyOccurrenceId": "1:0x1a5b3b2a142489e91cfdf92adfef03dc9ea01cf8355181f329f46eb338de2dc7:0x9ae052204f0a22ab7918b481b32e920cfd92efcfcfcbf2d99d1b87c9b0130c26:713", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x1a5b3b2a142489e91cfdf92adfef03dc9ea01cf8355181f329f46eb338de2dc7:0x9ae052204f0a22ab7918b481b32e920cfd92efcfcfcbf2d99d1b87c9b0130c26:716", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646294" + }, + { + "id": "1:stock-paired-v3:0xe800739e29723439475cdf77c4d178980c112582ae36283fe6668cdb030911cf", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xe800739e29723439475cdf77c4d178980c112582ae36283fe6668cdb030911cf", + "token": "0x113c90c1adabcee08ebc057eae779bc3d899edd4", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x14c3abf95cb9c93a8b82c1cdcb76d72cb87b2d4c", + "poolId": "0x0642efcd77001c764a0caa96e5e4ad09272a45eef2e9057e7eb689caaffcd6f6", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x17bc47cdd234311c60905b7764774e887835b3cf", + "positionRecipient": "0xadc804f77739ddaf1eab63b3f43635eb3f3e78af", + "positionTokenId": "355961", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x07e6b23d9284bb03d824ace0d815a90e86b052913883636751e6fa2545a8779c", + "quoteConfigurationHash": "0xb0e7d1e939706011d11b9c67e14f9c0862e9749f5d710f193708f719137b7a62", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999992417", + "lockedTokenDust": "7583", + "initialTick": -187000, + "tickLower": -187000, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "559651275521604040", + "initialBuyTokenAmount": "68198799736027600851493065", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0x2dd642f8805e5a8f020945f4373821c7b3876b9cfc8f2dc1e9d5e2a852d0ede0:0xa95de243bcec62423e9e9347af490af8f796c4a6623719ef4d03f304765ec494:38", + "liquidityOccurrenceId": "1:0x2dd642f8805e5a8f020945f4373821c7b3876b9cfc8f2dc1e9d5e2a852d0ede0:0xa95de243bcec62423e9e9347af490af8f796c4a6623719ef4d03f304765ec494:39", + "initialBuyOccurrenceId": "1:0x2dd642f8805e5a8f020945f4373821c7b3876b9cfc8f2dc1e9d5e2a852d0ede0:0xa95de243bcec62423e9e9347af490af8f796c4a6623719ef4d03f304765ec494:40", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x2dd642f8805e5a8f020945f4373821c7b3876b9cfc8f2dc1e9d5e2a852d0ede0:0xa95de243bcec62423e9e9347af490af8f796c4a6623719ef4d03f304765ec494:43", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646275" + }, + { + "id": "1:stock-paired-v3:0xe86221fdfaa14f1f1e679e65c72f4dc9fa8144818e5822cd6198379f4a2c0cac", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xe86221fdfaa14f1f1e679e65c72f4dc9fa8144818e5822cd6198379f4a2c0cac", + "token": "0xd3e81658cafbc4f7301dd8a228b13b0767dd8871", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0xe76d8a9fec93cd7a35db4554c43b7307268d179e604ea76817ef845fc64b6f12", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x9210d6f6e2457fccd238e531559660e83667f166", + "positionRecipient": "0x6a5d46a26a41ee49c409cddf1258f29832c0046f", + "positionTokenId": "356363", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x9cb701930546a611f8f6e034e32dfd6c9c11acaea305fa0548b2ae82d6856794", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "123561070418916284", + "initialBuyTokenAmount": "13853543124780650541984750", + "initialBuyEthAmount": "20000000000000000", + "launchOccurrenceId": "1:0x0bc54b592905e6920102d4f6bba6ce52040c34d215aa981141999b74622994ae:0x05316203467fb7d8fe81b2793a245933a9457e62df2199e8511851738cb38f64:438", + "liquidityOccurrenceId": "1:0x0bc54b592905e6920102d4f6bba6ce52040c34d215aa981141999b74622994ae:0x05316203467fb7d8fe81b2793a245933a9457e62df2199e8511851738cb38f64:439", + "initialBuyOccurrenceId": "1:0x0bc54b592905e6920102d4f6bba6ce52040c34d215aa981141999b74622994ae:0x05316203467fb7d8fe81b2793a245933a9457e62df2199e8511851738cb38f64:440", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x0bc54b592905e6920102d4f6bba6ce52040c34d215aa981141999b74622994ae:0x05316203467fb7d8fe81b2793a245933a9457e62df2199e8511851738cb38f64:443", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25647746" + }, + { + "id": "1:stock-paired-v3:0xea4535bb2ef44a5e00ce2e5d7b562e2c0c573deeb37769f0e8e0b70ed37c97d4", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xea4535bb2ef44a5e00ce2e5d7b562e2c0c573deeb37769f0e8e0b70ed37c97d4", + "token": "0xd6b7e95d90904ec69f40092dd4f55a60f0176304", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x165413aebe191611b2a88d3fdba3ea4d294fa0a1ffa9d9e9542946758ba60640", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x24d5f99f6a4b369ad4aa15d8c21875406c587946", + "positionRecipient": "0xf206e7f2d081377e38c0e3da6407b82a8aaa8b9d", + "positionTokenId": "355989", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x98a0523165204164c008b308f5566e4847eea4f9c3fa85dff63eb0893bb4c3d0", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "256903400913822539", + "initialBuyTokenAmount": "67023028776997310377609368", + "initialBuyEthAmount": "100000000000000000", + "launchOccurrenceId": "1:0xade89ae5ea6b8771a8ee1e90b015ecd62e4f197bc910a3047a3a59bc2f2d6c1c:0x442bf8ccce84b6c83d26d6daa001c220eeb4e57bdf908e41287cfaac0f234a64:197", + "liquidityOccurrenceId": "1:0xade89ae5ea6b8771a8ee1e90b015ecd62e4f197bc910a3047a3a59bc2f2d6c1c:0x442bf8ccce84b6c83d26d6daa001c220eeb4e57bdf908e41287cfaac0f234a64:198", + "initialBuyOccurrenceId": "1:0xade89ae5ea6b8771a8ee1e90b015ecd62e4f197bc910a3047a3a59bc2f2d6c1c:0x442bf8ccce84b6c83d26d6daa001c220eeb4e57bdf908e41287cfaac0f234a64:199", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0xade89ae5ea6b8771a8ee1e90b015ecd62e4f197bc910a3047a3a59bc2f2d6c1c:0x442bf8ccce84b6c83d26d6daa001c220eeb4e57bdf908e41287cfaac0f234a64:202", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646331" + }, + { + "id": "1:stock-paired-v3:0xec1235c81b8d4008ab3db4fb3e591438ed9e34313573f2157be46e8ed612e61d", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xec1235c81b8d4008ab3db4fb3e591438ed9e34313573f2157be46e8ed612e61d", + "token": "0xad1c4135ddc196b81ed5eda6c09a1f6de77cd0fd", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xf6b1117ec07684d3958cad8beb1b302bfd21103f", + "poolId": "0xdfc3ba5d89117ac9298dcfc9c9681d00bc4dca69cd15a75414c2f27f89c1d338", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xae38497642d23eec4f09e4a27b74d8afe32a7762", + "positionRecipient": "0x91f48353d602f8eec02ca7dc28e582b49ae3e9bc", + "positionTokenId": "355983", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x5a6ebf9723746386aa39726a7964f61b18451ec4cb5df0ac3ee6f9a6582793f5", + "quoteConfigurationHash": "0x89c4e67b3f2409680c83e65fc7196abab0dd707c77ca0618ed7d026aa87851a9", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999998949", + "lockedTokenDust": "1051", + "initialTick": -185600, + "tickLower": -185600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "308069320370941817", + "initialBuyTokenAmount": "33840371124476075580044274", + "initialBuyEthAmount": "50000000000000000", + "launchOccurrenceId": "1:0x15713a1cc43c53f1a8219f5ad5005d68014583c288c19d77448cfe5aaff7b7b9:0x3c93ba04f33d51512ee3156b93405b5c0ba3100027e23231e9b87043b710633b:234", + "liquidityOccurrenceId": "1:0x15713a1cc43c53f1a8219f5ad5005d68014583c288c19d77448cfe5aaff7b7b9:0x3c93ba04f33d51512ee3156b93405b5c0ba3100027e23231e9b87043b710633b:235", + "initialBuyOccurrenceId": "1:0x15713a1cc43c53f1a8219f5ad5005d68014583c288c19d77448cfe5aaff7b7b9:0x3c93ba04f33d51512ee3156b93405b5c0ba3100027e23231e9b87043b710633b:236", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x15713a1cc43c53f1a8219f5ad5005d68014583c288c19d77448cfe5aaff7b7b9:0x3c93ba04f33d51512ee3156b93405b5c0ba3100027e23231e9b87043b710633b:239", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646315" + }, + { + "id": "1:stock-paired-v3:0xf0468aace6ee478944fd323c37b94cf62fd0ef1aa0ac2e8cbd9a1b38d14d4b12", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xf0468aace6ee478944fd323c37b94cf62fd0ef1aa0ac2e8cbd9a1b38d14d4b12", + "token": "0x02d47b47ec0f0cde925fb57e7a4d22a783717bd2", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0xfedc5f4a6c38211c1338aa411018dfaf26612c08", + "poolId": "0x18703fcdf764d643383bd27eaeda8875ce8b7c50cba1b19d37738731132bd635", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0x479774e4e9c8198a5067288632b89445f5cbe0e3", + "positionRecipient": "0xd1c40482a64b140879e82cf20edfd6c5b10a4086", + "positionTokenId": "355996", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0x3263d43cd0285d194cf19addb773506b9d2440e84b2c4946700915d4a5b5b39e", + "quoteConfigurationHash": "0x97e868a6ca86c11a7ce360cbb91c6c63d3368d48e7f72d952de545b2e4e07ff8", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999996523", + "lockedTokenDust": "3477", + "initialTick": -194600, + "tickLower": -194600, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "156546312446187031", + "initialBuyTokenAmount": "41939111464535342457332247", + "initialBuyEthAmount": "60000000000000000", + "launchOccurrenceId": "1:0x0f4f3548fa9981ee9aa5c90552c6564b7ba9c807786fe8cc926775273d646f34:0xde92436a374181f77ccfd6c9d2723e0527f5050c7a1bfb194b8f85356011131f:29", + "liquidityOccurrenceId": "1:0x0f4f3548fa9981ee9aa5c90552c6564b7ba9c807786fe8cc926775273d646f34:0xde92436a374181f77ccfd6c9d2723e0527f5050c7a1bfb194b8f85356011131f:30", + "initialBuyOccurrenceId": "1:0x0f4f3548fa9981ee9aa5c90552c6564b7ba9c807786fe8cc926775273d646f34:0xde92436a374181f77ccfd6c9d2723e0527f5050c7a1bfb194b8f85356011131f:31", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x0f4f3548fa9981ee9aa5c90552c6564b7ba9c807786fe8cc926775273d646f34:0xde92436a374181f77ccfd6c9d2723e0527f5050c7a1bfb194b8f85356011131f:34", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25646355" + }, + { + "id": "1:stock-paired-v3:0xf0eaf9a4e1e51b32869fe5ac724c434e9d93a28564f0421cc066f396961a3fc2", + "chainId": 1, + "model": "stock-paired", + "releaseVersion": "stock-paired-v3", + "launchHash": "0xf0eaf9a4e1e51b32869fe5ac724c434e9d93a28564f0421cc066f396961a3fc2", + "token": "0x1af043d567cdfd23c7050828e50eebaadec5a85f", + "creator": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + "quoteAsset": "0x2d1f7226bd1f780af6b9a49dcc0ae00e8df4bdee", + "poolId": "0x316fcd4d0dfb9c52f4688ff9649e1d1df46a556df2fdfae3b4e67ceaed30218c", + "hook": "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + "rewardVault": "0xa5fb9b33df7fa2884ee5ad033b06c30f2156924c", + "positionRecipient": "0x1c016751794c3bc4108b9fb3f73127889a976ff4", + "positionTokenId": "356759", + "totalSwapFeeBps": null, + "buySwapFeeBps": null, + "sellSwapFeeBps": null, + "rewardConfigurationHash": "0xfe7c2230d252e0e6472ea4a89812318dd9957c0d5eedd41810a85b7ac6a7890b", + "quoteConfigurationHash": "0x3e7d93e83d986a7de1518061008cf425d1a72a167bc3aed6fc4b1b6193252d66", + "totalSupply": "1000000000000000000000000000", + "tokenLiquidityAmount": "999999999999999999999991835", + "lockedTokenDust": "8165", + "initialTick": -181200, + "tickLower": -181200, + "tickUpper": 887200, + "lpFeePips": 0, + "initialBuyQuoteAmount": "48391794909200978", + "initialBuyTokenAmount": "3530964092285107555957789", + "initialBuyEthAmount": "5000000000000000", + "launchOccurrenceId": "1:0x7da87b204383123192b653c16f47335f5c5e295b6afd0c56cf3d35312f0e3910:0xb26b3b9c6635d80aa6c581685d726394279720b5cf0185a3e4b2b99587ea36a1:337", + "liquidityOccurrenceId": "1:0x7da87b204383123192b653c16f47335f5c5e295b6afd0c56cf3d35312f0e3910:0xb26b3b9c6635d80aa6c581685d726394279720b5cf0185a3e4b2b99587ea36a1:338", + "initialBuyOccurrenceId": "1:0x7da87b204383123192b653c16f47335f5c5e295b6afd0c56cf3d35312f0e3910:0xb26b3b9c6635d80aa6c581685d726394279720b5cf0185a3e4b2b99587ea36a1:339", + "custodyOccurrenceId": null, + "coordinatorOccurrenceId": "1:0x7da87b204383123192b653c16f47335f5c5e295b6afd0c56cf3d35312f0e3910:0xb26b3b9c6635d80aa6c581685d726394279720b5cf0185a3e4b2b99587ea36a1:342", + "hasLaunchEvent": true, + "hasLiquidityEvent": true, + "hasInitialBuyEvent": true, + "hasCustodyEvent": false, + "hasCoordinatorEvent": true, + "hasPoolRegistrationEvent": true, + "hasPoolFeeDisclosureEvent": true, + "hasRewardVaultFactoryEvent": true, + "provenanceValid": false, + "isComplete": false, + "updatedBlock": "25649362" + } + ], + "digest": "0xc826c240381a57295707e138bd49ceddef18abd81e5e19731dffb4a160d8e09f" +} diff --git a/docs/data-pipeline/envio-candidate-7f24e63-deployment-7ffd15c.json b/docs/data-pipeline/envio-candidate-7f24e63-deployment-7ffd15c.json new file mode 100644 index 00000000..071857a7 --- /dev/null +++ b/docs/data-pipeline/envio-candidate-7f24e63-deployment-7ffd15c.json @@ -0,0 +1,134 @@ +{ + "schemaVersion": 1, + "kind": "envio-candidate-deployment-evidence", + "status": "deployed-synced-audited-not-promoted", + "observedAt": "2026-08-01T04:20:59.886Z", + "historicalPreparation": { + "path": "docs/data-pipeline/envio-candidate-7f24e63.json", + "fileSha256": "0xcf702343bcc7a724fbeca0731eb0c5a9c6a99080bccd04f5050273120ba31bf1", + "recordedStatus": "prepared-not-deployed", + "preservedUnchanged": true + }, + "auditRunner": { + "repository": "https://github.com/0xprogrammable/programmable", + "repositoryCommit": "f46fbf9ee2776b8d32789b7b3a68a063c2b0467d", + "script": "indexer/scripts/release-candidate.mjs", + "sourceArtifactsMatchReviewedCommit": true + }, + "source": { + "repository": "https://github.com/0xprogrammable/programmable", + "commit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "root": "indexer" + }, + "deploymentMirror": { + "repository": "https://github.com/0xprogrammable/programmable-indexer", + "branch": "production", + "branchProtected": false, + "candidateCommit": "7ffd15c2a28c481a2d3632e30b315262c2471b2e" + }, + "candidate": { + "controlPlaneStatus": "none", + "promoted": false, + "endpoint": "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + "endpointId": "d7a39a2", + "deploymentLabel": "production-7f24e63", + "identity": { + "sourceCommit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + "handlerSha256": "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + "sourceRegistrySha256": "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51 + } + }, + "checkpoint": { + "progressBlock": "25657578", + "bufferBlock": "25657578", + "sourceBlock": "25657590", + "eventsProcessed": "56002", + "stateProgressBlock": "25657571", + "stateProgressBlockHash": "0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716", + "stateProgressTimestamp": "1785557819", + "stateProgressTransactionHash": "0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1", + "stateProgressOccurrenceId": "1:0x33fd1bec65993e927529de3ef1b2a86b5e0c256bced4c6da1dfe905153684716:0x8912223855db7593efdcf58ea0297efaa48957153dc2acbbf49f73cf7e7943d1:778" + }, + "artifacts": { + "baseline": { + "path": "docs/data-pipeline/envio-candidate-7f24e63-baseline-20260801T042058Z.json", + "fileSha256": "0x2305e0782d4ad34132afbb753e3abb0f22add937f04e3de12d35188e49eb6b36", + "internalDigest": "0xc826c240381a57295707e138bd49ceddef18abd81e5e19731dffb4a160d8e09f", + "inventorySha256": "0x5a388ae00ff52fd63abf45560cdb456cafe883c17249cabce83ca31286104c6d" + }, + "candidateAudit": { + "path": "docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z.json", + "fileSha256": "0x4fdda10f8c0d824f2cfa455e4efe29b72a0b5de53411e315fd9f6a0cadaef5ba", + "internalDigest": "0x42199d74de38991b95324b937a3765932a612999260371c9d9f9e631da9ded97", + "inventorySha256": "0xa63d33aaee6065a612cd2a318496e9379c20389d3ca8fe0663919d25e6f2ad04", + "authenticatedCoordinatorCreatorRepairs": 52 + }, + "identity": { + "path": "docs/data-pipeline/envio-candidate-identity-7f24e63.json", + "fileSha256": "0x2f443d5e450b958f73f46f5ac63aa2426aca4e6135342f5cb6758382f545a542" + } + }, + "inventory": { + "count": 265, + "perRelease": { + "classic-v2": 27, + "classic-v3": 186, + "stock-paired-v1": 1, + "stock-paired-v2": 8, + "stock-paired-v3": 43 + } + }, + "nonPromotableDeployments": [ + { + "mirrorCommit": "6f2f408e137ce3c01450a13ed11f477ae4ac7240", + "controlPlaneStatus": "none", + "endpoint": "https://indexer.hyperindex.xyz/2ae4d96/v1/graphql", + "runtimeDeployment": "development-unverified", + "promotable": false, + "rejection": "candidate IndexerState.sourceCommit is invalid" + } + ], + "procedureDeviations": [ + { + "id": "candidate-pushed-directly-to-mirror-production", + "expected": "review branch followed by reviewed deployment commit", + "observed": "mirror production branch points to 7ffd15c2a28c481a2d3632e30b315262c2471b2e" + } + ], + "activeProduction": { + "controlPlaneStatus": "prod", + "mirrorCommit": "2cb1c35c7738fea63e656ad11589664dc93d785d", + "deploymentLabel": "production-1e7c381", + "endpoint": "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + "sourceCommit": "1e7c38125714e2f485f8be0c665b12e7d7fb1809" + }, + "rollback": { + "deploymentMirrorCommit": "2cb1c35c7738fea63e656ad11589664dc93d785d", + "deployment": "production-1e7c381", + "graphqlEndpoint": "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + "sourceCommit": "1e7c38125714e2f485f8be0c665b12e7d7fb1809", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0x3217def060af2d1053ec3bca854187ff547fb43d91b113bc87a9f3285489362d", + "handlerSha256": "0x241e18c3eda104b96eec4142826459c41c39cbce0474322634b5ea161d2fdf3e", + "sourceRegistrySha256": "0x552e941d2ad7fea1184bf1efb97f840bdce9835c647b76f753f1326c6afe211f", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51 + }, + "promotion": { + "state": "not-promoted", + "productionBindingMayChange": false, + "requiredBeforePromotion": [ + "postgres-backfill", + "dual-rpc-reconciliation", + "same-checkpoint-route-parity", + "reorg-gate", + "performance-gate", + "monitoring-gate", + "staged-vercel-gate" + ] + } +} diff --git a/docs/data-pipeline/envio-candidate-7f24e63.json b/docs/data-pipeline/envio-candidate-7f24e63.json new file mode 100644 index 00000000..6f5d63ab --- /dev/null +++ b/docs/data-pipeline/envio-candidate-7f24e63.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": 1, + "kind": "envio-release-candidate", + "status": "prepared-not-deployed", + "source": { + "repository": "https://github.com/0xprogrammable/programmable", + "commit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "root": "indexer" + }, + "deploymentMirror": { + "repository": "https://github.com/0xprogrammable/programmable-indexer", + "branch": "production", + "candidateCommit": null + }, + "identity": { + "deployment": "production-7f24e63", + "sourceCommit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + "handlerSha256": "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + "sourceRegistrySha256": "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51 + }, + "historicalScope": [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3" + ], + "inventoryPolicy": { + "countIsDerived": true, + "freezeImmediatelyBeforeReplay": true, + "requireEveryFrozenLaunch": true, + "permitLaterEligibleLaunches": true, + "requireCompleteAndProvenanceValid": true, + "requireEveryHistoricalRelease": true + }, + "observedLiveBaseline": { + "observedAt": "2026-08-01T01:48:00Z", + "deployment": "production-1e7c381", + "inventoryCount": 265, + "releaseCounts": { + "classic-v2": 27, + "classic-v3": 186, + "stock-paired-v1": 1, + "stock-paired-v2": 8, + "stock-paired-v3": 43 + }, + "note": "This count is an observation, not a release constant. The release gate freezes and checks the then-current inventory." + }, + "rollback": { + "deploymentMirrorCommit": "2cb1c35c7738fea63e656ad11589664dc93d785d", + "deployment": "production-1e7c381", + "graphqlEndpoint": "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + "sourceCommit": "1e7c38125714e2f485f8be0c665b12e7d7fb1809", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0x3217def060af2d1053ec3bca854187ff547fb43d91b113bc87a9f3285489362d", + "handlerSha256": "0x241e18c3eda104b96eec4142826459c41c39cbce0474322634b5ea161d2fdf3e", + "sourceRegistrySha256": "0x552e941d2ad7fea1184bf1efb97f840bdce9835c647b76f753f1326c6afe211f", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51, + "productBinding": "config/data-pipeline-release.v1.json" + } +} diff --git a/docs/data-pipeline/envio-candidate-identity-7f24e63.json b/docs/data-pipeline/envio-candidate-identity-7f24e63.json new file mode 100644 index 00000000..57bd1205 --- /dev/null +++ b/docs/data-pipeline/envio-candidate-identity-7f24e63.json @@ -0,0 +1,10 @@ +{ + "deployment": "production-7f24e63", + "sourceCommit": "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + "configSha256": "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + "schemaSha256": "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + "handlerSha256": "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + "sourceRegistrySha256": "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + "eventSetSha256": "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + "eventCount": 51 +} diff --git a/docs/operations/read-model-scheduler-cutover.md b/docs/operations/read-model-scheduler-cutover.md new file mode 100644 index 00000000..87f74bff --- /dev/null +++ b/docs/operations/read-model-scheduler-cutover.md @@ -0,0 +1,110 @@ +# Read-model scheduler cutover + +The production scheduler keeps the durable legacy index active while the +Postgres read model is staged. This avoids a gap in Explore or token discovery +during backfill and parity checks. + +## Schedule + +| Worker | Route | UTC schedule | Activation | +| --- | --- | --- | --- | +| Legacy index | `/api/ops/index-v2` | Every five minutes | Retained until indexed reads are promoted | +| Source projector | `/api/ops/projector` | Every minute | `PROGRAMMABLE_PROJECTOR_ACTIVE=true` | +| Market projector | `/api/ops/market-projector` | Every minute | `PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE=true` | +| QuickNode stream wake | `POST /api/ops/projector-wake` | Every delivered block | `PROGRAMMABLE_QUICKNODE_STREAM_SECRET` configured | + +Each projector has its own singleton execution guard. A second invocation +returns busy instead of overlapping an unfinished run. The market projector +only reads the last fully committed source checkpoint, never in-flight source +state. Both routes require Vercel's `CRON_SECRET` bearer token. Missing or exact +`false` activation values are harmless disabled runs. Exact `true` is the only +active value. Any other non-empty value is a configuration error and must fail +closed. + +The QuickNode stream is an authenticated latency trigger, not a third source of +truth. Its payload never enters the read model. A valid delivery returns `202` +immediately and uses Next.js background work to run the existing source +projector followed by the market projector. Envio remains the event source; +the independent Alchemy and QuickNode RPC reads, atomic publication fences and +singleton database leases remain mandatory. Duplicate deliveries are safe, and +the per-minute crons remain the watchdog if a webhook is delayed or lost. + +Configure the stream only after the exact staged deployment has passed its +normal release gate: + +1. Create a dedicated random secret of 32 to 1,024 UTF-8 bytes. Store it as + `PROGRAMMABLE_QUICKNODE_STREAM_SECRET` in the staged Vercel environment and + as the QuickNode webhook security token. Never reuse `CRON_SECRET`. +2. Use the Ethereum mainnet block dataset, one block per batch, sequential + delivery, reorg correction enabled and the smallest block-only payload the + stream filter permits. The endpoint accepts JSON and QuickNode gzip bodies, + but rejects encoded bodies above 64 KiB or decoded bodies above 128 KiB. +3. Point the test destination at + `https:///api/ops/projector-wake`. Confirm a signed + test delivery returns `202` and `Cache-Control: no-store`; invalid, + replayed, stale or malformed deliveries must not schedule work. +4. After production promotion and binding checks, move the destination to + `https://programmable.family/api/ops/projector-wake`, capture the stream ID + and destination evidence, then verify source and market projector telemetry + for at least two delivered blocks. Disable the stream on repeated `401`, + `413` or `5xx` responses; the minute crons keep the read model progressing. + +Visible Explore, token detail and price-chart clients refresh every five +seconds while the tab is visible and on focus. Ready public read-model +responses use a two-second CDN freshness window. This removes the scheduler and +cache minute-scale delay; it does not promise sub-second chain finality or hide +Envio, RPC, reorg or database latency. + +`/api/ops/index-v2` is the only legacy writer route. The former +`/api/ops/index` alias is permanently closed and is not scheduled. + +The route, runtime and migration SHA-256 values in +`config/read-model-operations.v1.json` are release inputs, not documentation. +The operations gate rejects any byte drift until the changed source is reviewed +and the approved digest is updated in the same commit. + +The pre-parity reconciler is deliberately absent from `vercel.json`. It remains +manual until its exact-block reader covers every active Classic and Stock-Paired +release family. + +## Promotion order + +Before this workflow is enabled, turn off **Auto-assign Custom Production +Domains** for the Vercel project. Git-connected production pushes must create +deployments without moving `programmable.family`; only the reviewed workflow +may promote it. The workflow records the current deployment before staging and +fails if Vercel has already moved production to the candidate commit. + +1. Produce and review the deterministic hosted database plan, then apply and + verify every ordered `supabase/migrations/*.sql` file at the exact reviewed + commit. `config/read-model-operations.v1.json` pins worker-specific release + inputs; it is not the complete migration inventory. Follow + `docs/data-pipeline/HOSTED-DATABASE-OPERATOR.md` and keep bootstrap separate. +2. Backfill Envio and Postgres at an exact, recorded checkpoint. +3. Enable the source projector and prove it catches up without partial-block + publication. +4. Enable the market projector and prove its market lineage at the same source + checkpoint. +5. Configure and test the QuickNode stream against the exact staged deployment, + but keep its production destination disabled. +6. Capture signed staged-deployment evidence and run the release gate. +7. Promote the exact staged deployment ID, never a mutable alias. +8. Verify that `programmable.family` resolves to that deployment ID and commit, + then verify health, populated Explore, the token list, and every indexed + route using the same release corpus. +9. Enable indexed read flags only after every check is green, then activate and + verify the stream's production destination. +10. Remove the legacy cron in a later reviewed cutover commit. + +Source files, migrations, schedules, activation names, workflow ordering and +post-promotion probes are checked by `npm run perf:read-model:ops-gate`. + +## Failure behavior + +An unauthorized cron call returns `401`. Invalid configuration, unavailable +dependencies or incomplete evidence returns `503` with `Cache-Control: +no-store`. A disabled worker does not open database or RPC connections. Public +read flags stay on the legacy path until signed release evidence for the exact +Vercel deployment is accepted. If any post-promotion binding or route check +fails, the workflow rolls the production domains back to the exact deployment +captured before staging and verifies that rollback binding. diff --git a/docs/operations/reconciler-preparity.md b/docs/operations/reconciler-preparity.md new file mode 100644 index 00000000..df1597ff --- /dev/null +++ b/docs/operations/reconciler-preparity.md @@ -0,0 +1,79 @@ +# Exact-checkpoint reconciler bootstrap + +The pre-parity reconciler is intentionally fail closed. It cannot read general +private tables and it cannot use a public indexed route as its own comparison +source. + +## Boundary + +One request identifies a complete checkpoint: + +- chain, release, model and source group +- epoch and pointer generation +- checkpoint ID, block number and block hash + +The database reader accepts the request only when every field still identifies +the current checkpoint. It returns the immutable projection fold manifest, the +exact applicable-route eligibility contract and a bounded list of current entity +identities. + +The runtime then: + +1. Reads that narrow contract and closes the read transaction. +2. Confirms the same block number and hash independently through Alchemy and + QuickNode. It does not read or compare latest heads. +3. Builds every applicable live DTO once per provider at that exact block. +4. Builds the same indexed DTO set for the same checkpoint. +5. Requires complete route coverage, non-empty comparisons and agreement + between both live providers. +6. Appends the reconciliation, one parity row and checkpoint binding per + applicable route, and the terminal outcome through one database function + call. + +An indexed mismatch is stored as a failed reconciliation. It never becomes +current route parity. + +## Activation boundary + +`POST /api/ops/reconcile-preparity` is protected by `CRON_SECRET`, accepts only +an explicit checkpoint request and returns `Cache-Control: no-store`. + +The server now provides the narrow indexed-corpus store and the strict RPC +transport used by an exact-block route reader. The transport binds each source +to its reviewed endpoint commitment, uses EIP-1898 `blockHash` plus +`requireCanonical` for `eth_call`, bounds physical requests and log results, +and verifies the checkpoint before and after each complete source read. + +Route coverage is release-specific. Classic V2 covers the four discovery and +creator routes. Stock releases add `launch-lookup`. Classic V3 additionally +covers `classic-v3-profile`, for six routes in total. A release can never gain +parity by submitting an empty DTO for a route it does not serve. + +Classic V2, Classic V3 and Stock V1 through V3 each have a release-specific +live builder. Every builder verifies its pinned release runtimes, reconstructs +launch provenance from canonical logs, binds each launch to its successful +transaction and receipt, and reads contract state with EIP-1898 at the agreed +block hash. The shared assembler then materializes only that release's +applicable corpora without depending on current parity: + +- `explore-list` +- `explore-token` +- `explore-chart` +- `creator-profile` +- `classic-v3-profile` +- `launch-lookup` + +The Stock builders preserve reads for existing launches only. Wiring their +historical reconciliation does not enable a Stock launch path. + +Every release is allowlisted independently and remains unavailable until its +own reviewed DTO family is wired. Do not replace a missing builder with public +read views, latest-block calls, event-corpus hashes or a projection identity +comparison. Those substitutions would compare the index with itself or record +parity for a different chain state. + +This route is not scheduled. Activation still requires successful fixtures and +provider-backed live dry runs for Classic V2, Classic V3 and all three Stock +releases, production reconciler credentials and complete checkpoint identities +from the projector handoff. The scheduler must never infer a checkpoint from a +latest-block RPC response. diff --git a/docs/public-indexer-feed.md b/docs/public-indexer-feed.md index 935dbcc5..3dc36bbb 100644 --- a/docs/public-indexer-feed.md +++ b/docs/public-indexer-feed.md @@ -15,7 +15,7 @@ the hook, fee disclosure and launch provenance. ## Launch models -`launch.modelId` is the stable product model, such as `classic`, `deep` or +`launch.modelId` is the stable product model, such as `classic` or `stock-paired`. `launch.modelVersion` identifies the contract release where a version is required. diff --git a/eslint.config.mjs b/eslint.config.mjs index d30df407..f82e2151 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,5 +15,7 @@ export default defineConfig([ "contracts/out/**", "contracts/cache/**", "contracts/broadcast/**", + "indexer/.envio/**", + "indexer/envio-env.d.ts", ]), ]); diff --git a/indexer/.env.example b/indexer/.env.example new file mode 100644 index 00000000..de58673d --- /dev/null +++ b/indexer/.env.example @@ -0,0 +1,14 @@ +# Tests, typecheck, and code generation require no credentials. +# Local Ethereum Mainnet sync may require an Envio API token. +ENVIO_API_TOKEN= + +# Complete deployment identity persisted in IndexerState. Keep the label at +# its fail-closed default and leave the commitments unset for local work. +ENVIO_DEPLOYMENT_LABEL=development-unverified +ENVIO_SOURCE_COMMIT= +ENVIO_CONFIG_SHA256= +ENVIO_SCHEMA_SHA256= +ENVIO_HANDLER_SHA256= +ENVIO_SOURCE_REGISTRY_SHA256= +ENVIO_EVENT_SET_SHA256= +ENVIO_EVENT_COUNT= diff --git a/indexer/.gitignore b/indexer/.gitignore new file mode 100644 index 00000000..78f318de --- /dev/null +++ b/indexer/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.env +.env.* +!.env.example +.envio/ +generated/ +dist/ +coverage/ diff --git a/indexer/README.md b/indexer/README.md new file mode 100644 index 00000000..f488f1c0 --- /dev/null +++ b/indexer/README.md @@ -0,0 +1,129 @@ +# Programmable Ethereum event indexer + +This isolated Envio HyperIndex project builds a reorg-aware read model for the +active Programmable releases on Ethereum Mainnet: + +- Classic V2 and Classic V3 +- Stock-Paired V1, V2 and V3 + +All other releases are intentionally out of scope. Source addresses and +inclusive start blocks are pinned to the checked-in deployment manifests. +Shared Stock V2/V3 hook and vault-factory events are attributed only after an +indexed `poolId` relation identifies the release. + +## Safety boundary + +Envio stores fork-specific candidate occurrences. A candidate ID is: + +```text +1::: +``` + +The block-global log index is not treated as a durable application identity. +`receiptLogOrdinal` and `downstreamLogicalId` remain unset here. A separate +application worker must compare receipts from two independent RPC providers, +match the exact candidate log, derive the receipt-local ordinal, and only then +write the logical identity. + +Handlers write only to Envio's transactional entity store. They do not call +Supabase, Blob storage, RPC endpoints, webhooks, files, or other external +systems. Beneficiary seed hydration is a separate block-pinned dual-RPC worker. + +## Local setup + +Requirements: + +- Node.js 22 or newer +- pnpm 10.32.0 +- Docker for a full local Envio stack + +From this directory: + +```bash +pnpm install --frozen-lockfile +pnpm codegen +pnpm typecheck +pnpm test +``` + +Those three validation commands require no production credentials. The test +suite uses Envio's typed `createTestIndexer()` simulations. Dynamic-registration +fixtures extend the simulated head by the configured 12-block lag so that the +factory block is finalized. + +For a local historical sync, copy `.env.example` to `.env`, provide a local +Envio API token if HyperSync requests one, then run: + +```bash +pnpm dev +``` + +Generated `.envio/` output, local environment files, databases, and coverage +artifacts are ignored. `envio-env.d.ts` and `pnpm-lock.yaml` are committed for +reproducible code generation and installs. + +## Data guarantees + +- Ethereum Mainnet only, with a 12-block lag and 200-block reorg depth. +- Lowercase addresses and hashes. +- Exact bigint accounting; no floating-point arithmetic. +- Deterministic payload hashes over reconstructed event topics and data. +- Full uint32 transaction, block-global log, and receipt-local ordinal domains; + Envio stores them as exact `BigInt` values rather than GraphQL `Int`. +- Canonical zero-byte event data (`0x`) is valid for indexed-only events. +- Duplicate candidate delivery does not increment aggregate fee totals twice. +- Factory events register reward vaults in the same block. +- Envio rollback removes orphaned current-state effects transactionally. + +The singleton `IndexerState` reports schema, chain, the latest processed +candidate, and the complete deployment identity. A reviewed deployment must +set `ENVIO_DEPLOYMENT_LABEL`, `ENVIO_SOURCE_COMMIT`, `ENVIO_CONFIG_SHA256`, +`ENVIO_SCHEMA_SHA256`, `ENVIO_HANDLER_SHA256`, +`ENVIO_SOURCE_REGISTRY_SHA256`, `ENVIO_EVENT_SET_SHA256`, and +`ENVIO_EVENT_COUNT`. Any missing, malformed, uppercase, zero-sentinel, or +incomplete value fails the whole identity closed to `development-unverified`. +Accepted labels use lowercase ASCII letters, digits, `.`, `_`, and `-`, +beginning with a letter or digit. + +Keep the default for local and unreviewed deployments. A production promotion +can set an explicit immutable reviewed label such as +`production-reviewed-2026-07-31` together with commitments computed from the +exact deployed commit. These fields record identity only; they are not proof +that the deployment passed the production gates below. + +## Deployment identity + +The runtime validates the complete identity supplied through the eight +`ENVIO_*` environment variables above and fails closed when any commitment is +missing or malformed. Artifact generation, historical baseline reproduction, +and Envio deployment evidence are owned by the canonical +[`programmable-indexer`](https://github.com/0xprogrammable/programmable-indexer) +repository so a clean product checkout never depends on Git objects from a +different repository. + +The four artifact commitments are SHA-256 hashes of the exact bytes in +`config.yaml`, `schema.graphql`, `src/EventHandlers.ts`, and +`src/lib/release-map.ts`. The event-set commitment is generated separately: + +1. Read every `contracts[].events[].event` signature from `config.yaml`. +2. Keep repeated signatures when different contracts emit the same event. +3. Sort the UTF-8 signatures bytewise. +4. Join them with `\n`, append one final `\n`, and hash those bytes with + SHA-256. + +The product release binding remains pinned to the last activated Envio +identity until a new indexer deployment has been reviewed, backfilled, +reconciled, and explicitly promoted. A candidate source checkout or passing +local test must not update that binding. + +## Production status + +Passing local checks or running an Envio development deployment does not make +this indexer a production authority. Production use still requires a reviewed +deployment, historical backfill, dual-RPC reconciliation, parity evidence, +monitoring, provider configuration, and an explicit release decision. + +Transaction preparation, claims, and launch availability must continue to +revalidate current manifests, runtime code, ownership, balances, and simulation +results onchain. Indexed entities are read-model inputs, not signing or +authorization evidence. diff --git a/indexer/config.yaml b/indexer/config.yaml new file mode 100644 index 00000000..b4dd82f1 --- /dev/null +++ b/indexer/config.yaml @@ -0,0 +1,143 @@ +# yaml-language-server: $schema=./node_modules/envio/evm.schema.json +name: programmable-realtime-mainnet +description: Candidate Ethereum Mainnet events for the Programmable verified read model +handlers: src +rollback_on_reorg: true +save_full_history: false +raw_events: false +address_format: lowercase +field_selection: + transaction_fields: + - hash + - transactionIndex +contracts: + - name: ClassicV2Launcher + events: + - event: "MemeTokenLaunched(address indexed creator, address indexed token, bytes32 indexed poolId, address feeHook, address positionRecipient, uint256 positionTokenId, uint16 totalSwapFeeBps, bytes32 launchHash)" + - event: "MemeLiquidityConfigured(address indexed token, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)" + - event: "MemeCreatorInitialBuy(address indexed creator, address indexed token, bytes32 indexed poolId, uint256 nativeAmount, uint256 tokenAmount, bytes32 launchHash)" + - name: ClassicV2Hook + events: + - event: "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed creator, address registrar, uint16 totalSwapFeeBps)" + - event: "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)" + - event: "NativeSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, uint256 grossNativeAmount, uint256 creatorFee, uint256 launcherFee)" + - event: "CreatorFeesClaimed(bytes32 indexed poolId, address indexed creator, address indexed recipient, address caller, uint256 amount)" + - event: "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed caller, uint256 amount)" + - name: ClassicV3Launcher + events: + - event: "MemeTokenLaunchedV2(address indexed deployer, address indexed token, bytes32 indexed poolId, address feeHook, address rewardVault, address positionRecipient, uint256 positionTokenId, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, bytes32 rewardConfigurationHash, bytes32 launchHash)" + - event: "MemeLiquidityConfiguredV2(address indexed token, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)" + - event: "MemeCreatorInitialBuyV2(address indexed deployer, address indexed token, bytes32 indexed poolId, uint256 nativeAmount, uint256 tokenAmount, bytes32 launchHash)" + - event: "MemeCreatorInitialBuyCustodyV2(address indexed deployer, address indexed token, address indexed custody, uint8 mode, uint16 durationDays, uint16 cliffDays, bytes32 configurationHash, bytes32 launchHash)" + - name: ClassicV3Hook + events: + - event: "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed rewardVault, address registrar, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, bytes32 rewardConfigurationHash)" + - event: "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, address indexed rewardVault, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 buyCreatorFeeBps, uint16 sellCreatorFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)" + - event: "NativeSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, bool indexed isBuy, uint16 appliedTotalSwapFeeBps, uint256 grossNativeAmount, uint256 creatorFee, uint256 launcherFee)" + - event: "CreatorFeesClaimed(bytes32 indexed poolId, address indexed rewardVault, address indexed caller, uint256 amount)" + - event: "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed caller, uint256 amount)" + - name: ClassicV3RewardVaultFactory + events: + - event: "ClassicRewardVaultDeployed(address indexed vault, bytes32 indexed poolId, address indexed feeHook, bytes32 salt, bytes32 configurationHash)" + - name: ClassicV3VestingWalletFactory + events: + - event: "ClassicInitialBuyVestingWalletDeployed(address indexed wallet, address indexed token, address indexed beneficiary, bytes32 salt, bytes32 configurationHash)" + - name: ClassicV3RewardVault + events: + - event: "CreatorFeesCheckpointed(bytes32 indexed poolId, uint64 indexed configurationEpoch, uint256 amount, uint256 totalCreatorFeesReceived)" + - event: "BeneficiaryFeesClaimed(address indexed beneficiary, uint256 amount, uint256 beneficiaryTotalClaimed, uint256 vaultTotalReceived)" + - event: "PayoutWalletChanged(bytes32 indexed poolId, uint256 indexed allocationIndex, address indexed previousPayoutWallet, address newPayoutWallet, uint16 shareBps, uint64 configurationEpoch, bytes32 activeConfigurationHash, uint256 effectiveTotalCreatorFeesReceived)" + - event: "CtoRewardConfigurationActivated(bytes32 indexed poolId, bytes32 indexed approvalReference, uint64 indexed configurationEpoch, bytes32 previousConfigurationHash, bytes32 newConfigurationHash, address[] beneficiaries, uint16[] sharesBps, uint256 effectiveTotalCreatorFeesReceived)" + - name: StockV1Launcher + events: + - event: "StockPairedTokenLaunched(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, address rewardVault, address positionRecipient, uint256 positionTokenId, bytes32 launchHash)" + - event: "StockPairedLiquidityConfigured(address indexed token, address indexed quoteAsset, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)" + - event: "StockPairedCreatorInitialBuy(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, uint256 quoteAmount, uint256 tokenAmount, bytes32 launchHash)" + - name: StockV1EthCoordinator + events: + - event: "StockPairedEthTokenLaunched(address indexed creator, address indexed token, address indexed quoteAsset, uint256 initialBuyEthAmount, uint256 initialBuyQuoteAmount, uint256 initialBuyTokenAmount, bytes32 launchHash)" + - name: StockV1Hook + events: + - event: "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, address registrar, bool quoteIsCurrency0, bytes32 rewardConfigurationHash, bytes32 quoteConfigurationHash)" + - event: "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 creatorFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)" + - event: "QuoteSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, address indexed quoteAsset, bool isBuy, uint256 grossQuoteAmount, uint256 creatorFee, uint256 launcherFee)" + - event: "CreatorFeesClaimed(bytes32 indexed poolId, address indexed rewardVault, address indexed quoteAsset, address caller, uint256 amount)" + - event: "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed quoteAsset, address caller, uint256 amount)" + - name: StockV1RewardVaultFactory + events: + - event: "QuoteAssetFeeSplitVaultDeployed(address indexed vault, address indexed feeHook, bytes32 indexed poolId, address quoteAsset)" + - name: StockV1RewardVault + events: + - event: "PayoutAddressUpdated(address indexed beneficiary, address indexed previousPayoutAddress, address indexed newPayoutAddress)" + - event: "BeneficiaryFeesClaimed(address indexed beneficiary, address indexed payoutAddress, address indexed quoteAsset, uint256 amount, uint256 beneficiaryTotalClaimed, uint256 vaultTotalReceived)" + - name: StockV2Launcher + events: + - event: "StockPairedTokenLaunched(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, address rewardVault, address positionRecipient, uint256 positionTokenId, bytes32 launchHash)" + - event: "StockPairedLiquidityConfigured(address indexed token, address indexed quoteAsset, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)" + - event: "StockPairedCreatorInitialBuy(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, uint256 quoteAmount, uint256 tokenAmount, bytes32 launchHash)" + - name: StockV2EthCoordinator + events: + - event: "StockPairedEthTokenLaunched(address indexed creator, address indexed token, address indexed quoteAsset, uint256 initialBuyEthAmount, uint256 initialBuyQuoteAmount, uint256 initialBuyTokenAmount, bytes32 launchHash)" + - name: StockV3Launcher + events: + - event: "StockPairedTokenLaunched(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, address rewardVault, address positionRecipient, uint256 positionTokenId, bytes32 launchHash)" + - event: "StockPairedLiquidityConfigured(address indexed token, address indexed quoteAsset, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)" + - event: "StockPairedCreatorInitialBuy(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, uint256 quoteAmount, uint256 tokenAmount, bytes32 launchHash)" + - name: StockV3EthCoordinator + events: + - event: "StockPairedEthTokenLaunched(address indexed creator, address indexed token, address indexed quoteAsset, uint256 initialBuyEthAmount, uint256 initialBuyQuoteAmount, uint256 initialBuyTokenAmount, bytes32 launchHash)" + - name: StockV2V3Hook + events: + - event: "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, address registrar, bool quoteIsCurrency0, bytes32 rewardConfigurationHash, bytes32 quoteConfigurationHash)" + - event: "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 creatorFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)" + - event: "QuoteSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, address indexed quoteAsset, bool isBuy, uint256 grossQuoteAmount, uint256 creatorFee, uint256 launcherFee)" + - event: "CreatorFeesClaimed(bytes32 indexed poolId, address indexed rewardVault, address indexed quoteAsset, address caller, uint256 amount)" + - event: "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed quoteAsset, address caller, uint256 amount)" + - name: StockV2V3RewardVaultFactory + events: + - event: "QuoteAssetFeeSplitVaultDeployed(address indexed vault, address indexed feeHook, bytes32 indexed poolId, address quoteAsset)" + - name: StockV2V3RewardVault + events: + - event: "PayoutAddressUpdated(address indexed beneficiary, address indexed previousPayoutAddress, address indexed newPayoutAddress)" + - event: "BeneficiaryFeesClaimed(address indexed beneficiary, address indexed payoutAddress, address indexed quoteAsset, uint256 amount, uint256 beneficiaryTotalClaimed, uint256 vaultTotalReceived)" +chains: + - id: 1 + start_block: 25624130 + max_reorg_depth: 200 + block_lag: 12 + contracts: + - name: ClassicV2Hook + address: "0x025a386eaa79f6067d29848fd05ccc71beab20cc" + - name: ClassicV2Launcher + address: "0xd240d06f8586eb799f20056054e5b527405e6bad" + - name: ClassicV3RewardVaultFactory + address: "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a" + - name: ClassicV3VestingWalletFactory + address: "0xde21b9c0cc0afdb9be20e8236113f066bb8c66f4" + - name: ClassicV3Hook + address: "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc" + - name: ClassicV3Launcher + address: "0xc3bd04aac2fb2ba58efd7eb673e544e0b80de770" + - name: ClassicV3RewardVault + - name: StockV1Launcher + address: "0x195750f33cad5ef2df857a53226b421297a1e79e" + - name: StockV1EthCoordinator + address: "0xfa5f17389ca28d071781d59750b32c842ab6a54b" + - name: StockV1Hook + address: "0x7773d183fe7b60d4f1885047fa42b815a62fe0cc" + - name: StockV1RewardVaultFactory + address: "0xd430d9162c153afdf9e4caca6d2317e72a044441" + - name: StockV1RewardVault + - name: StockV2Launcher + address: "0x5ea6be24838061ba45dbe8d82de1b267dc240daf" + - name: StockV2EthCoordinator + address: "0xfb9e1034df6161088e8f358502b19e7515c30fd2" + - name: StockV2V3Hook + address: "0x90c67c1e866f86526f0e338459cd435e1f23a0cc" + - name: StockV2V3RewardVaultFactory + address: "0x52d70971d6653a754c29385a2a6f241a481952d4" + - name: StockV2V3RewardVault + - name: StockV3Launcher + address: "0x0573879f72d8ee8b0e5a4ec5e8bcdb2fcab9e51c" + - name: StockV3EthCoordinator + address: "0xddc3abbab0df7f1189310a4f70e7e365796b74e2" diff --git a/indexer/envio-env.d.ts b/indexer/envio-env.d.ts new file mode 100644 index 00000000..c8458812 --- /dev/null +++ b/indexer/envio-env.d.ts @@ -0,0 +1,7 @@ +/** + * This file is generated by HyperIndex codegen. Do not edit manually. + * It wires project-specific types from `.envio/types.d.ts` into the `envio` module. + * If your project's types look out of date, run `envio codegen` + * (or your package manager's `codegen` script, e.g. `pnpm codegen`). + */ +/// diff --git a/indexer/package.json b/indexer/package.json new file mode 100644 index 00000000..a2d2dc8c --- /dev/null +++ b/indexer/package.json @@ -0,0 +1,39 @@ +{ + "name": "@programmable/realtime-indexer", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "packageManager": "pnpm@10.32.0", + "scripts": { + "codegen": "envio codegen", + "dev": "envio dev", + "start": "envio start", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "release:identity": "node scripts/release-candidate.mjs identity", + "release:snapshot": "node scripts/release-candidate.mjs snapshot", + "release:audit": "node scripts/release-candidate.mjs audit" + }, + "dependencies": { + "envio": "3.2.1", + "viem": "2.55.10" + }, + "devDependencies": { + "@types/node": "24.12.2", + "typescript": "6.0.3", + "vitest": "4.1.0", + "yaml": "2.8.3" + }, + "pnpm": { + "overrides": { + "esbuild": "0.28.1", + "express": "4.22.2", + "react-dom": "19.2.5", + "viem": "2.55.10" + } + } +} diff --git a/indexer/pnpm-lock.yaml b/indexer/pnpm-lock.yaml new file mode 100644 index 00000000..9ad56c78 --- /dev/null +++ b/indexer/pnpm-lock.yaml @@ -0,0 +1,3090 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + esbuild: 0.28.1 + express: 4.22.2 + react-dom: 19.2.5 + viem: 2.55.10 + +importers: + + .: + dependencies: + envio: + specifier: 3.2.1 + version: 3.2.1(react-dom@19.2.5(react@19.2.5))(typescript@6.0.3) + viem: + specifier: 2.55.10 + version: 2.55.10(typescript@6.0.3) + devDependencies: + '@types/node': + specifier: 24.12.2 + version: 24.12.2 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 4.1.0 + version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(vite@8.1.5(@types/node@24.12.2)(esbuild@0.28.1)(tsx@4.21.0)(yaml@2.8.3)) + yaml: + specifier: 2.8.3 + version: 2.8.3 + +packages: + + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + + '@alcalzone/ansi-tokenize@0.2.5': + resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} + engines: {node: '>=18'} + + '@clickhouse/client-common@1.17.0': + resolution: {integrity: sha512-MiwwgXViFAQA2YZkN4ymF1ynzG0K49KeSX9/iOcmJetWkxqSekDdpyp1GjwATWa9R215uQ+hGzJtJujeQVZZIw==} + deprecated: 'Deprecated: import from @clickhouse/client or @clickhouse/client-web instead.' + + '@clickhouse/client@1.17.0': + resolution: {integrity: sha512-Y3DQoamKZ/Iyosoq7Lj7lqpDkQDK4R/5mI52yJs4ZLPIO+d6/CYDqTbFBIb4No3C/AlXUYE4TKhj/kXDpe6rOA==} + engines: {node: '>=16'} + + '@elastic/ecs-helpers@1.1.0': + resolution: {integrity: sha512-MDLb2aFeGjg46O5mLpdCzT5yOUDnXToJSrco2ShqGIXxNJaM8uJjX+4nd+hRYV4Vex8YJyDtOFEVBldQct6ndg==} + engines: {node: '>=10'} + + '@elastic/ecs-pino-format@1.4.0': + resolution: {integrity: sha512-eCSBUTgl8KbPyxky8cecDRLCYu2C1oFV4AZ72bEsI+TxXEvaljaL2kgttfzfu7gW+M89eCz55s49uF2t+YMTWA==} + engines: {node: '>=10'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@envio-dev/hyperfuel-client-darwin-arm64@1.2.2': + resolution: {integrity: sha512-eQyd9kJCIz/4WCTjkjpQg80DA3pdneHP7qhJIVQ2ZG+Jew9o5XDG+uI0Y16AgGzZ6KGmJSJF6wyUaaAjJfbO1Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@envio-dev/hyperfuel-client-darwin-x64@1.2.2': + resolution: {integrity: sha512-l7lRMSoyIiIvKZgQPfgqg7H1xnrQ37A8yUp4S2ys47R8f/wSCSrmMaY1u7n6CxVYCpR9fajwy0/356UgwwhVKw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@envio-dev/hyperfuel-client-linux-arm64-gnu@1.2.2': + resolution: {integrity: sha512-kNiC/1fKuXnoSxp8yEsloDw4Ot/mIcNoYYGLl2CipSIpBtSuiBH5nb6eBcxnRZdKOwf5dKZtZ7MVPL9qJocNJw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@envio-dev/hyperfuel-client-linux-x64-gnu@1.2.2': + resolution: {integrity: sha512-XDkvkBG/frS+xiZkJdY4KqOaoAwyxPdi2MysDQgF8NmZdssi32SWch0r4LTqKWLLlCBg9/R55POeXL5UAjg2wQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@envio-dev/hyperfuel-client-linux-x64-musl@1.2.2': + resolution: {integrity: sha512-DKnKJJSwsYtA7YT0EFGhFB5Eqoo42X0l0vZBv4lDuxngEXiiNjeLemXoKQVDzhcbILD7eyXNa5jWUc+2hpmkEg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@envio-dev/hyperfuel-client-win32-x64-msvc@1.2.2': + resolution: {integrity: sha512-SwIgTAVM9QhCFPyHwL+e1yQ6o3paV6q25klESkXw+r/KW9QPhOOyA6Yr8nfnur3uqMTLJHAKHTLUnkyi/Nh7Aw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@envio-dev/hyperfuel-client@1.2.2': + resolution: {integrity: sha512-raKA6DshYSle0sAOHBV1OkSRFMN+Mkz8sFiMmS3k+m5nP6pP56E17CRRePBL5qmR6ZgSEvGOz/44QUiKNkK9Pg==} + engines: {node: '>= 10'} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fuel-ts/crypto@0.96.1': + resolution: {integrity: sha512-OrAZPZtm8HQouzip621/ci47PeTS06QegGdz7MeN6wK4yeCbJmlRQ1WE9S3iclb3p+VLF0RtbecEbHQfsNgsnA==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + + '@fuel-ts/errors@0.96.1': + resolution: {integrity: sha512-Xtso5v4a3UUvnMaOSDhMRlkb9LxLyCyC/1/RY7fZZ035ttscy6dNMuiD/iSptJ5pHklJ5R4rCPdOL5EKpgOaMA==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + + '@fuel-ts/hasher@0.96.1': + resolution: {integrity: sha512-7z4cah+5TOcCBA2Wgvje1L7wVTahiFLb9IUpRXRMVGXwaqsbV/wUNcyuc1mhPh/JKLgcOe+OqsrpcYD2dg2rpg==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + + '@fuel-ts/interfaces@0.96.1': + resolution: {integrity: sha512-mZ3sDHJml5AtLRSmGWo5rbU9//3oKDhAeiFealajcPaVztAAnaKnA5a19dd4ITbjZb2q3e2lQ7zX8iAXgHUwYA==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@fuel-ts/math@0.96.1': + resolution: {integrity: sha512-AZUChguQmE1ILYbcc6SOTjFJIUkGzD3Z6yHgvO4tszn2QS/jVTSAZKVx653J5u9m/Xn/WthwR35l1GbwXMwB4g==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + + '@fuel-ts/utils@0.96.1': + resolution: {integrity: sha512-XXZNEUPf7qtKpVO3ak2CSroBYEKh1Gne1zlmVSNUoVPqQvglcu0I2pu/QmVnZBN4m3yVSyHUoWR2Kbo8/Dh7sA==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + + '@fuel-ts/versions@0.96.1': + resolution: {integrity: sha512-C//ZT7U68Gksz9PzUJVzdzUARK7mfXf3MsF5ZqZaMegkE2tCy+twjy8PBdeVserY0uX6mEHRJyZJwcspk34ixg==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + hasBin: true + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.2.1': + resolution: {integrity: sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@rescript/react@0.14.1': + resolution: {integrity: sha512-tCdzMnzSEuEfWs/A6wq6kLx/E5nBkVJmFST+MwU01W9hzFwQi2JpvE82Fl66ets+oaC5UVpFf3vy4KvXRN+jPA==} + peerDependencies: + react: '>=19.1.0' + react-dom: 19.2.5 + + '@rescript/runtime@12.2.0': + resolution: {integrity: sha512-NwfljDRq1rjFPHUaca1nzFz13xsa9ZGkBkLvMhvVgavJT5+A4rMcLu8XAaVTi/oAhO/tlHf9ZDoOTF1AfyAk9Q==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/bn.js@5.2.0': + resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + + bn.js@5.2.5: + resolution: {integrity: sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==} + + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + cfonts@3.3.1: + resolution: {integrity: sha512-ZGEmN3W9mViWEDjsuPo4nK4h39sfh6YtoneFYp9WLPI/rw8BaSSrfQC6jkrGW3JMvV3ZnExJB/AEqXc/nHYxkw==} + engines: {node: '>=10'} + hasBin: true + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} + engines: {node: '>= 0.2.0'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + date-fns@3.3.1: + resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + envio-darwin-arm64@3.2.1: + resolution: {integrity: sha512-PohM1rGjlNxV0W/BOQOitsVPua944B2t2M0p81x9VpsZOwNWc9SgPsEjIsV4ZJOIceJaHrmWZnfwaWubgDGKMg==} + cpu: [arm64] + os: [darwin] + + envio-darwin-x64@3.2.1: + resolution: {integrity: sha512-70zjTm4tNFzhigja0mHqmG03JQu56e9JCTQR7zoEhVvwj47cSgAPmQIihpaPuDCcMIYibzrlW7pFKdnXAZp3fQ==} + cpu: [x64] + os: [darwin] + + envio-linux-arm64@3.2.1: + resolution: {integrity: sha512-OOVnX+aM8xJ5zYBmpqzSVCX5KFU+wi1gSfbv9X+nXk1rLjicHGm5U847oDmYBF5iyxkJDQBMsW9sr+7X4g8Yhg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + envio-linux-x64-musl@3.2.1: + resolution: {integrity: sha512-/0lWrS/l2VYwdx7t0QvBE8b1R2vsnWpgi2q2xWqSmyOI9SOPU1q9YUElkhaDQFHEUXvqzyDDZPqxQacFtJggXw==} + cpu: [x64] + os: [linux] + libc: [musl] + + envio-linux-x64@3.2.1: + resolution: {integrity: sha512-YACCs+YdemYj4KBYOw/o6fi3hofAWrJ8VOeCwaOWEAzlqQxqkZ6h3O7puZWLnmjgdJvBAReUikFxI+E9MiyHOQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + envio@3.2.1: + resolution: {integrity: sha512-mPvVeomNzi3pz7KqBEpfaiVM8JKZzNphAipT47KiTB+HUrvuMBGmqODgLYUJqQ+iJxiRuCqOVNDy8oCMsZjfwg==} + engines: {node: '>=22.0.0'} + hasBin: true + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@4.1.0: + resolution: {integrity: sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ==} + engines: {node: '>=20.0.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + + fast-copy@4.0.4: + resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@2.7.13: + resolution: {integrity: sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==} + engines: {node: '>= 10.0.0'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ink-big-text@2.0.0: + resolution: {integrity: sha512-Juzqv+rIOLGuhMJiE50VtS6dg6olWfzFdL7wsU/EARSL5Eaa5JNXMogMBm9AkjgzO2Y3UwWCOh87jbhSn8aNdw==} + engines: {node: '>=14.16'} + peerDependencies: + ink: '>=4' + react: '>=18' + + ink-spinner@5.0.0: + resolution: {integrity: sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==} + engines: {node: '>=14.16'} + peerDependencies: + ink: '>=4.0.0' + react: '>=18.0.0' + + ink@6.8.0: + resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} + engines: {node: '>=20'} + peerDependencies: + '@types/react': '>=19.0.0' + react: '>=19.0.0' + react-devtools-core: '>=6.1.2' + peerDependenciesMeta: + '@types/react': + optional: true + react-devtools-core: + optional: true + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-accessor-descriptor@1.0.2: + resolution: {integrity: sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.4: + resolution: {integrity: sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-in-ci@2.0.0: + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} + engines: {node: '>=20'} + hasBin: true + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-sdsl@4.4.2: + resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + ox@0.14.33: + resolution: {integrity: sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + patch-console@2.0.0: + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} + engines: {node: '>=12'} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-reconciler@0.33.0: + resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.2.0 + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rescript-schema@9.5.1: + resolution: {integrity: sha512-g2UEMqZ0IVkW8Os7vDs3JrcSS9pyQNubksY+doa3wgt86Y+c9ruvynikDrZLvZYQyQyF1vLJc05r0r2/jLdeZQ==} + peerDependencies: + rescript: ^12.0.0-alpha.8 + peerDependenciesMeta: + rescript: + optional: true + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-similarity@4.0.4: + resolution: {integrity: sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + viem@2.55.10: + resolution: {integrity: sha512-Q9Ba+/ma81U2M5o5P2AQ7Ux8rTIwmCZvUcr8rKdQ22bV0IBFHllM2m5gWDP8hFaUN2nH2oW3QG44amRazflYNQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: 0.28.1 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + widest-line@6.0.0: + resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} + engines: {node: '>=20'} + + window-size@1.1.1: + resolution: {integrity: sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==} + engines: {node: '>= 0.10.0'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + +snapshots: + + '@adraffy/ens-normalize@1.11.1': {} + + '@alcalzone/ansi-tokenize@0.2.5': + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + '@clickhouse/client-common@1.17.0': {} + + '@clickhouse/client@1.17.0': + dependencies: + '@clickhouse/client-common': 1.17.0 + + '@elastic/ecs-helpers@1.1.0': + dependencies: + fast-json-stringify: 2.7.13 + + '@elastic/ecs-pino-format@1.4.0': + dependencies: + '@elastic/ecs-helpers': 1.1.0 + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@envio-dev/hyperfuel-client-darwin-arm64@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client-darwin-x64@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client-linux-arm64-gnu@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client-linux-x64-gnu@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client-linux-x64-musl@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client-win32-x64-msvc@1.2.2': + optional: true + + '@envio-dev/hyperfuel-client@1.2.2': + optionalDependencies: + '@envio-dev/hyperfuel-client-darwin-arm64': 1.2.2 + '@envio-dev/hyperfuel-client-darwin-x64': 1.2.2 + '@envio-dev/hyperfuel-client-linux-arm64-gnu': 1.2.2 + '@envio-dev/hyperfuel-client-linux-x64-gnu': 1.2.2 + '@envio-dev/hyperfuel-client-linux-x64-musl': 1.2.2 + '@envio-dev/hyperfuel-client-win32-x64-msvc': 1.2.2 + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@fuel-ts/crypto@0.96.1': + dependencies: + '@fuel-ts/errors': 0.96.1 + '@fuel-ts/interfaces': 0.96.1 + '@fuel-ts/math': 0.96.1 + '@fuel-ts/utils': 0.96.1 + '@noble/hashes': 1.8.0 + + '@fuel-ts/errors@0.96.1': + dependencies: + '@fuel-ts/versions': 0.96.1 + + '@fuel-ts/hasher@0.96.1': + dependencies: + '@fuel-ts/crypto': 0.96.1 + '@fuel-ts/interfaces': 0.96.1 + '@fuel-ts/utils': 0.96.1 + '@noble/hashes': 1.8.0 + + '@fuel-ts/interfaces@0.96.1': {} + + '@fuel-ts/math@0.96.1': + dependencies: + '@fuel-ts/errors': 0.96.1 + '@types/bn.js': 5.2.0 + bn.js: 5.2.5 + + '@fuel-ts/utils@0.96.1': + dependencies: + '@fuel-ts/errors': 0.96.1 + '@fuel-ts/interfaces': 0.96.1 + '@fuel-ts/math': 0.96.1 + '@fuel-ts/versions': 0.96.1 + fflate: 0.8.3 + + '@fuel-ts/versions@0.96.1': + dependencies: + chalk: 4.1.2 + cli-table: 0.3.11 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@opentelemetry/api@1.9.1': {} + + '@oxc-project/types@0.139.0': {} + + '@pinojs/redact@0.4.0': {} + + '@rescript/react@0.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@rescript/runtime@12.2.0': {} + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/bn.js@5.2.0': + dependencies: + '@types/node': 24.12.2 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.0(vite@8.1.5(@types/node@24.12.2)(esbuild@0.28.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@24.12.2)(esbuild@0.28.1)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.0': {} + + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + abitype@1.2.3(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + array-flatten@1.1.1: {} + + assertion-error@2.0.1: {} + + atomic-sleep@1.0.0: {} + + auto-bind@5.0.1: {} + + bignumber.js@9.3.1: {} + + bintrees@1.0.2: {} + + bn.js@5.2.5: {} + + body-parser@1.20.6: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + cfonts@3.3.1: + dependencies: + supports-color: 8.1.1 + window-size: 1.1.1 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + cli-boxes@3.0.0: {} + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-spinners@2.9.2: {} + + cli-table@0.3.11: + dependencies: + colors: 1.0.3 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.2 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + colors@1.0.3: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + convert-to-spaces@2.0.1: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.2: {} + + date-fns@3.3.1: {} + + dateformat@4.6.3: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + deepmerge@4.3.1: {} + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.4 + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + dotenv@16.4.5: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + envio-darwin-arm64@3.2.1: + optional: true + + envio-darwin-x64@3.2.1: + optional: true + + envio-linux-arm64@3.2.1: + optional: true + + envio-linux-x64-musl@3.2.1: + optional: true + + envio-linux-x64@3.2.1: + optional: true + + envio@3.2.1(react-dom@19.2.5(react@19.2.5))(typescript@6.0.3): + dependencies: + '@clickhouse/client': 1.17.0 + '@elastic/ecs-pino-format': 1.4.0 + '@envio-dev/hyperfuel-client': 1.2.2 + '@fuel-ts/crypto': 0.96.1 + '@fuel-ts/errors': 0.96.1 + '@fuel-ts/hasher': 0.96.1 + '@fuel-ts/math': 0.96.1 + '@fuel-ts/utils': 0.96.1 + '@rescript/react': 0.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@rescript/runtime': 12.2.0 + bignumber.js: 9.3.1 + date-fns: 3.3.1 + dotenv: 16.4.5 + eventsource: 4.1.0 + express: 4.22.2 + ink: 6.8.0(react@19.2.5) + ink-big-text: 2.0.0(ink@6.8.0(react@19.2.5))(react@19.2.5) + ink-spinner: 5.0.0(ink@6.8.0(react@19.2.5))(react@19.2.5) + js-sdsl: 4.4.2 + pino: 10.3.1 + pino-pretty: 13.1.3 + postgres: 3.4.8 + prom-client: 15.1.3 + react: 19.2.5 + rescript-schema: 9.5.1 + tsx: 4.21.0 + viem: 2.55.10(typescript@6.0.3) + yargs: 17.7.2 + optionalDependencies: + envio-darwin-arm64: 3.2.1 + envio-darwin-x64: 3.2.1 + envio-linux-arm64: 3.2.1 + envio-linux-x64: 3.2.1 + envio-linux-x64-musl: 3.2.1 + transitivePeerDependencies: + - '@types/react' + - bufferutil + - react-devtools-core + - react-dom + - rescript + - supports-color + - typescript + - utf-8-validate + - zod + + environment@1.1.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.1: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-toolkit@1.50.0: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + etag@1.8.1: {} + + eventemitter3@5.0.1: {} + + eventsource-parser@3.1.0: {} + + eventsource@4.1.0: + dependencies: + eventsource-parser: 3.1.0 + + expect-type@1.4.0: {} + + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.6 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-copy@4.0.4: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-json-stringify@2.7.13: + dependencies: + ajv: 6.15.0 + deepmerge: 4.3.1 + rfdc: 1.4.1 + string-similarity: 4.0.4 + + fast-safe-stringify@2.1.1: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fflate@0.8.3: {} + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + help-me@5.0.0: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + indent-string@5.0.0: {} + + inherits@2.0.4: {} + + ink-big-text@2.0.0(ink@6.8.0(react@19.2.5))(react@19.2.5): + dependencies: + cfonts: 3.3.1 + ink: 6.8.0(react@19.2.5) + prop-types: 15.8.1 + react: 19.2.5 + + ink-spinner@5.0.0(ink@6.8.0(react@19.2.5))(react@19.2.5): + dependencies: + cli-spinners: 2.9.2 + ink: 6.8.0(react@19.2.5) + react: 19.2.5 + + ink@6.8.0(react@19.2.5): + dependencies: + '@alcalzone/ansi-tokenize': 0.2.5 + ansi-escapes: 7.3.0 + ansi-styles: 6.2.3 + auto-bind: 5.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + cli-cursor: 4.0.0 + cli-truncate: 5.2.0 + code-excerpt: 4.0.0 + es-toolkit: 1.50.0 + indent-string: 5.0.0 + is-in-ci: 2.0.0 + patch-console: 2.0.0 + react: 19.2.5 + react-reconciler: 0.33.0(react@19.2.5) + scheduler: 0.27.0 + signal-exit: 3.0.7 + slice-ansi: 8.0.0 + stack-utils: 2.0.6 + string-width: 8.2.2 + terminal-size: 4.0.1 + type-fest: 5.8.0 + widest-line: 6.0.0 + wrap-ansi: 9.0.2 + ws: 8.21.1 + yoga-layout: 3.2.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ipaddr.js@1.9.1: {} + + is-accessor-descriptor@1.0.2: + dependencies: + hasown: 2.0.4 + + is-buffer@1.1.6: {} + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.4 + + is-descriptor@1.0.4: + dependencies: + is-accessor-descriptor: 1.0.2 + is-data-descriptor: 1.0.1 + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-in-ci@2.0.0: {} + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + isows@1.0.7(ws@8.21.0): + dependencies: + ws: 8.21.0 + + joycon@3.1.1: {} + + js-sdsl@4.4.2: {} + + js-tokens@4.0.0: {} + + json-schema-traverse@0.4.1: {} + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + minimist@1.2.8: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + negotiator@0.6.3: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.4: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + ox@0.14.33(typescript@6.0.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@6.0.3) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - zod + + parseurl@1.3.3: {} + + patch-console@2.0.0: {} + + path-to-regexp@0.1.13: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.4 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + postcss@8.5.25: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres@3.4.8: {} + + process-warning@5.0.0: {} + + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.1 + tdigest: 0.1.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + quick-format-unescaped@4.0.4: {} + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-reconciler@0.33.0(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react@19.2.5: {} + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + require-directory@2.1.1: {} + + rescript-schema@9.5.1: {} + + resolve-pkg-maps@1.0.0: {} + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + rfdc@1.4.1: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + secure-json-parse@4.1.0: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackback@0.0.2: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + std-env@4.2.0: {} + + string-similarity@4.0.4: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@5.0.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tagged-tag@1.0.0: {} + + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + + terminal-size@4.0.1: {} + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.1: {} + + toidentifier@1.0.1: {} + + tslib@2.8.1: + optional: true + + tsx@4.21.0: + dependencies: + esbuild: 0.28.1 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + unpipe@1.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + utils-merge@1.0.1: {} + + vary@1.1.2: {} + + viem@2.55.10(typescript@6.0.3): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@6.0.3) + isows: 1.0.7(ws@8.21.0) + ox: 0.14.33(typescript@6.0.3) + ws: 8.21.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + vite@8.1.5(@types/node@24.12.2)(esbuild@0.28.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.12.2 + esbuild: 0.28.1 + fsevents: 2.3.3 + tsx: 4.21.0 + yaml: 2.8.3 + + vitest@4.1.0(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(vite@8.1.5(@types/node@24.12.2)(esbuild@0.28.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@8.1.5(@types/node@24.12.2)(esbuild@0.28.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.1.5(@types/node@24.12.2)(esbuild@0.28.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 24.12.2 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + widest-line@6.0.0: + dependencies: + string-width: 8.2.2 + + window-size@1.1.1: + dependencies: + define-property: 1.0.0 + is-number: 3.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + ws@8.21.1: {} + + y18n@5.0.8: {} + + yaml@2.8.3: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yoga-layout@3.2.1: {} diff --git a/indexer/pnpm-workspace.yaml b/indexer/pnpm-workspace.yaml new file mode 100644 index 00000000..495e53f1 --- /dev/null +++ b/indexer/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - "." + +allowBuilds: + esbuild: true + +onlyBuiltDependencies: + - esbuild diff --git a/indexer/schema.graphql b/indexer/schema.graphql new file mode 100644 index 00000000..3f8183dd --- /dev/null +++ b/indexer/schema.graphql @@ -0,0 +1,382 @@ +type IndexerState { + id: ID! + schemaVersion: String! + deployment: String! + sourceCommit: String! + configSha256: String! + schemaSha256: String! + handlerSha256: String! + sourceRegistrySha256: String! + eventSetSha256: String! + eventCount: Int! + chainId: Int! + progressBlock: BigInt! @index + progressBlockHash: String! + progressTimestamp: BigInt! + progressTransactionHash: String! + progressOccurrenceId: String! +} + +type ChainEvent { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + contractName: String! @index + eventName: String! @index + model: String! @index + releaseVersion: String! @index + topics: [String!]! + data: String! + decodedPayload: String! + payloadHash: String! +} + +type Launch { + id: ID! + chainId: Int! + model: String! @index + releaseVersion: String! @index + launchHash: String! @index + token: String @index + creator: String @index + quoteAsset: String @index + poolId: String @index + hook: String + rewardVault: String @index + positionRecipient: String + positionTokenId: BigInt + totalSwapFeeBps: Int + buySwapFeeBps: Int + sellSwapFeeBps: Int + rewardConfigurationHash: String + quoteConfigurationHash: String + totalSupply: BigInt + tokenLiquidityAmount: BigInt + lockedTokenDust: BigInt + initialTick: Int + tickLower: Int + tickUpper: Int + lpFeePips: Int + initialBuyQuoteAmount: BigInt + initialBuyTokenAmount: BigInt + initialBuyEthAmount: BigInt + launchOccurrenceId: String + liquidityOccurrenceId: String + initialBuyOccurrenceId: String + custodyOccurrenceId: String + coordinatorOccurrenceId: String + hasLaunchEvent: Boolean! + hasLiquidityEvent: Boolean! + hasInitialBuyEvent: Boolean! + hasCustodyEvent: Boolean! + hasCoordinatorEvent: Boolean! + hasPoolRegistrationEvent: Boolean! + hasPoolFeeDisclosureEvent: Boolean! + hasRewardVaultFactoryEvent: Boolean! + provenanceValid: Boolean! + isComplete: Boolean! + updatedBlock: BigInt! @index +} + +type PoolRelease { + id: ID! + chainId: Int! + launchId: String! + model: String! @index + releaseVersion: String! @index + token: String! @index + quoteAsset: String + hook: String! + rewardVault: String + blockNumber: BigInt! @index +} + +type PoolFeeConfig { + id: ID! + chainId: Int! + poolId: String! @index + token: String @index + creator: String @index + quoteAsset: String @index + rewardVault: String @index + registrar: String + model: String! @index + releaseVersion: String! @index + totalSwapFeeBps: Int + buySwapFeeBps: Int + sellSwapFeeBps: Int + buyCreatorFeeBps: Int + sellCreatorFeeBps: Int + creatorFeeBps: Int + launcherFeeBps: Int + transferTaxBps: Int + lpFeePips: Int + quoteIsCurrency0: Boolean + rewardConfigurationHash: String + quoteConfigurationHash: String + registrationOccurrenceId: String + disclosureOccurrenceId: String + provenanceValid: Boolean! + blockNumber: BigInt! @index +} + +type FeeAccrual { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + poolId: String! @index + swapSender: String! + quoteAsset: String + isBuy: Boolean + appliedTotalSwapFeeBps: Int + grossAmount: BigInt! + creatorFee: BigInt! + launcherFee: BigInt! +} + +type PoolFeeTotals { + id: ID! + chainId: Int! + poolId: String! @index + model: String! @index + releaseVersion: String! @index + grossAmount: BigInt! + creatorFees: BigInt! + launcherFees: BigInt! + swapCount: BigInt! + lastOccurrenceId: String! + blockNumber: BigInt! @index +} + +type RewardVault { + id: ID! + chainId: Int! + vault: String! @index + poolId: String! @index + hook: String! + quoteAsset: String + salt: String + configurationHash: String + model: String! @index + releaseVersion: String! @index + factoryOccurrenceId: String! + downstreamLogicalId: String + receiptLogOrdinal: BigInt + payloadHash: String! + sourceAddress: String! + blockNumber: BigInt! @index + blockHash: String! + transactionHash: String! + blockGlobalLogIndex: BigInt! +} + +type RewardCheckpoint { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + vault: String! @index + poolId: String! @index + configurationEpoch: BigInt! + amount: BigInt! + totalCreatorFeesReceived: BigInt! +} + +type BeneficiaryClaim { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + vault: String! @index + beneficiary: String! @index + payoutAddress: String + quoteAsset: String + amount: BigInt! + beneficiaryTotalClaimed: BigInt! + vaultTotalReceived: BigInt! +} + +type CreatorFeeClaim { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + poolId: String! @index + creator: String + rewardVault: String @index + recipient: String + quoteAsset: String + caller: String! + amount: BigInt! +} + +type LauncherFeeClaim { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + treasury: String! + recipient: String! + quoteAsset: String + caller: String! + amount: BigInt! +} + +type PayoutChange { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + vault: String! @index + poolId: String @index + beneficiary: String @index + allocationIndex: BigInt + previousPayoutAddress: String! + newPayoutAddress: String! + shareBps: Int + configurationEpoch: BigInt + activeConfigurationHash: String + effectiveTotalCreatorFeesReceived: BigInt +} + +type RewardConfigurationChange { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + vault: String! @index + poolId: String! @index + approvalReference: String! + configurationEpoch: BigInt! + previousConfigurationHash: String! + newConfigurationHash: String! + beneficiaries: [String!]! + sharesBps: [Int!]! + effectiveTotalCreatorFeesReceived: BigInt! +} + +type InitialBuyCustody { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + launchHash: String! @index + deployer: String! @index + token: String! @index + custody: String! + mode: Int! + durationDays: Int! + cliffDays: Int! + configurationHash: String! +} + +type VestingWallet { + id: ID! + downstreamLogicalId: String @index + receiptLogOrdinal: BigInt + chainId: Int! + blockNumber: BigInt! @index + blockHash: String! + blockTimestamp: BigInt! + transactionHash: String! @index + transactionIndex: BigInt! + blockGlobalLogIndex: BigInt! + sourceAddress: String! @index + model: String! @index + releaseVersion: String! @index + payloadHash: String! + wallet: String! @index + token: String! @index + beneficiary: String! @index + salt: String! + configurationHash: String! +} diff --git a/indexer/scripts/release-candidate.mjs b/indexer/scripts/release-candidate.mjs new file mode 100644 index 00000000..1a98721b --- /dev/null +++ b/indexer/scripts/release-candidate.mjs @@ -0,0 +1,1126 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { parse } from "yaml"; + +const SCRIPT_PATH = fileURLToPath(import.meta.url); +const INDEXER_ROOT = path.resolve(path.dirname(SCRIPT_PATH), ".."); +const REPOSITORY_ROOT = path.resolve(INDEXER_ROOT, ".."); +const ENVIO_HOST = "indexer.hyperindex.xyz"; +const ENVIO_OWNER = "0xprogrammable"; +const ENVIO_PROJECT = "programmable-indexer"; +const RELEASES = Object.freeze([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +]); +const MODELS = Object.freeze({ + "classic-v2": "classic", + "classic-v3": "classic", + "stock-paired-v1": "stock-paired", + "stock-paired-v2": "stock-paired", + "stock-paired-v3": "stock-paired", +}); +const STOCK_COORDINATOR_SOURCES = Object.freeze({ + "stock-paired-v1": "0xfa5f17389ca28d071781d59750b32c842ab6a54b", + "stock-paired-v2": "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + "stock-paired-v3": "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", +}); +const EXPECTED_CONTRACTS = Object.freeze([ + "ClassicV2Hook", + "ClassicV2Launcher", + "ClassicV3Hook", + "ClassicV3Launcher", + "ClassicV3RewardVault", + "ClassicV3RewardVaultFactory", + "ClassicV3VestingWalletFactory", + "StockV1EthCoordinator", + "StockV1Hook", + "StockV1Launcher", + "StockV1RewardVault", + "StockV1RewardVaultFactory", + "StockV2EthCoordinator", + "StockV2Launcher", + "StockV2V3Hook", + "StockV2V3RewardVault", + "StockV2V3RewardVaultFactory", + "StockV3EthCoordinator", + "StockV3Launcher", +].sort(compareUtf8)); + +const ARTIFACTS = Object.freeze({ + configSha256: "config.yaml", + schemaSha256: "schema.graphql", + handlerSha256: "src/EventHandlers.ts", + sourceRegistrySha256: "src/lib/release-map.ts", +}); +const IDENTITY_KEYS = Object.freeze([ + "deployment", + "sourceCommit", + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "eventCount", +]); +const LAUNCH_FIELDS = Object.freeze([ + "id", + "chainId", + "model", + "releaseVersion", + "launchHash", + "token", + "creator", + "quoteAsset", + "poolId", + "hook", + "rewardVault", + "positionRecipient", + "positionTokenId", + "totalSwapFeeBps", + "buySwapFeeBps", + "sellSwapFeeBps", + "rewardConfigurationHash", + "quoteConfigurationHash", + "totalSupply", + "tokenLiquidityAmount", + "lockedTokenDust", + "initialTick", + "tickLower", + "tickUpper", + "lpFeePips", + "initialBuyQuoteAmount", + "initialBuyTokenAmount", + "initialBuyEthAmount", + "launchOccurrenceId", + "liquidityOccurrenceId", + "initialBuyOccurrenceId", + "custodyOccurrenceId", + "coordinatorOccurrenceId", + "hasLaunchEvent", + "hasLiquidityEvent", + "hasInitialBuyEvent", + "hasCustodyEvent", + "hasCoordinatorEvent", + "hasPoolRegistrationEvent", + "hasPoolFeeDisclosureEvent", + "hasRewardVaultFactoryEvent", + "provenanceValid", + "isComplete", + "updatedBlock", +]); +const STABLE_LAUNCH_FIELDS = Object.freeze( + LAUNCH_FIELDS.filter( + (field) => !["provenanceValid", "isComplete", "updatedBlock"].includes(field), + ), +); +const BOOLEAN_LAUNCH_FIELDS = Object.freeze([ + "hasLaunchEvent", + "hasLiquidityEvent", + "hasInitialBuyEvent", + "hasCustodyEvent", + "hasCoordinatorEvent", + "hasPoolRegistrationEvent", + "hasPoolFeeDisclosureEvent", + "hasRewardVaultFactoryEvent", + "provenanceValid", + "isComplete", +]); +const ADDRESS_LAUNCH_FIELDS = Object.freeze([ + "token", + "creator", + "quoteAsset", + "hook", + "rewardVault", + "positionRecipient", +]); +const BYTES32_LAUNCH_FIELDS = Object.freeze([ + "launchHash", + "poolId", + "rewardConfigurationHash", + "quoteConfigurationHash", +]); +const UINT_LAUNCH_FIELDS = Object.freeze([ + "positionTokenId", + "totalSupply", + "tokenLiquidityAmount", + "lockedTokenDust", + "initialBuyQuoteAmount", + "initialBuyTokenAmount", + "initialBuyEthAmount", +]); +const INT_LAUNCH_FIELDS = Object.freeze([ + "totalSwapFeeBps", + "buySwapFeeBps", + "sellSwapFeeBps", + "initialTick", + "tickLower", + "tickUpper", + "lpFeePips", +]); +const OCCURRENCE_LAUNCH_FIELDS = Object.freeze([ + "launchOccurrenceId", + "liquidityOccurrenceId", + "initialBuyOccurrenceId", + "custodyOccurrenceId", + "coordinatorOccurrenceId", +]); + +function compareUtf8(left, right) { + return Buffer.from(left, "utf8").compare(Buffer.from(right, "utf8")); +} + +function sha256(value) { + return `0x${createHash("sha256").update(value).digest("hex")}`; +} + +function exactObject(value, label, keys) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${label} must be an object`); + } + const actual = Object.keys(value).sort(compareUtf8); + const expected = [...keys].sort(compareUtf8); + if ( + actual.length !== expected.length || + actual.some((key, index) => key !== expected[index]) + ) { + throw new Error(`${label} must contain exactly: ${expected.join(", ")}`); + } + return value; +} + +function exactString(value, label, pattern, maximum = 512) { + if ( + typeof value !== "string" || + value.length === 0 || + value.length > maximum || + !pattern.test(value) + ) { + throw new Error(`${label} is invalid`); + } + return value; +} + +function exactCommit(value, label = "source commit") { + return exactString( + value, + label, + /^(?!0{40}$)[0-9a-f]{40}$/u, + 40, + ); +} + +function exactSha256(value, label) { + return exactString(value, label, /^0x[0-9a-f]{64}$/u, 66); +} + +function exactAddress(value, label) { + return exactString(value, label, /^0x[0-9a-f]{40}$/u, 42); +} + +function exactBytes32(value, label) { + return exactString(value, label, /^0x[0-9a-f]{64}$/u, 66); +} + +function exactNullable(value, parser) { + return value === null ? null : parser(value); +} + +function exactSafeInteger(value, label, minimum = Number.MIN_SAFE_INTEGER) { + if (!Number.isSafeInteger(value) || value < minimum) { + throw new Error(`${label} must be a safe integer`); + } + return value; +} + +function exactUint(value, label) { + if (typeof value === "number") { + return String(exactSafeInteger(value, label, 0)); + } + if (typeof value !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(value)) { + throw new Error(`${label} must be an unsigned decimal integer`); + } + return value; +} + +function exactTimestamp(value, label) { + if (typeof value !== "string") throw new Error(`${label} is invalid`); + const parsed = new Date(value); + if (!Number.isFinite(parsed.valueOf()) || parsed.toISOString() !== value) { + throw new Error(`${label} is invalid`); + } + return value; +} + +function endpointIdFromUrl(value, expectedEndpointId) { + if (typeof value !== "string" || value.length > 256) { + throw new Error("endpoint must be the reviewed Envio GraphQL endpoint"); + } + let parsed; + try { + parsed = new URL(value); + } catch { + throw new Error("endpoint must be the reviewed Envio GraphQL endpoint"); + } + const match = /^\/([a-z0-9]{7,64})\/v1\/graphql$/u.exec(parsed.pathname); + if ( + parsed.protocol !== "https:" || + parsed.hostname !== ENVIO_HOST || + parsed.port !== "" || + parsed.username !== "" || + parsed.password !== "" || + parsed.search !== "" || + parsed.hash !== "" || + parsed.toString() !== value || + match === null || + (expectedEndpointId !== undefined && match[1] !== expectedEndpointId) + ) { + throw new Error("endpoint must be the reviewed Envio GraphQL endpoint"); + } + return match[1]; +} + +function exactEndpointId(value) { + return exactString(value, "Envio endpoint id", /^[a-z0-9]{7,64}$/u, 64); +} + +function exactNames(label, names) { + const sorted = [...names].sort(compareUtf8); + if ( + sorted.length !== EXPECTED_CONTRACTS.length || + new Set(sorted).size !== sorted.length || + sorted.some((name, index) => name !== EXPECTED_CONTRACTS[index]) + ) { + throw new Error(`${label} must be the exact reviewed 19-contract scope`); + } +} + +function reviewedArtifactBytes(sourceCommit, relativePath) { + try { + return execFileSync( + "git", + ["show", `${sourceCommit}:indexer/${relativePath}`], + { cwd: REPOSITORY_ROOT, encoding: "buffer", maxBuffer: 32 * 1024 * 1024 }, + ); + } catch { + throw new Error(`reviewed source commit does not contain indexer/${relativePath}`); + } +} + +function localArtifactBytes(sourceCommit, relativePath) { + const local = readFileSync(path.join(INDEXER_ROOT, relativePath)); + const reviewed = reviewedArtifactBytes(sourceCommit, relativePath); + if (!local.equals(reviewed)) { + throw new Error(`local indexer/${relativePath} differs from reviewed source commit`); + } + return local; +} + +function localIdentity(sourceCommitInput) { + const sourceCommit = exactCommit(sourceCommitInput); + const configBytes = localArtifactBytes(sourceCommit, ARTIFACTS.configSha256); + const config = parse(configBytes.toString("utf8")); + if (!Array.isArray(config?.contracts) || !Array.isArray(config?.chains)) { + throw new Error("config.yaml is missing the reviewed contract registry"); + } + if ( + config.chains.length !== 1 || + config.chains[0]?.id !== 1 || + config.chains[0]?.block_lag !== 12 || + config.chains[0]?.max_reorg_depth !== 200 + ) { + throw new Error("config.yaml must retain the reviewed Ethereum finality policy"); + } + exactNames("ABI registry", config.contracts.map((entry) => entry?.name)); + exactNames("chain registry", config.chains[0].contracts?.map((entry) => entry?.name) ?? []); + + const events = []; + for (const contract of config.contracts) { + if (!Array.isArray(contract.events) || contract.events.length === 0) { + throw new Error(`${contract.name} has no events`); + } + const local = new Set(); + for (const declaration of contract.events) { + const event = declaration?.event; + if (typeof event !== "string" || event.length === 0 || event.trim() !== event) { + throw new Error(`${contract.name} has an invalid event signature`); + } + if (local.has(event)) throw new Error(`${contract.name} repeats an event signature`); + local.add(event); + events.push(event); + } + } + const eventSet = Buffer.from(`${events.sort(compareUtf8).join("\n")}\n`, "utf8"); + return Object.freeze({ + deployment: `production-${sourceCommit.slice(0, 7)}`, + sourceCommit, + configSha256: sha256(configBytes), + schemaSha256: sha256(localArtifactBytes(sourceCommit, ARTIFACTS.schemaSha256)), + handlerSha256: sha256(localArtifactBytes(sourceCommit, ARTIFACTS.handlerSha256)), + sourceRegistrySha256: sha256( + localArtifactBytes(sourceCommit, ARTIFACTS.sourceRegistrySha256), + ), + eventSetSha256: sha256(eventSet), + eventCount: events.length, + }); +} + +function parseCandidateIdentity(value, label = "candidate identity") { + const object = exactObject(value, label, IDENTITY_KEYS); + return { + deployment: exactString( + object.deployment, + `${label}.deployment`, + /^[a-z0-9][a-z0-9._-]{0,127}$/u, + 128, + ), + sourceCommit: exactCommit(object.sourceCommit, `${label}.sourceCommit`), + configSha256: exactSha256(object.configSha256, `${label}.configSha256`), + schemaSha256: exactSha256(object.schemaSha256, `${label}.schemaSha256`), + handlerSha256: exactSha256(object.handlerSha256, `${label}.handlerSha256`), + sourceRegistrySha256: exactSha256( + object.sourceRegistrySha256, + `${label}.sourceRegistrySha256`, + ), + eventSetSha256: exactSha256(object.eventSetSha256, `${label}.eventSetSha256`), + eventCount: exactSafeInteger(object.eventCount, `${label}.eventCount`, 1), + }; +} + +function assertSameIdentity(actual, expected, label) { + for (const key of IDENTITY_KEYS) { + if (actual[key] !== expected[key]) { + throw new Error(`${label} identity mismatch at ${key}`); + } + } +} + +function parseArgs(argv) { + const [command, ...rest] = argv; + if (rest.length % 2 !== 0) throw new Error("every option requires one value"); + const values = {}; + for (let index = 0; index < rest.length; index += 2) { + const key = rest[index]; + const value = rest[index + 1]; + if (!/^--[a-z][a-z-]*$/u.test(key ?? "") || value === undefined) { + throw new Error(`invalid argument ${key ?? ""}`.trim()); + } + const name = key.slice(2); + if (Object.hasOwn(values, name)) throw new Error(`duplicate argument --${name}`); + values[name] = value; + } + return { command, values }; +} + +function exactCommandArgs(command, values, allowed, required = allowed) { + for (const key of Object.keys(values)) { + if (!allowed.includes(key)) throw new Error(`unexpected argument --${key} for ${command}`); + } + for (const key of required) { + if (!Object.hasOwn(values, key)) throw new Error(`--${key} is required`); + } +} + +async function graphql(endpoint, query, variables = {}, fetcher = fetch) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 30_000); + try { + const response = await fetcher(endpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ query, variables }), + redirect: "error", + signal: controller.signal, + }); + if (!response.ok) throw new Error(`GraphQL returned HTTP ${response.status}`); + const value = await response.json(); + if (Array.isArray(value.errors) && value.errors.length > 0) { + throw new Error(`GraphQL rejected the release audit: ${value.errors[0]?.message}`); + } + if (value === null || typeof value !== "object" || value.data === undefined) { + throw new Error("GraphQL returned an invalid response"); + } + return value.data; + } finally { + clearTimeout(timer); + } +} + +const PROGRESS_QUERY = ` + query ProgrammableReleaseCandidateProgress { + _meta(where: { chainId: { _eq: 1 } }) { + chainId progressBlock bufferBlock sourceBlock isReady eventsProcessed + } + IndexerState_by_pk(id: "ethereum-mainnet") { + id schemaVersion deployment sourceCommit configSha256 schemaSha256 + handlerSha256 sourceRegistrySha256 eventSetSha256 eventCount chainId + progressBlock progressBlockHash progressTimestamp progressTransactionHash + progressOccurrenceId + } + } +`; + +const BASELINE_PROGRESS_QUERY = ` + query ProgrammableReleaseBaselineProgress { + _meta(where: { chainId: { _eq: 1 } }) { + chainId progressBlock bufferBlock sourceBlock isReady eventsProcessed + } + IndexerState_by_pk(id: "ethereum-mainnet") { + id schemaVersion deployment chainId progressBlock progressBlockHash + progressTimestamp progressTransactionHash progressOccurrenceId + } + } +`; + +const INVENTORY_QUERY = ` + query ProgrammableReleaseInventory( + $afterId: String! + $anchorBlock: numeric! + $first: Int! + ) { + Launch( + where: { + _and: [ + { id: { _gt: $afterId } } + { updatedBlock: { _lte: $anchorBlock } } + ] + } + order_by: [{ id: asc }] + limit: $first + ) { + id chainId model releaseVersion launchHash token creator quoteAsset poolId hook + rewardVault positionRecipient positionTokenId totalSwapFeeBps buySwapFeeBps + sellSwapFeeBps rewardConfigurationHash quoteConfigurationHash totalSupply + tokenLiquidityAmount lockedTokenDust initialTick tickLower tickUpper lpFeePips + initialBuyQuoteAmount initialBuyTokenAmount initialBuyEthAmount + launchOccurrenceId liquidityOccurrenceId initialBuyOccurrenceId + custodyOccurrenceId coordinatorOccurrenceId hasLaunchEvent hasLiquidityEvent + hasInitialBuyEvent hasCustodyEvent hasCoordinatorEvent + hasPoolRegistrationEvent hasPoolFeeDisclosureEvent + hasRewardVaultFactoryEvent provenanceValid isComplete updatedBlock + } + } +`; + +function stableLaunch(value, label = "launch") { + const object = exactObject(value, label, LAUNCH_FIELDS); + const canonical = {}; + for (const field of LAUNCH_FIELDS) { + const fieldLabel = `${label}.${field}`; + if (field === "id") { + canonical[field] = exactString(object[field], fieldLabel, /^[\x21-\x7e]+$/u, 512); + } else if (field === "chainId") { + if (object[field] !== 1) throw new Error(`${fieldLabel} must be Ethereum Mainnet`); + canonical[field] = 1; + } else if (field === "model") { + canonical[field] = exactString(object[field], fieldLabel, /^(?:classic|stock-paired)$/u, 32); + } else if (field === "releaseVersion") { + canonical[field] = exactString(object[field], fieldLabel, /^[a-z0-9-]+$/u, 64); + } else if (ADDRESS_LAUNCH_FIELDS.includes(field)) { + canonical[field] = exactNullable(object[field], (entry) => exactAddress(entry, fieldLabel)); + } else if (BYTES32_LAUNCH_FIELDS.includes(field)) { + canonical[field] = exactNullable(object[field], (entry) => exactBytes32(entry, fieldLabel)); + } else if (UINT_LAUNCH_FIELDS.includes(field)) { + canonical[field] = exactNullable(object[field], (entry) => exactUint(entry, fieldLabel)); + } else if (INT_LAUNCH_FIELDS.includes(field)) { + canonical[field] = exactNullable(object[field], (entry) => + exactSafeInteger(entry, fieldLabel), + ); + } else if (OCCURRENCE_LAUNCH_FIELDS.includes(field)) { + canonical[field] = exactNullable(object[field], (entry) => + exactString(entry, fieldLabel, /^[\x21-\x7e]+$/u, 512), + ); + } else if (BOOLEAN_LAUNCH_FIELDS.includes(field)) { + if (typeof object[field] !== "boolean") throw new Error(`${fieldLabel} must be boolean`); + canonical[field] = object[field]; + } else if (field === "updatedBlock") { + canonical[field] = exactUint(object[field], fieldLabel); + } else { + throw new Error(`unhandled stable launch field ${field}`); + } + } + return canonical; +} + +function assertEligibleLaunch(row) { + if (!RELEASES.includes(row.releaseVersion) || row.model !== MODELS[row.releaseVersion]) { + throw new Error(`unsupported release in inventory: ${row.releaseVersion}`); + } + if (row.provenanceValid !== true || row.isComplete !== true) { + throw new Error(`incomplete or invalid launch ${row.id}`); + } + if ( + !row.token || + !row.launchOccurrenceId || + !row.liquidityOccurrenceId || + !row.initialBuyOccurrenceId + ) { + throw new Error(`launch ${row.id} is missing required identity evidence`); + } +} + +function assertSupportedLaunch(row) { + if (!RELEASES.includes(row.releaseVersion) || row.model !== MODELS[row.releaseVersion]) { + throw new Error(`unsupported release in inventory: ${row.releaseVersion}`); + } +} + +function canonicalRows(launches) { + return Buffer.from(`${launches.map((row) => JSON.stringify(row)).join("\n")}\n`, "utf8"); +} + +function inventoryEvidence(launches) { + const perRelease = Object.fromEntries(RELEASES.map((release) => [release, 0])); + for (const row of launches) perRelease[row.releaseVersion] += 1; + return { + count: launches.length, + perRelease, + sha256: sha256(canonicalRows(launches)), + }; +} + +function exactAnchor(data, label) { + if (!Array.isArray(data?._meta) || data._meta.length !== 1) { + throw new Error(`${label} returned an invalid Ethereum _meta row`); + } + const meta = data._meta[0]; + const state = data.IndexerState_by_pk; + if ( + state === null || + typeof state !== "object" || + meta.chainId !== 1 || + meta.isReady !== true || + state.id !== "ethereum-mainnet" || + state.schemaVersion !== "1" || + state.chainId !== 1 + ) { + throw new Error(`${label} is not a ready v1 Ethereum Mainnet deployment`); + } + const anchor = { + progressBlock: exactUint(meta.progressBlock, `${label}.progressBlock`), + bufferBlock: exactUint(meta.bufferBlock, `${label}.bufferBlock`), + sourceBlock: exactUint(meta.sourceBlock, `${label}.sourceBlock`), + eventsProcessed: exactUint(meta.eventsProcessed, `${label}.eventsProcessed`), + stateProgressBlock: exactUint(state.progressBlock, `${label}.stateProgressBlock`), + stateProgressBlockHash: exactBytes32( + state.progressBlockHash, + `${label}.stateProgressBlockHash`, + ), + stateProgressTimestamp: exactUint( + state.progressTimestamp, + `${label}.stateProgressTimestamp`, + ), + stateProgressTransactionHash: exactBytes32( + state.progressTransactionHash, + `${label}.stateProgressTransactionHash`, + ), + stateProgressOccurrenceId: exactString( + state.progressOccurrenceId, + `${label}.stateProgressOccurrenceId`, + /^[\x21-\x7e]+$/u, + 512, + ), + }; + if ( + BigInt(anchor.progressBlock) !== BigInt(anchor.bufferBlock) || + BigInt(anchor.sourceBlock) < BigInt(anchor.progressBlock) || + BigInt(anchor.stateProgressBlock) > BigInt(anchor.progressBlock) + ) { + throw new Error(`${label} does not expose a stable processed checkpoint`); + } + return anchor; +} + +async function readInventory(endpoint, anchorBlock, fetcher = fetch) { + const launches = []; + let afterId = ""; + for (;;) { + const page = await graphql( + endpoint, + INVENTORY_QUERY, + { afterId, anchorBlock, first: 250 }, + fetcher, + ); + const rows = page?.Launch; + if (!Array.isArray(rows)) throw new Error("inventory page is invalid"); + if (rows.length > 250) throw new Error("inventory page exceeds the requested bound"); + if (rows.length === 0) break; + for (const value of rows) { + const row = stableLaunch(value); + if (row.id <= afterId) throw new Error("inventory is not strictly ordered"); + if (BigInt(row.updatedBlock) > BigInt(anchorBlock)) { + throw new Error(`launch ${row.id} exceeds the frozen inventory anchor`); + } + launches.push(row); + afterId = row.id; + } + if (rows.length < 250) break; + } + return launches; +} + +async function readFrozenInventory(endpoint, progressQuery, fetcher = fetch) { + const progress = await graphql(endpoint, progressQuery, {}, fetcher); + const anchor = exactAnchor(progress, "deployment"); + const first = await readInventory(endpoint, anchor.progressBlock, fetcher); + const second = await readInventory(endpoint, anchor.progressBlock, fetcher); + if (sha256(canonicalRows(first)) !== sha256(canonicalRows(second))) { + throw new Error("inventory changed while reading the frozen checkpoint"); + } + const afterProgress = await graphql(endpoint, progressQuery, {}, fetcher); + const after = exactAnchor( + afterProgress, + "deployment after inventory", + ); + if (BigInt(after.progressBlock) < BigInt(anchor.progressBlock)) { + throw new Error("deployment checkpoint regressed during inventory capture"); + } + return { progress, afterProgress, anchor, launches: first }; +} + +function withDigest(kind, payload) { + const digest = sha256( + Buffer.from(`programmable:${kind}:v2\n${JSON.stringify(payload)}\n`, "utf8"), + ); + return { ...payload, digest }; +} + +function baselineDeployment(endpoint, progress) { + const label = exactString( + progress.IndexerState_by_pk?.deployment, + "baseline deployment label", + /^[a-z0-9][a-z0-9._-]{0,127}$/u, + 128, + ); + return { + provider: "envio-cloud", + host: ENVIO_HOST, + endpointId: endpointIdFromUrl(endpoint), + deploymentLabel: label, + chainId: 1, + }; +} + +async function snapshotBaseline(endpointInput, fetcher = fetch, now = () => new Date()) { + const endpoint = exactString( + endpointInput, + "endpoint", + /^https:\/\//u, + 256, + ); + endpointIdFromUrl(endpoint); + const { progress, anchor, launches } = await readFrozenInventory( + endpoint, + BASELINE_PROGRESS_QUERY, + fetcher, + ); + if (launches.length === 0) throw new Error("baseline inventory is empty"); + for (const row of launches) assertSupportedLaunch(row); + const inventory = inventoryEvidence(launches); + for (const release of RELEASES) { + if (inventory.perRelease[release] === 0) { + throw new Error("baseline does not contain every reviewed historical release"); + } + } + return withDigest("envio-launch-inventory-baseline", { + schemaVersion: 2, + kind: "envio-launch-inventory-baseline", + endpoint, + capturedAt: now().toISOString(), + deployment: baselineDeployment(endpoint, progress), + anchor, + inventory, + entries: launches, + }); +} + +function parseInventory(value, label) { + const object = exactObject(value, label, ["count", "perRelease", "sha256"]); + const perRelease = exactObject(object.perRelease, `${label}.perRelease`, RELEASES); + const canonicalPerRelease = {}; + let total = 0; + for (const release of RELEASES) { + const count = exactSafeInteger(perRelease[release], `${label}.perRelease.${release}`, 0); + canonicalPerRelease[release] = count; + total += count; + } + const count = exactSafeInteger(object.count, `${label}.count`, 1); + if (count !== total) throw new Error(`${label}.count does not match release counts`); + return { + count, + perRelease: canonicalPerRelease, + sha256: exactSha256(object.sha256, `${label}.sha256`), + }; +} + +function parseAnchor(value, label) { + const keys = [ + "progressBlock", + "bufferBlock", + "sourceBlock", + "eventsProcessed", + "stateProgressBlock", + "stateProgressBlockHash", + "stateProgressTimestamp", + "stateProgressTransactionHash", + "stateProgressOccurrenceId", + ]; + const object = exactObject(value, label, keys); + const anchor = { + progressBlock: exactUint(object.progressBlock, `${label}.progressBlock`), + bufferBlock: exactUint(object.bufferBlock, `${label}.bufferBlock`), + sourceBlock: exactUint(object.sourceBlock, `${label}.sourceBlock`), + eventsProcessed: exactUint(object.eventsProcessed, `${label}.eventsProcessed`), + stateProgressBlock: exactUint(object.stateProgressBlock, `${label}.stateProgressBlock`), + stateProgressBlockHash: exactBytes32( + object.stateProgressBlockHash, + `${label}.stateProgressBlockHash`, + ), + stateProgressTimestamp: exactUint( + object.stateProgressTimestamp, + `${label}.stateProgressTimestamp`, + ), + stateProgressTransactionHash: exactBytes32( + object.stateProgressTransactionHash, + `${label}.stateProgressTransactionHash`, + ), + stateProgressOccurrenceId: exactString( + object.stateProgressOccurrenceId, + `${label}.stateProgressOccurrenceId`, + /^[\x21-\x7e]+$/u, + 512, + ), + }; + if ( + anchor.progressBlock !== anchor.bufferBlock || + BigInt(anchor.sourceBlock) < BigInt(anchor.progressBlock) || + BigInt(anchor.stateProgressBlock) > BigInt(anchor.progressBlock) + ) { + throw new Error(`${label} is not a stable processed checkpoint`); + } + return anchor; +} + +function parseBaseline(value) { + const keys = [ + "schemaVersion", + "kind", + "endpoint", + "capturedAt", + "deployment", + "anchor", + "inventory", + "entries", + "digest", + ]; + const object = exactObject(value, "baseline", keys); + if (object.schemaVersion !== 2 || object.kind !== "envio-launch-inventory-baseline") { + throw new Error("--baseline must be a v2 launch inventory baseline"); + } + const endpoint = exactString(object.endpoint, "baseline.endpoint", /^https:\/\//u, 256); + const endpointId = endpointIdFromUrl(endpoint); + const deploymentObject = exactObject(object.deployment, "baseline.deployment", [ + "provider", + "host", + "endpointId", + "deploymentLabel", + "chainId", + ]); + const deployment = { + provider: deploymentObject.provider, + host: deploymentObject.host, + endpointId: deploymentObject.endpointId, + deploymentLabel: exactString( + deploymentObject.deploymentLabel, + "baseline.deployment.deploymentLabel", + /^[a-z0-9][a-z0-9._-]{0,127}$/u, + 128, + ), + chainId: deploymentObject.chainId, + }; + if ( + deployment.provider !== "envio-cloud" || + deployment.host !== ENVIO_HOST || + deployment.endpointId !== endpointId || + deployment.chainId !== 1 + ) { + throw new Error("baseline deployment does not match its Envio endpoint"); + } + const anchor = parseAnchor(object.anchor, "baseline.anchor"); + if (!Array.isArray(object.entries) || object.entries.length === 0) { + throw new Error("baseline.entries must be non-empty"); + } + const entries = object.entries.map((entry, index) => + stableLaunch(entry, `baseline.entries[${index}]`), + ); + for (let index = 0; index < entries.length; index += 1) { + const row = entries[index]; + assertSupportedLaunch(row); + if (index > 0 && entries[index - 1].id >= row.id) { + throw new Error("baseline entries are not strictly ordered"); + } + if (BigInt(row.updatedBlock) > BigInt(anchor.progressBlock)) { + throw new Error(`baseline launch ${row.id} exceeds its anchor`); + } + } + const inventory = parseInventory(object.inventory, "baseline.inventory"); + const computedInventory = inventoryEvidence(entries); + if (JSON.stringify(inventory) !== JSON.stringify(computedInventory)) { + throw new Error("baseline inventory count or digest does not match its entries"); + } + const canonical = { + schemaVersion: 2, + kind: "envio-launch-inventory-baseline", + endpoint, + capturedAt: exactTimestamp(object.capturedAt, "baseline.capturedAt"), + deployment, + anchor, + inventory, + entries, + }; + const expectedDigest = withDigest("envio-launch-inventory-baseline", canonical).digest; + if (object.digest !== expectedDigest) throw new Error("baseline digest mismatch"); + return { ...canonical, digest: expectedDigest }; +} + +function exactCoordinatorCreatorRepair(expected, actual, changedFields) { + const coordinatorSource = STOCK_COORDINATOR_SOURCES[expected.releaseVersion]; + if ( + coordinatorSource === undefined || + changedFields.length !== 1 || + changedFields[0] !== "creator" || + expected.provenanceValid !== false || + expected.isComplete !== false || + expected.creator !== coordinatorSource || + expected.hasLaunchEvent !== true || + expected.hasCoordinatorEvent !== true || + expected.launchOccurrenceId === null || + expected.coordinatorOccurrenceId === null || + actual.provenanceValid !== true || + actual.isComplete !== true || + actual.creator === null || + actual.creator === coordinatorSource || + actual.launchOccurrenceId !== expected.launchOccurrenceId || + actual.coordinatorOccurrenceId !== expected.coordinatorOccurrenceId + ) { + return undefined; + } + return { + id: expected.id, + releaseVersion: expected.releaseVersion, + priorCoordinatorSource: coordinatorSource, + authenticatedCreator: actual.creator, + launchOccurrenceId: actual.launchOccurrenceId, + coordinatorOccurrenceId: actual.coordinatorOccurrenceId, + }; +} + +function assertFrozenBaseline(launches, baseline) { + const current = new Map(launches.map((row) => [row.id, row])); + const repairs = []; + for (const expected of baseline.entries) { + const actual = current.get(expected.id); + if (actual === undefined) throw new Error(`candidate omitted frozen launch ${expected.id}`); + const changedFields = STABLE_LAUNCH_FIELDS.filter( + (field) => actual[field] !== expected[field], + ); + if (changedFields.length === 0) continue; + const repair = exactCoordinatorCreatorRepair(expected, actual, changedFields); + if (repair !== undefined) { + repairs.push(repair); + continue; + } + throw new Error(`candidate changed frozen launch ${expected.id} at ${changedFields[0]}`); + } + return repairs; +} + +function candidateDeployment(values, endpoint, expectedIdentity) { + const endpointId = exactEndpointId(values["deployment-endpoint-id"]); + endpointIdFromUrl(endpoint, endpointId); + const deploymentLabel = exactString( + values["deployment-label"], + "deployment label", + /^[a-z0-9][a-z0-9._-]{0,127}$/u, + 128, + ); + if (deploymentLabel !== expectedIdentity.deployment) { + throw new Error("control-plane deployment label does not match candidate identity"); + } + return { + provider: "envio-cloud", + owner: ENVIO_OWNER, + project: ENVIO_PROJECT, + mirrorCommit: exactCommit(values["mirror-commit"], "mirror commit"), + deploymentLabel, + endpointId, + }; +} + +function identityFromState(state) { + if ( + state === null || + typeof state !== "object" || + state.id !== "ethereum-mainnet" || + state.schemaVersion !== "1" || + state.chainId !== 1 + ) { + throw new Error("candidate IndexerState is not the v1 Ethereum singleton"); + } + const identity = {}; + for (const key of IDENTITY_KEYS) identity[key] = state[key]; + return parseCandidateIdentity(identity, "candidate IndexerState"); +} + +async function auditCandidate({ + endpoint: endpointInput, + expectedIdentity: identityInput, + baseline: baselineInput, + sourceCommit: sourceCommitInput, + deployment, + fetcher = fetch, + now = () => new Date(), +}) { + const sourceCommit = exactCommit(sourceCommitInput); + const expectedFileIdentity = parseCandidateIdentity(identityInput); + if (expectedFileIdentity.sourceCommit !== sourceCommit) { + throw new Error("candidate identity sourceCommit does not match reviewed input"); + } + const expectedIdentity = localIdentity(sourceCommit); + assertSameIdentity(expectedFileIdentity, expectedIdentity, "reviewed checkout"); + const endpoint = exactString(endpointInput, "endpoint", /^https:\/\//u, 256); + const controlPlane = candidateDeployment(deployment, endpoint, expectedIdentity); + const baseline = parseBaseline(baselineInput); + const { progress, afterProgress, anchor, launches } = await readFrozenInventory( + endpoint, + PROGRESS_QUERY, + fetcher, + ); + const runtimeIdentity = identityFromState(progress.IndexerState_by_pk); + assertSameIdentity(runtimeIdentity, expectedIdentity, "candidate IndexerState"); + const finalRuntimeIdentity = identityFromState(afterProgress.IndexerState_by_pk); + assertSameIdentity( + finalRuntimeIdentity, + expectedIdentity, + "candidate IndexerState after inventory", + ); + if (runtimeIdentity.deployment !== controlPlane.deploymentLabel) { + throw new Error("candidate runtime does not corroborate the control-plane deployment"); + } + if (BigInt(anchor.progressBlock) < BigInt(baseline.anchor.progressBlock)) { + throw new Error("candidate has not reached the frozen baseline checkpoint"); + } + if (launches.length === 0) throw new Error("candidate inventory is empty"); + const ids = new Set(); + const tokens = new Set(); + const launchHashes = new Set(); + for (const row of launches) { + assertEligibleLaunch(row); + if (ids.has(row.id) || tokens.has(row.token) || launchHashes.has(row.launchHash)) { + throw new Error(`duplicate launch identity in candidate inventory: ${row.id}`); + } + ids.add(row.id); + tokens.add(row.token); + launchHashes.add(row.launchHash); + } + const inventory = inventoryEvidence(launches); + for (const release of RELEASES) { + if (inventory.perRelease[release] === 0) { + throw new Error(`candidate has no ${release} launches`); + } + } + const authenticatedCoordinatorCreatorRepairs = assertFrozenBaseline(launches, baseline); + return withDigest("envio-release-inventory", { + schemaVersion: 2, + kind: "envio-release-inventory", + endpoint, + capturedAt: now().toISOString(), + deployment: controlPlane, + identity: expectedIdentity, + baseline: { + digest: baseline.digest, + deployment: baseline.deployment, + anchor: baseline.anchor, + inventory: baseline.inventory, + }, + anchor, + inventory, + authenticatedCoordinatorCreatorRepairs, + }); +} + +async function main(argv = process.argv.slice(2)) { + const { command, values } = parseArgs(argv); + if (command === "identity") { + exactCommandArgs(command, values, ["source-commit"]); + const identity = localIdentity(values["source-commit"]); + process.stdout.write(`${JSON.stringify(identity, null, 2)}\n`); + return; + } + if (command === "snapshot") { + exactCommandArgs(command, values, ["endpoint", "output"]); + const baseline = await snapshotBaseline(values.endpoint); + writeFileSync(path.resolve(values.output), `${JSON.stringify(baseline, null, 2)}\n`, { + flag: "wx", + }); + return; + } + if (command === "audit") { + const required = [ + "endpoint", + "deployment-endpoint-id", + "deployment-label", + "mirror-commit", + "source-commit", + "identity", + "baseline", + ]; + exactCommandArgs(command, values, [...required, "output"], required); + const expectedIdentity = JSON.parse(readFileSync(path.resolve(values.identity), "utf8")); + const baseline = JSON.parse(readFileSync(path.resolve(values.baseline), "utf8")); + const evidence = await auditCandidate({ + endpoint: values.endpoint, + expectedIdentity, + baseline, + sourceCommit: values["source-commit"], + deployment: values, + }); + const output = `${JSON.stringify(evidence, null, 2)}\n`; + if (values.output) writeFileSync(path.resolve(values.output), output, { flag: "wx" }); + else process.stdout.write(output); + return; + } + throw new Error("usage: release-candidate.mjs identity|snapshot|audit [...options]"); +} + +if (path.resolve(process.argv[1] ?? "") === SCRIPT_PATH) { + main().catch((error) => { + process.stderr.write( + `Envio release candidate: ${error instanceof Error ? error.message : String(error)}\n`, + ); + process.exitCode = 1; + }); +} + +export { + IDENTITY_KEYS, + INVENTORY_QUERY, + LAUNCH_FIELDS, + STABLE_LAUNCH_FIELDS, + assertFrozenBaseline, + auditCandidate, + endpointIdFromUrl, + localIdentity, + parseBaseline, + parseCandidateIdentity, + snapshotBaseline, + stableLaunch, +}; diff --git a/indexer/src/EventHandlers.ts b/indexer/src/EventHandlers.ts new file mode 100644 index 00000000..57302e9e --- /dev/null +++ b/indexer/src/EventHandlers.ts @@ -0,0 +1,2023 @@ +import { + indexer, + type BeneficiaryClaim, + type ChainEvent, + type CreatorFeeClaim, + type EvmEvent, + type EvmOnEventContext, + type FeeAccrual, + type InitialBuyCustody, + type Launch, + type LauncherFeeClaim, + type PayoutChange, + type PoolFeeConfig, + type RewardCheckpoint, + type RewardConfigurationChange, + type RewardVault, + type VestingWallet, +} from "envio"; +import type { AbiEvent } from "viem"; + +import { deploymentIdentityFromEnvironment } from "./lib/deployment-identity.js"; +import { launchEntityId, poolEntityId } from "./lib/ids.js"; +import { + canonicalPayloadJson, + encodeEventPayload, +} from "./lib/payload-hash.js"; +import { + eventProvenance, + lower, + lowerAddress, + type EventProvenance, +} from "./lib/provenance.js"; +import { + resolveRelease, + SOURCE_REGISTRY, + sourceStartBlock, + staticReleaseForContract, + type ReleaseIdentity, +} from "./lib/release-map.js"; + +type Mutable = { -readonly [K in keyof T]: T[K] }; + +type RecordedOccurrence = { + isNew: boolean; + provenance: EventProvenance; + release: ReleaseIdentity; + payloadHash: string; +}; + +const CHAIN_ID = 1; +const INDEXER_STATE_ID = "ethereum-mainnet"; +const SCHEMA_VERSION = "1"; + +const DYNAMIC_VAULT_CONTRACTS = new Set([ + "ClassicV3RewardVault", + "StockV1RewardVault", + "StockV2V3RewardVault", +]); + +const LAUNCH_IDENTITY_FIELDS = new Set([ + "token", + "creator", + "quoteAsset", + "poolId", + "hook", + "rewardVault", + "positionRecipient", + "positionTokenId", + "rewardConfigurationHash", + "quoteConfigurationHash", +]); + +const CLASSIC_V2_HOOK = sourceAddress("ClassicV2Hook"); +const CLASSIC_V3_HOOK = sourceAddress("ClassicV3Hook"); +const STOCK_V1_HOOK = sourceAddress("StockV1Hook"); +const STOCK_V2_V3_HOOK = sourceAddress("StockV2V3Hook"); + +async function handleEvent(args: { + event: EvmEvent; + context: EvmOnEventContext; +}): Promise { + const occurrence = await recordOccurrence(args.event, args.context); + if (!occurrence.isNew) { + return; + } + + const { event, context } = args; + switch (event.contractName) { + case "ClassicV2Launcher": + case "ClassicV3Launcher": + case "StockV1Launcher": + case "StockV2Launcher": + case "StockV3Launcher": + await handleLauncherEvent(event, context, occurrence); + return; + case "StockV1EthCoordinator": + case "StockV2EthCoordinator": + case "StockV3EthCoordinator": + await handleCoordinatorEvent(event, context, occurrence); + return; + case "ClassicV2Hook": + case "ClassicV3Hook": + case "StockV1Hook": + case "StockV2V3Hook": + await handleHookEvent(event, context, occurrence); + return; + case "ClassicV3RewardVaultFactory": + case "StockV1RewardVaultFactory": + case "StockV2V3RewardVaultFactory": + await handleVaultFactoryEvent(event, context, occurrence); + return; + case "ClassicV3VestingWalletFactory": + handleVestingFactoryEvent(event, context, occurrence); + return; + case "ClassicV3RewardVault": + case "StockV1RewardVault": + case "StockV2V3RewardVault": + await handleRewardVaultEvent(event, context, occurrence); + return; + } +} + +async function recordOccurrence( + event: EvmEvent, + context: EvmOnEventContext, +): Promise { + const provenance = eventProvenance(event); + const encoded = encodeEventPayload( + findEventAbi(event), + event.params as unknown as Readonly>, + ); + const topics = encoded.topics.map(lower); + const data = lower(encoded.data); + const decodedPayload = canonicalPayloadJson(event.params); + const payloadHash = lower(encoded.payloadHash); + const candidateRelease = staticReleaseForContract(event.contractName) ?? { + model: "unresolved", + releaseVersion: "unresolved", + }; + const existing = await context.ChainEvent.get(provenance.id); + if (existing !== undefined) { + const sameTopics = + existing.topics.length === topics.length && + existing.topics.every((topic, index) => topic === topics[index]); + const isConflict = + existing.chainId !== provenance.chainId || + existing.blockNumber !== provenance.blockNumber || + existing.blockHash !== provenance.blockHash || + existing.blockTimestamp !== provenance.blockTimestamp || + existing.transactionHash !== provenance.transactionHash || + existing.transactionIndex !== provenance.transactionIndex || + existing.blockGlobalLogIndex !== provenance.blockGlobalLogIndex || + existing.sourceAddress !== provenance.sourceAddress || + existing.contractName !== event.contractName || + existing.eventName !== event.eventName || + existing.model !== candidateRelease.model || + existing.releaseVersion !== candidateRelease.releaseVersion || + !sameTopics || + existing.data !== data || + existing.decodedPayload !== decodedPayload || + existing.payloadHash !== payloadHash; + if (isConflict) { + throw new Error( + `Conflicting duplicate candidate occurrence ${provenance.id}`, + ); + } + return { + isNew: false, + provenance, + release: { + model: existing.model, + releaseVersion: existing.releaseVersion, + }, + payloadHash: existing.payloadHash, + }; + } + + const poolId = eventPoolId(event); + const poolRelation = + poolId === undefined + ? undefined + : await context.PoolRelease.get(poolEntityId(CHAIN_ID, poolId)); + const vaultRelation = DYNAMIC_VAULT_CONTRACTS.has(event.contractName) + ? await context.RewardVault.get(provenance.sourceAddress) + : undefined; + const release = resolveRelease({ + contractName: event.contractName, + poolRelation: + poolRelation === undefined + ? undefined + : { + model: poolRelation.model, + releaseVersion: poolRelation.releaseVersion, + }, + vaultRelation: + vaultRelation === undefined + ? undefined + : { + model: vaultRelation.model, + releaseVersion: vaultRelation.releaseVersion, + }, + }); + const chainEvent: ChainEvent = { + ...provenance, + contractName: event.contractName, + eventName: event.eventName, + model: candidateRelease.model, + releaseVersion: candidateRelease.releaseVersion, + topics, + data, + decodedPayload, + payloadHash, + }; + context.ChainEvent.set(chainEvent); + await updateIndexerState(context, provenance); + + return { + isNew: true, + provenance, + release, + payloadHash: chainEvent.payloadHash, + }; +} + +async function updateIndexerState( + context: EvmOnEventContext, + provenance: EventProvenance, +): Promise { + const deploymentIdentity = deploymentIdentityFromEnvironment(); + const current = await context.IndexerState.get(INDEXER_STATE_ID); + const currentOccurrence = + current === undefined + ? undefined + : await context.ChainEvent.get(current.progressOccurrenceId); + if ( + current !== undefined && + (currentOccurrence !== undefined + ? comparePlacement(currentOccurrence, provenance) >= 0 + : current.progressBlock > provenance.blockNumber) + ) { + return; + } + context.IndexerState.set({ + id: INDEXER_STATE_ID, + schemaVersion: SCHEMA_VERSION, + ...deploymentIdentity, + chainId: provenance.chainId, + progressBlock: provenance.blockNumber, + progressBlockHash: provenance.blockHash, + progressTimestamp: provenance.blockTimestamp, + progressTransactionHash: provenance.transactionHash, + progressOccurrenceId: provenance.id, + }); +} + +type CandidatePlacement = { + readonly id: string; + readonly blockNumber: bigint; + readonly blockHash: string; + readonly transactionHash: string; + readonly transactionIndex: bigint; + readonly blockGlobalLogIndex: bigint; +}; + +function comparePlacement( + left: CandidatePlacement, + right: CandidatePlacement, +): number { + if (left.blockNumber !== right.blockNumber) { + return left.blockNumber > right.blockNumber ? 1 : -1; + } + if (left.transactionIndex !== right.transactionIndex) { + return left.transactionIndex > right.transactionIndex ? 1 : -1; + } + if (left.blockGlobalLogIndex !== right.blockGlobalLogIndex) { + return left.blockGlobalLogIndex > right.blockGlobalLogIndex ? 1 : -1; + } + const blockOrder = left.blockHash.localeCompare(right.blockHash); + if (blockOrder !== 0) { + return blockOrder; + } + const transactionOrder = left.transactionHash.localeCompare( + right.transactionHash, + ); + return transactionOrder === 0 + ? left.id.localeCompare(right.id) + : transactionOrder; +} + +async function handleLauncherEvent( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV2Launcher" + | "ClassicV3Launcher" + | "StockV1Launcher" + | "StockV2Launcher" + | "StockV3Launcher"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): Promise { + const release = + staticReleaseForContract(event.contractName) ?? occurrence.release; + + if (event.contractName === "ClassicV2Launcher") { + if (event.eventName === "MemeTokenLaunched") { + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + creator: lowerAddress(params.creator), + poolId: lower(params.poolId), + hook: lowerAddress(params.feeHook), + positionRecipient: lowerAddress(params.positionRecipient), + positionTokenId: params.positionTokenId, + totalSwapFeeBps: exactInt(params.totalSwapFeeBps, "totalSwapFeeBps"), + launchOccurrenceId: occurrence.provenance.id, + hasLaunchEvent: true, + }, "launch", occurrence); + await reconcileLaunch(context, launch); + return; + } + if (event.eventName === "MemeLiquidityConfigured") { + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + totalSupply: params.totalSupply, + tokenLiquidityAmount: params.tokenLiquidityAmount, + lockedTokenDust: params.lockedTokenDust, + initialTick: exactInt(params.initialTick, "initialTick"), + tickLower: exactInt(params.tickLower, "tickLower"), + tickUpper: exactInt(params.tickUpper, "tickUpper"), + lpFeePips: exactInt(params.lpFeePips, "lpFeePips"), + liquidityOccurrenceId: occurrence.provenance.id, + hasLiquidityEvent: true, + }, "liquidity", occurrence); + await reconcileLaunch(context, launch); + return; + } + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + creator: lowerAddress(params.creator), + poolId: lower(params.poolId), + initialBuyQuoteAmount: params.nativeAmount, + initialBuyTokenAmount: params.tokenAmount, + initialBuyOccurrenceId: occurrence.provenance.id, + hasInitialBuyEvent: true, + }, "initial-buy", occurrence); + await reconcileLaunch(context, launch); + return; + } + + if (event.contractName === "ClassicV3Launcher") { + if (event.eventName === "MemeTokenLaunchedV2") { + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + creator: lowerAddress(params.deployer), + poolId: lower(params.poolId), + hook: lowerAddress(params.feeHook), + rewardVault: lowerAddress(params.rewardVault), + positionRecipient: lowerAddress(params.positionRecipient), + positionTokenId: params.positionTokenId, + buySwapFeeBps: exactInt(params.buySwapFeeBps, "buySwapFeeBps"), + sellSwapFeeBps: exactInt(params.sellSwapFeeBps, "sellSwapFeeBps"), + rewardConfigurationHash: lower(params.rewardConfigurationHash), + launchOccurrenceId: occurrence.provenance.id, + hasLaunchEvent: true, + }, "launch", occurrence); + await reconcileLaunch(context, launch); + return; + } + if (event.eventName === "MemeLiquidityConfiguredV2") { + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + totalSupply: params.totalSupply, + tokenLiquidityAmount: params.tokenLiquidityAmount, + lockedTokenDust: params.lockedTokenDust, + initialTick: exactInt(params.initialTick, "initialTick"), + tickLower: exactInt(params.tickLower, "tickLower"), + tickUpper: exactInt(params.tickUpper, "tickUpper"), + lpFeePips: exactInt(params.lpFeePips, "lpFeePips"), + liquidityOccurrenceId: occurrence.provenance.id, + hasLiquidityEvent: true, + }, "liquidity", occurrence); + await reconcileLaunch(context, launch); + return; + } + if (event.eventName === "MemeCreatorInitialBuyV2") { + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + creator: lowerAddress(params.deployer), + poolId: lower(params.poolId), + initialBuyQuoteAmount: params.nativeAmount, + initialBuyTokenAmount: params.tokenAmount, + initialBuyOccurrenceId: occurrence.provenance.id, + hasInitialBuyEvent: true, + }, "initial-buy", occurrence); + await reconcileLaunch(context, launch); + return; + } + const params = event.params; + const custody: InitialBuyCustody = { + ...immutableFields(occurrence), + launchHash: lower(params.launchHash), + deployer: lowerAddress(params.deployer), + token: lowerAddress(params.token), + custody: lowerAddress(params.custody), + mode: exactInt(params.mode, "custody mode"), + durationDays: exactInt(params.durationDays, "durationDays"), + cliffDays: exactInt(params.cliffDays, "cliffDays"), + configurationHash: lower(params.configurationHash), + }; + context.InitialBuyCustody.set(custody); + const launch = await upsertLaunch(context, release, params.launchHash, { + token: custody.token, + creator: custody.deployer, + custodyOccurrenceId: occurrence.provenance.id, + hasCustodyEvent: true, + }, "custody", occurrence); + await reconcileLaunch(context, launch); + return; + } + + const hook = hookForRelease(release.releaseVersion); + if (event.eventName === "StockPairedTokenLaunched") { + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + creator: lowerAddress(params.deployer), + quoteAsset: lowerAddress(params.quoteAsset), + poolId: lower(params.poolId), + hook, + rewardVault: lowerAddress(params.rewardVault), + positionRecipient: lowerAddress(params.positionRecipient), + positionTokenId: params.positionTokenId, + launchOccurrenceId: occurrence.provenance.id, + hasLaunchEvent: true, + }, "launch", occurrence); + await reconcileLaunch(context, launch); + return; + } + if (event.eventName === "StockPairedLiquidityConfigured") { + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + quoteAsset: lowerAddress(params.quoteAsset), + totalSupply: params.totalSupply, + tokenLiquidityAmount: params.tokenLiquidityAmount, + lockedTokenDust: params.lockedTokenDust, + initialTick: exactInt(params.initialTick, "initialTick"), + tickLower: exactInt(params.tickLower, "tickLower"), + tickUpper: exactInt(params.tickUpper, "tickUpper"), + lpFeePips: exactInt(params.lpFeePips, "lpFeePips"), + liquidityOccurrenceId: occurrence.provenance.id, + hasLiquidityEvent: true, + }, "liquidity", occurrence); + await reconcileLaunch(context, launch); + return; + } + const params = event.params; + const launch = await upsertLaunch(context, release, params.launchHash, { + token: lowerAddress(params.token), + creator: lowerAddress(params.deployer), + quoteAsset: lowerAddress(params.quoteAsset), + poolId: lower(params.poolId), + initialBuyQuoteAmount: params.quoteAmount, + initialBuyTokenAmount: params.tokenAmount, + initialBuyOccurrenceId: occurrence.provenance.id, + hasInitialBuyEvent: true, + }, "initial-buy", occurrence); + await reconcileLaunch(context, launch); +} + +async function handleCoordinatorEvent( + event: Extract< + EvmEvent, + { + contractName: + | "StockV1EthCoordinator" + | "StockV2EthCoordinator" + | "StockV3EthCoordinator"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): Promise { + const params = event.params; + const release = + staticReleaseForContract(event.contractName) ?? occurrence.release; + const expectedCoordinatorSource = sourceAddress(event.contractName); + const launch = await upsertLaunch(context, release, params.launchHash, { + creator: lowerAddress(params.creator), + token: lowerAddress(params.token), + quoteAsset: lowerAddress(params.quoteAsset), + initialBuyEthAmount: params.initialBuyEthAmount, + initialBuyQuoteAmount: params.initialBuyQuoteAmount, + initialBuyTokenAmount: params.initialBuyTokenAmount, + coordinatorOccurrenceId: occurrence.provenance.id, + hasCoordinatorEvent: true, + }, "coordinator", occurrence, { expectedCoordinatorSource }); + await reconcileLaunch(context, launch); +} + +async function upsertLaunch( + context: EvmOnEventContext, + release: ReleaseIdentity, + launchHashValue: string, + patch: Partial, + kind: "launch" | "liquidity" | "initial-buy" | "custody" | "coordinator", + occurrence: RecordedOccurrence, + authorization: Readonly<{ + expectedCoordinatorSource?: string; + }> = {}, +): Promise { + const launchHash = lower(launchHashValue); + const id = launchEntityId(CHAIN_ID, release.releaseVersion, launchHash); + const existing = + (await context.Launch.get(id)) ?? + defaultLaunch(id, release, launchHash, occurrence.provenance.blockNumber); + const next: Mutable = { ...existing }; + let provenanceValid = existing.provenanceValid; + const authenticatedCoordinatorSource = + kind === "coordinator" && + authorization.expectedCoordinatorSource !== undefined && + sameValue( + occurrence.provenance.sourceAddress, + authorization.expectedCoordinatorSource, + ) + ? authorization.expectedCoordinatorSource + : undefined; + if (kind === "coordinator" && authenticatedCoordinatorSource === undefined) { + provenanceValid = false; + } + + for (const key of Object.keys(patch) as (keyof Launch)[]) { + const incoming = patch[key]; + if (incoming === undefined) { + continue; + } + const current = next[key]; + const replacesProvisionalCoordinatorCreator = + key === "creator" && + kind === "coordinator" && + authenticatedCoordinatorSource !== undefined && + current !== undefined && + sameValue(current, authenticatedCoordinatorSource); + if ( + LAUNCH_IDENTITY_FIELDS.has(key) && + current !== undefined && + !sameValue(current, incoming) && + !replacesProvisionalCoordinatorCreator + ) { + provenanceValid = false; + } + if ( + current === undefined || + kind === "launch" || + isEventFlag(key) || + replacesProvisionalCoordinatorCreator + ) { + (next as Record)[key] = incoming; + } + } + + next.provenanceValid = provenanceValid; + next.updatedBlock = + occurrence.provenance.blockNumber > next.updatedBlock + ? occurrence.provenance.blockNumber + : next.updatedBlock; + next.isComplete = false; + context.Launch.set(next); + return next; +} + +function defaultLaunch( + id: string, + release: ReleaseIdentity, + launchHash: string, + blockNumber: bigint, +): Launch { + return { + id, + chainId: CHAIN_ID, + model: release.model, + releaseVersion: release.releaseVersion, + launchHash, + token: undefined, + creator: undefined, + quoteAsset: undefined, + poolId: undefined, + hook: undefined, + rewardVault: undefined, + positionRecipient: undefined, + positionTokenId: undefined, + totalSwapFeeBps: undefined, + buySwapFeeBps: undefined, + sellSwapFeeBps: undefined, + rewardConfigurationHash: undefined, + quoteConfigurationHash: undefined, + totalSupply: undefined, + tokenLiquidityAmount: undefined, + lockedTokenDust: undefined, + initialTick: undefined, + tickLower: undefined, + tickUpper: undefined, + lpFeePips: undefined, + initialBuyQuoteAmount: undefined, + initialBuyTokenAmount: undefined, + initialBuyEthAmount: undefined, + launchOccurrenceId: undefined, + liquidityOccurrenceId: undefined, + initialBuyOccurrenceId: undefined, + custodyOccurrenceId: undefined, + coordinatorOccurrenceId: undefined, + hasLaunchEvent: false, + hasLiquidityEvent: false, + hasInitialBuyEvent: false, + hasCustodyEvent: false, + hasCoordinatorEvent: false, + hasPoolRegistrationEvent: false, + hasPoolFeeDisclosureEvent: false, + hasRewardVaultFactoryEvent: false, + provenanceValid: true, + isComplete: false, + updatedBlock: blockNumber, + }; +} + +function launchIsComplete(launch: Launch): boolean { + const base = + launch.provenanceValid && + launch.hasLaunchEvent && + launch.hasLiquidityEvent && + launch.hasInitialBuyEvent && + launch.hasPoolRegistrationEvent && + launch.hasPoolFeeDisclosureEvent && + (launch.releaseVersion === "classic-v2" || + launch.hasRewardVaultFactoryEvent); + return launch.releaseVersion === "classic-v3" + ? base && launch.hasCustodyEvent + : base; +} + +function applyPoolConfigurationToLaunch( + launchInput: Launch, + configInput: PoolFeeConfig, +): { launch: Launch; config: PoolFeeConfig } { + const launch: Mutable = { ...launchInput }; + launch.hasPoolRegistrationEvent = + configInput.registrationOccurrenceId !== undefined; + launch.hasPoolFeeDisclosureEvent = + configInput.disclosureOccurrenceId !== undefined; + if ( + launch.releaseVersion !== "classic-v3" && + launch.rewardConfigurationHash === undefined && + configInput.rewardConfigurationHash !== undefined + ) { + launch.rewardConfigurationHash = configInput.rewardConfigurationHash; + } + if ( + launch.quoteConfigurationHash === undefined && + configInput.quoteConfigurationHash !== undefined + ) { + launch.quoteConfigurationHash = configInput.quoteConfigurationHash; + } + const configValid = + configInput.provenanceValid && + optionalMatches(configInput.token, launch.token) && + optionalMatches(configInput.quoteAsset, launch.quoteAsset) && + optionalMatches(configInput.rewardVault, launch.rewardVault) && + (launch.releaseVersion !== "classic-v3" || + !launch.hasPoolRegistrationEvent || + exactOptionalValueMatch( + configInput.rewardConfigurationHash, + launch.rewardConfigurationHash, + )) && + compatibleOptionalValue( + configInput.quoteConfigurationHash, + launch.quoteConfigurationHash, + ); + if (!configValid) { + launch.provenanceValid = false; + } + launch.updatedBlock = + configInput.blockNumber > launch.updatedBlock + ? configInput.blockNumber + : launch.updatedBlock; + launch.isComplete = launchIsComplete(launch); + return { + launch, + config: { + ...configInput, + model: launch.model, + releaseVersion: launch.releaseVersion, + provenanceValid: configValid, + }, + }; +} + +function applyRewardVaultToLaunch( + launchInput: Launch, + vaultInput: RewardVault, +): { launch: Launch; vault: RewardVault } { + const launch: Mutable = { ...launchInput }; + const vaultValid = + launch.poolId !== undefined && + launch.hook !== undefined && + launch.rewardVault !== undefined && + sameValue(vaultInput.vault, launch.rewardVault) && + sameValue(vaultInput.poolId, launch.poolId) && + sameValue(vaultInput.hook, launch.hook) && + optionalMatches(vaultInput.quoteAsset, launch.quoteAsset) && + (launch.releaseVersion !== "classic-v3" || + exactOptionalValueMatch( + vaultInput.configurationHash, + launch.rewardConfigurationHash, + )); + if (vaultValid) { + launch.hasRewardVaultFactoryEvent = true; + } else { + launch.provenanceValid = false; + } + launch.updatedBlock = + vaultInput.blockNumber > launch.updatedBlock + ? vaultInput.blockNumber + : launch.updatedBlock; + launch.isComplete = launchIsComplete(launch); + return { + launch, + vault: { + ...vaultInput, + model: launch.model, + releaseVersion: launch.releaseVersion, + }, + }; +} + +async function reconcileLaunch( + context: EvmOnEventContext, + launchInput: Launch, +): Promise { + let launch: Mutable = { ...launchInput }; + const expectedHook = hookForRelease(launch.releaseVersion); + if (!launch.hasLaunchEvent) { + launch.isComplete = false; + context.Launch.set(launch); + return; + } + if ( + launch.token === undefined || + launch.poolId === undefined || + launch.hook === undefined || + expectedHook === undefined + ) { + launch.provenanceValid = false; + launch.isComplete = false; + context.Launch.set(launch); + return; + } + if (!sameValue(launch.hook, expectedHook)) { + launch.provenanceValid = false; + } + + const relationId = poolEntityId(CHAIN_ID, launch.poolId); + context.PoolRelease.set({ + id: relationId, + chainId: CHAIN_ID, + launchId: launch.id, + model: launch.model, + releaseVersion: launch.releaseVersion, + token: launch.token, + quoteAsset: launch.quoteAsset, + hook: launch.hook, + rewardVault: launch.rewardVault, + blockNumber: launch.updatedBlock, + }); + + const config = await context.PoolFeeConfig.get(relationId); + if (config !== undefined) { + const reconciled = applyPoolConfigurationToLaunch(launch, config); + launch = { ...reconciled.launch }; + context.PoolFeeConfig.set(reconciled.config); + } + + const poolVaults = await context.RewardVault.getWhere({ + poolId: { _eq: launch.poolId }, + }); + if (launch.rewardVault !== undefined) { + const vault = poolVaults.find(({ vault: address }) => + sameValue(address, launch.rewardVault) + ); + if ( + poolVaults.some(({ vault: address }) => + !sameValue(address, launch.rewardVault) + ) + ) { + launch.provenanceValid = false; + } + if (vault !== undefined) { + const reconciled = applyRewardVaultToLaunch(launch, vault); + launch = { ...reconciled.launch }; + context.RewardVault.set(reconciled.vault); + } + } + + const totals = await context.PoolFeeTotals.get(relationId); + if (totals !== undefined) { + context.PoolFeeTotals.set({ + ...totals, + model: launch.model, + releaseVersion: launch.releaseVersion, + }); + } + await relabelPoolScopedEntities(context, launch); + launch.isComplete = launchIsComplete(launch); + context.Launch.set(launch); +} + +async function relabelPoolScopedEntities( + context: EvmOnEventContext, + launch: Launch, +): Promise { + if (launch.poolId === undefined) { + return; + } + const poolFilter = { poolId: { _eq: launch.poolId } }; + const [ + feeAccruals, + creatorFeeClaims, + rewardCheckpoints, + payoutChanges, + rewardConfigurationChanges, + ] = await Promise.all([ + context.FeeAccrual.getWhere(poolFilter), + context.CreatorFeeClaim.getWhere(poolFilter), + context.RewardCheckpoint.getWhere(poolFilter), + context.PayoutChange.getWhere(poolFilter), + context.RewardConfigurationChange.getWhere(poolFilter), + ]); + + for (const entity of feeAccruals) { + context.FeeAccrual.set({ + ...entity, + model: launch.model, + releaseVersion: launch.releaseVersion, + }); + } + for (const entity of creatorFeeClaims) { + context.CreatorFeeClaim.set({ + ...entity, + model: launch.model, + releaseVersion: launch.releaseVersion, + }); + } + for (const entity of rewardCheckpoints) { + context.RewardCheckpoint.set({ + ...entity, + model: launch.model, + releaseVersion: launch.releaseVersion, + }); + } + for (const entity of payoutChanges) { + context.PayoutChange.set({ + ...entity, + model: launch.model, + releaseVersion: launch.releaseVersion, + }); + } + for (const entity of rewardConfigurationChanges) { + context.RewardConfigurationChange.set({ + ...entity, + model: launch.model, + releaseVersion: launch.releaseVersion, + }); + } + + if (launch.rewardVault === undefined) { + return; + } + const beneficiaryClaims = await context.BeneficiaryClaim.getWhere({ + vault: { _eq: launch.rewardVault }, + }); + for (const entity of beneficiaryClaims) { + context.BeneficiaryClaim.set({ + ...entity, + model: launch.model, + releaseVersion: launch.releaseVersion, + }); + } +} + +async function handleHookEvent( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV2Hook" + | "ClassicV3Hook" + | "StockV1Hook" + | "StockV2V3Hook"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): Promise { + switch (event.eventName) { + case "PoolRegistered": + case "PoolFeeDisclosure": + await handlePoolConfigurationEvent(event, context, occurrence); + return; + case "NativeSwapFeesAccrued": + case "QuoteSwapFeesAccrued": + await handleFeeAccrualEvent(event, context, occurrence); + return; + case "CreatorFeesClaimed": + handleCreatorFeeClaim(event, context, occurrence); + return; + case "LauncherFeesClaimed": + handleLauncherFeeClaim(event, context, occurrence); + return; + } +} + +async function handlePoolConfigurationEvent( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV2Hook" + | "ClassicV3Hook" + | "StockV1Hook" + | "StockV2V3Hook"; + eventName: "PoolRegistered" | "PoolFeeDisclosure"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): Promise { + const params = event.params; + const poolId = lower(params.poolId); + const id = poolEntityId(CHAIN_ID, poolId); + const existing = + (await context.PoolFeeConfig.get(id)) ?? + defaultPoolFeeConfig(id, poolId, occurrence); + const next: Mutable = { ...existing }; + let valid = existing.provenanceValid; + + const mergeIdentity = ( + key: "token" | "creator" | "quoteAsset" | "rewardVault", + value: string | undefined, + ): void => { + if (value === undefined) { + return; + } + const current = next[key]; + if (current !== undefined && !sameValue(current, value)) { + valid = false; + return; + } + next[key] = value; + }; + + mergeIdentity("token", lowerAddress(params.token)); + + if (event.contractName === "ClassicV2Hook") { + if (event.eventName === "PoolRegistered") { + const typedParams = event.params; + mergeIdentity("creator", lowerAddress(typedParams.creator)); + next.registrar = lowerAddress(typedParams.registrar); + next.totalSwapFeeBps = exactInt( + typedParams.totalSwapFeeBps, + "totalSwapFeeBps", + ); + next.registrationOccurrenceId = occurrence.provenance.id; + } else { + const typedParams = event.params; + next.buySwapFeeBps = exactInt(typedParams.buySwapFeeBps, "buySwapFeeBps"); + next.sellSwapFeeBps = exactInt(typedParams.sellSwapFeeBps, "sellSwapFeeBps"); + next.launcherFeeBps = exactInt(typedParams.launcherFeeBps, "launcherFeeBps"); + next.transferTaxBps = exactInt(typedParams.transferTaxBps, "transferTaxBps"); + next.lpFeePips = exactInt(typedParams.lpFeePips, "lpFeePips"); + next.disclosureOccurrenceId = occurrence.provenance.id; + } + } else if (event.contractName === "ClassicV3Hook") { + mergeIdentity("rewardVault", lowerAddress(event.params.rewardVault)); + if (event.eventName === "PoolRegistered") { + const typedParams = event.params; + next.registrar = lowerAddress(typedParams.registrar); + next.buySwapFeeBps = exactInt(typedParams.buySwapFeeBps, "buySwapFeeBps"); + next.sellSwapFeeBps = exactInt(typedParams.sellSwapFeeBps, "sellSwapFeeBps"); + next.rewardConfigurationHash = lower(typedParams.rewardConfigurationHash); + next.registrationOccurrenceId = occurrence.provenance.id; + } else { + const typedParams = event.params; + next.buySwapFeeBps = exactInt(typedParams.buySwapFeeBps, "buySwapFeeBps"); + next.sellSwapFeeBps = exactInt(typedParams.sellSwapFeeBps, "sellSwapFeeBps"); + next.buyCreatorFeeBps = exactInt( + typedParams.buyCreatorFeeBps, + "buyCreatorFeeBps", + ); + next.sellCreatorFeeBps = exactInt( + typedParams.sellCreatorFeeBps, + "sellCreatorFeeBps", + ); + next.launcherFeeBps = exactInt(typedParams.launcherFeeBps, "launcherFeeBps"); + next.transferTaxBps = exactInt(typedParams.transferTaxBps, "transferTaxBps"); + next.lpFeePips = exactInt(typedParams.lpFeePips, "lpFeePips"); + next.disclosureOccurrenceId = occurrence.provenance.id; + } + } else { + mergeIdentity("quoteAsset", lowerAddress(event.params.quoteAsset)); + mergeIdentity("rewardVault", lowerAddress(event.params.rewardVault)); + if (event.eventName === "PoolRegistered") { + const typedParams = event.params; + next.registrar = lowerAddress(typedParams.registrar); + next.quoteIsCurrency0 = typedParams.quoteIsCurrency0; + next.rewardConfigurationHash = lower(typedParams.rewardConfigurationHash); + next.quoteConfigurationHash = lower(typedParams.quoteConfigurationHash); + next.registrationOccurrenceId = occurrence.provenance.id; + } else { + const typedParams = event.params; + next.buySwapFeeBps = exactInt(typedParams.buySwapFeeBps, "buySwapFeeBps"); + next.sellSwapFeeBps = exactInt(typedParams.sellSwapFeeBps, "sellSwapFeeBps"); + next.creatorFeeBps = exactInt(typedParams.creatorFeeBps, "creatorFeeBps"); + next.launcherFeeBps = exactInt(typedParams.launcherFeeBps, "launcherFeeBps"); + next.transferTaxBps = exactInt(typedParams.transferTaxBps, "transferTaxBps"); + next.lpFeePips = exactInt(typedParams.lpFeePips, "lpFeePips"); + next.disclosureOccurrenceId = occurrence.provenance.id; + } + } + + const relation = await context.PoolRelease.get(id); + if (relation !== undefined) { + next.model = relation.model; + next.releaseVersion = relation.releaseVersion; + valid = + valid && + sameValue(next.token, relation.token) && + optionalMatches(next.quoteAsset, relation.quoteAsset) && + optionalMatches(next.rewardVault, relation.rewardVault); + } else if ( + next.releaseVersion === "unresolved" && + occurrence.release.releaseVersion !== "unresolved" + ) { + next.model = occurrence.release.model; + next.releaseVersion = occurrence.release.releaseVersion; + } + next.provenanceValid = valid; + next.blockNumber = + occurrence.provenance.blockNumber > next.blockNumber + ? occurrence.provenance.blockNumber + : next.blockNumber; + context.PoolFeeConfig.set(next); + if (relation !== undefined) { + const launch = await context.Launch.get(relation.launchId); + if (launch !== undefined) { + const reconciled = applyPoolConfigurationToLaunch(launch, next); + context.PoolFeeConfig.set(reconciled.config); + context.Launch.set(reconciled.launch); + } + } +} + +function defaultPoolFeeConfig( + id: string, + poolId: string, + occurrence: RecordedOccurrence, +): PoolFeeConfig { + return { + id, + chainId: CHAIN_ID, + poolId, + token: undefined, + creator: undefined, + quoteAsset: undefined, + rewardVault: undefined, + registrar: undefined, + model: occurrence.release.model, + releaseVersion: occurrence.release.releaseVersion, + totalSwapFeeBps: undefined, + buySwapFeeBps: undefined, + sellSwapFeeBps: undefined, + buyCreatorFeeBps: undefined, + sellCreatorFeeBps: undefined, + creatorFeeBps: undefined, + launcherFeeBps: undefined, + transferTaxBps: undefined, + lpFeePips: undefined, + quoteIsCurrency0: undefined, + rewardConfigurationHash: undefined, + quoteConfigurationHash: undefined, + registrationOccurrenceId: undefined, + disclosureOccurrenceId: undefined, + provenanceValid: true, + blockNumber: occurrence.provenance.blockNumber, + }; +} + +async function handleFeeAccrualEvent( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV2Hook" + | "ClassicV3Hook" + | "StockV1Hook" + | "StockV2V3Hook"; + eventName: "NativeSwapFeesAccrued" | "QuoteSwapFeesAccrued"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): Promise { + const params = event.params; + const poolId = lower(params.poolId); + let quoteAsset: string | undefined; + let isBuy: boolean | undefined; + let appliedTotalSwapFeeBps: number | undefined; + let grossAmount: bigint; + + if (event.eventName === "QuoteSwapFeesAccrued") { + const typedParams = event.params; + quoteAsset = lowerAddress(typedParams.quoteAsset); + isBuy = typedParams.isBuy; + grossAmount = typedParams.grossQuoteAmount; + } else { + const typedParams = event.params; + grossAmount = typedParams.grossNativeAmount; + if ("isBuy" in typedParams) { + isBuy = typedParams.isBuy; + appliedTotalSwapFeeBps = exactInt( + typedParams.appliedTotalSwapFeeBps, + "appliedTotalSwapFeeBps", + ); + } + } + + const feeAccrual: FeeAccrual = { + ...immutableFields(occurrence), + poolId, + swapSender: lowerAddress(params.swapSender), + quoteAsset, + isBuy, + appliedTotalSwapFeeBps, + grossAmount, + creatorFee: params.creatorFee, + launcherFee: params.launcherFee, + }; + context.FeeAccrual.set(feeAccrual); + + const totalsId = poolEntityId(CHAIN_ID, poolId); + const current = await context.PoolFeeTotals.get(totalsId); + context.PoolFeeTotals.set({ + id: totalsId, + chainId: CHAIN_ID, + poolId, + model: current?.model ?? occurrence.release.model, + releaseVersion: + current?.releaseVersion ?? occurrence.release.releaseVersion, + grossAmount: (current?.grossAmount ?? 0n) + grossAmount, + creatorFees: (current?.creatorFees ?? 0n) + params.creatorFee, + launcherFees: (current?.launcherFees ?? 0n) + params.launcherFee, + swapCount: (current?.swapCount ?? 0n) + 1n, + lastOccurrenceId: occurrence.provenance.id, + blockNumber: occurrence.provenance.blockNumber, + }); +} + +function handleCreatorFeeClaim( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV2Hook" + | "ClassicV3Hook" + | "StockV1Hook" + | "StockV2V3Hook"; + eventName: "CreatorFeesClaimed"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): void { + const params = event.params; + let creator: string | undefined; + let recipient: string | undefined; + let rewardVault: string | undefined; + let quoteAsset: string | undefined; + if (event.contractName === "ClassicV2Hook") { + creator = lowerAddress(event.params.creator); + recipient = lowerAddress(event.params.recipient); + } else { + rewardVault = lowerAddress(event.params.rewardVault); + if ( + event.contractName === "StockV1Hook" || + event.contractName === "StockV2V3Hook" + ) { + quoteAsset = lowerAddress(event.params.quoteAsset); + } + } + const claim: CreatorFeeClaim = { + ...immutableFields(occurrence), + poolId: lower(params.poolId), + creator, + rewardVault, + recipient, + quoteAsset, + caller: lowerAddress(params.caller), + amount: params.amount, + }; + context.CreatorFeeClaim.set(claim); +} + +function handleLauncherFeeClaim( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV2Hook" + | "ClassicV3Hook" + | "StockV1Hook" + | "StockV2V3Hook"; + eventName: "LauncherFeesClaimed"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): void { + const quoteAsset = + event.contractName === "StockV1Hook" || + event.contractName === "StockV2V3Hook" + ? lowerAddress(event.params.quoteAsset) + : undefined; + const params = event.params; + const claim: LauncherFeeClaim = { + ...immutableFields(occurrence), + treasury: lowerAddress(params.treasury), + recipient: lowerAddress(params.recipient), + quoteAsset, + caller: lowerAddress(params.caller), + amount: params.amount, + }; + context.LauncherFeeClaim.set(claim); +} + +async function handleVaultFactoryEvent( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV3RewardVaultFactory" + | "StockV1RewardVaultFactory" + | "StockV2V3RewardVaultFactory"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): Promise { + const params = event.params; + const vault = lowerAddress(params.vault); + const poolId = lower(params.poolId); + const relation = await context.PoolRelease.get( + poolEntityId(CHAIN_ID, poolId), + ); + const release = + relation === undefined + ? occurrence.release + : { + model: relation.model, + releaseVersion: relation.releaseVersion, + }; + const existing = await context.RewardVault.get(vault); + if (existing !== undefined) { + return; + } + const quoteAsset = + event.contractName === "ClassicV3RewardVaultFactory" + ? undefined + : lowerAddress(event.params.quoteAsset); + const salt = + event.contractName === "ClassicV3RewardVaultFactory" + ? lower(event.params.salt) + : undefined; + const configurationHash = + event.contractName === "ClassicV3RewardVaultFactory" + ? lower(event.params.configurationHash) + : undefined; + + const rewardVaultEntity: RewardVault = { + id: vault, + chainId: CHAIN_ID, + vault, + poolId, + hook: lowerAddress(params.feeHook), + quoteAsset, + salt, + configurationHash, + model: release.model, + releaseVersion: release.releaseVersion, + factoryOccurrenceId: occurrence.provenance.id, + downstreamLogicalId: undefined, + receiptLogOrdinal: undefined, + payloadHash: occurrence.payloadHash, + sourceAddress: occurrence.provenance.sourceAddress, + blockNumber: occurrence.provenance.blockNumber, + blockHash: occurrence.provenance.blockHash, + transactionHash: occurrence.provenance.transactionHash, + blockGlobalLogIndex: occurrence.provenance.blockGlobalLogIndex, + }; + context.RewardVault.set(rewardVaultEntity); + if (relation !== undefined) { + const launch = await context.Launch.get(relation.launchId); + if (launch !== undefined) { + const reconciled = applyRewardVaultToLaunch( + launch, + rewardVaultEntity, + ); + context.RewardVault.set(reconciled.vault); + context.Launch.set(reconciled.launch); + } + } +} + +function handleVestingFactoryEvent( + event: Extract< + EvmEvent, + { + contractName: "ClassicV3VestingWalletFactory"; + eventName: "ClassicInitialBuyVestingWalletDeployed"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): void { + const params = event.params; + const wallet: VestingWallet = { + ...immutableFields(occurrence), + wallet: lowerAddress(params.wallet), + token: lowerAddress(params.token), + beneficiary: lowerAddress(params.beneficiary), + salt: lower(params.salt), + configurationHash: lower(params.configurationHash), + }; + context.VestingWallet.set(wallet); +} + +async function handleRewardVaultEvent( + event: Extract< + EvmEvent, + { + contractName: + | "ClassicV3RewardVault" + | "StockV1RewardVault" + | "StockV2V3RewardVault"; + } + >, + context: EvmOnEventContext, + occurrence: RecordedOccurrence, +): Promise { + const vault = occurrence.provenance.sourceAddress; + const vaultEntity = await context.RewardVault.get(vault); + + if (event.contractName === "ClassicV3RewardVault") { + if (event.eventName === "CreatorFeesCheckpointed") { + const params = event.params; + const checkpoint: RewardCheckpoint = { + ...immutableFields(occurrence), + vault, + poolId: lower(params.poolId), + configurationEpoch: params.configurationEpoch, + amount: params.amount, + totalCreatorFeesReceived: params.totalCreatorFeesReceived, + }; + context.RewardCheckpoint.set(checkpoint); + return; + } + if (event.eventName === "BeneficiaryFeesClaimed") { + const params = event.params; + const claim: BeneficiaryClaim = { + ...immutableFields(occurrence), + vault, + beneficiary: lowerAddress(params.beneficiary), + payoutAddress: undefined, + quoteAsset: vaultEntity?.quoteAsset, + amount: params.amount, + beneficiaryTotalClaimed: params.beneficiaryTotalClaimed, + vaultTotalReceived: params.vaultTotalReceived, + }; + context.BeneficiaryClaim.set(claim); + return; + } + if (event.eventName === "PayoutWalletChanged") { + const params = event.params; + const change: PayoutChange = { + ...immutableFields(occurrence), + vault, + poolId: lower(params.poolId), + beneficiary: undefined, + allocationIndex: params.allocationIndex, + previousPayoutAddress: lowerAddress(params.previousPayoutWallet), + newPayoutAddress: lowerAddress(params.newPayoutWallet), + shareBps: exactInt(params.shareBps, "shareBps"), + configurationEpoch: params.configurationEpoch, + activeConfigurationHash: lower(params.activeConfigurationHash), + effectiveTotalCreatorFeesReceived: + params.effectiveTotalCreatorFeesReceived, + }; + context.PayoutChange.set(change); + return; + } + const params = event.params; + const change: RewardConfigurationChange = { + ...immutableFields(occurrence), + vault, + poolId: lower(params.poolId), + approvalReference: lower(params.approvalReference), + configurationEpoch: params.configurationEpoch, + previousConfigurationHash: lower(params.previousConfigurationHash), + newConfigurationHash: lower(params.newConfigurationHash), + beneficiaries: params.beneficiaries.map((value) => + lowerAddress(value) + ), + sharesBps: params.sharesBps.map((value) => + exactInt(value, "beneficiary shareBps") + ), + effectiveTotalCreatorFeesReceived: + params.effectiveTotalCreatorFeesReceived, + }; + context.RewardConfigurationChange.set(change); + return; + } + + if (event.eventName === "PayoutAddressUpdated") { + const params = event.params; + const change: PayoutChange = { + ...immutableFields(occurrence), + vault, + poolId: vaultEntity?.poolId, + beneficiary: lowerAddress(params.beneficiary), + allocationIndex: undefined, + previousPayoutAddress: lowerAddress(params.previousPayoutAddress), + newPayoutAddress: lowerAddress(params.newPayoutAddress), + shareBps: undefined, + configurationEpoch: undefined, + activeConfigurationHash: undefined, + effectiveTotalCreatorFeesReceived: undefined, + }; + context.PayoutChange.set(change); + return; + } + const params = event.params; + const claim: BeneficiaryClaim = { + ...immutableFields(occurrence), + vault, + beneficiary: lowerAddress(params.beneficiary), + payoutAddress: lowerAddress(params.payoutAddress), + quoteAsset: lowerAddress(params.quoteAsset), + amount: params.amount, + beneficiaryTotalClaimed: params.beneficiaryTotalClaimed, + vaultTotalReceived: params.vaultTotalReceived, + }; + context.BeneficiaryClaim.set(claim); +} + +function immutableFields(occurrence: RecordedOccurrence): { + id: string; + downstreamLogicalId: undefined; + receiptLogOrdinal: undefined; + chainId: number; + blockNumber: bigint; + blockHash: string; + blockTimestamp: bigint; + transactionHash: string; + transactionIndex: bigint; + blockGlobalLogIndex: bigint; + sourceAddress: string; + model: string; + releaseVersion: string; + payloadHash: string; +} { + return { + ...occurrence.provenance, + model: occurrence.release.model, + releaseVersion: occurrence.release.releaseVersion, + payloadHash: occurrence.payloadHash, + }; +} + +function findEventAbi(event: EvmEvent): AbiEvent { + const contract = indexer.chains[CHAIN_ID][event.contractName]; + const eventAbi = contract.abi.find( + (item): item is AbiEvent => + typeof item === "object" && + item !== null && + "type" in item && + item.type === "event" && + "name" in item && + item.name === event.eventName, + ); + if (eventAbi === undefined) { + throw new Error( + `Missing configured ABI for ${event.contractName}.${event.eventName}`, + ); + } + return eventAbi; +} + +function eventPoolId(event: EvmEvent): string | undefined { + const params = event.params as unknown as Record; + return typeof params.poolId === "string" ? lower(params.poolId) : undefined; +} + +function eventBlockFilter(contractName: string): { + readonly block: { readonly number: { readonly _gte: number } }; +} { + const startBlock = sourceStartBlock(contractName); + if (startBlock === undefined) { + throw new Error(`Missing source start block for ${contractName}`); + } + return { block: { number: { _gte: startBlock } } }; +} + +function sourceAddress(contractName: string): string { + const source = SOURCE_REGISTRY.find( + (entry) => entry.contractName === contractName, + ); + if (source === undefined) { + throw new Error(`Missing configured source for ${contractName}`); + } + return lowerAddress(source.address); +} + +function hookForRelease(releaseVersion: string): string | undefined { + switch (releaseVersion) { + case "classic-v2": + return CLASSIC_V2_HOOK; + case "classic-v3": + return CLASSIC_V3_HOOK; + case "stock-paired-v1": + return STOCK_V1_HOOK; + case "stock-paired-v2": + case "stock-paired-v3": + return STOCK_V2_V3_HOOK; + default: + return undefined; + } +} + +function exactInt(value: bigint, field: string): number { + const result = Number(value); + if (!Number.isSafeInteger(result)) { + throw new RangeError(`${field} exceeds the exact GraphQL Int range`); + } + return result; +} + +function sameValue(left: unknown, right: unknown): boolean { + if (typeof left === "string" && typeof right === "string") { + return lower(left) === lower(right); + } + return left === right; +} + +function optionalMatches( + actual: string | undefined, + expected: string | undefined, +): boolean { + return actual === undefined || expected === undefined + ? actual === expected || actual === undefined + : sameValue(actual, expected); +} + +function compatibleOptionalValue( + left: string | undefined, + right: string | undefined, +): boolean { + return left === undefined || right === undefined || sameValue(left, right); +} + +function exactOptionalValueMatch( + left: string | undefined, + right: string | undefined, +): boolean { + return ( + left !== undefined && + right !== undefined && + sameValue(left, right) + ); +} + +function isEventFlag(key: keyof Launch): boolean { + return ( + key === "hasLaunchEvent" || + key === "hasLiquidityEvent" || + key === "hasInitialBuyEvent" || + key === "hasCustodyEvent" || + key === "hasCoordinatorEvent" || + key === "hasPoolRegistrationEvent" || + key === "hasPoolFeeDisclosureEvent" || + key === "hasRewardVaultFactoryEvent" + ); +} + +indexer.onEvent( + { + contract: "ClassicV2Launcher", + event: "MemeTokenLaunched", + where: eventBlockFilter("ClassicV2Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV2Launcher", + event: "MemeLiquidityConfigured", + where: eventBlockFilter("ClassicV2Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV2Launcher", + event: "MemeCreatorInitialBuy", + where: eventBlockFilter("ClassicV2Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV2Hook", + event: "PoolRegistered", + where: eventBlockFilter("ClassicV2Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV2Hook", + event: "PoolFeeDisclosure", + where: eventBlockFilter("ClassicV2Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV2Hook", + event: "NativeSwapFeesAccrued", + where: eventBlockFilter("ClassicV2Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV2Hook", + event: "CreatorFeesClaimed", + where: eventBlockFilter("ClassicV2Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV2Hook", + event: "LauncherFeesClaimed", + where: eventBlockFilter("ClassicV2Hook"), + }, + handleEvent, +); + +indexer.onEvent( + { + contract: "ClassicV3Launcher", + event: "MemeTokenLaunchedV2", + where: eventBlockFilter("ClassicV3Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Launcher", + event: "MemeLiquidityConfiguredV2", + where: eventBlockFilter("ClassicV3Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Launcher", + event: "MemeCreatorInitialBuyV2", + where: eventBlockFilter("ClassicV3Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Launcher", + event: "MemeCreatorInitialBuyCustodyV2", + where: eventBlockFilter("ClassicV3Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Hook", + event: "PoolRegistered", + where: eventBlockFilter("ClassicV3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Hook", + event: "PoolFeeDisclosure", + where: eventBlockFilter("ClassicV3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Hook", + event: "NativeSwapFeesAccrued", + where: eventBlockFilter("ClassicV3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Hook", + event: "CreatorFeesClaimed", + where: eventBlockFilter("ClassicV3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3Hook", + event: "LauncherFeesClaimed", + where: eventBlockFilter("ClassicV3Hook"), + }, + handleEvent, +); +indexer.contractRegister( + { + contract: "ClassicV3RewardVaultFactory", + event: "ClassicRewardVaultDeployed", + where: eventBlockFilter("ClassicV3RewardVaultFactory"), + }, + async ({ event, context }) => { + context.chain.ClassicV3RewardVault.add(event.params.vault); + }, +); +indexer.onEvent( + { + contract: "ClassicV3RewardVaultFactory", + event: "ClassicRewardVaultDeployed", + where: eventBlockFilter("ClassicV3RewardVaultFactory"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3VestingWalletFactory", + event: "ClassicInitialBuyVestingWalletDeployed", + where: eventBlockFilter("ClassicV3VestingWalletFactory"), + }, + handleEvent, +); +indexer.onEvent( + { contract: "ClassicV3RewardVault", event: "CreatorFeesCheckpointed" }, + handleEvent, +); +indexer.onEvent( + { contract: "ClassicV3RewardVault", event: "BeneficiaryFeesClaimed" }, + handleEvent, +); +indexer.onEvent( + { contract: "ClassicV3RewardVault", event: "PayoutWalletChanged" }, + handleEvent, +); +indexer.onEvent( + { + contract: "ClassicV3RewardVault", + event: "CtoRewardConfigurationActivated", + }, + handleEvent, +); + +indexer.onEvent( + { + contract: "StockV1Launcher", + event: "StockPairedTokenLaunched", + where: eventBlockFilter("StockV1Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1Launcher", + event: "StockPairedLiquidityConfigured", + where: eventBlockFilter("StockV1Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1Launcher", + event: "StockPairedCreatorInitialBuy", + where: eventBlockFilter("StockV1Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1EthCoordinator", + event: "StockPairedEthTokenLaunched", + where: eventBlockFilter("StockV1EthCoordinator"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1Hook", + event: "PoolRegistered", + where: eventBlockFilter("StockV1Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1Hook", + event: "PoolFeeDisclosure", + where: eventBlockFilter("StockV1Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1Hook", + event: "QuoteSwapFeesAccrued", + where: eventBlockFilter("StockV1Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1Hook", + event: "CreatorFeesClaimed", + where: eventBlockFilter("StockV1Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV1Hook", + event: "LauncherFeesClaimed", + where: eventBlockFilter("StockV1Hook"), + }, + handleEvent, +); +indexer.contractRegister( + { + contract: "StockV1RewardVaultFactory", + event: "QuoteAssetFeeSplitVaultDeployed", + where: eventBlockFilter("StockV1RewardVaultFactory"), + }, + async ({ event, context }) => { + context.chain.StockV1RewardVault.add(event.params.vault); + }, +); +indexer.onEvent( + { + contract: "StockV1RewardVaultFactory", + event: "QuoteAssetFeeSplitVaultDeployed", + where: eventBlockFilter("StockV1RewardVaultFactory"), + }, + handleEvent, +); +indexer.onEvent( + { contract: "StockV1RewardVault", event: "PayoutAddressUpdated" }, + handleEvent, +); +indexer.onEvent( + { contract: "StockV1RewardVault", event: "BeneficiaryFeesClaimed" }, + handleEvent, +); + +indexer.onEvent( + { + contract: "StockV2Launcher", + event: "StockPairedTokenLaunched", + where: eventBlockFilter("StockV2Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV2Launcher", + event: "StockPairedLiquidityConfigured", + where: eventBlockFilter("StockV2Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV2Launcher", + event: "StockPairedCreatorInitialBuy", + where: eventBlockFilter("StockV2Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV2EthCoordinator", + event: "StockPairedEthTokenLaunched", + where: eventBlockFilter("StockV2EthCoordinator"), + }, + handleEvent, +); + +indexer.onEvent( + { + contract: "StockV3Launcher", + event: "StockPairedTokenLaunched", + where: eventBlockFilter("StockV3Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV3Launcher", + event: "StockPairedLiquidityConfigured", + where: eventBlockFilter("StockV3Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV3Launcher", + event: "StockPairedCreatorInitialBuy", + where: eventBlockFilter("StockV3Launcher"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV3EthCoordinator", + event: "StockPairedEthTokenLaunched", + where: eventBlockFilter("StockV3EthCoordinator"), + }, + handleEvent, +); + +indexer.onEvent( + { + contract: "StockV2V3Hook", + event: "PoolRegistered", + where: eventBlockFilter("StockV2V3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV2V3Hook", + event: "PoolFeeDisclosure", + where: eventBlockFilter("StockV2V3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV2V3Hook", + event: "QuoteSwapFeesAccrued", + where: eventBlockFilter("StockV2V3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV2V3Hook", + event: "CreatorFeesClaimed", + where: eventBlockFilter("StockV2V3Hook"), + }, + handleEvent, +); +indexer.onEvent( + { + contract: "StockV2V3Hook", + event: "LauncherFeesClaimed", + where: eventBlockFilter("StockV2V3Hook"), + }, + handleEvent, +); +indexer.contractRegister( + { + contract: "StockV2V3RewardVaultFactory", + event: "QuoteAssetFeeSplitVaultDeployed", + where: eventBlockFilter("StockV2V3RewardVaultFactory"), + }, + async ({ event, context }) => { + context.chain.StockV2V3RewardVault.add(event.params.vault); + }, +); +indexer.onEvent( + { + contract: "StockV2V3RewardVaultFactory", + event: "QuoteAssetFeeSplitVaultDeployed", + where: eventBlockFilter("StockV2V3RewardVaultFactory"), + }, + handleEvent, +); +indexer.onEvent( + { contract: "StockV2V3RewardVault", event: "PayoutAddressUpdated" }, + handleEvent, +); +indexer.onEvent( + { contract: "StockV2V3RewardVault", event: "BeneficiaryFeesClaimed" }, + handleEvent, +); diff --git a/indexer/src/lib/deployment-identity.ts b/indexer/src/lib/deployment-identity.ts new file mode 100644 index 00000000..81814789 --- /dev/null +++ b/indexer/src/lib/deployment-identity.ts @@ -0,0 +1,100 @@ +export const DEFAULT_DEPLOYMENT_LABEL = "development-unverified"; +export const DEFAULT_SOURCE_COMMIT = "0".repeat(40); +export const DEFAULT_ARTIFACT_SHA256 = `0x${"00".repeat(32)}`; + +export type DeploymentEnvironment = Readonly<{ + ENVIO_DEPLOYMENT_LABEL?: string; + ENVIO_SOURCE_COMMIT?: string; + ENVIO_CONFIG_SHA256?: string; + ENVIO_SCHEMA_SHA256?: string; + ENVIO_HANDLER_SHA256?: string; + ENVIO_SOURCE_REGISTRY_SHA256?: string; + ENVIO_EVENT_SET_SHA256?: string; + ENVIO_EVENT_COUNT?: string; +}>; + +const DEPLOYMENT_LABEL_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/; +const SOURCE_COMMIT_PATTERN = /^[0-9a-f]{40}$/; +const SHA256_PATTERN = /^0x[0-9a-f]{64}$/; + +export type DeploymentIdentity = Readonly<{ + deployment: string; + sourceCommit: string; + configSha256: string; + schemaSha256: string; + handlerSha256: string; + sourceRegistrySha256: string; + eventSetSha256: string; + eventCount: number; +}>; + +export const DEFAULT_DEPLOYMENT_IDENTITY: DeploymentIdentity = Object.freeze({ + deployment: DEFAULT_DEPLOYMENT_LABEL, + sourceCommit: DEFAULT_SOURCE_COMMIT, + configSha256: DEFAULT_ARTIFACT_SHA256, + schemaSha256: DEFAULT_ARTIFACT_SHA256, + handlerSha256: DEFAULT_ARTIFACT_SHA256, + sourceRegistrySha256: DEFAULT_ARTIFACT_SHA256, + eventSetSha256: DEFAULT_ARTIFACT_SHA256, + eventCount: 0, +}); + +export function deploymentLabelFromEnvironment( + environment: DeploymentEnvironment = process.env, +): string { + const deploymentLabel = environment.ENVIO_DEPLOYMENT_LABEL; + return deploymentLabel !== undefined && + DEPLOYMENT_LABEL_PATTERN.test(deploymentLabel) + ? deploymentLabel + : DEFAULT_DEPLOYMENT_LABEL; +} + +export function deploymentIdentityFromEnvironment( + environment: DeploymentEnvironment = process.env, +): DeploymentIdentity { + const deployment = deploymentLabelFromEnvironment(environment); + const sourceCommit = environment.ENVIO_SOURCE_COMMIT; + const configSha256 = environment.ENVIO_CONFIG_SHA256; + const schemaSha256 = environment.ENVIO_SCHEMA_SHA256; + const handlerSha256 = environment.ENVIO_HANDLER_SHA256; + const sourceRegistrySha256 = environment.ENVIO_SOURCE_REGISTRY_SHA256; + const eventSetSha256 = environment.ENVIO_EVENT_SET_SHA256; + const eventCount = environment.ENVIO_EVENT_COUNT; + if ( + deployment === DEFAULT_DEPLOYMENT_LABEL || + sourceCommit === undefined || + !SOURCE_COMMIT_PATTERN.test(sourceCommit) || + sourceCommit === DEFAULT_SOURCE_COMMIT || + configSha256 === undefined || + !SHA256_PATTERN.test(configSha256) || + configSha256 === DEFAULT_ARTIFACT_SHA256 || + schemaSha256 === undefined || + !SHA256_PATTERN.test(schemaSha256) || + schemaSha256 === DEFAULT_ARTIFACT_SHA256 || + handlerSha256 === undefined || + !SHA256_PATTERN.test(handlerSha256) || + handlerSha256 === DEFAULT_ARTIFACT_SHA256 || + sourceRegistrySha256 === undefined || + !SHA256_PATTERN.test(sourceRegistrySha256) || + sourceRegistrySha256 === DEFAULT_ARTIFACT_SHA256 || + eventSetSha256 === undefined || + !SHA256_PATTERN.test(eventSetSha256) || + eventSetSha256 === DEFAULT_ARTIFACT_SHA256 || + eventCount === undefined || + !/^[1-9]\d*$/.test(eventCount) || + !Number.isSafeInteger(Number(eventCount)) || + Number(eventCount) > 10_000 + ) { + return DEFAULT_DEPLOYMENT_IDENTITY; + } + return Object.freeze({ + deployment, + sourceCommit, + configSha256, + schemaSha256, + handlerSha256, + sourceRegistrySha256, + eventSetSha256, + eventCount: Number(eventCount), + }); +} diff --git a/indexer/src/lib/ids.ts b/indexer/src/lib/ids.ts new file mode 100644 index 00000000..a25c5a57 --- /dev/null +++ b/indexer/src/lib/ids.ts @@ -0,0 +1,112 @@ +export type CandidateOccurrenceIdentity = { + chainId: number; + blockHash: string; + transactionHash: string; + blockGlobalLogIndex: number; +}; + +export type DownstreamLogicalIdentity = { + chainId: number; + transactionHash: string; + receiptLogOrdinal: number; +}; + +export function candidateOccurrenceId( + identity: CandidateOccurrenceIdentity, +): string; +export function candidateOccurrenceId( + chainId: number, + blockHash: string, + transactionHash: string, + blockGlobalLogIndex: number, +): string; +export function candidateOccurrenceId( + identityOrChainId: CandidateOccurrenceIdentity | number, + blockHash?: string, + transactionHash?: string, + blockGlobalLogIndex?: number, +): string { + const identity = + typeof identityOrChainId === "number" + ? { + chainId: identityOrChainId, + blockHash: blockHash ?? "", + transactionHash: transactionHash ?? "", + blockGlobalLogIndex: blockGlobalLogIndex ?? Number.NaN, + } + : identityOrChainId; + + assertNonNegativeInteger("chainId", identity.chainId); + assertUint32( + "block-global log index", + identity.blockGlobalLogIndex, + ); + assertHash("block hash", identity.blockHash); + assertHash("transaction hash", identity.transactionHash); + + return [ + identity.chainId, + identity.blockHash.toLowerCase(), + identity.transactionHash.toLowerCase(), + identity.blockGlobalLogIndex, + ].join(":"); +} + +export function downstreamLogicalEventId( + identity: DownstreamLogicalIdentity, +): string { + return downstreamLogicalId( + identity.chainId, + identity.transactionHash, + identity.receiptLogOrdinal, + ); +} + +export function downstreamLogicalId( + chainId: number, + transactionHash: string, + receiptLogOrdinal: number | undefined, +): string { + assertNonNegativeInteger("chainId", chainId); + if (receiptLogOrdinal === undefined) { + throw new TypeError( + "receipt-local ordinal must be supplied by the downstream verifier", + ); + } + assertUint32("receipt-local ordinal", receiptLogOrdinal); + assertHash("transaction hash", transactionHash); + + return [chainId, transactionHash.toLowerCase(), receiptLogOrdinal].join(":"); +} + +export function launchEntityId( + chainId: number, + releaseVersion: string, + launchHash: string, +): string { + assertNonNegativeInteger("chainId", chainId); + return `${chainId}:${releaseVersion}:${launchHash.toLowerCase()}`; +} + +export function poolEntityId(chainId: number, poolId: string): string { + assertNonNegativeInteger("chainId", chainId); + return `${chainId}:${poolId.toLowerCase()}`; +} + +function assertNonNegativeInteger(name: string, value: number): void { + if (!Number.isSafeInteger(value) || value < 0) { + throw new RangeError(`${name} must be a non-negative safe integer`); + } +} + +function assertUint32(name: string, value: number): void { + if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff_ffff) { + throw new RangeError(`${name} must be an unsigned 32-bit integer`); + } +} + +function assertHash(name: string, value: string): void { + if (!/^0x[0-9a-fA-F]{64}$/.test(value)) { + throw new TypeError(`${name} must be a 32-byte hexadecimal value`); + } +} diff --git a/indexer/src/lib/payload-hash.ts b/indexer/src/lib/payload-hash.ts new file mode 100644 index 00000000..8fe72f96 --- /dev/null +++ b/indexer/src/lib/payload-hash.ts @@ -0,0 +1,101 @@ +import { + encodeAbiParameters, + encodeEventTopics, + keccak256, + type AbiEvent, + type AbiParameter, + type Hex, +} from "viem"; + +export type EncodedEventPayload = { + topics: readonly Hex[]; + data: Hex; + payloadHash: Hex; +}; + +export function encodeEventPayload( + eventAbi: AbiEvent, + params: Readonly>, +): EncodedEventPayload { + const encodedTopics = encodeEventTopics({ + abi: [eventAbi], + eventName: eventAbi.name, + args: params, + }); + if (!Array.isArray(encodedTopics)) { + throw new TypeError("event topic encoding did not produce a topic array"); + } + const topics = encodedTopics as readonly Hex[]; + const nonIndexedInputs = eventAbi.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, + ); + const nonIndexedValues = nonIndexedInputs.map((input) => { + if (input.name === undefined || input.name.length === 0) { + throw new TypeError("configured event inputs must be named"); + } + return params[input.name]; + }); + const data = encodeAbiParameters( + nonIndexedInputs as readonly AbiParameter[], + nonIndexedValues, + ); + const payloadHash = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [topics, data], + ), + ); + assertEventPayloadEncoding(topics, data, payloadHash); + + return { + topics, + data, + payloadHash, + }; +} + +export function canonicalPayloadJson(value: unknown): string { + return JSON.stringify(canonicalize(value)); +} + +function canonicalize(value: unknown): unknown { + if (typeof value === "bigint") { + return value.toString(); + } + if (Array.isArray(value)) { + return value.map(canonicalize); + } + if ( + typeof value === "string" && + /^0x(?:[0-9a-fA-F]{2})*$/.test(value) + ) { + return value.toLowerCase(); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => [key, canonicalize(item)]), + ); + } + return value; +} + +function assertEventPayloadEncoding( + topics: readonly Hex[], + data: Hex, + payloadHash: Hex, +): void { + if ( + topics.length === 0 || + topics.some((topic) => !/^0x[0-9a-fA-F]{64}$/.test(topic)) + ) { + throw new TypeError("event topics must be non-empty 32-byte hex values"); + } + if (!/^0x(?:[0-9a-fA-F]{2})*$/.test(data)) { + throw new TypeError("event data must be canonical even-length hexadecimal"); + } + if (!/^0x[0-9a-fA-F]{64}$/.test(payloadHash)) { + throw new TypeError("payload hash must be a 32-byte hexadecimal value"); + } +} diff --git a/indexer/src/lib/provenance.ts b/indexer/src/lib/provenance.ts new file mode 100644 index 00000000..6bf37fc2 --- /dev/null +++ b/indexer/src/lib/provenance.ts @@ -0,0 +1,67 @@ +import type { EvmEvent } from "envio"; + +import { candidateOccurrenceId } from "./ids.js"; + +export type EventProvenance = { + id: string; + downstreamLogicalId: undefined; + receiptLogOrdinal: undefined; + chainId: number; + blockNumber: bigint; + blockHash: string; + blockTimestamp: bigint; + transactionHash: string; + transactionIndex: bigint; + blockGlobalLogIndex: bigint; + sourceAddress: string; +}; + +export function eventProvenance(event: EvmEvent): EventProvenance { + const blockHash = event.block.hash.toLowerCase(); + const transactionHash = event.transaction.hash.toLowerCase(); + const transactionIndex = uint32Number( + event.transaction.transactionIndex, + "transaction index", + ); + const blockGlobalLogIndex = uint32Number( + event.logIndex, + "block-global log index", + ); + + return { + id: candidateOccurrenceId({ + chainId: event.chainId, + blockHash, + transactionHash, + blockGlobalLogIndex, + }), + downstreamLogicalId: undefined, + receiptLogOrdinal: undefined, + chainId: event.chainId, + blockNumber: BigInt(event.block.number), + blockHash, + blockTimestamp: BigInt(event.block.timestamp), + transactionHash, + transactionIndex: BigInt(transactionIndex), + blockGlobalLogIndex: BigInt(blockGlobalLogIndex), + sourceAddress: lowerAddress(event.srcAddress), + }; +} + +function uint32Number(value: number, name: string): number { + if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff_ffff) { + throw new RangeError(`${name} must be an unsigned 32-bit integer`); + } + return value; +} + +export function lower(value: string): string { + return value.toLowerCase(); +} + +export function lowerAddress(value: string): string { + if (!/^0x[0-9a-fA-F]{40}$/.test(value)) { + throw new TypeError("address must be a 20-byte hexadecimal value"); + } + return value.toLowerCase(); +} diff --git a/indexer/src/lib/release-map.ts b/indexer/src/lib/release-map.ts new file mode 100644 index 00000000..53ea03aa --- /dev/null +++ b/indexer/src/lib/release-map.ts @@ -0,0 +1,162 @@ +export type ReleaseIdentity = { + model: string; + releaseVersion: string; +}; + +export type SourceRegistryEntry = { + contractName: string; + address: `0x${string}`; + startBlock: number; +}; + +export const SOURCE_REGISTRY = [ + { + contractName: "ClassicV2Hook", + address: "0x025a386eaa79f6067d29848fd05ccc71beab20cc", + startBlock: 25_624_130, + }, + { + contractName: "ClassicV2Launcher", + address: "0xd240d06f8586eb799f20056054e5b527405e6bad", + startBlock: 25_624_131, + }, + { + contractName: "ClassicV3RewardVaultFactory", + address: "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + startBlock: 25_639_538, + }, + { + contractName: "ClassicV3VestingWalletFactory", + address: "0xde21b9c0cc0afdb9be20e8236113f066bb8c66f4", + startBlock: 25_639_564, + }, + { + contractName: "ClassicV3Hook", + address: "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc", + startBlock: 25_639_591, + }, + { + contractName: "ClassicV3Launcher", + address: "0xc3bd04aac2fb2ba58efd7eb673e544e0b80de770", + startBlock: 25_639_596, + }, + { + contractName: "StockV1Launcher", + address: "0x195750f33cad5ef2df857a53226b421297a1e79e", + startBlock: 25_637_469, + }, + { + contractName: "StockV1EthCoordinator", + address: "0xfa5f17389ca28d071781d59750b32c842ab6a54b", + startBlock: 25_637_469, + }, + { + contractName: "StockV1Hook", + address: "0x7773d183fe7b60d4f1885047fa42b815a62fe0cc", + startBlock: 25_637_469, + }, + { + contractName: "StockV1RewardVaultFactory", + address: "0xd430d9162c153afdf9e4caca6d2317e72a044441", + startBlock: 25_637_469, + }, + { + contractName: "StockV2Launcher", + address: "0x5ea6be24838061ba45dbe8d82de1b267dc240daf", + startBlock: 25_640_338, + }, + { + contractName: "StockV2EthCoordinator", + address: "0xfb9e1034df6161088e8f358502b19e7515c30fd2", + startBlock: 25_640_338, + }, + { + contractName: "StockV2V3Hook", + address: "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + startBlock: 25_640_338, + }, + { + contractName: "StockV2V3RewardVaultFactory", + address: "0x52d70971d6653a754c29385a2a6f241a481952d4", + startBlock: 25_640_338, + }, + { + contractName: "StockV3Launcher", + address: "0x0573879f72d8ee8b0e5a4ec5e8bcdb2fcab9e51c", + startBlock: 25_642_745, + }, + { + contractName: "StockV3EthCoordinator", + address: "0xddc3abbab0df7f1189310a4f70e7e365796b74e2", + startBlock: 25_642_745, + }, +] as const satisfies readonly SourceRegistryEntry[]; + +export function staticReleaseForContract( + contractName: string, +): ReleaseIdentity | undefined { + if (CLASSIC_V2_CONTRACTS.has(contractName)) { + return { model: "classic", releaseVersion: "classic-v2" }; + } + if (CLASSIC_V3_CONTRACTS.has(contractName)) { + return { model: "classic", releaseVersion: "classic-v3" }; + } + if (STOCK_V1_CONTRACTS.has(contractName)) { + return { model: "stock-paired", releaseVersion: "stock-paired-v1" }; + } + if (STOCK_V2_CONTRACTS.has(contractName)) { + return { model: "stock-paired", releaseVersion: "stock-paired-v2" }; + } + if (STOCK_V3_CONTRACTS.has(contractName)) { + return { model: "stock-paired", releaseVersion: "stock-paired-v3" }; + } + return undefined; +} + +export function resolveRelease(input: { + contractName: string; + poolRelation?: ReleaseIdentity; + vaultRelation?: ReleaseIdentity; +}): ReleaseIdentity { + const staticRelease = staticReleaseForContract(input.contractName); + if (staticRelease !== undefined) { + return staticRelease; + } + if (input.poolRelation !== undefined) { + return input.poolRelation; + } + if (input.vaultRelation !== undefined) { + return input.vaultRelation; + } + return { model: "unresolved", releaseVersion: "unresolved" }; +} + +export function sourceStartBlock(contractName: string): number | undefined { + return SOURCE_REGISTRY.find((source) => source.contractName === contractName) + ?.startBlock; +} + +const CLASSIC_V2_CONTRACTS = new Set([ + "ClassicV2Hook", + "ClassicV2Launcher", +]); +const CLASSIC_V3_CONTRACTS = new Set([ + "ClassicV3Hook", + "ClassicV3Launcher", + "ClassicV3RewardVaultFactory", + "ClassicV3VestingWalletFactory", +]); +const STOCK_V1_CONTRACTS = new Set([ + "StockV1Hook", + "StockV1Launcher", + "StockV1EthCoordinator", + "StockV1RewardVaultFactory", +]); +const STOCK_V2_CONTRACTS = new Set([ + "StockV2Launcher", + "StockV2EthCoordinator", +]); +const STOCK_V3_CONTRACTS = new Set([ + "StockV3Launcher", + "StockV3EthCoordinator", +]); diff --git a/indexer/test/classic-v2.test.ts b/indexer/test/classic-v2.test.ts new file mode 100644 index 00000000..7e676092 --- /dev/null +++ b/indexer/test/classic-v2.test.ts @@ -0,0 +1,202 @@ +import { createTestIndexer } from "envio"; +import { describe, expect, it } from "vitest"; + +const CREATOR = "0x1111111111111111111111111111111111111111"; +const TOKEN = "0x2222222222222222222222222222222222222222"; +const HOOK = "0x025a386eaa79f6067d29848fd05ccc71beab20cc"; +const POSITION_RECIPIENT = "0x3333333333333333333333333333333333333333"; +const POOL_ID = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const LAUNCH_HASH = + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const BLOCK_HASH = + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const TRANSACTION_HASH = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const LAUNCH_ID = `1:classic-v2:${LAUNCH_HASH}`; +const BLOCK_NUMBER = 25_650_000; + +const block = { number: BLOCK_NUMBER, timestamp: 1_800_000_000, hash: BLOCK_HASH }; +const transaction = { hash: TRANSACTION_HASH, transactionIndex: 8 }; + +describe("Classic V2 handlers", () => { + it("assembles shuffled launch facts by release, launch hash, and token", async () => { + const indexer = createTestIndexer(); + + await indexer.process({ + chains: { + 1: { + simulate: [ + { + contract: "ClassicV2Launcher", + event: "MemeLiquidityConfigured", + logIndex: 20, + block, + transaction, + params: { + token: TOKEN, + totalSupply: 1_000_000_000_000_000_000_000_000n, + tokenLiquidityAmount: 900_000_000_000_000_000_000_000n, + lockedTokenDust: 7n, + initialTick: 204_200n, + tickLower: 200_000n, + tickUpper: 210_000n, + lpFeePips: 3_000n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV2Launcher", + event: "MemeCreatorInitialBuy", + logIndex: 21, + block, + transaction, + params: { + creator: CREATOR, + token: TOKEN, + poolId: POOL_ID, + nativeAmount: 600_000_000_000_000n, + tokenAmount: 437_971_781_612_384_114_831_424n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV2Launcher", + event: "MemeTokenLaunched", + logIndex: 22, + block, + transaction, + params: { + creator: CREATOR, + token: TOKEN, + poolId: POOL_ID, + feeHook: HOOK, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 351_734n, + totalSwapFeeBps: 100n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV2Hook", + event: "PoolRegistered", + logIndex: 23, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + creator: CREATOR, + registrar: CREATOR, + totalSwapFeeBps: 100n, + }, + }, + { + contract: "ClassicV2Hook", + event: "PoolFeeDisclosure", + logIndex: 24, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + buySwapFeeBps: 100n, + sellSwapFeeBps: 100n, + launcherFeeBps: 10n, + transferTaxBps: 0n, + lpFeePips: 3_000n, + }, + }, + ], + }, + }, + }); + + const launch = await indexer.Launch.getOrThrow(LAUNCH_ID); + expect(launch).toMatchObject({ + releaseVersion: "classic-v2", + model: "classic", + token: TOKEN, + creator: CREATOR, + poolId: POOL_ID, + hook: HOOK, + totalSupply: 1_000_000_000_000_000_000_000_000n, + initialBuyQuoteAmount: 600_000_000_000_000n, + initialBuyTokenAmount: 437_971_781_612_384_114_831_424n, + hasLaunchEvent: true, + hasLiquidityEvent: true, + hasInitialBuyEvent: true, + provenanceValid: true, + isComplete: true, + }); + }); + + it("does not manufacture a complete launch from a mismatched token", async () => { + const indexer = createTestIndexer(); + const mismatchedToken = "0x4444444444444444444444444444444444444444"; + + await indexer.process({ + chains: { + 1: { + simulate: [ + { + contract: "ClassicV2Launcher", + event: "MemeTokenLaunched", + logIndex: 30, + block, + transaction, + params: { + creator: CREATOR, + token: TOKEN, + poolId: POOL_ID, + feeHook: HOOK, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 1n, + totalSwapFeeBps: 100n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV2Launcher", + event: "MemeLiquidityConfigured", + logIndex: 31, + block, + transaction, + params: { + token: mismatchedToken, + totalSupply: 1n, + tokenLiquidityAmount: 1n, + lockedTokenDust: 0n, + initialTick: 0n, + tickLower: -1n, + tickUpper: 1n, + lpFeePips: 3_000n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV2Launcher", + event: "MemeCreatorInitialBuy", + logIndex: 32, + block, + transaction, + params: { + creator: CREATOR, + token: TOKEN, + poolId: POOL_ID, + nativeAmount: 0n, + tokenAmount: 0n, + launchHash: LAUNCH_HASH, + }, + }, + ], + }, + }, + }); + + const launch = await indexer.Launch.getOrThrow(LAUNCH_ID); + expect(launch.provenanceValid).toBe(false); + expect(launch.isComplete).toBe(false); + expect(launch.token).toBe(TOKEN); + }); +}); diff --git a/indexer/test/classic-v3.test.ts b/indexer/test/classic-v3.test.ts new file mode 100644 index 00000000..39bf05bb --- /dev/null +++ b/indexer/test/classic-v3.test.ts @@ -0,0 +1,475 @@ +import { createTestIndexer } from "envio"; +import { describe, expect, it } from "vitest"; + +const DEPLOYER = "0x1111111111111111111111111111111111111111"; +const TOKEN = "0x2222222222222222222222222222222222222222"; +const HOOK = "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc"; +const VAULT = "0x3333333333333333333333333333333333333333"; +const CONFLICTING_VAULT = + "0x6666666666666666666666666666666666666666"; +const CUSTODY = "0x4444444444444444444444444444444444444444"; +const POSITION_RECIPIENT = "0x5555555555555555555555555555555555555555"; +const POOL_ID = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const CONFIGURATION_HASH = + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const CUSTODY_CONFIGURATION_HASH = + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const LAUNCH_HASH = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const BLOCK_HASH = + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; +const TRANSACTION_HASH = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; +const BLOCK_NUMBER = 25_650_010; + +const block = { number: BLOCK_NUMBER, timestamp: 1_800_000_010, hash: BLOCK_HASH }; +const transaction = { hash: TRANSACTION_HASH, transactionIndex: 2 }; + +describe("Classic V3 handlers", () => { + it("registers a vault in the factory block and preserves custody mode as uint8", async () => { + const indexer = createTestIndexer(); + const result = await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: [ + { + contract: "ClassicV3RewardVaultFactory", + event: "ClassicRewardVaultDeployed", + logIndex: 40, + block, + transaction, + params: { + vault: VAULT, + poolId: POOL_ID, + feeHook: HOOK, + salt: CONFIGURATION_HASH, + configurationHash: CONFIGURATION_HASH, + }, + }, + { + contract: "ClassicV3RewardVault", + event: "CreatorFeesCheckpointed", + srcAddress: VAULT, + logIndex: 41, + block, + transaction, + params: { + poolId: POOL_ID, + configurationEpoch: 1n, + amount: 9_007_199_254_740_993n, + totalCreatorFeesReceived: 90_071_992_547_409_931n, + }, + }, + { + contract: "ClassicV3Launcher", + event: "MemeLiquidityConfiguredV2", + logIndex: 42, + block, + transaction, + params: { + token: TOKEN, + totalSupply: 1_000_000n, + tokenLiquidityAmount: 900_000n, + lockedTokenDust: 0n, + initialTick: 0n, + tickLower: -10n, + tickUpper: 10n, + lpFeePips: 3_000n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3Launcher", + event: "MemeCreatorInitialBuyV2", + logIndex: 43, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + nativeAmount: 999n, + tokenAmount: 888n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3Launcher", + event: "MemeCreatorInitialBuyCustodyV2", + logIndex: 44, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + custody: CUSTODY, + mode: 2n, + durationDays: 365n, + cliffDays: 30n, + configurationHash: CUSTODY_CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3Launcher", + event: "MemeTokenLaunchedV2", + logIndex: 45, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + feeHook: HOOK, + rewardVault: VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 42n, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + rewardConfigurationHash: CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3Hook", + event: "PoolRegistered", + logIndex: 46, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + rewardVault: VAULT, + registrar: DEPLOYER, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + rewardConfigurationHash: CONFIGURATION_HASH, + }, + }, + { + contract: "ClassicV3Hook", + event: "PoolFeeDisclosure", + logIndex: 47, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + rewardVault: VAULT, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + buyCreatorFeeBps: 90n, + sellCreatorFeeBps: 90n, + launcherFeeBps: 10n, + transferTaxBps: 0n, + lpFeePips: 3_000n, + }, + }, + ], + }, + }, + }); + + expect(result.changes[0]?.addresses?.sets).toContainEqual({ + contract: "ClassicV3RewardVault", + address: VAULT, + }); + expect((await indexer.RewardVault.getOrThrow(VAULT)).releaseVersion).toBe( + "classic-v3", + ); + expect((await indexer.RewardCheckpoint.getAll())[0]).toMatchObject({ + vault: VAULT, + poolId: POOL_ID, + configurationEpoch: 1n, + amount: 9_007_199_254_740_993n, + totalCreatorFeesReceived: 90_071_992_547_409_931n, + downstreamLogicalId: undefined, + receiptLogOrdinal: undefined, + }); + const custody = (await indexer.InitialBuyCustody.getAll())[0]; + expect(custody?.mode).toBe(2); + expect(custody?.mode).not.toBe(2n); + expect( + ( + await indexer.Launch.getOrThrow( + `1:classic-v3:${LAUNCH_HASH}`, + ) + ).isComplete, + ).toBe(true); + }); + + it("marks a vault configuration mismatch as invalid provenance", async () => { + const indexer = createTestIndexer(); + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: [ + { + contract: "ClassicV3Launcher", + event: "MemeTokenLaunchedV2", + logIndex: 60, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + feeHook: HOOK, + rewardVault: VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 42n, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + rewardConfigurationHash: CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3RewardVaultFactory", + event: "ClassicRewardVaultDeployed", + logIndex: 61, + block, + transaction, + params: { + vault: VAULT, + poolId: POOL_ID, + feeHook: HOOK, + salt: CONFIGURATION_HASH, + configurationHash: CUSTODY_CONFIGURATION_HASH, + }, + }, + ], + }, + }, + }); + + expect( + ( + await indexer.Launch.getOrThrow( + `1:classic-v3:${LAUNCH_HASH}`, + ) + ).provenanceValid, + ).toBe(false); + }); + + it("rejects a late hook configuration hash that differs from the launcher", async () => { + const indexer = createTestIndexer(); + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: [ + { + contract: "ClassicV3Launcher", + event: "MemeTokenLaunchedV2", + logIndex: 70, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + feeHook: HOOK, + rewardVault: VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 42n, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + rewardConfigurationHash: CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3Hook", + event: "PoolRegistered", + logIndex: 71, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + rewardVault: VAULT, + registrar: DEPLOYER, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + rewardConfigurationHash: CUSTODY_CONFIGURATION_HASH, + }, + }, + ], + }, + }, + }); + + expect( + await indexer.Launch.getOrThrow( + `1:classic-v3:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + provenanceValid: false, + isComplete: false, + }); + }); + + it.each([ + ["before", 79], + ["after", 86], + ])( + "invalidates a conflicting same-pool factory vault emitted %s the launcher", + async (_order, factoryLogIndex) => { + const indexer = createTestIndexer(); + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: [ + { + contract: "ClassicV3Launcher", + event: "MemeTokenLaunchedV2", + logIndex: 85, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + feeHook: HOOK, + rewardVault: VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 42n, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + rewardConfigurationHash: CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3RewardVaultFactory", + event: "ClassicRewardVaultDeployed", + logIndex: factoryLogIndex, + block, + transaction, + params: { + vault: CONFLICTING_VAULT, + poolId: POOL_ID, + feeHook: HOOK, + salt: CONFIGURATION_HASH, + configurationHash: CONFIGURATION_HASH, + }, + }, + { + contract: "ClassicV3Launcher", + event: "MemeCreatorInitialBuyCustodyV2", + logIndex: 84, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + custody: CUSTODY, + mode: 2n, + durationDays: 365n, + cliffDays: 30n, + configurationHash: CUSTODY_CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3Hook", + event: "PoolFeeDisclosure", + logIndex: 81, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + rewardVault: VAULT, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + buyCreatorFeeBps: 90n, + sellCreatorFeeBps: 90n, + launcherFeeBps: 10n, + transferTaxBps: 0n, + lpFeePips: 3_000n, + }, + }, + { + contract: "ClassicV3Launcher", + event: "MemeLiquidityConfiguredV2", + logIndex: 82, + block, + transaction, + params: { + token: TOKEN, + totalSupply: 1_000_000n, + tokenLiquidityAmount: 900_000n, + lockedTokenDust: 0n, + initialTick: 0n, + tickLower: -10n, + tickUpper: 10n, + lpFeePips: 3_000n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "ClassicV3Hook", + event: "PoolRegistered", + logIndex: 80, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + rewardVault: VAULT, + registrar: DEPLOYER, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + rewardConfigurationHash: CONFIGURATION_HASH, + }, + }, + { + contract: "ClassicV3Launcher", + event: "MemeCreatorInitialBuyV2", + logIndex: 83, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + nativeAmount: 999n, + tokenAmount: 888n, + launchHash: LAUNCH_HASH, + }, + }, + ], + }, + }, + }); + + expect( + await indexer.RewardVault.getOrThrow(CONFLICTING_VAULT), + ).toMatchObject({ + poolId: POOL_ID, + hook: HOOK, + }); + expect( + await indexer.Launch.getOrThrow( + `1:classic-v3:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + provenanceValid: false, + isComplete: false, + }); + }, + ); +}); diff --git a/indexer/test/deployment-identity.test.ts b/indexer/test/deployment-identity.test.ts new file mode 100644 index 00000000..296755da --- /dev/null +++ b/indexer/test/deployment-identity.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from "vitest"; + +import { + DEFAULT_DEPLOYMENT_IDENTITY, + DEFAULT_DEPLOYMENT_LABEL, + deploymentIdentityFromEnvironment, + deploymentLabelFromEnvironment, +} from "../src/lib/deployment-identity.js"; + +const reviewedEnvironment = { + ENVIO_DEPLOYMENT_LABEL: "production-reviewed-2026-07-31", + ENVIO_SOURCE_COMMIT: "1".repeat(40), + ENVIO_CONFIG_SHA256: `0x${"22".repeat(32)}`, + ENVIO_SCHEMA_SHA256: `0x${"33".repeat(32)}`, + ENVIO_HANDLER_SHA256: `0x${"44".repeat(32)}`, + ENVIO_SOURCE_REGISTRY_SHA256: `0x${"55".repeat(32)}`, + ENVIO_EVENT_SET_SHA256: `0x${"66".repeat(32)}`, + ENVIO_EVENT_COUNT: "51", +} as const; + +describe("deployment identity", () => { + it("defaults to an explicitly unverified development identity", () => { + expect(deploymentLabelFromEnvironment({})).toBe( + DEFAULT_DEPLOYMENT_LABEL, + ); + }); + + it("accepts an explicit reviewed deployment label without a code change", () => { + expect( + deploymentLabelFromEnvironment({ + ENVIO_DEPLOYMENT_LABEL: "production-reviewed-2026-07-31", + }), + ).toBe("production-reviewed-2026-07-31"); + }); + + it("accepts only a complete reviewed artifact identity", () => { + expect(deploymentIdentityFromEnvironment(reviewedEnvironment)).toEqual({ + deployment: reviewedEnvironment.ENVIO_DEPLOYMENT_LABEL, + sourceCommit: reviewedEnvironment.ENVIO_SOURCE_COMMIT, + configSha256: reviewedEnvironment.ENVIO_CONFIG_SHA256, + schemaSha256: reviewedEnvironment.ENVIO_SCHEMA_SHA256, + handlerSha256: reviewedEnvironment.ENVIO_HANDLER_SHA256, + sourceRegistrySha256: + reviewedEnvironment.ENVIO_SOURCE_REGISTRY_SHA256, + eventSetSha256: reviewedEnvironment.ENVIO_EVENT_SET_SHA256, + eventCount: 51, + }); + }); + + it.each([ + ["missing source commit", { ENVIO_SOURCE_COMMIT: undefined }], + ["zero source commit", { ENVIO_SOURCE_COMMIT: "0".repeat(40) }], + ["uppercase commit", { ENVIO_SOURCE_COMMIT: "A".repeat(40) }], + ["short hash", { ENVIO_HANDLER_SHA256: "0x12" }], + ["uppercase hash", { ENVIO_CONFIG_SHA256: `0x${"AA".repeat(32)}` }], + ["zero config hash", { ENVIO_CONFIG_SHA256: `0x${"00".repeat(32)}` }], + ["zero schema hash", { ENVIO_SCHEMA_SHA256: `0x${"00".repeat(32)}` }], + ["zero handler hash", { ENVIO_HANDLER_SHA256: `0x${"00".repeat(32)}` }], + [ + "zero source registry hash", + { ENVIO_SOURCE_REGISTRY_SHA256: `0x${"00".repeat(32)}` }, + ], + ["zero event set hash", { ENVIO_EVENT_SET_SHA256: `0x${"00".repeat(32)}` }], + ["zero event count", { ENVIO_EVENT_COUNT: "0" }], + ["noncanonical event count", { ENVIO_EVENT_COUNT: "051" }], + ])("fails the full identity closed for %s", (_name, override) => { + expect( + deploymentIdentityFromEnvironment({ + ...reviewedEnvironment, + ...override, + }), + ).toBe(DEFAULT_DEPLOYMENT_IDENTITY); + }); + + it.each([ + "", + " production-reviewed-2026-07-31", + "production reviewed", + "PRODUCTION-REVIEWED", + "production/reviewed", + "a".repeat(65), + ])("fails closed for invalid deployment label %j", (deploymentLabel) => { + expect( + deploymentLabelFromEnvironment({ + ENVIO_DEPLOYMENT_LABEL: deploymentLabel, + }), + ).toBe(DEFAULT_DEPLOYMENT_LABEL); + }); +}); diff --git a/indexer/test/ids.test.ts b/indexer/test/ids.test.ts new file mode 100644 index 00000000..18635916 --- /dev/null +++ b/indexer/test/ids.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from "vitest"; + +import { + candidateOccurrenceId, + downstreamLogicalEventId, +} from "../src/lib/ids.js"; + +describe("candidateOccurrenceId", () => { + it("uses the fork placement and block-global log index", () => { + expect( + candidateOccurrenceId( + { + chainId: 1, + blockHash: + "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + transactionHash: + "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + blockGlobalLogIndex: 17, + }, + ), + ).toBe( + "1:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:17", + ); + }); + + it("creates another candidate when the same transaction is re-mined", () => { + const first = candidateOccurrenceId({ + chainId: 1, + blockHash: `0x${"11".repeat(32)}`, + transactionHash: `0x${"22".repeat(32)}`, + blockGlobalLogIndex: 9, + }); + const reMined = candidateOccurrenceId({ + chainId: 1, + blockHash: `0x${"33".repeat(32)}`, + transactionHash: `0x${"22".repeat(32)}`, + blockGlobalLogIndex: 4, + }); + + expect(reMined).not.toBe(first); + }); + + it("preserves the complete uint32 log-index domain", () => { + const common = { + chainId: 1, + blockHash: `0x${"11".repeat(32)}`, + transactionHash: `0x${"22".repeat(32)}`, + }; + + expect( + candidateOccurrenceId({ + ...common, + blockGlobalLogIndex: 0xffff_ffff, + }), + ).toBe( + `${common.chainId}:${common.blockHash}:${common.transactionHash}:4294967295`, + ); + expect(() => + candidateOccurrenceId({ + ...common, + blockGlobalLogIndex: 0x1_0000_0000, + }), + ).toThrow(/unsigned 32-bit integer/i); + }); + + it("rejects malformed hashes and invalid block-global indexes", () => { + expect(() => + candidateOccurrenceId({ + chainId: 1, + blockHash: "0x1234", + transactionHash: `0x${"22".repeat(32)}`, + blockGlobalLogIndex: 0, + }), + ).toThrow(/block hash/i); + expect(() => + candidateOccurrenceId({ + chainId: 1, + blockHash: `0x${"11".repeat(32)}`, + transactionHash: `0x${"22".repeat(32)}`, + blockGlobalLogIndex: -1, + }), + ).toThrow(/log index/i); + }); +}); + +describe("downstreamLogicalEventId", () => { + it("depends on a verified receipt-local ordinal rather than block placement", () => { + const transactionHash = `0x${"44".repeat(32)}`; + + expect( + downstreamLogicalEventId({ + chainId: 1, + transactionHash, + receiptLogOrdinal: 3, + }), + ).toBe( + `1:${transactionHash}:3`, + ); + }); + + it("requires the downstream worker to supply a receipt-local ordinal", () => { + expect(() => + downstreamLogicalEventId({ + chainId: 1, + transactionHash: `0x${"44".repeat(32)}`, + receiptLogOrdinal: undefined as unknown as number, + }), + ).toThrow(/receipt-local ordinal/i); + }); +}); diff --git a/indexer/test/provenance.test.ts b/indexer/test/provenance.test.ts new file mode 100644 index 00000000..d0a38c26 --- /dev/null +++ b/indexer/test/provenance.test.ts @@ -0,0 +1,48 @@ +import type { EvmEvent } from "envio"; +import { describe, expect, it } from "vitest"; + +import { eventProvenance } from "../src/lib/provenance.js"; + +const HASH_A = `0x${"11".repeat(32)}`; +const HASH_B = `0x${"22".repeat(32)}`; +const ADDRESS = `0x${"33".repeat(20)}`; + +function eventWithPlacement( + transactionIndex: number, + logIndex: number, +): EvmEvent { + return { + chainId: 1, + block: { + number: 1n, + hash: HASH_A, + timestamp: 2n, + }, + transaction: { + hash: HASH_B, + transactionIndex, + }, + logIndex, + srcAddress: ADDRESS, + } as unknown as EvmEvent; +} + +describe("event provenance placement", () => { + it("stores the complete uint32 placement as exact bigint values", () => { + expect( + eventProvenance(eventWithPlacement(0xffff_ffff, 0xffff_ffff)), + ).toMatchObject({ + transactionIndex: 4_294_967_295n, + blockGlobalLogIndex: 4_294_967_295n, + }); + }); + + it.each([ + [0x1_0000_0000, 0], + [0, 0x1_0000_0000], + ])("rejects placement outside uint32", (transactionIndex, logIndex) => { + expect(() => + eventProvenance(eventWithPlacement(transactionIndex, logIndex)), + ).toThrow(/unsigned 32-bit integer/i); + }); +}); diff --git a/indexer/test/release-candidate.test.ts b/indexer/test/release-candidate.test.ts new file mode 100644 index 00000000..a5d34841 --- /dev/null +++ b/indexer/test/release-candidate.test.ts @@ -0,0 +1,576 @@ +import { execFileSync } from "node:child_process"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +// The release gate is deliberately a standalone Node script rather than indexer runtime code. +// @ts-expect-error The checked-in mjs script has no declaration file by design. +import { IDENTITY_KEYS, INVENTORY_QUERY, LAUNCH_FIELDS, STABLE_LAUNCH_FIELDS, assertFrozenBaseline, auditCandidate, endpointIdFromUrl, localIdentity, parseBaseline, parseCandidateIdentity, snapshotBaseline } from "../scripts/release-candidate.mjs"; + +const ROOT = path.resolve(import.meta.dirname, ".."); +const REPOSITORY_ROOT = path.resolve(ROOT, ".."); +const SCRIPT = path.join(ROOT, "scripts/release-candidate.mjs"); +const SOURCE_COMMIT = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: REPOSITORY_ROOT, + encoding: "utf8", +}).trim(); +const MIRROR_COMMIT = "2".repeat(40); +const BASELINE_ENDPOINT = "https://indexer.hyperindex.xyz/base001/v1/graphql"; +const CANDIDATE_ENDPOINT = "https://indexer.hyperindex.xyz/cand001/v1/graphql"; +const CAPTURED_AT = "2026-08-01T10:00:00.000Z"; +const RELEASES = [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const; + +type JsonRecord = Record; +type FixtureOptions = Readonly<{ + identity?: JsonRecord; + rows?: JsonRecord[]; + progressBlock?: number; + deploymentLabel?: string; + mutateSecondInventoryRead?: (rows: JsonRecord[]) => JsonRecord[]; +}>; + +function hex(bytes: number, value: number): string { + return `0x${value.toString(16).padStart(bytes * 2, "0")}`; +} + +function launchRow(releaseVersion: (typeof RELEASES)[number], index: number): JsonRecord { + const model = releaseVersion.startsWith("classic") ? "classic" : "stock-paired"; + const stock = model === "stock-paired"; + return { + id: `1:${releaseVersion}:${hex(32, index)}`, + chainId: 1, + model, + releaseVersion, + launchHash: hex(32, index), + token: hex(20, 100 + index), + creator: hex(20, 200 + index), + quoteAsset: stock ? hex(20, 300 + index) : null, + poolId: hex(32, 400 + index), + hook: hex(20, 500 + index), + rewardVault: releaseVersion === "classic-v2" ? null : hex(20, 600 + index), + positionRecipient: stock ? null : hex(20, 700 + index), + positionTokenId: stock ? null : String(800 + index), + totalSwapFeeBps: stock ? null : 100, + buySwapFeeBps: releaseVersion === "classic-v3" ? 100 : null, + sellSwapFeeBps: releaseVersion === "classic-v3" ? 100 : null, + rewardConfigurationHash: releaseVersion === "classic-v2" ? null : hex(32, 900 + index), + quoteConfigurationHash: stock ? hex(32, 1_000 + index) : null, + totalSupply: String(1_000_000 + index), + tokenLiquidityAmount: String(900_000 + index), + lockedTokenDust: String(index), + initialTick: 204_200 + index, + tickLower: -887_200, + tickUpper: 204_200 + index, + lpFeePips: stock ? 3_000 : 0, + initialBuyQuoteAmount: String(10_000 + index), + initialBuyTokenAmount: String(20_000 + index), + initialBuyEthAmount: stock ? String(30_000 + index) : null, + launchOccurrenceId: `1:${hex(32, 1_100 + index)}:${hex(32, 1_200 + index)}:1`, + liquidityOccurrenceId: `1:${hex(32, 1_100 + index)}:${hex(32, 1_200 + index)}:2`, + initialBuyOccurrenceId: `1:${hex(32, 1_100 + index)}:${hex(32, 1_200 + index)}:3`, + custodyOccurrenceId: stock + ? `1:${hex(32, 1_100 + index)}:${hex(32, 1_200 + index)}:4` + : null, + coordinatorOccurrenceId: stock + ? `1:${hex(32, 1_100 + index)}:${hex(32, 1_200 + index)}:5` + : null, + hasLaunchEvent: true, + hasLiquidityEvent: true, + hasInitialBuyEvent: true, + hasCustodyEvent: stock, + hasCoordinatorEvent: stock, + hasPoolRegistrationEvent: true, + hasPoolFeeDisclosureEvent: true, + hasRewardVaultFactoryEvent: releaseVersion !== "classic-v2", + provenanceValid: true, + isComplete: true, + updatedBlock: String(900 + index), + }; +} + +function rows(): JsonRecord[] { + return RELEASES.map((release, index) => launchRow(release, index + 1)).sort((left, right) => + String(left.id).localeCompare(String(right.id)), + ); +} + +function state( + identity: JsonRecord | undefined, + progressBlock: number, + deploymentLabel = "production-old", +): JsonRecord { + return { + id: "ethereum-mainnet", + schemaVersion: "1", + deployment: identity?.deployment ?? deploymentLabel, + ...(identity ?? {}), + chainId: 1, + progressBlock: String(progressBlock - 1), + progressBlockHash: hex(32, progressBlock - 1), + progressTimestamp: "1785552707", + progressTransactionHash: hex(32, progressBlock + 1), + progressOccurrenceId: `1:${hex(32, progressBlock - 1)}:${hex(32, progressBlock + 1)}:7`, + }; +} + +function fixtureFetcher(options: FixtureOptions = {}) { + const fixtureRows = options.rows ?? rows(); + const progressBlock = options.progressBlock ?? 1_000; + let inventoryReads = 0; + const anchors: unknown[] = []; + const fetcher = async (_url: string | URL | Request, init?: RequestInit): Promise => { + const body = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + if (body.query.includes("ProgrammableReleaseInventory")) { + inventoryReads += 1; + anchors.push(body.variables.anchorBlock); + const sourceRows = + inventoryReads === 2 && options.mutateSecondInventoryRead + ? options.mutateSecondInventoryRead(fixtureRows.map((row) => ({ ...row }))) + : fixtureRows; + const afterId = String(body.variables.afterId); + const page = sourceRows + .filter((row) => String(row.id) > afterId) + .slice(0, Number(body.variables.first)); + return Response.json({ data: { Launch: page } }); + } + const candidate = body.query.includes("ProgrammableReleaseCandidateProgress"); + return Response.json({ + data: { + _meta: [ + { + chainId: 1, + progressBlock, + bufferBlock: progressBlock, + sourceBlock: progressBlock + 12, + isReady: true, + eventsProcessed: 50, + }, + ], + IndexerState_by_pk: state( + candidate ? options.identity : undefined, + progressBlock, + options.deploymentLabel, + ), + }, + }); + }; + return { fetcher, anchors, inventoryReads: () => inventoryReads }; +} + +async function baseline(fixtureRows = rows()) { + const fixture = fixtureFetcher({ rows: fixtureRows }); + return snapshotBaseline(BASELINE_ENDPOINT, fixture.fetcher, () => new Date(CAPTURED_AT)); +} + +function deployment(identity: JsonRecord): JsonRecord { + return { + "deployment-endpoint-id": "cand001", + "deployment-label": identity.deployment, + "mirror-commit": MIRROR_COMMIT, + }; +} + +function changedValue(field: string, current: unknown): unknown { + if (typeof current === "boolean") return !current; + if (typeof current === "number") return current + 1; + if (field === "chainId") return 2; + if (field === "model") return "stock-paired"; + if (field === "releaseVersion") return "classic-v3"; + if (field === "id") return `${String(current)}-changed`; + if (["token", "creator", "quoteAsset", "hook", "rewardVault", "positionRecipient"].includes(field)) { + return current === null ? hex(20, 9_999) : hex(20, 9_998); + } + if (["launchHash", "poolId", "rewardConfigurationHash", "quoteConfigurationHash"].includes(field)) { + return current === null ? hex(32, 9_999) : hex(32, 9_998); + } + if (current === null) return "1"; + if (typeof current === "string" && /^(?:0|[1-9][0-9]*)$/u.test(current)) { + return String(BigInt(current) + 1n); + } + return `${String(current)}-changed`; +} + +describe("Envio release candidate identity", () => { + it("recomputes the exact eight-key identity from the reviewed checkout", () => { + const identity = JSON.parse( + execFileSync( + process.execPath, + [SCRIPT, "identity", "--source-commit", SOURCE_COMMIT], + { cwd: ROOT, encoding: "utf8" }, + ), + ) as JsonRecord; + + expect(Object.keys(identity)).toEqual(IDENTITY_KEYS); + expect(identity).toEqual({ + deployment: `production-${SOURCE_COMMIT.slice(0, 7)}`, + sourceCommit: SOURCE_COMMIT, + configSha256: + "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + schemaSha256: + "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + handlerSha256: + "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + sourceRegistrySha256: + "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + eventSetSha256: + "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + eventCount: 51, + }); + }); + + it.each([ + ["symbolic commit", "HEAD"], + ["unknown valid-looking commit", "1".repeat(40)], + ["uppercase commit", SOURCE_COMMIT.toUpperCase()], + ])("rejects a %s", (_label, sourceCommit) => { + expect(() => + execFileSync( + process.execPath, + [SCRIPT, "identity", "--source-commit", sourceCommit], + { cwd: ROOT, encoding: "utf8", stdio: "pipe" }, + ), + ).toThrow(); + }); + + it("rejects partial, extra and mistyped candidate identity JSON", () => { + const identity = localIdentity(SOURCE_COMMIT) as JsonRecord; + const partial = { ...identity }; + delete partial.eventCount; + expect(() => parseCandidateIdentity(partial)).toThrow(/exactly/u); + expect(() => parseCandidateIdentity({ ...identity, arbitrary: true })).toThrow(/exactly/u); + expect(() => parseCandidateIdentity({ ...identity, eventCount: "51" })).toThrow(/safe integer/u); + expect(() => + parseCandidateIdentity({ ...identity, handlerSha256: String(identity.handlerSha256).toUpperCase() }), + ).toThrow(/invalid/u); + }); +}); + +describe("Envio endpoint and frozen evidence", () => { + it("accepts only the exact Envio host and deployment endpoint path", () => { + expect(endpointIdFromUrl(CANDIDATE_ENDPOINT, "cand001")).toBe("cand001"); + for (const value of [ + "http://indexer.hyperindex.xyz/cand001/v1/graphql", + "https://evil.example/cand001/v1/graphql", + "https://indexer.hyperindex.xyz:443/cand001/v1/graphql", + "https://user@indexer.hyperindex.xyz/cand001/v1/graphql", + "https://indexer.hyperindex.xyz/cand001/v1/graphql?query=x", + "https://indexer.hyperindex.xyz/cand001/v1/graphql#fragment", + "https://indexer.hyperindex.xyz/cand001/v1/graphql/", + "https://indexer.hyperindex.xyz/short/v1/graphql", + ]) { + expect(() => endpointIdFromUrl(value)).toThrow(/reviewed Envio/u); + } + expect(() => endpointIdFromUrl(CANDIDATE_ENDPOINT, "other01")).toThrow(/reviewed Envio/u); + }); + + it("uses one fixed anchor for both inventory reads and emits deterministic digests", async () => { + const firstFixture = fixtureFetcher(); + const first = await snapshotBaseline( + BASELINE_ENDPOINT, + firstFixture.fetcher, + () => new Date(CAPTURED_AT), + ); + const second = await baseline(); + + expect(first).toEqual(second); + expect(firstFixture.anchors).toEqual(["1000", "1000"]); + expect(firstFixture.inventoryReads()).toBe(2); + expect(INVENTORY_QUERY).toContain("updatedBlock: { _lte: $anchorBlock }"); + expect(first.entries).toHaveLength(5); + expect(Object.keys(first.entries[0] as JsonRecord)).toEqual(LAUNCH_FIELDS); + expect(first.inventory.sha256).toMatch(/^0x[0-9a-f]{64}$/u); + expect(first.digest).toMatch(/^0x[0-9a-f]{64}$/u); + expect(first.deployment).toEqual({ + provider: "envio-cloud", + host: "indexer.hyperindex.xyz", + endpointId: "base001", + deploymentLabel: "production-old", + chainId: 1, + }); + }); + + it("rejects an inventory that changes while the anchored pages are read", async () => { + const fixture = fixtureFetcher({ + mutateSecondInventoryRead: (values) => + values.map((row, index) => (index === 0 ? { ...row, creator: hex(20, 9_999) } : row)), + }); + await expect( + snapshotBaseline(BASELINE_ENDPOINT, fixture.fetcher, () => new Date(CAPTURED_AT)), + ).rejects.toThrow(/changed while reading/u); + }); + + it("rejects baseline tampering across entries, identity, anchor and top-level digest", async () => { + const value = (await baseline()) as JsonRecord; + const entries = value.entries as JsonRecord[]; + expect(() => + parseBaseline({ + ...value, + entries: [{ ...entries[0], creator: hex(20, 9_999) }, ...entries.slice(1)], + }), + ).toThrow(/inventory count or digest/u); + expect(() => + parseBaseline({ + ...value, + deployment: { ...(value.deployment as JsonRecord), endpointId: "other01" }, + }), + ).toThrow(/does not match/u); + expect(() => + parseBaseline({ + ...value, + anchor: { ...(value.anchor as JsonRecord), progressBlock: "999" }, + }), + ).toThrow(); + expect(() => parseBaseline({ ...value, digest: hex(32, 123) })).toThrow(/digest mismatch/u); + expect(() => parseBaseline({ ...value, arbitrary: true })).toThrow(/exactly/u); + }); + + it("compares every frozen stable launch field", () => { + const expected = rows()[0] as JsonRecord; + for (const field of STABLE_LAUNCH_FIELDS as string[]) { + const changed = { ...expected, [field]: changedValue(field, expected[field]) }; + expect( + () => assertFrozenBaseline([changed], { entries: [expected] }), + `field ${field} was not compared`, + ).toThrow(); + } + }); + + it("records only the exact authenticated Stock coordinator creator repair", async () => { + const frozenRows = rows(); + const stockIndex = frozenRows.findIndex( + (row) => row.releaseVersion === "stock-paired-v1", + ); + const coordinatorSource = "0xfa5f17389ca28d071781d59750b32c842ab6a54b"; + frozenRows[stockIndex] = { + ...frozenRows[stockIndex], + creator: coordinatorSource, + provenanceValid: false, + isComplete: false, + }; + const candidateRows = frozenRows.map((row, index) => + index === stockIndex + ? { + ...row, + creator: hex(20, 8_888), + provenanceValid: true, + isComplete: true, + } + : row, + ); + const frozenBaseline = await baseline(frozenRows); + + expect(assertFrozenBaseline(candidateRows, frozenBaseline)).toEqual([ + { + id: frozenRows[stockIndex].id, + releaseVersion: "stock-paired-v1", + priorCoordinatorSource: coordinatorSource, + authenticatedCreator: hex(20, 8_888), + launchOccurrenceId: frozenRows[stockIndex].launchOccurrenceId, + coordinatorOccurrenceId: frozenRows[stockIndex].coordinatorOccurrenceId, + }, + ]); + + expect(() => + assertFrozenBaseline( + candidateRows.map((row, index) => + index === stockIndex ? { ...row, totalSupply: "999999999" } : row, + ), + frozenBaseline, + ), + ).toThrow(/changed frozen launch .* at creator/u); + expect(() => + assertFrozenBaseline(candidateRows, { + entries: frozenRows.map((row, index) => + index === stockIndex ? { ...row, provenanceValid: true } : row, + ), + }), + ).toThrow(/changed frozen launch .* at creator/u); + }); +}); + +describe("Envio candidate audit", () => { + it("corroborates control-plane, endpoint, reviewed runtime and baseline evidence", async () => { + const identity = localIdentity(SOURCE_COMMIT) as JsonRecord; + const frozenBaseline = await baseline(); + const firstFixture = fixtureFetcher({ identity, progressBlock: 1_100 }); + const first = await auditCandidate({ + endpoint: CANDIDATE_ENDPOINT, + expectedIdentity: identity, + baseline: frozenBaseline, + sourceCommit: SOURCE_COMMIT, + deployment: deployment(identity), + fetcher: firstFixture.fetcher, + now: () => new Date(CAPTURED_AT), + }); + const secondFixture = fixtureFetcher({ identity, progressBlock: 1_100 }); + const second = await auditCandidate({ + endpoint: CANDIDATE_ENDPOINT, + expectedIdentity: identity, + baseline: frozenBaseline, + sourceCommit: SOURCE_COMMIT, + deployment: deployment(identity), + fetcher: secondFixture.fetcher, + now: () => new Date(CAPTURED_AT), + }); + + expect(first).toEqual(second); + expect(first.deployment).toEqual({ + provider: "envio-cloud", + owner: "0xprogrammable", + project: "programmable-indexer", + mirrorCommit: MIRROR_COMMIT, + deploymentLabel: identity.deployment, + endpointId: "cand001", + }); + expect(first.identity).toEqual(identity); + expect(first.baseline.digest).toBe(frozenBaseline.digest); + expect(first.anchor.progressBlock).toBe("1100"); + expect(first.inventory.sha256).toMatch(/^0x[0-9a-f]{64}$/u); + expect(first.authenticatedCoordinatorCreatorRepairs).toEqual([]); + expect(first.digest).toMatch(/^0x[0-9a-f]{64}$/u); + }); + + it("includes an exact Stock coordinator creator repair in signed audit evidence", async () => { + const identity = localIdentity(SOURCE_COMMIT) as JsonRecord; + const frozenRows = rows(); + const stockIndex = frozenRows.findIndex( + (row) => row.releaseVersion === "stock-paired-v1", + ); + frozenRows[stockIndex] = { + ...frozenRows[stockIndex], + creator: "0xfa5f17389ca28d071781d59750b32c842ab6a54b", + provenanceValid: false, + isComplete: false, + }; + const candidateRows = frozenRows.map((row, index) => + index === stockIndex + ? { + ...row, + creator: hex(20, 8_888), + provenanceValid: true, + isComplete: true, + } + : row, + ); + const result = await auditCandidate({ + endpoint: CANDIDATE_ENDPOINT, + expectedIdentity: identity, + baseline: await baseline(frozenRows), + sourceCommit: SOURCE_COMMIT, + deployment: deployment(identity), + fetcher: fixtureFetcher({ + identity, + rows: candidateRows, + progressBlock: 1_100, + }).fetcher, + now: () => new Date(CAPTURED_AT), + }); + + expect(result.authenticatedCoordinatorCreatorRepairs).toHaveLength(1); + expect(result.authenticatedCoordinatorCreatorRepairs[0]).toMatchObject({ + id: frozenRows[stockIndex].id, + releaseVersion: "stock-paired-v1", + priorCoordinatorSource: "0xfa5f17389ca28d071781d59750b32c842ab6a54b", + authenticatedCreator: hex(20, 8_888), + }); + }); + + it("rejects identity JSON that differs from the reviewed checkout", async () => { + const identity = localIdentity(SOURCE_COMMIT) as JsonRecord; + await expect( + auditCandidate({ + endpoint: CANDIDATE_ENDPOINT, + expectedIdentity: { ...identity, handlerSha256: hex(32, 123) }, + baseline: await baseline(), + sourceCommit: SOURCE_COMMIT, + deployment: deployment(identity), + fetcher: fixtureFetcher({ identity, progressBlock: 1_100 }).fetcher, + }), + ).rejects.toThrow(/reviewed checkout identity mismatch/u); + }); + + it("rejects endpoint, deployment-label and runtime identity substitution", async () => { + const identity = localIdentity(SOURCE_COMMIT) as JsonRecord; + const frozenBaseline = await baseline(); + const common = { + expectedIdentity: identity, + baseline: frozenBaseline, + sourceCommit: SOURCE_COMMIT, + }; + await expect( + auditCandidate({ + ...common, + endpoint: CANDIDATE_ENDPOINT, + deployment: { ...deployment(identity), "deployment-endpoint-id": "other01" }, + fetcher: fixtureFetcher({ identity, progressBlock: 1_100 }).fetcher, + }), + ).rejects.toThrow(/reviewed Envio/u); + await expect( + auditCandidate({ + ...common, + endpoint: CANDIDATE_ENDPOINT, + deployment: { ...deployment(identity), "deployment-label": "production-substitute" }, + fetcher: fixtureFetcher({ identity, progressBlock: 1_100 }).fetcher, + }), + ).rejects.toThrow(/does not match candidate identity/u); + await expect( + auditCandidate({ + ...common, + endpoint: CANDIDATE_ENDPOINT, + deployment: { ...deployment(identity), "mirror-commit": "HEAD" }, + fetcher: fixtureFetcher({ identity, progressBlock: 1_100 }).fetcher, + }), + ).rejects.toThrow(/mirror commit is invalid/u); + await expect( + auditCandidate({ + ...common, + endpoint: CANDIDATE_ENDPOINT, + deployment: deployment(identity), + fetcher: fixtureFetcher({ + identity: { ...identity, eventSetSha256: hex(32, 123) }, + progressBlock: 1_100, + }).fetcher, + }), + ).rejects.toThrow(/candidate IndexerState identity mismatch/u); + }); + + it("rejects a candidate behind the frozen checkpoint", async () => { + const identity = localIdentity(SOURCE_COMMIT) as JsonRecord; + await expect( + auditCandidate({ + endpoint: CANDIDATE_ENDPOINT, + expectedIdentity: identity, + baseline: await baseline(), + sourceCommit: SOURCE_COMMIT, + deployment: deployment(identity), + fetcher: fixtureFetcher({ identity, progressBlock: 999 }).fetcher, + }), + ).rejects.toThrow(/has not reached/u); + }); + + it("rejects a change to a frozen launch even when all digests are otherwise valid", async () => { + const identity = localIdentity(SOURCE_COMMIT) as JsonRecord; + const frozenBaseline = await baseline(); + const changed = rows(); + changed[0] = { ...changed[0], totalSupply: "999999999" }; + await expect( + auditCandidate({ + endpoint: CANDIDATE_ENDPOINT, + expectedIdentity: identity, + baseline: frozenBaseline, + sourceCommit: SOURCE_COMMIT, + deployment: deployment(identity), + fetcher: fixtureFetcher({ identity, rows: changed, progressBlock: 1_100 }).fetcher, + }), + ).rejects.toThrow(/changed frozen launch .* at totalSupply/u); + }); +}); diff --git a/indexer/test/replay.test.ts b/indexer/test/replay.test.ts new file mode 100644 index 00000000..b3c81d30 --- /dev/null +++ b/indexer/test/replay.test.ts @@ -0,0 +1,495 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; + +import { createTestIndexer } from "envio"; +import { parse } from "yaml"; +import { parseAbiItem } from "viem"; +import { describe, expect, it, vi } from "vitest"; + +import { + canonicalPayloadJson, + encodeEventPayload, +} from "../src/lib/payload-hash.js"; +import { SOURCE_REGISTRY } from "../src/lib/release-map.js"; + +const POOL_ID = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const SWAP_SENDER: `0x${string}` = + "0x1111111111111111111111111111111111111111"; +const TRANSACTION_HASH = + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const FIRST_BLOCK_HASH = + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const SECOND_BLOCK_HASH = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const BLOCK_NUMBER = 25_650_030; + +describe("replay and occurrence behavior", () => { + it("accepts canonical zero-byte data for an indexed-only event", () => { + const eventAbi = parseAbiItem( + "event PayoutAddressUpdated(address indexed beneficiary, address indexed previousPayoutAddress, address indexed newPayoutAddress)", + ); + const encoded = encodeEventPayload(eventAbi, { + beneficiary: "0x1111111111111111111111111111111111111111", + previousPayoutAddress: "0x2222222222222222222222222222222222222222", + newPayoutAddress: "0x3333333333333333333333333333333333333333", + }); + + expect(encoded.data).toBe("0x"); + expect(encoded.topics).toHaveLength(4); + expect(encoded.topics.every((topic) => /^0x[0-9a-f]{64}$/.test(topic))) + .toBe(true); + expect(encoded.payloadHash).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it("rejects malformed fixed-width fields and canonicalizes hex payloads", () => { + const eventAbi = parseAbiItem( + "event PayoutAddressUpdated(address indexed beneficiary, address indexed previousPayoutAddress, address indexed newPayoutAddress)", + ); + + expect(() => + encodeEventPayload(eventAbi, { + beneficiary: "0x1234", + previousPayoutAddress: "0x2222222222222222222222222222222222222222", + newPayoutAddress: "0x3333333333333333333333333333333333333333", + }), + ).toThrow(); + expect( + canonicalPayloadJson({ + z: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + a: 42n, + }), + ).toBe( + '{"a":"42","z":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}', + ); + }); + + it("deduplicates the same candidate occurrence and keeps bigint totals exact", async () => { + const indexer = createTestIndexer(); + const event = { + contract: "ClassicV2Hook" as const, + event: "NativeSwapFeesAccrued" as const, + logIndex: 60, + block: { + number: BLOCK_NUMBER, + timestamp: 1_800_000_030, + hash: FIRST_BLOCK_HASH, + }, + transaction: { hash: TRANSACTION_HASH, transactionIndex: 1 }, + params: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 90_071_992_547_409_931n, + creatorFee: 9_007_199_254_740_993n, + launcherFee: 1_000_000_000_000_007n, + }, + }; + + await indexer.process({ + chains: { + 1: { + simulate: [event, event], + }, + }, + }); + + expect(await indexer.FeeAccrual.getAll()).toHaveLength(1); + expect(await indexer.PoolFeeTotals.getOrThrow(`1:${POOL_ID}`)).toMatchObject({ + grossAmount: 90_071_992_547_409_931n, + creatorFees: 9_007_199_254_740_993n, + launcherFees: 1_000_000_000_000_007n, + swapCount: 1n, + }); + }); + + it("rejects a duplicate candidate occurrence with conflicting payload facts", async () => { + const indexer = createTestIndexer(); + const event = { + contract: "ClassicV2Hook" as const, + event: "NativeSwapFeesAccrued" as const, + logIndex: 59, + block: { + number: BLOCK_NUMBER, + timestamp: 1_800_000_030, + hash: FIRST_BLOCK_HASH, + }, + transaction: { hash: TRANSACTION_HASH, transactionIndex: 1 }, + params: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 10n, + creatorFee: 1n, + launcherFee: 1n, + }, + }; + + await expect( + indexer.process({ + chains: { + 1: { + simulate: [ + event, + { + ...event, + params: { + ...event.params, + grossNativeAmount: 11n, + }, + }, + ], + }, + }, + }), + ).rejects.toThrow(/worker exited with code 1/i); + }); + + it("retains two fork candidates when a transaction is re-mined at a new global log index", async () => { + const indexer = createTestIndexer(); + const params = { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 10n, + creatorFee: 1n, + launcherFee: 1n, + }; + + await indexer.process({ + chains: { + 1: { + simulate: [ + { + contract: "ClassicV2Hook", + event: "NativeSwapFeesAccrued", + logIndex: 60, + block: { + number: BLOCK_NUMBER, + timestamp: 1_800_000_030, + hash: FIRST_BLOCK_HASH, + }, + transaction: { hash: TRANSACTION_HASH, transactionIndex: 1 }, + params, + }, + { + contract: "ClassicV2Hook", + event: "NativeSwapFeesAccrued", + logIndex: 8, + block: { + number: BLOCK_NUMBER + 1, + timestamp: 1_800_000_042, + hash: SECOND_BLOCK_HASH, + }, + transaction: { hash: TRANSACTION_HASH, transactionIndex: 0 }, + params, + }, + ], + }, + }, + }); + + const candidates = (await indexer.ChainEvent.getAll()).sort((a, b) => + a.id.localeCompare(b.id), + ); + expect(candidates).toHaveLength(2); + expect( + candidates + .map(({ blockGlobalLogIndex }) => blockGlobalLogIndex) + .sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)), + ) + .toEqual([8n, 60n]); + expect(candidates.every(({ downstreamLogicalId }) => downstreamLogicalId === undefined)) + .toBe(true); + expect(candidates[0]?.id).not.toBe(candidates[1]?.id); + expect(await indexer.FeeAccrual.getAll()).toHaveLength(2); + expect(await indexer.PoolFeeTotals.getOrThrow(`1:${POOL_ID}`)).toMatchObject({ + grossAmount: 20n, + creatorFees: 2n, + launcherFees: 2n, + swapCount: 2n, + }); + expect( + (await indexer.IndexerState.getOrThrow("ethereum-mainnet")) + .progressOccurrenceId, + ).toBe(`1:${SECOND_BLOCK_HASH}:${TRANSACTION_HASH}:8`); + }); + + it("tracks health progress by chain placement rather than transaction-hash sorting", async () => { + const indexer = createTestIndexer(); + const earlierTransactionHash = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + const laterTransactionHash = + "0x0000000000000000000000000000000000000000000000000000000000000001"; + const params = { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 10n, + creatorFee: 1n, + launcherFee: 1n, + }; + + await indexer.process({ + chains: { + 1: { + simulate: [ + { + contract: "ClassicV2Hook", + event: "NativeSwapFeesAccrued", + logIndex: 60, + block: { + number: BLOCK_NUMBER, + timestamp: 1_800_000_030, + hash: FIRST_BLOCK_HASH, + }, + transaction: { + hash: earlierTransactionHash, + transactionIndex: 1, + }, + params, + }, + { + contract: "ClassicV2Hook", + event: "NativeSwapFeesAccrued", + logIndex: 61, + block: { + number: BLOCK_NUMBER, + timestamp: 1_800_000_030, + hash: FIRST_BLOCK_HASH, + }, + transaction: { + hash: laterTransactionHash, + transactionIndex: 2, + }, + params, + }, + { + contract: "ClassicV2Hook", + event: "NativeSwapFeesAccrued", + logIndex: 62, + block: { + number: BLOCK_NUMBER, + timestamp: 1_800_000_030, + hash: FIRST_BLOCK_HASH, + }, + transaction: { + hash: laterTransactionHash, + transactionIndex: 2, + }, + params, + }, + ], + }, + }, + }); + + expect( + (await indexer.IndexerState.getOrThrow("ethereum-mainnet")) + .progressOccurrenceId, + ).toBe( + `1:${FIRST_BLOCK_HASH}:${laterTransactionHash}:62`, + ); + }); + + it("persists the complete reviewed deployment identity from the worker environment", async () => { + const sourceCommit = "1".repeat(40); + const configSha256 = `0x${"22".repeat(32)}`; + const schemaSha256 = `0x${"33".repeat(32)}`; + const handlerSha256 = `0x${"44".repeat(32)}`; + const sourceRegistrySha256 = `0x${"55".repeat(32)}`; + const eventSetSha256 = `0x${"66".repeat(32)}`; + vi.stubEnv("ENVIO_DEPLOYMENT_LABEL", "production-reviewed-2026-07-31"); + vi.stubEnv("ENVIO_SOURCE_COMMIT", sourceCommit); + vi.stubEnv("ENVIO_CONFIG_SHA256", configSha256); + vi.stubEnv("ENVIO_SCHEMA_SHA256", schemaSha256); + vi.stubEnv("ENVIO_HANDLER_SHA256", handlerSha256); + vi.stubEnv("ENVIO_SOURCE_REGISTRY_SHA256", sourceRegistrySha256); + vi.stubEnv("ENVIO_EVENT_SET_SHA256", eventSetSha256); + vi.stubEnv("ENVIO_EVENT_COUNT", "51"); + + try { + const indexer = createTestIndexer(); + await indexer.process({ + chains: { + 1: { + simulate: [ + { + contract: "ClassicV2Hook", + event: "NativeSwapFeesAccrued", + logIndex: 62, + block: { + number: BLOCK_NUMBER, + timestamp: 1_800_000_030, + hash: FIRST_BLOCK_HASH, + }, + transaction: { + hash: TRANSACTION_HASH, + transactionIndex: 1, + }, + params: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 10n, + creatorFee: 1n, + launcherFee: 1n, + }, + }, + ], + }, + }, + }); + + expect( + await indexer.IndexerState.getOrThrow("ethereum-mainnet"), + ).toMatchObject({ + deployment: "production-reviewed-2026-07-31", + sourceCommit, + configSha256, + schemaSha256, + handlerSha256, + sourceRegistrySha256, + eventSetSha256, + eventCount: 51, + }); + } finally { + vi.unstubAllEnvs(); + } + }); +}); + +describe("checked-in manifest fixtures", () => { + it("pins every configured address and inclusive source cutoff to its deployment manifest", () => { + const projectRoot = path.resolve(process.cwd(), ".."); + const readJson = (relativePath: string) => + JSON.parse( + readFileSync(path.join(projectRoot, relativePath), "utf8"), + ) as T; + const classicV2 = readJson<{ + addresses: { feeHook: string; memeLauncher: string }; + transactions: { + feeHook: { blockNumber: number }; + memeLauncher: { blockNumber: number }; + }; + }>( + "contracts/deployments/mainnet-classic-v2.json", + ); + const classicV3 = readJson<{ + addresses: { + rewardVaultFactory: string; + initialBuyVestingWalletFactory: string; + feeHook: string; + launcher: string; + }; + deploymentBlocks: { + rewardVaultFactory: number; + initialBuyVestingWalletFactory: number; + feeHook: number; + launcher: number; + }; + }>( + "contracts/deployments/mainnet-classic-v3.json", + ); + const stockV1 = readJson<{ + addresses: { + launcher: string; + ethLaunchCoordinator: string; + feeHook: string; + feeSplitVaultFactory: string; + }; + startBlock: number; + }>( + "contracts/deployments/mainnet-stock-paired-v1.json", + ); + const stockV2 = readJson<{ + addresses: { + launcher: string; + ethLaunchCoordinator: string; + feeHook: string; + feeSplitVaultFactory: string; + }; + startBlock: number; + }>( + "contracts/deployments/mainnet-stock-paired-v2.json", + ); + const stockV3 = readJson<{ + addresses: { launcher: string; ethLaunchCoordinator: string }; + startBlock: number; + }>( + "contracts/deployments/mainnet-stock-paired-v3.json", + ); + + const expected = [ + ["ClassicV2Hook", classicV2.addresses.feeHook, classicV2.transactions.feeHook.blockNumber], + ["ClassicV2Launcher", classicV2.addresses.memeLauncher, classicV2.transactions.memeLauncher.blockNumber], + ["ClassicV3RewardVaultFactory", classicV3.addresses.rewardVaultFactory, classicV3.deploymentBlocks.rewardVaultFactory], + ["ClassicV3VestingWalletFactory", classicV3.addresses.initialBuyVestingWalletFactory, classicV3.deploymentBlocks.initialBuyVestingWalletFactory], + ["ClassicV3Hook", classicV3.addresses.feeHook, classicV3.deploymentBlocks.feeHook], + ["ClassicV3Launcher", classicV3.addresses.launcher, classicV3.deploymentBlocks.launcher], + ["StockV1Launcher", stockV1.addresses.launcher, stockV1.startBlock], + ["StockV1EthCoordinator", stockV1.addresses.ethLaunchCoordinator, stockV1.startBlock], + ["StockV1Hook", stockV1.addresses.feeHook, stockV1.startBlock], + ["StockV1RewardVaultFactory", stockV1.addresses.feeSplitVaultFactory, stockV1.startBlock], + ["StockV2Launcher", stockV2.addresses.launcher, stockV2.startBlock], + ["StockV2EthCoordinator", stockV2.addresses.ethLaunchCoordinator, stockV2.startBlock], + ["StockV2V3Hook", stockV2.addresses.feeHook, stockV2.startBlock], + ["StockV2V3RewardVaultFactory", stockV2.addresses.feeSplitVaultFactory, stockV2.startBlock], + ["StockV3Launcher", stockV3.addresses.launcher, stockV3.startBlock], + ["StockV3EthCoordinator", stockV3.addresses.ethLaunchCoordinator, stockV3.startBlock], + ].map(([contractName, address, startBlock]) => ({ + contractName: String(contractName), + address: String(address).toLowerCase(), + startBlock: Number(startBlock), + })); + + expect([...SOURCE_REGISTRY]).toEqual(expected); + + const config = parse( + readFileSync(path.join(process.cwd(), "config.yaml"), "utf8"), + ) as { + address_format: string; + rollback_on_reorg: boolean; + save_full_history: boolean; + raw_events: boolean; + chains: Array<{ + id: number; + start_block: number; + max_reorg_depth: number; + block_lag: number; + contracts: Array<{ name: string; address?: string | string[] }>; + }>; + }; + const chain = config.chains[0]!; + const configuredAddresses = chain.contracts + .flatMap(({ name, address }) => + address === undefined + ? [] + : (Array.isArray(address) ? address : [address]).map((value) => ({ + contractName: name, + address: value.toLowerCase(), + })), + ); + expect(configuredAddresses).toEqual( + expected.map(({ contractName, address }) => ({ contractName, address })), + ); + expect(config).toMatchObject({ + address_format: "lowercase", + rollback_on_reorg: true, + save_full_history: false, + raw_events: false, + }); + expect(chain).toMatchObject({ + id: 1, + start_block: 25_624_130, + max_reorg_depth: 200, + block_lag: 12, + }); + }); + + it("pins HyperIndex to 3.2.1", () => { + const packageJson = JSON.parse( + readFileSync(path.join(process.cwd(), "package.json"), "utf8"), + ) as { + dependencies: { envio: string }; + packageManager: string; + }; + expect(packageJson.dependencies.envio).toBe("3.2.1"); + expect(packageJson.packageManager).toBe("pnpm@10.32.0"); + }); +}); diff --git a/indexer/test/stock-paired.test.ts b/indexer/test/stock-paired.test.ts new file mode 100644 index 00000000..ccf83271 --- /dev/null +++ b/indexer/test/stock-paired.test.ts @@ -0,0 +1,604 @@ +import { createTestIndexer } from "envio"; +import { describe, expect, it } from "vitest"; + +const DEPLOYER: `0x${string}` = "0x1111111111111111111111111111111111111111"; +const TOKEN: `0x${string}` = "0x2222222222222222222222222222222222222222"; +const QUOTE: `0x${string}` = "0x3333333333333333333333333333333333333333"; +const VAULT: `0x${string}` = "0x4444444444444444444444444444444444444444"; +const POSITION_RECIPIENT: `0x${string}` = + "0x5555555555555555555555555555555555555555"; +const SHARED_HOOK: `0x${string}` = + "0x90c67c1e866f86526f0e338459cd435e1f23a0cc"; +const POOL_ID = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const LAUNCH_HASH = + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const REWARD_CONFIGURATION_HASH = + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const QUOTE_CONFIGURATION_HASH = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const BLOCK_HASH = + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; +const TRANSACTION_HASH = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; +const BLOCK_NUMBER = 25_650_020; +const MISMATCHED_VAULT: `0x${string}` = + "0x6666666666666666666666666666666666666666"; +const COORDINATED_CREATOR: `0x${string}` = + "0x2bb333d48dfaf1596d9036671d2e43168994249e"; +const CONFLICTING_CREATOR: `0x${string}` = + "0x7777777777777777777777777777777777777777"; +const STOCK_ETH_TRANSACTION_HASH = + "0xbe52bd2bb71159a1f6ab085cb7c7d5eb4f0583608b8adaf7d62f283be6693b19"; + +const stockReleaseCases = [ + { + releaseVersion: "stock-paired-v1", + launcherContract: "StockV1Launcher" as const, + launcherSource: "0x195750f33cad5ef2df857a53226b421297a1e79e" as const, + coordinatorContract: "StockV1EthCoordinator" as const, + coordinatorSource: "0xfa5f17389ca28d071781d59750b32c842ab6a54b" as const, + }, + { + releaseVersion: "stock-paired-v2", + launcherContract: "StockV2Launcher" as const, + launcherSource: "0x5ea6be24838061ba45dbe8d82de1b267dc240daf" as const, + coordinatorContract: "StockV2EthCoordinator" as const, + coordinatorSource: "0xfb9e1034df6161088e8f358502b19e7515c30fd2" as const, + }, + { + releaseVersion: "stock-paired-v3", + launcherContract: "StockV3Launcher" as const, + launcherSource: "0x0573879f72d8ee8b0e5a4ec5e8bcdb2fcab9e51c" as const, + coordinatorContract: "StockV3EthCoordinator" as const, + coordinatorSource: "0xddc3abbab0df7f1189310a4f70e7e365796b74e2" as const, + }, +] as const; + +type StockReleaseCase = (typeof stockReleaseCases)[number]; + +function coordinatedLaunchEvents( + release: StockReleaseCase, + overrides: { + launcherDeployer?: `0x${string}`; + coordinatorCreator?: `0x${string}`; + coordinatorSource?: `0x${string}`; + } = {}, +) { + const stockTransaction = { + hash: STOCK_ETH_TRANSACTION_HASH, + transactionIndex: 3, + }; + return [ + { + contract: release.launcherContract, + event: "StockPairedTokenLaunched" as const, + srcAddress: release.launcherSource, + logIndex: 60, + block, + transaction: stockTransaction, + params: { + deployer: overrides.launcherDeployer ?? release.coordinatorSource, + token: TOKEN, + quoteAsset: QUOTE, + poolId: POOL_ID, + rewardVault: VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 12n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: release.coordinatorContract, + event: "StockPairedEthTokenLaunched" as const, + srcAddress: overrides.coordinatorSource ?? release.coordinatorSource, + logIndex: 61, + block, + transaction: stockTransaction, + params: { + creator: overrides.coordinatorCreator ?? COORDINATED_CREATOR, + token: TOKEN, + quoteAsset: QUOTE, + initialBuyEthAmount: 1_000n, + initialBuyQuoteAmount: 2_000n, + initialBuyTokenAmount: 3_000n, + launchHash: LAUNCH_HASH, + }, + }, + ]; +} + +const block = { number: BLOCK_NUMBER, timestamp: 1_800_000_020, hash: BLOCK_HASH }; +const transaction = { hash: TRANSACTION_HASH, transactionIndex: 3 }; + +const v3LaunchEvents = [ + { + contract: "StockV2V3Hook" as const, + event: "PoolRegistered" as const, + logIndex: 50, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + quoteAsset: QUOTE, + rewardVault: VAULT, + registrar: DEPLOYER, + quoteIsCurrency0: true, + rewardConfigurationHash: REWARD_CONFIGURATION_HASH, + quoteConfigurationHash: QUOTE_CONFIGURATION_HASH, + }, + }, + { + contract: "StockV2V3Hook" as const, + event: "PoolFeeDisclosure" as const, + logIndex: 51, + block, + transaction, + params: { + poolId: POOL_ID, + token: TOKEN, + quoteAsset: QUOTE, + rewardVault: VAULT, + buySwapFeeBps: 100n, + sellSwapFeeBps: 200n, + creatorFeeBps: 90n, + launcherFeeBps: 10n, + transferTaxBps: 0n, + lpFeePips: 3_000n, + }, + }, + { + contract: "StockV2V3RewardVaultFactory" as const, + event: "QuoteAssetFeeSplitVaultDeployed" as const, + logIndex: 52, + block, + transaction, + params: { + vault: VAULT, + feeHook: SHARED_HOOK, + poolId: POOL_ID, + quoteAsset: QUOTE, + }, + }, + { + contract: "StockV2V3Hook" as const, + event: "QuoteSwapFeesAccrued" as const, + logIndex: 53, + block, + transaction, + params: { + poolId: POOL_ID, + swapSender: DEPLOYER, + quoteAsset: QUOTE, + isBuy: true, + grossQuoteAmount: 10_000n, + creatorFee: 900n, + launcherFee: 100n, + }, + }, + { + contract: "StockV3Launcher" as const, + event: "StockPairedLiquidityConfigured" as const, + logIndex: 54, + block, + transaction, + params: { + token: TOKEN, + quoteAsset: QUOTE, + totalSupply: 1_000_000n, + tokenLiquidityAmount: 900_000n, + lockedTokenDust: 0n, + initialTick: 0n, + tickLower: -10n, + tickUpper: 10n, + lpFeePips: 3_000n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "StockV3Launcher" as const, + event: "StockPairedCreatorInitialBuy" as const, + logIndex: 55, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + quoteAsset: QUOTE, + poolId: POOL_ID, + quoteAmount: 777n, + tokenAmount: 888n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "StockV3Launcher" as const, + event: "StockPairedTokenLaunched" as const, + logIndex: 56, + block, + transaction, + params: { + deployer: DEPLOYER, + token: TOKEN, + quoteAsset: QUOTE, + poolId: POOL_ID, + rewardVault: VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 12n, + launchHash: LAUNCH_HASH, + }, + }, + { + contract: "StockV2V3RewardVault" as const, + event: "BeneficiaryFeesClaimed" as const, + srcAddress: VAULT, + logIndex: 57, + block, + transaction, + params: { + beneficiary: DEPLOYER, + payoutAddress: POSITION_RECIPIENT, + quoteAsset: QUOTE, + amount: 9_007_199_254_740_993n, + beneficiaryTotalClaimed: 90_071_992_547_409_931n, + vaultTotalReceived: 900_719_925_474_099_311n, + }, + }, +]; + +describe("Stock-Paired handlers", () => { + it.each(stockReleaseCases)( + "replaces the $releaseVersion provisional coordinator deployer with the authenticated creator", + async (release) => { + const indexer = createTestIndexer(); + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: coordinatedLaunchEvents(release), + }, + }, + }); + + expect( + await indexer.Launch.getOrThrow( + `1:${release.releaseVersion}:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + creator: COORDINATED_CREATOR, + token: TOKEN, + quoteAsset: QUOTE, + provenanceValid: true, + hasLaunchEvent: true, + hasCoordinatorEvent: true, + }); + }, + ); + + it.each(stockReleaseCases)( + "rejects a $releaseVersion coordinator creator when the launcher deployer was not provisional", + async (release) => { + const indexer = createTestIndexer(); + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: coordinatedLaunchEvents(release, { + launcherDeployer: CONFLICTING_CREATOR, + }), + }, + }, + }); + + expect( + await indexer.Launch.getOrThrow( + `1:${release.releaseVersion}:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + creator: CONFLICTING_CREATOR, + provenanceValid: false, + }); + }, + ); + + it.each(stockReleaseCases)( + "rejects a $releaseVersion coordinator event from a non-release source", + async (release) => { + const indexer = createTestIndexer(); + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: coordinatedLaunchEvents(release, { + coordinatorSource: CONFLICTING_CREATOR, + }), + }, + }, + }); + + expect( + await indexer.Launch.getOrThrow( + `1:${release.releaseVersion}:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + creator: release.coordinatorSource, + provenanceValid: false, + }); + }, + ); + + it.each(stockReleaseCases)( + "invalidates a conflicting duplicate $releaseVersion coordinator creator", + async (release) => { + const indexer = createTestIndexer(); + const baseEvents = coordinatedLaunchEvents(release); + const firstCoordinator = baseEvents.find( + (event) => event.event === "StockPairedEthTokenLaunched", + )!; + const events = [...baseEvents, { + ...firstCoordinator, + logIndex: 62, + params: { + ...firstCoordinator.params, + creator: CONFLICTING_CREATOR, + }, + }]; + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: events, + }, + }, + }); + + expect( + await indexer.Launch.getOrThrow( + `1:${release.releaseVersion}:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + creator: COORDINATED_CREATOR, + provenanceValid: false, + }); + }, + ); + + it("resolves the shared hook and factory through poolId to Stock V3", async () => { + const indexer = createTestIndexer(); + const result = await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: v3LaunchEvents, + }, + }, + }); + + expect(result.changes[0]?.addresses?.sets).toContainEqual({ + contract: "StockV2V3RewardVault", + address: VAULT, + }); + expect((await indexer.PoolFeeConfig.getOrThrow(`1:${POOL_ID}`))).toMatchObject({ + poolId: POOL_ID, + releaseVersion: "stock-paired-v3", + model: "stock-paired", + provenanceValid: true, + }); + expect( + await indexer.Launch.getOrThrow( + `1:stock-paired-v3:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + rewardConfigurationHash: REWARD_CONFIGURATION_HASH, + quoteConfigurationHash: QUOTE_CONFIGURATION_HASH, + provenanceValid: true, + isComplete: true, + }); + expect(await indexer.RewardVault.getOrThrow(VAULT)).toMatchObject({ + releaseVersion: "stock-paired-v3", + poolId: POOL_ID, + hook: SHARED_HOOK, + quoteAsset: QUOTE, + }); + expect((await indexer.BeneficiaryClaim.getAll())[0]).toMatchObject({ + vault: VAULT, + beneficiary: DEPLOYER, + payoutAddress: POSITION_RECIPIENT, + quoteAsset: QUOTE, + amount: 9_007_199_254_740_993n, + beneficiaryTotalClaimed: 90_071_992_547_409_931n, + vaultTotalReceived: 900_719_925_474_099_311n, + releaseVersion: "stock-paired-v3", + downstreamLogicalId: undefined, + receiptLogOrdinal: undefined, + }); + const accrual = (await indexer.FeeAccrual.getAll())[0]; + expect(accrual).toMatchObject({ + releaseVersion: "stock-paired-v3", + model: "stock-paired", + poolId: POOL_ID, + quoteAsset: QUOTE, + grossAmount: 10_000n, + creatorFee: 900n, + launcherFee: 100n, + }); + expect( + await indexer.ChainEvent.getOrThrow(accrual!.id), + ).toMatchObject({ + releaseVersion: "unresolved", + model: "unresolved", + }); + expect(await indexer.PoolFeeTotals.getOrThrow(`1:${POOL_ID}`)).toMatchObject({ + releaseVersion: "stock-paired-v3", + model: "stock-paired", + grossAmount: 10_000n, + creatorFees: 900n, + launcherFees: 100n, + swapCount: 1n, + }); + }); + + it("resolves the same shared hook and factory through poolId to Stock V2", async () => { + const indexer = createTestIndexer(); + const v2LaunchEvents = v3LaunchEvents.map((event) => + event.contract === "StockV3Launcher" + ? { ...event, contract: "StockV2Launcher" as const } + : event, + ); + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: v2LaunchEvents, + }, + }, + }); + + expect((await indexer.PoolFeeConfig.getOrThrow(`1:${POOL_ID}`))).toMatchObject({ + releaseVersion: "stock-paired-v2", + model: "stock-paired", + provenanceValid: true, + }); + expect(await indexer.RewardVault.getOrThrow(VAULT)).toMatchObject({ + releaseVersion: "stock-paired-v2", + poolId: POOL_ID, + hook: SHARED_HOOK, + quoteAsset: QUOTE, + }); + expect((await indexer.BeneficiaryClaim.getAll())[0]).toMatchObject({ + releaseVersion: "stock-paired-v2", + amount: 9_007_199_254_740_993n, + }); + expect((await indexer.FeeAccrual.getAll())[0]).toMatchObject({ + releaseVersion: "stock-paired-v2", + model: "stock-paired", + grossAmount: 10_000n, + }); + }); + + it("invalidates a complete launch when a mismatched shared hook event arrives later", async () => { + const indexer = createTestIndexer(); + const events = v3LaunchEvents.map((event) => + event.contract === "StockV2V3Hook" && + event.event === "PoolRegistered" + ? { + ...event, + logIndex: 58, + params: { + ...event.params, + rewardVault: MISMATCHED_VAULT, + }, + } + : event, + ); + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: events, + }, + }, + }); + + expect( + await indexer.Launch.getOrThrow( + `1:stock-paired-v3:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + provenanceValid: false, + isComplete: false, + }); + }); + + it.each([ + ["before", 52], + ["after", 58], + ])( + "invalidates a conflicting same-pool factory vault emitted %s the launcher", + async (_order, factoryLogIndex) => { + const indexer = createTestIndexer(); + const events = v3LaunchEvents + .filter((event) => event.contract !== "StockV2V3RewardVault") + .map((event) => + event.contract === "StockV2V3RewardVaultFactory" + ? { + ...event, + logIndex: factoryLogIndex, + params: { + ...event.params, + vault: MISMATCHED_VAULT, + }, + } + : event, + ); + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: events, + }, + }, + }); + + expect( + await indexer.RewardVault.getOrThrow(MISMATCHED_VAULT), + ).toMatchObject({ + poolId: POOL_ID, + hook: SHARED_HOOK, + quoteAsset: QUOTE, + }); + expect( + await indexer.Launch.getOrThrow( + `1:stock-paired-v3:${LAUNCH_HASH}`, + ), + ).toMatchObject({ + provenanceValid: false, + isComplete: false, + }); + }, + ); + + it("keeps mismatched quote provenance incomplete", async () => { + const indexer = createTestIndexer(); + const mismatchedQuote: `0x${string}` = + "0x6666666666666666666666666666666666666666"; + const events = v3LaunchEvents.map((event) => + event.event === "StockPairedLiquidityConfigured" + ? { + ...event, + params: { ...event.params, quoteAsset: mismatchedQuote }, + } + : event, + ); + + await indexer.process({ + chains: { + 1: { + startBlock: BLOCK_NUMBER, + endBlock: BLOCK_NUMBER + 12, + simulate: events, + }, + }, + }); + + const launch = await indexer.Launch.getOrThrow( + `1:stock-paired-v3:${LAUNCH_HASH}`, + ); + expect(launch.provenanceValid).toBe(false); + expect(launch.isComplete).toBe(false); + expect(launch.quoteAsset).toBe(QUOTE); + }); +}); diff --git a/indexer/tsconfig.json b/indexer/tsconfig.json new file mode 100644 index 00000000..405d5e68 --- /dev/null +++ b/indexer/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": false, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "lib": ["es2022"], + "types": ["node", "vitest/globals"] + }, + "include": ["src", "test", "envio-env.d.ts"] +} diff --git a/indexer/vitest.config.ts b/indexer/vitest.config.ts new file mode 100644 index 00000000..d9ebefd5 --- /dev/null +++ b/indexer/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + environment: "node", + }, +}); diff --git a/lib/data-pipeline/action-lookup.ts b/lib/data-pipeline/action-lookup.ts new file mode 100644 index 00000000..0e95e9a7 --- /dev/null +++ b/lib/data-pipeline/action-lookup.ts @@ -0,0 +1,556 @@ +import "server-only"; + +import { + formatUnits, + getAddress, + type Address, + type Hex, +} from "viem"; + +import type { ExploreReadModel } from "../onchain/types"; +import type { LauncherToken } from "../tokens"; +import { + addressFromBytea, + bytes32FromBytea, + canonicalAddress, + canonicalBytes32, + hexToBytes, + parseNonnegativeIntegerText, + parseUint256Text, +} from "./codecs"; +import type { PostgresTransaction } from "./postgres"; +import { getServerReadModel } from "./read-model.server"; + +const SUPPORTED_RELEASES = Object.freeze({ + "classic-v2": "classic", + "classic-v3": "classic", + "stock-paired-v1": "stock-paired", + "stock-paired-v2": "stock-paired", + "stock-paired-v3": "stock-paired", +} as const); + +export type ActionReleaseVersion = keyof typeof SUPPORTED_RELEASES; +export type ActionModelVersion = + (typeof SUPPORTED_RELEASES)[ActionReleaseVersion]; + +export type ActionTokenLookup = Readonly<{ + chainId: 1 | 11_155_111; + releaseVersion: ActionReleaseVersion; + modelVersion: ActionModelVersion; + tokenAddress: Address; + creatorAddress: Address; + launchTransactionHash: Hex; + poolId: Hex; + rewardVaultAddress: Address | null; + launchHash: Hex; + tokenName: string; + tokenSymbol: string; + totalSupplyRaw: string; + launchedAt: string; + hookAddress: Address; + quoteAssetAddress: Address | null; + totalSwapFeeBps: number; + buySwapFeeBps: number; + sellSwapFeeBps: number; + buyCreatorFeeBps: number; + sellCreatorFeeBps: number; + creatorFeeBps: number | null; + launcherFeeBps: number; + transferTaxBps: number; + lpFeePips: number; + promotedBlockNumber: string; + promotedBlockHash: Hex; + verifiedAt: string; +}>; + +export type ActionRewardLookup = Readonly<{ + chainId: 1 | 11_155_111; + account: Address; + vaultAddress: Address; + poolId: Hex; + hookAddress: Address; + quoteAssetAddress: Address | null; + claimableRaw: string; + claimedRaw: string; + entitledRaw: string; + releaseVersion: ActionReleaseVersion; + modelVersion: ActionModelVersion; + promotedBlockNumber: string; + promotedBlockHash: Hex; + verifiedAt: string; + token: ActionTokenLookup; +}>; + +export type ActionLookupErrorCode = + | "read-model-unavailable" + | "not-found" + | "ambiguous" + | "unsupported-release" + | "scope-mismatch" + | "projection-incomplete"; + +export class ActionLookupError extends Error { + readonly code: ActionLookupErrorCode; + + constructor(code: ActionLookupErrorCode) { + super("The indexed launch identity is unavailable"); + this.name = "ActionLookupError"; + this.code = code; + } +} + +type DatabaseRow = Record; + +function fail(code: ActionLookupErrorCode): never { + throw new ActionLookupError(code); +} + +function integerText(value: unknown): string { + if (typeof value === "bigint") { + if (value < 0n) fail("projection-incomplete"); + return value.toString(); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) { + fail("projection-incomplete"); + } + return String(value); + } + try { + return parseNonnegativeIntegerText(value); + } catch { + fail("projection-incomplete"); + } +} + +function uintText(value: unknown): string { + if (typeof value === "bigint") { + if (value < 0n) fail("projection-incomplete"); + return parseUint256Text(value.toString()); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) { + fail("projection-incomplete"); + } + return parseUint256Text(String(value)); + } + try { + return parseUint256Text(value); + } catch { + fail("projection-incomplete"); + } +} + +function boundedNumber(value: unknown, maximum: number): number { + const parsed = BigInt(integerText(value)); + if (parsed > BigInt(maximum)) fail("projection-incomplete"); + return Number(parsed); +} + +function nullableBoundedNumber( + value: unknown, + maximum: number, +): number | null { + return value === null ? null : boundedNumber(value, maximum); +} + +function requiredText(value: unknown, maximum: number): string { + if ( + typeof value !== "string" || + Buffer.byteLength(value, "utf8") < 1 || + Buffer.byteLength(value, "utf8") > maximum || + /[\u0000-\u001f\u007f]/u.test(value) + ) { + fail("projection-incomplete"); + } + return value; +} + +function isoTimestamp(value: unknown): string { + const date = + value instanceof Date + ? value + : typeof value === "string" + ? new Date(value) + : null; + if (!date || Number.isNaN(date.valueOf())) { + fail("projection-incomplete"); + } + return date.toISOString(); +} + +function address(value: unknown): Address { + try { + return getAddress(addressFromBytea(value)); + } catch { + fail("projection-incomplete"); + } +} + +function nullableAddress(value: unknown): Address | null { + return value === null ? null : address(value); +} + +function bytes32(value: unknown): Hex { + try { + return bytes32FromBytea(value) as Hex; + } catch { + fail("projection-incomplete"); + } +} + +function chainId(value: unknown): 1 | 11_155_111 { + const parsed = boundedNumber(value, 11_155_111); + if (parsed !== 1 && parsed !== 11_155_111) { + fail("scope-mismatch"); + } + return parsed; +} + +function releaseIdentity(row: DatabaseRow) { + const releaseVersion = requiredText(row.release_id, 64); + const modelVersion = requiredText(row.model_id, 64); + const expected = + SUPPORTED_RELEASES[releaseVersion as ActionReleaseVersion]; + if (!expected || expected !== modelVersion) { + fail("unsupported-release"); + } + return { + releaseVersion: releaseVersion as ActionReleaseVersion, + modelVersion: modelVersion as ActionModelVersion, + }; +} + +function parseTokenRow(row: DatabaseRow): ActionTokenLookup { + const release = releaseIdentity(row); + const totalSwapFeeBps = boundedNumber(row.total_swap_fee_bps, 10_000); + const buySwapFeeBps = boundedNumber(row.buy_swap_fee_bps, 10_000); + const sellSwapFeeBps = boundedNumber(row.sell_swap_fee_bps, 10_000); + const buyCreatorFeeBps = boundedNumber( + row.buy_creator_fee_bps, + 10_000, + ); + const sellCreatorFeeBps = boundedNumber( + row.sell_creator_fee_bps, + 10_000, + ); + const creatorFeeBps = nullableBoundedNumber( + row.creator_fee_bps, + 10_000, + ); + const launcherFeeBps = boundedNumber(row.launcher_fee_bps, 10_000); + const transferTaxBps = boundedNumber(row.transfer_tax_bps, 10_000); + const lpFeePips = boundedNumber(row.lp_fee_pips, 1_000_000); + if ( + totalSwapFeeBps !== Math.max(buySwapFeeBps, sellSwapFeeBps) || + buyCreatorFeeBps + launcherFeeBps !== buySwapFeeBps || + sellCreatorFeeBps + launcherFeeBps !== sellSwapFeeBps || + (creatorFeeBps !== null && + (creatorFeeBps !== buyCreatorFeeBps || + creatorFeeBps !== sellCreatorFeeBps)) + ) { + fail("projection-incomplete"); + } + return Object.freeze({ + chainId: chainId(row.chain_id), + ...release, + tokenAddress: address(row.token), + creatorAddress: address(row.creator), + launchTransactionHash: bytes32(row.launch_transaction_hash), + poolId: bytes32(row.pool_id), + rewardVaultAddress: nullableAddress(row.reward_vault), + launchHash: bytes32(row.launch_hash), + tokenName: requiredText(row.token_name, 128), + tokenSymbol: requiredText(row.token_symbol, 32), + totalSupplyRaw: uintText(row.total_supply), + launchedAt: isoTimestamp(row.launch_block_timestamp), + hookAddress: address(row.hook), + quoteAssetAddress: nullableAddress(row.quote_asset), + totalSwapFeeBps, + buySwapFeeBps, + sellSwapFeeBps, + buyCreatorFeeBps, + sellCreatorFeeBps, + creatorFeeBps, + launcherFeeBps, + transferTaxBps, + lpFeePips, + promotedBlockNumber: integerText(row.promoted_block_number), + promotedBlockHash: bytes32(row.promoted_block_hash), + verifiedAt: isoTimestamp(row.verified_at), + }); +} + +function parseRewardRow( + row: DatabaseRow, + token: ActionTokenLookup, +): ActionRewardLookup { + const release = releaseIdentity(row); + const claimableRaw = uintText(row.claimable_accrued); + const claimedRaw = uintText(row.claimed_total); + const entitledRaw = uintText(row.entitled); + if ( + BigInt(claimedRaw) > BigInt(entitledRaw) || + BigInt(entitledRaw) - BigInt(claimedRaw) !== BigInt(claimableRaw) + ) { + fail("projection-incomplete"); + } + const parsed = Object.freeze({ + chainId: chainId(row.chain_id), + account: address(row.account), + vaultAddress: address(row.vault), + poolId: bytes32(row.pool_id), + hookAddress: address(row.hook), + quoteAssetAddress: nullableAddress(row.quote_asset), + claimableRaw, + claimedRaw, + entitledRaw, + ...release, + promotedBlockNumber: integerText(row.promoted_block_number), + promotedBlockHash: bytes32(row.promoted_block_hash), + verifiedAt: isoTimestamp(row.verified_at), + token, + }); + if ( + parsed.chainId !== token.chainId || + parsed.releaseVersion !== token.releaseVersion || + parsed.modelVersion !== token.modelVersion || + parsed.poolId.toLowerCase() !== token.poolId.toLowerCase() || + parsed.hookAddress.toLowerCase() !== token.hookAddress.toLowerCase() || + parsed.vaultAddress.toLowerCase() !== + token.rewardVaultAddress?.toLowerCase() || + parsed.quoteAssetAddress?.toLowerCase() !== + token.quoteAssetAddress?.toLowerCase() + ) { + fail("scope-mismatch"); + } + return parsed; +} + +const TOKEN_COLUMNS = ` + chain_id, release_id, model_id, token, creator, + launch_transaction_hash, pool_id, reward_vault, launch_hash, + token_name, token_symbol, total_supply, launch_block_timestamp, + hook, quote_asset, total_swap_fee_bps, buy_swap_fee_bps, + sell_swap_fee_bps, buy_creator_fee_bps, sell_creator_fee_bps, + creator_fee_bps, launcher_fee_bps, + transfer_tax_bps, lp_fee_pips, promoted_block_number, + promoted_block_hash, verified_at +`; + +async function oneToken( + transaction: PostgresTransaction, + where: "token" | "pool_id", + expectedChainId: 1 | 11_155_111, + value: Address | Hex, +): Promise { + const rows = await transaction.query( + `select ${TOKEN_COLUMNS} + from programmable_private.launch_by_token_v1 + where chain_id = $1 and ${where} = $2 + order by promoted_block_number desc, token + limit 2`, + [expectedChainId, hexToBytes(value)], + ); + if (rows.length === 0) fail("not-found"); + if (rows.length !== 1) fail("ambiguous"); + const token = parseTokenRow(rows[0]!); + if ( + token.chainId !== expectedChainId || + (where === "token" + ? token.tokenAddress.toLowerCase() !== value.toLowerCase() + : token.poolId.toLowerCase() !== value.toLowerCase()) + ) { + fail("scope-mismatch"); + } + return token; +} + +export async function queryActionTokenByAddress( + transaction: PostgresTransaction, + input: { chainId: 1 | 11_155_111; token: Address }, +): Promise { + return oneToken( + transaction, + "token", + input.chainId, + canonicalAddress(input.token) as Address, + ); +} + +export async function queryActionTokenByPoolId( + transaction: PostgresTransaction, + input: { chainId: 1 | 11_155_111; poolId: Hex }, +): Promise { + return oneToken( + transaction, + "pool_id", + input.chainId, + canonicalBytes32(input.poolId) as Hex, + ); +} + +export async function queryActionReward( + transaction: PostgresTransaction, + input: { + chainId: 1 | 11_155_111; + account: Address; + vaultAddress: Address; + }, +): Promise { + const account = canonicalAddress(input.account) as Address; + const vaultAddress = canonicalAddress(input.vaultAddress) as Address; + const rows = await transaction.query( + `select * + from programmable_private.get_account_reward_summary_v1($1, $2) + where vault = $3 + order by promoted_block_number desc + limit 2`, + [ + input.chainId, + hexToBytes(account), + hexToBytes(vaultAddress), + ], + ); + if (rows.length === 0) fail("not-found"); + if (rows.length !== 1) fail("ambiguous"); + const raw = rows[0]!; + const rawPoolId = bytes32(raw.pool_id); + const rawRelease = releaseIdentity(raw); + const tokenRows = await transaction.query( + `select ${TOKEN_COLUMNS} + from programmable_private.launch_by_token_v1 + where chain_id = $1 + and pool_id = $2 + and reward_vault = $3 + and release_id = $4 + and model_id = $5 + order by promoted_block_number desc, token + limit 2`, + [ + input.chainId, + hexToBytes(rawPoolId), + hexToBytes(vaultAddress), + rawRelease.releaseVersion, + rawRelease.modelVersion, + ], + ); + if (tokenRows.length === 0) fail("not-found"); + if (tokenRows.length !== 1) fail("ambiguous"); + const token = parseTokenRow(tokenRows[0]!); + const reward = parseRewardRow(raw, token); + if ( + reward.chainId !== input.chainId || + reward.account.toLowerCase() !== account.toLowerCase() || + reward.vaultAddress.toLowerCase() !== vaultAddress.toLowerCase() + ) { + fail("scope-mismatch"); + } + return reward; +} + +async function withReadSnapshot( + work: (transaction: PostgresTransaction) => Promise, +): Promise { + const readModel = await getServerReadModel(); + if (!readModel) fail("read-model-unavailable"); + return readModel.repeatableReadSnapshot(work); +} + +export function lookupActionTokenByAddress(input: { + chainId: 1 | 11_155_111; + token: Address; +}) { + return withReadSnapshot((transaction) => + queryActionTokenByAddress(transaction, input), + ); +} + +export function lookupActionTokenByPoolId(input: { + chainId: 1 | 11_155_111; + poolId: Hex; +}) { + return withReadSnapshot((transaction) => + queryActionTokenByPoolId(transaction, input), + ); +} + +export function lookupActionReward(input: { + chainId: 1 | 11_155_111; + account: Address; + vaultAddress: Address; +}) { + return withReadSnapshot((transaction) => + queryActionReward(transaction, input), + ); +} + +export function actionTokenAsExploreModel( + token: ActionTokenLookup, + options: { creatorFeesAccruedRaw?: string } = {}, +): ExploreReadModel { + const launchModel = token.modelVersion; + const launchModelVersion: LauncherToken["launchModelVersion"] = + token.releaseVersion === "classic-v3" || + token.releaseVersion === "stock-paired-v1" || + token.releaseVersion === "stock-paired-v2" || + token.releaseVersion === "stock-paired-v3" + ? token.releaseVersion + : undefined; + const launcherToken: LauncherToken = { + id: token.tokenAddress.toLowerCase(), + name: token.tokenName, + symbol: token.tokenSymbol, + tokenAddress: token.tokenAddress, + hookAddress: token.hookAddress, + poolId: token.poolId, + creatorAddress: token.creatorAddress, + ...(token.rewardVaultAddress + ? { rewardVaultAddress: token.rewardVaultAddress } + : {}), + ...(token.quoteAssetAddress + ? { quoteAssetAddress: token.quoteAssetAddress } + : {}), + launchHash: token.launchHash, + launchTransactionHash: token.launchTransactionHash, + launchedAt: token.launchedAt, + totalSupplyRaw: token.totalSupplyRaw, + tokenDecimals: 18, + totalSwapFeeBps: token.totalSwapFeeBps, + buyCreatorFeeBps: token.buyCreatorFeeBps, + sellCreatorFeeBps: token.sellCreatorFeeBps, + ...(token.creatorFeeBps === null + ? {} + : { creatorFeeBps: token.creatorFeeBps }), + launcherFeeBps: token.launcherFeeBps, + transferTaxBps: token.transferTaxBps, + lpFeePips: token.lpFeePips, + launchModel, + ...(launchModelVersion ? { launchModelVersion } : {}), + ...(options.creatorFeesAccruedRaw + ? { + creatorFeesAccruedWei: parseUint256Text( + options.creatorFeesAccruedRaw, + ), + creatorFeesAccruedEth: formatUnits( + BigInt(options.creatorFeesAccruedRaw), + 18, + ), + } + : {}), + liquidityPath: "meme", + }; + return { + status: "ready", + tokens: [launcherToken], + snapshot: { + chainId: token.chainId, + blockNumber: token.promotedBlockNumber, + blockHash: token.promotedBlockHash, + confirmations: 0, + }, + creatorClaims: [], + launcherFeesAccruedWei: "0", + launcherFeesAccruedEth: "0", + }; +} diff --git a/lib/data-pipeline/cache.ts b/lib/data-pipeline/cache.ts new file mode 100644 index 00000000..721c7659 --- /dev/null +++ b/lib/data-pipeline/cache.ts @@ -0,0 +1,98 @@ +import "server-only"; + +import { + canonicalBytes32, + parseNonnegativeIntegerText, + type HexBytes32, +} from "./codecs"; +import { invalidInput } from "./errors"; + +export const CACHE_POLICIES = Object.freeze({ + public: Object.freeze({ + visibility: "public" as const, + cacheControl: "public, s-maxage=2, stale-while-revalidate=2", + }), + private: Object.freeze({ + visibility: "private" as const, + cacheControl: "private, no-store", + }), +}); + +export type ReadKind = + | "explore-list" + | "token-detail" + | "chart" + | "public-indexer" + | "account-rewards" + | "claimability" + | "launch-confirmation" + | "transaction-adjacent"; + +export function cachePolicyForRead(kind: ReadKind) { + switch (kind) { + case "explore-list": + case "token-detail": + case "chart": + case "public-indexer": + return CACHE_POLICIES.public; + default: + return CACHE_POLICIES.private; + } +} +export type ReadProvenance = { + source: "indexed" | "blob" | "rpc"; + projectionBlock?: string; + projectionHash?: HexBytes32; + projectionLag?: number; + reconciledAt?: string; + releaseVersion?: string; +}; + +export function provenanceHeaders( + provenance: ReadProvenance, +): Readonly> { + const headers: Record = { + "X-Programmable-Read-Source": provenance.source, + }; + if (provenance.projectionBlock !== undefined) { + headers["X-Programmable-Projection-Block"] = + parseNonnegativeIntegerText(provenance.projectionBlock); + } + if (provenance.projectionHash !== undefined) { + headers["X-Programmable-Projection-Hash"] = canonicalBytes32( + provenance.projectionHash, + ); + } + if (provenance.projectionLag !== undefined) { + if ( + !Number.isSafeInteger(provenance.projectionLag) || + provenance.projectionLag < 0 || + provenance.projectionLag > 1_000_000 + ) { + throw invalidInput("config", "projection-lag"); + } + headers["X-Programmable-Projection-Lag"] = String( + provenance.projectionLag, + ); + } + if (provenance.reconciledAt !== undefined) { + const date = new Date(provenance.reconciledAt); + if ( + Number.isNaN(date.valueOf()) || + date.toISOString() !== provenance.reconciledAt + ) { + throw invalidInput("config", "reconciled-at"); + } + headers["X-Programmable-Reconciled-At"] = provenance.reconciledAt; + } + if (provenance.releaseVersion !== undefined) { + if ( + !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(provenance.releaseVersion) || + provenance.releaseVersion.length > 64 + ) { + throw invalidInput("config", "release-version"); + } + headers["X-Programmable-Release-Version"] = provenance.releaseVersion; + } + return Object.freeze(headers); +} diff --git a/lib/data-pipeline/candidate-projector-runtime-binding.server.ts b/lib/data-pipeline/candidate-projector-runtime-binding.server.ts new file mode 100644 index 00000000..14edc4a8 --- /dev/null +++ b/lib/data-pipeline/candidate-projector-runtime-binding.server.ts @@ -0,0 +1,450 @@ +import "server-only"; + +import candidateJson from "../../config/data-pipeline-envio-candidate.v1.json"; +import { hexToBytes, type HexBytes32 } from "./codecs"; +import { invalidInput } from "./errors"; +import type { PostgresExecutor } from "./postgres"; +import { + projectorEnvioDeploymentCommitment, + projectorEnvioSchemaCommitment, +} from "./projector-provider-commitments"; +import type { DataPipelineReleaseBinding } from "./release-binding.server"; + +type Environment = Readonly>; + +export const CANDIDATE_PROJECTOR_RUNTIME_MODE = "candidate-backfill"; + +const PUBLIC_INDEXED_FLAGS = Object.freeze([ + "INDEXED_EXPLORE_LIST_READS_ENABLED", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED", + "INDEXED_EXPLORE_CHART_READS_ENABLED", + "INDEXED_CREATOR_PROFILE_READS_ENABLED", + "INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED", + "INDEXED_LAUNCH_LOOKUP_ENABLED", + "INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", + "INDEXED_READ_SHADOW_COMPARE_ENABLED", +] as const); + +const EXACT = Object.freeze({ + activeProductionLabel: "production-1e7c381", + activeProductionEndpoint: + "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + deploymentLabel: "production-7f24e63", + endpoint: "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + redactedIdentity: "envio:production-7f24e63", + sourceCommit: "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + mirrorCommit: "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + configSha256: + "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + schemaSha256: + "0xdf3d65e033e96d7ebbe62b6f114b6a30f10c8944e5c6fca6b020c3130bb738c0", + handlerSha256: + "0x9f68d05cc8907f1c422cb2584b338ed42375eb4b6033cbec1338d00577267491", + sourceRegistrySha256: + "0x55e7a7c7cd0e419a6be0f9c784990f5048b9845e46e329939025c3fab405565a", + eventSetSha256: + "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + eventCount: 51, + deploymentCommitment: + "0xa4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259", + schemaCommitment: + "0x5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1", + providerDeploymentId: "d08b62a6-74fb-5e0a-a698-dc6877150db4", + initializationInputCommitment: + "0xe3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44", + initializedAt: "2026-08-01T09:00:00.000Z", +} as const); + +const ZERO_BYTES32 = `0x${"00".repeat(32)}`; + +function invalidCandidateBinding(): never { + throw invalidInput("config", "candidate-projector-runtime-binding"); +} + +function assertReviewedCandidateConfig(): void { + if ( + candidateJson.schemaVersion !== 1 || + candidateJson.status !== "deployed-synced-audited-not-promoted" || + candidateJson.deploymentLabel !== EXACT.deploymentLabel || + candidateJson.graphqlEndpoint !== EXACT.endpoint || + candidateJson.sourceCommit !== EXACT.sourceCommit || + candidateJson.configSha256 !== EXACT.configSha256 || + candidateJson.schemaSha256 !== EXACT.schemaSha256 || + candidateJson.handlerSha256 !== EXACT.handlerSha256 || + candidateJson.sourceRegistrySha256 !== EXACT.sourceRegistrySha256 || + candidateJson.eventSetSha256 !== EXACT.eventSetSha256 || + candidateJson.eventCount !== EXACT.eventCount || + candidateJson.redactedIdentity !== EXACT.redactedIdentity || + candidateJson.deploymentCommitment !== EXACT.deploymentCommitment || + candidateJson.schemaCommitment !== EXACT.schemaCommitment || + candidateJson.policy.databaseMode !== "candidate-only" || + candidateJson.policy.legacyProductionDeploymentRegistered !== false || + candidateJson.policy.publicationAllowedBeforePromotion !== false || + candidateJson.policy.promotion !== "atomic-attestation-required" + ) { + return invalidCandidateBinding(); + } +} + +export type CandidateProjectorRuntimeBinding = Readonly<{ + mode: typeof CANDIDATE_PROJECTOR_RUNTIME_MODE; + releaseBinding: DataPipelineReleaseBinding; + mirrorCommit: string; + databaseBootstrap: Readonly<{ + mode: "candidate-only"; + providerDeploymentId: string; + deploymentCommitment: HexBytes32; + schemaCommitment: HexBytes32; + initializationInputCommitment: HexBytes32; + initializedAt: string; + }>; + promotionTransition: Readonly<{ + requiredRuntimeMode: "release"; + requiredCanonicalEndpoint: string; + requiredCanonicalIdentity: string; + requiresDatabasePromotionAttestation: true; + }>; +}>; + +export type CandidateDatabasePromotionBinding = Readonly<{ + providerDeploymentId: string; + deploymentCommitment: HexBytes32; + schemaCommitment: HexBytes32; + initializationInputCommitment: HexBytes32; + initializedAt: string; + productCommit: string; + stagedDeploymentId: string; +}>; + +export type ProjectorRuntimeBindingSelection = + | Readonly<{ + mode: typeof CANDIDATE_PROJECTOR_RUNTIME_MODE; + releaseBinding: DataPipelineReleaseBinding; + candidate: CandidateProjectorRuntimeBinding; + promotedDatabase: null; + }> + | Readonly<{ + mode: "release"; + releaseBinding: DataPipelineReleaseBinding; + candidate: null; + promotedDatabase: CandidateDatabasePromotionBinding | null; + }>; + +function isExactCandidateReleaseBinding( + binding: DataPipelineReleaseBinding, +): boolean { + const envio = binding.envio; + return ( + envio.deploymentLabel === EXACT.deploymentLabel && + envio.graphqlEndpoint === EXACT.endpoint && + envio.schemaVersion === "1" && + envio.sourceCommit === EXACT.sourceCommit && + envio.configSha256 === EXACT.configSha256 && + envio.schemaSha256 === EXACT.schemaSha256 && + envio.handlerSha256 === EXACT.handlerSha256 && + envio.sourceRegistrySha256 === EXACT.sourceRegistrySha256 && + envio.eventSetSha256 === EXACT.eventSetSha256 && + envio.eventCount === EXACT.eventCount + ); +} + +function candidateReleaseBinding( + activeProductionBinding: DataPipelineReleaseBinding, +): DataPipelineReleaseBinding { + if (isExactCandidateReleaseBinding(activeProductionBinding)) { + return activeProductionBinding; + } + if ( + activeProductionBinding.envio.deploymentLabel !== + EXACT.activeProductionLabel || + activeProductionBinding.envio.graphqlEndpoint !== + EXACT.activeProductionEndpoint + ) { + return invalidCandidateBinding(); + } + return Object.freeze({ + ...activeProductionBinding, + envio: Object.freeze({ + deploymentLabel: EXACT.deploymentLabel, + graphqlEndpoint: EXACT.endpoint, + schemaVersion: "1" as const, + sourceCommit: EXACT.sourceCommit, + configSha256: EXACT.configSha256, + schemaSha256: EXACT.schemaSha256, + handlerSha256: EXACT.handlerSha256, + sourceRegistrySha256: EXACT.sourceRegistrySha256, + eventSetSha256: EXACT.eventSetSha256, + eventCount: EXACT.eventCount, + }), + }) satisfies DataPipelineReleaseBinding; +} + +function nonzeroBytes32(value: unknown): HexBytes32 { + if ( + typeof value !== "string" || + !/^0x[0-9a-f]{64}$/u.test(value) || + value === ZERO_BYTES32 + ) { + return invalidCandidateBinding(); + } + return value as HexBytes32; +} + +function canonicalTimestamp(value: unknown): string { + if (typeof value !== "string" || value.length > 32) { + return invalidCandidateBinding(); + } + const parsed = Date.parse(value); + if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) { + return invalidCandidateBinding(); + } + return value; +} + +function loadCandidateDatabasePromotionBinding( + env: Environment, +): CandidateDatabasePromotionBinding { + const productCommit = env.VERCEL_GIT_COMMIT_SHA; + const stagedDeploymentId = env.VERCEL_DEPLOYMENT_ID; + if ( + typeof productCommit !== "string" || + !/^[0-9a-f]{40}$/u.test(productCommit) || + productCommit === "0".repeat(40) || + typeof stagedDeploymentId !== "string" || + !/^dpl_[A-Za-z0-9]{20,128}$/u.test(stagedDeploymentId) + ) { + return invalidCandidateBinding(); + } + return Object.freeze({ + providerDeploymentId: EXACT.providerDeploymentId, + deploymentCommitment: nonzeroBytes32(EXACT.deploymentCommitment), + schemaCommitment: nonzeroBytes32(EXACT.schemaCommitment), + initializationInputCommitment: nonzeroBytes32( + EXACT.initializationInputCommitment, + ), + initializedAt: canonicalTimestamp(EXACT.initializedAt), + productCommit, + stagedDeploymentId, + }); +} + +export function loadCandidateProjectorRuntimeBinding(input: Readonly<{ + env: Environment; + activeProductionBinding: DataPipelineReleaseBinding; +}>): CandidateProjectorRuntimeBinding { + assertReviewedCandidateConfig(); + if ( + input.env.PROGRAMMABLE_PROJECTOR_BINDING_MODE !== + CANDIDATE_PROJECTOR_RUNTIME_MODE || + input.env.PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT !== + EXACT.mirrorCommit || + input.env.PROGRAMMABLE_ENVIO_GRAPHQL_URL !== EXACT.endpoint || + input.env.PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY !== + EXACT.redactedIdentity || + PUBLIC_INDEXED_FLAGS.some((name) => input.env[name] !== "false") || + (!isExactCandidateReleaseBinding(input.activeProductionBinding) && + (input.activeProductionBinding.envio.deploymentLabel !== + EXACT.activeProductionLabel || + input.activeProductionBinding.envio.graphqlEndpoint !== + EXACT.activeProductionEndpoint)) + ) { + return invalidCandidateBinding(); + } + + const releaseBinding = candidateReleaseBinding(input.activeProductionBinding); + + if ( + projectorEnvioDeploymentCommitment({ + endpoint: EXACT.endpoint, + redactedIdentity: EXACT.redactedIdentity, + binding: releaseBinding, + }) !== EXACT.deploymentCommitment || + projectorEnvioSchemaCommitment(releaseBinding) !== EXACT.schemaCommitment + ) { + return invalidCandidateBinding(); + } + + return Object.freeze({ + mode: CANDIDATE_PROJECTOR_RUNTIME_MODE, + releaseBinding, + mirrorCommit: EXACT.mirrorCommit, + databaseBootstrap: Object.freeze({ + mode: "candidate-only" as const, + providerDeploymentId: EXACT.providerDeploymentId, + deploymentCommitment: EXACT.deploymentCommitment, + schemaCommitment: EXACT.schemaCommitment, + initializationInputCommitment: EXACT.initializationInputCommitment, + initializedAt: EXACT.initializedAt, + }), + promotionTransition: Object.freeze({ + requiredRuntimeMode: "release" as const, + requiredCanonicalEndpoint: EXACT.endpoint, + requiredCanonicalIdentity: EXACT.redactedIdentity, + requiresDatabasePromotionAttestation: true as const, + }), + }); +} + +export function selectProjectorRuntimeBinding(input: Readonly<{ + env: Environment; + canonicalBinding: DataPipelineReleaseBinding; +}>): ProjectorRuntimeBindingSelection { + const mode = input.env.PROGRAMMABLE_PROJECTOR_BINDING_MODE; + if (mode === CANDIDATE_PROJECTOR_RUNTIME_MODE) { + const candidate = loadCandidateProjectorRuntimeBinding({ + env: input.env, + activeProductionBinding: input.canonicalBinding, + }); + return Object.freeze({ + mode: CANDIDATE_PROJECTOR_RUNTIME_MODE, + releaseBinding: candidate.releaseBinding, + candidate, + promotedDatabase: null, + }); + } + if (mode !== undefined && mode !== "" && mode !== "release") { + return invalidCandidateBinding(); + } + if (!isExactCandidateReleaseBinding(input.canonicalBinding)) { + if ( + input.canonicalBinding.envio.deploymentLabel !== + EXACT.activeProductionLabel || + input.canonicalBinding.envio.graphqlEndpoint !== + EXACT.activeProductionEndpoint + ) { + return invalidCandidateBinding(); + } + return Object.freeze({ + mode: "release" as const, + releaseBinding: input.canonicalBinding, + candidate: null, + promotedDatabase: null, + }); + } + if ( + input.env.PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT !== + EXACT.mirrorCommit || + input.env.PROGRAMMABLE_ENVIO_GRAPHQL_URL !== EXACT.endpoint || + input.env.PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY !== + EXACT.redactedIdentity + ) { + return invalidCandidateBinding(); + } + return Object.freeze({ + mode: "release" as const, + releaseBinding: input.canonicalBinding, + candidate: null, + promotedDatabase: loadCandidateDatabasePromotionBinding(input.env), + }); +} + +export async function assertCandidateDatabaseBootstrapState(input: Readonly<{ + executor: PostgresExecutor; + binding: CandidateProjectorRuntimeBinding; +}>): Promise { + if ( + input.binding.mode !== CANDIDATE_PROJECTOR_RUNTIME_MODE || + input.binding.releaseBinding.envio.graphqlEndpoint !== EXACT.endpoint || + input.binding.releaseBinding.envio.deploymentLabel !== EXACT.deploymentLabel || + input.binding.databaseBootstrap.mode !== "candidate-only" || + input.binding.databaseBootstrap.providerDeploymentId !== + EXACT.providerDeploymentId || + input.binding.databaseBootstrap.deploymentCommitment !== + EXACT.deploymentCommitment || + input.binding.databaseBootstrap.schemaCommitment !== + EXACT.schemaCommitment || + input.binding.databaseBootstrap.initializationInputCommitment !== + EXACT.initializationInputCommitment || + input.binding.databaseBootstrap.initializedAt !== EXACT.initializedAt + ) { + return invalidCandidateBinding(); + } + await input.executor.transaction(async (transaction) => { + const login = await transaction.query<{ session_user: unknown }>( + "select session_user::text as session_user", + ); + if ( + login.length !== 1 || + login[0]?.session_user !== "programmable_projector_login" + ) { + return invalidCandidateBinding(); + } + await transaction.query("set local role programmable_projector"); + await transaction.query("set local statement_timeout = '1000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + const role = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + if ( + role.length !== 1 || + role[0]?.session_user !== "programmable_projector_login" || + role[0]?.current_role !== "programmable_projector" + ) { + return invalidCandidateBinding(); + } + const rows = await transaction.query<{ verified: unknown }>( + "select programmable_private.verify_candidate_database_unpromoted_v1($1::uuid, $2::bytea, $3::bytea, $4::bytea, $5::timestamptz) as verified", + [ + input.binding.databaseBootstrap.providerDeploymentId, + hexToBytes(input.binding.databaseBootstrap.deploymentCommitment), + hexToBytes(input.binding.databaseBootstrap.schemaCommitment), + hexToBytes(input.binding.databaseBootstrap.initializationInputCommitment), + input.binding.databaseBootstrap.initializedAt, + ], + ); + if (rows.length !== 1 || rows[0]?.verified !== true) { + return invalidCandidateBinding(); + } + }); +} + +export async function assertCandidateDatabasePromotedState(input: Readonly<{ + executor: PostgresExecutor; + binding: CandidateDatabasePromotionBinding; +}>): Promise { + await input.executor.transaction(async (transaction) => { + const login = await transaction.query<{ session_user: unknown }>( + "select session_user::text as session_user", + ); + if ( + login.length !== 1 || + login[0]?.session_user !== "programmable_projector_login" + ) { + return invalidCandidateBinding(); + } + await transaction.query("set local role programmable_projector"); + await transaction.query("set local statement_timeout = '1000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + const role = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + if ( + role.length !== 1 || + role[0]?.session_user !== "programmable_projector_login" || + role[0]?.current_role !== "programmable_projector" + ) { + return invalidCandidateBinding(); + } + const rows = await transaction.query<{ verified: unknown }>( + "select programmable_private.verify_candidate_database_promoted_v2($1::uuid, $2::bytea, $3::bytea, $4::bytea, $5::timestamptz, $6::text, $7::text) as verified", + [ + input.binding.providerDeploymentId, + hexToBytes(input.binding.deploymentCommitment), + hexToBytes(input.binding.schemaCommitment), + hexToBytes(input.binding.initializationInputCommitment), + input.binding.initializedAt, + input.binding.productCommit, + input.binding.stagedDeploymentId, + ], + ); + if (rows.length !== 1 || rows[0]?.verified !== true) { + return invalidCandidateBinding(); + } + }); +} diff --git a/lib/data-pipeline/canonical-fingerprint.ts b/lib/data-pipeline/canonical-fingerprint.ts new file mode 100644 index 00000000..68243f5f --- /dev/null +++ b/lib/data-pipeline/canonical-fingerprint.ts @@ -0,0 +1,417 @@ +import "server-only"; + +import { keccak256, type Hex } from "viem"; + +export type CanonicalJsonValue = + | null + | boolean + | number + | string + | CanonicalJsonValue[] + | { [key: string]: CanonicalJsonValue }; + +export type UnsignedCanonicalInput = string | number | bigint; + +export type OccurrenceFingerprintInput = { + chain_id: UnsignedCanonicalInput; + transaction_hash: string; + receipt_log_ordinal: UnsignedCanonicalInput; + block_number: UnsignedCanonicalInput; + block_hash: string; + transaction_index: UnsignedCanonicalInput; + block_global_log_index: UnsignedCanonicalInput; + source_address: string; + event_signature: string; + ordered_topics: string[]; + raw_data: string; + decoded_payload: CanonicalJsonValue; + payload_hash: string; + decoder_version: string; + abi_event_set_commitment: string; + release_id: string; + model_id: string; + envio_candidate_id: string; + provider_cursor: string; + block_timestamp_unix: UnsignedCanonicalInput; +}; + +export type OccurrenceFingerprintReference = { + transaction_hash: string; + receipt_log_ordinal: UnsignedCanonicalInput; + block_hash: string; + role: string; +}; + +export type AllocationFingerprintInput = { + chain_id: UnsignedCanonicalInput; + release_id: string; + model_id: string; + vault: string; + factory_transaction_hash: string; + factory_receipt_log_ordinal: UnsignedCanonicalInput; + factory_block_hash: string; + creation_block_number: UnsignedCanonicalInput; + creation_transaction_index: UnsignedCanonicalInput; + ordered_beneficiaries: string[]; + ordered_shares_bps: UnsignedCanonicalInput[]; + allocation_hash: string; + configuration_hash: string; + active_configuration_hash: string | null; + artifact_creation_code_commitment: string; + required_occurrences: OccurrenceFingerprintReference[]; +}; + +export type EvidenceFingerprintInput = { + allocation_fingerprint: string; + recovery_method: string; + evidence_version: string; + top_level_destination: string | null; + method_selector: string | null; + transaction_input_hash: string | null; + constructor_arguments_commitment: string; + local_init_code_hash: string; + create2_salt: string; + local_create2_address: string; + historical_enrichment_status: string; + getter_block_hash: string | null; + getter_result_hash_a: string | null; + getter_result_hash_b: string | null; + predict_result_hash_a: string | null; + predict_result_hash_b: string | null; + predicted_vault_a: string | null; + predicted_vault_b: string | null; + selected_rpc_result_hash_a: string; + selected_rpc_result_hash_b: string; + selected_rpc_transaction_receipt_hash_a: string | null; + selected_rpc_transaction_receipt_hash_b: string | null; + extra_note: string | null; + required_occurrence_fingerprints: string[]; +}; + +export type CanonicalFingerprintDomain = + | "occurrence" + | "allocation" + | "evidence"; + +export type CanonicalFingerprintInput = + | OccurrenceFingerprintInput + | AllocationFingerprintInput + | EvidenceFingerprintInput; + +const textEncoder = new TextEncoder(); +const domainPrefix = { + occurrence: textEncoder.encode("programmable:occurrence:v1\0"), + allocation: textEncoder.encode("programmable:allocation:v1\0"), + evidence: textEncoder.encode("programmable:evidence:v1\0"), +} as const; + +function concatenate(...chunks: Uint8Array[]): Uint8Array { + const output = new Uint8Array( + chunks.reduce((length, chunk) => length + chunk.length, 0), + ); + let cursor = 0; + for (const chunk of chunks) { + output.set(chunk, cursor); + cursor += chunk.length; + } + return output; +} + +export function decodeCanonicalFingerprintHex( + value: string, + byteLength?: number, +): Uint8Array { + if (!value.startsWith("0x")) { + throw new Error("canonical hex input requires 0x"); + } + const digits = value.slice(2); + if (digits.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(digits)) { + throw new Error("canonical hex input must be even-length hexadecimal"); + } + if (byteLength !== undefined && digits.length !== byteLength * 2) { + throw new Error( + `canonical hex input must be exactly ${byteLength} bytes`, + ); + } + const bytes = new Uint8Array(digits.length / 2); + for (let index = 0; index < digits.length; index += 2) { + bytes[index / 2] = Number.parseInt(digits.slice(index, index + 2), 16); + } + return bytes; +} + +function parseUnsigned(value: UnsignedCanonicalInput): bigint { + if (typeof value === "bigint") return value; + if (typeof value === "number") { + if (!Number.isSafeInteger(value)) { + throw new Error("unsigned number must be a safe integer"); + } + return BigInt(value); + } + if (!/^(0|[1-9]\d*)$/.test(value)) { + throw new Error("unsigned string must use canonical decimal encoding"); + } + return BigInt(value); +} + +function encodeUnsigned( + value: UnsignedCanonicalInput, + width: number, +): Uint8Array { + let integer = parseUnsigned(value); + if (integer < 0n || integer >= 1n << BigInt(width * 8)) { + throw new Error(`unsigned integer exceeds ${width * 8} bits`); + } + const encoded = new Uint8Array(width); + for (let index = width - 1; index >= 0; index -= 1) { + encoded[index] = Number(integer & 0xffn); + integer >>= 8n; + } + return encoded; +} + +function frameBytes(value: Uint8Array): Uint8Array { + return concatenate(encodeUnsigned(value.length, 4), value); +} + +function assertValidUnicode(value: string): void { + for (let index = 0; index < value.length; index += 1) { + const codeUnit = value.charCodeAt(index); + if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { + const next = value.charCodeAt(index + 1); + if ( + index + 1 >= value.length || + next < 0xdc00 || + next > 0xdfff + ) { + throw new Error("canonical JSON contains an unpaired high surrogate"); + } + index += 1; + } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { + throw new Error("canonical JSON contains an unpaired low surrogate"); + } + } +} + +function frameString(value: string): Uint8Array { + assertValidUnicode(value); + return frameBytes(textEncoder.encode(value)); +} + +function frameNullable( + value: T | null, + encodePresent: (present: T) => Uint8Array, +): Uint8Array { + return value === null + ? Uint8Array.of(0) + : concatenate(Uint8Array.of(1), encodePresent(value)); +} + +function frameArray( + values: readonly T[], + encodeElement: (value: T) => Uint8Array, +): Uint8Array { + return concatenate( + encodeUnsigned(values.length, 4), + ...values.map(encodeElement), + ); +} + +export function canonicalizeFingerprintJson( + value: CanonicalJsonValue, +): string { + if (value === null || typeof value === "boolean") { + return JSON.stringify(value); + } + if (typeof value === "string") { + assertValidUnicode(value); + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value) || !Number.isSafeInteger(value)) { + throw new Error( + "canonical JSON numbers must be finite safe integers; large integers use strings", + ); + } + return JSON.stringify(value); + } + if (Array.isArray(value)) { + return `[${value.map(canonicalizeFingerprintJson).join(",")}]`; + } + const members = Object.keys(value) + .sort() + .map((key) => { + assertValidUnicode(key); + return `${JSON.stringify(key)}:${canonicalizeFingerprintJson(value[key])}`; + }); + return `{${members.join(",")}}`; +} + +function occurrencePreimage(input: OccurrenceFingerprintInput): Uint8Array { + return concatenate( + domainPrefix.occurrence, + encodeUnsigned(input.chain_id, 8), + decodeCanonicalFingerprintHex(input.transaction_hash, 32), + encodeUnsigned(input.receipt_log_ordinal, 4), + encodeUnsigned(input.block_number, 8), + decodeCanonicalFingerprintHex(input.block_hash, 32), + encodeUnsigned(input.transaction_index, 4), + encodeUnsigned(input.block_global_log_index, 4), + decodeCanonicalFingerprintHex(input.source_address, 20), + decodeCanonicalFingerprintHex(input.event_signature, 32), + frameArray(input.ordered_topics, (topic) => + decodeCanonicalFingerprintHex(topic, 32), + ), + frameBytes(decodeCanonicalFingerprintHex(input.raw_data)), + frameString(canonicalizeFingerprintJson(input.decoded_payload)), + decodeCanonicalFingerprintHex(input.payload_hash, 32), + frameString(input.decoder_version), + decodeCanonicalFingerprintHex(input.abi_event_set_commitment, 32), + frameString(input.release_id), + frameString(input.model_id), + frameString(input.envio_candidate_id), + frameString(input.provider_cursor), + encodeUnsigned(input.block_timestamp_unix, 8), + ); +} + +function occurrenceReferencePreimage( + reference: OccurrenceFingerprintReference, +): Uint8Array { + return concatenate( + decodeCanonicalFingerprintHex(reference.transaction_hash, 32), + encodeUnsigned(reference.receipt_log_ordinal, 4), + decodeCanonicalFingerprintHex(reference.block_hash, 32), + frameString(reference.role), + ); +} + +function allocationPreimage(input: AllocationFingerprintInput): Uint8Array { + return concatenate( + domainPrefix.allocation, + encodeUnsigned(input.chain_id, 8), + frameString(input.release_id), + frameString(input.model_id), + decodeCanonicalFingerprintHex(input.vault, 20), + decodeCanonicalFingerprintHex(input.factory_transaction_hash, 32), + encodeUnsigned(input.factory_receipt_log_ordinal, 4), + decodeCanonicalFingerprintHex(input.factory_block_hash, 32), + encodeUnsigned(input.creation_block_number, 8), + encodeUnsigned(input.creation_transaction_index, 4), + frameArray(input.ordered_beneficiaries, (address) => + decodeCanonicalFingerprintHex(address, 20), + ), + frameArray(input.ordered_shares_bps, (share) => + encodeUnsigned(share, 2), + ), + decodeCanonicalFingerprintHex(input.allocation_hash, 32), + decodeCanonicalFingerprintHex(input.configuration_hash, 32), + frameNullable(input.active_configuration_hash, (hash) => + decodeCanonicalFingerprintHex(hash, 32), + ), + decodeCanonicalFingerprintHex( + input.artifact_creation_code_commitment, + 32, + ), + frameArray(input.required_occurrences, occurrenceReferencePreimage), + ); +} + +function evidencePreimage(input: EvidenceFingerprintInput): Uint8Array { + const nullableHash = (value: string | null) => + frameNullable(value, (hash) => + decodeCanonicalFingerprintHex(hash, 32), + ); + return concatenate( + domainPrefix.evidence, + decodeCanonicalFingerprintHex(input.allocation_fingerprint, 32), + frameString(input.recovery_method), + frameString(input.evidence_version), + frameNullable(input.top_level_destination, (address) => + decodeCanonicalFingerprintHex(address, 20), + ), + frameNullable(input.method_selector, (selector) => + decodeCanonicalFingerprintHex(selector, 4), + ), + nullableHash(input.transaction_input_hash), + decodeCanonicalFingerprintHex(input.constructor_arguments_commitment, 32), + decodeCanonicalFingerprintHex(input.local_init_code_hash, 32), + decodeCanonicalFingerprintHex(input.create2_salt, 32), + decodeCanonicalFingerprintHex(input.local_create2_address, 20), + frameString(input.historical_enrichment_status), + nullableHash(input.getter_block_hash), + nullableHash(input.getter_result_hash_a), + nullableHash(input.getter_result_hash_b), + nullableHash(input.predict_result_hash_a), + nullableHash(input.predict_result_hash_b), + frameNullable(input.predicted_vault_a, (address) => + decodeCanonicalFingerprintHex(address, 20), + ), + frameNullable(input.predicted_vault_b, (address) => + decodeCanonicalFingerprintHex(address, 20), + ), + decodeCanonicalFingerprintHex(input.selected_rpc_result_hash_a, 32), + decodeCanonicalFingerprintHex(input.selected_rpc_result_hash_b, 32), + nullableHash(input.selected_rpc_transaction_receipt_hash_a), + nullableHash(input.selected_rpc_transaction_receipt_hash_b), + frameNullable(input.extra_note, frameString), + frameArray(input.required_occurrence_fingerprints, (fingerprint) => + decodeCanonicalFingerprintHex(fingerprint, 32), + ), + ); +} + +export function canonicalFingerprintPreimageV1( + domain: "occurrence", + input: OccurrenceFingerprintInput, +): Uint8Array; +export function canonicalFingerprintPreimageV1( + domain: "allocation", + input: AllocationFingerprintInput, +): Uint8Array; +export function canonicalFingerprintPreimageV1( + domain: "evidence", + input: EvidenceFingerprintInput, +): Uint8Array; +export function canonicalFingerprintPreimageV1( + domain: CanonicalFingerprintDomain, + input: CanonicalFingerprintInput, +): Uint8Array { + if (domain === "occurrence") { + return occurrencePreimage(input as OccurrenceFingerprintInput); + } + if (domain === "allocation") { + return allocationPreimage(input as AllocationFingerprintInput); + } + return evidencePreimage(input as EvidenceFingerprintInput); +} + +export function canonicalFingerprintV1( + domain: "occurrence", + input: OccurrenceFingerprintInput, +): Hex; +export function canonicalFingerprintV1( + domain: "allocation", + input: AllocationFingerprintInput, +): Hex; +export function canonicalFingerprintV1( + domain: "evidence", + input: EvidenceFingerprintInput, +): Hex; +export function canonicalFingerprintV1( + domain: CanonicalFingerprintDomain, + input: CanonicalFingerprintInput, +): Hex { + return keccak256( + canonicalFingerprintPreimageV1( + domain as "occurrence", + input as OccurrenceFingerprintInput, + ), + ); +} + +export function canonicalFingerprintBytesToHex(value: Uint8Array): Hex { + return `0x${Array.from(value, (byte) => + byte.toString(16).padStart(2, "0"), + ).join("")}`; +} diff --git a/lib/data-pipeline/circuit.ts b/lib/data-pipeline/circuit.ts new file mode 100644 index 00000000..2b831b2e --- /dev/null +++ b/lib/data-pipeline/circuit.ts @@ -0,0 +1,83 @@ +import "server-only"; + +import { + DataPipelineError, + dataPipelineError, + type DataPipelineDependency, +} from "./errors"; + +export type CircuitState = "closed" | "open" | "half-open"; + +export type CircuitSnapshot = { + state: CircuitState; + consecutiveFailures: number; + openUntil: number; + halfOpenProbeActive: boolean; +}; + +export class CircuitBreaker { + private consecutiveFailures = 0; + private openUntil = 0; + private halfOpenProbeActive = false; + private readonly dependency: DataPipelineDependency; + private readonly now: () => number; + + constructor(input: { + dependency: DataPipelineDependency; + now?: () => number; + }) { + this.dependency = input.dependency; + this.now = input.now ?? Date.now; + } + + snapshot(): CircuitSnapshot { + const now = this.now(); + return { + state: + this.openUntil > now + ? "open" + : this.consecutiveFailures >= 3 + ? "half-open" + : "closed", + consecutiveFailures: this.consecutiveFailures, + openUntil: this.openUntil, + halfOpenProbeActive: this.halfOpenProbeActive, + }; + } + + async execute(operation: () => Promise): Promise { + const now = this.now(); + const isHalfOpen = this.consecutiveFailures >= 3 && this.openUntil <= now; + if (this.openUntil > now || (isHalfOpen && this.halfOpenProbeActive)) { + throw dataPipelineError({ + dependency: this.dependency, + code: "circuit_open", + retryable: true, + countsTowardCircuit: false, + metadata: { state: this.openUntil > now ? "open" : "half-open" }, + }); + } + if (isHalfOpen) this.halfOpenProbeActive = true; + + try { + const result = await operation(); + this.consecutiveFailures = 0; + this.openUntil = 0; + return result; + } catch (error) { + const counts = + error instanceof DataPipelineError + ? error.countsTowardCircuit + : true; + if (counts) { + this.consecutiveFailures += 1; + if (this.consecutiveFailures >= 3) { + this.openUntil = this.now() + 30_000; + } + } + throw error; + } finally { + if (isHalfOpen) this.halfOpenProbeActive = false; + } + } +} diff --git a/lib/data-pipeline/classic-v2-reconciler-route-builder.server.ts b/lib/data-pipeline/classic-v2-reconciler-route-builder.server.ts new file mode 100644 index 00000000..b6d1161c --- /dev/null +++ b/lib/data-pipeline/classic-v2-reconciler-route-builder.server.ts @@ -0,0 +1,1793 @@ +import "server-only"; + +import { + decodeEventLog, + decodeFunctionData, + decodeFunctionResult, + encodeFunctionData, + getAddress, + isAddress, + parseAbi, + parseAbiItem, + toEventSelector, + type Abi, + type AbiEvent, + type Address, + type Hex, +} from "viem"; + +import deployment from "../../contracts/deployments/mainnet-classic-v2.json"; +import dependencies from "../../contracts/dependencies/ethereum-mainnet.json"; +import type { CanonicalJsonValue } from "./canonical-fingerprint"; +import { canonicalBytes32, type HexBytes32 } from "./codecs"; +import { + assembleReconcilerCorpusPages, + createReconcilerCorpusManifest, +} from "./reconciler-corpus-partitions"; +import { + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import type { + ExactBlockRpcClient, + ExactBlockRpcLog, + ExactBlockRpcReceipt, + ExactBlockRpcTransaction, +} from "./reconciler-exact-block-reader.server"; +import type { + ReconcilerPreParityContract, + ReconcilerRouteKey, +} from "./reconciler-preparity"; +import { + creatorFeesClaimedEvent, + creatorFeeHookReadAbi, + stateViewReadAbi, + uerc20ReadAbi, +} from "../onchain/abis"; + +const ZERO_ADDRESS = `0x${"00".repeat(20)}` as Address; +const MAXIMUM_LOGS_PER_REQUEST = 20_000; +const MAXIMUM_POOLS_PER_LOG_REQUEST = 64; +const CALLS_PER_LAUNCH = 14; +const EXPECTED_INITIAL_TICK = 204_200; +const EXPECTED_TICK_LOWER = -887_200; + +export const CLASSIC_V2_RECONCILER_ROUTE_KEYS = Object.freeze([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", +] as const satisfies readonly ReconcilerRouteKey[]); + +// QuickNode and the portable fallback both support this exact range. Keeping +// every request at the common boundary avoids provider-dependent corpora. +export const CLASSIC_V2_RECONCILER_LOG_BLOCK_RANGE = 10_000n; + +export type ClassicV2ReconcilerRouteContribution = Readonly<{ + tokens: readonly CanonicalJsonValue[]; + charts: readonly CanonicalJsonValue[]; +}>; + +const launcherAbi = parseAbi([ + "function launch((string name,string symbol,uint16 totalSwapFeeBps,bytes32 creatorSalt,(string description,string website,string image,bytes extraData) metadata) parameters) payable", + "function predictTokenAddress(string name,string symbol,address creator,bytes32 creatorSalt) view returns (address token,bytes32 effectiveGraffiti)", + "function predictPositionRecipient(address token,address creator) view returns (address)", + "function poolKey(address token) view returns (address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks)", + "function launchHashOf(address token) view returns (bytes32)", + "function poolManager() view returns (address)", + "function feeHook() view returns (address)", + "function MIN_INITIAL_BUY_WEI() view returns (uint256)", +]); + +const hookInfrastructureAbi = parseAbi([ + "function poolManager() view returns (address)", + "function launcherFeeRecipient() view returns (address)", + "function TICK_SPACING() view returns (int24)", +]); + +const launchedEvent = parseAbiItem( + "event MemeTokenLaunched(address indexed creator,address indexed token,bytes32 indexed poolId,address feeHook,address positionRecipient,uint256 positionTokenId,uint16 totalSwapFeeBps,bytes32 launchHash)", +); +const liquidityEvent = parseAbiItem( + "event MemeLiquidityConfigured(address indexed token,uint256 totalSupply,uint256 tokenLiquidityAmount,uint256 lockedTokenDust,int24 initialTick,int24 tickLower,int24 tickUpper,uint24 lpFeePips,bytes32 launchHash)", +); +const initialBuyEvent = parseAbiItem( + "event MemeCreatorInitialBuy(address indexed creator,address indexed token,bytes32 indexed poolId,uint256 nativeAmount,uint256 tokenAmount,bytes32 launchHash)", +); +const registeredEvent = parseAbiItem( + "event PoolRegistered(bytes32 indexed poolId,address indexed token,address indexed creator,address registrar,uint16 totalSwapFeeBps)", +); +const disclosureEvent = parseAbiItem( + "event PoolFeeDisclosure(bytes32 indexed poolId,address indexed token,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,uint16 launcherFeeBps,uint16 transferTaxBps,uint24 lpFeePips)", +); +const feeAccruedEvent = parseAbiItem( + "event NativeSwapFeesAccrued(bytes32 indexed poolId,address indexed swapSender,uint256 grossNativeAmount,uint256 creatorFee,uint256 launcherFee)", +); +const swapEvent = parseAbiItem( + "event Swap(bytes32 indexed id,address indexed sender,int128 amount0,int128 amount1,uint160 sqrtPriceX96,uint128 liquidity,int24 tick,uint24 fee)", +); + +const LAUNCHER_EVENTS = Object.freeze([ + launchedEvent, + liquidityEvent, + initialBuyEvent, +]); +const HOOK_EVENTS = Object.freeze([ + registeredEvent, + disclosureEvent, + feeAccruedEvent, + creatorFeesClaimedEvent, +]); + +type Json = CanonicalJsonValue; + +type DecodedLog = Readonly<{ + eventName: string; + args: Readonly>; + log: ExactBlockRpcLog; +}>; + +type CallSpec = Readonly<{ + to: Address; + data: Hex; + decode: (data: Hex) => unknown; +}>; + +type LaunchRecord = Readonly<{ + creator: Address; + token: Address; + poolId: HexBytes32; + hook: Address; + positionRecipient: Address; + positionTokenId: bigint; + totalSwapFeeBps: number; + launchHash: HexBytes32; + blockNumber: bigint; + blockHash: HexBytes32; + transactionHash: HexBytes32; + transactionIndex: number; + blockGlobalLogIndex: number; + log: ExactBlockRpcLog; +}>; + +type Release = Readonly<{ + launcher: Address; + hook: Address; + hookFactory: Address; + positionForwarderFactory: Address; + treasury: Address; + poolManager: Address; + stateView: Address; + startBlock: bigint; + runtime: ReadonlyArray>; +}>; + +type LaunchTransactionEvidence = Readonly<{ + receiptLogIndex: number; + value: bigint; + name: string; + symbol: string; + totalSwapFeeBps: number; + creatorSalt: HexBytes32; + description: string; + website: string; + image: string; + extraData: Hex; +}>; + +type LaunchCompanionEvidence = Readonly<{ + liquidity: DecodedLog; + initialBuy: DecodedLog; + registration: DecodedLog; + disclosure: DecodedLog; +}>; + +function fail(operation: string): never { + throw validationError("uniswap", operation); +} + +function lowerAddress(value: Address): string { + return value.toLowerCase(); +} + +function exactAddress(value: unknown, operation: string): Address { + if (typeof value !== "string" || !isAddress(value)) fail(operation); + return getAddress(value); +} + +function exactBytes32(value: unknown, operation: string): HexBytes32 { + try { + return canonicalBytes32(value); + } catch { + return fail(operation); + } +} + +function exactData(value: unknown, operation: string): Hex { + if ( + typeof value !== "string" || + !/^0x(?:[0-9a-fA-F]{2})*$/u.test(value) + ) { + fail(operation); + } + return value.toLowerCase() as Hex; +} + +function exactText(value: unknown, operation: string): string { + if (typeof value !== "string") fail(operation); + return value; +} + +function integer(value: unknown, operation: string): bigint { + if (typeof value === "bigint") return value; + if (typeof value === "number" && Number.isSafeInteger(value)) { + return BigInt(value); + } + return fail(operation); +} + +function nonnegative(value: unknown, operation: string): bigint { + const parsed = integer(value, operation); + if (parsed < 0n) fail(operation); + return parsed; +} + +function absolute(value: unknown, operation: string): bigint { + const parsed = integer(value, operation); + return parsed < 0n ? -parsed : parsed; +} + +function safeInteger( + value: unknown, + minimum: number, + maximum: number, + operation: string, +): number { + const parsed = integer(value, operation); + if (parsed < BigInt(minimum) || parsed > BigInt(maximum)) fail(operation); + return Number(parsed); +} + +function tuple( + value: unknown, + length: number, + operation: string, +): readonly unknown[] { + if (!Array.isArray(value) || value.length !== length) fail(operation); + return value; +} + +function record( + value: unknown, + operation: string, +): Readonly> { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + fail(operation); + } + return value as Readonly>; +} + +function sameHex(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase(); +} + +function exactRouteKeys(contract: ReconcilerPreParityContract): boolean { + return contract.routeKeys.length === CLASSIC_V2_RECONCILER_ROUTE_KEYS.length && + contract.routeKeys.every( + (routeKey, index) => routeKey === CLASSIC_V2_RECONCILER_ROUTE_KEYS[index], + ); +} + +export function classicV2ReconcilerBlockRanges( + fromBlock: bigint, + toBlock: bigint, +): readonly Readonly<{ fromBlock: bigint; toBlock: bigint }>[] { + if (fromBlock < 0n || toBlock < fromBlock) { + throw invalidInput("rpc", "classic-v2-log-range"); + } + const ranges: Array> = []; + for ( + let start = fromBlock; + start <= toBlock; + start += CLASSIC_V2_RECONCILER_LOG_BLOCK_RANGE + ) { + const end = start + CLASSIC_V2_RECONCILER_LOG_BLOCK_RANGE - 1n; + ranges.push(Object.freeze({ + fromBlock: start, + toBlock: end > toBlock ? toBlock : end, + })); + } + return Object.freeze(ranges); +} + +export function assertClassicV2ReconcilerLaunchCount(count: number): number { + if ( + !Number.isSafeInteger(count) || + count < 1 + ) { + fail("classic-v2-launch-cardinality"); + } + return count; +} + +function callSpec( + to: Address, + abi: Abi, + functionName: string, + args: readonly unknown[] = [], +): CallSpec { + const request = { abi, functionName, args } as never; + return Object.freeze({ + to, + data: encodeFunctionData(request), + decode: (result: Hex) => + decodeFunctionResult({ + abi, + functionName, + data: result, + } as never) as unknown, + }); +} + +function eventMap(events: readonly AbiEvent[]) { + return new Map( + events.map((event) => [toEventSelector(event).toLowerCase(), event]), + ); +} + +function decodeKnownEvent( + eventBySelector: ReadonlyMap, + log: ExactBlockRpcLog, +): DecodedLog { + const selector = log.topics[0]?.toLowerCase(); + const event = selector ? eventBySelector.get(selector) : undefined; + if (!event) fail("classic-v2-log-selector"); + let decoded: ReturnType; + try { + decoded = decodeEventLog({ + abi: [event], + data: log.data, + topics: log.topics as [Hex, ...Hex[]], + strict: true, + }); + } catch { + return fail("classic-v2-log-decode"); + } + if ( + typeof decoded.args !== "object" || + decoded.args === null || + Array.isArray(decoded.args) + ) { + fail("classic-v2-log-args"); + } + return Object.freeze({ + eventName: decoded.eventName, + args: decoded.args as Readonly>, + log, + }); +} + +async function readUncappedLogs(input: { + rpc: ExactBlockRpcClient; + addresses: Address | readonly Address[]; + topics: readonly (Hex | readonly Hex[] | null)[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}): Promise { + const logs = await input.rpc.getLogs({ + addresses: input.addresses, + topics: input.topics, + fromBlock: input.fromBlock, + toBlock: input.toBlock, + maximumLogs: MAXIMUM_LOGS_PER_REQUEST, + signal: input.signal, + }); + if (logs.length < MAXIMUM_LOGS_PER_REQUEST) return logs; + if (input.fromBlock === input.toBlock) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "classic-v2-single-block-log-boundary" }, + }); + } + const midpoint = input.fromBlock + (input.toBlock - input.fromBlock) / 2n; + const [left, right] = await Promise.all([ + readUncappedLogs({ ...input, toBlock: midpoint }), + readUncappedLogs({ ...input, fromBlock: midpoint + 1n }), + ]); + return Object.freeze([...left, ...right]); +} + +async function readLogsInRanges(input: { + rpc: ExactBlockRpcClient; + addresses: Address | readonly Address[]; + events: readonly AbiEvent[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}): Promise { + if (input.toBlock < input.fromBlock) return Object.freeze([]); + const selectorMap = eventMap(input.events); + const selectors = [...selectorMap.keys()] as Hex[]; + const allowedAddresses = new Set( + (Array.isArray(input.addresses) ? input.addresses : [input.addresses]) + .map((address) => lowerAddress(address)), + ); + const output: DecodedLog[] = []; + for (const { fromBlock, toBlock } of classicV2ReconcilerBlockRanges( + input.fromBlock, + input.toBlock, + )) { + const logs = await readUncappedLogs({ + rpc: input.rpc, + addresses: input.addresses, + topics: [selectors], + fromBlock, + toBlock, + signal: input.signal, + }); + if (logs.some((log) => + !allowedAddresses.has(lowerAddress(log.address)) || + !selectorMap.has((log.topics[0] ?? "").toLowerCase()) || + log.blockNumber < fromBlock || + log.blockNumber > toBlock + )) { + fail("classic-v2-log-filter-binding"); + } + output.push(...logs.map((log) => decodeKnownEvent(selectorMap, log))); + } + for (let index = 1; index < output.length; index += 1) { + const previous = output[index - 1]!.log; + const current = output[index]!.log; + if ( + current.blockNumber < previous.blockNumber || + (current.blockNumber === previous.blockNumber && + (current.transactionIndex < previous.transactionIndex || + (current.transactionIndex === previous.transactionIndex && + current.logIndex <= previous.logIndex))) + ) { + fail("classic-v2-log-corpus-order"); + } + } + return Object.freeze(output); +} + +async function readPoolSwapBatches(input: { + rpc: ExactBlockRpcClient; + poolManager: Address; + poolIds: readonly HexBytes32[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}): Promise { + const selectorMap = eventMap([swapEvent]); + const output: DecodedLog[] = []; + for ( + let poolIndex = 0; + poolIndex < input.poolIds.length; + poolIndex += MAXIMUM_POOLS_PER_LOG_REQUEST + ) { + const poolIds = input.poolIds.slice( + poolIndex, + poolIndex + MAXIMUM_POOLS_PER_LOG_REQUEST, + ); + for (const { fromBlock, toBlock } of classicV2ReconcilerBlockRanges( + input.fromBlock, + input.toBlock, + )) { + const logs = await readUncappedLogs({ + rpc: input.rpc, + addresses: input.poolManager, + topics: [toEventSelector(swapEvent), poolIds], + fromBlock, + toBlock, + signal: input.signal, + }); + if (logs.some((log) => + !sameHex(log.address, input.poolManager) || + !sameHex(log.topics[0] ?? "0x", toEventSelector(swapEvent)) || + log.blockNumber < fromBlock || + log.blockNumber > toBlock + )) { + fail("classic-v2-swap-log-filter-binding"); + } + output.push(...logs.map((log) => decodeKnownEvent(selectorMap, log))); + } + } + output.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + return Object.freeze(output); +} + +function resolvedRelease(contract: ReconcilerPreParityContract): Release { + if ( + contract.chainId !== "1" || + contract.releaseId !== "classic-v2" || + contract.modelId !== "classic" || + !exactRouteKeys(contract) + ) { + throw invalidInput("config", "classic-v2-reconciler-release"); + } + if ( + deployment.chainId !== 1 || + deployment.status !== "deployment-and-source-verified" || + deployment.lifecycleEvidence.status !== "verified-current-release" || + deployment.lifecycleEvidence.releaseEligible !== true + ) { + fail("classic-v2-reconciler-manifest"); + } + const launcher = exactAddress( + deployment.addresses.memeLauncher, + "classic-v2-launcher", + ); + const hook = exactAddress(deployment.addresses.feeHook, "classic-v2-hook"); + const hookFactory = exactAddress( + deployment.addresses.hookFactory, + "classic-v2-hook-factory", + ); + const positionForwarderFactory = exactAddress( + deployment.addresses.positionForwarderFactory, + "classic-v2-position-forwarder-factory", + ); + const treasury = exactAddress( + deployment.addresses.treasury, + "classic-v2-treasury", + ); + const poolManager = exactAddress( + dependencies.contracts.poolManager.address, + "classic-v2-pool-manager", + ); + const stateView = exactAddress( + dependencies.contracts.stateView.address, + "classic-v2-state-view", + ); + const startBlock = BigInt(deployment.transactions.memeLauncher.blockNumber); + return Object.freeze({ + launcher, + hook, + hookFactory, + positionForwarderFactory, + treasury, + poolManager, + stateView, + startBlock, + runtime: Object.freeze([ + Object.freeze({ + address: hookFactory, + expectedHash: exactBytes32( + deployment.runtimeCodeHashes.hookFactory, + "classic-v2-hook-factory-runtime-hash", + ), + label: "hook-factory", + }), + Object.freeze({ + address: hook, + expectedHash: exactBytes32( + deployment.runtimeCodeHashes.feeHook, + "classic-v2-hook-runtime-hash", + ), + label: "hook", + }), + Object.freeze({ + address: launcher, + expectedHash: exactBytes32( + deployment.runtimeCodeHashes.memeLauncher, + "classic-v2-launcher-runtime-hash", + ), + label: "launcher", + }), + Object.freeze({ + address: positionForwarderFactory, + expectedHash: exactBytes32( + deployment.runtimeCodeHashes.positionForwarderFactory, + "classic-v2-position-forwarder-factory-runtime-hash", + ), + label: "position-forwarder-factory", + }), + Object.freeze({ + address: poolManager, + expectedHash: exactBytes32( + dependencies.contracts.poolManager.runtimeCodeHash, + "classic-v2-pool-manager-runtime-hash", + ), + label: "pool-manager", + }), + Object.freeze({ + address: stateView, + expectedHash: exactBytes32( + dependencies.contracts.stateView.runtimeCodeHash, + "classic-v2-state-view-runtime-hash", + ), + label: "state-view", + }), + ]), + }); +} + +async function assertRuntime( + rpc: ExactBlockRpcClient, + release: Release, + blockHash: HexBytes32, + signal: AbortSignal, +) { + for (const runtime of release.runtime) { + const codeHash = await rpc.getCodeHash({ + address: runtime.address, + blockHash, + signal, + }); + if (codeHash !== runtime.expectedHash) { + fail(`classic-v2-runtime-${runtime.label}`); + } + } +} + +function oneByKey( + values: readonly DecodedLog[], + key: (value: DecodedLog) => string, + operation: string, +): ReadonlyMap { + const output = new Map(); + for (const value of values) { + const identity = key(value).toLowerCase(); + if (output.has(identity)) fail(operation); + output.set(identity, value); + } + return output; +} + +function launchRecords( + logs: readonly DecodedLog[], + release: Release, +): readonly LaunchRecord[] { + const launched = logs.filter((value) => + value.eventName === "MemeTokenLaunched" + ); + assertClassicV2ReconcilerLaunchCount(launched.length); + const tokens = new Set(); + const pools = new Set(); + const output = launched.map(({ args, log }) => { + const token = exactAddress(args.token, "classic-v2-launch-token"); + const poolId = exactBytes32(args.poolId, "classic-v2-launch-pool"); + const totalSwapFeeBps = safeInteger( + args.totalSwapFeeBps, + 100, + 1_000, + "classic-v2-launch-fee", + ); + if ( + tokens.has(lowerAddress(token)) || + pools.has(poolId) || + totalSwapFeeBps % 100 !== 0 || + !sameHex( + exactAddress(args.feeHook, "classic-v2-launch-hook"), + release.hook, + ) + ) { + fail("classic-v2-launch-identity"); + } + tokens.add(lowerAddress(token)); + pools.add(poolId); + return Object.freeze({ + creator: exactAddress(args.creator, "classic-v2-launch-creator"), + token, + poolId, + hook: release.hook, + positionRecipient: exactAddress( + args.positionRecipient, + "classic-v2-position-recipient", + ), + positionTokenId: nonnegative( + args.positionTokenId, + "classic-v2-position-token-id", + ), + totalSwapFeeBps, + launchHash: exactBytes32(args.launchHash, "classic-v2-launch-hash"), + blockNumber: log.blockNumber, + blockHash: log.blockHash, + transactionHash: log.transactionHash, + transactionIndex: log.transactionIndex, + blockGlobalLogIndex: log.logIndex, + log, + }); + }); + output.sort((left, right) => + left.blockNumber === right.blockNumber + ? left.transactionIndex === right.transactionIndex + ? left.blockGlobalLogIndex - right.blockGlobalLogIndex + : left.transactionIndex - right.transactionIndex + : left.blockNumber < right.blockNumber ? -1 : 1 + ); + return Object.freeze(output); +} + +function sameTransaction(event: DecodedLog, launch: LaunchRecord): boolean { + return event.log.blockNumber === launch.blockNumber && + sameHex(event.log.blockHash, launch.blockHash) && + sameHex(event.log.transactionHash, launch.transactionHash) && + event.log.transactionIndex === launch.transactionIndex; +} + +function validatedCompanions(input: { + launches: readonly LaunchRecord[]; + launcherLogs: readonly DecodedLog[]; + hookLogs: readonly DecodedLog[]; + release: Release; +}): ReadonlyMap { + const liquidity = oneByKey( + input.launcherLogs.filter((value) => + value.eventName === "MemeLiquidityConfigured" + ), + (value) => lowerAddress(exactAddress( + value.args.token, + "classic-v2-liquidity-token", + )), + "classic-v2-liquidity-cardinality", + ); + const initialBuy = oneByKey( + input.launcherLogs.filter((value) => + value.eventName === "MemeCreatorInitialBuy" + ), + (value) => lowerAddress(exactAddress( + value.args.token, + "classic-v2-initial-buy-token", + )), + "classic-v2-initial-buy-cardinality", + ); + const registered = oneByKey( + input.hookLogs.filter((value) => value.eventName === "PoolRegistered"), + (value) => exactBytes32( + value.args.poolId, + "classic-v2-registration-pool", + ), + "classic-v2-registration-cardinality", + ); + const disclosure = oneByKey( + input.hookLogs.filter((value) => + value.eventName === "PoolFeeDisclosure" + ), + (value) => exactBytes32( + value.args.poolId, + "classic-v2-disclosure-pool", + ), + "classic-v2-disclosure-cardinality", + ); + const companions = new Map(); + for (const launch of input.launches) { + const tokenKey = lowerAddress(launch.token); + const liquidityLog = liquidity.get(tokenKey); + const initialBuyLog = initialBuy.get(tokenKey); + const registrationLog = registered.get(launch.poolId); + const disclosureLog = disclosure.get(launch.poolId); + if ( + !liquidityLog || + !initialBuyLog || + !registrationLog || + !disclosureLog || + !sameTransaction(liquidityLog, launch) || + !sameTransaction(initialBuyLog, launch) || + !sameTransaction(registrationLog, launch) || + !sameTransaction(disclosureLog, launch) + ) { + fail("classic-v2-launch-companion-provenance"); + } + if ( + !sameHex( + exactBytes32( + liquidityLog.args.launchHash, + "classic-v2-liquidity-launch-hash", + ), + launch.launchHash, + ) || + !sameHex( + exactBytes32( + initialBuyLog.args.launchHash, + "classic-v2-initial-buy-launch-hash", + ), + launch.launchHash, + ) || + !sameHex( + exactAddress(initialBuyLog.args.creator, "classic-v2-initial-creator"), + launch.creator, + ) || + !sameHex( + exactBytes32(initialBuyLog.args.poolId, "classic-v2-initial-pool"), + launch.poolId, + ) || + !sameHex( + exactAddress(registrationLog.args.token, "classic-v2-registered-token"), + launch.token, + ) || + !sameHex( + exactAddress( + registrationLog.args.creator, + "classic-v2-registered-creator", + ), + launch.creator, + ) || + !sameHex( + exactAddress( + registrationLog.args.registrar, + "classic-v2-registered-registrar", + ), + input.release.launcher, + ) || + safeInteger( + registrationLog.args.totalSwapFeeBps, + 100, + 1_000, + "classic-v2-registered-fee", + ) !== launch.totalSwapFeeBps || + !sameHex( + exactAddress(disclosureLog.args.token, "classic-v2-disclosed-token"), + launch.token, + ) || + safeInteger( + disclosureLog.args.buySwapFeeBps, + 100, + 1_000, + "classic-v2-disclosed-buy-fee", + ) !== launch.totalSwapFeeBps || + safeInteger( + disclosureLog.args.sellSwapFeeBps, + 100, + 1_000, + "classic-v2-disclosed-sell-fee", + ) !== launch.totalSwapFeeBps || + safeInteger( + disclosureLog.args.launcherFeeBps, + 0, + 1_000, + "classic-v2-disclosed-launcher-fee", + ) !== 10 || + safeInteger( + disclosureLog.args.transferTaxBps, + 0, + 10_000, + "classic-v2-disclosed-transfer-tax", + ) !== 0 || + safeInteger( + disclosureLog.args.lpFeePips, + 0, + 1_000_000, + "classic-v2-disclosed-lp-fee", + ) !== 0 + ) { + fail("classic-v2-launch-companion-mismatch"); + } + companions.set(tokenKey, Object.freeze({ + liquidity: liquidityLog, + initialBuy: initialBuyLog, + registration: registrationLog, + disclosure: disclosureLog, + })); + } + return companions; +} + +function receiptContains( + receipt: ExactBlockRpcReceipt, + expected: ExactBlockRpcLog, +): readonly { receiptLogIndex: number }[] { + return receipt.logs.filter((log) => + sameHex(log.address, expected.address) && + log.logIndex === expected.logIndex && + sameHex(log.transactionHash, expected.transactionHash) && + sameHex(log.data, expected.data) && + log.topics.length === expected.topics.length && + log.topics.every((topic, index) => + sameHex(topic, expected.topics[index]!) + ) + ); +} + +function validatedLaunchTransactions(input: { + launches: readonly LaunchRecord[]; + transactions: readonly ExactBlockRpcTransaction[]; + receipts: readonly ExactBlockRpcReceipt[]; + companions: ReadonlyMap; + release: Release; +}): ReadonlyMap { + if ( + input.transactions.length !== input.launches.length || + input.receipts.length !== input.launches.length + ) { + fail("classic-v2-launch-transaction-cardinality"); + } + const output = new Map(); + for (let index = 0; index < input.launches.length; index += 1) { + const launch = input.launches[index]!; + const transaction = input.transactions[index]!; + const receipt = input.receipts[index]!; + if ( + !sameHex(transaction.transactionHash, launch.transactionHash) || + transaction.blockNumber !== launch.blockNumber || + !sameHex(transaction.blockHash, launch.blockHash) || + transaction.transactionIndex !== launch.transactionIndex || + !sameHex(transaction.from, launch.creator) || + !sameHex(transaction.to, input.release.launcher) || + !sameHex(receipt.transactionHash, launch.transactionHash) || + receipt.blockNumber !== launch.blockNumber || + !sameHex(receipt.blockHash, launch.blockHash) || + receipt.transactionIndex !== launch.transactionIndex + ) { + fail("classic-v2-launch-transaction-binding"); + } + const launchReceiptLogs = receiptContains(receipt, launch.log); + const companion = input.companions.get(lowerAddress(launch.token)); + if (!companion || launchReceiptLogs.length !== 1) { + fail("classic-v2-launch-receipt-log"); + } + for (const expected of [ + companion.liquidity, + companion.initialBuy, + companion.registration, + companion.disclosure, + ]) { + if (receiptContains(receipt, expected.log).length !== 1) { + fail("classic-v2-launch-receipt-companion"); + } + } + let decoded: ReturnType; + try { + decoded = decodeFunctionData({ + abi: launcherAbi, + data: transaction.input, + }); + } catch { + return fail("classic-v2-launch-calldata-decode"); + } + if (decoded.functionName !== "launch" || decoded.args.length !== 1) { + fail("classic-v2-launch-calldata-selector"); + } + const parameters = record(decoded.args[0], "classic-v2-launch-parameters"); + const metadata = record( + parameters.metadata, + "classic-v2-launch-metadata", + ); + const totalSwapFeeBps = safeInteger( + parameters.totalSwapFeeBps, + 100, + 1_000, + "classic-v2-launch-calldata-fee", + ); + if ( + totalSwapFeeBps !== launch.totalSwapFeeBps || + totalSwapFeeBps % 100 !== 0 || + transaction.value <= 0n || + transaction.value !== nonnegative( + companion.initialBuy.args.nativeAmount, + "classic-v2-initial-buy-native", + ) + ) { + fail("classic-v2-launch-calldata-economics"); + } + output.set(lowerAddress(launch.token), Object.freeze({ + receiptLogIndex: launchReceiptLogs[0]!.receiptLogIndex, + value: transaction.value, + name: exactText(parameters.name, "classic-v2-launch-name"), + symbol: exactText(parameters.symbol, "classic-v2-launch-symbol"), + totalSwapFeeBps, + creatorSalt: exactBytes32( + parameters.creatorSalt, + "classic-v2-creator-salt", + ), + description: exactText( + metadata.description, + "classic-v2-launch-description", + ), + website: exactText(metadata.website, "classic-v2-launch-website"), + image: exactText(metadata.image, "classic-v2-launch-image"), + extraData: exactData(metadata.extraData, "classic-v2-launch-extra-data"), + })); + } + return output; +} + +async function readCalls( + rpc: ExactBlockRpcClient, + specs: readonly CallSpec[], + blockHash: HexBytes32, + signal: AbortSignal, +): Promise { + const results = await rpc.callMany({ + calls: specs.map(({ to, data }) => Object.freeze({ to, data })), + blockHash, + signal, + }); + if (results.length !== specs.length) fail("classic-v2-call-cardinality"); + return Object.freeze(results.map((result, index) => + specs[index]!.decode(result) + )); +} + +function feeTotals( + feeLogs: readonly DecodedLog[], + swapLogs: readonly DecodedLog[], + poolId: HexBytes32, + totalSwapFeeBps: number, +) { + const fees = feeLogs.filter((event) => + event.eventName === "NativeSwapFeesAccrued" && + sameHex( + exactBytes32(event.args.poolId, "classic-v2-accrual-pool"), + poolId, + ) + ); + const swaps = swapLogs.filter((event) => + event.eventName === "Swap" && + sameHex(exactBytes32(event.args.id, "classic-v2-swap-pool"), poolId) + ); + if (swaps.length < 1 || fees.length > swaps.length) { + fail("classic-v2-swap-fee-event-coverage"); + } + let gross = 0n; + let creator = 0n; + let launcher = 0n; + const feeAmounts = fees.map((fee) => { + const grossAmount = nonnegative( + fee.args.grossNativeAmount, + "classic-v2-gross-fee", + ); + const creatorAmount = nonnegative( + fee.args.creatorFee, + "classic-v2-creator-fee", + ); + const launcherAmount = nonnegative( + fee.args.launcherFee, + "classic-v2-launcher-fee", + ); + const actualTotalFee = creatorAmount + launcherAmount; + const floorTotalFee = grossAmount * BigInt(totalSwapFeeBps) / 10_000n; + const ceilingTotalFee = + (grossAmount * BigInt(totalSwapFeeBps) + 9_999n) / 10_000n; + const expectedLauncherFee = grossAmount * 10n / 10_000n; + if ( + actualTotalFee === 0n || + (actualTotalFee !== floorTotalFee && + actualTotalFee !== ceilingTotalFee) || + launcherAmount !== ( + expectedLauncherFee > actualTotalFee + ? actualTotalFee + : expectedLauncherFee + ) || + creatorAmount !== actualTotalFee - launcherAmount + ) { + fail("classic-v2-fee-conservation"); + } + gross += grossAmount; + creator += creatorAmount; + launcher += launcherAmount; + return Object.freeze({ + grossAmount, + actualTotalFee, + sender: exactAddress(fee.args.swapSender, "classic-v2-fee-sender"), + }); + }); + + const swapNativeAmounts = swaps.map((swap) => { + if ( + safeInteger(swap.args.fee, 0, 1_000_000, "classic-v2-swap-lp-fee") !== 0 + ) { + fail("classic-v2-fee-conservation"); + } + const amount = absolute(swap.args.amount0, "classic-v2-swap-native"); + if (amount === 0n) fail("classic-v2-swap-native"); + return Object.freeze({ + amount, + sender: exactAddress(swap.args.sender, "classic-v2-swap-sender"), + }); + }); + + const candidates = fees.map((fee, feeIndex) => { + const related = swaps.flatMap((swap, swapIndex) => { + if ( + fee.log.blockNumber !== swap.log.blockNumber || + fee.log.transactionIndex !== swap.log.transactionIndex || + !sameHex(fee.log.blockHash, swap.log.blockHash) || + !sameHex(fee.log.transactionHash, swap.log.transactionHash) || + !sameHex(feeAmounts[feeIndex]!.sender, swapNativeAmounts[swapIndex]!.sender) + ) { + return []; + } + return [swapIndex]; + }); + const previous = related.filter((swapIndex) => + swaps[swapIndex]!.log.logIndex < fee.log.logIndex + ).at(-1); + const next = related.find((swapIndex) => + swaps[swapIndex]!.log.logIndex > fee.log.logIndex + ); + return Object.freeze([previous, next] + .filter((swapIndex): swapIndex is number => swapIndex !== undefined) + .filter((swapIndex, index, values) => + values.indexOf(swapIndex) === index && + ( + feeAmounts[feeIndex]!.grossAmount === + swapNativeAmounts[swapIndex]!.amount || + feeAmounts[feeIndex]!.grossAmount === + swapNativeAmounts[swapIndex]!.amount + + feeAmounts[feeIndex]!.actualTotalFee + ) + )); + }); + + let assignmentCount = 0; + let matchedSwapIndexes: readonly number[] = Object.freeze([]); + function assign(feeIndex: number, previousSwapIndex: number, path: number[]) { + if (assignmentCount > 1) return; + if (feeIndex === candidates.length) { + assignmentCount += 1; + matchedSwapIndexes = Object.freeze([...path]); + return; + } + for (const swapIndex of candidates[feeIndex]!) { + if (swapIndex <= previousSwapIndex) continue; + path.push(swapIndex); + assign(feeIndex + 1, swapIndex, path); + path.pop(); + } + } + assign(0, -1, []); + if (assignmentCount !== 1) { + fail("classic-v2-swap-fee-provenance"); + } + const matched = new Set(matchedSwapIndexes); + for (let index = 0; index < swaps.length; index += 1) { + if ( + !matched.has(index) && + swapNativeAmounts[index]!.amount * BigInt(totalSwapFeeBps) / 10_000n !== 0n + ) { + fail("classic-v2-swap-fee-event-coverage"); + } + } + return Object.freeze({ + gross, + creator, + launcher, + lastSwap: swaps.at(-1)!, + }); +} + +function creatorClaimTotal( + hookLogs: readonly DecodedLog[], + poolId: HexBytes32, + creator: Address, +): bigint { + let total = 0n; + for (const event of hookLogs) { + if (event.eventName !== "CreatorFeesClaimed") continue; + if (!sameHex( + exactBytes32(event.args.poolId, "classic-v2-claim-pool"), + poolId, + )) { + continue; + } + const amount = nonnegative(event.args.amount, "classic-v2-claim-amount"); + const recipient = exactAddress( + event.args.recipient, + "classic-v2-claim-recipient", + ); + const caller = exactAddress(event.args.caller, "classic-v2-claim-caller"); + if ( + amount === 0n || + !sameHex( + exactAddress(event.args.creator, "classic-v2-claim-creator"), + creator, + ) || + sameHex(recipient, ZERO_ADDRESS) || + sameHex(caller, ZERO_ADDRESS) || + (!sameHex(recipient, creator) && !sameHex(caller, creator)) + ) { + fail("classic-v2-claim-provenance"); + } + total += amount; + } + return total; +} + +function isoTimestamp(timestamp: bigint): string { + if (timestamp < 0n || timestamp > 8_640_000_000_000n) { + fail("classic-v2-block-timestamp"); + } + return new Date(Number(timestamp) * 1_000).toISOString(); +} + +export async function buildClassicV2ExactBlockContribution(input: { + rpc: ExactBlockRpcClient; + contract: ReconcilerPreParityContract; + blockNumber: bigint; + blockHash: HexBytes32; + signal: AbortSignal; +}): Promise { + const release = resolvedRelease(input.contract); + if ( + input.blockNumber < release.startBlock || + input.blockNumber.toString() !== input.contract.checkpointBlockNumber || + !sameHex(input.blockHash, input.contract.checkpointBlockHash) + ) { + fail("classic-v2-checkpoint-binding"); + } + await assertRuntime(input.rpc, release, input.blockHash, input.signal); + + const infrastructureSpecs = [ + callSpec(release.launcher, launcherAbi, "poolManager"), + callSpec(release.launcher, launcherAbi, "feeHook"), + callSpec(release.launcher, launcherAbi, "MIN_INITIAL_BUY_WEI"), + callSpec(release.hook, hookInfrastructureAbi, "poolManager"), + callSpec(release.hook, hookInfrastructureAbi, "launcherFeeRecipient"), + callSpec(release.hook, creatorFeeHookReadAbi, "LAUNCHER_FEE_BPS"), + callSpec(release.hook, creatorFeeHookReadAbi, "LP_FEE_PIPS"), + callSpec(release.hook, hookInfrastructureAbi, "TICK_SPACING"), + ]; + const [infrastructure, launcherLogs, hookLogs] = await Promise.all([ + readCalls( + input.rpc, + infrastructureSpecs, + input.blockHash, + input.signal, + ), + readLogsInRanges({ + rpc: input.rpc, + addresses: release.launcher, + events: LAUNCHER_EVENTS, + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + readLogsInRanges({ + rpc: input.rpc, + addresses: release.hook, + events: HOOK_EVENTS, + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + ]); + const minimumInitialBuy = nonnegative( + infrastructure[2], + "classic-v2-minimum-initial-buy", + ); + if ( + !sameHex( + exactAddress(infrastructure[0], "classic-v2-launcher-pool-manager"), + release.poolManager, + ) || + !sameHex( + exactAddress(infrastructure[1], "classic-v2-launcher-hook"), + release.hook, + ) || + minimumInitialBuy < 1n || + !sameHex( + exactAddress(infrastructure[3], "classic-v2-hook-pool-manager"), + release.poolManager, + ) || + !sameHex( + exactAddress(infrastructure[4], "classic-v2-hook-treasury"), + release.treasury, + ) || + safeInteger( + infrastructure[5], + 0, + 10_000, + "classic-v2-launcher-fee-constant", + ) !== 10 || + safeInteger( + infrastructure[6], + 0, + 1_000_000, + "classic-v2-lp-fee-constant", + ) !== 0 || + safeInteger( + infrastructure[7], + -887_272, + 887_272, + "classic-v2-tick-spacing", + ) !== 200 + ) { + fail("classic-v2-infrastructure-state"); + } + + const launches = launchRecords(launcherLogs, release); + const corpusManifest = createReconcilerCorpusManifest({ + contract: input.contract, + identities: launches.map((launch) => Object.freeze({ + tokenAddress: lowerAddress(launch.token), + poolId: launch.poolId, + launchTransactionHash: launch.transactionHash, + launchBlockNumber: launch.blockNumber.toString(), + launchTransactionIndex: launch.transactionIndex, + launchLogIndex: launch.blockGlobalLogIndex, + })), + }); + const companions = validatedCompanions({ + launches, + launcherLogs, + hookLogs, + release, + }); + const launchTransactions = new Map(); + const stateValues: unknown[] = []; + const poolSwapLogs: DecodedLog[] = []; + const timestamps = new Map(); + const timestampHashes = new Map(); + const completedCorpusPages: Array<(typeof corpusManifest.pages)[number]> = []; + for (const page of corpusManifest.pages) { + const pageRpc = input.rpc.createPartitionClient(page); + await pageRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + const pageLaunches = launches.slice(page.startIndex, page.endIndexExclusive); + const [transactions, receipts, pagePoolSwapLogs] = await Promise.all([ + pageRpc.getTransactions({ + transactions: pageLaunches.map((launch) => Object.freeze({ + transactionHash: launch.transactionHash, + expectedBlockNumber: launch.blockNumber, + expectedBlockHash: launch.blockHash, + expectedTo: release.launcher, + })), + signal: input.signal, + }), + pageRpc.getTransactionReceipts({ + receipts: pageLaunches.map((launch) => Object.freeze({ + transactionHash: launch.transactionHash, + expectedBlockNumber: launch.blockNumber, + expectedBlockHash: launch.blockHash, + })), + signal: input.signal, + }), + readPoolSwapBatches({ + rpc: pageRpc, + poolManager: release.poolManager, + poolIds: pageLaunches.map(({ poolId }) => poolId), + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + ]); + poolSwapLogs.push(...pagePoolSwapLogs); + const pageTransactions = validatedLaunchTransactions({ + launches: pageLaunches, + transactions, + receipts, + companions, + release, + }); + for (const [key, transaction] of pageTransactions) { + if (launchTransactions.has(key)) fail("classic-v2-launch-transaction-duplicate"); + launchTransactions.set(key, transaction); + } + const stateSpecs = pageLaunches.flatMap((launch) => { + const transaction = pageTransactions.get(lowerAddress(launch.token)); + if (!transaction) fail("classic-v2-launch-transaction-missing"); + return [ + callSpec(launch.token, uerc20ReadAbi, "name"), + callSpec(launch.token, uerc20ReadAbi, "symbol"), + callSpec(launch.token, uerc20ReadAbi, "decimals"), + callSpec(launch.token, uerc20ReadAbi, "totalSupply"), + callSpec(launch.token, uerc20ReadAbi, "creator"), + callSpec(launch.token, uerc20ReadAbi, "metadata"), + callSpec(release.stateView, stateViewReadAbi, "getSlot0", [launch.poolId]), + callSpec(release.stateView, stateViewReadAbi, "getLiquidity", [launch.poolId]), + callSpec(release.hook, creatorFeeHookReadAbi, "feeDisclosure", [launch.poolId]), + callSpec(release.hook, creatorFeeHookReadAbi, "poolFeeConfig", [launch.poolId]), + callSpec(release.launcher, launcherAbi, "launchHashOf", [launch.token]), + callSpec(release.launcher, launcherAbi, "predictTokenAddress", [ + transaction.name, + transaction.symbol, + launch.creator, + transaction.creatorSalt, + ]), + callSpec(release.launcher, launcherAbi, "predictPositionRecipient", [ + launch.token, + launch.creator, + ]), + callSpec(release.launcher, launcherAbi, "poolKey", [launch.token]), + ]; + }); + stateValues.push(...await readCalls( + pageRpc, + stateSpecs, + input.blockHash, + input.signal, + )); + const timestampBindings = []; + for (const launch of pageLaunches) { + const key = launch.blockNumber.toString(); + const knownHash = timestampHashes.get(key); + if (knownHash !== undefined && !sameHex(knownHash, launch.blockHash)) { + fail("classic-v2-launch-block-hash-conflict"); + } + if (!timestamps.has(key) && knownHash === undefined) { + timestampHashes.set(key, launch.blockHash); + timestampBindings.push({ + blockNumber: launch.blockNumber, + expectedHash: launch.blockHash, + }); + } + } + const pageTimestamps = await pageRpc.getBlockTimestamps({ + blocks: timestampBindings, + signal: input.signal, + }); + if (pageTimestamps.length !== timestampBindings.length) { + fail("classic-v2-launch-timestamp-cardinality"); + } + timestampBindings.forEach((binding, index) => { + timestamps.set(binding.blockNumber.toString(), pageTimestamps[index]!); + }); + await pageRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + completedCorpusPages.push(page); + } + assembleReconcilerCorpusPages(corpusManifest, completedCorpusPages); + poolSwapLogs.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + + const tokens: Json[] = []; + const charts: Json[] = []; + for (let index = 0; index < launches.length; index += 1) { + const launch = launches[index]!; + const companion = companions.get(lowerAddress(launch.token)); + const transaction = launchTransactions.get(lowerAddress(launch.token)); + if (!companion || !transaction) fail("classic-v2-launch-evidence-missing"); + if (transaction.value < minimumInitialBuy) { + fail("classic-v2-initial-buy-below-current-minimum"); + } + const offset = index * CALLS_PER_LAUNCH; + const name = exactText(stateValues[offset], "classic-v2-token-name"); + const symbol = exactText(stateValues[offset + 1], "classic-v2-token-symbol"); + const decimals = safeInteger( + stateValues[offset + 2], + 0, + 255, + "classic-v2-token-decimals", + ); + const totalSupply = nonnegative( + stateValues[offset + 3], + "classic-v2-total-supply", + ); + const recordedCreator = exactAddress( + stateValues[offset + 4], + "classic-v2-token-creator", + ); + const metadata = tuple( + stateValues[offset + 5], + 4, + "classic-v2-token-metadata", + ); + const slot0 = tuple(stateValues[offset + 6], 4, "classic-v2-slot0"); + nonnegative(stateValues[offset + 7], "classic-v2-active-liquidity"); + const disclosure = tuple( + stateValues[offset + 8], + 6, + "classic-v2-fee-disclosure", + ); + const poolConfig = tuple( + stateValues[offset + 9], + 5, + "classic-v2-pool-config", + ); + const currentLaunchHash = exactBytes32( + stateValues[offset + 10], + "classic-v2-current-launch-hash", + ); + const predictedToken = tuple( + stateValues[offset + 11], + 2, + "classic-v2-predicted-token", + ); + const predictedPositionRecipient = exactAddress( + stateValues[offset + 12], + "classic-v2-predicted-position-recipient", + ); + const poolKey = tuple( + stateValues[offset + 13], + 5, + "classic-v2-pool-key", + ); + const [descriptionValue, websiteValue, imageValue, extraDataValue] = metadata; + const description = exactText(descriptionValue, "classic-v2-description"); + const website = exactText(websiteValue, "classic-v2-website"); + const image = exactText(imageValue, "classic-v2-image"); + const extraData = exactData(extraDataValue, "classic-v2-extra-data"); + const [sqrtPriceValue, tickValue, protocolFeeValue, currentLpFeeValue] = slot0; + nonnegative(sqrtPriceValue, "classic-v2-current-price"); + safeInteger(tickValue, -887_272, 887_272, "classic-v2-current-tick"); + safeInteger(protocolFeeValue, 0, 1_000_000, "classic-v2-protocol-fee"); + const currentLpFeePips = safeInteger( + currentLpFeeValue, + 0, + 1_000_000, + "classic-v2-current-lp-fee", + ); + const [ + buySwapFeeValue, + sellSwapFeeValue, + creatorFeeValue, + launcherFeeValue, + transferTaxValue, + disclosedLpFeeValue, + ] = disclosure; + const buySwapFeeBps = safeInteger( + buySwapFeeValue, + 100, + 1_000, + "classic-v2-buy-fee", + ); + const sellSwapFeeBps = safeInteger( + sellSwapFeeValue, + 100, + 1_000, + "classic-v2-sell-fee", + ); + const creatorFeeBps = safeInteger( + creatorFeeValue, + 0, + 1_000, + "classic-v2-creator-fee-bps", + ); + const launcherFeeBps = safeInteger( + launcherFeeValue, + 0, + 1_000, + "classic-v2-launcher-fee-bps", + ); + const transferTaxBps = safeInteger( + transferTaxValue, + 0, + 10_000, + "classic-v2-transfer-tax", + ); + const lpFeePips = safeInteger( + disclosedLpFeeValue, + 0, + 1_000_000, + "classic-v2-disclosed-lp-fee", + ); + const [ + configuredCreatorValue, + registrarValue, + configuredTotalFeeValue, + registeredValue, + pendingCreatorFeesValue, + ] = poolConfig; + if ( + !sameHex(recordedCreator, release.launcher) || + !sameHex(currentLaunchHash, launch.launchHash) || + !sameHex( + exactAddress(predictedToken[0], "classic-v2-predicted-token-address"), + launch.token, + ) || + !sameHex(predictedPositionRecipient, launch.positionRecipient) || + !sameHex( + exactAddress(poolKey[0], "classic-v2-pool-currency-zero"), + ZERO_ADDRESS, + ) || + !sameHex( + exactAddress(poolKey[1], "classic-v2-pool-currency-one"), + launch.token, + ) || + safeInteger(poolKey[2], 0, 1_000_000, "classic-v2-pool-fee") !== 0 || + safeInteger( + poolKey[3], + -887_272, + 887_272, + "classic-v2-pool-tick-spacing", + ) !== 200 || + !sameHex( + exactAddress(poolKey[4], "classic-v2-pool-hook"), + release.hook, + ) || + !sameHex( + exactAddress( + configuredCreatorValue, + "classic-v2-configured-creator", + ), + launch.creator, + ) || + !sameHex( + exactAddress(registrarValue, "classic-v2-configured-registrar"), + release.launcher, + ) || + safeInteger( + configuredTotalFeeValue, + 100, + 1_000, + "classic-v2-configured-total-fee", + ) !== launch.totalSwapFeeBps || + registeredValue !== true || + buySwapFeeBps !== launch.totalSwapFeeBps || + sellSwapFeeBps !== launch.totalSwapFeeBps || + creatorFeeBps + launcherFeeBps !== launch.totalSwapFeeBps || + launcherFeeBps !== 10 || + transferTaxBps !== 0 || + lpFeePips !== 0 || + currentLpFeePips !== 0 || + name !== transaction.name || + symbol !== transaction.symbol || + description !== transaction.description || + website !== transaction.website || + image !== transaction.image || + !sameHex(extraData, transaction.extraData) + ) { + fail("classic-v2-current-state-mismatch"); + } + exactBytes32(predictedToken[1], "classic-v2-effective-graffiti"); + const pendingCreatorFees = nonnegative( + pendingCreatorFeesValue, + "classic-v2-pending-creator-fees", + ); + + const liquidityArgs = companion.liquidity.args; + const liquiditySupply = nonnegative( + liquidityArgs.totalSupply, + "classic-v2-liquidity-supply", + ); + const tokenLiquidity = nonnegative( + liquidityArgs.tokenLiquidityAmount, + "classic-v2-token-liquidity", + ); + const lockedDust = nonnegative( + liquidityArgs.lockedTokenDust, + "classic-v2-locked-dust", + ); + const initialTick = safeInteger( + liquidityArgs.initialTick, + -887_272, + 887_272, + "classic-v2-initial-tick", + ); + const tickLower = safeInteger( + liquidityArgs.tickLower, + -887_272, + 887_272, + "classic-v2-tick-lower", + ); + const tickUpper = safeInteger( + liquidityArgs.tickUpper, + -887_272, + 887_272, + "classic-v2-tick-upper", + ); + const eventLpFeePips = safeInteger( + liquidityArgs.lpFeePips, + 0, + 1_000_000, + "classic-v2-event-lp-fee", + ); + if ( + liquiditySupply !== totalSupply || + tokenLiquidity + lockedDust !== totalSupply || + initialTick !== EXPECTED_INITIAL_TICK || + tickLower !== EXPECTED_TICK_LOWER || + tickUpper !== initialTick || + eventLpFeePips !== lpFeePips || + nonnegative( + companion.initialBuy.args.tokenAmount, + "classic-v2-initial-buy-token", + ) < 1n + ) { + fail("classic-v2-liquidity-conservation"); + } + const totals = feeTotals( + hookLogs, + poolSwapLogs, + launch.poolId, + launch.totalSwapFeeBps, + ); + const claimedCreatorFees = creatorClaimTotal( + hookLogs, + launch.poolId, + launch.creator, + ); + if (totals.creator !== pendingCreatorFees + claimedCreatorFees) { + fail("classic-v2-creator-fee-accounting"); + } + const lastSwap = totals.lastSwap; + const timestamp = timestamps.get(launch.blockNumber.toString()); + if (timestamp === undefined) fail("classic-v2-launch-timestamp-missing"); + + tokens.push({ + releaseVersion: "classic-v2", + modelId: "classic", + tokenAddress: lowerAddress(launch.token), + creatorAddress: lowerAddress(launch.creator), + launchTransactionHash: launch.transactionHash, + launchBlockNumber: launch.blockNumber.toString(), + launchTransactionIndex: launch.transactionIndex, + launchLogIndex: transaction.receiptLogIndex, + launchedAt: isoTimestamp(timestamp), + poolId: launch.poolId, + hookAddress: lowerAddress(launch.hook), + rewardVaultAddress: null, + positionRecipient: lowerAddress(launch.positionRecipient), + positionTokenId: launch.positionTokenId.toString(), + launchHash: launch.launchHash, + name, + symbol, + decimals, + totalSupplyRaw: totalSupply.toString(), + quoteAssetAddress: lowerAddress(ZERO_ADDRESS), + fees: { + buySwapFeeBps, + sellSwapFeeBps, + buyCreatorFeeBps: creatorFeeBps, + sellCreatorFeeBps: creatorFeeBps, + launcherFeeBps, + transferTaxBps, + lpFeePips, + }, + liquidity: { + tokenLiquidityAmountRaw: tokenLiquidity.toString(), + lockedTokenDustRaw: lockedDust.toString(), + initialTick, + tickLower, + tickUpper, + }, + }); + charts.push({ + releaseVersion: "classic-v2", + modelId: "classic", + tokenAddress: lowerAddress(launch.token), + poolId: launch.poolId, + quoteAssetAddress: lowerAddress(ZERO_ADDRESS), + state: { + blockNumber: lastSwap.log.blockNumber.toString(), + blockHash: lastSwap.log.blockHash, + transactionHash: lastSwap.log.transactionHash, + transactionIndex: lastSwap.log.transactionIndex, + logIndex: lastSwap.log.logIndex, + sqrtPriceX96: nonnegative( + lastSwap.args.sqrtPriceX96, + "classic-v2-latest-swap-price", + ).toString(), + liquidity: nonnegative( + lastSwap.args.liquidity, + "classic-v2-latest-swap-liquidity", + ).toString(), + tick: safeInteger( + lastSwap.args.tick, + -887_272, + 887_272, + "classic-v2-latest-swap-tick", + ), + lpFeePips, + }, + volume: { + quoteAssetAddress: lowerAddress(ZERO_ADDRESS), + grossQuoteRaw: totals.gross.toString(), + creatorFeeQuoteRaw: totals.creator.toString(), + launcherFeeQuoteRaw: totals.launcher.toString(), + }, + }); + } + + return Object.freeze({ + tokens: Object.freeze(tokens), + charts: Object.freeze(charts), + }); +} diff --git a/lib/data-pipeline/classic-v3-activation-model.ts b/lib/data-pipeline/classic-v3-activation-model.ts new file mode 100644 index 00000000..062fd6bf --- /dev/null +++ b/lib/data-pipeline/classic-v3-activation-model.ts @@ -0,0 +1,488 @@ +import "server-only"; + +import { keccak256, toBytes } from "viem"; + +import { + canonicalizeFingerprintJson, + type CanonicalJsonValue, +} from "./canonical-fingerprint"; +import type { HexAddress, HexBytes32 } from "./codecs"; +import { + readDualRpcInitialRewardConfiguration, + readDualRpcRewardSnapshot, + verifyDynamicRuntimeAtActivationWithDualRpc, + type CandidateRpcProvider, + type DualRpcCandidateWindowEvidence, + type DualRpcDynamicRuntimeActivationObservation, + type DualRpcInitialRewardConfigurationEvidence, + type DualRpcRewardSnapshot, + type ProjectorDynamicSourceTemplate, +} from "./dual-rpc"; +import type { EnvioCandidate } from "./envio"; +import { dataPipelineError, invalidInput, validationError } from "./errors"; +import type { CanonicalDynamicSourceDeploymentEvidence } from "./projector-dynamic-activation"; +import { projectorOccurrenceUuid } from "./projector-ids"; +import { + foldProjectorRewardState, + type ProjectorRewardBaseline, + type ProjectorRewardEvent, + type ProjectorRewardSnapshot, +} from "./projector-reward-fold"; +import { expectedRewardRpcCallCount } from "./projector-reward-rpc-contract"; + +const EVIDENCE_DOMAIN = + "programmable:classic-v3-activation-model-evidence:v1\0"; + +export type ClassicV3ActivationModelEvidence = Readonly<{ + activationId: string; + evidenceKind: + | "classic-v3-runtime-activation-v1" + | "classic-v3-initial-reward-configuration-v1" + | "classic-v3-launch-reward-conservation-v1"; + payload: CanonicalJsonValue; + evidenceCommitment: HexBytes32; +}>; + +export type ClassicV3ActivationModelVerification = Readonly<{ + runtimeObservation: DualRpcDynamicRuntimeActivationObservation; + initialConfiguration: DualRpcInitialRewardConfigurationEvidence; + baseline: ProjectorRewardBaseline; + projectedSnapshot: ProjectorRewardSnapshot; + rewardEvidence: DualRpcRewardSnapshot; + modelVerificationEvidence: readonly [ + ClassicV3ActivationModelEvidence, + ClassicV3ActivationModelEvidence, + ClassicV3ActivationModelEvidence, + ]; +}>; + +function canonicalJsonValue(value: unknown): CanonicalJsonValue { + if ( + value === null || + typeof value === "string" || + typeof value === "boolean" + ) { + return value; + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value)) { + throw validationError("rpc", "activation-model-evidence-json"); + } + return value; + } + if (Array.isArray(value)) { + return value.map(canonicalJsonValue); + } + if ( + typeof value !== "object" || + Object.getPrototypeOf(value) !== Object.prototype + ) { + throw validationError("rpc", "activation-model-evidence-json"); + } + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [ + key, + canonicalJsonValue(entry), + ]), + ); +} + +function evidenceCommitment( + evidenceKind: ClassicV3ActivationModelEvidence["evidenceKind"], + activationId: string, + payload: CanonicalJsonValue, +): HexBytes32 { + return keccak256( + toBytes( + `${EVIDENCE_DOMAIN}${canonicalizeFingerprintJson({ + activationId, + evidenceKind, + payload, + })}`, + ), + ); +} + +function modelEvidence( + activationId: string, + evidenceKind: ClassicV3ActivationModelEvidence["evidenceKind"], + value: unknown, +): ClassicV3ActivationModelEvidence { + const payload = canonicalJsonValue(value); + return Object.freeze({ + activationId, + evidenceKind, + payload, + evidenceCommitment: evidenceCommitment( + evidenceKind, + activationId, + payload, + ), + }); +} + +function deterministicRewardEvidence( + evidence: DualRpcRewardSnapshot, +): Readonly> { + return Object.freeze( + Object.fromEntries( + Object.entries(evidence).filter(([key]) => key !== "executionTrace"), + ), + ); +} + +function deterministicRuntimeEvidence( + evidence: DualRpcDynamicRuntimeActivationObservation, +): Readonly> { + const nondeterministicKeys = new Set([ + "startedAtMs", + "completedAtMs", + "elapsedMs", + "hardDeadlineMs", + ]); + return Object.freeze( + Object.fromEntries( + Object.entries(evidence).filter( + ([key]) => !nondeterministicKeys.has(key), + ), + ), + ); +} + +function deterministicInitialConfigurationEvidence( + evidence: DualRpcInitialRewardConfigurationEvidence, +): Readonly> { + return Object.freeze({ + ...evidence, + endConfigurationSnapshot: deterministicRewardEvidence( + evidence.endConfigurationSnapshot, + ), + }); +} + +function exactProviderTuple( + actual: readonly string[], + expected: readonly string[], +): boolean { + return ( + actual.length === expected.length && + actual.every((value, index) => value === expected[index]) + ); +} + +function rewardEventKind( + eventName: string, +): ProjectorRewardEvent["kind"] { + switch (eventName) { + case "CreatorFeesCheckpointed": + return "creator-fee-checkpoint"; + case "BeneficiaryFeesClaimed": + return "beneficiary-claim"; + case "PayoutWalletChanged": + return "payout-change"; + case "CtoRewardConfigurationActivated": + return "reward-configuration-activation"; + default: + throw validationError("rpc", "activation-reward-event-kind"); + } +} + +function rewardEventValues( + candidate: EnvioCandidate, +): Readonly> { + return Object.freeze( + Object.fromEntries( + Object.entries(candidate.decodedPayload).map(([key, value]) => { + if ( + typeof value === "string" || + (Array.isArray(value) && + value.every((entry) => typeof entry === "string")) + ) { + return [key, value] as const; + } + throw validationError("rpc", "activation-reward-event-values"); + }), + ), + ); +} + +function rewardEvents(input: { + candidates: readonly EnvioCandidate[]; + evidence: DualRpcCandidateWindowEvidence; + vault: HexAddress; +}): readonly ProjectorRewardEvent[] { + const evidenceByCandidate = new Map( + input.evidence.candidates.map((candidate) => [ + candidate.candidateId, + candidate, + ]), + ); + return Object.freeze( + [...input.candidates] + .sort((left, right) => { + if (left.transactionIndex !== right.transactionIndex) { + return left.transactionIndex - right.transactionIndex; + } + return left.blockGlobalLogIndex - right.blockGlobalLogIndex; + }) + .map((candidate) => { + const evidence = evidenceByCandidate.get(candidate.candidateId); + if ( + !evidence || + evidence.sourceAddress !== input.vault || + evidence.transactionHash !== candidate.transactionHash || + evidence.transactionIndex !== candidate.transactionIndex + ) { + throw validationError("rpc", "activation-reward-event-evidence"); + } + return Object.freeze({ + occurrenceId: projectorOccurrenceUuid({ + transactionHash: candidate.transactionHash, + receiptLogOrdinal: String(evidence.receiptLogOrdinal), + blockHash: candidate.blockHash, + }), + vault: input.vault, + blockNumber: candidate.blockNumber, + transactionIndex: String(candidate.transactionIndex), + blockGlobalLogIndex: String(candidate.blockGlobalLogIndex), + kind: rewardEventKind(candidate.eventName), + values: rewardEventValues(candidate), + }); + }), + ); +} + +/** + * Produces all three model-specific evidences required before a staged Classic + * activation can be committed: exact runtime/immutables, epoch-one reward + * configuration, and independently folded full-account reward conservation at + * the exact canonical launch block. The three results share one deadline and + * one aggregate physical-call budget per provider. + */ +export async function verifyClassicV3ActivationModel(input: Readonly<{ + activationId: string; + parentCandidate: EnvioCandidate; + launchCandidate: EnvioCandidate; + sameBlockVaultEvents: readonly EnvioCandidate[]; + candidateEvidence: DualRpcCandidateWindowEvidence; + sourceAddress: HexAddress; + template: ProjectorDynamicSourceTemplate; + canonicalDeployment: CanonicalDynamicSourceDeploymentEvidence; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + deadlineMs?: number; +}>): Promise { + const hardDeadlineMs = input.deadlineMs ?? 75_000; + if ( + !Number.isSafeInteger(hardDeadlineMs) || + hardDeadlineMs < 10 || + hardDeadlineMs > 75_000 + ) { + throw invalidInput("rpc", "activation-model-deadline"); + } + const deadlineAt = Date.now() + hardDeadlineMs; + const remaining = () => { + const value = deadlineAt - Date.now(); + if (value < 10) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); + } + return value; + }; + if ( + input.candidateEvidence.coveredCandidateCount !== + input.candidateEvidence.candidates.length || + input.candidateEvidence.coverage.throughBlockNumber !== + input.launchCandidate.blockNumber || + input.candidateEvidence.coverage.throughBlockHash !== + input.launchCandidate.blockHash || + input.candidateEvidence.coverage.throughBlockGlobalLogIndex !== + "4294967295" + ) { + throw validationError("rpc", "activation-model-block-coverage"); + } + const runtimeObservation = + await verifyDynamicRuntimeAtActivationWithDualRpc({ + parentCandidate: input.parentCandidate, + launchCandidate: input.launchCandidate, + sourceAddress: input.sourceAddress, + template: input.template, + canonicalDeployment: input.canonicalDeployment, + activationEvidence: input.candidateEvidence, + providers: input.providers, + deadlineMs: remaining(), + }); + const initialConfiguration = await readDualRpcInitialRewardConfiguration({ + parentCandidate: input.parentCandidate, + launchCandidate: input.launchCandidate, + sameBlockVaultEvents: input.sameBlockVaultEvents, + candidateEvidence: input.candidateEvidence, + canonicalDeployment: input.canonicalDeployment, + template: input.template, + providers: input.providers, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxAttempts: 1, + maxCallsPerProvider: 128, + }, + }); + const baseline: ProjectorRewardBaseline = Object.freeze({ + vault: initialConfiguration.vault, + poolId: initialConfiguration.poolId, + configurationEpoch: "1", + activeConfigurationHash: + initialConfiguration.initialActiveConfigurationHash, + allocations: Object.freeze( + initialConfiguration.allocations.map( + ({ allocationIndex, beneficiary, shareBps }) => + Object.freeze({ + allocationIndex, + beneficiary, + payoutAddress: beneficiary, + shareBps, + }), + ), + ), + balances: Object.freeze( + initialConfiguration.allocations.map(({ beneficiary }) => + Object.freeze({ + account: beneficiary, + payoutAddress: beneficiary, + claimableAccrued: "0", + claimedTotal: "0", + }), + ), + ), + }); + const events = rewardEvents({ + candidates: input.sameBlockVaultEvents, + evidence: input.candidateEvidence, + vault: initialConfiguration.vault, + }); + const launchEvidence = input.candidateEvidence.candidates.find( + ({ candidateId }) => candidateId === input.launchCandidate.candidateId, + ); + if (!launchEvidence) { + throw validationError("rpc", "activation-launch-evidence"); + } + const projectedSnapshot = events.length > 0 + ? foldProjectorRewardState({ + model: "classic-v3", + baseline, + events, + }) + : Object.freeze({ + ...baseline, + totalCreatorFeesReceived: "0", + snapshotSourceOccurrenceId: projectorOccurrenceUuid({ + transactionHash: input.launchCandidate.transactionHash, + receiptLogOrdinal: String(launchEvidence.receiptLogOrdinal), + blockHash: input.launchCandidate.blockHash, + }), + }); + const fullSnapshotCallCount = expectedRewardRpcCallCount( + "classic-v3", + projectedSnapshot.allocations.length, + projectedSnapshot.balances.length, + ); + const configurationCallCount = + initialConfiguration.endConfigurationSnapshot.providerCallCounts[0] + + initialConfiguration.factoryProviderCallCounts[0]; + const aggregateCallsPerProvider = + runtimeObservation.providerCallCounts[0] + + configurationCallCount + + fullSnapshotCallCount; + if ( + projectedSnapshot.balances.length > 48 || + aggregateCallsPerProvider > 128 + ) { + throw validationError("rpc", "activation-model-call-budget"); + } + const rewardEvidence = await readDualRpcRewardSnapshot({ + model: "classic-v3", + baseline, + expected: projectedSnapshot, + blockNumber: initialConfiguration.activationBlockNumber, + blockHash: initialConfiguration.activationBlockHash, + providers: input.providers, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxAttempts: 1, + maxCallsPerProvider: fullSnapshotCallCount, + }, + }); + const configuration = initialConfiguration.endConfigurationSnapshot; + if ( + rewardEvidence.chunks.length !== 1 || + rewardEvidence.providerCallCounts[0] !== fullSnapshotCallCount || + rewardEvidence.providerCallCounts[1] !== fullSnapshotCallCount || + rewardEvidence.vault !== configuration.vault || + rewardEvidence.poolId !== configuration.poolId || + rewardEvidence.blockNumber !== configuration.blockNumber || + rewardEvidence.blockHash !== configuration.blockHash || + rewardEvidence.configurationEpoch !== configuration.configurationEpoch || + rewardEvidence.configurationHash !== configuration.configurationHash || + rewardEvidence.totalCreatorFeesReceived !== + configuration.totalCreatorFeesReceived || + rewardEvidence.totalCreatorFeesClaimed !== + configuration.totalCreatorFeesClaimed || + JSON.stringify(rewardEvidence.allocations) !== + JSON.stringify(configuration.allocations) || + !exactProviderTuple( + rewardEvidence.providerIdentities, + input.candidateEvidence.providerIdentities, + ) || + !exactProviderTuple( + rewardEvidence.providerVendorGroups, + input.candidateEvidence.providerVendorGroups, + ) || + !exactProviderTuple( + rewardEvidence.providerEndpointCommitments, + input.candidateEvidence.providerEndpointCommitments, + ) || + !exactProviderTuple( + rewardEvidence.providerOriginCommitments, + input.candidateEvidence.providerOriginCommitments, + ) + ) { + throw validationError("rpc", "activation-reward-evidence-binding"); + } + return Object.freeze({ + runtimeObservation, + initialConfiguration, + baseline, + projectedSnapshot, + rewardEvidence, + modelVerificationEvidence: Object.freeze([ + modelEvidence( + input.activationId, + "classic-v3-runtime-activation-v1", + Object.freeze({ + canonicalDeployment: input.canonicalDeployment, + runtimeObservation: + deterministicRuntimeEvidence(runtimeObservation), + }), + ), + modelEvidence( + input.activationId, + "classic-v3-initial-reward-configuration-v1", + deterministicInitialConfigurationEvidence(initialConfiguration), + ), + modelEvidence( + input.activationId, + "classic-v3-launch-reward-conservation-v1", + Object.freeze({ + projectedSnapshot, + rewardEvidence: deterministicRewardEvidence(rewardEvidence), + }), + ), + ]) as readonly [ + ClassicV3ActivationModelEvidence, + ClassicV3ActivationModelEvidence, + ClassicV3ActivationModelEvidence, + ], + }); +} diff --git a/lib/data-pipeline/classic-v3-reconciler-route-builder.server.ts b/lib/data-pipeline/classic-v3-reconciler-route-builder.server.ts new file mode 100644 index 00000000..c919b7cd --- /dev/null +++ b/lib/data-pipeline/classic-v3-reconciler-route-builder.server.ts @@ -0,0 +1,2164 @@ +import "server-only"; + +import { + decodeEventLog, + decodeFunctionData, + decodeFunctionResult, + encodeAbiParameters, + encodeFunctionData, + getAddress, + isAddress, + keccak256, + parseAbi, + parseAbiItem, + parseAbiParameters, + toEventSelector, + type Abi, + type AbiEvent, + type Address, + type Hex, +} from "viem"; + +import dependencies from "../../contracts/dependencies/ethereum-mainnet.json"; +import { + classicRewardVaultAbi, + classicV3HookAbi, + classicV3LaunchAbi, +} from "../classic-v3"; +import type { CanonicalJsonValue } from "./canonical-fingerprint"; +import { + getConfiguredClassicV3Release, + isClassicV3ReleaseVerified, +} from "../classic-v3-release"; +import { stateViewReadAbi, uerc20ReadAbi } from "../onchain/abis"; +import { + assembleReconcilerRoutesFromContributions, + type ReconcilerRouteContribution, +} from "./classic-v3-reconciler-route-contract"; +import { canonicalBytes32, type HexBytes32 } from "./codecs"; +import { + assembleReconcilerCorpusPages, + assembleReconcilerEntitlementPages, + createReconcilerCorpusManifest, + createReconcilerEntitlementManifest, +} from "./reconciler-corpus-partitions"; +import { dataPipelineError, invalidInput, validationError } from "./errors"; +import type { + ExactBlockRouteBuilder, + ExactBlockRpcClient, + ExactBlockRpcLog, + ExactBlockRpcReceipt, + ExactBlockRpcTransaction, +} from "./reconciler-exact-block-reader.server"; +import { + RECONCILER_ROUTE_KEYS, + type ReconcilerPreParityContract, +} from "./reconciler-preparity"; + +const ZERO_ADDRESS = `0x${"00".repeat(20)}` as Address; +// QuickNode's paid Ethereum eth_getLogs range is capped at 10,000 blocks. +// Keeping the request at that exact portable boundary avoids provider-specific +// success on one side of the dual-provider comparison. +export const CLASSIC_V3_RECONCILER_LOG_BLOCK_RANGE = 10_000n; +const MAXIMUM_LOGS_PER_REQUEST = 20_000; +const MAXIMUM_VAULTS_PER_LOG_REQUEST = 64; +const MAXIMUM_POOLS_PER_LOG_REQUEST = 64; +const CALLS_PER_LAUNCH = 21; +const ACTIVE_REWARD_CONFIGURATION_PARAMETERS = parseAbiParameters( + "uint256 chainId,address vault,bytes32 configurationHash,uint64 epoch,address[] beneficiaries,uint16[] sharesBps", +); + +const reconcilerRewardVaultFactoryAbi = parseAbi([ + "function isFactoryVault(address vault) view returns (bool)", + "function configurationHashOf(address vault) view returns (bytes32)", +]); + +const launchedEvent = parseAbiItem( + "event MemeTokenLaunchedV2(address indexed deployer,address indexed token,bytes32 indexed poolId,address feeHook,address rewardVault,address positionRecipient,uint256 positionTokenId,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,bytes32 rewardConfigurationHash,bytes32 launchHash)", +); +const liquidityEvent = parseAbiItem( + "event MemeLiquidityConfiguredV2(address indexed token,uint256 totalSupply,uint256 tokenLiquidityAmount,uint256 lockedTokenDust,int24 initialTick,int24 tickLower,int24 tickUpper,uint24 lpFeePips,bytes32 launchHash)", +); +const initialBuyEvent = parseAbiItem( + "event MemeCreatorInitialBuyV2(address indexed deployer,address indexed token,bytes32 indexed poolId,uint256 nativeAmount,uint256 tokenAmount,bytes32 launchHash)", +); +const initialBuyCustodyEvent = parseAbiItem( + "event MemeCreatorInitialBuyCustodyV2(address indexed deployer,address indexed token,address indexed custody,uint8 mode,uint16 durationDays,uint16 cliffDays,bytes32 configurationHash,bytes32 launchHash)", +); +const poolRegisteredEvent = parseAbiItem( + "event PoolRegistered(bytes32 indexed poolId,address indexed token,address indexed rewardVault,address registrar,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,bytes32 rewardConfigurationHash)", +); +const poolFeeDisclosureEvent = parseAbiItem( + "event PoolFeeDisclosure(bytes32 indexed poolId,address indexed token,address indexed rewardVault,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,uint16 buyCreatorFeeBps,uint16 sellCreatorFeeBps,uint16 launcherFeeBps,uint16 transferTaxBps,uint24 lpFeePips)", +); +const feeAccruedEvent = parseAbiItem( + "event NativeSwapFeesAccrued(bytes32 indexed poolId,address indexed swapSender,bool indexed isBuy,uint16 appliedTotalSwapFeeBps,uint256 grossNativeAmount,uint256 creatorFee,uint256 launcherFee)", +); +const creatorHookClaimEvent = parseAbiItem( + "event CreatorFeesClaimed(bytes32 indexed poolId,address indexed rewardVault,address indexed caller,uint256 amount)", +); +const launcherHookClaimEvent = parseAbiItem( + "event LauncherFeesClaimed(address indexed treasury,address indexed recipient,address indexed caller,uint256 amount)", +); +const vaultDeployedEvent = parseAbiItem( + "event ClassicRewardVaultDeployed(address indexed vault,bytes32 indexed poolId,address indexed feeHook,bytes32 salt,bytes32 configurationHash)", +); +const checkpointEvent = parseAbiItem( + "event CreatorFeesCheckpointed(bytes32 indexed poolId,uint64 indexed configurationEpoch,uint256 amount,uint256 totalCreatorFeesReceived)", +); +const beneficiaryClaimEvent = parseAbiItem( + "event BeneficiaryFeesClaimed(address indexed beneficiary,uint256 amount,uint256 beneficiaryTotalClaimed,uint256 vaultTotalReceived)", +); +const payoutChangedEvent = parseAbiItem( + "event PayoutWalletChanged(bytes32 indexed poolId,uint256 indexed allocationIndex,address indexed previousPayoutWallet,address newPayoutWallet,uint16 shareBps,uint64 configurationEpoch,bytes32 activeConfigurationHash,uint256 effectiveTotalCreatorFeesReceived)", +); +const ctoActivatedEvent = parseAbiItem( + "event CtoRewardConfigurationActivated(bytes32 indexed poolId,bytes32 indexed approvalReference,uint64 indexed configurationEpoch,bytes32 previousConfigurationHash,bytes32 newConfigurationHash,address[] beneficiaries,uint16[] sharesBps,uint256 effectiveTotalCreatorFeesReceived)", +); +const swapEvent = parseAbiItem( + "event Swap(bytes32 indexed id,address indexed sender,int128 amount0,int128 amount1,uint160 sqrtPriceX96,uint128 liquidity,int24 tick,uint24 fee)", +); + +const LAUNCHER_EVENTS = Object.freeze([ + launchedEvent, + liquidityEvent, + initialBuyEvent, + initialBuyCustodyEvent, +]); +const HOOK_EVENTS = Object.freeze([ + poolRegisteredEvent, + poolFeeDisclosureEvent, + feeAccruedEvent, + creatorHookClaimEvent, + launcherHookClaimEvent, +]); +const VAULT_EVENTS = Object.freeze([ + checkpointEvent, + beneficiaryClaimEvent, + payoutChangedEvent, + ctoActivatedEvent, +]); + +type Json = CanonicalJsonValue; + +type DecodedLog = Readonly<{ + eventName: string; + args: Readonly>; + log: ExactBlockRpcLog; +}>; + +type CallSpec = Readonly<{ + to: Address; + data: Hex; + decode: (data: Hex) => unknown; +}>; + +type LaunchRecord = Readonly<{ + deployer: Address; + token: Address; + poolId: HexBytes32; + hook: Address; + rewardVault: Address; + positionRecipient: Address; + positionTokenId: bigint; + buySwapFeeBps: number; + sellSwapFeeBps: number; + rewardConfigurationHash: HexBytes32; + launchHash: HexBytes32; + blockNumber: bigint; + blockHash: HexBytes32; + transactionHash: HexBytes32; + transactionIndex: number; + blockGlobalLogIndex: number; + log: ExactBlockRpcLog; +}>; + +type Release = Readonly<{ + launcher: Address; + hook: Address; + rewardVaultFactory: Address; + poolManager: Address; + stateView: Address; + startBlock: bigint; + runtime: ReadonlyArray>; +}>; + +type LaunchTransactionEvidence = Readonly<{ + receiptLogIndex: number; + value: bigint; + name: string; + symbol: string; + buySwapFeeBps: number; + sellSwapFeeBps: number; + description: string; + website: string; + image: string; + extraData: Hex; + rewardBeneficiaries: readonly Address[]; + rewardSharesBps: readonly number[]; + custodyMode: number; + custodyDurationDays: number; + custodyCliffDays: number; +}>; + +type LaunchCompanionEvidence = Readonly<{ + liquidity: DecodedLog; + initialBuy: DecodedLog; + custody: DecodedLog; + registration: DecodedLog; + disclosure: DecodedLog; + vaultDeployment: DecodedLog; +}>; + +function fail(operation: string): never { + throw validationError("uniswap", operation); +} + +function lowerAddress(value: Address): string { + return value.toLowerCase(); +} + +function exactAddress(value: unknown, operation: string): Address { + if (typeof value !== "string" || !isAddress(value)) fail(operation); + return getAddress(value); +} + +function exactBytes32(value: unknown, operation: string): HexBytes32 { + try { + return canonicalBytes32(value); + } catch { + return fail(operation); + } +} + +function exactData(value: unknown, operation: string): Hex { + if ( + typeof value !== "string" || + !/^0x(?:[0-9a-fA-F]{2})*$/u.test(value) + ) { + fail(operation); + } + return value.toLowerCase() as Hex; +} + +function exactText(value: unknown, operation: string): string { + if (typeof value !== "string") fail(operation); + return value; +} + +function integer(value: unknown, operation: string): bigint { + if (typeof value === "bigint") return value; + if (typeof value === "number" && Number.isSafeInteger(value)) { + return BigInt(value); + } + return fail(operation); +} + +function nonnegative(value: unknown, operation: string): bigint { + const parsed = integer(value, operation); + if (parsed < 0n) fail(operation); + return parsed; +} + +function absolute(value: unknown, operation: string): bigint { + const parsed = integer(value, operation); + return parsed < 0n ? -parsed : parsed; +} + +function safeInteger( + value: unknown, + minimum: number, + maximum: number, + operation: string, +): number { + const parsed = integer(value, operation); + if (parsed < BigInt(minimum) || parsed > BigInt(maximum)) fail(operation); + return Number(parsed); +} + +function tuple( + value: unknown, + length: number, + operation: string, +): readonly unknown[] { + if (!Array.isArray(value) || value.length !== length) fail(operation); + return value; +} + +function record( + value: unknown, + operation: string, +): Readonly> { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + fail(operation); + } + return value as Readonly>; +} + +function array(value: unknown, operation: string): readonly unknown[] { + if (!Array.isArray(value)) fail(operation); + return value; +} + +function sameHex(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase(); +} + +export function classicV3ReconcilerBlockRanges( + fromBlock: bigint, + toBlock: bigint, +): readonly Readonly<{ fromBlock: bigint; toBlock: bigint }>[] { + if (fromBlock < 0n || toBlock < fromBlock) { + throw invalidInput("rpc", "classic-v3-log-range"); + } + const ranges: Array> = []; + for ( + let start = fromBlock; + start <= toBlock; + start += CLASSIC_V3_RECONCILER_LOG_BLOCK_RANGE + ) { + const end = start + CLASSIC_V3_RECONCILER_LOG_BLOCK_RANGE - 1n; + ranges.push(Object.freeze({ + fromBlock: start, + toBlock: end > toBlock ? toBlock : end, + })); + } + return Object.freeze(ranges); +} + +export function assertClassicV3ReconcilerLaunchCount(count: number): number { + if ( + !Number.isSafeInteger(count) || + count < 1 + ) { + fail("classic-v3-launch-cardinality"); + } + return count; +} + +function callSpec( + to: Address, + abi: Abi, + functionName: string, + args: readonly unknown[] = [], +): CallSpec { + const request = { abi, functionName, args } as never; + return Object.freeze({ + to, + data: encodeFunctionData(request), + decode: (result: Hex) => + decodeFunctionResult({ + abi, + functionName, + data: result, + } as never) as unknown, + }); +} + +function decodeKnownEvent( + eventBySelector: ReadonlyMap, + log: ExactBlockRpcLog, +): DecodedLog { + const selector = log.topics[0]?.toLowerCase(); + const event = selector ? eventBySelector.get(selector) : undefined; + if (!event) fail("classic-v3-log-selector"); + let decoded: ReturnType; + try { + decoded = decodeEventLog({ + abi: [event], + data: log.data, + topics: log.topics as [Hex, ...Hex[]], + strict: true, + }); + } catch { + return fail("classic-v3-log-decode"); + } + if ( + typeof decoded.args !== "object" || + decoded.args === null || + Array.isArray(decoded.args) + ) { + fail("classic-v3-log-args"); + } + return Object.freeze({ + eventName: decoded.eventName, + args: decoded.args as Readonly>, + log, + }); +} + +function eventMap(events: readonly AbiEvent[]) { + return new Map( + events.map((event) => [toEventSelector(event).toLowerCase(), event]), + ); +} + +async function readUncappedLogs(input: { + rpc: ExactBlockRpcClient; + addresses: Address | readonly Address[]; + topics: readonly (Hex | readonly Hex[] | null)[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}): Promise { + const logs = await input.rpc.getLogs({ + addresses: input.addresses, + topics: input.topics, + fromBlock: input.fromBlock, + toBlock: input.toBlock, + maximumLogs: MAXIMUM_LOGS_PER_REQUEST, + signal: input.signal, + }); + if (logs.length < MAXIMUM_LOGS_PER_REQUEST) return logs; + if (input.fromBlock === input.toBlock) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "classic-v3-single-block-log-boundary" }, + }); + } + const midpoint = input.fromBlock + (input.toBlock - input.fromBlock) / 2n; + const [left, right] = await Promise.all([ + readUncappedLogs({ ...input, toBlock: midpoint }), + readUncappedLogs({ ...input, fromBlock: midpoint + 1n }), + ]); + return Object.freeze([...left, ...right]); +} + +async function readLogsInRanges(input: { + rpc: ExactBlockRpcClient; + addresses: Address | readonly Address[]; + events: readonly AbiEvent[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}): Promise { + if (input.toBlock < input.fromBlock) return Object.freeze([]); + const selectorMap = eventMap(input.events); + const selectors = [...selectorMap.keys()] as Hex[]; + const allowedAddresses = new Set( + (Array.isArray(input.addresses) ? input.addresses : [input.addresses]) + .map((address) => lowerAddress(address)), + ); + const output: DecodedLog[] = []; + for (const { fromBlock, toBlock } of classicV3ReconcilerBlockRanges( + input.fromBlock, + input.toBlock, + )) { + const logs = await readUncappedLogs({ + rpc: input.rpc, + addresses: input.addresses, + topics: [selectors], + fromBlock, + toBlock, + signal: input.signal, + }); + if (logs.some((log) => + !allowedAddresses.has(lowerAddress(log.address)) || + !selectorMap.has((log.topics[0] ?? "").toLowerCase()) || + log.blockNumber < fromBlock || + log.blockNumber > toBlock + )) { + fail("classic-v3-log-filter-binding"); + } + output.push(...logs.map((log) => decodeKnownEvent(selectorMap, log))); + } + for (let index = 1; index < output.length; index += 1) { + const previous = output[index - 1]!.log; + const current = output[index]!.log; + if ( + current.blockNumber < previous.blockNumber || + (current.blockNumber === previous.blockNumber && + (current.transactionIndex < previous.transactionIndex || + (current.transactionIndex === previous.transactionIndex && + current.logIndex <= previous.logIndex))) + ) { + fail("classic-v3-log-corpus-order"); + } + } + return Object.freeze(output); +} + +async function readAddressBatches(input: { + rpc: ExactBlockRpcClient; + addresses: readonly Address[]; + events: readonly AbiEvent[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}): Promise { + const output: DecodedLog[] = []; + for ( + let index = 0; + index < input.addresses.length; + index += MAXIMUM_VAULTS_PER_LOG_REQUEST + ) { + output.push(...await readLogsInRanges({ + ...input, + addresses: input.addresses.slice( + index, + index + MAXIMUM_VAULTS_PER_LOG_REQUEST, + ), + })); + } + output.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + return Object.freeze(output); +} + +async function readPoolSwapBatches(input: { + rpc: ExactBlockRpcClient; + poolManager: Address; + poolIds: readonly HexBytes32[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}): Promise { + const selectorMap = eventMap([swapEvent]); + const output: DecodedLog[] = []; + for ( + let poolIndex = 0; + poolIndex < input.poolIds.length; + poolIndex += MAXIMUM_POOLS_PER_LOG_REQUEST + ) { + const poolIds = input.poolIds.slice( + poolIndex, + poolIndex + MAXIMUM_POOLS_PER_LOG_REQUEST, + ); + const allowedPoolIds = new Set( + poolIds.map((poolId) => poolId.toLowerCase()), + ); + for (const { fromBlock, toBlock } of classicV3ReconcilerBlockRanges( + input.fromBlock, + input.toBlock, + )) { + const logs = await readUncappedLogs({ + rpc: input.rpc, + addresses: input.poolManager, + topics: [toEventSelector(swapEvent), poolIds], + fromBlock, + toBlock, + signal: input.signal, + }); + if (logs.some((log) => + !sameHex(log.address, input.poolManager) || + !sameHex(log.topics[0] ?? "0x", toEventSelector(swapEvent)) || + !allowedPoolIds.has((log.topics[1] ?? "").toLowerCase()) || + log.blockNumber < fromBlock || + log.blockNumber > toBlock + )) { + fail("classic-v3-swap-log-filter-binding"); + } + output.push(...logs.map((log) => decodeKnownEvent(selectorMap, log))); + } + } + output.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + return Object.freeze(output); +} + +function resolvedRelease(contract: ReconcilerPreParityContract): Release { + if ( + contract.chainId !== "1" || + contract.releaseId !== "classic-v3" || + contract.modelId !== "classic" || + contract.routeKeys.length !== RECONCILER_ROUTE_KEYS.length || + contract.routeKeys.some( + (routeKey, index) => routeKey !== RECONCILER_ROUTE_KEYS[index], + ) + ) { + throw invalidInput("config", "classic-v3-reconciler-release"); + } + const configured = getConfiguredClassicV3Release("production"); + if ( + configured.chainId !== 1 || + !isClassicV3ReleaseVerified( + configured.appManifest, + configured.releaseManifest, + 1, + ) + ) { + fail("classic-v3-reconciler-manifest"); + } + const app = configured.appManifest; + const launcher = exactAddress(app.memeLaunchV2, "classic-v3-launcher"); + const hook = exactAddress(app.ethCreatorFeeHookV3, "classic-v3-hook"); + const rewardVaultFactory = exactAddress( + app.classicRewardVaultFactoryV1, + "classic-v3-reward-vault-factory", + ); + const poolManager = exactAddress( + dependencies.contracts.poolManager.address, + "classic-v3-pool-manager", + ); + const stateView = exactAddress( + dependencies.contracts.stateView.address, + "classic-v3-state-view", + ); + const start = app.deploymentBlocks?.memeLaunchV2; + if (!Number.isSafeInteger(start) || (start ?? -1) < 0) { + fail("classic-v3-start-block"); + } + return Object.freeze({ + launcher, + hook, + rewardVaultFactory, + poolManager, + stateView, + startBlock: BigInt(start!), + runtime: Object.freeze([ + Object.freeze({ + address: exactAddress( + app.classicCtoAuthorityV1, + "classic-v3-cto-authority", + ), + expectedHash: exactBytes32( + app.runtimeCodeHashes?.classicCtoAuthorityV1, + "classic-v3-cto-authority-runtime-hash", + ), + label: "cto-authority", + }), + Object.freeze({ + address: launcher, + expectedHash: exactBytes32( + app.runtimeCodeHashes?.memeLaunchV2, + "classic-v3-launcher-runtime-hash", + ), + label: "launcher", + }), + Object.freeze({ + address: hook, + expectedHash: exactBytes32( + app.runtimeCodeHashes?.ethCreatorFeeHookV3, + "classic-v3-hook-runtime-hash", + ), + label: "hook", + }), + Object.freeze({ + address: rewardVaultFactory, + expectedHash: exactBytes32( + app.runtimeCodeHashes?.classicRewardVaultFactoryV1, + "classic-v3-reward-factory-runtime-hash", + ), + label: "reward-vault-factory", + }), + Object.freeze({ + address: exactAddress( + app.classicInitialBuyVestingWalletFactoryV1, + "classic-v3-initial-buy-vesting-factory", + ), + expectedHash: exactBytes32( + app.runtimeCodeHashes?.classicInitialBuyVestingWalletFactoryV1, + "classic-v3-initial-buy-vesting-factory-runtime-hash", + ), + label: "initial-buy-vesting-factory", + }), + Object.freeze({ + address: exactAddress( + app.classicLaunchPolicyV1, + "classic-v3-launch-policy", + ), + expectedHash: exactBytes32( + app.runtimeCodeHashes?.classicLaunchPolicyV1, + "classic-v3-launch-policy-runtime-hash", + ), + label: "launch-policy", + }), + Object.freeze({ + address: exactAddress( + app.ethCreatorFeeHookFactoryV3, + "classic-v3-hook-factory", + ), + expectedHash: exactBytes32( + app.runtimeCodeHashes?.ethCreatorFeeHookFactoryV3, + "classic-v3-hook-factory-runtime-hash", + ), + label: "hook-factory", + }), + Object.freeze({ + address: exactAddress( + app.lockedPositionFeeForwarderFactory, + "classic-v3-position-forwarder-factory", + ), + expectedHash: exactBytes32( + app.runtimeCodeHashes?.lockedPositionFeeForwarderFactory, + "classic-v3-position-forwarder-factory-runtime-hash", + ), + label: "position-forwarder-factory", + }), + Object.freeze({ + address: poolManager, + expectedHash: exactBytes32( + dependencies.contracts.poolManager.runtimeCodeHash, + "classic-v3-pool-manager-runtime-hash", + ), + label: "pool-manager", + }), + Object.freeze({ + address: stateView, + expectedHash: exactBytes32( + dependencies.contracts.stateView.runtimeCodeHash, + "classic-v3-state-view-runtime-hash", + ), + label: "state-view", + }), + ]), + }); +} + +async function assertRuntime( + rpc: ExactBlockRpcClient, + release: Release, + blockHash: HexBytes32, + signal: AbortSignal, +) { + for (const runtime of release.runtime) { + const codeHash = await rpc.getCodeHash({ + address: runtime.address, + blockHash, + signal, + }); + if (codeHash !== runtime.expectedHash) { + fail(`classic-v3-runtime-${runtime.label}`); + } + } +} + +function oneByKey( + values: readonly DecodedLog[], + key: (value: DecodedLog) => string, + operation: string, +): ReadonlyMap { + const output = new Map(); + for (const value of values) { + const identity = key(value).toLowerCase(); + if (output.has(identity)) fail(operation); + output.set(identity, value); + } + return output; +} + +function launchRecords( + logs: readonly DecodedLog[], + release: Release, +): readonly LaunchRecord[] { + const launched = logs.filter((value) => value.eventName === "MemeTokenLaunchedV2"); + assertClassicV3ReconcilerLaunchCount(launched.length); + const tokens = new Set(); + const pools = new Set(); + const output = launched.map(({ args, log }) => { + const token = exactAddress(args.token, "classic-v3-launch-token"); + const poolId = exactBytes32(args.poolId, "classic-v3-launch-pool"); + if ( + tokens.has(lowerAddress(token)) || + pools.has(poolId) || + !sameHex( + exactAddress(args.feeHook, "classic-v3-launch-hook"), + release.hook, + ) + ) { + fail("classic-v3-launch-identity"); + } + tokens.add(lowerAddress(token)); + pools.add(poolId); + return Object.freeze({ + deployer: exactAddress(args.deployer, "classic-v3-launch-deployer"), + token, + poolId, + hook: release.hook, + rewardVault: exactAddress(args.rewardVault, "classic-v3-launch-vault"), + positionRecipient: exactAddress( + args.positionRecipient, + "classic-v3-position-recipient", + ), + positionTokenId: nonnegative( + args.positionTokenId, + "classic-v3-position-token-id", + ), + buySwapFeeBps: safeInteger( + args.buySwapFeeBps, + 100, + 1_000, + "classic-v3-buy-fee", + ), + sellSwapFeeBps: safeInteger( + args.sellSwapFeeBps, + 100, + 1_000, + "classic-v3-sell-fee", + ), + rewardConfigurationHash: exactBytes32( + args.rewardConfigurationHash, + "classic-v3-reward-configuration-hash", + ), + launchHash: exactBytes32(args.launchHash, "classic-v3-launch-hash"), + blockNumber: log.blockNumber, + blockHash: log.blockHash, + transactionHash: log.transactionHash, + transactionIndex: log.transactionIndex, + blockGlobalLogIndex: log.logIndex, + log, + }); + }); + output.sort((left, right) => + left.blockNumber === right.blockNumber + ? left.transactionIndex === right.transactionIndex + ? left.blockGlobalLogIndex - right.blockGlobalLogIndex + : left.transactionIndex - right.transactionIndex + : left.blockNumber < right.blockNumber ? -1 : 1 + ); + return Object.freeze(output); +} + +function eventIdentityMatches( + event: DecodedLog, + launch: LaunchRecord, +): boolean { + return event.log.blockHash === launch.blockHash && + event.log.transactionHash === launch.transactionHash; +} + +function validatedCompanions(input: { + launches: readonly LaunchRecord[]; + launcherLogs: readonly DecodedLog[]; + hookLogs: readonly DecodedLog[]; + factoryLogs: readonly DecodedLog[]; + release: Release; +}) { + const liquidity = oneByKey( + input.launcherLogs.filter((value) => value.eventName === "MemeLiquidityConfiguredV2"), + (value) => lowerAddress(exactAddress(value.args.token, "classic-v3-liquidity-token")), + "classic-v3-liquidity-cardinality", + ); + const initialBuy = oneByKey( + input.launcherLogs.filter((value) => value.eventName === "MemeCreatorInitialBuyV2"), + (value) => lowerAddress(exactAddress(value.args.token, "classic-v3-initial-buy-token")), + "classic-v3-initial-buy-cardinality", + ); + const custody = oneByKey( + input.launcherLogs.filter((value) => value.eventName === "MemeCreatorInitialBuyCustodyV2"), + (value) => lowerAddress(exactAddress(value.args.token, "classic-v3-custody-token")), + "classic-v3-custody-cardinality", + ); + const registered = oneByKey( + input.hookLogs.filter((value) => value.eventName === "PoolRegistered"), + (value) => exactBytes32(value.args.poolId, "classic-v3-registration-pool"), + "classic-v3-registration-cardinality", + ); + const disclosure = oneByKey( + input.hookLogs.filter((value) => value.eventName === "PoolFeeDisclosure"), + (value) => exactBytes32(value.args.poolId, "classic-v3-disclosure-pool"), + "classic-v3-disclosure-cardinality", + ); + const vaultDeployment = oneByKey( + input.factoryLogs.filter((value) => value.eventName === "ClassicRewardVaultDeployed"), + (value) => lowerAddress(exactAddress(value.args.vault, "classic-v3-deployed-vault")), + "classic-v3-vault-deployment-cardinality", + ); + + const companions = new Map(); + for (const launch of input.launches) { + const tokenKey = lowerAddress(launch.token); + const liquidityEvent = liquidity.get(tokenKey); + const initialBuyEvent = initialBuy.get(tokenKey); + const custodyEvent = custody.get(tokenKey); + const registrationEvent = registered.get(launch.poolId); + const disclosureEvent = disclosure.get(launch.poolId); + const vaultEvent = vaultDeployment.get(lowerAddress(launch.rewardVault)); + if ( + !liquidityEvent || + !initialBuyEvent || + !custodyEvent || + !registrationEvent || + !disclosureEvent || + !vaultEvent || + !eventIdentityMatches(liquidityEvent, launch) || + !eventIdentityMatches(initialBuyEvent, launch) || + !eventIdentityMatches(custodyEvent, launch) || + !eventIdentityMatches(registrationEvent, launch) || + !eventIdentityMatches(disclosureEvent, launch) || + vaultEvent.log.blockNumber > launch.blockNumber || + ( + vaultEvent.log.blockNumber === launch.blockNumber && + ( + vaultEvent.log.transactionIndex > launch.transactionIndex || + ( + vaultEvent.log.transactionIndex === launch.transactionIndex && + vaultEvent.log.logIndex > launch.blockGlobalLogIndex + ) + ) + ) + ) { + fail("classic-v3-launch-companion-provenance"); + } + const values = [ + liquidityEvent.args.launchHash, + initialBuyEvent.args.launchHash, + custodyEvent.args.launchHash, + ]; + if (values.some((value) => !sameHex(exactBytes32(value, "classic-v3-companion-hash"), launch.launchHash))) { + fail("classic-v3-launch-companion-hash"); + } + if ( + !sameHex(exactBytes32(initialBuyEvent.args.poolId, "classic-v3-initial-buy-pool"), launch.poolId) || + !sameHex(exactAddress(initialBuyEvent.args.deployer, "classic-v3-initial-buy-deployer"), launch.deployer) || + !sameHex(exactAddress(custodyEvent.args.deployer, "classic-v3-custody-deployer"), launch.deployer) || + !sameHex(exactAddress(registrationEvent.args.token, "classic-v3-registration-token"), launch.token) || + !sameHex(exactAddress(registrationEvent.args.rewardVault, "classic-v3-registration-vault"), launch.rewardVault) || + !sameHex(exactAddress(registrationEvent.args.registrar, "classic-v3-registration-registrar"), input.release.launcher) || + !sameHex(exactBytes32(registrationEvent.args.rewardConfigurationHash, "classic-v3-registration-hash"), launch.rewardConfigurationHash) || + !sameHex(exactAddress(disclosureEvent.args.token, "classic-v3-disclosure-token"), launch.token) || + !sameHex(exactAddress(disclosureEvent.args.rewardVault, "classic-v3-disclosure-vault"), launch.rewardVault) || + !sameHex(exactBytes32(vaultEvent.args.poolId, "classic-v3-deployed-vault-pool"), launch.poolId) || + !sameHex(exactAddress(vaultEvent.args.feeHook, "classic-v3-deployed-vault-hook"), input.release.hook) || + !sameHex(exactBytes32(vaultEvent.args.configurationHash, "classic-v3-deployed-vault-hash"), launch.rewardConfigurationHash) + ) { + fail("classic-v3-launch-companion-mismatch"); + } + companions.set(tokenKey, Object.freeze({ + liquidity: liquidityEvent, + initialBuy: initialBuyEvent, + custody: custodyEvent, + registration: registrationEvent, + disclosure: disclosureEvent, + vaultDeployment: vaultEvent, + })); + } + return companions; +} + +function validatedLaunchTransactions(input: { + launches: readonly LaunchRecord[]; + transactions: readonly ExactBlockRpcTransaction[]; + receipts: readonly ExactBlockRpcReceipt[]; + companions: ReadonlyMap; + release: Release; +}): ReadonlyMap { + if ( + input.transactions.length !== input.launches.length || + input.receipts.length !== input.launches.length + ) { + fail("classic-v3-launch-transaction-cardinality"); + } + const output = new Map(); + for (let index = 0; index < input.launches.length; index += 1) { + const launch = input.launches[index]!; + const transaction = input.transactions[index]!; + const receipt = input.receipts[index]!; + if ( + !sameHex(transaction.transactionHash, launch.transactionHash) || + transaction.blockNumber !== launch.blockNumber || + !sameHex(transaction.blockHash, launch.blockHash) || + transaction.transactionIndex !== launch.transactionIndex || + !sameHex(transaction.from, launch.deployer) || + !sameHex(transaction.to, input.release.launcher) || + !sameHex(receipt.transactionHash, launch.transactionHash) || + receipt.blockNumber !== launch.blockNumber || + !sameHex(receipt.blockHash, launch.blockHash) || + receipt.transactionIndex !== launch.transactionIndex + ) { + fail("classic-v3-launch-transaction-binding"); + } + const launchReceiptLogs = receipt.logs.filter((log) => + sameHex(log.address, input.release.launcher) && + log.logIndex === launch.blockGlobalLogIndex && + sameHex(log.transactionHash, launch.transactionHash) && + sameHex(log.data, launch.log.data) && + log.topics.length === launch.log.topics.length && + log.topics.every((topic, topicIndex) => + sameHex(topic, launch.log.topics[topicIndex]!) + ) && + sameHex(log.topics[0] ?? "0x", toEventSelector(launchedEvent)) + ); + if (launchReceiptLogs.length !== 1) { + fail("classic-v3-launch-receipt-log"); + } + const companion = input.companions.get(lowerAddress(launch.token)); + if (!companion) fail("classic-v3-launch-receipt-companion"); + const sameTransactionCompanions = [ + companion.liquidity, + companion.initialBuy, + companion.custody, + companion.registration, + companion.disclosure, + ]; + for (const expected of sameTransactionCompanions) { + const matchingLogs = receipt.logs.filter((log) => + sameHex(log.address, expected.log.address) && + log.logIndex === expected.log.logIndex && + sameHex(log.transactionHash, expected.log.transactionHash) && + sameHex(log.data, expected.log.data) && + log.topics.length === expected.log.topics.length && + log.topics.every((topic, topicIndex) => + sameHex(topic, expected.log.topics[topicIndex]!) + ) + ); + if (matchingLogs.length !== 1) { + fail("classic-v3-launch-receipt-companion"); + } + } + + let decoded: ReturnType; + try { + decoded = decodeFunctionData({ + abi: classicV3LaunchAbi, + data: transaction.input, + }); + } catch { + return fail("classic-v3-launch-calldata-decode"); + } + if (decoded.functionName !== "launch" || decoded.args.length !== 1) { + fail("classic-v3-launch-calldata-selector"); + } + const parameters = record(decoded.args[0], "classic-v3-launch-parameters"); + exactBytes32(parameters.creatorSalt, "classic-v3-creator-salt"); + const metadata = record(parameters.metadata, "classic-v3-launch-metadata"); + const custody = record( + parameters.initialBuyCustody, + "classic-v3-launch-custody", + ); + const beneficiaries = array( + parameters.rewardBeneficiaries, + "classic-v3-launch-beneficiaries", + ).map((value) => exactAddress(value, "classic-v3-launch-beneficiary")); + const shares = array( + parameters.rewardSharesBps, + "classic-v3-launch-reward-shares", + ).map((value) => safeInteger( + value, + 1, + 10_000, + "classic-v3-launch-reward-share", + )); + if ( + beneficiaries.length < 1 || + beneficiaries.length > 5 || + beneficiaries.length !== shares.length || + new Set(beneficiaries.map(lowerAddress)).size !== beneficiaries.length || + shares.reduce((total, value) => total + value, 0) !== 10_000 + ) { + fail("classic-v3-launch-reward-configuration"); + } + const buySwapFeeBps = safeInteger( + parameters.buySwapFeeBps, + 100, + 1_000, + "classic-v3-launch-calldata-buy-fee", + ); + const sellSwapFeeBps = safeInteger( + parameters.sellSwapFeeBps, + 100, + 1_000, + "classic-v3-launch-calldata-sell-fee", + ); + if ( + buySwapFeeBps !== launch.buySwapFeeBps || + sellSwapFeeBps !== launch.sellSwapFeeBps || + buySwapFeeBps % 100 !== 0 || + sellSwapFeeBps % 100 !== 0 || + transaction.value <= 0n + ) { + fail("classic-v3-launch-calldata-economics"); + } + output.set(lowerAddress(launch.token), Object.freeze({ + receiptLogIndex: launchReceiptLogs[0]!.receiptLogIndex, + value: transaction.value, + name: exactText(parameters.name, "classic-v3-launch-name"), + symbol: exactText(parameters.symbol, "classic-v3-launch-symbol"), + buySwapFeeBps, + sellSwapFeeBps, + description: exactText(metadata.description, "classic-v3-launch-description"), + website: exactText(metadata.website, "classic-v3-launch-website"), + image: exactText(metadata.image, "classic-v3-launch-image"), + extraData: exactData(metadata.extraData, "classic-v3-launch-extra-data"), + rewardBeneficiaries: Object.freeze(beneficiaries), + rewardSharesBps: Object.freeze(shares), + custodyMode: safeInteger(custody.mode, 0, 3, "classic-v3-launch-custody-mode"), + custodyDurationDays: safeInteger( + custody.durationDays, + 0, + 65_535, + "classic-v3-launch-custody-duration", + ), + custodyCliffDays: safeInteger( + custody.cliffDays, + 0, + 65_535, + "classic-v3-launch-custody-cliff", + ), + })); + } + return output; +} + +async function readCalls( + rpc: ExactBlockRpcClient, + specs: readonly CallSpec[], + blockHash: HexBytes32, + signal: AbortSignal, +): Promise { + const results = await rpc.callMany({ + calls: specs.map(({ to, data }) => Object.freeze({ to, data })), + blockHash, + signal, + }); + if (results.length !== specs.length) fail("classic-v3-call-cardinality"); + return Object.freeze(results.map((result, index) => specs[index]!.decode(result))); +} + +function groupLogsByAddress(logs: readonly DecodedLog[]) { + const output = new Map(); + for (const log of logs) { + const key = lowerAddress(log.log.address); + const values = output.get(key) ?? []; + values.push(log); + output.set(key, values); + } + return output; +} + +function grossFeeTotals( + hookLogs: readonly DecodedLog[], + swapLogs: readonly DecodedLog[], + poolId: HexBytes32, + buySwapFeeBps: number, + sellSwapFeeBps: number, +) { + const fees = hookLogs.filter((event) => + event.eventName === "NativeSwapFeesAccrued" && + sameHex(exactBytes32(event.args.poolId, "classic-v3-accrual-pool"), poolId) + ); + const swaps = swapLogs.filter((event) => + event.eventName === "Swap" && + sameHex(exactBytes32(event.args.id, "classic-v3-swap-pool"), poolId) + ); + if (swaps.length < 1 || fees.length > swaps.length) { + fail("classic-v3-swap-fee-event-coverage"); + } + let gross = 0n; + let creator = 0n; + let launcher = 0n; + const feeAmounts = fees.map((event) => { + const grossAmount = nonnegative(event.args.grossNativeAmount, "classic-v3-gross-fee"); + const creatorAmount = nonnegative(event.args.creatorFee, "classic-v3-creator-fee"); + const launcherAmount = nonnegative(event.args.launcherFee, "classic-v3-launcher-fee"); + if (typeof event.args.isBuy !== "boolean") { + fail("classic-v3-fee-direction"); + } + const appliedFeeBps = safeInteger( + event.args.appliedTotalSwapFeeBps, + 100, + 1_000, + "classic-v3-applied-swap-fee", + ); + const configuredFeeBps = event.args.isBuy + ? buySwapFeeBps + : sellSwapFeeBps; + const expectedFloorTotalFee = + grossAmount * BigInt(configuredFeeBps) / 10_000n; + const expectedCeilingTotalFee = + (grossAmount * BigInt(configuredFeeBps) + 9_999n) / 10_000n; + const actualTotalFee = creatorAmount + launcherAmount; + const expectedLauncherFee = grossAmount * 10n / 10_000n; + if ( + appliedFeeBps !== configuredFeeBps || + actualTotalFee === 0n || + ( + actualTotalFee !== expectedFloorTotalFee && + actualTotalFee !== expectedCeilingTotalFee + ) || + launcherAmount !== ( + expectedLauncherFee > actualTotalFee + ? actualTotalFee + : expectedLauncherFee + ) || + creatorAmount !== actualTotalFee - launcherAmount + ) { + fail("classic-v3-fee-conservation"); + } + gross += grossAmount; + creator += creatorAmount; + launcher += launcherAmount; + return Object.freeze({ + grossAmount, + actualTotalFee, + isBuy: event.args.isBuy, + sender: exactAddress(event.args.swapSender, "classic-v3-fee-sender"), + }); + }); + + const swapNativeAmounts = swaps.map((event) => { + if (safeInteger(event.args.fee, 0, 1_000_000, "classic-v3-swap-lp-fee") !== 0) { + fail("classic-v3-fee-conservation"); + } + const amount = absolute(event.args.amount0, "classic-v3-swap-native"); + if (amount === 0n) fail("classic-v3-swap-native"); + return Object.freeze({ + amount, + isBuy: integer(event.args.amount0, "classic-v3-swap-direction") > 0n, + sender: exactAddress(event.args.sender, "classic-v3-swap-sender"), + }); + }); + + const candidates = fees.map((fee, feeIndex) => { + const related = swaps.flatMap((swap, swapIndex) => { + if ( + fee.log.blockNumber !== swap.log.blockNumber || + fee.log.transactionIndex !== swap.log.transactionIndex || + !sameHex(fee.log.blockHash, swap.log.blockHash) || + !sameHex(fee.log.transactionHash, swap.log.transactionHash) || + feeAmounts[feeIndex]!.isBuy !== swapNativeAmounts[swapIndex]!.isBuy || + !sameHex(feeAmounts[feeIndex]!.sender, swapNativeAmounts[swapIndex]!.sender) + ) { + return []; + } + return [swapIndex]; + }); + const previous = related.filter((swapIndex) => + swaps[swapIndex]!.log.logIndex < fee.log.logIndex + ).at(-1); + const next = related.find((swapIndex) => + swaps[swapIndex]!.log.logIndex > fee.log.logIndex + ); + return Object.freeze([previous, next] + .filter((swapIndex): swapIndex is number => swapIndex !== undefined) + .filter((swapIndex, index, values) => + values.indexOf(swapIndex) === index && + ( + feeAmounts[feeIndex]!.grossAmount === swapNativeAmounts[swapIndex]!.amount || + feeAmounts[feeIndex]!.grossAmount === + swapNativeAmounts[swapIndex]!.amount + feeAmounts[feeIndex]!.actualTotalFee + ) + )); + }); + + let assignmentCount = 0; + let matchedSwapIndexes: readonly number[] = Object.freeze([]); + function assign(feeIndex: number, previousSwapIndex: number, path: number[]) { + if (assignmentCount > 1) return; + if (feeIndex === candidates.length) { + assignmentCount += 1; + matchedSwapIndexes = Object.freeze([...path]); + return; + } + for (const swapIndex of candidates[feeIndex]!) { + if (swapIndex <= previousSwapIndex) continue; + path.push(swapIndex); + assign(feeIndex + 1, swapIndex, path); + path.pop(); + } + } + assign(0, -1, []); + if (assignmentCount !== 1) { + fail("classic-v3-swap-fee-provenance"); + } + const matched = new Set(matchedSwapIndexes); + for (let index = 0; index < swaps.length; index += 1) { + if (matched.has(index)) continue; + const configuredFeeBps = integer(swaps[index]!.args.amount0, "classic-v3-swap-direction") > 0n + ? buySwapFeeBps + : sellSwapFeeBps; + if (swapNativeAmounts[index]!.amount * BigInt(configuredFeeBps) / 10_000n !== 0n) { + fail("classic-v3-swap-fee-event-coverage"); + } + } + return Object.freeze({ gross, creator, launcher, count: fees.length }); +} + +function vaultEventInputs(logs: readonly DecodedLog[], poolId: HexBytes32) { + return Object.freeze(logs.map((event) => { + const common = { + blockNumber: event.log.blockNumber.toString(), + blockHash: event.log.blockHash, + transactionHash: event.log.transactionHash, + transactionIndex: event.log.transactionIndex, + logIndex: event.log.logIndex, + }; + if (event.eventName === "CreatorFeesCheckpointed") { + if (!sameHex(exactBytes32(event.args.poolId, "classic-v3-checkpoint-pool"), poolId)) { + fail("classic-v3-checkpoint-pool-mismatch"); + } + return { + ...common, + kind: "checkpoint", + configurationEpoch: nonnegative(event.args.configurationEpoch, "classic-v3-checkpoint-epoch").toString(), + amountWei: nonnegative(event.args.amount, "classic-v3-checkpoint-amount").toString(), + totalCreatorFeesReceivedWei: nonnegative(event.args.totalCreatorFeesReceived, "classic-v3-checkpoint-total").toString(), + } satisfies Json; + } + if (event.eventName === "BeneficiaryFeesClaimed") { + return { + ...common, + kind: "claim", + beneficiary: lowerAddress(exactAddress(event.args.beneficiary, "classic-v3-claim-beneficiary")), + amountWei: nonnegative(event.args.amount, "classic-v3-claim-amount").toString(), + beneficiaryTotalClaimedWei: nonnegative(event.args.beneficiaryTotalClaimed, "classic-v3-claim-total").toString(), + vaultTotalReceivedWei: nonnegative(event.args.vaultTotalReceived, "classic-v3-claim-vault-total").toString(), + } satisfies Json; + } + if (event.eventName === "PayoutWalletChanged") { + if (!sameHex(exactBytes32(event.args.poolId, "classic-v3-payout-pool"), poolId)) { + fail("classic-v3-payout-pool-mismatch"); + } + return { + ...common, + kind: "payout-change", + allocationIndex: nonnegative(event.args.allocationIndex, "classic-v3-payout-index").toString(), + previousPayoutWallet: lowerAddress(exactAddress(event.args.previousPayoutWallet, "classic-v3-previous-payout")), + newPayoutWallet: lowerAddress(exactAddress(event.args.newPayoutWallet, "classic-v3-new-payout")), + shareBps: safeInteger(event.args.shareBps, 1, 10_000, "classic-v3-payout-share"), + configurationEpoch: nonnegative(event.args.configurationEpoch, "classic-v3-payout-epoch").toString(), + activeConfigurationHash: exactBytes32(event.args.activeConfigurationHash, "classic-v3-active-configuration-hash"), + effectiveTotalCreatorFeesReceivedWei: nonnegative(event.args.effectiveTotalCreatorFeesReceived, "classic-v3-payout-total").toString(), + } satisfies Json; + } + if (!sameHex(exactBytes32(event.args.poolId, "classic-v3-cto-pool"), poolId)) { + fail("classic-v3-cto-pool-mismatch"); + } + const beneficiaries = event.args.beneficiaries; + const shares = event.args.sharesBps; + if (!Array.isArray(beneficiaries) || !Array.isArray(shares) || beneficiaries.length !== shares.length) { + fail("classic-v3-cto-allocation"); + } + return { + ...common, + kind: "cto-activation", + approvalReference: exactBytes32(event.args.approvalReference, "classic-v3-cto-reference"), + configurationEpoch: nonnegative(event.args.configurationEpoch, "classic-v3-cto-epoch").toString(), + previousConfigurationHash: exactBytes32(event.args.previousConfigurationHash, "classic-v3-cto-previous-hash"), + newConfigurationHash: exactBytes32(event.args.newConfigurationHash, "classic-v3-cto-new-hash"), + allocations: beneficiaries.map((beneficiary, index) => ({ + beneficiary: lowerAddress(exactAddress(beneficiary, "classic-v3-cto-beneficiary")), + shareBps: safeInteger(shares[index], 1, 10_000, "classic-v3-cto-share"), + })), + effectiveTotalCreatorFeesReceivedWei: nonnegative(event.args.effectiveTotalCreatorFeesReceived, "classic-v3-cto-total").toString(), + } satisfies Json; + })); +} + +function entitlementAccounts(input: Readonly<{ + initialBeneficiaries: readonly Address[]; + currentBeneficiaries: readonly Address[]; + logs: readonly DecodedLog[]; + poolId: HexBytes32; +}>): readonly Address[] { + const accounts = new Map(); + const add = (value: unknown, operation: string) => { + const account = exactAddress(value, operation); + accounts.set(lowerAddress(account), account); + }; + input.initialBeneficiaries.forEach((account) => + add(account, "classic-v3-initial-entitlement-account") + ); + input.currentBeneficiaries.forEach((account) => + add(account, "classic-v3-current-entitlement-account") + ); + for (const event of input.logs) { + if (event.eventName === "BeneficiaryFeesClaimed") { + add(event.args.beneficiary, "classic-v3-claimed-entitlement-account"); + continue; + } + if (event.eventName === "CreatorFeesCheckpointed") { + if (!sameHex( + exactBytes32(event.args.poolId, "classic-v3-entitlement-checkpoint-pool"), + input.poolId, + )) { + fail("classic-v3-entitlement-checkpoint-pool"); + } + continue; + } + if (event.eventName === "PayoutWalletChanged") { + if (!sameHex( + exactBytes32(event.args.poolId, "classic-v3-entitlement-payout-pool"), + input.poolId, + )) { + fail("classic-v3-entitlement-payout-pool"); + } + add( + event.args.previousPayoutWallet, + "classic-v3-previous-entitlement-account", + ); + add(event.args.newPayoutWallet, "classic-v3-new-entitlement-account"); + continue; + } + if (event.eventName !== "CtoRewardConfigurationActivated" || !sameHex( + exactBytes32(event.args.poolId, "classic-v3-entitlement-cto-pool"), + input.poolId, + )) { + fail("classic-v3-entitlement-event"); + } + if (!Array.isArray(event.args.beneficiaries)) { + fail("classic-v3-entitlement-cto-beneficiaries"); + } + event.args.beneficiaries.forEach((account) => + add(account, "classic-v3-cto-entitlement-account") + ); + } + if (accounts.size < 1) fail("classic-v3-entitlement-account-count"); + return Object.freeze( + [...accounts.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([, account]) => account), + ); +} + +function swapPoints(logs: readonly DecodedLog[], poolId: HexBytes32) { + const matches = logs.filter((event) => + event.eventName === "Swap" && + sameHex(exactBytes32(event.args.id, "classic-v3-swap-pool"), poolId) + ); + const byBlock = new Map(); + for (const event of matches) { + byBlock.set(event.log.blockNumber.toString(), event); + } + return Object.freeze({ + swapCount: matches.length, + last: matches.at(-1) ?? null, + points: Object.freeze([...byBlock.values()].map((event) => ({ + blockNumber: event.log.blockNumber.toString(), + blockHash: event.log.blockHash, + transactionHash: event.log.transactionHash, + transactionIndex: event.log.transactionIndex, + logIndex: event.log.logIndex, + sqrtPriceX96: nonnegative(event.args.sqrtPriceX96, "classic-v3-swap-price").toString(), + liquidity: nonnegative(event.args.liquidity, "classic-v3-swap-liquidity").toString(), + tick: safeInteger(event.args.tick, -887_272, 887_272, "classic-v3-swap-tick"), + feePips: safeInteger(event.args.fee, 0, 1_000_000, "classic-v3-swap-fee"), + }))), + }); +} + +function isoTimestamp(timestamp: bigint) { + if (timestamp > 8_640_000_000_000n) fail("classic-v3-block-timestamp"); + return new Date(Number(timestamp) * 1_000).toISOString(); +} + +async function buildContribution(input: { + rpc: ExactBlockRpcClient; + contract: ReconcilerPreParityContract; + blockNumber: bigint; + blockHash: HexBytes32; + signal: AbortSignal; +}): Promise { + const release = resolvedRelease(input.contract); + if ( + input.blockNumber < release.startBlock || + input.blockNumber.toString() !== input.contract.checkpointBlockNumber || + !sameHex(input.blockHash, input.contract.checkpointBlockHash) + ) { + fail("classic-v3-checkpoint-binding"); + } + await assertRuntime(input.rpc, release, input.blockHash, input.signal); + + const [launcherLogs, hookLogs, factoryLogs] = await Promise.all([ + readLogsInRanges({ + rpc: input.rpc, + addresses: release.launcher, + events: LAUNCHER_EVENTS, + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + readLogsInRanges({ + rpc: input.rpc, + addresses: release.hook, + events: HOOK_EVENTS, + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + readLogsInRanges({ + rpc: input.rpc, + addresses: release.rewardVaultFactory, + events: [vaultDeployedEvent], + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + ]); + const launches = launchRecords(launcherLogs, release); + const corpusManifest = createReconcilerCorpusManifest({ + contract: input.contract, + identities: launches.map((launch) => Object.freeze({ + tokenAddress: lowerAddress(launch.token), + poolId: launch.poolId, + launchTransactionHash: launch.transactionHash, + launchBlockNumber: launch.blockNumber.toString(), + launchTransactionIndex: launch.transactionIndex, + launchLogIndex: launch.blockGlobalLogIndex, + })), + }); + const companions = validatedCompanions({ + launches, + launcherLogs, + hookLogs, + factoryLogs, + release, + }); + const vaultLogs: DecodedLog[] = []; + const poolSwapLogs: DecodedLog[] = []; + const launchTransactions = new Map(); + const initialValues: unknown[] = []; + const beneficiaryCounts: number[] = []; + const beneficiaryBaseValues: unknown[] = []; + const activeBeneficiaries: Address[][] = []; + const entitlementAccountsByLaunch: Address[][] = []; + const balanceValues: unknown[] = []; + const timestamps = new Map(); + const timestampHashes = new Map(); + const completedCorpusPages: Array<(typeof corpusManifest.pages)[number]> = []; + for (const page of corpusManifest.pages) { + const pageRpc = input.rpc.createPartitionClient(page); + await pageRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + const pageLaunches = launches.slice(page.startIndex, page.endIndexExclusive); + const [transactions, receipts, pageVaultLogs, pagePoolSwapLogs] = + await Promise.all([ + pageRpc.getTransactions({ + transactions: pageLaunches.map((launch) => Object.freeze({ + transactionHash: launch.transactionHash, + expectedBlockNumber: launch.blockNumber, + expectedBlockHash: launch.blockHash, + expectedTo: release.launcher, + })), + signal: input.signal, + }), + pageRpc.getTransactionReceipts({ + receipts: pageLaunches.map((launch) => Object.freeze({ + transactionHash: launch.transactionHash, + expectedBlockNumber: launch.blockNumber, + expectedBlockHash: launch.blockHash, + })), + signal: input.signal, + }), + readAddressBatches({ + rpc: pageRpc, + addresses: pageLaunches.map(({ rewardVault }) => rewardVault), + events: VAULT_EVENTS, + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + readPoolSwapBatches({ + rpc: pageRpc, + poolManager: release.poolManager, + poolIds: pageLaunches.map(({ poolId }) => poolId), + fromBlock: release.startBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + ]); + vaultLogs.push(...pageVaultLogs); + poolSwapLogs.push(...pagePoolSwapLogs); + const pageVaultLogsByAddress = groupLogsByAddress(pageVaultLogs); + const pageTransactions = validatedLaunchTransactions({ + launches: pageLaunches, + transactions, + receipts, + companions, + release, + }); + for (const [key, transaction] of pageTransactions) { + if (launchTransactions.has(key)) fail("classic-v3-launch-transaction-duplicate"); + launchTransactions.set(key, transaction); + } + const pageInitialValues = await readCalls( + pageRpc, + pageLaunches.flatMap((launch) => { + const transaction = pageTransactions.get(lowerAddress(launch.token)); + if (!transaction) fail("classic-v3-launch-transaction-missing"); + return [ + callSpec(launch.token, uerc20ReadAbi, "name"), + callSpec(launch.token, uerc20ReadAbi, "symbol"), + callSpec(launch.token, uerc20ReadAbi, "decimals"), + callSpec(launch.token, uerc20ReadAbi, "totalSupply"), + callSpec(launch.token, uerc20ReadAbi, "creator"), + callSpec(launch.token, uerc20ReadAbi, "metadata"), + callSpec(release.stateView, stateViewReadAbi, "getSlot0", [launch.poolId]), + callSpec(release.stateView, stateViewReadAbi, "getLiquidity", [launch.poolId]), + callSpec(release.hook, classicV3HookAbi, "feeDisclosure", [launch.poolId]), + callSpec(release.hook, classicV3HookAbi, "poolFeeConfig", [launch.poolId]), + callSpec(release.launcher, classicV3LaunchAbi, "predictRewardVault", [ + launch.token, + launch.deployer, + transaction.rewardBeneficiaries, + transaction.rewardSharesBps, + ]), + callSpec(release.rewardVaultFactory, reconcilerRewardVaultFactoryAbi, "isFactoryVault", [launch.rewardVault]), + callSpec(release.rewardVaultFactory, reconcilerRewardVaultFactoryAbi, "configurationHashOf", [launch.rewardVault]), + callSpec(launch.rewardVault, classicRewardVaultAbi, "feeHook"), + callSpec(launch.rewardVault, classicRewardVaultAbi, "poolId"), + callSpec(launch.rewardVault, classicRewardVaultAbi, "configurationHash"), + callSpec(launch.rewardVault, classicRewardVaultAbi, "activeConfigurationHash"), + callSpec(launch.rewardVault, classicRewardVaultAbi, "configurationEpoch"), + callSpec(launch.rewardVault, classicRewardVaultAbi, "beneficiaryCount"), + callSpec(launch.rewardVault, classicRewardVaultAbi, "totalCreatorFeesReceived"), + callSpec(launch.rewardVault, classicRewardVaultAbi, "totalCreatorFeesClaimed"), + ]; + }), + input.blockHash, + input.signal, + ); + initialValues.push(...pageInitialValues); + const pageCounts: number[] = []; + const beneficiarySpecs: CallSpec[] = []; + for (let index = 0; index < pageLaunches.length; index += 1) { + const count = safeInteger( + pageInitialValues[index * CALLS_PER_LAUNCH + 18], + 1, + 5, + "classic-v3-beneficiary-count", + ); + pageCounts.push(count); + beneficiaryCounts.push(count); + for (let allocationIndex = 0; allocationIndex < count; allocationIndex += 1) { + beneficiarySpecs.push( + callSpec(pageLaunches[index]!.rewardVault, classicRewardVaultAbi, "beneficiaryAt", [BigInt(allocationIndex)]), + callSpec(pageLaunches[index]!.rewardVault, classicRewardVaultAbi, "shareBpsAt", [BigInt(allocationIndex)]), + ); + } + } + const pageBeneficiaryValues = await readCalls( + pageRpc, + beneficiarySpecs, + input.blockHash, + input.signal, + ); + beneficiaryBaseValues.push(...pageBeneficiaryValues); + let pageBeneficiaryCursor = 0; + const pageActiveBeneficiaries: Address[][] = []; + for (const count of pageCounts) { + const values: Address[] = []; + for (let index = 0; index < count; index += 1) { + values.push(exactAddress( + pageBeneficiaryValues[pageBeneficiaryCursor], + "classic-v3-beneficiary", + )); + pageBeneficiaryCursor += 2; + } + pageActiveBeneficiaries.push(values); + activeBeneficiaries.push(values); + } + const pageEntitlements = pageLaunches.map((launch, launchIndex) => { + const transaction = pageTransactions.get(lowerAddress(launch.token)); + if (!transaction) fail("classic-v3-entitlement-launch-transaction"); + const logs = pageVaultLogsByAddress.get( + lowerAddress(launch.rewardVault), + ) ?? []; + vaultEventInputs(logs, launch.poolId); + return entitlementAccounts({ + initialBeneficiaries: transaction.rewardBeneficiaries, + currentBeneficiaries: pageActiveBeneficiaries[launchIndex]!, + logs, + poolId: launch.poolId, + }); + }); + entitlementAccountsByLaunch.push(...pageEntitlements.map((values) => [...values])); + const entitlementManifest = createReconcilerEntitlementManifest({ + contract: input.contract, + parentPage: page, + identities: pageLaunches.flatMap((launch, launchIndex) => + pageEntitlements[launchIndex]!.map((account) => Object.freeze({ + tokenAddress: lowerAddress(launch.token), + vaultAddress: lowerAddress(launch.rewardVault), + account: lowerAddress(account), + })) + ), + }); + const completedEntitlementPages: Array< + (typeof entitlementManifest.pages)[number] + > = []; + for (const entitlementPage of entitlementManifest.pages) { + const entitlementRpc = pageRpc.createPartitionClient(entitlementPage); + await entitlementRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + balanceValues.push(...await readCalls( + entitlementRpc, + entitlementPage.identities.flatMap((identity) => [ + callSpec( + getAddress(identity.vaultAddress), + classicRewardVaultAbi, + "claimable", + [getAddress(identity.account)], + ), + callSpec( + getAddress(identity.vaultAddress), + classicRewardVaultAbi, + "claimedBy", + [getAddress(identity.account)], + ), + ]), + input.blockHash, + input.signal, + )); + await entitlementRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + completedEntitlementPages.push(entitlementPage); + } + assembleReconcilerEntitlementPages( + entitlementManifest, + completedEntitlementPages, + ); + const timestampBindings = []; + for (const launch of pageLaunches) { + const key = launch.blockNumber.toString(); + const knownHash = timestampHashes.get(key); + if (knownHash !== undefined && !sameHex(knownHash, launch.blockHash)) { + fail("classic-v3-launch-block-hash-conflict"); + } + if (!timestamps.has(key) && knownHash === undefined) { + timestampHashes.set(key, launch.blockHash); + timestampBindings.push({ + blockNumber: launch.blockNumber, + expectedHash: launch.blockHash, + }); + } + } + const pageTimestamps = await pageRpc.getBlockTimestamps({ + blocks: timestampBindings, + signal: input.signal, + }); + if (pageTimestamps.length !== timestampBindings.length) { + fail("classic-v3-launch-timestamp-cardinality"); + } + timestampBindings.forEach((binding, index) => { + timestamps.set(binding.blockNumber.toString(), pageTimestamps[index]!); + }); + await pageRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + completedCorpusPages.push(page); + } + assembleReconcilerCorpusPages(corpusManifest, completedCorpusPages); + vaultLogs.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + poolSwapLogs.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + const vaultLogsByAddress = groupLogsByAddress(vaultLogs); + + const tokens: Json[] = []; + const charts: Json[] = []; + const rewards: Json[] = []; + let beneficiaryValueCursor = 0; + let balanceValueCursor = 0; + for (let index = 0; index < launches.length; index += 1) { + const launch = launches[index]!; + const companion = companions.get(lowerAddress(launch.token)); + const launchTransaction = launchTransactions.get(lowerAddress(launch.token)); + if (!companion || !launchTransaction) { + fail("classic-v3-companion-missing"); + } + const offset = index * CALLS_PER_LAUNCH; + const name = exactText(initialValues[offset], "classic-v3-token-name"); + const symbol = exactText(initialValues[offset + 1], "classic-v3-token-symbol"); + const decimals = safeInteger(initialValues[offset + 2], 0, 255, "classic-v3-token-decimals"); + const totalSupply = nonnegative(initialValues[offset + 3], "classic-v3-total-supply"); + const recordedCreator = exactAddress(initialValues[offset + 4], "classic-v3-token-creator"); + const metadata = tuple(initialValues[offset + 5], 4, "classic-v3-token-metadata"); + const slot0 = tuple(initialValues[offset + 6], 4, "classic-v3-slot0"); + nonnegative(initialValues[offset + 7], "classic-v3-active-liquidity"); + const disclosure = tuple(initialValues[offset + 8], 8, "classic-v3-fee-disclosure"); + const poolConfig = tuple(initialValues[offset + 9], 6, "classic-v3-pool-config"); + const predictedRewardVault = exactAddress(initialValues[offset + 10], "classic-v3-predicted-vault"); + const factoryVault = initialValues[offset + 11]; + const factoryConfigurationHash = exactBytes32(initialValues[offset + 12], "classic-v3-factory-configuration-hash"); + const vaultHook = exactAddress(initialValues[offset + 13], "classic-v3-vault-hook"); + const vaultPoolId = exactBytes32(initialValues[offset + 14], "classic-v3-vault-pool"); + const configurationHash = exactBytes32(initialValues[offset + 15], "classic-v3-vault-configuration-hash"); + const activeConfigurationHash = exactBytes32(initialValues[offset + 16], "classic-v3-active-configuration-hash"); + const configurationEpoch = nonnegative(initialValues[offset + 17], "classic-v3-configuration-epoch"); + const totalReceived = nonnegative(initialValues[offset + 19], "classic-v3-total-received"); + const totalClaimed = nonnegative(initialValues[offset + 20], "classic-v3-total-claimed"); + if ( + factoryVault !== true || + !sameHex(predictedRewardVault, launch.rewardVault) || + !sameHex(factoryConfigurationHash, launch.rewardConfigurationHash) || + !sameHex(recordedCreator, release.launcher) || + !sameHex(vaultHook, release.hook) || + !sameHex(vaultPoolId, launch.poolId) || + !sameHex(configurationHash, launch.rewardConfigurationHash) || + configurationEpoch < 1n + ) { + fail("classic-v3-current-provenance"); + } + const [sqrtPriceValue, tickValue, protocolFeeValue, lpFeeValue] = slot0; + nonnegative(sqrtPriceValue, "classic-v3-current-price"); + safeInteger(tickValue, -887_272, 887_272, "classic-v3-current-tick"); + safeInteger(protocolFeeValue, 0, 1_000_000, "classic-v3-protocol-fee"); + const currentLpFeePips = safeInteger(lpFeeValue, 0, 1_000_000, "classic-v3-current-lp-fee"); + const [ + buySwapFee, + sellSwapFee, + buyCreatorFee, + sellCreatorFee, + launcherFee, + transferTax, + disclosedLpFee, + disclosedVault, + ] = disclosure; + const [configuredVault, registrar, configuredBuy, configuredSell, registered, pendingCreatorFees] = poolConfig; + const buySwapFeeBps = safeInteger(buySwapFee, 100, 1_000, "classic-v3-disclosed-buy-fee"); + const sellSwapFeeBps = safeInteger(sellSwapFee, 100, 1_000, "classic-v3-disclosed-sell-fee"); + const buyCreatorFeeBps = safeInteger(buyCreatorFee, 0, 1_000, "classic-v3-disclosed-buy-creator-fee"); + const sellCreatorFeeBps = safeInteger(sellCreatorFee, 0, 1_000, "classic-v3-disclosed-sell-creator-fee"); + const launcherFeeBps = safeInteger(launcherFee, 0, 1_000, "classic-v3-disclosed-launcher-fee"); + const transferTaxBps = safeInteger(transferTax, 0, 10_000, "classic-v3-transfer-tax"); + const lpFeePips = safeInteger(disclosedLpFee, 0, 1_000_000, "classic-v3-disclosed-lp-fee"); + if ( + buySwapFeeBps !== launch.buySwapFeeBps || + sellSwapFeeBps !== launch.sellSwapFeeBps || + buyCreatorFeeBps + launcherFeeBps !== buySwapFeeBps || + sellCreatorFeeBps + launcherFeeBps !== sellSwapFeeBps || + launcherFeeBps !== 10 || + transferTaxBps !== 0 || + lpFeePips !== 0 || + currentLpFeePips !== 0 || + !sameHex(exactAddress(disclosedVault, "classic-v3-disclosed-vault"), launch.rewardVault) || + !sameHex(exactAddress(configuredVault, "classic-v3-configured-vault"), launch.rewardVault) || + !sameHex(exactAddress(registrar, "classic-v3-registrar"), release.launcher) || + registered !== true || + safeInteger(configuredBuy, 100, 1_000, "classic-v3-configured-buy-fee") !== buySwapFeeBps || + safeInteger(configuredSell, 100, 1_000, "classic-v3-configured-sell-fee") !== sellSwapFeeBps + ) { + fail("classic-v3-current-state-mismatch"); + } + const registrationArgs = companion.registration.args; + const eventDisclosureArgs = companion.disclosure.args; + if ( + safeInteger(registrationArgs.buySwapFeeBps, 100, 1_000, "classic-v3-registration-buy-fee") !== buySwapFeeBps || + safeInteger(registrationArgs.sellSwapFeeBps, 100, 1_000, "classic-v3-registration-sell-fee") !== sellSwapFeeBps || + safeInteger(eventDisclosureArgs.buySwapFeeBps, 100, 1_000, "classic-v3-event-buy-fee") !== buySwapFeeBps || + safeInteger(eventDisclosureArgs.sellSwapFeeBps, 100, 1_000, "classic-v3-event-sell-fee") !== sellSwapFeeBps || + safeInteger(eventDisclosureArgs.buyCreatorFeeBps, 0, 1_000, "classic-v3-event-buy-creator-fee") !== buyCreatorFeeBps || + safeInteger(eventDisclosureArgs.sellCreatorFeeBps, 0, 1_000, "classic-v3-event-sell-creator-fee") !== sellCreatorFeeBps || + safeInteger(eventDisclosureArgs.launcherFeeBps, 0, 1_000, "classic-v3-event-launcher-fee") !== launcherFeeBps || + safeInteger(eventDisclosureArgs.transferTaxBps, 0, 10_000, "classic-v3-event-transfer-tax") !== transferTaxBps || + safeInteger(eventDisclosureArgs.lpFeePips, 0, 1_000_000, "classic-v3-event-lp-fee") !== lpFeePips + ) { + fail("classic-v3-registration-disclosure-mismatch"); + } + const [descriptionValue, websiteValue, imageValue, extraDataValue] = metadata; + const description = exactText(descriptionValue, "classic-v3-description"); + const website = exactText(websiteValue, "classic-v3-website"); + const image = exactText(imageValue, "classic-v3-image"); + const extraData = exactData(extraDataValue, "classic-v3-extra-data"); + const feeTotals = grossFeeTotals( + hookLogs, + poolSwapLogs, + launch.poolId, + buySwapFeeBps, + sellSwapFeeBps, + ); + const chart = swapPoints(poolSwapLogs, launch.poolId); + if (chart.swapCount < 1 || chart.swapCount < feeTotals.count) { + fail("classic-v3-swap-fee-event-coverage"); + } + const liquidityArgs = companion.liquidity.args; + const initialBuyArgs = companion.initialBuy.args; + const custodyArgs = companion.custody.args; + const initialBuyNative = nonnegative( + initialBuyArgs.nativeAmount, + "classic-v3-initial-buy-native", + ); + const custodyAddress = exactAddress( + custodyArgs.custody, + "classic-v3-custody-address", + ); + const custodyMode = safeInteger( + custodyArgs.mode, + 0, + 3, + "classic-v3-custody-mode", + ); + const custodyDurationDays = safeInteger( + custodyArgs.durationDays, + 0, + 65_535, + "classic-v3-custody-duration", + ); + const custodyCliffDays = safeInteger( + custodyArgs.cliffDays, + 0, + 65_535, + "classic-v3-custody-cliff", + ); + if ( + launchTransaction.value !== initialBuyNative || + launchTransaction.name !== name || + launchTransaction.symbol !== symbol || + launchTransaction.description !== description || + launchTransaction.website !== website || + launchTransaction.image !== image || + !sameHex(launchTransaction.extraData, extraData) || + launchTransaction.custodyMode !== custodyMode || + launchTransaction.custodyDurationDays !== custodyDurationDays || + launchTransaction.custodyCliffDays !== custodyCliffDays || + (custodyMode === 0 && !sameHex(custodyAddress, ZERO_ADDRESS)) || + (custodyMode !== 0 && sameHex(custodyAddress, ZERO_ADDRESS)) + ) { + fail("classic-v3-launch-input-state-mismatch"); + } + const liquidityTotalSupply = nonnegative(liquidityArgs.totalSupply, "classic-v3-liquidity-supply"); + const tokenLiquidity = nonnegative(liquidityArgs.tokenLiquidityAmount, "classic-v3-token-liquidity"); + const lockedDust = nonnegative(liquidityArgs.lockedTokenDust, "classic-v3-locked-dust"); + if (liquidityTotalSupply !== totalSupply || tokenLiquidity + lockedDust > totalSupply) { + fail("classic-v3-liquidity-conservation"); + } + const initialTick = safeInteger(liquidityArgs.initialTick, -887_272, 887_272, "classic-v3-initial-tick"); + const tickLower = safeInteger(liquidityArgs.tickLower, -887_272, 887_272, "classic-v3-tick-lower"); + const tickUpper = safeInteger(liquidityArgs.tickUpper, -887_272, 887_272, "classic-v3-tick-upper"); + const eventLpFee = safeInteger(liquidityArgs.lpFeePips, 0, 1_000_000, "classic-v3-event-lp-fee"); + if (eventLpFee !== lpFeePips || tickLower >= initialTick || initialTick > tickUpper) { + fail("classic-v3-liquidity-shape"); + } + const timestamp = timestamps.get(launch.blockNumber.toString()); + if (timestamp === undefined) fail("classic-v3-launch-timestamp-missing"); + + const count = beneficiaryCounts[index]!; + const allocations: Json[] = []; + const currentBeneficiaries: Address[] = []; + const currentShares: number[] = []; + let shareTotal = 0; + for (let allocationIndex = 0; allocationIndex < count; allocationIndex += 1) { + const beneficiary = exactAddress( + beneficiaryBaseValues[beneficiaryValueCursor], + "classic-v3-beneficiary", + ); + const shareBps = safeInteger( + beneficiaryBaseValues[beneficiaryValueCursor + 1], + 1, + 10_000, + "classic-v3-beneficiary-share", + ); + beneficiaryValueCursor += 2; + shareTotal += shareBps; + currentBeneficiaries.push(beneficiary); + currentShares.push(shareBps); + allocations.push({ + allocationIndex, + payoutAddress: lowerAddress(beneficiary), + shareBps, + }); + } + const entitlements: Json[] = []; + let claimableTotal = 0n; + let claimedEntitlementTotal = 0n; + for (const account of entitlementAccountsByLaunch[index]!) { + const claimable = nonnegative( + balanceValues[balanceValueCursor], + "classic-v3-claimable", + ); + const claimed = nonnegative( + balanceValues[balanceValueCursor + 1], + "classic-v3-claimed", + ); + balanceValueCursor += 2; + claimableTotal += claimable; + claimedEntitlementTotal += claimed; + entitlements.push({ + account: lowerAddress(account), + claimableWei: claimable.toString(), + claimedWei: claimed.toString(), + }); + } + const pendingCreatorFeeTotal = nonnegative( + pendingCreatorFees, + "classic-v3-pending-creator-fees", + ); + if ( + shareTotal !== 10_000 || + totalClaimed !== claimedEntitlementTotal || + totalReceived !== totalClaimed + claimableTotal || + feeTotals.creator !== totalReceived + pendingCreatorFeeTotal + ) { + fail("classic-v3-reward-conservation"); + } + const expectedActiveConfigurationHash = keccak256(encodeAbiParameters( + ACTIVE_REWARD_CONFIGURATION_PARAMETERS, + [ + 1n, + launch.rewardVault, + configurationHash, + configurationEpoch, + currentBeneficiaries, + currentShares, + ], + )); + if (!sameHex(activeConfigurationHash, expectedActiveConfigurationHash)) { + fail("classic-v3-active-configuration-hash"); + } + if ( + configurationEpoch === 1n && + ( + launchTransaction.rewardBeneficiaries.length !== currentBeneficiaries.length || + launchTransaction.rewardBeneficiaries.some((beneficiary, allocationIndex) => + !sameHex(beneficiary, currentBeneficiaries[allocationIndex]!) + ) || + launchTransaction.rewardSharesBps.some((share, allocationIndex) => + share !== currentShares[allocationIndex] + ) + ) + ) { + fail("classic-v3-initial-reward-state-mismatch"); + } + const rewardEvents = vaultEventInputs( + vaultLogsByAddress.get(lowerAddress(launch.rewardVault)) ?? [], + launch.poolId, + ); + const tokenJson: Json = { + releaseVersion: "classic-v3", + modelId: "classic", + tokenAddress: lowerAddress(launch.token), + creatorAddress: lowerAddress(launch.deployer), + launchTransactionHash: launch.transactionHash, + launchBlockNumber: launch.blockNumber.toString(), + launchTransactionIndex: launch.transactionIndex, + launchLogIndex: launchTransaction.receiptLogIndex, + launchedAt: isoTimestamp(timestamp), + poolId: launch.poolId, + hookAddress: lowerAddress(launch.hook), + rewardVaultAddress: lowerAddress(launch.rewardVault), + positionRecipient: lowerAddress(launch.positionRecipient), + positionTokenId: launch.positionTokenId.toString(), + launchHash: launch.launchHash, + name, + symbol, + decimals, + totalSupplyRaw: totalSupply.toString(), + quoteAssetAddress: lowerAddress(ZERO_ADDRESS), + fees: { + buySwapFeeBps, + sellSwapFeeBps, + buyCreatorFeeBps, + sellCreatorFeeBps, + launcherFeeBps, + transferTaxBps, + lpFeePips, + }, + liquidity: { + tokenLiquidityAmountRaw: tokenLiquidity.toString(), + lockedTokenDustRaw: lockedDust.toString(), + initialTick, + tickLower, + tickUpper, + }, + }; + nonnegative(initialBuyArgs.tokenAmount, "classic-v3-initial-buy-token"); + exactBytes32(custodyArgs.configurationHash, "classic-v3-custody-hash"); + tokens.push(tokenJson); + const lastSwap = chart.last; + if (!lastSwap) fail("classic-v3-latest-swap-missing"); + charts.push({ + releaseVersion: "classic-v3", + modelId: "classic", + tokenAddress: lowerAddress(launch.token), + poolId: launch.poolId, + quoteAssetAddress: lowerAddress(ZERO_ADDRESS), + state: { + blockNumber: lastSwap.log.blockNumber.toString(), + blockHash: lastSwap.log.blockHash, + transactionHash: lastSwap.log.transactionHash, + transactionIndex: lastSwap.log.transactionIndex, + logIndex: lastSwap.log.logIndex, + sqrtPriceX96: nonnegative( + lastSwap.args.sqrtPriceX96, + "classic-v3-latest-swap-price", + ).toString(), + liquidity: nonnegative( + lastSwap.args.liquidity, + "classic-v3-latest-swap-liquidity", + ).toString(), + tick: safeInteger( + lastSwap.args.tick, + -887_272, + 887_272, + "classic-v3-latest-swap-tick", + ), + lpFeePips, + }, + volume: { + quoteAssetAddress: lowerAddress(ZERO_ADDRESS), + grossQuoteRaw: feeTotals.gross.toString(), + creatorFeeQuoteRaw: feeTotals.creator.toString(), + launcherFeeQuoteRaw: feeTotals.launcher.toString(), + }, + }); + rewards.push({ + releaseVersion: "classic-v3", + modelId: "classic", + vaultAddress: lowerAddress(launch.rewardVault), + poolId: launch.poolId, + tokenAddress: lowerAddress(launch.token), + tokenName: name, + tokenSymbol: symbol, + launchTransactionHash: launch.transactionHash, + buySwapFeeBps, + sellSwapFeeBps, + launcherFeeBps, + configurationHash, + activeConfigurationHash, + configurationEpoch: configurationEpoch.toString(), + totalCreatorFeesReceivedWei: totalReceived.toString(), + totalCreatorFeesClaimedWei: totalClaimed.toString(), + pendingCreatorFeesWei: pendingCreatorFeeTotal.toString(), + allocations, + entitlements, + events: [...rewardEvents], + }); + } + + return Object.freeze({ + tokens: Object.freeze(tokens), + charts: Object.freeze(charts), + rewards: Object.freeze(rewards), + }); +} + +/** + * Release-specific live source for the only currently proven route family. + * Other releases deliberately remain unconfigured until their exact DTO + * corpus and lifecycle evidence are complete. + */ +export const buildClassicV3ExactBlockContribution = buildContribution; + +export const buildClassicV3ExactBlockRoutes: ExactBlockRouteBuilder = + async (input) => assembleReconcilerRoutesFromContributions([ + await buildContribution(input), + ]); diff --git a/lib/data-pipeline/classic-v3-reconciler-route-contract.ts b/lib/data-pipeline/classic-v3-reconciler-route-contract.ts new file mode 100644 index 00000000..a8a91f6c --- /dev/null +++ b/lib/data-pipeline/classic-v3-reconciler-route-contract.ts @@ -0,0 +1,951 @@ +import type { CanonicalJsonValue } from "./canonical-fingerprint"; +import { validationError } from "./errors"; +import { + RECONCILER_ROUTE_KEYS, + type ReconcilerRouteDto, + type ReconcilerRouteKey, +} from "./reconciler-preparity"; + +export const RECONCILER_ROUTE_CONTRACT = + "programmable-route-corpus-v1" as const; +export const CLASSIC_V3_RECONCILER_ROUTE_CONTRACT = + RECONCILER_ROUTE_CONTRACT; + +export const CLASSIC_V3_RECONCILER_REWARD_FIELDS = Object.freeze([ + "releaseVersion", + "modelId", + "vaultAddress", + "poolId", + "tokenAddress", + "tokenName", + "tokenSymbol", + "launchTransactionHash", + "buySwapFeeBps", + "sellSwapFeeBps", + "launcherFeeBps", + "configurationHash", + "activeConfigurationHash", + "configurationEpoch", + "totalCreatorFeesReceivedWei", + "totalCreatorFeesClaimedWei", + "pendingCreatorFeesWei", + "allocations", + "entitlements", + "events", +] as const); + +export const CLASSIC_V3_RECONCILER_REWARD_ALLOCATION_FIELDS = Object.freeze([ + "allocationIndex", + "payoutAddress", + "shareBps", +] as const); + +export const CLASSIC_V3_RECONCILER_REWARD_ENTITLEMENT_FIELDS = Object.freeze([ + "account", + "claimableWei", + "claimedWei", +] as const); + +const RELEASE_MODELS = Object.freeze({ + "classic-v2": "classic", + "classic-v3": "classic", + "stock-paired-v1": "stock-paired", + "stock-paired-v2": "stock-paired", + "stock-paired-v3": "stock-paired", +} as const); + +type JsonRecord = Record; + +export type ClassicV3ReconcilerRouteParts = Readonly<{ + tokens: readonly CanonicalJsonValue[]; + charts: readonly CanonicalJsonValue[]; + profiles: readonly CanonicalJsonValue[]; + rewards: readonly CanonicalJsonValue[]; + launches: readonly CanonicalJsonValue[]; +}>; + +export type ReconcilerRouteContribution = Readonly<{ + tokens: readonly CanonicalJsonValue[]; + charts: readonly CanonicalJsonValue[]; + rewards?: readonly CanonicalJsonValue[]; +}>; + +function fail(operation: string): never { + throw validationError("postgres", operation); +} + +function object(value: CanonicalJsonValue, operation: string): JsonRecord { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + fail(operation); + } + return value as JsonRecord; +} + +function exactKeys( + value: JsonRecord, + keys: readonly string[], + operation: string, +): void { + if (Object.keys(value).sort().join("\0") !== [...keys].sort().join("\0")) { + fail(operation); + } +} + +function text(value: CanonicalJsonValue, operation: string): string { + if (typeof value !== "string") fail(operation); + return value; +} + +function integer( + value: CanonicalJsonValue, + minimum: number, + maximum: number, + operation: string, +): number { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < minimum || + value > maximum + ) { + fail(operation); + } + return value; +} + +function integerText(value: CanonicalJsonValue, operation: string): string { + const parsed = text(value, operation); + if (!/^(?:0|[1-9][0-9]{0,77})$/u.test(parsed)) fail(operation); + return parsed; +} + +function timestamp(value: CanonicalJsonValue, operation: string): string { + const parsed = text(value, operation); + if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(parsed)) { + fail(operation); + } + return parsed; +} + +function hex( + value: CanonicalJsonValue, + bytes: number, + operation: string, +): string { + const parsed = text(value, operation); + if (!new RegExp(`^0x[0-9a-f]{${bytes * 2}}$`, "u").test(parsed)) { + fail(operation); + } + return parsed; +} + +function array( + value: CanonicalJsonValue, + operation: string, +): readonly CanonicalJsonValue[] { + if (!Array.isArray(value)) fail(operation); + return value; +} + +function releaseIdentity(row: JsonRecord, operation: string): void { + const releaseVersion = text(row.releaseVersion, `${operation}-release`); + const modelId = text(row.modelId, `${operation}-model`); + if ( + !(releaseVersion in RELEASE_MODELS) || + RELEASE_MODELS[releaseVersion as keyof typeof RELEASE_MODELS] !== modelId + ) { + fail(operation); + } +} + +function releaseVersion(row: JsonRecord): keyof typeof RELEASE_MODELS { + return row.releaseVersion as keyof typeof RELEASE_MODELS; +} + +function token(value: CanonicalJsonValue): JsonRecord { + const row = object(value, "reconciler-route-token"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "tokenAddress", + "creatorAddress", + "launchTransactionHash", + "launchBlockNumber", + "launchTransactionIndex", + "launchLogIndex", + "launchedAt", + "poolId", + "hookAddress", + "rewardVaultAddress", + "positionRecipient", + "positionTokenId", + "launchHash", + "name", + "symbol", + "decimals", + "totalSupplyRaw", + "quoteAssetAddress", + "fees", + "liquidity", + ], "reconciler-route-token-fields"); + releaseIdentity(row, "reconciler-route-token-release"); + hex(row.tokenAddress, 20, "reconciler-route-token-address"); + hex(row.creatorAddress, 20, "reconciler-route-token-creator"); + hex(row.launchTransactionHash, 32, "reconciler-route-token-transaction"); + integerText(row.launchBlockNumber, "reconciler-route-token-block"); + integer(row.launchTransactionIndex, 0, Number.MAX_SAFE_INTEGER, + "reconciler-route-token-transaction-index"); + integer(row.launchLogIndex, 0, Number.MAX_SAFE_INTEGER, + "reconciler-route-token-log-index"); + timestamp(row.launchedAt, "reconciler-route-token-timestamp"); + hex(row.poolId, 32, "reconciler-route-token-pool"); + hex(row.hookAddress, 20, "reconciler-route-token-hook"); + if (releaseVersion(row) === "classic-v2") { + if (row.rewardVaultAddress !== null) { + fail("reconciler-route-token-vault"); + } + } else { + hex(row.rewardVaultAddress, 20, "reconciler-route-token-vault"); + } + hex(row.positionRecipient, 20, "reconciler-route-token-recipient"); + integerText(row.positionTokenId, "reconciler-route-token-position"); + hex(row.launchHash, 32, "reconciler-route-token-launch-hash"); + text(row.name, "reconciler-route-token-name"); + text(row.symbol, "reconciler-route-token-symbol"); + integer(row.decimals, 0, 255, "reconciler-route-token-decimals"); + integerText(row.totalSupplyRaw, "reconciler-route-token-supply"); + const quoteAssetAddress = hex( + row.quoteAssetAddress, + 20, + "reconciler-route-token-quote-asset", + ); + const isNativeQuote = quoteAssetAddress === `0x${"00".repeat(20)}`; + if ((row.modelId === "classic") !== isNativeQuote) { + fail("reconciler-route-token-quote-model"); + } + + const fees = object(row.fees, "reconciler-route-token-fees"); + exactKeys(fees, [ + "buySwapFeeBps", + "sellSwapFeeBps", + "buyCreatorFeeBps", + "sellCreatorFeeBps", + "launcherFeeBps", + "transferTaxBps", + "lpFeePips", + ], "reconciler-route-token-fee-fields"); + integer(fees.buySwapFeeBps, 0, 10_000, "reconciler-route-token-buy-fee"); + integer(fees.sellSwapFeeBps, 0, 10_000, "reconciler-route-token-sell-fee"); + integer(fees.buyCreatorFeeBps, 0, 10_000, + "reconciler-route-token-buy-creator-fee"); + integer(fees.sellCreatorFeeBps, 0, 10_000, + "reconciler-route-token-sell-creator-fee"); + integer(fees.launcherFeeBps, 0, 10_000, + "reconciler-route-token-launcher-fee"); + integer(fees.transferTaxBps, 0, 10_000, + "reconciler-route-token-transfer-tax"); + integer(fees.lpFeePips, 0, 1_000_000, + "reconciler-route-token-lp-fee"); + + const liquidity = object(row.liquidity, "reconciler-route-token-liquidity"); + exactKeys(liquidity, [ + "tokenLiquidityAmountRaw", + "lockedTokenDustRaw", + "initialTick", + "tickLower", + "tickUpper", + ], "reconciler-route-token-liquidity-fields"); + integerText(liquidity.tokenLiquidityAmountRaw, + "reconciler-route-token-liquidity-amount"); + integerText(liquidity.lockedTokenDustRaw, + "reconciler-route-token-locked-dust"); + integer(liquidity.initialTick, -887_272, 887_272, + "reconciler-route-token-initial-tick"); + integer(liquidity.tickLower, -887_272, 887_272, + "reconciler-route-token-lower-tick"); + integer(liquidity.tickUpper, -887_272, 887_272, + "reconciler-route-token-upper-tick"); + return row; +} + +function chart(value: CanonicalJsonValue): JsonRecord { + const row = object(value, "reconciler-route-chart"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "tokenAddress", + "poolId", + "quoteAssetAddress", + "state", + "volume", + ], + "reconciler-route-chart-fields"); + releaseIdentity(row, "reconciler-route-chart-release"); + hex(row.tokenAddress, 20, "reconciler-route-chart-token"); + hex(row.poolId, 32, "reconciler-route-chart-pool"); + const quoteAssetAddress = hex( + row.quoteAssetAddress, + 20, + "reconciler-route-chart-quote-asset", + ); + const isNativeQuote = quoteAssetAddress === `0x${"00".repeat(20)}`; + if ((row.modelId === "classic") !== isNativeQuote) { + fail("reconciler-route-chart-quote-model"); + } + const state = object(row.state, "reconciler-route-chart-state"); + exactKeys(state, [ + "blockNumber", + "blockHash", + "transactionHash", + "transactionIndex", + "logIndex", + "sqrtPriceX96", + "liquidity", + "tick", + "lpFeePips", + ], "reconciler-route-chart-state-fields"); + integerText(state.blockNumber, "reconciler-route-chart-block"); + hex(state.blockHash, 32, "reconciler-route-chart-block-hash"); + hex(state.transactionHash, 32, "reconciler-route-chart-transaction"); + integer(state.transactionIndex, 0, Number.MAX_SAFE_INTEGER, + "reconciler-route-chart-transaction-index"); + integer(state.logIndex, 0, Number.MAX_SAFE_INTEGER, + "reconciler-route-chart-log-index"); + integerText(state.sqrtPriceX96, "reconciler-route-chart-price"); + integerText(state.liquidity, "reconciler-route-chart-liquidity"); + integer(state.tick, -887_272, 887_272, "reconciler-route-chart-tick"); + integer(state.lpFeePips, 0, 1_000_000, "reconciler-route-chart-lp-fee"); + const volume = object(row.volume, "reconciler-route-chart-volume"); + exactKeys(volume, [ + "quoteAssetAddress", + "grossQuoteRaw", + "creatorFeeQuoteRaw", + "launcherFeeQuoteRaw", + ], + "reconciler-route-chart-volume-fields"); + hex(volume.quoteAssetAddress, 20, + "reconciler-route-chart-volume-quote-asset"); + if (volume.quoteAssetAddress !== quoteAssetAddress) { + fail("reconciler-route-chart-volume-quote-asset-mismatch"); + } + integerText(volume.grossQuoteRaw, "reconciler-route-chart-gross"); + integerText(volume.creatorFeeQuoteRaw, + "reconciler-route-chart-creator-fee"); + integerText(volume.launcherFeeQuoteRaw, + "reconciler-route-chart-launcher-fee"); + return row; +} + +function tokenReference(value: CanonicalJsonValue): JsonRecord { + const row = object(value, "reconciler-route-token-reference"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "tokenAddress", + "launchTransactionHash", + ], + "reconciler-route-token-reference-fields"); + releaseIdentity(row, "reconciler-route-token-reference-release"); + hex(row.tokenAddress, 20, "reconciler-route-token-reference-address"); + hex(row.launchTransactionHash, 32, + "reconciler-route-token-reference-transaction"); + return row; +} + +function profile(value: CanonicalJsonValue): JsonRecord { + const row = object(value, "reconciler-route-profile"); + exactKeys(row, ["account", "tokens"], "reconciler-route-profile-fields"); + hex(row.account, 20, "reconciler-route-profile-account"); + array(row.tokens, "reconciler-route-profile-tokens").forEach(tokenReference); + return row; +} + +function reward(value: CanonicalJsonValue): JsonRecord { + const row = object(value, "reconciler-route-reward"); + exactKeys( + row, + CLASSIC_V3_RECONCILER_REWARD_FIELDS, + "reconciler-route-reward-fields", + ); + releaseIdentity(row, "reconciler-route-reward-release"); + if (row.releaseVersion !== "classic-v3" || row.modelId !== "classic") { + fail("reconciler-route-reward-release"); + } + hex(row.vaultAddress, 20, "reconciler-route-reward-vault"); + hex(row.poolId, 32, "reconciler-route-reward-pool"); + hex(row.tokenAddress, 20, "reconciler-route-reward-token"); + text(row.tokenName, "reconciler-route-reward-name"); + text(row.tokenSymbol, "reconciler-route-reward-symbol"); + hex(row.launchTransactionHash, 32, + "reconciler-route-reward-transaction"); + integer(row.buySwapFeeBps, 0, 10_000, + "reconciler-route-reward-buy-fee"); + integer(row.sellSwapFeeBps, 0, 10_000, + "reconciler-route-reward-sell-fee"); + integer(row.launcherFeeBps, 0, 10_000, + "reconciler-route-reward-launcher-fee"); + hex(row.configurationHash, 32, + "reconciler-route-reward-configuration-hash"); + hex(row.activeConfigurationHash, 32, + "reconciler-route-reward-active-configuration-hash"); + integerText(row.configurationEpoch, + "reconciler-route-reward-configuration-epoch"); + integerText(row.totalCreatorFeesReceivedWei, + "reconciler-route-reward-total-received"); + integerText(row.totalCreatorFeesClaimedWei, + "reconciler-route-reward-total-claimed"); + integerText(row.pendingCreatorFeesWei, + "reconciler-route-reward-pending"); + const allocations = array(row.allocations, + "reconciler-route-reward-allocations"); + if (allocations.length < 1 || allocations.length > 5) { + fail("reconciler-route-reward-allocation-count"); + } + let shareTotal = 0; + allocations.forEach((allocation, index) => { + const item = object(allocation, "reconciler-route-reward-allocation"); + exactKeys( + item, + CLASSIC_V3_RECONCILER_REWARD_ALLOCATION_FIELDS, + "reconciler-route-reward-allocation-fields", + ); + if (integer(item.allocationIndex, 0, 4, + "reconciler-route-reward-allocation-index") !== index) { + fail("reconciler-route-reward-allocation-order"); + } + hex(item.payoutAddress, 20, + "reconciler-route-reward-payout-address"); + shareTotal += integer(item.shareBps, 1, 10_000, + "reconciler-route-reward-share"); + }); + if (shareTotal !== 10_000) fail("reconciler-route-reward-share-total"); + const entitlementAccounts = new Set(); + const entitlements = array(row.entitlements, + "reconciler-route-reward-entitlements"); + if (entitlements.length < 1) { + fail("reconciler-route-reward-entitlement-count"); + } + let previousAccount: string | undefined; + let claimableTotal = 0n; + let claimedTotal = 0n; + entitlements.forEach((entitlement) => { + const item = object(entitlement, "reconciler-route-reward-entitlement"); + exactKeys( + item, + CLASSIC_V3_RECONCILER_REWARD_ENTITLEMENT_FIELDS, + "reconciler-route-reward-entitlement-fields", + ); + const account = hex( + item.account, + 20, + "reconciler-route-reward-entitlement-account", + ); + if ( + entitlementAccounts.has(account) || + (previousAccount !== undefined && account.localeCompare(previousAccount) <= 0) + ) { + fail("reconciler-route-reward-entitlement-order"); + } + entitlementAccounts.add(account); + previousAccount = account; + claimableTotal += BigInt(integerText( + item.claimableWei, + "reconciler-route-reward-entitlement-claimable", + )); + claimedTotal += BigInt(integerText( + item.claimedWei, + "reconciler-route-reward-entitlement-claimed", + )); + }); + const totalReceived = BigInt(row.totalCreatorFeesReceivedWei as string); + const totalClaimed = BigInt(row.totalCreatorFeesClaimedWei as string); + if ( + totalClaimed !== claimedTotal || + totalReceived !== totalClaimed + claimableTotal + ) { + fail("reconciler-route-reward-entitlement-conservation"); + } + array(row.events, "reconciler-route-reward-events"); + return row; +} + +function lookup(value: CanonicalJsonValue): JsonRecord { + const row = object(value, "reconciler-route-lookup"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "account", + "launchTransactionHash", + "tokenAddress", + ], + "reconciler-route-lookup-fields"); + releaseIdentity(row, "reconciler-route-lookup-release"); + hex(row.account, 20, "reconciler-route-lookup-account"); + hex(row.launchTransactionHash, 32, + "reconciler-route-lookup-transaction"); + hex(row.tokenAddress, 20, "reconciler-route-lookup-token"); + return row; +} + +function tokenIdentity(row: JsonRecord): string { + return `${row.releaseVersion}:${row.tokenAddress}`; +} + +function compareTokenRows(left: JsonRecord, right: JsonRecord): number { + const leftBlock = BigInt(left.launchBlockNumber as string); + const rightBlock = BigInt(right.launchBlockNumber as string); + if (leftBlock !== rightBlock) return leftBlock < rightBlock ? -1 : 1; + const numberFields = ["launchTransactionIndex", "launchLogIndex"] as const; + for (const field of numberFields) { + const difference = (left[field] as number) - (right[field] as number); + if (difference !== 0) return difference; + } + return (left.launchTransactionHash as string).localeCompare( + right.launchTransactionHash as string, + ) || (left.tokenAddress as string).localeCompare( + right.tokenAddress as string, + ) || (left.releaseVersion as string).localeCompare( + right.releaseVersion as string, + ); +} + +export function assembleReconcilerRoutesFromContributions( + contributions: readonly ReconcilerRouteContribution[], +): readonly ReconcilerRouteDto[] { + if (!Array.isArray(contributions) || contributions.length < 1) { + fail("reconciler-route-contributions"); + } + const tokens = contributions.flatMap((contribution) => + [...contribution.tokens] + ); + const charts = contributions.flatMap((contribution) => + [...contribution.charts] + ); + const rewards = contributions.flatMap((contribution) => + [...(contribution.rewards ?? [])] + ); + if (tokens.length < 1 || charts.length !== tokens.length) { + fail("reconciler-route-contribution-cardinality"); + } + tokens.forEach(token); + charts.forEach(chart); + rewards.forEach(reward); + const chartByIdentity = new Map(); + for (const entry of charts) { + const row = object(entry, "reconciler-route-contribution-chart"); + const identity = tokenIdentity(row); + if (chartByIdentity.has(identity)) { + fail("reconciler-route-contribution-chart-identity"); + } + chartByIdentity.set(identity, entry); + } + const orderedTokens = [...tokens].sort((left, right) => + compareTokenRows( + object(left, "reconciler-route-contribution-token-order"), + object(right, "reconciler-route-contribution-token-order"), + ) + ); + const orderedCharts = orderedTokens.map((entry) => { + const row = object(entry, "reconciler-route-contribution-token-chart"); + const resolved = chartByIdentity.get(tokenIdentity(row)); + if (!resolved) fail("reconciler-route-contribution-chart-coverage"); + return resolved; + }); + const profileByAccount = new Map(); + for (const entry of orderedTokens) { + const row = object(entry, "reconciler-route-contribution-profile-token"); + const account = row.creatorAddress as string; + const values = profileByAccount.get(account) ?? []; + values.push({ + releaseVersion: row.releaseVersion!, + modelId: row.modelId!, + tokenAddress: row.tokenAddress!, + launchTransactionHash: row.launchTransactionHash!, + }); + profileByAccount.set(account, values); + } + const profiles = [...profileByAccount.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([account, profileTokens]) => ({ + account, + tokens: profileTokens, + })); + const orderedRewards = [...rewards].sort((left, right) => + (object(left, "reconciler-route-contribution-reward").vaultAddress as string) + .localeCompare( + object(right, "reconciler-route-contribution-reward").vaultAddress as string, + ) + ); + const launches = orderedTokens + .map((entry) => object(entry, "reconciler-route-contribution-lookup")) + .filter((row) => row.releaseVersion !== "classic-v2") + .map((row) => ({ + releaseVersion: row.releaseVersion!, + modelId: row.modelId!, + account: row.creatorAddress!, + launchTransactionHash: row.launchTransactionHash!, + tokenAddress: row.tokenAddress!, + })) + .sort((left, right) => + (left.account as string).localeCompare(right.account as string) || + (left.launchTransactionHash as string).localeCompare( + right.launchTransactionHash as string, + ) || + (left.tokenAddress as string).localeCompare(right.tokenAddress as string) + ); + const classicV3Count = orderedTokens.filter((entry) => + object(entry, "reconciler-route-contribution-release").releaseVersion === + "classic-v3" + ).length; + if ( + orderedRewards.length !== classicV3Count || + launches.length !== orderedTokens.length - + orderedTokens.filter((entry) => + object(entry, "reconciler-route-contribution-classic-v2") + .releaseVersion === "classic-v2" + ).length + ) { + fail("reconciler-route-contribution-release-cardinality"); + } + const routeKeys: ReconcilerRouteKey[] = [ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + ]; + if (classicV3Count > 0) routeKeys.push("classic-v3-profile"); + if (launches.length > 0) routeKeys.push("launch-lookup"); + return assertReconcilerRouteSetForKeys( + assembleReconcilerRouteParts({ + tokens: orderedTokens, + charts: orderedCharts, + profiles, + rewards: orderedRewards, + launches, + }, routeKeys), + routeKeys, + ); +} + +function assembleReconcilerRouteParts( + parts: ClassicV3ReconcilerRouteParts, + routeKeys: readonly ReconcilerRouteKey[], +): readonly ReconcilerRouteDto[] { + const count = parts.tokens.length; + if ( + count < 1 || + parts.charts.length !== count || + parts.rewards.length > count || + parts.launches.length > count + ) { + fail("reconciler-route-part-cardinality"); + } + const routes = new Map([ + ["explore-list", Object.freeze({ + routeKey: "explore-list", + comparedCount: count, + dto: { + contractVersion: RECONCILER_ROUTE_CONTRACT, + tokens: [...parts.tokens], + }, + })], + ["explore-token", Object.freeze({ + routeKey: "explore-token", + comparedCount: count, + dto: { + contractVersion: RECONCILER_ROUTE_CONTRACT, + tokens: [...parts.tokens], + }, + })], + ["explore-chart", Object.freeze({ + routeKey: "explore-chart", + comparedCount: count, + dto: { + contractVersion: RECONCILER_ROUTE_CONTRACT, + charts: [...parts.charts], + }, + })], + ["creator-profile", Object.freeze({ + routeKey: "creator-profile", + comparedCount: count, + dto: { + contractVersion: RECONCILER_ROUTE_CONTRACT, + profiles: [...parts.profiles], + }, + })], + ["classic-v3-profile", Object.freeze({ + routeKey: "classic-v3-profile", + comparedCount: parts.rewards.length, + dto: { + contractVersion: RECONCILER_ROUTE_CONTRACT, + rewards: [...parts.rewards], + }, + })], + ["launch-lookup", Object.freeze({ + routeKey: "launch-lookup", + comparedCount: parts.launches.length, + dto: { + contractVersion: RECONCILER_ROUTE_CONTRACT, + launches: [...parts.launches], + }, + })], + ]); + return Object.freeze(routeKeys.map((routeKey) => { + const route = routes.get(routeKey); + if (!route) fail("reconciler-route-part-key"); + return route; + })); +} + +export function assembleClassicV3ReconcilerRoutes( + parts: ClassicV3ReconcilerRouteParts, +): readonly ReconcilerRouteDto[] { + if (parts.rewards.length < 1 || parts.launches.length < 1) { + fail("reconciler-route-part-cardinality"); + } + return assembleReconcilerRouteParts(parts, RECONCILER_ROUTE_KEYS); +} + +export function assertReconcilerRouteSetForKeys( + routes: readonly ReconcilerRouteDto[], + expectedRouteKeys: readonly ReconcilerRouteKey[], +): readonly ReconcilerRouteDto[] { + const expectedSet = new Set(expectedRouteKeys); + const baseRouteKeys: readonly ReconcilerRouteKey[] = [ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + ]; + if ( + expectedRouteKeys.length < baseRouteKeys.length || + expectedRouteKeys.length > RECONCILER_ROUTE_KEYS.length || + expectedSet.size !== expectedRouteKeys.length || + baseRouteKeys.some((routeKey) => !expectedSet.has(routeKey)) || + expectedRouteKeys.some((routeKey, index) => + !RECONCILER_ROUTE_KEYS.includes(routeKey) || + (index > 0 && + RECONCILER_ROUTE_KEYS.indexOf(routeKey) <= + RECONCILER_ROUTE_KEYS.indexOf(expectedRouteKeys[index - 1]!)) + ) || + routes.length !== expectedRouteKeys.length || + routes.some((route, index) => + route.routeKey !== expectedRouteKeys[index] || + !Number.isSafeInteger(route.comparedCount) || + route.comparedCount < 1 + ) + ) { + fail("reconciler-route-set"); + } + const documents = new Map< + ReconcilerRouteKey, + readonly CanonicalJsonValue[] + >(); + for (const route of routes) { + const document = object(route.dto, "reconciler-route-document"); + const collectionKey = route.routeKey === "explore-chart" + ? "charts" + : route.routeKey === "creator-profile" + ? "profiles" + : route.routeKey === "classic-v3-profile" + ? "rewards" + : route.routeKey === "launch-lookup" + ? "launches" + : "tokens"; + exactKeys(document, ["contractVersion", collectionKey], + "reconciler-route-document-fields"); + if (document.contractVersion !== RECONCILER_ROUTE_CONTRACT) { + fail("reconciler-route-contract-version"); + } + const collection = array(document[collectionKey], + "reconciler-route-collection"); + if ( + route.routeKey !== "creator-profile" && + collection.length !== route.comparedCount + ) { + fail("reconciler-route-collection-cardinality"); + } + if (route.routeKey === "explore-list" || route.routeKey === "explore-token") { + collection.forEach(token); + } else if (route.routeKey === "explore-chart") { + collection.forEach(chart); + } else if (route.routeKey === "creator-profile") { + collection.forEach(profile); + let profileTokenCount = 0; + for (const entry of collection) { + profileTokenCount += array( + object(entry, "reconciler-route-profile-count").tokens, + "reconciler-route-profile-count-tokens", + ).length; + } + if (profileTokenCount !== route.comparedCount) { + fail("reconciler-route-profile-cardinality"); + } + } else if (route.routeKey === "classic-v3-profile") { + collection.forEach(reward); + } else { + collection.forEach(lookup); + } + documents.set(route.routeKey, collection); + } + const tokenRows = documents.get("explore-list")!; + const detailRows = documents.get("explore-token")!; + const chartRows = documents.get("explore-chart")!; + const profileRows = documents.get("creator-profile")!; + const rewardRows = documents.get("classic-v3-profile") ?? []; + const lookupRows = documents.get("launch-lookup") ?? []; + if (JSON.stringify(tokenRows) !== JSON.stringify(detailRows)) { + fail("reconciler-route-cross-contract"); + } + const tokenByIdentity = new Map(); + let previousTokenOrder: readonly (bigint | number | string)[] | undefined; + for (const entry of tokenRows) { + const row = object(entry, "reconciler-route-token-identity"); + const identity = `${row.releaseVersion}:${row.tokenAddress}`; + if (tokenByIdentity.has(identity)) { + fail("reconciler-route-token-identity"); + } + const order = [ + BigInt(row.launchBlockNumber as string), + row.launchTransactionIndex as number, + row.launchLogIndex as number, + row.launchTransactionHash as string, + row.tokenAddress as string, + ] as const; + if (previousTokenOrder) { + let comparison = 0; + for (let index = 0; index < order.length; index += 1) { + if (order[index]! < previousTokenOrder[index]!) { + comparison = -1; + break; + } + if (order[index]! > previousTokenOrder[index]!) { + comparison = 1; + break; + } + } + if (comparison < 0) fail("reconciler-route-token-order"); + } + previousTokenOrder = order; + tokenByIdentity.set(identity, row); + } + + chartRows.forEach((entry, index) => { + const row = object(entry, "reconciler-route-chart-identity"); + const source = tokenRows[index] === undefined + ? undefined + : object(tokenRows[index]!, "reconciler-route-chart-token-source"); + if ( + !source || + row.releaseVersion !== source.releaseVersion || + row.modelId !== source.modelId || + row.tokenAddress !== source.tokenAddress || + row.poolId !== source.poolId || + row.quoteAssetAddress !== source.quoteAssetAddress + ) { + fail("reconciler-route-chart-token-mismatch"); + } + }); + + const profileTokenIdentities = new Set(); + let previousAccount = ""; + for (const entry of profileRows) { + const row = object(entry, "reconciler-route-profile-identity"); + const account = row.account as string; + if (account <= previousAccount) fail("reconciler-route-profile-order"); + previousAccount = account; + for (const reference of array( + row.tokens, + "reconciler-route-profile-identity-tokens", + )) { + const item = object(reference, "reconciler-route-profile-reference"); + const identity = `${item.releaseVersion}:${item.tokenAddress}`; + const source = tokenByIdentity.get(identity); + if ( + !source || + source.creatorAddress !== account || + source.modelId !== item.modelId || + source.launchTransactionHash !== item.launchTransactionHash || + profileTokenIdentities.has(identity) + ) { + fail("reconciler-route-profile-token-mismatch"); + } + profileTokenIdentities.add(identity); + } + } + if (profileTokenIdentities.size !== tokenByIdentity.size) { + fail("reconciler-route-profile-token-coverage"); + } + + const classicV3Tokens = new Set( + [...tokenByIdentity.entries()] + .filter(([, row]) => row.releaseVersion === "classic-v3") + .map(([identity]) => identity), + ); + if (!documents.has("classic-v3-profile") && classicV3Tokens.size > 0) { + fail("reconciler-route-reward-route-missing"); + } + let previousVault = ""; + for (const entry of rewardRows) { + const row = object(entry, "reconciler-route-reward-identity"); + const identity = `${row.releaseVersion}:${row.tokenAddress}`; + const source = tokenByIdentity.get(identity); + const vault = row.vaultAddress as string; + if ( + !source || + !classicV3Tokens.delete(identity) || + source.poolId !== row.poolId || + source.rewardVaultAddress !== vault || + source.launchTransactionHash !== row.launchTransactionHash || + source.name !== row.tokenName || + source.symbol !== row.tokenSymbol || + vault <= previousVault + ) { + fail("reconciler-route-reward-token-mismatch"); + } + previousVault = vault; + } + if (classicV3Tokens.size !== 0) { + fail("reconciler-route-reward-token-coverage"); + } + + const lookupExpected = new Set( + [...tokenByIdentity.entries()] + .filter(([, row]) => row.releaseVersion !== "classic-v2") + .map(([identity]) => identity), + ); + if (!documents.has("launch-lookup") && lookupExpected.size > 0) { + fail("reconciler-route-lookup-route-missing"); + } + let previousLookupOrder = ""; + for (const entry of lookupRows) { + const row = object(entry, "reconciler-route-lookup-identity"); + const identity = `${row.releaseVersion}:${row.tokenAddress}`; + const source = tokenByIdentity.get(identity); + const order = `${row.account}:${row.launchTransactionHash}:${row.tokenAddress}`; + if ( + !source || + !lookupExpected.delete(identity) || + source.modelId !== row.modelId || + source.creatorAddress !== row.account || + source.launchTransactionHash !== row.launchTransactionHash || + order <= previousLookupOrder + ) { + fail("reconciler-route-lookup-token-mismatch"); + } + previousLookupOrder = order; + } + if (lookupExpected.size !== 0) { + fail("reconciler-route-lookup-token-coverage"); + } + return routes; +} + +export function assertClassicV3ReconcilerRouteSet( + routes: readonly ReconcilerRouteDto[], +): readonly ReconcilerRouteDto[] { + return assertReconcilerRouteSetForKeys(routes, RECONCILER_ROUTE_KEYS); +} + +export const assembleReconcilerRoutes = assembleClassicV3ReconcilerRoutes; +export const assertReconcilerRouteSet = assertClassicV3ReconcilerRouteSet; diff --git a/lib/data-pipeline/classic-v3-reward-commitments.ts b/lib/data-pipeline/classic-v3-reward-commitments.ts new file mode 100644 index 00000000..869c220f --- /dev/null +++ b/lib/data-pipeline/classic-v3-reward-commitments.ts @@ -0,0 +1,83 @@ +import { encodeAbiParameters, keccak256 } from "viem"; + +import type { HexAddress, HexBytes32 } from "./codecs"; + +export type ClassicV3InitialRewardCommitmentInput = Readonly<{ + vault: HexAddress; + feeHook: HexAddress; + poolId: HexBytes32; + ctoAuthority: HexAddress; + salt: HexBytes32; + factoryConfigurationHash: HexBytes32; + beneficiaries: readonly HexAddress[]; + sharesBps: readonly number[]; +}>; + +/** + * Recomputes the three immutable Classic reward commitments at the database + * trust boundary. The ABI order mirrors ClassicRewardVaultFactoryV1 and + * ClassicRewardVaultV1 exactly; CREATE2 salt is deliberately not a constructor + * argument. + */ +export function classicV3InitialRewardCommitments( + input: ClassicV3InitialRewardCommitmentInput, +): Readonly<{ + factoryInputCommitment: HexBytes32; + constructorArgumentsCommitment: HexBytes32; + initialActiveConfigurationHash: HexBytes32; +}> { + const beneficiaries = [...input.beneficiaries]; + const sharesBps = [...input.sharesBps]; + return Object.freeze({ + factoryInputCommitment: keccak256( + encodeAbiParameters( + [ + { type: "bytes32" }, + { type: "address" }, + { type: "bytes32" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [input.salt, input.feeHook, input.poolId, beneficiaries, sharesBps], + ), + ), + constructorArgumentsCommitment: keccak256( + encodeAbiParameters( + [ + { type: "address" }, + { type: "bytes32" }, + { type: "address" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [ + input.feeHook, + input.poolId, + input.ctoAuthority, + beneficiaries, + sharesBps, + ], + ), + ), + initialActiveConfigurationHash: keccak256( + encodeAbiParameters( + [ + { type: "uint256" }, + { type: "address" }, + { type: "bytes32" }, + { type: "uint64" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [ + 1n, + input.vault, + input.factoryConfigurationHash, + 1n, + beneficiaries, + sharesBps, + ], + ), + ), + }); +} diff --git a/lib/data-pipeline/codecs.ts b/lib/data-pipeline/codecs.ts new file mode 100644 index 00000000..d707bff5 --- /dev/null +++ b/lib/data-pipeline/codecs.ts @@ -0,0 +1,143 @@ +import "server-only"; + +import { invalidInput } from "./errors"; + +export type HexAddress = `0x${string}`; +export type HexBytes32 = `0x${string}`; +export type HexSelector = `0x${string}`; +export type HexData = `0x${string}`; + +const UINT256_MAX = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + +function canonicalFixedHex( + value: unknown, + bytes: number, + operation: string, +): `0x${string}` { + if ( + typeof value !== "string" || + !new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value) + ) { + throw invalidInput("config", operation); + } + return value.toLowerCase() as `0x${string}`; +} +export function canonicalAddress(value: unknown): HexAddress { + return canonicalFixedHex(value, 20, "address") as HexAddress; +} + +export function canonicalBytes32(value: unknown): HexBytes32 { + return canonicalFixedHex(value, 32, "bytes32") as HexBytes32; +} + +export function canonicalSelector(value: unknown): HexSelector { + return canonicalFixedHex(value, 4, "selector") as HexSelector; +} + +export function canonicalRawData(value: unknown): HexData { + if ( + typeof value !== "string" || + !/^0x(?:[0-9a-fA-F]{2})*$/.test(value) + ) { + throw invalidInput("config", "raw-data"); + } + return value.toLowerCase() as HexData; +} + +function byteaToBytes(value: unknown, expectedBytes: number): Uint8Array { + if (value instanceof Uint8Array) { + if (value.byteLength !== expectedBytes) { + throw invalidInput("postgres", "bytea-width"); + } + return value; + } + if ( + typeof value === "string" && + new RegExp(`^\\\\x[0-9a-fA-F]{${expectedBytes * 2}}$`).test(value) + ) { + return Uint8Array.from( + value + .slice(2) + .match(/.{2}/g)! + .map((part) => Number.parseInt(part, 16)), + ); + } + throw invalidInput("postgres", "bytea"); +} + +function encodeBytes(value: Uint8Array): `0x${string}` { + return `0x${Array.from(value, (byte) => + byte.toString(16).padStart(2, "0"), + ).join("")}`; +} + +export function addressFromBytea(value: unknown): HexAddress { + return encodeBytes(byteaToBytes(value, 20)) as HexAddress; +} + +export function bytes32FromBytea(value: unknown): HexBytes32 { + return encodeBytes(byteaToBytes(value, 32)) as HexBytes32; +} + +export function dataFromBytea(value: unknown): HexData { + if (value instanceof Uint8Array) return encodeBytes(value) as HexData; + if (typeof value === "string" && /^\\x(?:[0-9a-fA-F]{2})*$/.test(value)) { + const bytes = + value.length === 2 + ? new Uint8Array() + : Uint8Array.from( + value + .slice(2) + .match(/.{2}/g)! + .map((part) => Number.parseInt(part, 16)), + ); + return encodeBytes(bytes) as HexData; + } + throw invalidInput("postgres", "bytea"); +} + +export function hexToBytes(value: HexData): Uint8Array { + const canonical = canonicalRawData(value); + if (canonical === "0x") return new Uint8Array(); + return Uint8Array.from( + canonical + .slice(2) + .match(/.{2}/g)! + .map((part) => Number.parseInt(part, 16)), + ); +} + +export function parseUint256Text(value: unknown): string { + if (typeof value !== "string" || !/^\d+$/.test(value) || value.length > 79) { + throw invalidInput("config", "uint256"); + } + const parsed = BigInt(value); + if (parsed > UINT256_MAX) throw invalidInput("config", "uint256"); + return parsed.toString(); +} + +export function parseNonnegativeIntegerText( + value: unknown, + maximumDigits = 78, +): string { + if ( + typeof value !== "string" || + !/^(0|[1-9]\d*)$/.test(value) || + value.length > maximumDigits + ) { + throw invalidInput("config", "integer"); + } + return value; +} + +export function parseCanonicalDecimalText(value: unknown): string { + if ( + typeof value !== "string" || + value.length > 160 || + !/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value) + ) { + throw invalidInput("config", "decimal"); + } + return value; +} diff --git a/lib/data-pipeline/config.ts b/lib/data-pipeline/config.ts new file mode 100644 index 00000000..63f84f5c --- /dev/null +++ b/lib/data-pipeline/config.ts @@ -0,0 +1,307 @@ +import "server-only"; + +import { dataPipelineError } from "./errors"; +import { + validatedPostgresConnectionTarget, + validatedPostgresConnectionString, + validatedPostgresSslCa, +} from "./postgres-connection.server"; + +export const INDEXED_ROUTE_FLAG_NAMES = [ + "INDEXED_EXPLORE_LIST_READS_ENABLED", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED", + "INDEXED_EXPLORE_CHART_READS_ENABLED", + "INDEXED_CREATOR_PROFILE_READS_ENABLED", + "INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED", + "INDEXED_LAUNCH_LOOKUP_ENABLED", + "INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", +] as const; + +export const INDEXED_CONTROL_FLAG_NAMES = [ + "INDEXED_READ_SHADOW_COMPARE_ENABLED", + "INDEXED_READ_REQUIRE_PARITY_ENABLED", + "INDEXED_READ_LIVE_FALLBACK_ENABLED", +] as const; + +type RouteFlagName = (typeof INDEXED_ROUTE_FLAG_NAMES)[number]; +type ControlFlagName = (typeof INDEXED_CONTROL_FLAG_NAMES)[number]; +export type DataPipelineFlagName = RouteFlagName | ControlFlagName; + +type Environment = Readonly>; + +const OFFICIAL_UNISWAP_GRAPH_GATEWAY_BASE_URL = + "https://gateway.thegraph.com"; + +const BROWSER_FORBIDDEN_NAMES = [ + "NEXT_PUBLIC_PROGRAMMABLE_ENVIO_GRAPHQL_URL", + "NEXT_PUBLIC_PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN", + "NEXT_PUBLIC_PROGRAMMABLE_API_READER_DATABASE_URL", + "NEXT_PUBLIC_PROGRAMMABLE_RELEASE_PROBE_DATABASE_URL", + "NEXT_PUBLIC_PROGRAMMABLE_POSTGRES_SSL_CA_PEM", + "NEXT_PUBLIC_PROGRAMMABLE_UNISWAP_GRAPH_API_KEY", + "NEXT_PUBLIC_UNISWAP_V4_SUBGRAPH_API_KEY", + "NEXT_PUBLIC_PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL", + "NEXT_PUBLIC_PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL", + "NEXT_PUBLIC_PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL", + "NEXT_PUBLIC_PROGRAMMABLE_SHADOW_PROBE_TOKEN", +] as const; + +function invalidConfig(): never { + throw dataPipelineError({ + dependency: "config", + code: "invalid_config", + retryable: false, + countsTowardCircuit: false, + }); +} +function parseBoolean( + value: string | undefined, + defaultValue: boolean, +): boolean { + if (value === undefined || value === "") return defaultValue; + if (value === "true") return true; + if (value === "false") return false; + return invalidConfig(); +} + +function parseInteger( + value: string | undefined, + defaultValue: number, + minimum: number, + maximum: number, +) { + if (value === undefined || value === "") return defaultValue; + if (!/^(0|[1-9]\d*)$/.test(value)) return invalidConfig(); + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) { + return invalidConfig(); + } + return parsed; +} + +function parseHttpsUrl(value: string | undefined): string | undefined { + if (value === undefined || value === "") return undefined; + try { + const url = new URL(value); + if ( + url.protocol !== "https:" || + url.username !== "" || + url.password !== "" || + url.search !== "" || + url.hash !== "" + ) { + return invalidConfig(); + } + return url.toString().replace(/\/$/, ""); + } catch { + return invalidConfig(); + } +} + +function parseDatabaseUrl(value: string | undefined): string | undefined { + if (value === undefined || value === "") return undefined; + try { + return validatedPostgresConnectionString(value); + } catch { + return invalidConfig(); + } +} + +function parsePostgresSslCa(value: string | undefined): string | undefined { + if (value === undefined || value === "") return undefined; + try { + return validatedPostgresSslCa(value); + } catch { + return invalidConfig(); + } +} + +function optionalSecret(value: string | undefined): string | undefined { + if (value === undefined || value === "") return undefined; + if ( + value.length < 8 || + value.length > 512 || + /[\u0000-\u001f\u007f]/.test(value) + ) { + return invalidConfig(); + } + return value; +} + +export type DataPipelineConfig = { + flags: Readonly>; + envio: { + endpoint?: string; + token?: string; + timeoutMs: 2_000; + maximumBodyBytes: number; + }; + postgres: { + connectionString?: string; + releaseProbeConnectionString?: string; + sslCaPem?: string; + maxConnections: number; + connectTimeoutMs: number; + idleTimeoutMs: number; + statementTimeoutMs: 1_000; + lockTimeoutMs: number; + }; + uniswap: { + gatewayBaseUrl: string; + apiKey?: string; + timeoutMs: 2_500; + maximumBodyBytes: number; + }; +}; + +export function loadDataPipelineConfig( + env: Environment = process.env, +): DataPipelineConfig { + for (const name of BROWSER_FORBIDDEN_NAMES) { + if (env[name] !== undefined && env[name] !== "") invalidConfig(); + } + + const flags = { + INDEXED_EXPLORE_LIST_READS_ENABLED: parseBoolean( + env.INDEXED_EXPLORE_LIST_READS_ENABLED, + false, + ), + INDEXED_EXPLORE_TOKEN_READS_ENABLED: parseBoolean( + env.INDEXED_EXPLORE_TOKEN_READS_ENABLED, + false, + ), + INDEXED_EXPLORE_CHART_READS_ENABLED: parseBoolean( + env.INDEXED_EXPLORE_CHART_READS_ENABLED, + false, + ), + INDEXED_CREATOR_PROFILE_READS_ENABLED: parseBoolean( + env.INDEXED_CREATOR_PROFILE_READS_ENABLED, + false, + ), + INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED: parseBoolean( + env.INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED, + false, + ), + INDEXED_LAUNCH_LOOKUP_ENABLED: parseBoolean( + env.INDEXED_LAUNCH_LOOKUP_ENABLED, + false, + ), + INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED: parseBoolean( + env.INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED, + false, + ), + INDEXED_READ_SHADOW_COMPARE_ENABLED: parseBoolean( + env.INDEXED_READ_SHADOW_COMPARE_ENABLED, + false, + ), + INDEXED_READ_REQUIRE_PARITY_ENABLED: parseBoolean( + env.INDEXED_READ_REQUIRE_PARITY_ENABLED, + true, + ), + INDEXED_READ_LIVE_FALLBACK_ENABLED: parseBoolean( + env.INDEXED_READ_LIVE_FALLBACK_ENABLED, + true, + ), + } satisfies Record; + + const isProduction = + env.NODE_ENV === "production" || + env.VERCEL_ENV === "production" || + process.env.NODE_ENV === "production" || + process.env.VERCEL_ENV === "production"; + if (isProduction && !flags.INDEXED_READ_REQUIRE_PARITY_ENABLED) { + invalidConfig(); + } + + const uniswapGraphGatewayBaseUrl = + parseHttpsUrl(env.PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL) ?? + OFFICIAL_UNISWAP_GRAPH_GATEWAY_BASE_URL; + if ( + isProduction && + uniswapGraphGatewayBaseUrl !== OFFICIAL_UNISWAP_GRAPH_GATEWAY_BASE_URL + ) { + invalidConfig(); + } + const postgresConnectionString = parseDatabaseUrl( + env.PROGRAMMABLE_API_READER_DATABASE_URL, + ); + const releaseProbeConnectionString = parseDatabaseUrl( + env.PROGRAMMABLE_RELEASE_PROBE_DATABASE_URL, + ); + const postgresSslCaPem = parsePostgresSslCa( + env.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + ); + if ( + !postgresConnectionString && + !releaseProbeConnectionString && + postgresSslCaPem + ) { + invalidConfig(); + } + for (const connectionString of [ + postgresConnectionString, + releaseProbeConnectionString, + ]) { + if (!connectionString) continue; + const target = validatedPostgresConnectionTarget( + connectionString, + ); + const requiresCa = + !target.isLoopback || target.sslMode === "verify-full"; + if (requiresCa && !postgresSslCaPem) invalidConfig(); + } + + return Object.freeze({ + flags: Object.freeze(flags), + envio: Object.freeze({ + endpoint: parseHttpsUrl(env.PROGRAMMABLE_ENVIO_GRAPHQL_URL), + token: optionalSecret(env.PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN), + timeoutMs: 2_000 as const, + maximumBodyBytes: parseInteger( + env.PROGRAMMABLE_ENVIO_MAXIMUM_BODY_BYTES, + 128 * 1024, + 4 * 1024, + 256 * 1024, + ), + }), + postgres: Object.freeze({ + connectionString: postgresConnectionString, + releaseProbeConnectionString, + sslCaPem: postgresSslCaPem, + maxConnections: parseInteger( + env.PROGRAMMABLE_POSTGRES_MAX_CONNECTIONS, + 2, + 1, + 5, + ), + connectTimeoutMs: parseInteger( + env.PROGRAMMABLE_POSTGRES_CONNECT_TIMEOUT_MS, + 1_000, + 100, + 5_000, + ), + idleTimeoutMs: parseInteger( + env.PROGRAMMABLE_POSTGRES_IDLE_TIMEOUT_MS, + 5_000, + 1_000, + 60_000, + ), + statementTimeoutMs: 1_000 as const, + lockTimeoutMs: parseInteger( + env.PROGRAMMABLE_POSTGRES_LOCK_TIMEOUT_MS, + 250, + 50, + 1_000, + ), + }), + uniswap: Object.freeze({ + gatewayBaseUrl: uniswapGraphGatewayBaseUrl, + apiKey: optionalSecret( + env.PROGRAMMABLE_UNISWAP_GRAPH_API_KEY ?? + env.UNISWAP_V4_SUBGRAPH_API_KEY, + ), + timeoutMs: 2_500 as const, + maximumBodyBytes: 128 * 1024, + }), + }); +} diff --git a/lib/data-pipeline/dual-rpc.ts b/lib/data-pipeline/dual-rpc.ts new file mode 100644 index 00000000..f39d9129 --- /dev/null +++ b/lib/data-pipeline/dual-rpc.ts @@ -0,0 +1,4918 @@ +import "server-only"; + +import { + bytesToHex, + concat, + encodeAbiParameters, + getContractAddress, + hexToBytes, + keccak256, + toBytes, + type Hex, +} from "viem"; + +import { + canonicalAddress, + canonicalBytes32, + canonicalRawData, + parseNonnegativeIntegerText, + parseUint256Text, + type HexAddress, + type HexBytes32, +} from "./codecs"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import type { EnvioCandidate, EnvioCandidateCursor } from "./envio"; +import { decodeManifestEvent, manifestEventSelectors } from "./event-manifest"; +import { + canonicalCoverageLog, + canonicalUint32DecimalText, + coverageLogPlacementKey, + type CanonicalCoverageLog, +} from "./provider-evidence"; +import { + canonicalDynamicSourceLineages, + type VerifiedDynamicSourceLineage, +} from "./projector-identities"; +import type { CanonicalDynamicSourceDeploymentEvidence } from "./projector-dynamic-activation"; +import { + expectedRewardRpcCallCount, + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1, + type ProjectorRewardRpcModel, +} from "./projector-reward-rpc-contract"; +import { + PROJECTOR_JSON_RPC_BATCH_SIZE, + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE, +} from "./projector-runtime-limits"; +import type { + ProjectorRewardBaseline, + ProjectorRewardSnapshot, +} from "./projector-reward-fold"; +import { getDataPipelineReleaseBinding } from "./release-binding.server"; +import { + canonicalImmutableReferences, + immutableReferencesCommitment, + normalizeRuntimeBytecode, + runtimeBytecodeEvidence, + type ImmutableReference, +} from "./runtime-bytecode"; +import { assertProductionDualRpcProviders } from "./rpc-providers.server"; + +export type CandidateRpcBlock = { + number: bigint | null; + hash: Hex | null; + timestamp: bigint; +}; + +export type CandidateRpcLog = { + address: Hex; + blockNumber: bigint | null; + blockHash: Hex | null; + transactionHash: Hex | null; + transactionIndex: number | null; + logIndex: number | null; + removed?: boolean; + topics: readonly Hex[]; + data: Hex; +}; + +export type CandidateRpcReceipt = { + status: "success" | "reverted"; + blockNumber: bigint; + blockHash: Hex; + transactionHash: Hex; + transactionIndex: number; + logs: readonly CandidateRpcLog[]; +}; + +export type CandidateRpcLogFilter = Readonly<{ + addresses: readonly HexAddress[]; + topic0: readonly HexBytes32[]; + fromBlock: bigint; + toBlock: bigint; +}>; + +export type CandidateRpcClient = { + getChainId(): Promise; + getBlockNumber(): Promise; + getBlock(input: { blockNumber: bigint }): Promise; + /** Exact block headers carried by one physical JSON-RPC batch. */ + getBlocks?(input: { + blockNumbers: readonly bigint[]; + }): Promise; + getTransactionReceipt(input: { + hash: HexBytes32; + }): Promise; + /** + * One physical JSON-RPC batch. The verifier never supplies more than 100 + * hashes, retains the input order, and counts the whole batch as one traced + * provider call. + */ + getTransactionReceipts?(input: { + hashes: readonly HexBytes32[]; + }): Promise; + getBytecode(input: { + address: HexAddress; + } & ( + | { + blockNumber: bigint; + blockHash?: never; + requireCanonical?: never; + } + | { + blockNumber?: never; + blockHash: HexBytes32; + requireCanonical: true; + } + )): Promise; + /** Exact-state eth_getCode requests carried by one physical JSON-RPC batch. */ + getBytecodes?(input: { + requests: readonly Readonly<{ + address: HexAddress; + blockHash: HexBytes32; + requireCanonical: true; + }>[]; + }): Promise; + /** + * Reads immutable launch-token display metadata at the launch block. The + * projector accepts the values only when both independent providers return + * the exact same UTF-8 strings. This deliberately does not fall back to a + * subgraph, token list, or latest-block read. + */ + readErc20Metadata?(input: { + address: HexAddress; + blockHash: HexBytes32; + requireCanonical: true; + }): Promise>; + /** + * Executes only the frozen reward-vault call shapes at one exact block. + * The returned call count is verified against the committed formula. + */ + readRewardSnapshot?(input: { + model: ProjectorRewardRpcModel; + vault: HexAddress; + blockNumber: bigint; + blockHash: HexBytes32; + balanceAccounts: readonly HexAddress[]; + }): Promise; + /** + * Reads the authenticated Classic vault-factory mapping, immutable CTO + * authority and both CREATE2 helpers at one exact canonical block. These are + * separate physical calls; their count is part of the activation evidence + * budget. + */ + readClassicRewardFactorySnapshot?(input: { + factory: HexAddress; + vault: HexAddress; + blockNumber: bigint; + blockHash: HexBytes32; + salt: HexBytes32; + feeHook: HexAddress; + poolId: HexBytes32; + beneficiaries: readonly HexAddress[]; + sharesBps: readonly number[]; + }): Promise; + getLogs?(input: CandidateRpcLogFilter): Promise; + /** Up to 100 exact eth_getLogs filters in one physical JSON-RPC batch. */ + getLogsBatch?(input: { + requests: readonly CandidateRpcLogFilter[]; + }): Promise; +}; + +export type CandidateRpcRewardSnapshot = Readonly<{ + model: unknown; + vault: unknown; + blockNumber: unknown; + blockHash: unknown; + poolId: unknown; + configurationEpoch: unknown; + configurationHash: unknown; + totalCreatorFeesReceived: unknown; + totalCreatorFeesClaimed: unknown; + beneficiaryCount: unknown; + allocations: unknown; + balances: unknown; + rpcCallCount: unknown; +}>; + +export type CandidateRpcClassicRewardFactorySnapshot = Readonly<{ + factory: unknown; + vault: unknown; + blockNumber: unknown; + blockHash: unknown; + configurationHash: unknown; + ctoAuthority: unknown; + initCodeHash: unknown; + predictedVault: unknown; + rpcCallCount: unknown; +}>; + +export type DualRpcRewardSnapshot = Readonly<{ + model: ProjectorRewardRpcModel; + vault: HexAddress; + blockNumber: string; + blockHash: HexBytes32; + poolId: HexBytes32; + configurationEpoch: string | null; + configurationHash: HexBytes32; + totalCreatorFeesReceived: string; + totalCreatorFeesClaimed: string; + allocations: readonly Readonly<{ + allocationIndex: number; + beneficiary: HexAddress; + payoutAddress: HexAddress; + shareBps: string; + }>[]; + balances: readonly Readonly<{ + account: HexAddress; + payoutAddress: HexAddress; + claimableAccrued: string; + claimedTotal: string; + }>[]; + rpcCallCount: number; + verificationAccounts: readonly HexAddress[]; + providerIdentities: readonly [string, string]; + providerVendorGroups: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + providerCallCounts: readonly [number, number]; + providerSnapshotCommitments: readonly [HexBytes32, HexBytes32]; + chunks: readonly Readonly<{ + chunkIndex: number; + verificationAccounts: readonly HexAddress[]; + providerCallCounts: readonly [number, number]; + providerSnapshotCommitments: readonly [HexBytes32, HexBytes32]; + }>[]; + executionTrace: DualRpcExecutionTrace; +}>; + +export type DualRpcInitialRewardConfigurationEvidence = Readonly<{ + parentCandidateId: string; + launchCandidateId: string; + vault: HexAddress; + poolId: HexBytes32; + deploymentBlockNumber: string; + deploymentBlockHash: HexBytes32; + activationBlockNumber: string; + activationBlockHash: HexBytes32; + activationBlockGlobalLogIndex: number; + coveredRewardCandidateIds: readonly string[]; + factory: HexAddress; + salt: HexBytes32; + factoryInputCommitment: HexBytes32; + ctoAuthority: HexAddress; + constructorArgumentsCommitment: HexBytes32; + deployedArtifactCreationCodeCommitment: HexBytes32; + factoryConfigurationHash: HexBytes32; + providerFactoryConfigurationHashes: readonly [HexBytes32, HexBytes32]; + providerCtoAuthorities: readonly [HexAddress, HexAddress]; + providerInitCodeHashes: readonly [HexBytes32, HexBytes32]; + providerPredictedVaults: readonly [HexAddress, HexAddress]; + locallyPredictedVault: HexAddress; + factoryProviderCallCounts: readonly [4, 4]; + factoryProviderSnapshotCommitments: readonly [HexBytes32, HexBytes32]; + initialActiveConfigurationHash: HexBytes32; + allocations: readonly Readonly<{ + allocationIndex: number; + beneficiary: HexAddress; + shareBps: string; + }>[]; + /** + * Configuration-only provider evidence used to reconstruct epoch one. It is + * deliberately not a reward-state/conservation proof because it reads only + * the vault sentinel account. A launch block containing reward events must + * still pass the normal full-account reward fold and snapshot verifier. + */ + endConfigurationSnapshot: DualRpcRewardSnapshot; +}>; + +export type DualRpcDynamicRuntimeActivationObservation = Readonly<{ + chainId: 1; + parentCandidateId: string; + launchCandidateId: string; + sourceAddress: HexAddress; + deploymentBlockNumber: string; + deploymentBlockHash: HexBytes32; + activationBlockNumber: string; + activationBlockHash: HexBytes32; + activationBlockGlobalLogIndex: number; + providerIdentities: readonly [string, string]; + providerVendorGroups: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + rawRuntimeCodeA: Hex; + rawRuntimeCodeB: Hex; + runtimeCodeHashA: HexBytes32; + runtimeCodeHashB: HexBytes32; + normalizedRuntimeCodeHashA: HexBytes32; + normalizedRuntimeCodeHashB: HexBytes32; + runtimeByteLengthA: string; + runtimeByteLengthB: string; + immutableReferences: readonly ImmutableReference[]; + immutableReferencesCommitment: HexBytes32; + immutableValues: readonly Hex[]; + immutableValuesCommitment: HexBytes32; + reconstructedRuntimeCode: Hex; + reconstructedRuntimeCodeHash: HexBytes32; + factoryConfigurationCommitment: HexBytes32; + template: ProjectorDynamicSourceTemplate; + startedAtMs: number; + completedAtMs: number; + elapsedMs: number; + hardDeadlineMs: number; + providerCallCounts: readonly [1, 1]; +}>; + +export type DualRpcTokenMetadata = Readonly<{ + token: HexAddress; + blockNumber: string; + blockHash: HexBytes32; + name: string; + symbol: string; +}>; + +export type CandidateRpcProvider = { + identity: string; + vendorGroup: string; + endpointCommitment: HexBytes32; + endpointOriginCommitment: HexBytes32; + client: CandidateRpcClient; +}; + +export type DualRpcOperation = + | "getChainId" + | "getBlockNumber" + | "getBlock" + | "getTransactionReceipt" + | "getBytecode" + | "readRewardSnapshot" + | "readClassicRewardFactorySnapshot"; + +export type DualRpcCallTrace = Readonly<{ + providerIdentity: string; + providerVendorGroup: string; + providerEndpointCommitment: HexBytes32; + providerOriginCommitment: HexBytes32; + operation: DualRpcOperation; + attempt: number; + startedOffsetMs: number; + durationMs: number; + outcome: "success" | "error"; +}>; + +export type DualRpcExecutionTrace = Readonly<{ + startedAtMs: number; + completedAtMs: number; + candidateBatchSize: number; + hardDeadlineMs: number; + maxCallsPerProvider: number; + elapsedMs: number; + providerCallCounts: readonly [number, number]; + calls: readonly DualRpcCallTrace[]; +}>; + +export type DualRpcExecutionPolicy = Readonly<{ + maxConcurrency?: number; + maxAttempts?: number; + baseBackoffMs?: number; + hardDeadlineMs?: number; + maxCallsPerProvider?: number; + signal?: AbortSignal; + sleep?: (milliseconds: number) => Promise; + /** @deprecated Use hardDeadlineMs. Kept only for migration compatibility. */ + deadlineMs?: number; + /** @deprecated Use maxCallsPerProvider. Kept only for migration compatibility. */ + maxProviderCalls?: number; +}>; + +export type DualRpcCandidateEvidence = { + chainId: 1; + candidateId: string; + sourceAddress: HexAddress; + contractName: string; + eventName: string; + sourceKind: "static" | "dynamic-unresolved" | "dynamic-attested"; + model: "classic" | "stock-paired" | "unresolved"; + releaseVersion: string; + payloadHash: HexBytes32; + rawLogCommitment: HexBytes32; + providerIdentities: readonly [string, string]; + providerVendorGroups: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + providerHeads: readonly [string, string]; + safeBlockNumber: string; + safeBlockHash: HexBytes32; + candidateBlockNumber: string; + candidateBlockHash: HexBytes32; + candidateBlockTimestamp: string; + transactionHash: HexBytes32; + transactionIndex: number; + receiptCommitment: HexBytes32; + sourceCodeHash: HexBytes32; + receiptLogOrdinal: number; + dynamicSourceAttestationId?: string; + normalizedRuntimeCodeHash?: HexBytes32; + immutableReferencesCommitment?: HexBytes32; + runtimeByteLength?: string; +}; + +export type DualRpcCandidateBatchEvidence = { + chainId: 1; + providerIdentities: readonly [string, string]; + providerVendorGroups: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + providerHeads: readonly [string, string]; + safeBlockNumber: string; + safeBlockHash: HexBytes32; + candidates: readonly DualRpcCandidateEvidence[]; + executionTrace: DualRpcExecutionTrace; +}; + +export type DualRpcCandidateWindowEvidence = + DualRpcCandidateBatchEvidence & { + coveredCandidateCount: number; + coverage: { + fromBlockNumber: string; + throughBlockNumber: string; + throughBlockHash: HexBytes32; + throughBlockGlobalLogIndex: string; + filterCommitment: HexBytes32; + providerLogCommitments: readonly [HexBytes32, HexBytes32]; + }; + }; + +export type ProjectorDynamicSourceTemplate = Readonly<{ + templateId: string; + contractName: + | "ClassicV3RewardVault" + | "StockV1RewardVault" + | "StockV2V3RewardVault"; + model: "classic" | "stock-paired"; + releaseVersion: + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + parentFactoryAddress: HexAddress; + parentFactoryContractName: + | "ClassicV3RewardVaultFactory" + | "StockV1RewardVaultFactory" + | "StockV2V3RewardVaultFactory"; + parentFactoryBindingId: string; + parentFactoryBindingCommitment: HexBytes32; + parentSourceRole: string; + factoryEventName: + | "ClassicRewardVaultDeployed" + | "QuoteAssetFeeSplitVaultDeployed"; + deployedAddressField: "vault"; + deployedSourceRole: "reward_vault"; + deployedArtifactCreationCodeCommitment: HexBytes32; + expectedExactRuntimeCodeHash: HexBytes32 | null; + expectedNormalizedRuntimeCodeHash: HexBytes32; + expectedImmutableReferencesCommitment: HexBytes32; + expectedRuntimeByteLength: string; + immutableReferences: readonly ImmutableReference[]; + immutableBindingSpec: Readonly>; + immutableBindingCommitment: HexBytes32; + abiEventSetCommitment: HexBytes32; + templateCommitment: HexBytes32; + database: Readonly<{ + scope: Readonly<{ + releaseId: string; + modelId: string; + sourceGroup: string; + }>; + epochId: string; + pointerGeneration: string; + reorgGeneration: string; + envioProviderDeploymentId: string; + rpcProviderDeploymentIds: readonly [string, string]; + }>; +}>; + +export type VerifiedDeferredAllocationEvidence = Readonly<{ + source: "dual-rpc-reward-allocation"; + vault: HexAddress; + blockNumber: string; + blockHash: HexBytes32; + configurationHash: HexBytes32; + beneficiaryCount: string; + providerIdentities: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + evidenceCommitment: HexBytes32; +}>; + +/** + * A deliberately small, run-scoped observation used only to bridge a factory + * deployment event and the first event emitted by the newly deployed dynamic + * source in the same block. The parent window already owns the safe-head, + * block and log-coverage proof. This observation adds one exact-block + * `getBytecode` read per independent provider and nothing else. + */ +export type DualRpcDynamicRuntimeObservation = Readonly<{ + chainId: 1; + parentCandidateId: string; + sourceAddress: HexAddress; + deploymentBlockNumber: string; + deploymentBlockHash: HexBytes32; + providerIdentities: readonly [string, string]; + providerVendorGroups: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + rawRuntimeCodeA: Hex; + rawRuntimeCodeB: Hex; + runtimeCodeHashA: HexBytes32; + runtimeCodeHashB: HexBytes32; + normalizedRuntimeCodeHashA: HexBytes32; + normalizedRuntimeCodeHashB: HexBytes32; + runtimeByteLengthA: string; + runtimeByteLengthB: string; + immutableReferences: readonly ImmutableReference[]; + immutableReferencesCommitment: HexBytes32; + immutableValues: readonly Hex[]; + immutableValuesCommitment: HexBytes32; + reconstructedRuntimeCode: Hex; + reconstructedRuntimeCodeHash: HexBytes32; + factoryConfigurationCommitment: HexBytes32; + deferredAllocationEvidenceCommitment: HexBytes32 | null; + template: ProjectorDynamicSourceTemplate; + startedAtMs: number; + completedAtMs: number; + elapsedMs: number; + hardDeadlineMs: number; + providerCallCounts: readonly [1, 1]; +}>; + +export type DualRpcSafeHeadEvidence = Readonly<{ + providerHeads: readonly [string, string]; + safeBlockNumber: string; + safeBlockHash: HexBytes32; + cursorBlockHash: HexBytes32; +}>; + +const RELEASE_BINDING = getDataPipelineReleaseBinding(); +const PROVIDER_IDENTITY_PATTERN = /^[a-z0-9][a-z0-9:-]{0,63}$/; +const CANDIDATE_ID_PATTERN = + /^1:(0x[0-9a-f]{64}):(0x[0-9a-f]{64}):(0|[1-9]\d*)$/; +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-58][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const IMMUTABLE_VALUES_DOMAIN = toBytes( + "programmable:data-pipeline:immutable-values:v1\0", +); +const ZERO_BYTES32 = `0x${"00".repeat(32)}`; +const DEFAULT_RPC_CONCURRENCY = 4; +const DEFAULT_RPC_ATTEMPTS = 3; +const DEFAULT_RPC_BACKOFF_MS = 50; +const DEFAULT_RPC_DEADLINE_MS = 75_000; +const DEFAULT_MAXIMUM_PROVIDER_CALLS = 48; +const DEFAULT_COVERAGE_BLOCK_SPAN = 500; +const DEFAULT_COVERAGE_MAXIMUM_REQUESTS = 64; +const MAXIMUM_JSON_RPC_BATCH_SIZE = PROJECTOR_JSON_RPC_BATCH_SIZE; +const MAXIMUM_LOG_FILTER_ADDRESSES = 512; +const MAXIMUM_LOG_FILTER_TOPIC0 = 64; +const MAXIMUM_LOG_FILTER_BLOCK_SPAN = 1n; + +function safeInteger(value: unknown, operation: string) { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < 0 || + value > 0x7fff_ffff + ) { + throw validationError("rpc", operation); + } + return value; +} + +function nonnegativeBigint(value: unknown, operation: string) { + if (typeof value !== "bigint" || value < 0n) { + throw validationError("rpc", operation); + } + return value; +} + +function rpcBytes32(value: unknown, operation: string) { + try { + return canonicalBytes32(value); + } catch { + throw validationError("rpc", operation); + } +} + +function rpcAddress(value: unknown, operation: string) { + try { + return canonicalAddress(value); + } catch { + throw validationError("rpc", operation); + } +} + +function rpcData(value: unknown, operation: string) { + try { + return canonicalRawData(value); + } catch { + throw validationError("rpc", operation); + } +} + +function canonicalBlock( + value: CandidateRpcBlock, + expectedNumber: bigint, + operation: string, +) { + if ( + value === null || + typeof value !== "object" || + value.number !== expectedNumber || + value.hash === null + ) { + throw validationError("rpc", operation); + } + return { + number: expectedNumber, + hash: rpcBytes32(value.hash, operation), + timestamp: nonnegativeBigint(value.timestamp, operation), + }; +} + +function sameHex(left: string, right: string) { + return left.toLowerCase() === right.toLowerCase(); +} + +function canonicalReceipt(input: { + receipt: CandidateRpcReceipt; + candidate: EnvioCandidate; + candidateBlockNumber: bigint; +}) { + const { receipt, candidate, candidateBlockNumber } = input; + if ( + receipt === null || + typeof receipt !== "object" || + receipt.status !== "success" || + receipt.blockNumber !== candidateBlockNumber || + !sameHex(rpcBytes32(receipt.blockHash, "receipt"), candidate.blockHash) || + !sameHex( + rpcBytes32(receipt.transactionHash, "receipt"), + candidate.transactionHash, + ) || + safeInteger(receipt.transactionIndex, "receipt") !== + candidate.transactionIndex || + !Array.isArray(receipt.logs) || + receipt.logs.length === 0 || + receipt.logs.length > 10_000 + ) { + throw validationError("rpc", "receipt"); + } + + let previousLogIndex = -1; + let selectedOrdinal = -1; + const receiptLogs = receipt.logs as readonly CandidateRpcLog[]; + const logs = receiptLogs.map((log, ordinal) => { + if ( + log === null || + typeof log !== "object" || + log.blockNumber !== candidateBlockNumber || + log.blockHash === null || + log.transactionHash === null || + log.transactionIndex === null || + log.logIndex === null || + log.removed !== false || + !Array.isArray(log.topics) || + log.topics.length > 4 + ) { + throw validationError("rpc", "receipt-log"); + } + const logIndex = safeInteger(log.logIndex, "receipt-log"); + if (logIndex <= previousLogIndex) { + throw validationError("rpc", "receipt-log-order"); + } + previousLogIndex = logIndex; + const transactionIndex = safeInteger( + log.transactionIndex, + "receipt-log", + ); + const blockHash = rpcBytes32(log.blockHash, "receipt-log"); + const transactionHash = rpcBytes32( + log.transactionHash, + "receipt-log", + ); + const address = rpcAddress(log.address, "receipt-log"); + const topics = log.topics.map((topic) => + rpcBytes32(topic, "receipt-log"), + ); + const data = rpcData(log.data, "receipt-log"); + if ( + !sameHex(blockHash, candidate.blockHash) || + !sameHex(transactionHash, candidate.transactionHash) || + transactionIndex !== candidate.transactionIndex + ) { + throw validationError("rpc", "receipt-log-placement"); + } + if (logIndex === candidate.blockGlobalLogIndex) { + if (selectedOrdinal !== -1) { + throw validationError("rpc", "receipt-log-duplicate"); + } + selectedOrdinal = ordinal; + if ( + !sameHex(address, candidate.sourceAddress) || + data !== candidate.rawData || + topics.length !== candidate.orderedTopics.length || + topics.some( + (topic, index) => topic !== candidate.orderedTopics[index], + ) + ) { + throw validationError("rpc", "candidate-log"); + } + } + return [ + address, + blockHash, + transactionHash, + transactionIndex, + logIndex, + topics, + data, + ] as const; + }); + if (selectedOrdinal < 0) { + throw validationError("rpc", "candidate-log-missing"); + } + + const preimage = JSON.stringify([ + receipt.status, + candidateBlockNumber.toString(), + candidate.blockHash, + candidate.transactionHash, + candidate.transactionIndex, + logs, + ]); + return { + commitment: keccak256(toBytes(preimage)), + selectedOrdinal, + }; +} + +function providerIdentity(value: unknown) { + if (typeof value !== "string" || !PROVIDER_IDENTITY_PATTERN.test(value)) { + throw invalidInput("rpc", "provider-identity"); + } + return value; +} + +type RpcExecutionPolicyInput = DualRpcExecutionPolicy; + +function rpcExecutionPolicy(input: RpcExecutionPolicyInput | undefined) { + const maxConcurrency = input?.maxConcurrency ?? DEFAULT_RPC_CONCURRENCY; + const maxAttempts = input?.maxAttempts ?? DEFAULT_RPC_ATTEMPTS; + const baseBackoffMs = input?.baseBackoffMs ?? DEFAULT_RPC_BACKOFF_MS; + if ( + (input?.hardDeadlineMs !== undefined && input.deadlineMs !== undefined) || + (input?.maxCallsPerProvider !== undefined && + input.maxProviderCalls !== undefined) + ) { + throw invalidInput("rpc", "execution-policy"); + } + const hardDeadlineMs = + input?.hardDeadlineMs ?? input?.deadlineMs ?? DEFAULT_RPC_DEADLINE_MS; + const maxCallsPerProvider = + input?.maxCallsPerProvider ?? + input?.maxProviderCalls ?? + DEFAULT_MAXIMUM_PROVIDER_CALLS; + if ( + !Number.isSafeInteger(maxConcurrency) || + maxConcurrency < 1 || + maxConcurrency > 8 || + !Number.isSafeInteger(maxAttempts) || + maxAttempts < 1 || + maxAttempts > 3 || + !Number.isSafeInteger(baseBackoffMs) || + baseBackoffMs < 0 || + baseBackoffMs > 1_000 || + !Number.isSafeInteger(hardDeadlineMs) || + hardDeadlineMs < 10 || + hardDeadlineMs > DEFAULT_RPC_DEADLINE_MS || + !Number.isSafeInteger(maxCallsPerProvider) || + maxCallsPerProvider < 1 || + maxCallsPerProvider > 128 || + (input?.signal !== undefined && + !(input.signal instanceof AbortSignal)) || + (input?.sleep !== undefined && typeof input.sleep !== "function") + ) { + throw invalidInput("rpc", "execution-policy"); + } + return { + maxConcurrency, + maxAttempts, + baseBackoffMs, + hardDeadlineMs, + deadlineAt: Date.now() + hardDeadlineMs, + maxCallsPerProvider, + callerSignal: input?.signal, + sleep: + input?.sleep ?? + ((milliseconds: number) => + new Promise((resolve) => setTimeout(resolve, milliseconds))), + }; +} + +type RpcTraceContext = { + providerIdentity: string; + providerVendorGroup: string; + providerEndpointCommitment: HexBytes32; + providerOriginCommitment: HexBytes32; + startedAtMs: number; + callCount: number; + calls: DualRpcCallTrace[]; +}; + +function rpcCallBudgetExceeded(): DataPipelineError { + return dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "call-budget" }, + }); +} + +async function retryTracedRpc( + operationName: DualRpcOperation, + operation: () => Promise, + policy: ReturnType, + context: RpcTraceContext, +): Promise { + let lastError: unknown; + for (let attempt = 0; attempt < policy.maxAttempts; attempt += 1) { + if (policy.callerSignal?.aborted) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); + } + if (context.callCount >= policy.maxCallsPerProvider) { + throw rpcCallBudgetExceeded(); + } + context.callCount += 1; + const startedAtMs = Date.now(); + try { + const value = await withinRpcDeadline(operation, policy); + context.calls.push( + Object.freeze({ + providerIdentity: context.providerIdentity, + providerVendorGroup: context.providerVendorGroup, + providerEndpointCommitment: context.providerEndpointCommitment, + providerOriginCommitment: context.providerOriginCommitment, + operation: operationName, + attempt: attempt + 1, + startedOffsetMs: Math.max(0, startedAtMs - context.startedAtMs), + durationMs: Math.max(0, Date.now() - startedAtMs), + outcome: "success" as const, + }), + ); + return value; + } catch (error) { + context.calls.push( + Object.freeze({ + providerIdentity: context.providerIdentity, + providerVendorGroup: context.providerVendorGroup, + providerEndpointCommitment: context.providerEndpointCommitment, + providerOriginCommitment: context.providerOriginCommitment, + operation: operationName, + attempt: attempt + 1, + startedOffsetMs: Math.max(0, startedAtMs - context.startedAtMs), + durationMs: Math.max(0, Date.now() - startedAtMs), + outcome: "error" as const, + }), + ); + lastError = error; + if (attempt + 1 < policy.maxAttempts) { + await policy.sleep(policy.baseBackoffMs * 2 ** attempt); + } + } + } + if (lastError instanceof DataPipelineError) throw lastError; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: operationName }, + }); +} + +async function withinRpcDeadline( + operation: () => Promise, + policy: ReturnType, +): Promise { + const remaining = policy.deadlineAt - Date.now(); + if (remaining <= 0) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); + } + let timeout: ReturnType | undefined; + try { + return await Promise.race([ + operation(), + new Promise((_resolve, reject) => { + timeout = setTimeout( + () => + reject( + dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }), + ), + remaining, + ); + }), + ]); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +async function retryRpc( + operation: () => Promise, + policy: ReturnType, + budget?: { used: number; maximum: number }, + attemptCost = 1, +) { + let lastError: unknown; + for (let attempt = 0; attempt < policy.maxAttempts; attempt += 1) { + if ( + !Number.isSafeInteger(attemptCost) || + attemptCost < 1 || + (budget && budget.used + attemptCost > budget.maximum) + ) { + throw rpcCallBudgetExceeded(); + } + if (budget) budget.used += attemptCost; + try { + return await withinRpcDeadline(operation, policy); + } catch (error) { + lastError = error; + if (attempt + 1 < policy.maxAttempts) { + await policy.sleep(policy.baseBackoffMs * 2 ** attempt); + } + } + } + throw lastError; +} + +async function boundedRpcMap( + values: readonly Input[], + concurrency: number, + operation: (value: Input) => Promise, +) { + const output = new Array(values.length); + let cursor = 0; + const worker = async () => { + while (cursor < values.length) { + const index = cursor; + cursor += 1; + output[index] = await operation(values[index]!); + } + }; + await Promise.all( + Array.from( + { length: Math.min(concurrency, values.length) }, + () => worker(), + ), + ); + return output; +} + +function boundedRpcChunks( + values: readonly Input[], + size = MAXIMUM_JSON_RPC_BATCH_SIZE, +): readonly (readonly Input[])[] { + if (!Number.isSafeInteger(size) || size < 1) { + throw invalidInput("rpc", "batch-size"); + } + const chunks: Input[][] = []; + for (let index = 0; index < values.length; index += size) { + chunks.push(values.slice(index, index + size)); + } + return Object.freeze(chunks.map((chunk) => Object.freeze(chunk))); +} + +function validateCandidateBoundary( + candidate: EnvioCandidate, + dynamicSources: ReadonlyMap, + requireDynamicLineage: boolean, +) { + if (candidate === null || typeof candidate !== "object") { + throw invalidInput("rpc", "candidate"); + } + let blockNumber: bigint; + let timestamp: bigint; + try { + blockNumber = BigInt(parseNonnegativeIntegerText(candidate.blockNumber)); + timestamp = BigInt( + parseNonnegativeIntegerText(candidate.blockTimestamp), + ); + } catch { + throw invalidInput("rpc", "candidate"); + } + const blockHash = rpcBytes32(candidate.blockHash, "candidate"); + const transactionHash = rpcBytes32( + candidate.transactionHash, + "candidate", + ); + const sourceAddress = rpcAddress(candidate.sourceAddress, "candidate"); + const payloadHash = rpcBytes32(candidate.payloadHash, "candidate"); + const logIndex = safeInteger( + candidate.blockGlobalLogIndex, + "candidate-placement", + ); + const idMatch = CANDIDATE_ID_PATTERN.exec(candidate.candidateId); + if ( + candidate.chainId !== RELEASE_BINDING.chainId || + !idMatch || + idMatch[1] !== blockHash || + idMatch[2] !== transactionHash || + BigInt(idMatch[3]) !== BigInt(logIndex) || + typeof candidate.contractName !== "string" || + !/^[A-Za-z][A-Za-z0-9]{0,95}$/.test(candidate.contractName) || + typeof candidate.eventName !== "string" || + !/^[A-Za-z][A-Za-z0-9]{0,95}$/.test(candidate.eventName) || + !Array.isArray(candidate.orderedTopics) || + candidate.orderedTopics.length < 1 || + candidate.orderedTopics.length > 4 + ) { + throw validationError("rpc", "candidate-envelope"); + } + const topics = candidate.orderedTopics.map((topic) => + rpcBytes32(topic, "candidate-topic"), + ); + const rawData = rpcData(candidate.rawData, "candidate-data"); + const recomputedPayloadHash = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [topics, rawData], + ), + ); + if (payloadHash !== recomputedPayloadHash) { + throw validationError("rpc", "candidate-payload"); + } + const model = candidate.releaseHint?.model; + const releaseVersion = candidate.releaseHint?.releaseVersion; + if ( + (model !== "classic" && + model !== "stock-paired" && + model !== "unresolved") || + typeof releaseVersion !== "string" + ) { + throw validationError("rpc", "candidate-release"); + } + const staticSource = RELEASE_BINDING.sources.find( + (source) => source.address === sourceAddress, + ); + let sourceKind: "static" | "dynamic-unresolved" | "dynamic-attested"; + let expectedRuntimeCodeHash: HexBytes32 | null; + let dynamicSourceLineage: VerifiedDynamicSourceLineage | undefined; + if (staticSource) { + if ( + staticSource.contractName !== candidate.contractName || + blockNumber < BigInt(staticSource.startBlock) + ) { + throw validationError("rpc", "candidate-source"); + } + const releases = RELEASE_BINDING.releases.filter( + (release) => + release.sourceContracts.includes(candidate.contractName) && + blockNumber >= BigInt(release.activationBlock), + ); + const allSourceReleases = RELEASE_BINDING.releases.filter((release) => + release.sourceContracts.includes(candidate.contractName), + ); + const exact = releases.some( + (release) => + allSourceReleases.length === 1 && + release.model === model && + release.releaseVersion === releaseVersion, + ); + const unresolved = + model === "unresolved" && + releaseVersion === "unresolved" && + allSourceReleases.length > 1 && + new Set(allSourceReleases.map((release) => release.model)).size === 1 && + releases.length > 0; + if (!exact && !unresolved) { + throw validationError("rpc", "candidate-release"); + } + sourceKind = "static"; + expectedRuntimeCodeHash = staticSource.runtimeCodeHash; + } else { + const matchingReleases = RELEASE_BINDING.releases.filter( + (release) => release.dynamicContracts.includes(candidate.contractName), + ); + if ( + matchingReleases.length < 1 || + model !== "unresolved" || + releaseVersion !== "unresolved" || + matchingReleases.every( + (release) => blockNumber < BigInt(release.activationBlock), + ) + ) { + throw validationError("rpc", "dynamic-source-release"); + } + dynamicSourceLineage = dynamicSources.get(sourceAddress); + if (requireDynamicLineage && !dynamicSourceLineage) { + throw validationError( + "rpc", + `dynamic-source-lineage-missing:${sourceAddress}`, + ); + } + if (dynamicSourceLineage) { + const activationBeforeChild = + dynamicSourceLineage.activationBlockNumber !== undefined && + dynamicSourceLineage.activationBlockHash !== undefined && + dynamicSourceLineage.activationBlockGlobalLogIndex !== undefined && + (BigInt(dynamicSourceLineage.activationBlockNumber) < blockNumber || + (BigInt(dynamicSourceLineage.activationBlockNumber) === blockNumber && + dynamicSourceLineage.activationBlockHash === candidate.blockHash && + BigInt(dynamicSourceLineage.activationBlockGlobalLogIndex) < + BigInt(logIndex))); + if ( + dynamicSourceLineage.contractName !== candidate.contractName || + !activationBeforeChild + ) { + throw validationError("rpc", "dynamic-source-lineage-boundary"); + } + sourceKind = "dynamic-attested"; + expectedRuntimeCodeHash = + dynamicSourceLineage.expectedExactRuntimeCodeHash; + } else { + sourceKind = "dynamic-unresolved"; + expectedRuntimeCodeHash = null; + } + } + return { + candidate: { + ...candidate, + blockHash, + transactionHash, + sourceAddress, + orderedTopics: topics, + rawData, + payloadHash, + }, + blockNumber, + timestamp, + sourceKind, + expectedRuntimeCodeHash, + dynamicSourceLineage, + }; +} + +export async function readDualRpcSafeHead(input: { + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + cursor: { blockNumber: string; blockHash: HexBytes32 }; + rpcPolicy?: RpcExecutionPolicyInput; +}): Promise { + assertProductionDualRpcProviders(input.providers); + const firstIdentity = providerIdentity(input.providers?.[0]?.identity); + const secondIdentity = providerIdentity(input.providers?.[1]?.identity); + const firstVendor = providerIdentity(input.providers?.[0]?.vendorGroup); + const secondVendor = providerIdentity(input.providers?.[1]?.vendorGroup); + if ( + firstIdentity === secondIdentity || + firstVendor === secondVendor || + input.providers[0].client === input.providers[1].client + ) { + throw invalidInput("rpc", "provider-independence"); + } + let cursorBlockNumber: bigint; + let expectedCursorHash: HexBytes32; + try { + cursorBlockNumber = BigInt( + parseNonnegativeIntegerText(input.cursor.blockNumber), + ); + expectedCursorHash = canonicalBytes32(input.cursor.blockHash); + } catch { + throw invalidInput("rpc", "safe-head-cursor"); + } + const policy = rpcExecutionPolicy(input.rpcPolicy); + if (4 > policy.maxCallsPerProvider) { + throw invalidInput("rpc", "provider-call-budget"); + } + try { + const budgets = input.providers.map(() => ({ + used: 0, + maximum: policy.maxCallsPerProvider, + })); + const states = await Promise.all( + input.providers.map(async ({ client }, providerIndex) => { + const budget = budgets[providerIndex]!; + const [chainId, head] = await Promise.all([ + retryRpc(() => client.getChainId(), policy, budget), + retryRpc(() => client.getBlockNumber(), policy, budget), + ]); + if ( + chainId !== RELEASE_BINDING.chainId || + typeof head !== "bigint" || + head < BigInt(RELEASE_BINDING.confirmations) + ) { + throw validationError("rpc", "safe-head-state"); + } + return { client, head }; + }), + ); + const lowestHead = + states[0]!.head < states[1]!.head + ? states[0]!.head + : states[1]!.head; + const safeBlockNumber = + lowestHead - BigInt(RELEASE_BINDING.confirmations); + if (cursorBlockNumber > safeBlockNumber) { + throw validationError("rpc", "safe-head-cursor-finality"); + } + const blocks = await Promise.all( + states.map(async ({ client }, providerIndex) => { + const budget = budgets[providerIndex]!; + const [safe, cursor] = await Promise.all([ + retryRpc( + () => client.getBlock({ blockNumber: safeBlockNumber }), + policy, + budget, + ), + safeBlockNumber === cursorBlockNumber + ? retryRpc( + () => client.getBlock({ blockNumber: safeBlockNumber }), + policy, + budget, + ) + : retryRpc( + () => client.getBlock({ blockNumber: cursorBlockNumber }), + policy, + budget, + ), + ]); + return { + safe: canonicalBlock(safe, safeBlockNumber, "safe-head-block"), + cursor: canonicalBlock( + cursor, + cursorBlockNumber, + "safe-head-cursor-block", + ), + }; + }), + ); + if ( + blocks[0]!.safe.hash !== blocks[1]!.safe.hash || + blocks[0]!.safe.timestamp !== blocks[1]!.safe.timestamp || + blocks[0]!.cursor.hash !== blocks[1]!.cursor.hash || + blocks[0]!.cursor.timestamp !== blocks[1]!.cursor.timestamp || + (safeBlockNumber === cursorBlockNumber && + (blocks[0]!.safe.hash !== blocks[0]!.cursor.hash || + blocks[0]!.safe.timestamp !== blocks[0]!.cursor.timestamp || + blocks[1]!.safe.hash !== blocks[1]!.cursor.hash || + blocks[1]!.safe.timestamp !== blocks[1]!.cursor.timestamp)) + ) { + throw validationError("rpc", "safe-head-provider-disagreement"); + } + if (blocks[0]!.cursor.hash !== expectedCursorHash) { + // Both independent providers agree on the canonical block, but the + // durable cursor names another hash. Only this exact shape may enter the + // bounded rewind path; provider disagreement always fails closed. + throw validationError("rpc", "safe-head-cursor-orphaned"); + } + return Object.freeze({ + providerHeads: Object.freeze([ + states[0]!.head.toString(), + states[1]!.head.toString(), + ]) as readonly [string, string], + safeBlockNumber: safeBlockNumber.toString(), + safeBlockHash: blocks[0]!.safe.hash, + cursorBlockHash: blocks[0]!.cursor.hash, + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +function canonicalMetadataText( + value: unknown, + field: "name" | "symbol", +): string { + const maximumBytes = field === "name" ? 128 : 32; + if ( + typeof value !== "string" || + value.normalize("NFC") !== value || + /[\u0000-\u001f\u007f]/u.test(value) || + Buffer.byteLength(value, "utf8") < 1 || + Buffer.byteLength(value, "utf8") > maximumBytes + ) { + throw validationError("rpc", `erc20-${field}`); + } + return value; +} + +/** + * Reads token metadata from the two canonical providers at the exact launch + * block. Provider disagreement is an integrity failure, never a preference + * or a reason to silently use one answer. + */ +export async function readDualRpcTokenMetadata(input: { + tokens: readonly Readonly<{ + token: HexAddress; + blockNumber: string; + blockHash: HexBytes32; + }>[]; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + rpcPolicy?: RpcExecutionPolicyInput; +}): Promise { + assertProductionDualRpcProviders(input.providers); + if (!Array.isArray(input.tokens) || input.tokens.length > 16) { + throw invalidInput("rpc", "erc20-metadata-batch"); + } + const first = input.providers[0]; + const second = input.providers[1]; + providerIdentity(first.identity); + providerIdentity(second.identity); + if ( + first.identity === second.identity || + first.vendorGroup === second.vendorGroup || + first.client === second.client || + typeof first.client.readErc20Metadata !== "function" || + typeof second.client.readErc20Metadata !== "function" + ) { + throw invalidInput("rpc", "erc20-metadata-providers"); + } + const policy = rpcExecutionPolicy(input.rpcPolicy); + if (input.tokens.length * 2 > policy.maxCallsPerProvider) { + throw invalidInput("rpc", "provider-call-budget"); + } + const seen = new Set(); + const providerBudgets = input.providers.map(() => ({ + used: 0, + maximum: policy.maxCallsPerProvider, + })); + try { + return Object.freeze( + await boundedRpcMap( + input.tokens, + policy.maxConcurrency, + async (requested) => { + const token = rpcAddress(requested.token, "erc20-token"); + let blockNumber: bigint; + try { + blockNumber = BigInt( + parseNonnegativeIntegerText(requested.blockNumber), + ); + } catch { + throw invalidInput("rpc", "erc20-block"); + } + const blockHash = rpcBytes32( + requested.blockHash, + "erc20-block-hash", + ); + if (seen.has(token)) { + throw invalidInput("rpc", "erc20-metadata-duplicate"); + } + seen.add(token); + const [left, right] = await Promise.all([ + retryRpc( + () => first.client.readErc20Metadata!({ + address: token, + blockHash, + requireCanonical: true, + }), + policy, + providerBudgets[0], + 2, + ), + retryRpc( + () => second.client.readErc20Metadata!({ + address: token, + blockHash, + requireCanonical: true, + }), + policy, + providerBudgets[1], + 2, + ), + ]); + const leftName = canonicalMetadataText(left.name, "name"); + const rightName = canonicalMetadataText(right.name, "name"); + const leftSymbol = canonicalMetadataText(left.symbol, "symbol"); + const rightSymbol = canonicalMetadataText(right.symbol, "symbol"); + if (leftName !== rightName || leftSymbol !== rightSymbol) { + throw validationError("rpc", "erc20-metadata-agreement"); + } + return Object.freeze({ + token, + blockNumber: blockNumber.toString(), + blockHash, + name: leftName, + symbol: leftSymbol, + }); + }, + ), + ); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +function rewardUint(value: unknown, field: string): string { + try { + return parseUint256Text(value); + } catch { + throw validationError("rpc", `reward-${field}`); + } +} + +function rewardEpoch(value: unknown): string { + const epoch = BigInt(rewardUint(value, "configuration-epoch")); + if (epoch > (1n << 64n) - 1n) { + throw validationError("rpc", "reward-configuration-epoch"); + } + return epoch.toString(); +} + +function rewardArray(value: unknown, field: string): readonly Record[] { + if ( + !Array.isArray(value) || + value.some( + (entry) => + entry === null || + typeof entry !== "object" || + Array.isArray(entry), + ) + ) { + throw validationError("rpc", `reward-${field}`); + } + return value as readonly Record[]; +} + +function canonicalRewardSnapshot( + raw: CandidateRpcRewardSnapshot, + request: Readonly<{ + model: ProjectorRewardRpcModel; + vault: HexAddress; + blockNumber: string; + blockHash: HexBytes32; + balanceAccounts: readonly HexAddress[]; + }>, +): Omit< + DualRpcRewardSnapshot, + | "verificationAccounts" + | "providerIdentities" + | "providerVendorGroups" + | "providerEndpointCommitments" + | "providerOriginCommitments" + | "providerCallCounts" + | "providerSnapshotCommitments" + | "chunks" + | "executionTrace" +> { + if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { + throw validationError("rpc", "reward-snapshot"); + } + const contract = PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1.models[request.model]; + const model = raw.model; + const vault = rpcAddress(raw.vault, "reward-vault"); + const blockNumber = rewardUint(raw.blockNumber, "block-number"); + const blockHash = rpcBytes32(raw.blockHash, "reward-block-hash"); + const poolId = rpcBytes32(raw.poolId, "reward-pool-id"); + const configurationEpoch = request.model === "classic-v3" + ? rewardEpoch(raw.configurationEpoch) + : raw.configurationEpoch === null + ? null + : (() => { + throw validationError("rpc", "reward-configuration-epoch"); + })(); + const configurationHash = rpcBytes32( + raw.configurationHash, + "reward-configuration-hash", + ); + const totalCreatorFeesReceived = rewardUint( + raw.totalCreatorFeesReceived, + "total-received", + ); + const totalCreatorFeesClaimed = rewardUint( + raw.totalCreatorFeesClaimed, + "total-claimed", + ); + const beneficiaryCountText = rewardUint( + raw.beneficiaryCount, + "beneficiary-count", + ); + const beneficiaryCount = Number(beneficiaryCountText); + const allocations = rewardArray(raw.allocations, "allocations").map( + (allocation, allocationIndex) => { + const receivedIndex = typeof allocation.allocationIndex === "number" + ? allocation.allocationIndex + : Number(rewardUint(allocation.allocationIndex, "allocation-index")); + if ( + !Number.isSafeInteger(receivedIndex) || + receivedIndex !== allocationIndex + ) { + throw validationError("rpc", "reward-allocation-index"); + } + const beneficiary = rpcAddress( + allocation.beneficiary, + "reward-beneficiary", + ); + const payoutAddress = rpcAddress( + allocation.payoutAddress, + "reward-payout-address", + ); + const shareBps = rewardUint(allocation.shareBps, "share-bps"); + if ( + BigInt(shareBps) < 1n || + BigInt(shareBps) > 10_000n || + (request.model === "classic-v3" && beneficiary !== payoutAddress) + ) { + throw validationError("rpc", "reward-allocation"); + } + return Object.freeze({ + allocationIndex, + beneficiary, + payoutAddress, + shareBps, + }); + }, + ); + if ( + model !== request.model || + vault !== request.vault || + blockNumber !== request.blockNumber || + blockHash !== request.blockHash || + !Number.isSafeInteger(beneficiaryCount) || + beneficiaryCount < 1 || + beneficiaryCount > contract.maximumAllocations || + allocations.length !== beneficiaryCount || + allocations.reduce((sum, { shareBps }) => sum + BigInt(shareBps), 0n) !== + 10_000n || + (request.model === "stock-paired" && + new Set(allocations.map(({ beneficiary }) => beneficiary)).size !== + allocations.length) + ) { + throw validationError("rpc", "reward-snapshot-header"); + } + const balances = rewardArray(raw.balances, "balances").map( + (balance, index) => { + const account = rpcAddress(balance.account, "reward-account"); + const payoutAddress = rpcAddress( + balance.payoutAddress, + "reward-balance-payout", + ); + if ( + account !== request.balanceAccounts[index] || + (request.model === "classic-v3" && payoutAddress !== account) + ) { + throw validationError("rpc", "reward-balance-account"); + } + return Object.freeze({ + account, + payoutAddress, + claimableAccrued: rewardUint( + balance.claimableAccrued, + "claimable", + ), + claimedTotal: rewardUint(balance.claimedTotal, "claimed"), + }); + }, + ); + const expectedRpcCallCount = expectedRewardRpcCallCount( + request.model, + allocations.length, + balances.length, + ); + if ( + balances.length !== request.balanceAccounts.length || + request.balanceAccounts.length > contract.maximumBalanceAccounts || + request.balanceAccounts.some( + (account, index) => index > 0 && account <= request.balanceAccounts[index - 1]!, + ) || + (request.model === "stock-paired" && + (balances.length !== allocations.length || + balances.some( + ({ account }) => + !allocations.some(({ beneficiary }) => beneficiary === account), + ))) || + typeof raw.rpcCallCount !== "number" || + !Number.isSafeInteger(raw.rpcCallCount) || + raw.rpcCallCount !== expectedRpcCallCount + ) { + throw validationError("rpc", "reward-snapshot-conservation"); + } + return Object.freeze({ + model: request.model, + vault, + blockNumber, + blockHash, + poolId, + configurationEpoch, + configurationHash, + totalCreatorFeesReceived, + totalCreatorFeesClaimed, + allocations: Object.freeze(allocations), + balances: Object.freeze(balances), + rpcCallCount: expectedRpcCallCount, + }); +} + +function assertRewardSnapshotMatchesProjection( + snapshot: Omit< + DualRpcRewardSnapshot, + | "verificationAccounts" + | "providerIdentities" + | "providerVendorGroups" + | "providerEndpointCommitments" + | "providerOriginCommitments" + | "providerCallCounts" + | "providerSnapshotCommitments" + | "chunks" + | "executionTrace" + >, + expected: ProjectorRewardSnapshot, + verificationAccounts: readonly HexAddress[], +): void { + const expectedConfigurationHash = expected.activeConfigurationHash; + const expectedClaimed = expected.balances.reduce( + (sum, { claimedTotal }) => sum + BigInt(claimedTotal), + 0n, + ).toString(); + if ( + expectedConfigurationHash === null || + snapshot.vault !== expected.vault || + snapshot.poolId !== expected.poolId || + snapshot.configurationHash !== expectedConfigurationHash || + snapshot.totalCreatorFeesReceived !== expected.totalCreatorFeesReceived || + snapshot.totalCreatorFeesClaimed !== expectedClaimed || + (snapshot.model === "classic-v3" && + snapshot.configurationEpoch !== expected.configurationEpoch) || + JSON.stringify(snapshot.allocations) !== + JSON.stringify(expected.allocations) || + JSON.stringify(snapshot.balances) !== + JSON.stringify( + expected.balances.filter(({ account }) => + verificationAccounts.includes(account) + ), + ) + ) { + throw validationError("rpc", "reward-projection-agreement"); + } +} + +function canonicalClassicRewardFactorySnapshot( + raw: CandidateRpcClassicRewardFactorySnapshot, + expected: Readonly<{ + factory: HexAddress; + vault: HexAddress; + blockNumber: string; + blockHash: HexBytes32; + }>, +) { + if (raw === null || typeof raw !== "object") { + throw validationError("rpc", "reward-factory-snapshot"); + } + let blockNumber: string; + try { + blockNumber = parseNonnegativeIntegerText(raw.blockNumber); + } catch { + throw validationError("rpc", "reward-factory-snapshot"); + } + const factory = rpcAddress(raw.factory, "reward-factory-address"); + const vault = rpcAddress(raw.vault, "reward-factory-vault"); + const blockHash = rpcBytes32(raw.blockHash, "reward-factory-block-hash"); + const configurationHash = rpcBytes32( + raw.configurationHash, + "reward-factory-configuration-hash", + ); + const ctoAuthority = rpcAddress( + raw.ctoAuthority, + "reward-factory-cto-authority", + ); + const initCodeHash = rpcBytes32( + raw.initCodeHash, + "reward-factory-init-code-hash", + ); + const predictedVault = rpcAddress( + raw.predictedVault, + "reward-factory-predicted-vault", + ); + if ( + factory !== expected.factory || + vault !== expected.vault || + blockNumber !== expected.blockNumber || + blockHash !== expected.blockHash || + typeof raw.rpcCallCount !== "number" || + raw.rpcCallCount !== 4 + ) { + throw validationError("rpc", "reward-factory-snapshot"); + } + return Object.freeze({ + factory, + vault, + blockNumber, + blockHash, + configurationHash, + ctoAuthority, + initCodeHash, + predictedVault, + rpcCallCount: 4 as const, + }); +} + +/** + * Proves one folded reward delta against two independent exact-block vault + * snapshots. Any provider, call-count, conservation or projected-state + * disagreement rejects the complete projection transaction. + */ +export async function readDualRpcRewardSnapshot(input: Readonly<{ + model: ProjectorRewardRpcModel; + expected: ProjectorRewardSnapshot; + baseline?: ProjectorRewardBaseline; + blockNumber: string; + blockHash: HexBytes32; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + rpcPolicy?: RpcExecutionPolicyInput; +}>): Promise { + assertProductionDualRpcProviders(input.providers); + const first = input.providers[0]; + const second = input.providers[1]; + providerIdentity(first.identity); + providerIdentity(second.identity); + if ( + first.identity === second.identity || + first.vendorGroup === second.vendorGroup || + first.client === second.client || + typeof first.client.readRewardSnapshot !== "function" || + typeof second.client.readRewardSnapshot !== "function" || + (input.rpcPolicy?.maxAttempts !== undefined && + input.rpcPolicy.maxAttempts !== 1) + ) { + throw invalidInput("rpc", "reward-snapshot-providers"); + } + let blockNumber: bigint; + try { + blockNumber = BigInt(parseNonnegativeIntegerText(input.blockNumber)); + } catch { + throw invalidInput("rpc", "reward-snapshot-block"); + } + const vault = rpcAddress(input.expected.vault, "reward-vault"); + const expectedBalances = new Map( + input.expected.balances.map((balance) => [balance.account, balance]), + ); + const baselineBalances = new Map( + input.baseline?.balances.map((balance) => [balance.account, balance]) ?? [], + ); + if ( + input.baseline !== undefined && + (input.baseline.vault !== input.expected.vault || + input.baseline.poolId !== input.expected.poolId) + ) { + throw invalidInput("rpc", "reward-baseline"); + } + const balanceAccounts = Object.freeze( + [...new Set([ + ...input.expected.allocations.map(({ beneficiary }) => beneficiary), + ...input.expected.balances + .filter((balance) => + input.baseline === undefined || + JSON.stringify(balance) !== + JSON.stringify(baselineBalances.get(balance.account)) + ) + .map(({ account }) => account), + ])] + .map((account) => rpcAddress(account, "reward-balance-account")) + .sort(), + ); + const maximum = + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1.models[input.model] + .maximumBalanceAccounts; + if ( + balanceAccounts.length < 1 || + balanceAccounts.length > PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + balanceAccounts.some( + (account, index) => index > 0 && account <= balanceAccounts[index - 1]!, + ) + ) { + throw invalidInput("rpc", "reward-balance-accounts"); + } + const verificationChunks: readonly (readonly HexAddress[])[] = Object.freeze( + Array.from( + { length: Math.ceil(balanceAccounts.length / maximum) }, + (_value, chunkIndex) => Object.freeze( + balanceAccounts.slice( + chunkIndex * maximum, + (chunkIndex + 1) * maximum, + ), + ), + ), + ); + if ( + verificationChunks.length < 1 || + verificationChunks.length > 86 || + verificationChunks.some((chunk) => + chunk.length < 1 || + chunk.length > maximum || + chunk.some( + (account, index) => index > 0 && account <= chunk[index - 1]!, + ) + ) + ) { + throw invalidInput("rpc", "reward-verification-chunks"); + } + const expectedChunkCallCounts = verificationChunks.map((chunk) => + expectedRewardRpcCallCount( + input.model, + input.expected.allocations.length, + chunk.length, + ) + ); + const policy = rpcExecutionPolicy({ + ...input.rpcPolicy, + maxAttempts: 1, + }); + if (expectedChunkCallCounts.some((count) => + count > policy.maxCallsPerProvider + )) { + throw invalidInput("rpc", "provider-call-budget"); + } + const expectedTotalCallCount = expectedChunkCallCounts.reduce( + (sum, count) => sum + count, + 0, + ); + if ( + !Number.isSafeInteger(expectedTotalCallCount) || + expectedTotalCallCount < 1 || + expectedTotalCallCount > 65_535 + ) { + throw invalidInput("rpc", "reward-total-call-budget"); + } + const blockHash = rpcBytes32(input.blockHash, "reward-block-hash"); + const startedAtMs = Date.now(); + try { + const rewardBudgets = input.providers.map(() => ({ + used: 0, + maximum: expectedTotalCallCount, + })); + const chunkEvidence: Array = []; + const providerTraceCalls: [DualRpcCallTrace[], DualRpcCallTrace[]] = [ + [], + [], + ]; + const mergedBalances = new Map(); + let canonicalHeader: string | null = null; + let canonicalSnapshot: ReturnType | null = + null; + for ( + let chunkIndex = 0; + chunkIndex < verificationChunks.length; + chunkIndex += 1 + ) { + const chunkAccounts = verificationChunks[chunkIndex]!; + const expectedChunkCallCount = expectedChunkCallCounts[chunkIndex]!; + const request = Object.freeze({ + model: input.model, + vault, + blockNumber, + blockHash, + balanceAccounts: chunkAccounts, + }); + const chunkStartedAtMs = Date.now(); + const [leftRaw, rightRaw] = await Promise.all([ + retryRpc( + () => first.client.readRewardSnapshot!(request), + policy, + rewardBudgets[0], + expectedChunkCallCount, + ), + retryRpc( + () => second.client.readRewardSnapshot!(request), + policy, + rewardBudgets[1], + expectedChunkCallCount, + ), + ]); + const chunkCompletedAtMs = Date.now(); + const canonicalRequest = Object.freeze({ + model: input.model, + vault, + blockNumber: blockNumber.toString(), + blockHash, + balanceAccounts: chunkAccounts, + }); + const left = canonicalRewardSnapshot(leftRaw, canonicalRequest); + const right = canonicalRewardSnapshot(rightRaw, canonicalRequest); + if (JSON.stringify(left) !== JSON.stringify(right)) { + throw validationError("rpc", "reward-provider-agreement"); + } + const header = JSON.stringify({ + model: left.model, + vault: left.vault, + blockNumber: left.blockNumber, + blockHash: left.blockHash, + poolId: left.poolId, + configurationEpoch: left.configurationEpoch, + configurationHash: left.configurationHash, + totalCreatorFeesReceived: left.totalCreatorFeesReceived, + totalCreatorFeesClaimed: left.totalCreatorFeesClaimed, + allocations: left.allocations, + }); + if (canonicalHeader !== null && header !== canonicalHeader) { + throw validationError("rpc", "reward-chunk-header-agreement"); + } + canonicalHeader = header; + canonicalSnapshot ??= left; + for (const balance of left.balances) { + const existing = mergedBalances.get(balance.account); + if (existing && JSON.stringify(existing) !== JSON.stringify(balance)) { + throw validationError("rpc", "reward-chunk-balance-agreement"); + } + mergedBalances.set(balance.account, balance); + } + const providerSnapshotCommitments = [ + keccak256(toBytes(JSON.stringify(left))), + keccak256(toBytes(JSON.stringify(right))), + ] as const; + chunkEvidence.push(Object.freeze({ + chunkIndex, + verificationAccounts: chunkAccounts, + providerCallCounts: [left.rpcCallCount, right.rpcCallCount] as const, + providerSnapshotCommitments, + })); + input.providers.forEach((provider, providerIndex) => { + providerTraceCalls[providerIndex]!.push(Object.freeze({ + providerIdentity: provider.identity, + providerVendorGroup: provider.vendorGroup, + providerEndpointCommitment: provider.endpointCommitment, + providerOriginCommitment: provider.endpointOriginCommitment, + operation: "readRewardSnapshot" as const, + attempt: 1, + startedOffsetMs: Math.max(0, chunkStartedAtMs - startedAtMs), + durationMs: Math.max(0, chunkCompletedAtMs - chunkStartedAtMs), + outcome: "success" as const, + })); + }); + } + if (!canonicalSnapshot) { + throw validationError("rpc", "reward-empty-chunk-set"); + } + const mergedSnapshot = Object.freeze({ + ...canonicalSnapshot, + balances: Object.freeze(balanceAccounts.map((account) => { + const balance = mergedBalances.get(account); + if (!balance) { + throw validationError("rpc", "reward-chunk-account-coverage"); + } + return balance; + })), + rpcCallCount: expectedTotalCallCount, + }); + assertRewardSnapshotMatchesProjection( + mergedSnapshot, + input.expected, + balanceAccounts, + ); + const expectedReceived = input.expected.balances.reduce( + (sum, { claimableAccrued, claimedTotal }) => + sum + BigInt(claimableAccrued) + BigInt(claimedTotal), + 0n, + ).toString(); + const expectedClaimed = input.expected.balances.reduce( + (sum, { claimedTotal }) => sum + BigInt(claimedTotal), + 0n, + ).toString(); + if ( + expectedReceived !== input.expected.totalCreatorFeesReceived || + expectedClaimed !== mergedSnapshot.totalCreatorFeesClaimed || + balanceAccounts.some((account) => !expectedBalances.has(account)) + ) { + throw validationError("rpc", "reward-projection-conservation"); + } + const completedAtMs = Date.now(); + const providerCallCounts = [ + rewardBudgets[0]!.used, + rewardBudgets[1]!.used, + ] as const; + const providerSnapshotCommitments = [ + keccak256(toBytes(JSON.stringify( + chunkEvidence.map((chunk) => [ + chunk.chunkIndex, + chunk.verificationAccounts, + chunk.providerCallCounts[0], + chunk.providerSnapshotCommitments[0], + ]), + ))), + keccak256(toBytes(JSON.stringify( + chunkEvidence.map((chunk) => [ + chunk.chunkIndex, + chunk.verificationAccounts, + chunk.providerCallCounts[1], + chunk.providerSnapshotCommitments[1], + ]), + ))), + ] as const; + if ( + providerSnapshotCommitments[0] !== providerSnapshotCommitments[1] || + providerCallCounts[0] !== expectedTotalCallCount || + providerCallCounts[1] !== expectedTotalCallCount + ) { + throw validationError("rpc", "reward-chunk-aggregate-agreement"); + } + return Object.freeze({ + ...mergedSnapshot, + balances: Object.freeze([...input.expected.balances]), + rpcCallCount: providerCallCounts[0] + providerCallCounts[1], + verificationAccounts: balanceAccounts, + providerIdentities: [first.identity, second.identity] as const, + providerVendorGroups: [first.vendorGroup, second.vendorGroup] as const, + providerEndpointCommitments: [ + first.endpointCommitment, + second.endpointCommitment, + ] as const, + providerOriginCommitments: [ + first.endpointOriginCommitment, + second.endpointOriginCommitment, + ] as const, + providerCallCounts, + providerSnapshotCommitments, + chunks: Object.freeze(chunkEvidence), + executionTrace: Object.freeze({ + startedAtMs, + completedAtMs, + candidateBatchSize: 0, + hardDeadlineMs: policy.hardDeadlineMs, + maxCallsPerProvider: policy.maxCallsPerProvider, + elapsedMs: Math.max(0, completedAtMs - startedAtMs), + providerCallCounts, + calls: Object.freeze([ + ...providerTraceCalls[0], + ...providerTraceCalls[1], + ]), + }), + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +function classicActiveConfigurationHash(input: { + vault: HexAddress; + factoryConfigurationHash: HexBytes32; + configurationEpoch: string; + beneficiaries: readonly HexAddress[]; + sharesBps: readonly string[]; +}): HexBytes32 { + return keccak256( + encodeAbiParameters( + [ + { type: "uint256" }, + { type: "address" }, + { type: "bytes32" }, + { type: "uint64" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [ + 1n, + input.vault, + input.factoryConfigurationHash, + BigInt(input.configurationEpoch), + [...input.beneficiaries], + input.sharesBps.map(Number), + ], + ), + ); +} + +function sameOrderedTuple( + actual: readonly string[], + expected: readonly string[], +): boolean { + return ( + actual.length === expected.length && + actual.every((value, index) => value === expected[index]) + ); +} + +/** + * Binds a historical factory candidate to the canonical deployment row that + * survived the current reorg generation. The local ABI decode is intentional: + * neither a raw Envio object nor a matching candidate id is sufficient proof + * of the parent payload used to derive the vault configuration. + */ +function assertCanonicalDynamicDeploymentBinding(input: Readonly<{ + parent: EnvioCandidate; + launch: EnvioCandidate; + sourceAddress: HexAddress; + template: ProjectorDynamicSourceTemplate; + canonicalDeployment: CanonicalDynamicSourceDeploymentEvidence; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; +}>): void { + const { + parent, + launch, + sourceAddress, + template, + canonicalDeployment, + } = input; + const candidateMatch = CANDIDATE_ID_PATTERN.exec(parent.candidateId); + const providerIdentities = input.providers.map(({ identity }) => + providerIdentity(identity) + ); + const providerVendorGroups = input.providers.map(({ vendorGroup }) => + providerIdentity(vendorGroup) + ); + const providerEndpointCommitments = input.providers.map( + ({ endpointCommitment }) => + rpcBytes32(endpointCommitment, "dynamic-deployment-provider-endpoint"), + ); + const providerOriginCommitments = input.providers.map( + ({ endpointOriginCommitment }) => + rpcBytes32(endpointOriginCommitment, "dynamic-deployment-provider-origin"), + ); + let localPayloadHash: HexBytes32; + let localRawLogCommitment: HexBytes32; + try { + decodeManifestEvent({ + contractName: parent.contractName, + eventName: parent.eventName, + topics: parent.orderedTopics, + data: parent.rawData, + providerPayload: parent.decodedPayload, + }); + localPayloadHash = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [parent.orderedTopics, parent.rawData], + ), + ); + localRawLogCommitment = keccak256( + encodeAbiParameters( + [{ type: "address" }, { type: "bytes32[]" }, { type: "bytes" }], + [parent.sourceAddress, parent.orderedTopics, parent.rawData], + ), + ); + } catch { + throw validationError("rpc", "dynamic-deployment-parent-decode"); + } + const parentBeforeLaunch = + BigInt(parent.blockNumber) < BigInt(launch.blockNumber) || + (parent.blockNumber === launch.blockNumber && + parent.blockHash === launch.blockHash && + parent.blockGlobalLogIndex < launch.blockGlobalLogIndex); + if ( + !candidateMatch || + !parentBeforeLaunch || + !UUID_PATTERN.test(canonicalDeployment.provisionalPageId) || + !UUID_PATTERN.test(canonicalDeployment.provisionalLineageId) || + !UUID_PATTERN.test(canonicalDeployment.dynamicSourceAttestationId) || + !UUID_PATTERN.test(canonicalDeployment.runtimeCodeEvidenceId) || + !UUID_PATTERN.test(canonicalDeployment.dynamicSourceTemplateId) || + !UUID_PATTERN.test(canonicalDeployment.parentOccurrenceId) || + !UUID_PATTERN.test(canonicalDeployment.canonicalStatusHistoryId) || + !UUID_PATTERN.test(canonicalDeployment.safeHeadObservationId) || + !UUID_PATTERN.test(canonicalDeployment.blockEvidenceId) || + canonicalDeployment.parentCandidateId !== parent.candidateId || + canonicalDeployment.parentBlockNumber !== parent.blockNumber || + canonicalDeployment.parentBlockHash !== parent.blockHash || + canonicalDeployment.parentBlockGlobalLogIndex !== + parent.blockGlobalLogIndex || + canonicalDeployment.parentTransactionHash !== parent.transactionHash || + canonicalDeployment.parentTransactionIndex !== parent.transactionIndex || + canonicalDeployment.parentSourceAddress !== parent.sourceAddress || + sourceAddress !== + rpcAddress(parent.decodedPayload.vault, "dynamic-deployment-source") || + canonicalDeployment.parentContractName !== parent.contractName || + canonicalDeployment.parentEventName !== parent.eventName || + canonicalDeployment.parentPayloadHash !== parent.payloadHash || + canonicalDeployment.parentPayloadHash !== localPayloadHash || + canonicalDeployment.parentRawLogCommitment !== localRawLogCommitment || + canonicalDeployment.dynamicSourceTemplateId !== template.templateId || + canonicalDeployment.parentSourceAddress !== + template.parentFactoryAddress || + canonicalDeployment.parentContractName !== + template.parentFactoryContractName || + canonicalDeployment.parentEventName !== template.factoryEventName || + canonicalDeployment.reorgGeneration !== + template.database.reorgGeneration || + canonicalDeployment.envioProviderDeploymentId !== + template.database.envioProviderDeploymentId || + !sameOrderedTuple( + canonicalDeployment.rpcProviderDeploymentIds, + template.database.rpcProviderDeploymentIds, + ) || + !sameOrderedTuple( + canonicalDeployment.providerIdentities, + providerIdentities, + ) || + !sameOrderedTuple( + canonicalDeployment.providerVendorGroups, + providerVendorGroups, + ) || + !sameOrderedTuple( + canonicalDeployment.providerEndpointCommitments, + providerEndpointCommitments, + ) || + !sameOrderedTuple( + canonicalDeployment.providerOriginCommitments, + providerOriginCommitments, + ) || + candidateMatch[1] !== parent.blockHash || + candidateMatch[2] !== parent.transactionHash || + BigInt(candidateMatch[3]!) !== BigInt(parent.blockGlobalLogIndex) + ) { + throw validationError("rpc", "dynamic-deployment-canonical-binding"); + } +} + +/** + * Reads a launch-bound Classic reward vault at the exact launch block from two + * independent providers and reconstructs its activation state. Only vault + * events strictly after the launch log may be reversed. A CTO allocation + * replacement in that block is deliberately unsupported and fails closed. + */ +export async function readDualRpcInitialRewardConfiguration(input: Readonly<{ + parentCandidate: EnvioCandidate; + launchCandidate: EnvioCandidate; + sameBlockVaultEvents: readonly EnvioCandidate[]; + candidateEvidence: DualRpcCandidateWindowEvidence; + canonicalDeployment: CanonicalDynamicSourceDeploymentEvidence; + template: ProjectorDynamicSourceTemplate; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + rpcPolicy?: RpcExecutionPolicyInput; +}>): Promise { + assertProductionDualRpcProviders(input.providers); + const template = canonicalDynamicSourceTemplate(input.template); + const parent = input.parentCandidate; + const launch = input.launchCandidate; + const vault = rpcAddress(parent.decodedPayload.vault, "reward-seed-vault"); + const poolId = rpcBytes32(parent.decodedPayload.poolId, "reward-seed-pool"); + const feeHook = rpcAddress( + parent.decodedPayload.feeHook, + "reward-seed-fee-hook", + ); + const factoryConfigurationHash = rpcBytes32( + parent.decodedPayload.configurationHash, + "reward-seed-configuration", + ); + const deploymentBlockHash = rpcBytes32( + parent.blockHash, + "reward-seed-block", + ); + let deploymentBlockNumber: string; + try { + deploymentBlockNumber = parseNonnegativeIntegerText(parent.blockNumber); + } catch { + throw invalidInput("rpc", "reward-seed-block"); + } + const activationBlockHash = rpcBytes32( + launch.blockHash, + "reward-seed-activation-block", + ); + let activationBlockNumber: string; + try { + activationBlockNumber = parseNonnegativeIntegerText(launch.blockNumber); + } catch { + throw invalidInput("rpc", "reward-seed-activation-block"); + } + const launchVault = rpcAddress( + launch.decodedPayload.rewardVault, + "reward-seed-launch-vault", + ); + const launchPoolId = rpcBytes32( + launch.decodedPayload.poolId, + "reward-seed-launch-pool", + ); + const launchFeeHook = rpcAddress( + launch.decodedPayload.feeHook, + "reward-seed-launch-hook", + ); + const launchConfigurationHash = rpcBytes32( + launch.decodedPayload.rewardConfigurationHash, + "reward-seed-launch-configuration", + ); + const factory = rpcAddress( + parent.sourceAddress, + "reward-seed-factory", + ); + const salt = rpcBytes32( + parent.decodedPayload.salt, + "reward-seed-salt", + ); + const token = rpcAddress( + launch.decodedPayload.token, + "reward-seed-token", + ); + const deployer = rpcAddress( + launch.decodedPayload.deployer, + "reward-seed-deployer", + ); + const expectedSalt = keccak256( + encodeAbiParameters( + [{ type: "string" }, { type: "address" }, { type: "address" }], + ["programmable.classic-reward-vault.v1", token, deployer], + ), + ); + assertCanonicalDynamicDeploymentBinding({ + parent, + launch, + sourceAddress: vault, + template, + canonicalDeployment: input.canonicalDeployment, + providers: input.providers, + }); + const providerIdentities = input.providers.map(({ identity }) => + providerIdentity(identity) + ) as [string, string]; + const providerVendorGroups = input.providers.map(({ vendorGroup }) => + providerIdentity(vendorGroup) + ) as [string, string]; + const providerEndpointCommitments = input.providers.map( + ({ endpointCommitment }) => + rpcBytes32(endpointCommitment, "reward-seed-provider-endpoint"), + ) as [HexBytes32, HexBytes32]; + const providerOriginCommitments = input.providers.map( + ({ endpointOriginCommitment }) => + rpcBytes32(endpointOriginCommitment, "reward-seed-provider-origin"), + ) as [HexBytes32, HexBytes32]; + const exactTuple = ( + actual: readonly string[], + expected: readonly string[], + ) => + actual.length === expected.length && + actual.every((value, index) => value === expected[index]); + if ( + parent.chainId !== 1 || + parent.contractName !== "ClassicV3RewardVaultFactory" || + parent.eventName !== "ClassicRewardVaultDeployed" || + launch.chainId !== 1 || + launch.contractName !== "ClassicV3Launcher" || + launch.eventName !== "MemeTokenLaunchedV2" || + BigInt(activationBlockNumber) < BigInt(deploymentBlockNumber) || + launchVault !== vault || + launchPoolId !== poolId || + launchFeeHook !== feeHook || + launchConfigurationHash !== factoryConfigurationHash || + salt !== expectedSalt || + input.providers[0].identity === input.providers[1].identity || + input.providers[0].vendorGroup === input.providers[1].vendorGroup || + providerEndpointCommitments[0] === providerEndpointCommitments[1] || + providerOriginCommitments[0] === providerOriginCommitments[1] || + input.providers[0].client === input.providers[1].client || + input.providers.some( + ({ client }) => + typeof client.readRewardSnapshot !== "function" || + typeof client.readClassicRewardFactorySnapshot !== "function", + ) || + !exactTuple( + input.candidateEvidence.providerIdentities, + providerIdentities, + ) || + !exactTuple( + input.candidateEvidence.providerVendorGroups, + providerVendorGroups, + ) || + !exactTuple( + input.candidateEvidence.providerEndpointCommitments, + providerEndpointCommitments, + ) || + !exactTuple( + input.candidateEvidence.providerOriginCommitments, + providerOriginCommitments, + ) || + input.candidateEvidence.candidates.some( + (candidate) => + !exactTuple(candidate.providerIdentities, providerIdentities) || + !exactTuple(candidate.providerVendorGroups, providerVendorGroups) || + !exactTuple( + candidate.providerEndpointCommitments, + providerEndpointCommitments, + ) || + !exactTuple( + candidate.providerOriginCommitments, + providerOriginCommitments, + ), + ) + ) { + throw invalidInput("rpc", "reward-seed-parent"); + } + const orderedEvents = [...input.sameBlockVaultEvents].sort( + (left, right) => left.blockGlobalLogIndex - right.blockGlobalLogIndex, + ); + const evidencedLaunch = input.candidateEvidence.candidates.filter( + ({ candidateId }) => candidateId === launch.candidateId, + ); + const evidencedVaultEvents = input.candidateEvidence.candidates + .filter( + (event) => + event.sourceAddress === vault && + event.contractName === "ClassicV3RewardVault" && + event.candidateBlockNumber === activationBlockNumber && + event.candidateBlockHash === activationBlockHash, + ); + const coveredRewardCandidateIds = orderedEvents.map( + ({ candidateId }) => candidateId, + ); + if ( + evidencedLaunch.length !== 1 || + evidencedLaunch[0]!.candidateBlockNumber !== activationBlockNumber || + evidencedLaunch[0]!.candidateBlockHash !== activationBlockHash || + evidencedLaunch[0]!.sourceAddress !== launch.sourceAddress || + evidencedLaunch[0]!.eventName !== launch.eventName || + evidencedLaunch[0]!.contractName !== launch.contractName || + evidencedVaultEvents.length !== orderedEvents.length || + evidencedVaultEvents.some( + ({ candidateId }, index) => + candidateId !== orderedEvents[index]!.candidateId, + ) || + new Set(coveredRewardCandidateIds).size !== coveredRewardCandidateIds.length || + orderedEvents.some( + (event, index) => + event.chainId !== 1 || + event.blockNumber !== activationBlockNumber || + event.blockHash !== activationBlockHash || + event.sourceAddress !== vault || + event.contractName !== "ClassicV3RewardVault" || + event.blockGlobalLogIndex <= launch.blockGlobalLogIndex || + (index > 0 && + event.blockGlobalLogIndex <= + orderedEvents[index - 1]!.blockGlobalLogIndex) || + ![ + "CreatorFeesCheckpointed", + "BeneficiaryFeesClaimed", + "PayoutWalletChanged", + "CtoRewardConfigurationActivated", + ].includes(event.eventName), + ) || + orderedEvents.some((event) => { + if (event.eventName === "BeneficiaryFeesClaimed") return false; + try { + return rpcBytes32( + event.decodedPayload.poolId, + "reward-seed-event-pool", + ) !== poolId; + } catch { + return true; + } + }) || + orderedEvents.some( + ({ eventName }) => eventName === "CtoRewardConfigurationActivated", + ) + ) { + throw validationError("rpc", "reward-seed-same-block-events"); + } + const policy = rpcExecutionPolicy({ + ...input.rpcPolicy, + maxAttempts: 1, + }); + const maximumCallCount = + expectedRewardRpcCallCount("classic-v3", 5, 1) + 3; + if (maximumCallCount > policy.maxCallsPerProvider) { + throw invalidInput("rpc", "provider-call-budget"); + } + const request = Object.freeze({ + model: "classic-v3" as const, + vault, + blockNumber: BigInt(activationBlockNumber), + blockHash: activationBlockHash, + balanceAccounts: Object.freeze([vault]), + }); + const startedAtMs = Date.now(); + try { + const callStartedAtMs = Date.now(); + const [leftRaw, rightRaw] = await Promise.all([ + retryRpc( + () => input.providers[0].client.readRewardSnapshot!(request), + policy, + ), + retryRpc( + () => input.providers[1].client.readRewardSnapshot!(request), + policy, + ), + ]); + const completedAtMs = Date.now(); + const canonicalRequest = Object.freeze({ + model: "classic-v3" as const, + vault, + blockNumber: activationBlockNumber, + blockHash: activationBlockHash, + balanceAccounts: Object.freeze([vault]), + }); + const left = canonicalRewardSnapshot(leftRaw, canonicalRequest); + const right = canonicalRewardSnapshot(rightRaw, canonicalRequest); + if ( + JSON.stringify(left) !== JSON.stringify(right) || + left.poolId !== poolId || + left.configurationEpoch === null || + left.rpcCallCount > policy.maxCallsPerProvider + ) { + throw validationError("rpc", "reward-seed-provider-agreement"); + } + const snapshotCommitment = keccak256(toBytes(JSON.stringify(left))); + const providerCallCounts = [left.rpcCallCount, right.rpcCallCount] as const; + const traceCalls = input.providers.map((provider) => + Object.freeze({ + providerIdentity: provider.identity, + providerVendorGroup: provider.vendorGroup, + providerEndpointCommitment: provider.endpointCommitment, + providerOriginCommitment: provider.endpointOriginCommitment, + operation: "readRewardSnapshot" as const, + attempt: 1, + startedOffsetMs: Math.max(0, callStartedAtMs - startedAtMs), + durationMs: Math.max(0, completedAtMs - callStartedAtMs), + outcome: "success" as const, + }), + ); + const endConfigurationSnapshot: DualRpcRewardSnapshot = Object.freeze({ + ...left, + rpcCallCount: left.rpcCallCount + right.rpcCallCount, + verificationAccounts: Object.freeze([vault]), + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerCallCounts, + providerSnapshotCommitments: [ + snapshotCommitment, + snapshotCommitment, + ] as const, + chunks: Object.freeze([ + Object.freeze({ + chunkIndex: 0, + verificationAccounts: Object.freeze([vault]), + providerCallCounts, + providerSnapshotCommitments: [ + snapshotCommitment, + snapshotCommitment, + ] as const, + }), + ]), + executionTrace: Object.freeze({ + startedAtMs, + completedAtMs, + candidateBatchSize: 0, + hardDeadlineMs: policy.hardDeadlineMs, + maxCallsPerProvider: policy.maxCallsPerProvider, + elapsedMs: Math.max(0, completedAtMs - startedAtMs), + providerCallCounts, + calls: Object.freeze(traceCalls), + }), + }); + let epoch = BigInt(left.configurationEpoch); + const beneficiaries = left.allocations.map(({ beneficiary }) => beneficiary); + const sharesBps = left.allocations.map(({ shareBps }) => shareBps); + if ( + classicActiveConfigurationHash({ + vault, + factoryConfigurationHash, + configurationEpoch: epoch.toString(), + beneficiaries, + sharesBps, + }) !== left.configurationHash + ) { + throw validationError("rpc", "reward-seed-active-configuration"); + } + for (const event of [...orderedEvents].reverse()) { + if (event.eventName !== "PayoutWalletChanged") continue; + const values = event.decodedPayload; + let allocationIndex: number; + let eventEpoch: bigint; + try { + allocationIndex = Number(parseUint256Text(values.allocationIndex)); + eventEpoch = BigInt(parseNonnegativeIntegerText(values.configurationEpoch)); + } catch { + throw validationError("rpc", "reward-seed-payout-event"); + } + const previous = rpcAddress( + values.previousPayoutWallet, + "reward-seed-previous-payout", + ); + const next = rpcAddress( + values.newPayoutWallet, + "reward-seed-next-payout", + ); + const share = parseNonnegativeIntegerText(values.shareBps); + const eventHash = rpcBytes32( + values.activeConfigurationHash, + "reward-seed-event-configuration", + ); + if ( + !Number.isSafeInteger(allocationIndex) || + allocationIndex < 0 || + allocationIndex >= beneficiaries.length || + eventEpoch !== epoch || + eventHash !== classicActiveConfigurationHash({ + vault, + factoryConfigurationHash, + configurationEpoch: epoch.toString(), + beneficiaries, + sharesBps, + }) || + beneficiaries[allocationIndex] !== next || + sharesBps[allocationIndex] !== share || + epoch <= 1n + ) { + throw validationError("rpc", "reward-seed-payout-reversal"); + } + beneficiaries[allocationIndex] = previous; + epoch -= 1n; + } + if ( + epoch !== 1n || + new Set(beneficiaries).size !== beneficiaries.length || + sharesBps.reduce((sum, share) => sum + BigInt(share), 0n) !== 10_000n + ) { + throw validationError("rpc", "reward-seed-initial-state"); + } + const initialActiveConfigurationHash = classicActiveConfigurationHash({ + vault, + factoryConfigurationHash, + configurationEpoch: "1", + beneficiaries, + sharesBps, + }); + const factoryInputCommitment = keccak256( + encodeAbiParameters( + [ + { type: "bytes32" }, + { type: "address" }, + { type: "bytes32" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [ + salt, + feeHook, + poolId, + beneficiaries, + sharesBps.map(Number), + ], + ), + ); + const factoryRequest = Object.freeze({ + factory, + vault, + blockNumber: BigInt(activationBlockNumber), + blockHash: activationBlockHash, + salt, + feeHook, + poolId, + beneficiaries: Object.freeze([...beneficiaries]), + sharesBps: Object.freeze(sharesBps.map(Number)), + }); + const remainingDeadlineMs = + policy.hardDeadlineMs - (Date.now() - startedAtMs); + if (remainingDeadlineMs < 10) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); + } + const factoryPolicy = rpcExecutionPolicy({ + ...input.rpcPolicy, + maxAttempts: 1, + hardDeadlineMs: remainingDeadlineMs, + maxCallsPerProvider: 4, + }); + const factoryProviderBudgets = input.providers.map(() => ({ + used: 0, + maximum: factoryPolicy.maxCallsPerProvider, + })); + const [leftFactoryRaw, rightFactoryRaw] = await Promise.all([ + retryRpc( + () => + input.providers[0].client.readClassicRewardFactorySnapshot!( + factoryRequest, + ), + factoryPolicy, + factoryProviderBudgets[0], + 4, + ), + retryRpc( + () => + input.providers[1].client.readClassicRewardFactorySnapshot!( + factoryRequest, + ), + factoryPolicy, + factoryProviderBudgets[1], + 4, + ), + ]); + if ( + factoryProviderBudgets[0]!.used !== 4 || + factoryProviderBudgets[1]!.used !== 4 + ) { + throw validationError("rpc", "reward-factory-call-budget"); + } + const canonicalFactoryRequest = Object.freeze({ + factory, + vault, + blockNumber: activationBlockNumber, + blockHash: activationBlockHash, + }); + const leftFactory = canonicalClassicRewardFactorySnapshot( + leftFactoryRaw, + canonicalFactoryRequest, + ); + const rightFactory = canonicalClassicRewardFactorySnapshot( + rightFactoryRaw, + canonicalFactoryRequest, + ); + const locallyPredictedVault = rpcAddress( + getContractAddress({ + bytecodeHash: leftFactory.initCodeHash, + from: factory, + opcode: "CREATE2", + salt, + }), + "reward-seed-local-prediction", + ); + const constructorArgumentsCommitment = keccak256( + encodeAbiParameters( + [ + { type: "address" }, + { type: "bytes32" }, + { type: "address" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [ + feeHook, + poolId, + leftFactory.ctoAuthority, + beneficiaries, + sharesBps.map(Number), + ], + ), + ); + if ( + JSON.stringify(leftFactory) !== JSON.stringify(rightFactory) || + leftFactory.configurationHash !== factoryConfigurationHash || + leftFactory.predictedVault !== vault || + locallyPredictedVault !== vault + ) { + throw validationError("rpc", "reward-seed-factory-agreement"); + } + const factorySnapshotCommitment = keccak256( + toBytes(JSON.stringify(leftFactory)), + ); + return Object.freeze({ + parentCandidateId: parent.candidateId, + launchCandidateId: launch.candidateId, + vault, + poolId, + deploymentBlockNumber, + deploymentBlockHash, + activationBlockNumber, + activationBlockHash, + activationBlockGlobalLogIndex: launch.blockGlobalLogIndex, + coveredRewardCandidateIds: Object.freeze(coveredRewardCandidateIds), + factory, + salt, + factoryInputCommitment, + ctoAuthority: leftFactory.ctoAuthority, + constructorArgumentsCommitment, + deployedArtifactCreationCodeCommitment: + template.deployedArtifactCreationCodeCommitment, + factoryConfigurationHash, + providerFactoryConfigurationHashes: Object.freeze([ + leftFactory.configurationHash, + rightFactory.configurationHash, + ]) as readonly [HexBytes32, HexBytes32], + providerCtoAuthorities: Object.freeze([ + leftFactory.ctoAuthority, + rightFactory.ctoAuthority, + ]) as readonly [HexAddress, HexAddress], + providerInitCodeHashes: Object.freeze([ + leftFactory.initCodeHash, + rightFactory.initCodeHash, + ]) as readonly [HexBytes32, HexBytes32], + providerPredictedVaults: Object.freeze([ + leftFactory.predictedVault, + rightFactory.predictedVault, + ]) as readonly [HexAddress, HexAddress], + locallyPredictedVault, + factoryProviderCallCounts: Object.freeze([4, 4]) as readonly [4, 4], + factoryProviderSnapshotCommitments: Object.freeze([ + factorySnapshotCommitment, + factorySnapshotCommitment, + ]) as readonly [HexBytes32, HexBytes32], + initialActiveConfigurationHash, + allocations: Object.freeze( + beneficiaries.map((beneficiary, allocationIndex) => + Object.freeze({ + allocationIndex, + beneficiary, + shareBps: sharesBps[allocationIndex]!, + }), + ), + ), + endConfigurationSnapshot, + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +export async function verifyEnvioCandidateBatchWithDualRpc(input: { + candidates: readonly EnvioCandidate[]; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + rpcPolicy?: RpcExecutionPolicyInput; + dynamicSources?: readonly VerifiedDynamicSourceLineage[]; + requireDynamicLineage?: boolean; + maximumCandidateCount?: number; +}): Promise { + const executionStartedAtMs = Date.now(); + assertProductionDualRpcProviders(input.providers); + const firstIdentity = providerIdentity(input.providers?.[0]?.identity); + const secondIdentity = providerIdentity(input.providers?.[1]?.identity); + const firstVendor = providerIdentity(input.providers?.[0]?.vendorGroup); + const secondVendor = providerIdentity(input.providers?.[1]?.vendorGroup); + const firstEndpointCommitment = rpcBytes32( + input.providers?.[0]?.endpointCommitment, + "provider-endpoint-commitment", + ); + const secondEndpointCommitment = rpcBytes32( + input.providers?.[1]?.endpointCommitment, + "provider-endpoint-commitment", + ); + const firstOriginCommitment = rpcBytes32( + input.providers?.[0]?.endpointOriginCommitment, + "provider-origin-commitment", + ); + const secondOriginCommitment = rpcBytes32( + input.providers?.[1]?.endpointOriginCommitment, + "provider-origin-commitment", + ); + if ( + firstIdentity === secondIdentity || + firstVendor === secondVendor || + firstEndpointCommitment === secondEndpointCommitment || + firstOriginCommitment === secondOriginCommitment + ) { + throw invalidInput("rpc", "provider-independence"); + } + const clients = [input.providers[0].client, input.providers[1].client] as const; + if ( + clients.some((client) => client === null || typeof client !== "object") || + clients[0] === clients[1] + ) { + throw invalidInput("rpc", "provider-client"); + } + const policy = rpcExecutionPolicy(input.rpcPolicy); + const dynamicSources = canonicalDynamicSourceLineages( + input.dynamicSources, + ); + const maximumCandidateCount = + input.maximumCandidateCount ?? PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE; + if ( + !Number.isSafeInteger(maximumCandidateCount) || + (maximumCandidateCount !== PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE && + maximumCandidateCount !== + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP) || + !Array.isArray(input.candidates) || + input.candidates.length > maximumCandidateCount + ) { + throw invalidInput("rpc", "candidate-batch"); + } + const seenCandidateIds = new Set(); + let previousBlock = -1n; + let previousLogIndex = -1; + const candidates = input.candidates.map((candidate) => { + const validated = validateCandidateBoundary( + candidate, + dynamicSources, + input.requireDynamicLineage === true, + ); + const { blockNumber } = validated; + const logIndex = safeInteger( + validated.candidate.blockGlobalLogIndex, + "candidate-placement", + ); + if ( + seenCandidateIds.has(validated.candidate.candidateId) || + blockNumber < previousBlock || + (blockNumber === previousBlock && logIndex <= previousLogIndex) + ) { + throw validationError("rpc", "candidate-batch-order"); + } + seenCandidateIds.add(validated.candidate.candidateId); + previousBlock = blockNumber; + previousLogIndex = logIndex; + return validated; + }); + const uniqueCandidateBlocks = new Set( + candidates.map(({ blockNumber }) => blockNumber.toString()), + ).size; + const uniqueTransactions = new Set( + candidates.map(({ candidate }) => candidate.transactionHash), + ).size; + const uniqueCodeRequests = new Set( + candidates.map( + ({ candidate }) => `${candidate.blockHash}:${candidate.sourceAddress}`, + ), + ).size; + const estimatedCallsByProvider = clients.map((client) => + 2 + + (client.getBlocks === undefined + ? uniqueCandidateBlocks + 1 + : Math.ceil( + (uniqueCandidateBlocks + 1) / MAXIMUM_JSON_RPC_BATCH_SIZE, + )) + + (client.getTransactionReceipts === undefined + ? uniqueTransactions + : Math.ceil(uniqueTransactions / MAXIMUM_JSON_RPC_BATCH_SIZE)) + + (client.getBytecodes === undefined + ? uniqueCodeRequests + : Math.ceil(uniqueCodeRequests / MAXIMUM_JSON_RPC_BATCH_SIZE)), + ); + if ( + estimatedCallsByProvider.some( + (estimated) => estimated > policy.maxCallsPerProvider, + ) + ) { + throw invalidInput("rpc", "provider-call-budget"); + } + + const traceContexts = input.providers.map((provider) => ({ + providerIdentity: provider.identity, + providerVendorGroup: provider.vendorGroup, + providerEndpointCommitment: provider.endpointCommitment, + providerOriginCommitment: provider.endpointOriginCommitment, + startedAtMs: executionStartedAtMs, + callCount: 0, + calls: [] as DualRpcCallTrace[], + })) as [RpcTraceContext, RpcTraceContext]; + + try { + const states = await Promise.all( + clients.map(async (client, providerIndex) => { + const traceContext = traceContexts[providerIndex]!; + const [chainId, head] = await Promise.all([ + retryTracedRpc( + "getChainId", + () => client.getChainId(), + policy, + traceContext, + ), + retryTracedRpc( + "getBlockNumber", + () => client.getBlockNumber(), + policy, + traceContext, + ), + ]); + return { chainId, head }; + }), + ); + if ( + states.some( + (state) => + state.chainId !== RELEASE_BINDING.chainId || + typeof state.head !== "bigint" || + state.head < 0n, + ) + ) { + throw validationError("rpc", "provider-state"); + } + const lowestHead = + states[0].head < states[1].head ? states[0].head : states[1].head; + const confirmations = BigInt(RELEASE_BINDING.confirmations); + if (lowestHead < confirmations) { + throw validationError("rpc", "safe-head"); + } + const safeBlockNumber = lowestHead - confirmations; + if (candidates.some(({ blockNumber }) => blockNumber > safeBlockNumber)) { + throw validationError("rpc", "candidate-finality"); + } + + const blockNumbers = [ + ...new Set([ + safeBlockNumber.toString(), + ...candidates.map(({ blockNumber }) => blockNumber.toString()), + ]), + ].map((value) => BigInt(value)); + const transactionHashes = [ + ...new Set( + candidates.map(({ candidate }) => candidate.transactionHash), + ), + ]; + const codeRequests = [ + ...new Map( + candidates.map(({ candidate }) => [ + `${candidate.blockHash}:${candidate.sourceAddress}`, + { + address: candidate.sourceAddress, + blockHash: candidate.blockHash, + requireCanonical: true as const, + }, + ]), + ).entries(), + ]; + const providerData = await Promise.all( + clients.map(async (client, providerIndex) => { + const traceContext = traceContexts[providerIndex]!; + const blocks = client.getBlocks === undefined + ? await boundedRpcMap( + blockNumbers, + policy.maxConcurrency, + async (blockNumber) => [ + blockNumber.toString(), + await retryTracedRpc( + "getBlock", + () => client.getBlock({ blockNumber }), + policy, + traceContext, + ), + ] as const, + ) + : ( + await boundedRpcMap( + boundedRpcChunks(blockNumbers), + policy.maxConcurrency, + async (numbers) => { + const values = await retryTracedRpc( + "getBlock", + () => client.getBlocks!({ blockNumbers: numbers }), + policy, + traceContext, + ); + if (!Array.isArray(values) || values.length !== numbers.length) { + throw validationError("rpc", "block-batch-shape"); + } + return numbers.map( + (number, index) => [ + number.toString(), + values[index]!, + ] as const, + ); + }, + ) + ).flat(); + const receipts = client.getTransactionReceipts === undefined + ? await boundedRpcMap( + transactionHashes, + policy.maxConcurrency, + async (transactionHash) => [ + transactionHash, + await retryTracedRpc( + "getTransactionReceipt", + () => client.getTransactionReceipt({ hash: transactionHash }), + policy, + traceContext, + ), + ] as const, + ) + : ( + await boundedRpcMap( + boundedRpcChunks(transactionHashes), + policy.maxConcurrency, + async (hashes) => { + const values = await retryTracedRpc( + "getTransactionReceipt", + () => client.getTransactionReceipts!({ hashes }), + policy, + traceContext, + ); + if (!Array.isArray(values) || values.length !== hashes.length) { + throw validationError("rpc", "receipt-batch-shape"); + } + return hashes.map( + (hash, index) => [hash, values[index]!] as const, + ); + }, + ) + ).flat(); + const bytecodes = client.getBytecodes === undefined + ? await boundedRpcMap( + codeRequests, + policy.maxConcurrency, + async ([key, request]) => [ + key, + await retryTracedRpc( + "getBytecode", + () => client.getBytecode(request), + policy, + traceContext, + ), + ] as const, + ) + : ( + await boundedRpcMap( + boundedRpcChunks(codeRequests), + policy.maxConcurrency, + async (entries) => { + const values = await retryTracedRpc( + "getBytecode", + () => client.getBytecodes!({ + requests: entries.map(([, request]) => request), + }), + policy, + traceContext, + ); + if (!Array.isArray(values) || values.length !== entries.length) { + throw validationError("rpc", "bytecode-batch-shape"); + } + return entries.map( + ([key], index) => [key, values[index]] as const, + ); + }, + ) + ).flat(); + return { + blocks: new Map(blocks), + receipts: new Map(receipts), + bytecodes: new Map(bytecodes), + }; + }), + ); + + const safe = providerData.map((data) => + canonicalBlock( + data.blocks.get(safeBlockNumber.toString())!, + safeBlockNumber, + "safe-block", + ), + ); + if ( + safe[0].hash !== safe[1].hash || + safe[0].timestamp !== safe[1].timestamp + ) { + throw validationError("rpc", "safe-block-agreement"); + } + + const providerIdentities = [firstIdentity, secondIdentity] as const; + const providerVendorGroups = [firstVendor, secondVendor] as const; + const providerEndpointCommitments = [ + firstEndpointCommitment, + secondEndpointCommitment, + ] as const; + const providerOriginCommitments = [ + firstOriginCommitment, + secondOriginCommitment, + ] as const; + const providerHeads = [ + states[0].head.toString(), + states[1].head.toString(), + ] as const; + const evidence = candidates.map(({ + candidate, + blockNumber, + timestamp, + sourceKind, + expectedRuntimeCodeHash, + dynamicSourceLineage, + }) => { + const blocks = providerData.map((data) => + canonicalBlock( + data.blocks.get(blockNumber.toString())!, + blockNumber, + "candidate-block", + ), + ); + if ( + blocks.some( + (block) => + block.hash !== candidate.blockHash || + block.timestamp !== timestamp, + ) || + blocks[0].hash !== blocks[1].hash || + blocks[0].timestamp !== blocks[1].timestamp + ) { + throw validationError("rpc", "candidate-block-agreement"); + } + + const canonicalReceipts = providerData.map((data) => + canonicalReceipt({ + receipt: data.receipts.get(candidate.transactionHash)!, + candidate, + candidateBlockNumber: blockNumber, + }), + ); + if ( + canonicalReceipts[0].commitment !== + canonicalReceipts[1].commitment || + canonicalReceipts[0].selectedOrdinal !== + canonicalReceipts[1].selectedOrdinal + ) { + throw validationError("rpc", "receipt-agreement"); + } + + const codeKey = `${candidate.blockHash}:${candidate.sourceAddress}`; + const code = providerData.map((data) => { + const value = data.bytecodes.get(codeKey); + if (value === undefined) throw validationError("rpc", "source-code"); + const canonical = rpcData(value, "source-code"); + if (canonical === "0x") throw validationError("rpc", "source-code"); + return canonical; + }); + if (code[0] !== code[1]) { + throw validationError("rpc", "source-code-agreement"); + } + const sourceCodeHash = keccak256(code[0]); + if ( + expectedRuntimeCodeHash !== null && + sourceCodeHash !== expectedRuntimeCodeHash + ) { + throw validationError("rpc", "source-code-release"); + } + let dynamicRuntimeEvidence: + | ReturnType + | undefined; + if (dynamicSourceLineage) { + dynamicRuntimeEvidence = runtimeBytecodeEvidence({ + runtimeBytecode: code[0], + expectedByteLength: Number( + dynamicSourceLineage.expectedRuntimeByteLength, + ), + immutableReferences: dynamicSourceLineage.immutableReferences, + }); + if ( + dynamicRuntimeEvidence.exactRuntimeCodeHash !== + dynamicSourceLineage.expectedExactRuntimeCodeHash || + dynamicRuntimeEvidence.normalizedRuntimeCodeHash !== + dynamicSourceLineage.expectedNormalizedRuntimeCodeHash || + dynamicRuntimeEvidence.immutableReferencesCommitment !== + dynamicSourceLineage.expectedImmutableReferencesCommitment + ) { + throw validationError("rpc", "dynamic-runtime-template"); + } + } + const rawLogCommitment = keccak256( + encodeAbiParameters( + [{ type: "address" }, { type: "bytes32[]" }, { type: "bytes" }], + [ + candidate.sourceAddress, + candidate.orderedTopics, + candidate.rawData, + ], + ), + ); + + return { + chainId: 1, + candidateId: candidate.candidateId, + sourceAddress: candidate.sourceAddress, + contractName: candidate.contractName, + eventName: candidate.eventName, + sourceKind, + model: + dynamicSourceLineage?.model ?? candidate.releaseHint.model, + releaseVersion: + dynamicSourceLineage?.releaseVersion ?? + candidate.releaseHint.releaseVersion, + payloadHash: candidate.payloadHash, + rawLogCommitment, + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads, + safeBlockNumber: safeBlockNumber.toString(), + safeBlockHash: safe[0].hash, + candidateBlockNumber: blockNumber.toString(), + candidateBlockHash: blocks[0].hash, + candidateBlockTimestamp: timestamp.toString(), + transactionHash: candidate.transactionHash, + transactionIndex: candidate.transactionIndex, + receiptCommitment: canonicalReceipts[0].commitment, + sourceCodeHash, + receiptLogOrdinal: canonicalReceipts[0].selectedOrdinal, + ...(dynamicSourceLineage && dynamicRuntimeEvidence + ? { + dynamicSourceAttestationId: + dynamicSourceLineage.attestationId, + normalizedRuntimeCodeHash: + dynamicRuntimeEvidence.normalizedRuntimeCodeHash, + immutableReferencesCommitment: + dynamicRuntimeEvidence.immutableReferencesCommitment, + runtimeByteLength: String( + dynamicRuntimeEvidence.runtimeByteLength, + ), + } + : {}), + } satisfies DualRpcCandidateEvidence; + }); + + const executionCompletedAtMs = Date.now(); + return { + chainId: 1, + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads, + safeBlockNumber: safeBlockNumber.toString(), + safeBlockHash: safe[0].hash, + candidates: evidence, + executionTrace: Object.freeze({ + startedAtMs: executionStartedAtMs, + completedAtMs: executionCompletedAtMs, + candidateBatchSize: candidates.length, + hardDeadlineMs: policy.hardDeadlineMs, + maxCallsPerProvider: policy.maxCallsPerProvider, + elapsedMs: Math.max(0, executionCompletedAtMs - executionStartedAtMs), + providerCallCounts: Object.freeze([ + traceContexts[0].callCount, + traceContexts[1].callCount, + ]) as readonly [number, number], + calls: Object.freeze([ + ...traceContexts[0].calls, + ...traceContexts[1].calls, + ]), + }), + }; + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +export async function verifyEnvioCandidateWithDualRpc(input: { + candidate: EnvioCandidate; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + rpcPolicy?: RpcExecutionPolicyInput; + dynamicSources?: readonly VerifiedDynamicSourceLineage[]; + requireDynamicLineage?: boolean; +}): Promise { + const result = await verifyEnvioCandidateBatchWithDualRpc({ + candidates: [input.candidate], + providers: input.providers, + rpcPolicy: input.rpcPolicy, + dynamicSources: input.dynamicSources, + requireDynamicLineage: input.requireDynamicLineage, + }); + return result.candidates[0]!; +} + +function coverageCursor( + value: EnvioCandidateCursor, + operation: string, + terminalBoundary = false, +) { + if (value === null || typeof value !== "object") { + throw invalidInput("rpc", operation); + } + let blockNumber: string; + try { + blockNumber = parseNonnegativeIntegerText(value.blockNumber); + } catch { + throw invalidInput("rpc", operation); + } + const logIndex = value.blockGlobalLogIndex; + if (logIndex === -1 && value.candidateId === "") { + return { blockNumber, blockGlobalLogIndex: -1, candidateId: "" }; + } + const blockGlobalLogIndex = Number( + canonicalUint32DecimalText(logIndex, operation), + ); + if ( + !terminalBoundary && + blockGlobalLogIndex === 0xffff_ffff && + value.candidateId === "" + ) { + return { blockNumber, blockGlobalLogIndex, candidateId: "" }; + } + if ( + terminalBoundary && + blockGlobalLogIndex === 0xffff_ffff && + value.candidateId === "empty-page" + ) { + return { blockNumber, blockGlobalLogIndex, candidateId: "empty-page" }; + } + if ( + typeof value.candidateId !== "string" || + !CANDIDATE_ID_PATTERN.test(value.candidateId) + ) { + throw invalidInput("rpc", operation); + } + return { blockNumber, blockGlobalLogIndex, candidateId: value.candidateId }; +} + +function comparePlacement( + left: { blockNumber: string; blockGlobalLogIndex: number }, + right: { blockNumber: string; blockGlobalLogIndex: number }, +) { + const block = BigInt(left.blockNumber) - BigInt(right.blockNumber); + if (block !== 0n) return block < 0n ? -1 : 1; + return left.blockGlobalLogIndex - right.blockGlobalLogIndex; +} + +function canonicalCandidateCoverageLog(candidate: EnvioCandidate) { + return canonicalCoverageLog({ + address: candidate.sourceAddress, + blockNumber: BigInt(candidate.blockNumber), + blockHash: candidate.blockHash, + transactionHash: candidate.transactionHash, + transactionIndex: candidate.transactionIndex, + logIndex: candidate.blockGlobalLogIndex, + removed: false, + topics: candidate.orderedTopics, + data: candidate.rawData, + }); +} + +function coverageCommitment(logs: readonly CanonicalCoverageLog[]) { + return keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }], + [logs.map(({ commitment }) => commitment)], + ), + ); +} + +/** + * Verifies that Envio supplied every reviewed event in a frozen cursor window. + * Receipt checks prove included candidates; independent getLogs scans also + * prove that no reviewed event was omitted before the cursor advances. + */ +export async function verifyEnvioCandidateWindowWithDualRpc(input: { + candidates: readonly EnvioCandidate[]; + cursor: EnvioCandidateCursor; + through: EnvioCandidateCursor; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + rpcPolicy?: RpcExecutionPolicyInput; + coveragePolicy?: { + maximumBlockSpan?: number; + maximumRequests?: number; + }; + dynamicSources?: readonly VerifiedDynamicSourceLineage[]; + coverageSourceAddresses?: readonly HexAddress[]; + maximumCandidateCount?: number; +}): Promise { + const windowStartedAt = Date.now(); + const cursor = coverageCursor(input.cursor, "coverage-cursor"); + const through = coverageCursor(input.through, "coverage-through", true); + if (comparePlacement(cursor, through) >= 0) { + throw invalidInput("rpc", "coverage-window"); + } + const maximumBlockSpan = + input.coveragePolicy?.maximumBlockSpan ?? DEFAULT_COVERAGE_BLOCK_SPAN; + const maximumRequests = + input.coveragePolicy?.maximumRequests ?? + DEFAULT_COVERAGE_MAXIMUM_REQUESTS; + if ( + !Number.isSafeInteger(maximumBlockSpan) || + maximumBlockSpan < 1 || + maximumBlockSpan > 2_000 || + !Number.isSafeInteger(maximumRequests) || + maximumRequests < 1 || + maximumRequests > 128 + ) { + throw invalidInput("rpc", "coverage-policy"); + } + if (!Array.isArray(input.candidates)) { + throw invalidInput("rpc", "coverage-candidates"); + } + const lastCandidate = input.candidates[input.candidates.length - 1]; + const throughIsBlockComplete = + through.blockGlobalLogIndex === 0xffff_ffff && + through.candidateId === "empty-page"; + if (lastCandidate) { + if (throughIsBlockComplete) { + if ( + comparePlacement( + { + blockNumber: lastCandidate.blockNumber, + blockGlobalLogIndex: lastCandidate.blockGlobalLogIndex, + }, + through, + ) >= 0 + ) { + throw invalidInput("rpc", "coverage-through-boundary"); + } + } else if ( + comparePlacement( + { + blockNumber: lastCandidate.blockNumber, + blockGlobalLogIndex: lastCandidate.blockGlobalLogIndex, + }, + through, + ) !== 0 || + lastCandidate.candidateId !== through.candidateId + ) { + throw invalidInput("rpc", "coverage-through-candidate"); + } + } else if (!throughIsBlockComplete) { + throw invalidInput("rpc", "coverage-empty-through"); + } + + const batch = await verifyEnvioCandidateBatchWithDualRpc({ + candidates: input.candidates, + providers: input.providers, + rpcPolicy: input.rpcPolicy, + dynamicSources: input.dynamicSources, + requireDynamicLineage: true, + maximumCandidateCount: input.maximumCandidateCount, + }); + if (BigInt(through.blockNumber) > BigInt(batch.safeBlockNumber)) { + throw validationError("rpc", "coverage-finality"); + } + + const dynamicSources = canonicalDynamicSourceLineages( + input.dynamicSources, + ); + const configuredSources = RELEASE_BINDING.sources + .filter(({ startBlock }) => BigInt(startBlock) <= BigInt(through.blockNumber)) + .map(({ address, contractName }) => ({ + address: rpcAddress(address, "coverage-source"), + selectors: new Set( + manifestEventSelectors(contractName).map((selector) => + rpcBytes32(selector, "coverage-selector"), + ), + ), + })) + .concat( + [...dynamicSources.values()].map(({ sourceAddress, contractName }) => ({ + address: sourceAddress, + selectors: new Set( + manifestEventSelectors(contractName).map((selector) => + rpcBytes32(selector, "coverage-selector"), + ), + ), + })), + ); + const mergedSelectors = new Map>(); + for (const { address, selectors } of configuredSources) { + const current = mergedSelectors.get(address) ?? new Set(); + for (const selector of selectors) current.add(selector); + mergedSelectors.set(address, current); + } + let sources = [...mergedSelectors.entries()] + .map(([address, selectors]) => ({ address, selectors })) + .sort((left, right) => left.address.localeCompare(right.address)); + if (input.coverageSourceAddresses !== undefined) { + if ( + !Array.isArray(input.coverageSourceAddresses) || + input.coverageSourceAddresses.length < 1 || + input.coverageSourceAddresses.length > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + ) { + throw invalidInput("rpc", "coverage-source-addresses"); + } + const requested = new Set( + input.coverageSourceAddresses.map((address) => + rpcAddress(address, "coverage-source-address"), + ), + ); + if (requested.size !== input.coverageSourceAddresses.length) { + throw invalidInput("rpc", "coverage-source-addresses"); + } + const available = new Set(sources.map(({ address }) => address)); + if ([...requested].some((address) => !available.has(address))) { + throw invalidInput("rpc", "coverage-source-addresses"); + } + sources = sources.filter(({ address }) => requested.has(address)); + } + const selectorsByAddress = new Map( + sources.map(({ address, selectors }) => [address, selectors] as const), + ); + const addresses = sources.map(({ address }) => address); + const topic0 = [ + ...new Set(sources.flatMap(({ selectors }) => [...selectors])), + ].sort(); + if (addresses.length < 1 || topic0.length < 1) { + throw invalidInput("rpc", "coverage-filter"); + } + const filterCommitment = keccak256( + toBytes( + JSON.stringify( + sources + .map(({ address, selectors }) => [ + address, + [...selectors].sort(), + ]) + .sort(([left], [right]) => String(left).localeCompare(String(right))), + ), + ), + ); + const fromBlock = BigInt(cursor.blockNumber); + const effectiveFromBlock = + cursor.blockGlobalLogIndex === 0xffff_ffff && cursor.candidateId === "" + ? fromBlock + 1n + : fromBlock; + const toBlock = BigInt(through.blockNumber); + const span = BigInt(maximumBlockSpan); + const ranges: { fromBlock: bigint; toBlock: bigint }[] = []; + for (let start = effectiveFromBlock; start <= toBlock; start += span) { + ranges.push({ + fromBlock: start, + toBlock: start + span - 1n < toBlock ? start + span - 1n : toBlock, + }); + } + if (ranges.length > maximumRequests) { + throw invalidInput("rpc", "coverage-request-budget"); + } + const logFilters: CandidateRpcLogFilter[] = []; + const addressChunks = boundedRpcChunks( + addresses, + MAXIMUM_LOG_FILTER_ADDRESSES, + ); + const topicChunks = boundedRpcChunks(topic0, MAXIMUM_LOG_FILTER_TOPIC0); + for (const range of ranges) { + for ( + let block = range.fromBlock; + block <= range.toBlock; + block += MAXIMUM_LOG_FILTER_BLOCK_SPAN + ) { + const filterToBlock = + block + MAXIMUM_LOG_FILTER_BLOCK_SPAN - 1n < range.toBlock + ? block + MAXIMUM_LOG_FILTER_BLOCK_SPAN - 1n + : range.toBlock; + for (const addressChunk of addressChunks) { + for (const topicChunk of topicChunks) { + logFilters.push(Object.freeze({ + addresses: addressChunk, + topic0: topicChunk, + fromBlock: block, + toBlock: filterToBlock, + })); + } + } + } + } + if (logFilters.length < 1) { + throw invalidInput("rpc", "coverage-filter"); + } + const remainingDeadlineMs = + (input.rpcPolicy?.hardDeadlineMs ?? + input.rpcPolicy?.deadlineMs ?? + DEFAULT_RPC_DEADLINE_MS) - + (Date.now() - windowStartedAt); + if (remainingDeadlineMs < 10) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); + } + const coveragePolicy = rpcExecutionPolicy({ + ...input.rpcPolicy, + hardDeadlineMs: remainingDeadlineMs, + deadlineMs: undefined, + }); + if (input.providers.some(({ client }, providerIndex) => { + const logCallCount = typeof client.getLogsBatch === "function" + ? Math.ceil(logFilters.length / MAXIMUM_JSON_RPC_BATCH_SIZE) + : logFilters.length; + return batch.executionTrace.providerCallCounts[providerIndex]! + + 1 + logCallCount > coveragePolicy.maxCallsPerProvider; + })) { + throw invalidInput("rpc", "provider-call-budget"); + } + + try { + const providerBudgets = input.providers.map((_provider, providerIndex) => ({ + used: batch.executionTrace.providerCallCounts[providerIndex]!, + maximum: coveragePolicy.maxCallsPerProvider, + })); + const throughBlocks = await Promise.all( + input.providers.map(({ client }, providerIndex) => + retryRpc( + () => client.getBlock({ blockNumber: BigInt(through.blockNumber) }), + coveragePolicy, + providerBudgets[providerIndex], + ), + ), + ); + const canonicalThroughBlocks = throughBlocks.map((block) => + canonicalBlock( + block, + BigInt(through.blockNumber), + "coverage-through-block", + ), + ); + if ( + canonicalThroughBlocks[0]!.hash !== canonicalThroughBlocks[1]!.hash || + canonicalThroughBlocks[0]!.timestamp !== + canonicalThroughBlocks[1]!.timestamp + ) { + throw validationError("rpc", "coverage-through-block-agreement"); + } + const providerLogs = await Promise.all( + input.providers.map(async ({ client }, providerIndex) => { + if ( + typeof client.getLogs !== "function" && + typeof client.getLogsBatch !== "function" + ) { + throw invalidInput("rpc", "coverage-get-logs"); + } + const pages = typeof client.getLogsBatch === "function" + ? (await boundedRpcMap( + boundedRpcChunks(logFilters), + coveragePolicy.maxConcurrency, + (requests) => retryRpc( + () => client.getLogsBatch!({ requests }), + coveragePolicy, + providerBudgets[providerIndex], + ), + )).flat() + : await boundedRpcMap( + logFilters, + coveragePolicy.maxConcurrency, + (filter) => retryRpc( + () => client.getLogs!(filter), + coveragePolicy, + providerBudgets[providerIndex], + ), + ); + if (pages.length !== logFilters.length) { + throw validationError("rpc", "coverage-batch-shape"); + } + const seen = new Map(); + const logs: CanonicalCoverageLog[] = []; + for (const page of pages) { + if (!Array.isArray(page) || page.length > 10_000) { + throw validationError("rpc", "coverage-page"); + } + for (const rawLog of page) { + const log = canonicalCoverageLog(rawLog); + const selectors = selectorsByAddress.get(log.address); + if (!selectors || !selectors.has(log.topics[0]!)) continue; + const placement = { + blockNumber: log.blockNumber, + blockGlobalLogIndex: Number(log.blockGlobalLogIndex), + }; + if ( + comparePlacement(placement, cursor) <= 0 || + comparePlacement(placement, through) > 0 + ) { + continue; + } + const key = coverageLogPlacementKey(log); + const existingCommitment = seen.get(key); + if (existingCommitment !== undefined) { + if (existingCommitment !== log.commitment) { + throw validationError("rpc", "coverage-duplicate"); + } + continue; + } + seen.set(key, log.commitment); + logs.push(log); + } + } + logs.sort((left, right) => + comparePlacement( + { + blockNumber: left.blockNumber, + blockGlobalLogIndex: Number(left.blockGlobalLogIndex), + }, + { + blockNumber: right.blockNumber, + blockGlobalLogIndex: Number(right.blockGlobalLogIndex), + }, + ), + ); + return logs; + }), + ); + const expected = input.candidates.map(canonicalCandidateCoverageLog); + const expectedCommitment = coverageCommitment(expected); + const commitments = providerLogs.map(coverageCommitment) as [ + HexBytes32, + HexBytes32, + ]; + if ( + commitments[0] !== commitments[1] || + commitments[0] !== expectedCommitment + ) { + throw validationError("rpc", "coverage-agreement"); + } + return Object.freeze({ + ...batch, + coveredCandidateCount: expected.length, + coverage: Object.freeze({ + fromBlockNumber: effectiveFromBlock.toString(), + throughBlockNumber: through.blockNumber, + throughBlockHash: canonicalThroughBlocks[0]!.hash, + throughBlockGlobalLogIndex: String(through.blockGlobalLogIndex), + filterCommitment, + providerLogCommitments: Object.freeze(commitments), + }), + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +const DYNAMIC_TEMPLATE_BINDINGS = Object.freeze({ + ClassicV3RewardVault: Object.freeze({ + model: "classic", + releaseVersions: Object.freeze(["classic-v3"]), + factoryContractName: "ClassicV3RewardVaultFactory", + factoryEventName: "ClassicRewardVaultDeployed", + }), + StockV1RewardVault: Object.freeze({ + model: "stock-paired", + releaseVersions: Object.freeze(["stock-paired-v1"]), + factoryContractName: "StockV1RewardVaultFactory", + factoryEventName: "QuoteAssetFeeSplitVaultDeployed", + }), + StockV2V3RewardVault: Object.freeze({ + model: "stock-paired", + releaseVersions: Object.freeze(["stock-paired-v2", "stock-paired-v3"]), + factoryContractName: "StockV2V3RewardVaultFactory", + factoryEventName: "QuoteAssetFeeSplitVaultDeployed", + }), +} as const); + +function canonicalDynamicSourceTemplate( + value: ProjectorDynamicSourceTemplate, +): ProjectorDynamicSourceTemplate { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw invalidInput("rpc", "dynamic-runtime-template"); + } + const expected = DYNAMIC_TEMPLATE_BINDINGS[value.contractName]; + let parentFactoryAddress: HexAddress; + let parentFactoryBindingCommitment: HexBytes32; + let deployedArtifactCreationCodeCommitment: HexBytes32; + let expectedExactRuntimeCodeHash: HexBytes32 | null; + let expectedNormalizedRuntimeCodeHash: HexBytes32; + let expectedImmutableReferencesCommitment: HexBytes32; + let immutableBindingCommitment: HexBytes32; + let abiEventSetCommitment: HexBytes32; + let templateCommitment: HexBytes32; + let expectedRuntimeByteLength: string; + let pointerGeneration: string; + let reorgGeneration: string; + try { + parentFactoryAddress = canonicalAddress(value.parentFactoryAddress); + parentFactoryBindingCommitment = canonicalBytes32( + value.parentFactoryBindingCommitment, + ); + deployedArtifactCreationCodeCommitment = canonicalBytes32( + value.deployedArtifactCreationCodeCommitment, + ); + expectedExactRuntimeCodeHash = + value.expectedExactRuntimeCodeHash === null + ? null + : canonicalBytes32(value.expectedExactRuntimeCodeHash); + expectedNormalizedRuntimeCodeHash = canonicalBytes32( + value.expectedNormalizedRuntimeCodeHash, + ); + expectedImmutableReferencesCommitment = canonicalBytes32( + value.expectedImmutableReferencesCommitment, + ); + immutableBindingCommitment = canonicalBytes32( + value.immutableBindingCommitment, + ); + abiEventSetCommitment = canonicalBytes32(value.abiEventSetCommitment); + templateCommitment = canonicalBytes32(value.templateCommitment); + expectedRuntimeByteLength = parseNonnegativeIntegerText( + value.expectedRuntimeByteLength, + ); + pointerGeneration = parseNonnegativeIntegerText( + value.database.pointerGeneration, + ); + reorgGeneration = parseNonnegativeIntegerText( + value.database.reorgGeneration, + ); + } catch { + throw invalidInput("rpc", "dynamic-runtime-template"); + } + const byteLength = Number(expectedRuntimeByteLength); + const immutableReferences = canonicalImmutableReferences( + value.immutableReferences, + byteLength, + ); + const scopePattern = /^[a-z][a-z0-9-]{0,95}$/; + if ( + !expected || + value.model !== expected.model || + !(expected.releaseVersions as readonly string[]).includes( + value.releaseVersion, + ) || + value.parentFactoryContractName !== expected.factoryContractName || + value.factoryEventName !== expected.factoryEventName || + value.deployedAddressField !== "vault" || + value.deployedSourceRole !== "reward_vault" || + !UUID_PATTERN.test(value.templateId) || + !UUID_PATTERN.test(value.parentFactoryBindingId) || + !UUID_PATTERN.test(value.database.epochId) || + !UUID_PATTERN.test(value.database.envioProviderDeploymentId) || + value.database.rpcProviderDeploymentIds.length !== 2 || + value.database.rpcProviderDeploymentIds.some( + (providerId) => !UUID_PATTERN.test(providerId), + ) || + value.database.rpcProviderDeploymentIds[0] === + value.database.rpcProviderDeploymentIds[1] || + !scopePattern.test(value.database.scope.releaseId) || + !scopePattern.test(value.database.scope.modelId) || + !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test( + value.database.scope.sourceGroup, + ) || + value.database.scope.releaseId !== value.releaseVersion || + BigInt(pointerGeneration) < 1n || + !Number.isSafeInteger(byteLength) || + byteLength < 1 || + byteLength > 24_576 || + expectedNormalizedRuntimeCodeHash === ZERO_BYTES32 || + expectedImmutableReferencesCommitment === ZERO_BYTES32 || + parentFactoryBindingCommitment === ZERO_BYTES32 || + deployedArtifactCreationCodeCommitment === ZERO_BYTES32 || + immutableBindingCommitment === ZERO_BYTES32 || + abiEventSetCommitment === ZERO_BYTES32 || + templateCommitment === ZERO_BYTES32 || + (expectedExactRuntimeCodeHash !== null && + expectedExactRuntimeCodeHash === ZERO_BYTES32) || + immutableReferencesCommitment(immutableReferences, byteLength) !== + expectedImmutableReferencesCommitment || + value.immutableBindingSpec === null || + typeof value.immutableBindingSpec !== "object" || + Array.isArray(value.immutableBindingSpec) + ) { + throw validationError("rpc", "dynamic-runtime-template"); + } + return Object.freeze({ + ...value, + parentFactoryAddress, + parentFactoryBindingCommitment, + deployedArtifactCreationCodeCommitment, + expectedExactRuntimeCodeHash, + expectedNormalizedRuntimeCodeHash, + expectedImmutableReferencesCommitment, + expectedRuntimeByteLength, + immutableReferences, + immutableBindingCommitment, + abiEventSetCommitment, + templateCommitment, + database: Object.freeze({ + ...value.database, + pointerGeneration, + reorgGeneration, + rpcProviderDeploymentIds: Object.freeze([ + value.database.rpcProviderDeploymentIds[0], + value.database.rpcProviderDeploymentIds[1], + ]) as readonly [string, string], + }), + }); +} + +function dynamicImmutableEvidence(input: { + template: ProjectorDynamicSourceTemplate; + parentCandidate: EnvioCandidate; + sourceAddress: HexAddress; + runtimeBytecode: Hex; + deferredAllocationEvidence?: VerifiedDeferredAllocationEvidence; +}) { + const { template, parentCandidate, sourceAddress, runtimeBytecode } = input; + const spec = template.immutableBindingSpec; + const bindings = spec.bindings; + const factoryConfigurationField = spec.factoryConfigurationField; + const deferred = input.deferredAllocationEvidence; + if ( + !Array.isArray(bindings) || + bindings.length !== template.immutableReferences.length || + bindings.length < 1 || + bindings.length > 64 || + !( + (typeof factoryConfigurationField === "string" && + /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(factoryConfigurationField)) || + factoryConfigurationField === null + ) + ) { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + const deployedAddress = parentCandidate.decodedPayload[ + template.deployedAddressField + ]; + if ( + typeof deployedAddress !== "string" || + canonicalAddress(deployedAddress) !== sourceAddress + ) { + throw validationError("rpc", "dynamic-runtime-deployed-address"); + } + let factoryConfigurationCommitment: HexBytes32; + try { + factoryConfigurationCommitment = factoryConfigurationField === null + ? canonicalBytes32(deferred?.configurationHash) + : canonicalBytes32( + parentCandidate.decodedPayload[factoryConfigurationField], + ); + } catch { + throw validationError("rpc", "dynamic-runtime-factory-configuration"); + } + const runtimeBytes = hexToBytes(runtimeBytecode); + const immutableValues = bindings.map((rawBinding, index) => { + if ( + rawBinding === null || + typeof rawBinding !== "object" || + Array.isArray(rawBinding) + ) { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + const binding = rawBinding as Record; + const reference = template.immutableReferences[index]!; + let ordinal: string; + let offset: string; + let length: string; + try { + ordinal = parseNonnegativeIntegerText(binding.ordinal); + offset = parseNonnegativeIntegerText(binding.offset); + length = parseNonnegativeIntegerText(binding.length); + } catch { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + if ( + ordinal !== String(index) || + offset !== String(reference.start) || + length !== String(reference.length) + ) { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + const source = binding.source; + const encoding = binding.encoding; + if ( + (source !== "factory_event" && + source !== "constant" && + source !== "deployed_address" && + source !== "deferred_allocation_evidence") || + (encoding !== "address" && encoding !== "bytes") || + (encoding === "address" && + reference.length !== 20 && + reference.length !== 32) + ) { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + let expected: Hex; + if (source === "deferred_allocation_evidence") { + if ( + !deferred || binding.field !== undefined || binding.value !== undefined || + encoding !== "bytes" || reference.length !== 32 || + (binding.evidenceRole !== "configuration_hash" && + binding.evidenceRole !== "beneficiary_count") + ) { + throw validationError("rpc", "dynamic-runtime-deferred-allocation"); + } + if (binding.evidenceRole === "configuration_hash") { + expected = canonicalBytes32(deferred.configurationHash); + } else { + let beneficiaryCount: string; + try { + beneficiaryCount = parseNonnegativeIntegerText( + deferred.beneficiaryCount, + ); + } catch { + throw validationError("rpc", "dynamic-runtime-deferred-allocation"); + } + const integer = BigInt(beneficiaryCount); + if (integer < 1n || integer > 64n) { + throw validationError("rpc", "dynamic-runtime-deferred-allocation"); + } + expected = `0x${integer.toString(16).padStart(64, "0")}`; + } + } else if (source === "deployed_address") { + if ( + binding.field !== undefined || + binding.value !== undefined || + encoding !== "address" + ) { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + expected = + reference.length === 20 + ? sourceAddress + : bytesToHex( + Uint8Array.from([ + ...new Uint8Array(12), + ...hexToBytes(sourceAddress), + ]), + ); + } else if (source === "constant") { + if ( + binding.field !== undefined || + typeof binding.value !== "string" + ) { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + expected = rpcData(binding.value, "dynamic-runtime-binding-constant"); + } else { + if ( + typeof binding.field !== "string" || + !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(binding.field) || + binding.value !== undefined + ) { + throw validationError("rpc", "dynamic-runtime-binding-spec"); + } + const payloadValue = parentCandidate.decodedPayload[binding.field]; + if (encoding === "address") { + const address = canonicalAddress(payloadValue); + expected = + reference.length === 20 + ? address + : bytesToHex( + Uint8Array.from([ + ...new Uint8Array(12), + ...hexToBytes(address), + ]), + ); + } else { + expected = rpcData(payloadValue, "dynamic-runtime-binding-field"); + } + } + if ((expected.length - 2) / 2 !== reference.length) { + throw validationError("rpc", "dynamic-runtime-binding-length"); + } + const observed = bytesToHex( + runtimeBytes.slice(reference.start, reference.start + reference.length), + ); + if (observed !== expected) { + throw validationError("rpc", "dynamic-runtime-immutable-value"); + } + return observed; + }); + const immutableValuesCommitment = keccak256( + concat([ + IMMUTABLE_VALUES_DOMAIN, + encodeAbiParameters([{ type: "bytes[]" }], [immutableValues]), + ]), + ); + const normalizedRuntimeCode = normalizeRuntimeBytecode({ + runtimeBytecode, + expectedByteLength: Number(template.expectedRuntimeByteLength), + immutableReferences: template.immutableReferences, + }); + const reconstructedBytes = hexToBytes(normalizedRuntimeCode); + immutableValues.forEach((value, index) => { + const reference = template.immutableReferences[index]!; + reconstructedBytes.set(hexToBytes(value), reference.start); + }); + const reconstructedRuntimeCode = bytesToHex(reconstructedBytes); + if (reconstructedRuntimeCode !== runtimeBytecode) { + throw validationError("rpc", "dynamic-runtime-reconstruction"); + } + return Object.freeze({ + immutableValues: Object.freeze(immutableValues), + immutableValuesCommitment, + reconstructedRuntimeCode, + reconstructedRuntimeCodeHash: keccak256(reconstructedRuntimeCode), + factoryConfigurationCommitment, + deferredAllocationEvidenceCommitment: + deferred === undefined + ? null + : canonicalBytes32(deferred.evidenceCommitment), + }); +} + +/** + * Reads a just-deployed dynamic source at the exact factory-event block from + * the same two providers that proved the parent window. There are no retries: + * the evidence contract is exactly one successful `getBytecode` call per + * provider. A transient failure therefore fails closed and is retried by the + * next projector cycle without advancing the canonical cursor. + */ +async function verifyDynamicRuntimeAtBlockWithDualRpcInternal(input: { + parentCandidate: EnvioCandidate; + sourceAddress: HexAddress; + deploymentBlockNumber: string; + deploymentBlockHash: HexBytes32; + template: ProjectorDynamicSourceTemplate; + parentEvidence: DualRpcCandidateWindowEvidence; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + deferredAllocationEvidence?: VerifiedDeferredAllocationEvidence; + deadlineMs?: number; +}, preloaded?: Readonly<{ + rawCodes: readonly [Hex | undefined, Hex | undefined]; + startedAtMs: number; +}>): Promise { + const template = canonicalDynamicSourceTemplate(input.template); + const sourceAddress = rpcAddress( + input.sourceAddress, + "dynamic-runtime-source", + ); + const deploymentBlockHash = rpcBytes32( + input.deploymentBlockHash, + "dynamic-runtime-block", + ); + let deploymentBlockNumber: string; + try { + deploymentBlockNumber = parseNonnegativeIntegerText( + input.deploymentBlockNumber, + ); + } catch { + throw invalidInput("rpc", "dynamic-runtime-block"); + } + if ( + input.parentCandidate === null || + typeof input.parentCandidate !== "object" || + !CANDIDATE_ID_PATTERN.test(input.parentCandidate.candidateId) || + input.parentEvidence.chainId !== RELEASE_BINDING.chainId || + input.parentEvidence.coveredCandidateCount !== + input.parentEvidence.candidates.length || + input.parentEvidence.candidates.length < 1 || + input.parentEvidence.candidates.length > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + input.parentEvidence.coverage.throughBlockNumber !== + deploymentBlockNumber || + input.parentEvidence.coverage.throughBlockHash !== deploymentBlockHash || + input.parentEvidence.coverage.throughBlockGlobalLogIndex !== + String(0xffff_ffff) + ) { + throw invalidInput("rpc", "dynamic-runtime-parent-evidence"); + } + const matchingParents = input.parentEvidence.candidates.filter( + ({ candidateId }) => candidateId === input.parentCandidate.candidateId, + ); + if (matchingParents.length !== 1) { + throw validationError("rpc", "dynamic-runtime-parent-binding"); + } + const parent = matchingParents[0]!; + if ( + parent.candidateId !== input.parentCandidate.candidateId || + parent.candidateBlockNumber !== deploymentBlockNumber || + parent.candidateBlockHash !== deploymentBlockHash || + parent.sourceAddress === sourceAddress || + input.parentCandidate.blockNumber !== deploymentBlockNumber || + input.parentCandidate.blockHash !== deploymentBlockHash || + input.parentCandidate.sourceAddress !== parent.sourceAddress || + input.parentCandidate.contractName !== parent.contractName || + input.parentCandidate.eventName !== parent.eventName || + template.parentFactoryAddress !== parent.sourceAddress || + template.parentFactoryContractName !== parent.contractName || + template.factoryEventName !== parent.eventName + ) { + throw validationError("rpc", "dynamic-runtime-parent-binding"); + } + + const providerIdentities = input.providers.map(({ identity }) => + providerIdentity(identity), + ) as [string, string]; + const providerVendorGroups = input.providers.map(({ vendorGroup }) => + providerIdentity(vendorGroup), + ) as [string, string]; + const providerEndpointCommitments = input.providers.map( + ({ endpointCommitment }) => + rpcBytes32(endpointCommitment, "provider-endpoint-commitment"), + ) as [HexBytes32, HexBytes32]; + const providerOriginCommitments = input.providers.map( + ({ endpointOriginCommitment }) => + rpcBytes32(endpointOriginCommitment, "provider-origin-commitment"), + ) as [HexBytes32, HexBytes32]; + if (input.deferredAllocationEvidence !== undefined) { + const deferred = input.deferredAllocationEvidence; + if ( + deferred.source !== "dual-rpc-reward-allocation" || + canonicalAddress(deferred.vault) !== sourceAddress || + parseNonnegativeIntegerText(deferred.blockNumber) !== deploymentBlockNumber || + canonicalBytes32(deferred.blockHash) !== deploymentBlockHash || + deferred.providerIdentities[0] !== providerIdentities[0] || + deferred.providerIdentities[1] !== providerIdentities[1] || + deferred.providerEndpointCommitments[0] !== providerEndpointCommitments[0] || + deferred.providerEndpointCommitments[1] !== providerEndpointCommitments[1] || + canonicalBytes32(deferred.evidenceCommitment) === ZERO_BYTES32 + ) { + throw validationError("rpc", "dynamic-runtime-deferred-allocation"); + } + } + const exactTuple = ( + actual: readonly string[], + expected: readonly string[], + ) => + actual.length === expected.length && + actual.every((value, index) => value === expected[index]); + if ( + !exactTuple( + providerIdentities, + input.parentEvidence.providerIdentities, + ) || + !exactTuple( + providerVendorGroups, + input.parentEvidence.providerVendorGroups, + ) || + !exactTuple( + providerEndpointCommitments, + input.parentEvidence.providerEndpointCommitments, + ) || + !exactTuple( + providerOriginCommitments, + input.parentEvidence.providerOriginCommitments, + ) + ) { + throw validationError("rpc", "dynamic-runtime-provider-binding"); + } + + const policy = rpcExecutionPolicy({ + maxConcurrency: 2, + maxAttempts: 1, + baseBackoffMs: 0, + hardDeadlineMs: input.deadlineMs, + maxCallsPerProvider: 1, + }); + const startedAtMs = preloaded?.startedAtMs ?? Date.now(); + try { + const rawCodes = preloaded?.rawCodes ?? await Promise.all( + input.providers.map(({ client }) => + withinRpcDeadline( + () => + client.getBytecode({ + address: sourceAddress, + blockHash: deploymentBlockHash, + requireCanonical: true, + }), + policy, + ), + ), + ); + const code = rawCodes.map((value) => { + if (value === undefined) { + throw validationError("rpc", "dynamic-runtime-code"); + } + const canonical = rpcData(value, "dynamic-runtime-code"); + const byteLength = (canonical.length - 2) / 2; + if ( + canonical === "0x" || + !Number.isSafeInteger(byteLength) || + byteLength < 1 || + byteLength > 24_576 + ) { + throw validationError("rpc", "dynamic-runtime-code"); + } + return Object.freeze({ canonical, byteLength }); + }); + if ( + code[0]!.canonical !== code[1]!.canonical || + code[0]!.byteLength !== code[1]!.byteLength + ) { + throw validationError("rpc", "dynamic-runtime-code-agreement"); + } + if ( + code[0]!.byteLength !== Number(template.expectedRuntimeByteLength) + ) { + throw validationError("rpc", "dynamic-runtime-template-length"); + } + const runtimeEvidence = runtimeBytecodeEvidence({ + runtimeBytecode: code[0]!.canonical, + expectedByteLength: code[0]!.byteLength, + immutableReferences: template.immutableReferences, + }); + if ( + runtimeEvidence.normalizedRuntimeCodeHash !== + template.expectedNormalizedRuntimeCodeHash || + runtimeEvidence.immutableReferencesCommitment !== + template.expectedImmutableReferencesCommitment || + (template.expectedExactRuntimeCodeHash !== null && + runtimeEvidence.exactRuntimeCodeHash !== + template.expectedExactRuntimeCodeHash) + ) { + throw validationError("rpc", "dynamic-runtime-template-mismatch"); + } + const immutableEvidence = dynamicImmutableEvidence({ + template, + parentCandidate: input.parentCandidate, + sourceAddress, + runtimeBytecode: code[0]!.canonical, + deferredAllocationEvidence: input.deferredAllocationEvidence, + }); + const completedAtMs = Date.now(); + return Object.freeze({ + chainId: 1 as const, + parentCandidateId: input.parentCandidate.candidateId, + sourceAddress, + deploymentBlockNumber, + deploymentBlockHash, + providerIdentities: Object.freeze(providerIdentities) as readonly [ + string, + string, + ], + providerVendorGroups: Object.freeze(providerVendorGroups) as readonly [ + string, + string, + ], + providerEndpointCommitments: Object.freeze( + providerEndpointCommitments, + ) as readonly [HexBytes32, HexBytes32], + providerOriginCommitments: Object.freeze( + providerOriginCommitments, + ) as readonly [HexBytes32, HexBytes32], + rawRuntimeCodeA: code[0]!.canonical, + rawRuntimeCodeB: code[1]!.canonical, + runtimeCodeHashA: runtimeEvidence.exactRuntimeCodeHash, + runtimeCodeHashB: runtimeEvidence.exactRuntimeCodeHash, + normalizedRuntimeCodeHashA: + runtimeEvidence.normalizedRuntimeCodeHash, + normalizedRuntimeCodeHashB: + runtimeEvidence.normalizedRuntimeCodeHash, + runtimeByteLengthA: String(code[0]!.byteLength), + runtimeByteLengthB: String(code[1]!.byteLength), + immutableReferences: template.immutableReferences, + immutableReferencesCommitment: + runtimeEvidence.immutableReferencesCommitment, + immutableValues: immutableEvidence.immutableValues, + immutableValuesCommitment: + immutableEvidence.immutableValuesCommitment, + reconstructedRuntimeCode: + immutableEvidence.reconstructedRuntimeCode, + reconstructedRuntimeCodeHash: + immutableEvidence.reconstructedRuntimeCodeHash, + factoryConfigurationCommitment: + immutableEvidence.factoryConfigurationCommitment, + deferredAllocationEvidenceCommitment: + immutableEvidence.deferredAllocationEvidenceCommitment, + template, + startedAtMs, + completedAtMs, + elapsedMs: completedAtMs - startedAtMs, + hardDeadlineMs: policy.hardDeadlineMs, + providerCallCounts: Object.freeze([1, 1]) as readonly [1, 1], + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +export async function verifyDynamicRuntimeAtBlockWithDualRpc(input: { + parentCandidate: EnvioCandidate; + sourceAddress: HexAddress; + deploymentBlockNumber: string; + deploymentBlockHash: HexBytes32; + template: ProjectorDynamicSourceTemplate; + parentEvidence: DualRpcCandidateWindowEvidence; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + deadlineMs?: number; +}): Promise { + assertProductionDualRpcProviders(input.providers); + return verifyDynamicRuntimeAtBlockWithDualRpcInternal(input); +} + +/** + * Verifies every provisional runtime through bounded EIP-1898 eth_getCode + * batches. Each item is still validated against its exact parent candidate and + * template, while a physical provider call carries at most 100 requests. + */ +export async function verifyDynamicRuntimesAtBlockWithDualRpc(input: { + items: readonly Readonly<{ + parentCandidate: EnvioCandidate; + sourceAddress: HexAddress; + deploymentBlockNumber: string; + deploymentBlockHash: HexBytes32; + template: ProjectorDynamicSourceTemplate; + }>[]; + parentEvidence: DualRpcCandidateWindowEvidence; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + deadlineMs?: number; +}): Promise { + assertProductionDualRpcProviders(input.providers); + if ( + !Array.isArray(input.items) || + input.items.length < 1 || + input.items.length > PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + new Set(input.items.map(({ parentCandidate }) => parentCandidate.candidateId)) + .size !== input.items.length || + new Set(input.items.map(({ sourceAddress }) => sourceAddress)).size !== + input.items.length + ) { + throw invalidInput("rpc", "dynamic-runtime-batch"); + } + const policy = rpcExecutionPolicy({ + maxConcurrency: 2, + maxAttempts: 1, + baseBackoffMs: 0, + hardDeadlineMs: input.deadlineMs, + maxCallsPerProvider: 128, + }); + const requests = input.items.map((item) => Object.freeze({ + address: rpcAddress(item.sourceAddress, "dynamic-runtime-source"), + blockHash: rpcBytes32( + item.deploymentBlockHash, + "dynamic-runtime-block", + ), + requireCanonical: true as const, + })); + const chunks: Readonly<{ start: number; requests: typeof requests }>[] = []; + for (let start = 0; start < requests.length; start += MAXIMUM_JSON_RPC_BATCH_SIZE) { + chunks.push(Object.freeze({ + start, + requests: requests.slice(start, start + MAXIMUM_JSON_RPC_BATCH_SIZE), + })); + } + if (chunks.length > policy.maxCallsPerProvider) { + throw validationError("rpc", "dynamic-runtime-call-budget"); + } + const startedAtMs = Date.now(); + try { + const providerCodes = await Promise.all( + input.providers.map(async ({ client }) => { + const output: (Hex | undefined)[] = new Array(requests.length); + for (const chunk of chunks) { + const values = client.getBytecodes + ? await withinRpcDeadline( + () => client.getBytecodes!({ requests: chunk.requests }), + policy, + ) + : chunk.requests.length === 1 + ? [await withinRpcDeadline( + () => client.getBytecode(chunk.requests[0]!), + policy, + )] + : (() => { + throw validationError( + "rpc", + "dynamic-runtime-batch-unavailable", + ); + })(); + if (values.length !== chunk.requests.length) { + throw validationError("rpc", "dynamic-runtime-batch-size"); + } + values.forEach((value, offset) => { + output[chunk.start + offset] = value; + }); + } + return output; + }), + ); + return Object.freeze(await Promise.all(input.items.map((item, index) => { + const providerACode = providerCodes[0]![index]; + const providerBCode = providerCodes[1]![index]; + return verifyDynamicRuntimeAtBlockWithDualRpcInternal({ + ...item, + parentEvidence: input.parentEvidence, + providers: input.providers, + deadlineMs: policy.hardDeadlineMs, + }, { + rawCodes: Object.freeze([ + providerACode, + providerBCode, + ]) as readonly [Hex | undefined, Hex | undefined], + startedAtMs, + }); + }))); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +/** + * Re-verifies a previously staged Classic reward vault at the exact canonical + * launch block. The launch is the activation boundary: the factory event may + * be in an earlier block, but the runtime and immutable binding must still be + * present and identical when the launcher first exposes the vault publicly. + */ +export async function verifyDynamicRuntimeAtActivationWithDualRpc(input: { + parentCandidate: EnvioCandidate; + launchCandidate: EnvioCandidate; + sourceAddress: HexAddress; + template: ProjectorDynamicSourceTemplate; + canonicalDeployment: CanonicalDynamicSourceDeploymentEvidence; + activationEvidence: DualRpcCandidateBatchEvidence; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + deadlineMs?: number; +}): Promise { + assertProductionDualRpcProviders(input.providers); + const template = canonicalDynamicSourceTemplate(input.template); + const parent = input.parentCandidate; + const launch = input.launchCandidate; + const sourceAddress = rpcAddress( + input.sourceAddress, + "dynamic-activation-source", + ); + let deploymentBlockNumber: string; + let activationBlockNumber: string; + let deploymentBlockHash: HexBytes32; + let activationBlockHash: HexBytes32; + try { + deploymentBlockNumber = parseNonnegativeIntegerText(parent.blockNumber); + activationBlockNumber = parseNonnegativeIntegerText(launch.blockNumber); + deploymentBlockHash = canonicalBytes32(parent.blockHash); + activationBlockHash = canonicalBytes32(launch.blockHash); + } catch { + throw invalidInput("rpc", "dynamic-activation-block"); + } + const parentVault = rpcAddress( + parent.decodedPayload.vault, + "dynamic-activation-parent-vault", + ); + const launchVault = rpcAddress( + launch.decodedPayload.rewardVault, + "dynamic-activation-launch-vault", + ); + const parentPoolId = rpcBytes32( + parent.decodedPayload.poolId, + "dynamic-activation-parent-pool", + ); + const launchPoolId = rpcBytes32( + launch.decodedPayload.poolId, + "dynamic-activation-launch-pool", + ); + const parentFeeHook = rpcAddress( + parent.decodedPayload.feeHook, + "dynamic-activation-parent-hook", + ); + const launchFeeHook = rpcAddress( + launch.decodedPayload.feeHook, + "dynamic-activation-launch-hook", + ); + const parentConfigurationHash = rpcBytes32( + parent.decodedPayload.configurationHash, + "dynamic-activation-parent-configuration", + ); + const launchConfigurationHash = rpcBytes32( + launch.decodedPayload.rewardConfigurationHash, + "dynamic-activation-launch-configuration", + ); + assertCanonicalDynamicDeploymentBinding({ + parent, + launch, + sourceAddress, + template, + canonicalDeployment: input.canonicalDeployment, + providers: input.providers, + }); + const launchEvidence = input.activationEvidence.candidates.filter( + ({ candidateId }) => candidateId === launch.candidateId, + ); + const launcher = RELEASE_BINDING.sources.find( + (source) => source.contractName === "ClassicV3Launcher", + ); + const launchCandidateMatch = CANDIDATE_ID_PATTERN.exec(launch.candidateId); + if ( + parent.chainId !== RELEASE_BINDING.chainId || + launch.chainId !== RELEASE_BINDING.chainId || + parent.contractName !== template.parentFactoryContractName || + parent.eventName !== template.factoryEventName || + parent.sourceAddress !== template.parentFactoryAddress || + launch.contractName !== "ClassicV3Launcher" || + launch.eventName !== "MemeTokenLaunchedV2" || + !launcher || + launch.sourceAddress !== launcher.address || + parentVault !== sourceAddress || + launchVault !== sourceAddress || + parentPoolId !== launchPoolId || + parentFeeHook !== launchFeeHook || + parentConfigurationHash !== launchConfigurationHash || + BigInt(activationBlockNumber) < BigInt(deploymentBlockNumber) || + !launchCandidateMatch || + BigInt(launchCandidateMatch[3]!) !== + BigInt(launch.blockGlobalLogIndex) || + launchEvidence.length !== 1 || + launchEvidence[0]!.candidateBlockNumber !== activationBlockNumber || + launchEvidence[0]!.candidateBlockHash !== activationBlockHash || + launchEvidence[0]!.sourceAddress !== launch.sourceAddress || + launchEvidence[0]!.contractName !== launch.contractName || + launchEvidence[0]!.eventName !== launch.eventName || + launchEvidence[0]!.transactionHash !== launch.transactionHash || + launchEvidence[0]!.transactionIndex !== launch.transactionIndex || + BigInt(input.activationEvidence.safeBlockNumber) < + BigInt(activationBlockNumber) + ) { + throw validationError("rpc", "dynamic-activation-binding"); + } + + const providerIdentities = input.providers.map(({ identity }) => + providerIdentity(identity), + ) as [string, string]; + const providerVendorGroups = input.providers.map(({ vendorGroup }) => + providerIdentity(vendorGroup), + ) as [string, string]; + const providerEndpointCommitments = input.providers.map( + ({ endpointCommitment }) => + rpcBytes32(endpointCommitment, "provider-endpoint-commitment"), + ) as [HexBytes32, HexBytes32]; + const providerOriginCommitments = input.providers.map( + ({ endpointOriginCommitment }) => + rpcBytes32(endpointOriginCommitment, "provider-origin-commitment"), + ) as [HexBytes32, HexBytes32]; + const exactTuple = ( + actual: readonly string[], + expected: readonly string[], + ) => + actual.length === expected.length && + actual.every((value, index) => value === expected[index]); + if ( + !exactTuple( + input.activationEvidence.providerIdentities, + providerIdentities, + ) || + !exactTuple( + input.activationEvidence.providerVendorGroups, + providerVendorGroups, + ) || + !exactTuple( + input.activationEvidence.providerEndpointCommitments, + providerEndpointCommitments, + ) || + !exactTuple( + input.activationEvidence.providerOriginCommitments, + providerOriginCommitments, + ) || + input.activationEvidence.candidates.some( + (candidate) => + !exactTuple(candidate.providerIdentities, providerIdentities) || + !exactTuple(candidate.providerVendorGroups, providerVendorGroups) || + !exactTuple( + candidate.providerEndpointCommitments, + providerEndpointCommitments, + ) || + !exactTuple( + candidate.providerOriginCommitments, + providerOriginCommitments, + ), + ) + ) { + throw validationError("rpc", "dynamic-activation-provider-binding"); + } + + const policy = rpcExecutionPolicy({ + maxConcurrency: 2, + maxAttempts: 1, + baseBackoffMs: 0, + hardDeadlineMs: input.deadlineMs, + maxCallsPerProvider: 1, + }); + const startedAtMs = Date.now(); + try { + const rawCodes = await Promise.all( + input.providers.map(({ client }) => + withinRpcDeadline( + () => + client.getBytecode({ + address: sourceAddress, + blockHash: activationBlockHash, + requireCanonical: true, + }), + policy, + ), + ), + ); + const code = rawCodes.map((value) => { + if (value === undefined) { + throw validationError("rpc", "dynamic-activation-code"); + } + const canonical = rpcData(value, "dynamic-activation-code"); + const byteLength = (canonical.length - 2) / 2; + if ( + canonical === "0x" || + !Number.isSafeInteger(byteLength) || + byteLength < 1 || + byteLength > 24_576 + ) { + throw validationError("rpc", "dynamic-activation-code"); + } + return Object.freeze({ canonical, byteLength }); + }); + if ( + code[0]!.canonical !== code[1]!.canonical || + code[0]!.byteLength !== code[1]!.byteLength + ) { + throw validationError("rpc", "dynamic-activation-code-agreement"); + } + if ( + code[0]!.byteLength !== Number(template.expectedRuntimeByteLength) + ) { + throw validationError("rpc", "dynamic-activation-template-length"); + } + const runtimeEvidence = runtimeBytecodeEvidence({ + runtimeBytecode: code[0]!.canonical, + expectedByteLength: code[0]!.byteLength, + immutableReferences: template.immutableReferences, + }); + if ( + runtimeEvidence.normalizedRuntimeCodeHash !== + template.expectedNormalizedRuntimeCodeHash || + runtimeEvidence.immutableReferencesCommitment !== + template.expectedImmutableReferencesCommitment || + (template.expectedExactRuntimeCodeHash !== null && + runtimeEvidence.exactRuntimeCodeHash !== + template.expectedExactRuntimeCodeHash) + ) { + throw validationError("rpc", "dynamic-activation-template-mismatch"); + } + const immutableEvidence = dynamicImmutableEvidence({ + template, + parentCandidate: parent, + sourceAddress, + runtimeBytecode: code[0]!.canonical, + }); + if ( + immutableEvidence.factoryConfigurationCommitment !== + parentConfigurationHash + ) { + throw validationError("rpc", "dynamic-activation-configuration"); + } + const completedAtMs = Date.now(); + return Object.freeze({ + chainId: 1 as const, + parentCandidateId: parent.candidateId, + launchCandidateId: launch.candidateId, + sourceAddress, + deploymentBlockNumber, + deploymentBlockHash, + activationBlockNumber, + activationBlockHash, + activationBlockGlobalLogIndex: launch.blockGlobalLogIndex, + providerIdentities: Object.freeze(providerIdentities) as readonly [ + string, + string, + ], + providerVendorGroups: Object.freeze(providerVendorGroups) as readonly [ + string, + string, + ], + providerEndpointCommitments: Object.freeze( + providerEndpointCommitments, + ) as readonly [HexBytes32, HexBytes32], + providerOriginCommitments: Object.freeze( + providerOriginCommitments, + ) as readonly [HexBytes32, HexBytes32], + rawRuntimeCodeA: code[0]!.canonical, + rawRuntimeCodeB: code[1]!.canonical, + runtimeCodeHashA: runtimeEvidence.exactRuntimeCodeHash, + runtimeCodeHashB: runtimeEvidence.exactRuntimeCodeHash, + normalizedRuntimeCodeHashA: + runtimeEvidence.normalizedRuntimeCodeHash, + normalizedRuntimeCodeHashB: + runtimeEvidence.normalizedRuntimeCodeHash, + runtimeByteLengthA: String(code[0]!.byteLength), + runtimeByteLengthB: String(code[1]!.byteLength), + immutableReferences: template.immutableReferences, + immutableReferencesCommitment: + runtimeEvidence.immutableReferencesCommitment, + immutableValues: immutableEvidence.immutableValues, + immutableValuesCommitment: + immutableEvidence.immutableValuesCommitment, + reconstructedRuntimeCode: + immutableEvidence.reconstructedRuntimeCode, + reconstructedRuntimeCodeHash: + immutableEvidence.reconstructedRuntimeCodeHash, + factoryConfigurationCommitment: + immutableEvidence.factoryConfigurationCommitment, + template, + startedAtMs, + completedAtMs, + elapsedMs: completedAtMs - startedAtMs, + hardDeadlineMs: policy.hardDeadlineMs, + providerCallCounts: Object.freeze([1, 1]) as readonly [1, 1], + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } +} diff --git a/lib/data-pipeline/envio.ts b/lib/data-pipeline/envio.ts new file mode 100644 index 00000000..bf913322 --- /dev/null +++ b/lib/data-pipeline/envio.ts @@ -0,0 +1,1044 @@ +import "server-only"; + +import { encodeAbiParameters, keccak256 } from "viem"; + +import { CircuitBreaker } from "./circuit"; +import { + canonicalAddress, + canonicalBytes32, + canonicalRawData, + parseNonnegativeIntegerText, + type HexAddress, + type HexBytes32, + type HexData, +} from "./codecs"; +import { loadDataPipelineConfig } from "./config"; +import { + DataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import { decodeManifestEvent } from "./event-manifest"; +import { + getDataPipelineReleaseBinding, + parseDataPipelineReleaseBinding, + type DataPipelineReleaseBinding, +} from "./release-binding.server"; +import { + boundedJsonRequest, + type DataPipelineFetcher, +} from "./request"; + +const CANDIDATE_QUERY = ` + query ProgrammableCandidate($candidateId: String!) { + ChainEvent_by_pk(id: $candidateId) { + id + downstreamLogicalId + receiptLogOrdinal + chainId + blockNumber + blockHash + blockTimestamp + transactionHash + transactionIndex + blockGlobalLogIndex + sourceAddress + contractName + eventName + model + releaseVersion + topics + data + decodedPayload + payloadHash + } + } +`; + +const CANDIDATES_AFTER_QUERY = ` + query ProgrammableCandidatesAfter( + $afterBlock: numeric! + $afterLogIndex: numeric! + $afterCandidateId: String! + $first: Int! + ) { + ChainEvent( + where: { + _and: [ + { + contractName: { + _nin: ["StockV1RewardVault", "StockV2V3RewardVault"] + } + } + { + _or: [ + { blockNumber: { _gt: $afterBlock } } + { + _and: [ + { blockNumber: { _eq: $afterBlock } } + { blockGlobalLogIndex: { _gt: $afterLogIndex } } + ] + } + { + _and: [ + { blockNumber: { _eq: $afterBlock } } + { blockGlobalLogIndex: { _eq: $afterLogIndex } } + { id: { _gt: $afterCandidateId } } + ] + } + ] + } + ] + } + order_by: [ + { blockNumber: asc } + { blockGlobalLogIndex: asc } + { id: asc } + ] + limit: $first + ) { + id + downstreamLogicalId + receiptLogOrdinal + chainId + blockNumber + blockHash + blockTimestamp + transactionHash + transactionIndex + blockGlobalLogIndex + sourceAddress + contractName + eventName + model + releaseVersion + topics + data + decodedPayload + payloadHash + } + } +`; + +const CANDIDATES_WINDOW_QUERY = ` + query ProgrammableCandidatesWindow( + $afterBlock: numeric! + $afterLogIndex: numeric! + $afterCandidateId: String! + $throughBlock: numeric! + $first: Int! + ) { + ChainEvent( + where: { + _and: [ + { + contractName: { + _nin: ["StockV1RewardVault", "StockV2V3RewardVault"] + } + } + { blockNumber: { _lte: $throughBlock } } + { + _or: [ + { blockNumber: { _gt: $afterBlock } } + { + _and: [ + { blockNumber: { _eq: $afterBlock } } + { blockGlobalLogIndex: { _gt: $afterLogIndex } } + ] + } + { + _and: [ + { blockNumber: { _eq: $afterBlock } } + { blockGlobalLogIndex: { _eq: $afterLogIndex } } + { id: { _gt: $afterCandidateId } } + ] + } + ] + } + ] + } + order_by: [ + { blockNumber: asc } + { blockGlobalLogIndex: asc } + { id: asc } + ] + limit: $first + ) { + id + downstreamLogicalId + receiptLogOrdinal + chainId + blockNumber + blockHash + blockTimestamp + transactionHash + transactionIndex + blockGlobalLogIndex + sourceAddress + contractName + eventName + model + releaseVersion + topics + data + decodedPayload + payloadHash + } + } +`; + +const PROGRESS_QUERY = ` + query ProgrammableIndexerProgress($stateId: String!) { + _meta(where: { chainId: { _eq: 1 } }) { + chainId + progressBlock + bufferBlock + sourceBlock + isReady + eventsProcessed + } + IndexerState_by_pk(id: $stateId) { + id + schemaVersion + deployment + sourceCommit + configSha256 + schemaSha256 + handlerSha256 + sourceRegistrySha256 + eventSetSha256 + eventCount + chainId + progressBlock + progressBlockHash + progressTimestamp + progressTransactionHash + progressOccurrenceId + } + } +`; + +const INDEXER_STATE_ID = "ethereum-mainnet"; +const SCHEMA_VERSION = "1"; + +type ReviewedModel = "classic" | "stock-paired"; +type ReviewedReleaseVersion = + | "classic-v2" + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; +type ReviewedRelease = { + model: ReviewedModel; + releaseVersion: ReviewedReleaseVersion; + sourceContracts: readonly string[]; + dynamicContracts: readonly string[]; + activationBlock: bigint; +}; + +function reviewedModel(value: string): ReviewedModel { + if (value !== "classic" && value !== "stock-paired") { + throw new Error("Unsupported data pipeline model binding"); + } + return value; +} + +function reviewedReleaseVersion(value: string): ReviewedReleaseVersion { + if ( + ![ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ].includes(value) + ) { + throw new Error("Unsupported data pipeline release binding"); + } + return value as ReviewedReleaseVersion; +} + +type ReviewedEnvioBinding = Readonly<{ + releaseBinding: DataPipelineReleaseBinding; + releases: readonly ReviewedRelease[]; + staticSources: ReadonlyMap< + HexAddress, + Readonly<{ + contractName: string; + startBlock: bigint; + releases: readonly ReviewedRelease[]; + }> + >; + dynamicSources: ReadonlyMap; +}>; + +function reviewedEnvioBinding( + selectedBinding: DataPipelineReleaseBinding, +): ReviewedEnvioBinding { + const releaseBinding = parseDataPipelineReleaseBinding(selectedBinding); + const releases: readonly ReviewedRelease[] = releaseBinding.releases.map( + (release) => { + return { + model: reviewedModel(release.model), + releaseVersion: reviewedReleaseVersion(release.releaseVersion), + sourceContracts: release.sourceContracts, + dynamicContracts: release.dynamicContracts, + activationBlock: BigInt(release.activationBlock), + }; + }, + ); + const staticSources = new Map( + releaseBinding.sources.map((source) => { + const sourceReleases = releases.filter((release) => + release.sourceContracts.includes(source.contractName), + ); + if (sourceReleases.length === 0) { + throw new Error("Orphaned data pipeline source binding"); + } + return [ + source.address, + Object.freeze({ + contractName: source.contractName, + startBlock: BigInt(source.startBlock), + releases: Object.freeze(sourceReleases), + }), + ] as const; + }), + ); + const dynamicSources = new Map(); + for (const release of releases) { + for (const contractName of release.dynamicContracts) { + dynamicSources.set( + contractName, + Object.freeze([...(dynamicSources.get(contractName) ?? []), release]), + ); + } + } + return Object.freeze({ + releaseBinding, + releases: Object.freeze(releases), + staticSources, + dynamicSources, + }); +} + +const CANDIDATE_PATTERN = + /^1:(0x[0-9a-f]{64}):(0x[0-9a-f]{64}):(0|[1-9]\d*)$/; +const UINT32_MAXIMUM = 0xffff_ffff; +const DEFAULT_CANDIDATE_PAGE_LIMIT = 25; +const MAXIMUM_CANDIDATE_PAGE_LIMIT = 32; + +function isRecord(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function onlyKeys(value: Record, keys: readonly string[]) { + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + return ( + actual.length === expected.length && + actual.every((key, index) => key === expected[index]) + ); +} + +function strictSafeInteger(value: unknown, maximum = Number.MAX_SAFE_INTEGER) { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < 0 || + value > maximum + ) { + throw validationError("envio", "placement"); + } + return value; +} + +function strictUint32Decimal(value: unknown, operation: string) { + if (typeof value !== "string" || !/^(0|[1-9]\d*)$/.test(value)) { + throw validationError("envio", operation); + } + const parsed = BigInt(value); + if (parsed > BigInt(UINT32_MAXIMUM)) { + throw validationError("envio", operation); + } + return Number(parsed); +} + +function graphqlUint32OrGenesis(value: number): string { + return String(value); +} + +function strictString(value: unknown, pattern: RegExp, operation: string) { + if (typeof value !== "string" || !pattern.test(value)) { + throw validationError("envio", operation); + } + return value; +} + +export type EnvioCandidate = { + candidateId: string; + chainId: 1; + blockNumber: string; + blockHash: HexBytes32; + blockTimestamp: string; + transactionHash: HexBytes32; + transactionIndex: number; + blockGlobalLogIndex: number; + sourceAddress: HexAddress; + contractName: string; + eventName: string; + releaseHint: { + model: "classic" | "stock-paired" | "unresolved"; + releaseVersion: string; + }; + orderedTopics: HexBytes32[]; + rawData: HexData; + decodedPayload: Record; + payloadHash: HexBytes32; +}; + +export type EnvioCandidateCursor = { + blockNumber: string; + blockGlobalLogIndex: number; + candidateId: string; +}; + +function canonicalCandidateCursor( + value: EnvioCandidateCursor, +): EnvioCandidateCursor { + const inputBlockNumber = value?.blockNumber; + const blockGlobalLogIndex = value?.blockGlobalLogIndex; + const candidateId = value?.candidateId; + let blockNumber: string; + try { + blockNumber = parseNonnegativeIntegerText(inputBlockNumber); + } catch { + throw invalidInput("envio", "candidate-cursor"); + } + if ( + !Number.isSafeInteger(blockGlobalLogIndex) || + blockGlobalLogIndex < -1 || + blockGlobalLogIndex > UINT32_MAXIMUM + ) { + throw invalidInput("envio", "candidate-cursor"); + } + const candidateMatch = + typeof candidateId === "string" + ? CANDIDATE_PATTERN.exec(candidateId) + : null; + const isGenesisCursor = + blockGlobalLogIndex === -1 && candidateId === ""; + const isPredecessorBlockCursor = + blockGlobalLogIndex === UINT32_MAXIMUM && candidateId === ""; + const isTerminalBlockCursor = + blockGlobalLogIndex === UINT32_MAXIMUM && candidateId === "empty-page"; + const isPlacedCursor = + blockGlobalLogIndex >= 0 && + candidateMatch !== null && + BigInt(candidateMatch[3]) === BigInt(blockGlobalLogIndex); + if ( + !isGenesisCursor && + !isPredecessorBlockCursor && + !isTerminalBlockCursor && + !isPlacedCursor + ) { + throw invalidInput("envio", "candidate-cursor"); + } + return { + blockNumber, + blockGlobalLogIndex, + candidateId, + }; +} + +function placementAfter( + candidate: Pick< + EnvioCandidate, + "blockNumber" | "blockGlobalLogIndex" | "candidateId" + >, + cursor: EnvioCandidateCursor, +) { + const candidateBlock = BigInt(candidate.blockNumber); + const cursorBlock = BigInt(cursor.blockNumber); + return ( + candidateBlock > cursorBlock || + (candidateBlock === cursorBlock && + (candidate.blockGlobalLogIndex > cursor.blockGlobalLogIndex || + (candidate.blockGlobalLogIndex === cursor.blockGlobalLogIndex && + candidate.candidateId > cursor.candidateId))) + ); +} + +function parseCandidatePage(input: { + response: unknown; + cursor: EnvioCandidateCursor; + limit: number; + throughBlock?: string; + reviewedBinding: ReviewedEnvioBinding; +}): EnvioCandidate[] { + const { response, cursor, limit, throughBlock, reviewedBinding } = input; + if ( + !isRecord(response) || + !onlyKeys(response, ["data"]) || + !isRecord(response.data) || + !onlyKeys(response.data, ["ChainEvent"]) || + !Array.isArray(response.data.ChainEvent) || + response.data.ChainEvent.length > limit + ) { + throw validationError("envio", "candidate-page-response"); + } + try { + const result: EnvioCandidate[] = []; + let previous = cursor; + for (const row of response.data.ChainEvent) { + if (!isRecord(row) || typeof row.id !== "string") { + throw validationError("envio", "candidate-page-row"); + } + const parsed = parseCandidate(row, row.id, reviewedBinding); + if ( + !placementAfter(parsed, previous) || + (throughBlock !== undefined && + BigInt(parsed.blockNumber) > BigInt(throughBlock)) + ) { + throw validationError("envio", "candidate-page-order"); + } + result.push(parsed); + previous = { + blockNumber: parsed.blockNumber, + blockGlobalLogIndex: parsed.blockGlobalLogIndex, + candidateId: parsed.candidateId, + }; + } + return result; + } catch (error) { + if (error instanceof DataPipelineError) { + throw validationError("envio", "candidate-page-response"); + } + throw error; + } +} + +function validateSource(input: { + sourceAddress: HexAddress; + contractName: string; + blockNumber: bigint; + model: string; + releaseVersion: string; +}, reviewedBinding: ReviewedEnvioBinding) { + const source = reviewedBinding.staticSources.get(input.sourceAddress); + if (source) { + if ( + source.contractName !== input.contractName || + input.blockNumber < source.startBlock + ) { + throw validationError("envio", "source-provenance"); + } + const exact = source.releases.some( + (release) => + source.releases.length === 1 && + release.model === input.model && + release.releaseVersion === input.releaseVersion && + input.blockNumber >= release.activationBlock, + ); + const activeReleases = source.releases.filter( + (release) => input.blockNumber >= release.activationBlock, + ); + const unresolved = + input.model === "unresolved" && + input.releaseVersion === "unresolved" && + source.releases.length > 1 && + new Set(source.releases.map((release) => release.model)).size === 1 && + activeReleases.length > 0; + if (!exact && !unresolved) { + throw validationError("envio", "source-release-provenance"); + } + return; + } + + const dynamicReleases = reviewedBinding.dynamicSources.get( + input.contractName, + ); + if ( + !dynamicReleases || + input.model !== "unresolved" || + input.releaseVersion !== "unresolved" || + dynamicReleases.every( + (release) => input.blockNumber < release.activationBlock, + ) + ) { + throw validationError("envio", "dynamic-source-provenance"); + } +} + +function parseCandidate( + value: unknown, + requestedCandidateId: string, + reviewedBinding: ReviewedEnvioBinding, +): EnvioCandidate { + const keys = [ + "id", + "downstreamLogicalId", + "receiptLogOrdinal", + "chainId", + "blockNumber", + "blockHash", + "blockTimestamp", + "transactionHash", + "transactionIndex", + "blockGlobalLogIndex", + "sourceAddress", + "contractName", + "eventName", + "model", + "releaseVersion", + "topics", + "data", + "decodedPayload", + "payloadHash", + ] as const; + if ( + !isRecord(value) || + !onlyKeys(value, keys) || + value.downstreamLogicalId !== null || + value.receiptLogOrdinal !== null || + value.chainId !== 1 || + value.id !== requestedCandidateId || + typeof value.id !== "string" + ) { + throw validationError("envio", "candidate"); + } + const match = CANDIDATE_PATTERN.exec(value.id); + if (!match) throw validationError("envio", "candidate-id"); + const blockNumber = parseNonnegativeIntegerText(value.blockNumber); + const blockHash = canonicalBytes32(value.blockHash); + const transactionHash = canonicalBytes32(value.transactionHash); + const blockGlobalLogIndex = strictUint32Decimal( + value.blockGlobalLogIndex, + "block-global-log-index", + ); + if ( + match[1] !== blockHash || + match[2] !== transactionHash || + BigInt(match[3]) !== BigInt(blockGlobalLogIndex) + ) { + throw validationError("envio", "candidate-placement"); + } + const sourceAddress = canonicalAddress(value.sourceAddress); + const contractName = strictString( + value.contractName, + /^[A-Za-z][A-Za-z0-9]{0,63}$/, + "contract-name", + ); + const eventName = strictString( + value.eventName, + /^[A-Za-z][A-Za-z0-9]{0,95}$/, + "event-name", + ); + const model = strictString( + value.model, + /^(classic|stock-paired|unresolved)$/, + "model", + ) as "classic" | "stock-paired" | "unresolved"; + const releaseVersion = strictString( + value.releaseVersion, + /^(classic-v[23]|stock-paired-v[123]|unresolved)$/, + "release-version", + ); + validateSource( + { + sourceAddress, + contractName, + blockNumber: BigInt(blockNumber), + model, + releaseVersion, + }, + reviewedBinding, + ); + if ( + !Array.isArray(value.topics) || + value.topics.length < 1 || + value.topics.length > 4 + ) { + throw validationError("envio", "topics"); + } + const orderedTopics = value.topics.map((topic) => canonicalBytes32(topic)); + const rawData = canonicalRawData(value.data); + const payloadHash = canonicalBytes32(value.payloadHash); + const recomputedPayloadHash = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [orderedTopics, rawData], + ), + ); + if (payloadHash !== recomputedPayloadHash) { + throw validationError("envio", "payload-hash"); + } + let decodedPayload: unknown; + try { + decodedPayload = + typeof value.decodedPayload === "string" + ? JSON.parse(value.decodedPayload) + : null; + } catch { + throw validationError("envio", "decoded-payload"); + } + if (!isRecord(decodedPayload)) { + throw validationError("envio", "decoded-payload"); + } + let locallyDecodedPayload: Record; + try { + locallyDecodedPayload = decodeManifestEvent({ + contractName, + eventName, + topics: orderedTopics, + data: rawData, + providerPayload: decodedPayload, + }); + } catch { + throw validationError("envio", "event-abi"); + } + + return { + candidateId: value.id, + chainId: 1, + blockNumber, + blockHash, + blockTimestamp: parseNonnegativeIntegerText(value.blockTimestamp), + transactionHash, + transactionIndex: strictUint32Decimal( + value.transactionIndex, + "transaction-index", + ), + blockGlobalLogIndex, + sourceAddress, + contractName, + eventName, + releaseHint: { model, releaseVersion }, + orderedTopics, + rawData, + decodedPayload: locallyDecodedPayload, + payloadHash, + }; +} + +export type EnvioProgress = { + chainId: 1; + deployment: string; + schemaVersion: "1"; + progressBlock: string; + bufferBlock: string; + sourceBlock: string; + eventsProcessed: string; + lastHandledEventBlock: string; + lastHandledEventBlockHash: HexBytes32; + lastHandledEventTimestamp: string; + lastHandledEventTransactionHash: HexBytes32; + lastHandledEventOccurrenceId: string; + requiredBlock: string; + lagBlocks: string; + isReady: boolean; +}; + +function parseProgress( + metaValue: unknown, + value: unknown, + requiredBlock: string, + releaseBinding: DataPipelineReleaseBinding, +): EnvioProgress { + const keys = [ + "id", + "schemaVersion", + "deployment", + "sourceCommit", + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "eventCount", + "chainId", + "progressBlock", + "progressBlockHash", + "progressTimestamp", + "progressTransactionHash", + "progressOccurrenceId", + ] as const; + if ( + !isRecord(value) || + !onlyKeys(value, keys) || + value.id !== INDEXER_STATE_ID || + value.schemaVersion !== SCHEMA_VERSION || + value.chainId !== 1 || + value.deployment !== releaseBinding.envio.deploymentLabel || + value.sourceCommit !== releaseBinding.envio.sourceCommit || + value.configSha256 !== releaseBinding.envio.configSha256 || + value.schemaSha256 !== releaseBinding.envio.schemaSha256 || + value.handlerSha256 !== releaseBinding.envio.handlerSha256 || + value.sourceRegistrySha256 !== + releaseBinding.envio.sourceRegistrySha256 || + value.eventSetSha256 !== releaseBinding.envio.eventSetSha256 || + value.eventCount !== releaseBinding.envio.eventCount || + !Array.isArray(metaValue) || + metaValue.length !== 1 || + !isRecord(metaValue[0]) || + !onlyKeys(metaValue[0], [ + "chainId", + "progressBlock", + "bufferBlock", + "sourceBlock", + "isReady", + "eventsProcessed", + ]) + ) { + throw validationError("envio", "progress"); + } + const meta = metaValue[0]; + const officialProgress = strictSafeInteger(meta.progressBlock); + const bufferBlock = strictSafeInteger(meta.bufferBlock); + const sourceBlock = strictSafeInteger(meta.sourceBlock); + const eventsProcessed = strictSafeInteger(meta.eventsProcessed); + if ( + meta.chainId !== 1 || + typeof meta.isReady !== "boolean" || + officialProgress > bufferBlock || + bufferBlock > sourceBlock + ) { + throw validationError("envio", "progress-meta"); + } + const lastHandledEventBlock = parseNonnegativeIntegerText( + value.progressBlock, + ); + if (BigInt(lastHandledEventBlock) > BigInt(officialProgress)) { + throw validationError("envio", "progress-order"); + } + const canonicalRequired = parseNonnegativeIntegerText(requiredBlock); + const progressOccurrenceId = strictString( + value.progressOccurrenceId, + CANDIDATE_PATTERN, + "progress-occurrence", + ); + const occurrenceMatch = CANDIDATE_PATTERN.exec(progressOccurrenceId); + if (!occurrenceMatch) { + throw validationError("envio", "progress-occurrence"); + } + const progressBlockHash = canonicalBytes32(value.progressBlockHash); + const progressTransactionHash = canonicalBytes32( + value.progressTransactionHash, + ); + if ( + occurrenceMatch[1] !== progressBlockHash || + occurrenceMatch[2] !== progressTransactionHash + ) { + throw validationError("envio", "progress-occurrence-identity"); + } + const progress = BigInt(officialProgress); + const required = BigInt(canonicalRequired); + return { + chainId: 1, + deployment: releaseBinding.envio.deploymentLabel, + schemaVersion: "1", + progressBlock: String(officialProgress), + bufferBlock: String(bufferBlock), + sourceBlock: String(sourceBlock), + eventsProcessed: String(eventsProcessed), + lastHandledEventBlock, + lastHandledEventBlockHash: progressBlockHash, + lastHandledEventTimestamp: parseNonnegativeIntegerText( + value.progressTimestamp, + ), + lastHandledEventTransactionHash: progressTransactionHash, + lastHandledEventOccurrenceId: progressOccurrenceId, + requiredBlock: canonicalRequired, + lagBlocks: (required > progress ? required - progress : 0n).toString(), + isReady: meta.isReady && progress >= required, + }; +} + +export function createEnvioClient(options: { + endpoint: string; + token?: string; + releaseBinding?: DataPipelineReleaseBinding; + fetcher?: DataPipelineFetcher; + circuit?: CircuitBreaker; +}) { + const reviewedBinding = reviewedEnvioBinding( + options.releaseBinding ?? getDataPipelineReleaseBinding(), + ); + const config = loadDataPipelineConfig({ + PROGRAMMABLE_ENVIO_GRAPHQL_URL: options.endpoint, + PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN: options.token, + }); + if (!config.envio.endpoint) { + throw invalidInput("config", "envio-config"); + } + const circuit = + options.circuit ?? new CircuitBreaker({ dependency: "envio" }); + const request = (body: unknown) => + boundedJsonRequest({ + dependency: "envio", + endpoint: config.envio.endpoint!, + timeoutMs: config.envio.timeoutMs, + maximumBodyBytes: config.envio.maximumBodyBytes, + fetcher: options.fetcher, + headers: config.envio.token + ? { authorization: `Bearer ${config.envio.token}` } + : undefined, + body, + }); + + return Object.freeze({ + async readCandidate( + candidateId: string, + ): Promise { + if (!CANDIDATE_PATTERN.test(candidateId)) { + throw invalidInput("envio", "candidate-id"); + } + return circuit.execute(async () => { + const response = await request({ + query: CANDIDATE_QUERY, + variables: { candidateId }, + }); + if ( + !isRecord(response) || + !onlyKeys(response, ["data"]) || + !isRecord(response.data) || + !onlyKeys(response.data, ["ChainEvent_by_pk"]) + ) { + throw validationError("envio", "candidate-response"); + } + if (response.data.ChainEvent_by_pk === null) return null; + try { + return parseCandidate( + response.data.ChainEvent_by_pk, + candidateId, + reviewedBinding, + ); + } catch (error) { + if (error instanceof DataPipelineError) { + throw validationError("envio", "candidate-response"); + } + throw error; + } + }); + }, + + async readCandidatesAfter(input: { + cursor: EnvioCandidateCursor; + limit?: number; + }): Promise { + const cursor = canonicalCandidateCursor(input?.cursor); + const limit = input?.limit ?? DEFAULT_CANDIDATE_PAGE_LIMIT; + if ( + !Number.isSafeInteger(limit) || + limit < 1 || + limit > MAXIMUM_CANDIDATE_PAGE_LIMIT + ) { + throw invalidInput("envio", "candidate-page-limit"); + } + return circuit.execute(async () => { + const response = await request({ + query: CANDIDATES_AFTER_QUERY, + variables: { + afterBlock: cursor.blockNumber, + afterLogIndex: graphqlUint32OrGenesis( + cursor.blockGlobalLogIndex, + ), + afterCandidateId: cursor.candidateId, + first: limit, + }, + }); + return parseCandidatePage({ + response, + cursor, + limit, + reviewedBinding, + }); + }); + }, + + async readCandidatesWindow(input: { + cursor: EnvioCandidateCursor; + throughBlock: string; + limit?: number; + }): Promise { + const cursor = canonicalCandidateCursor(input?.cursor); + let throughBlock: string; + try { + throughBlock = parseNonnegativeIntegerText(input?.throughBlock); + } catch { + throw invalidInput("envio", "candidate-window"); + } + const limit = input?.limit ?? DEFAULT_CANDIDATE_PAGE_LIMIT; + if ( + BigInt(throughBlock) < BigInt(cursor.blockNumber) || + !Number.isSafeInteger(limit) || + limit < 1 || + limit > MAXIMUM_CANDIDATE_PAGE_LIMIT + ) { + throw invalidInput("envio", "candidate-window"); + } + return circuit.execute(async () => { + const response = await request({ + query: CANDIDATES_WINDOW_QUERY, + variables: { + afterBlock: cursor.blockNumber, + afterLogIndex: graphqlUint32OrGenesis( + cursor.blockGlobalLogIndex, + ), + afterCandidateId: cursor.candidateId, + throughBlock, + first: limit, + }, + }); + return parseCandidatePage({ + response, + cursor, + limit, + throughBlock, + reviewedBinding, + }); + }); + }, + + async readProgress(input: { + requiredBlock: string; + }): Promise { + const requiredBlock = parseNonnegativeIntegerText(input.requiredBlock); + return circuit.execute(async () => { + const response = await request({ + query: PROGRESS_QUERY, + variables: { stateId: INDEXER_STATE_ID }, + }); + if ( + !isRecord(response) || + !onlyKeys(response, ["data"]) || + !isRecord(response.data) || + !onlyKeys(response.data, ["_meta", "IndexerState_by_pk"]) || + response.data.IndexerState_by_pk === null + ) { + throw validationError("envio", "progress-response"); + } + try { + return parseProgress( + response.data._meta, + response.data.IndexerState_by_pk, + requiredBlock, + reviewedBinding.releaseBinding, + ); + } catch (error) { + if (error instanceof DataPipelineError) { + throw validationError("envio", "progress-response"); + } + throw error; + } + }); + }, + + circuitSnapshot: () => circuit.snapshot(), + }); +} diff --git a/lib/data-pipeline/errors.ts b/lib/data-pipeline/errors.ts new file mode 100644 index 00000000..607d7194 --- /dev/null +++ b/lib/data-pipeline/errors.ts @@ -0,0 +1,131 @@ +import "server-only"; + +export type DataPipelineDependency = + | "config" + | "envio" + | "rpc" + | "postgres" + | "uniswap" + | "blob"; + +export type DataPipelineErrorCode = + | "invalid_config" + | "invalid_input" + | "validation_failed" + | "dependency_unavailable" + | "timeout" + | "circuit_open" + | "response_oversize" + | "invalid_json" + | "graphql_error" + | "query_failed"; + +type SafeMetadataValue = string | number | boolean; + +const SAFE_MESSAGES: Record = { + invalid_config: "Invalid data-pipeline configuration", + invalid_input: "Invalid data-pipeline input", + validation_failed: "Data-pipeline response validation failed", + dependency_unavailable: "Data-pipeline dependency unavailable", + timeout: "Data-pipeline dependency timed out", + circuit_open: "Data-pipeline dependency circuit is open", + response_oversize: "Data-pipeline response exceeded its size limit", + invalid_json: "Data-pipeline dependency returned invalid JSON", + graphql_error: "Data-pipeline dependency returned a GraphQL error", + query_failed: "Data-pipeline database query failed", +}; + +const SAFE_METADATA_KEYS = new Set([ + "operation", + "status", + "limit", + "state", + "page", + "dependency", +]); + +function sanitizeMetadata( + metadata: Readonly> | undefined, +) { + if (!metadata) return undefined; + const safe: Record = {}; + for (const [key, value] of Object.entries(metadata)) { + if (!SAFE_METADATA_KEYS.has(key)) continue; + if ( + typeof value === "string" && + (value.length > 96 || + /(?:https?:\/\/|postgres(?:ql)?:\/\/|password|token|secret|key=)/i.test( + value, + )) + ) { + continue; + } + safe[key] = value; + } + return Object.keys(safe).length > 0 ? Object.freeze(safe) : undefined; +} +export class DataPipelineError extends Error { + readonly dependency: DataPipelineDependency; + readonly code: DataPipelineErrorCode; + readonly retryable: boolean; + readonly countsTowardCircuit: boolean; + readonly safeMetadata?: Readonly>; + + constructor(input: { + dependency: DataPipelineDependency; + code: DataPipelineErrorCode; + retryable: boolean; + countsTowardCircuit: boolean; + metadata?: Readonly>; + }) { + super(SAFE_MESSAGES[input.code]); + this.name = "DataPipelineError"; + this.dependency = input.dependency; + this.code = input.code; + this.retryable = input.retryable; + this.countsTowardCircuit = input.countsTowardCircuit; + this.safeMetadata = sanitizeMetadata(input.metadata); + } + + toJSON() { + return { + name: this.name, + dependency: this.dependency, + code: this.code, + retryable: this.retryable, + ...(this.safeMetadata ? { metadata: this.safeMetadata } : {}), + }; + } +} + +export function dataPipelineError( + input: ConstructorParameters[0], +) { + return new DataPipelineError(input); +} + +export function validationError( + dependency: DataPipelineDependency, + operation?: string, +) { + return dataPipelineError({ + dependency, + code: "validation_failed", + retryable: true, + countsTowardCircuit: true, + metadata: operation ? { operation } : undefined, + }); +} + +export function invalidInput( + dependency: DataPipelineDependency, + operation?: string, +) { + return dataPipelineError({ + dependency, + code: "invalid_input", + retryable: false, + countsTowardCircuit: false, + metadata: operation ? { operation } : undefined, + }); +} diff --git a/lib/data-pipeline/event-manifest.ts b/lib/data-pipeline/event-manifest.ts new file mode 100644 index 00000000..bbe799a0 --- /dev/null +++ b/lib/data-pipeline/event-manifest.ts @@ -0,0 +1,402 @@ +import "server-only"; + +import { + decodeEventLog, + parseAbiItem, + toEventSelector, + type AbiEvent, + type AbiParameter, + type Hex, +} from "viem"; + +/** + * Runtime ABI authority for Envio candidate events. This is intentionally + * checked in rather than accepted from the provider response. The drift test + * keeps it byte-for-byte aligned with indexer/config.yaml. + */ +export const PROGRAMMABLE_EVENT_SIGNATURES = { + ClassicV2Launcher: [ + "MemeTokenLaunched(address indexed creator, address indexed token, bytes32 indexed poolId, address feeHook, address positionRecipient, uint256 positionTokenId, uint16 totalSwapFeeBps, bytes32 launchHash)", + "MemeLiquidityConfigured(address indexed token, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)", + "MemeCreatorInitialBuy(address indexed creator, address indexed token, bytes32 indexed poolId, uint256 nativeAmount, uint256 tokenAmount, bytes32 launchHash)", + ], + ClassicV2Hook: [ + "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed creator, address registrar, uint16 totalSwapFeeBps)", + "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)", + "NativeSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, uint256 grossNativeAmount, uint256 creatorFee, uint256 launcherFee)", + "CreatorFeesClaimed(bytes32 indexed poolId, address indexed creator, address indexed recipient, address caller, uint256 amount)", + "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed caller, uint256 amount)", + ], + ClassicV3Launcher: [ + "MemeTokenLaunchedV2(address indexed deployer, address indexed token, bytes32 indexed poolId, address feeHook, address rewardVault, address positionRecipient, uint256 positionTokenId, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, bytes32 rewardConfigurationHash, bytes32 launchHash)", + "MemeLiquidityConfiguredV2(address indexed token, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)", + "MemeCreatorInitialBuyV2(address indexed deployer, address indexed token, bytes32 indexed poolId, uint256 nativeAmount, uint256 tokenAmount, bytes32 launchHash)", + "MemeCreatorInitialBuyCustodyV2(address indexed deployer, address indexed token, address indexed custody, uint8 mode, uint16 durationDays, uint16 cliffDays, bytes32 configurationHash, bytes32 launchHash)", + ], + ClassicV3Hook: [ + "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed rewardVault, address registrar, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, bytes32 rewardConfigurationHash)", + "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, address indexed rewardVault, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 buyCreatorFeeBps, uint16 sellCreatorFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)", + "NativeSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, bool indexed isBuy, uint16 appliedTotalSwapFeeBps, uint256 grossNativeAmount, uint256 creatorFee, uint256 launcherFee)", + "CreatorFeesClaimed(bytes32 indexed poolId, address indexed rewardVault, address indexed caller, uint256 amount)", + "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed caller, uint256 amount)", + ], + ClassicV3RewardVaultFactory: [ + "ClassicRewardVaultDeployed(address indexed vault, bytes32 indexed poolId, address indexed feeHook, bytes32 salt, bytes32 configurationHash)", + ], + ClassicV3VestingWalletFactory: [ + "ClassicInitialBuyVestingWalletDeployed(address indexed wallet, address indexed token, address indexed beneficiary, bytes32 salt, bytes32 configurationHash)", + ], + ClassicV3RewardVault: [ + "CreatorFeesCheckpointed(bytes32 indexed poolId, uint64 indexed configurationEpoch, uint256 amount, uint256 totalCreatorFeesReceived)", + "BeneficiaryFeesClaimed(address indexed beneficiary, uint256 amount, uint256 beneficiaryTotalClaimed, uint256 vaultTotalReceived)", + "PayoutWalletChanged(bytes32 indexed poolId, uint256 indexed allocationIndex, address indexed previousPayoutWallet, address newPayoutWallet, uint16 shareBps, uint64 configurationEpoch, bytes32 activeConfigurationHash, uint256 effectiveTotalCreatorFeesReceived)", + "CtoRewardConfigurationActivated(bytes32 indexed poolId, bytes32 indexed approvalReference, uint64 indexed configurationEpoch, bytes32 previousConfigurationHash, bytes32 newConfigurationHash, address[] beneficiaries, uint16[] sharesBps, uint256 effectiveTotalCreatorFeesReceived)", + ], + StockV1Launcher: [ + "StockPairedTokenLaunched(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, address rewardVault, address positionRecipient, uint256 positionTokenId, bytes32 launchHash)", + "StockPairedLiquidityConfigured(address indexed token, address indexed quoteAsset, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)", + "StockPairedCreatorInitialBuy(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, uint256 quoteAmount, uint256 tokenAmount, bytes32 launchHash)", + ], + StockV1EthCoordinator: [ + "StockPairedEthTokenLaunched(address indexed creator, address indexed token, address indexed quoteAsset, uint256 initialBuyEthAmount, uint256 initialBuyQuoteAmount, uint256 initialBuyTokenAmount, bytes32 launchHash)", + ], + StockV1Hook: [ + "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, address registrar, bool quoteIsCurrency0, bytes32 rewardConfigurationHash, bytes32 quoteConfigurationHash)", + "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 creatorFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)", + "QuoteSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, address indexed quoteAsset, bool isBuy, uint256 grossQuoteAmount, uint256 creatorFee, uint256 launcherFee)", + "CreatorFeesClaimed(bytes32 indexed poolId, address indexed rewardVault, address indexed quoteAsset, address caller, uint256 amount)", + "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed quoteAsset, address caller, uint256 amount)", + ], + StockV1RewardVaultFactory: [ + "QuoteAssetFeeSplitVaultDeployed(address indexed vault, address indexed feeHook, bytes32 indexed poolId, address quoteAsset)", + ], + StockV1RewardVault: [ + "PayoutAddressUpdated(address indexed beneficiary, address indexed previousPayoutAddress, address indexed newPayoutAddress)", + "BeneficiaryFeesClaimed(address indexed beneficiary, address indexed payoutAddress, address indexed quoteAsset, uint256 amount, uint256 beneficiaryTotalClaimed, uint256 vaultTotalReceived)", + ], + StockV2Launcher: [ + "StockPairedTokenLaunched(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, address rewardVault, address positionRecipient, uint256 positionTokenId, bytes32 launchHash)", + "StockPairedLiquidityConfigured(address indexed token, address indexed quoteAsset, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)", + "StockPairedCreatorInitialBuy(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, uint256 quoteAmount, uint256 tokenAmount, bytes32 launchHash)", + ], + StockV2EthCoordinator: [ + "StockPairedEthTokenLaunched(address indexed creator, address indexed token, address indexed quoteAsset, uint256 initialBuyEthAmount, uint256 initialBuyQuoteAmount, uint256 initialBuyTokenAmount, bytes32 launchHash)", + ], + StockV3Launcher: [ + "StockPairedTokenLaunched(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, address rewardVault, address positionRecipient, uint256 positionTokenId, bytes32 launchHash)", + "StockPairedLiquidityConfigured(address indexed token, address indexed quoteAsset, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)", + "StockPairedCreatorInitialBuy(address indexed deployer, address indexed token, address indexed quoteAsset, bytes32 poolId, uint256 quoteAmount, uint256 tokenAmount, bytes32 launchHash)", + ], + StockV3EthCoordinator: [ + "StockPairedEthTokenLaunched(address indexed creator, address indexed token, address indexed quoteAsset, uint256 initialBuyEthAmount, uint256 initialBuyQuoteAmount, uint256 initialBuyTokenAmount, bytes32 launchHash)", + ], + StockV2V3Hook: [ + "PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, address registrar, bool quoteIsCurrency0, bytes32 rewardConfigurationHash, bytes32 quoteConfigurationHash)", + "PoolFeeDisclosure(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, uint16 creatorFeeBps, uint16 launcherFeeBps, uint16 transferTaxBps, uint24 lpFeePips)", + "QuoteSwapFeesAccrued(bytes32 indexed poolId, address indexed swapSender, address indexed quoteAsset, bool isBuy, uint256 grossQuoteAmount, uint256 creatorFee, uint256 launcherFee)", + "CreatorFeesClaimed(bytes32 indexed poolId, address indexed rewardVault, address indexed quoteAsset, address caller, uint256 amount)", + "LauncherFeesClaimed(address indexed treasury, address indexed recipient, address indexed quoteAsset, address caller, uint256 amount)", + ], + StockV2V3RewardVaultFactory: [ + "QuoteAssetFeeSplitVaultDeployed(address indexed vault, address indexed feeHook, bytes32 indexed poolId, address quoteAsset)", + ], + StockV2V3RewardVault: [ + "PayoutAddressUpdated(address indexed beneficiary, address indexed previousPayoutAddress, address indexed newPayoutAddress)", + "BeneficiaryFeesClaimed(address indexed beneficiary, address indexed payoutAddress, address indexed quoteAsset, uint256 amount, uint256 beneficiaryTotalClaimed, uint256 vaultTotalReceived)", + ], +} as const satisfies Readonly>; + +type CanonicalValue = + | null + | boolean + | number + | string + | CanonicalValue[] + | { [key: string]: CanonicalValue }; + +function isPlainRecord(value: unknown): value is Record { + const prototype = + typeof value === "object" && value !== null + ? Object.getPrototypeOf(value) + : undefined; + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + (prototype === Object.prototype || prototype === null) + ); +} + +type CanonicalizationSource = "local-decode" | "provider"; + +function canonicalInteger( + value: unknown, + signed: boolean, + source: CanonicalizationSource, +): string { + if (source === "local-decode") { + if (typeof value === "bigint") return value.toString(); + if (typeof value === "number" && Number.isSafeInteger(value)) { + return value.toString(); + } + throw new TypeError("decoded ABI integer is not an exact integer"); + } + + const pattern = signed + ? /^(?:0|[1-9]\d*|-[1-9]\d*)$/u + : /^(?:0|[1-9]\d*)$/u; + if (typeof value !== "string" || !pattern.test(value)) { + throw new TypeError("provider ABI integer is not a canonical decimal string"); + } + return value; +} + +function canonicalHex( + value: unknown, + pattern: RegExp, + source: CanonicalizationSource, +): string { + if (typeof value !== "string" || !pattern.test(value)) { + throw new TypeError("ABI hexadecimal value has invalid width"); + } + const lowercase = value.toLowerCase(); + if (source === "provider" && value !== lowercase) { + throw new TypeError("provider ABI hexadecimal value is not lowercase"); + } + return lowercase; +} + +function arrayItemParameter( + parameter: AbiParameter, + itemType: string, +): AbiParameter { + return { ...parameter, type: itemType } as AbiParameter; +} + +function canonicalizeAbiValue( + parameter: AbiParameter, + value: unknown, + source: CanonicalizationSource, +): CanonicalValue { + const array = /^(.*)\[([0-9]*)\]$/u.exec(parameter.type); + if (array) { + if (!Array.isArray(value)) { + throw new TypeError("ABI array value is not an array"); + } + if (array[2] !== "" && value.length !== Number(array[2])) { + throw new TypeError("ABI fixed array length does not match"); + } + const item = arrayItemParameter(parameter, array[1]); + return value.map((entry) => + canonicalizeAbiValue(item, entry, source), + ); + } + + if (/^uint(?:[0-9]+)?$/u.test(parameter.type)) { + return canonicalInteger(value, false, source); + } + if (/^int(?:[0-9]+)?$/u.test(parameter.type)) { + return canonicalInteger(value, true, source); + } + if (parameter.type === "address") { + return canonicalHex(value, /^0x[0-9a-fA-F]{40}$/u, source); + } + if (parameter.type === "bytes") { + return canonicalHex(value, /^0x(?:[0-9a-fA-F]{2})*$/u, source); + } + const fixedBytes = /^bytes([1-9]|[12][0-9]|3[0-2])$/u.exec( + parameter.type, + ); + if (fixedBytes) { + return canonicalHex( + value, + new RegExp(`^0x[0-9a-fA-F]{${Number(fixedBytes[1]) * 2}}$`, "u"), + source, + ); + } + if (parameter.type === "bool") { + if (typeof value !== "boolean") { + throw new TypeError("ABI bool value is not a boolean"); + } + return value; + } + if (parameter.type === "string") { + if (typeof value !== "string") { + throw new TypeError("ABI string value is not a string"); + } + return value; + } + if (parameter.type === "tuple") { + if (!("components" in parameter) || !parameter.components) { + throw new TypeError("ABI tuple is missing components"); + } + const components = parameter.components; + const hasNamedComponents = components.every( + (component) => component.name !== undefined && component.name !== "", + ); + if (!hasNamedComponents) { + if (!Array.isArray(value) || value.length !== components.length) { + throw new TypeError("unnamed ABI tuple must be a positional array"); + } + return components.map((component, index) => + canonicalizeAbiValue(component, value[index], source), + ); + } + if (!isPlainRecord(value)) { + throw new TypeError("named ABI tuple must be an object"); + } + return canonicalizeNamedAbiValues(components, value, source); + } + + throw new TypeError(`unsupported ABI parameter type: ${parameter.type}`); +} + +function canonicalizeNamedAbiValues( + parameters: readonly AbiParameter[], + value: Record, + source: CanonicalizationSource, +): Record { + if ( + parameters.some( + (parameter) => parameter.name === undefined || parameter.name === "", + ) + ) { + throw new TypeError("event and named tuple ABI parameters must be named"); + } + const namedParameters = parameters as readonly (AbiParameter & { + name: string; + })[]; + const expectedKeys = namedParameters + .map((parameter) => parameter.name) + .sort((left, right) => left.localeCompare(right)); + const actualKeys = Object.keys(value); + const sortedActualKeys = [...actualKeys].sort((left, right) => + left.localeCompare(right), + ); + if ( + actualKeys.length !== expectedKeys.length || + sortedActualKeys.some((key, index) => key !== expectedKeys[index]) || + (source === "provider" && + actualKeys.some((key, index) => key !== expectedKeys[index])) + ) { + throw new TypeError("ABI object keys do not match canonical parameter keys"); + } + + return Object.fromEntries( + namedParameters + .map( + (parameter) => + [ + parameter.name, + canonicalizeAbiValue( + parameter, + value[parameter.name], + source, + ), + ] as const, + ) + .sort(([left], [right]) => left.localeCompare(right)), + ); +} + +export function canonicalizeAbiEventArguments( + parameters: readonly AbiParameter[], + value: unknown, +): Record { + if (!isPlainRecord(value)) { + throw new TypeError("decoded event arguments must be an object"); + } + return canonicalizeNamedAbiValues(parameters, value, "local-decode"); +} + +function validateProviderEventArguments( + parameters: readonly AbiParameter[], + value: unknown, +): Record { + if (!isPlainRecord(value)) { + throw new TypeError("provider event arguments must be an object"); + } + return canonicalizeNamedAbiValues(parameters, value, "provider"); +} + +const EVENT_ABIS = new Map>(); + +for (const [contractName, signatures] of Object.entries( + PROGRAMMABLE_EVENT_SIGNATURES, +)) { + const events = new Map(); + for (const signature of signatures) { + const item = parseAbiItem(`event ${signature}`); + if (item.type !== "event" || events.has(item.name)) { + throw new TypeError("invalid or overloaded runtime event manifest"); + } + events.set(item.name, item); + } + EVENT_ABIS.set(contractName, events); +} + +export function decodeManifestEvent(input: { + contractName: string; + eventName: string; + topics: readonly Hex[]; + data: Hex; + providerPayload: unknown; +}): Record { + const event = EVENT_ABIS.get(input.contractName)?.get(input.eventName); + if (!event || event.name !== input.eventName) { + throw new TypeError("event is not authorized for this contract"); + } + + const indexedInputCount = event.inputs.filter( + (parameter) => "indexed" in parameter && parameter.indexed === true, + ).length; + if (input.topics.length !== indexedInputCount + 1) { + throw new TypeError("event indexed topic count does not match ABI"); + } + if (input.topics[0]?.toLowerCase() !== toEventSelector(event)) { + throw new TypeError("event signature topic does not match ABI"); + } + const topics = [input.topics[0], ...input.topics.slice(1)] as [ + Hex, + ...Hex[], + ]; + + const decoded = decodeEventLog({ + abi: [event], + eventName: event.name, + topics, + data: input.data, + strict: true, + }); + if (decoded.eventName !== event.name || !isPlainRecord(decoded.args)) { + throw new TypeError("event ABI decode did not return named arguments"); + } + + const localPayload = canonicalizeAbiEventArguments( + event.inputs, + decoded.args, + ); + const providerPayload = validateProviderEventArguments( + event.inputs, + input.providerPayload, + ); + if ( + JSON.stringify(localPayload) !== JSON.stringify(providerPayload) + ) { + throw new TypeError("provider event payload does not match local decode"); + } + + return localPayload as Record; +} + +export function manifestEventSelectors( + contractName: string, +): readonly Hex[] { + const events = EVENT_ABIS.get(contractName); + if (!events) throw new TypeError("contract is not in the runtime event manifest"); + return Object.freeze( + [...events.values()] + .map((event) => toEventSelector(event)) + .sort((left, right) => left.localeCompare(right)), + ); +} diff --git a/lib/data-pipeline/market-projector-runtime.server.ts b/lib/data-pipeline/market-projector-runtime.server.ts new file mode 100644 index 00000000..740eee84 --- /dev/null +++ b/lib/data-pipeline/market-projector-runtime.server.ts @@ -0,0 +1,1993 @@ +import "server-only"; + +import { randomUUID } from "node:crypto"; + +import { keccak256, toBytes, type Hex } from "viem"; + +import { + bytes32FromBytea, + canonicalBytes32, + hexToBytes, + parseNonnegativeIntegerText, + type HexAddress, + type HexBytes32, +} from "./codecs"; +import { loadDataPipelineConfig } from "./config"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import { + createPostgresExecutor, + type PostgresExecutor, + type PostgresTransaction, +} from "./postgres"; +import { + validatedPostgresConnectionString, + validatedPostgresSslCa, +} from "./postgres-connection.server"; +import { boundedJsonRequest, type DataPipelineFetcher } from "./request"; +import { getDataPipelineReleaseBinding } from "./release-binding.server"; +import { + assertProductionDualRpcProviders, + createProductionDualRpcProviders, +} from "./rpc-providers.server"; +import { + createUniswapAnalyticsClient, + OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + OFFICIAL_V4_SUBGRAPH_ID, + priceRatiosFromSqrtPriceX96, + UNISWAP_ANALYTICS_QUERY_CONTRACT, + type AnalyticsResult, + type CandleAnalytics, + type PoolSnapshot, + type VerifiedPoolKey, +} from "./uniswap"; + +type Environment = Readonly>; + +const CHAIN_ID = 1; +const GRAPH_GATEWAY = "https://gateway.thegraph.com"; +const MARKET_PROJECTOR_VERSION = "market-projector-v1"; +const MARKET_PROJECTOR_LOGIN_ROLE = "programmable_reconciler_login"; +const MARKET_PROJECTOR_CAPABILITY_ROLE = "programmable_reconciler"; +const CHAINLINK_ETH_USD = + "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419" as HexAddress; +const LATEST_ROUND_DATA_SELECTOR = "0xfeaf968c"; +const MAXIMUM_CLOSE_BLOCKS = 8; +const MAXIMUM_POOLS_PER_CYCLE = 4; +const PROVIDER_CONCURRENCY = 4; +const MAXIMUM_PENDING_POOLS_PER_SCOPE = 4; +const DEADLINE_MS = 75_000; +const CLOSE_RESERVE_MS = 5_000; +const DECIMAL_SCALE = 36; +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u; +const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u; +const HEX32_PATTERN = /^0x[0-9a-f]{64}$/u; +const BROWSER_FORBIDDEN = [ + "NEXT_PUBLIC_PROGRAMMABLE_RECONCILER_DATABASE_URL", + "NEXT_PUBLIC_PROGRAMMABLE_UNISWAP_GRAPH_API_KEY", + "NEXT_PUBLIC_PROGRAMMABLE_UNISWAP_GRAPH_REDACTED_IDENTITY", + "NEXT_PUBLIC_PROGRAMMABLE_UNISWAP_GRAPH_DEPLOYMENT_COMMITMENT", + "NEXT_PUBLIC_PROGRAMMABLE_UNISWAP_GRAPH_SCHEMA_COMMITMENT", +] as const; + +export const MARKET_GRAPH_QUERY_CONTRACT = Object.freeze({ + gateway: GRAPH_GATEWAY, + subgraphId: OFFICIAL_V4_SUBGRAPH_ID, + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + metadata: Object.freeze({ + requireDeployment: true, + requireNoIndexingErrors: true, + requireExactBlockNumberAndHash: true, + }), + analytics: UNISWAP_ANALYTICS_QUERY_CONTRACT, +}); + +export const MARKET_GRAPH_DEPLOYMENT_COMMITMENT = commitment( + "graph-deployment-binding", + { + gateway: MARKET_GRAPH_QUERY_CONTRACT.gateway, + subgraphId: MARKET_GRAPH_QUERY_CONTRACT.subgraphId, + deployment: MARKET_GRAPH_QUERY_CONTRACT.deployment, + }, +); +export const MARKET_GRAPH_SCHEMA_COMMITMENT = commitment( + "graph-query-contract", + MARKET_GRAPH_QUERY_CONTRACT, +); + +const RELEASE_SCOPES = Object.freeze([ + Object.freeze({ + releaseId: "classic-v2", + modelId: "classic", + sourceGroup: "core", + }), + Object.freeze({ + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + }), + Object.freeze({ + releaseId: "stock-paired-v1", + modelId: "stock-paired", + sourceGroup: "core", + }), + Object.freeze({ + releaseId: "stock-paired-v2", + modelId: "stock-paired", + sourceGroup: "core", + }), + Object.freeze({ + releaseId: "stock-paired-v3", + modelId: "stock-paired", + sourceGroup: "core", + }), +]); + +export type MarketProjectorScope = (typeof RELEASE_SCOPES)[number]; + +export type MarketCursor = Readonly<{ + id: string; + epochId: string; + pointerGeneration: string; + cursorGeneration: string; + reorgGeneration: string; + sourceCheckpointId: string; + sourceCheckpointGeneration: string; + sourceReorgGeneration: string; + blockEvidenceId: string; + blockNumber: string; + blockHash: HexBytes32; + providerCursor: string; + hourCoverageEnd: Date | null; + dayCoverageEnd: Date | null; + advancedAt: Date; +}>; + +export type MarketPoolKey = Readonly< + Omit & { poolId: HexBytes32 } +>; + +export type MarketPoolPlan = Readonly<{ + scope: MarketProjectorScope; + epochId: string; + pointerGeneration: string; + sourceCheckpointId: string; + sourceCheckpointGeneration: string; + sourceReorgGeneration: string; + sourceCheckpointBlockNumber: string; + sourceCheckpointBlockHash: HexBytes32; + sourceCheckpointBlockEvidenceId: string; + token: HexAddress; + poolKey: MarketPoolKey; + totalSupply: string; + launchBlockNumber: string; + launchBlockTimestamp: Date; + cursor: MarketCursor | null; +}>; + +export type MarketCloseAnchor = Readonly<{ + occurrenceId: string; + logicalEventId: string; + blockEvidenceId: string; + blockNumber: string; + blockHash: HexBytes32; + blockTimestamp: Date; + transactionHash: HexBytes32; + transactionIndex: string; + blockGlobalLogIndex: string; +}>; + +export type VerifiedChainlinkBlock = Readonly<{ + blockNumber: string; + blockHash: HexBytes32; + blockTimestamp: Date; + rawResult: Hex; + feedRoundId: string; + answer: string; + feedUpdatedAt: Date; +}>; + +type PreparedMarketClose = Readonly<{ + anchor: MarketCloseAnchor; + global: VerifiedChainlinkBlock; + snapshot: PoolSnapshot; + token0Price: string; + token1Price: string; + feesUsd: string; +}>; + +type PreparedMarketCandle = Readonly<{ + interval: "hour" | "day"; + periodStart: Date; + periodEnd: Date; + data: CandleAnalytics; +}>; + +export type PreparedMarketPage = Readonly<{ + plan: MarketPoolPlan; + graphProviderId: string; + targetEvidenceId: string; + target: VerifiedChainlinkBlock; + targetSnapshot: PoolSnapshot; + targetToken0Price: string; + targetToken1Price: string; + closes: readonly PreparedMarketClose[]; + candles: readonly PreparedMarketCandle[]; + nextHourCoverageEnd: Date | null; + nextDayCoverageEnd: Date | null; + providerCursor: string; + pageCommitment: HexBytes32; + isReorg: boolean; +}>; + +export type MarketProjectorStore = Readonly<{ + tryAcquireLease(): Promise; + releaseLease(lease: MarketProjectorLease): Promise; + loadPlans(): Promise; + listCloseAnchors( + input: Readonly<{ + plan: MarketPoolPlan; + fromBlockExclusive: string; + toBlockInclusive: string; + limit: number; + }>, + ): Promise; + resolveGraphProvider( + input: Readonly<{ + redactedIdentity: string; + deploymentCommitment: HexBytes32; + schemaCommitment: HexBytes32; + }>, + ): Promise; + commit(page: PreparedMarketPage): Promise; + close(): Promise; +}>; + +export type MarketProjectorLease = Readonly<{ + holderId: string; + generation: string; + tokenHash: HexBytes32; + acquiredAt: Date; + expiresAt: Date; +}>; + +export type MarketRpc = Readonly<{ + readChainlinkBlock( + input: Readonly<{ + blockNumber: string; + expectedBlockHash: HexBytes32; + }>, + ): Promise; +}>; + +export type MarketAnalytics = ReturnType; + +export type MarketProjectorCycleResult = Readonly<{ + status: "committed" | "caught-up" | "idle" | "disabled" | "busy"; + releaseId?: string; + poolId?: HexBytes32; + blockNumber?: string; + lagBlocks: string; + closeCount: number; + candleCount: number; + caughtUp: boolean; +}>; + +export type MarketRpcProviderEvidence = Readonly<{ + identity: string; + endpointCommitment: HexBytes32; + endpointOriginCommitment: HexBytes32; +}>; + +function exactText(value: unknown, pattern: RegExp, operation: string): string { + if ( + typeof value !== "string" || + value.length === 0 || + value.length > 512 || + !pattern.test(value) + ) { + throw validationError("postgres", operation); + } + return value; +} + +function uuid(value: unknown, operation = "uuid"): string { + return exactText(value, UUID_PATTERN, operation); +} + +function integer(value: unknown, operation = "integer"): string { + try { + if (typeof value === "bigint") + return parseNonnegativeIntegerText(value.toString()); + if (typeof value === "number") { + if (!Number.isSafeInteger(value)) throw new Error("unsafe"); + return parseNonnegativeIntegerText(String(value)); + } + return parseNonnegativeIntegerText(value); + } catch { + throw validationError("postgres", operation); + } +} + +function byteaAddress(value: unknown): HexAddress { + if (!(value instanceof Uint8Array) || value.byteLength !== 20) { + throw validationError("postgres", "address"); + } + return `0x${Buffer.from(value).toString("hex")}` as HexAddress; +} + +function byteaBytes32(value: unknown): HexBytes32 { + try { + return bytes32FromBytea(value); + } catch { + throw validationError("postgres", "bytes32"); + } +} + +function date(value: unknown, operation = "timestamp"): Date { + const parsed = + value instanceof Date ? new Date(value) : new Date(String(value)); + if (!Number.isFinite(parsed.getTime())) + throw validationError("postgres", operation); + return parsed; +} + +function nullableDate(value: unknown): Date | null { + return value === null ? null : date(value); +} + +function canonicalJson(value: unknown): string { + if (value instanceof Date) { + if (!Number.isFinite(value.getTime())) + throw invalidInput("config", "canonical-date"); + return JSON.stringify(value.toISOString()); + } + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value)) + throw invalidInput("config", "canonical-number"); + return JSON.stringify(value); + } + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (typeof value === "object") { + const entries = Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`); + return `{${entries.join(",")}}`; + } + throw invalidInput("config", "canonical-value"); +} + +function commitment(domain: string, value: unknown): HexBytes32 { + return keccak256( + toBytes( + `programmable:market-projector:${domain}:v1\0${canonicalJson(value)}`, + ), + ); +} + +function deterministicUuid( + domain: string, + ...values: readonly string[] +): string { + const digits = commitment(domain, values).slice(2, 34).split(""); + digits[12] = "8"; + digits[16] = ((Number.parseInt(digits[16]!, 16) & 0x3) | 0x8).toString(16); + const hex = digits.join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +function exactDecimalRatio( + numeratorText: string, + denominatorText: string, +): string { + const numerator = BigInt(integer(numeratorText, "ratio-numerator")); + const denominator = BigInt(integer(denominatorText, "ratio-denominator")); + if (denominator === 0n) throw validationError("uniswap", "zero-ratio"); + const whole = numerator / denominator; + const remainder = numerator % denominator; + if (remainder === 0n) return whole.toString(); + const scaled = (remainder * 10n ** BigInt(DECIMAL_SCALE)) / denominator; + const fraction = scaled + .toString() + .padStart(DECIMAL_SCALE, "0") + .replace(/0+$/u, ""); + return fraction.length === 0 ? whole.toString() : `${whole}.${fraction}`; +} + +function prices(snapshot: PoolSnapshot, poolKey: VerifiedPoolKey) { + const ratios = priceRatiosFromSqrtPriceX96({ + sqrtPriceX96: snapshot.sqrtPriceX96, + token0Decimals: poolKey.token0Decimals, + token1Decimals: poolKey.token1Decimals, + }); + return Object.freeze({ + token0Price: exactDecimalRatio( + ratios.token1PerToken0.numerator, + ratios.token1PerToken0.denominator, + ), + token1Price: exactDecimalRatio( + ratios.token0PerToken1.numerator, + ratios.token0PerToken1.denominator, + ), + }); +} + +function finiteDuration(startedAt: number): number { + return Math.max(0, Math.min(86_400_000, Date.now() - startedAt)); +} + +function exactResult( + value: AnalyticsResult, + expectedBlock: Readonly<{ number: string; hash: HexBytes32 }>, +): T { + if (value.status !== "ready") { + throw dataPipelineError({ + dependency: "uniswap", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } + if ( + value.provenance.deployment !== OFFICIAL_V4_SUBGRAPH_DEPLOYMENT || + value.provenance.blockNumber !== expectedBlock.number || + value.provenance.blockHash !== expectedBlock.hash + ) { + throw validationError("uniswap", "market-query-provenance"); + } + return value.data; +} + +function floorDate(value: Date, seconds: number): Date { + const unix = Math.floor(value.getTime() / 1_000); + return new Date(Math.floor(unix / seconds) * seconds * 1_000); +} + +function plusSeconds(value: Date, seconds: number): Date { + return new Date(value.getTime() + seconds * 1_000); +} + +function dateSeconds(value: Date): number { + const seconds = value.getTime() / 1_000; + if ( + !Number.isSafeInteger(seconds) || + seconds < 0 || + seconds > 2_147_483_647 + ) { + throw invalidInput("config", "timestamp-range"); + } + return seconds; +} + +function parseCursor(row: Record): MarketCursor { + return Object.freeze({ + id: uuid(row.market_cursor_id), + epochId: uuid(row.epoch_id), + pointerGeneration: integer(row.pointer_generation), + cursorGeneration: integer(row.cursor_generation), + reorgGeneration: integer(row.reorg_generation), + sourceCheckpointId: uuid(row.source_checkpoint_id), + sourceCheckpointGeneration: integer(row.source_checkpoint_generation), + sourceReorgGeneration: integer(row.source_reorg_generation), + blockEvidenceId: uuid(row.block_evidence_id), + blockNumber: integer(row.block_number), + blockHash: byteaBytes32(row.block_hash), + providerCursor: exactText( + row.provider_cursor, + /^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$/u, + "provider-cursor", + ), + hourCoverageEnd: nullableDate(row.hour_coverage_end), + dayCoverageEnd: nullableDate(row.day_coverage_end), + advancedAt: date(row.advanced_at), + }); +} + +function parsePoolCursor(row: Record): MarketCursor | null { + if (row.market_cursor_id === null || row.market_cursor_id === undefined) { + return null; + } + return parseCursor({ + market_cursor_id: row.market_cursor_id, + epoch_id: row.cursor_epoch_id, + pointer_generation: row.cursor_pointer_generation, + cursor_generation: row.cursor_generation, + reorg_generation: row.cursor_reorg_generation, + source_checkpoint_id: row.cursor_source_checkpoint_id, + source_checkpoint_generation: row.cursor_source_checkpoint_generation, + source_reorg_generation: row.cursor_source_reorg_generation, + block_evidence_id: row.cursor_block_evidence_id, + block_number: row.cursor_block_number, + block_hash: row.cursor_block_hash, + provider_cursor: row.provider_cursor, + hour_coverage_end: row.hour_coverage_end, + day_coverage_end: row.day_coverage_end, + page_commitment: row.page_commitment, + advanced_at: row.advanced_at, + }); +} + +function marketGatewayIdentityFailure(): DataPipelineError { + return invalidInput("postgres", "market-gateway-membership"); +} + +async function assertMarketGatewayLogin( + transaction: PostgresTransaction, +): Promise { + const rows = await transaction.query<{ session_user: unknown }>( + "select session_user::text as session_user", + ); + if ( + rows.length !== 1 || + rows[0]?.session_user !== MARKET_PROJECTOR_LOGIN_ROLE + ) { + throw marketGatewayIdentityFailure(); + } +} + +async function assumeAndVerifyMarketCapabilityRole( + transaction: PostgresTransaction, +): Promise { + try { + await transaction.query("set local role programmable_reconciler"); + } catch { + throw marketGatewayIdentityFailure(); + } + await transaction.query("set local statement_timeout = '900ms'"); + await transaction.query("set local lock_timeout = '200ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '2000ms'", + ); + const rows = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + if ( + rows.length !== 1 || + rows[0]?.session_user !== MARKET_PROJECTOR_LOGIN_ROLE || + rows[0]?.current_role !== MARKET_PROJECTOR_CAPABILITY_ROLE + ) { + throw marketGatewayIdentityFailure(); + } +} + +export function createMarketProjectorDatabaseGateway(input: { + executor: PostgresExecutor; +}) { + return Object.freeze({ + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return input.executor.transaction(async (transaction) => { + await assertMarketGatewayLogin(transaction); + await assumeAndVerifyMarketCapabilityRole(transaction); + return work(transaction); + }); + }, + }); +} + +export function createPostgresMarketProjectorStore( + input: Readonly<{ + executor: PostgresExecutor; + sourceProjectorVersion: string; + rpcProviders: readonly [ + MarketRpcProviderEvidence, + MarketRpcProviderEvidence, + ]; + marketProjectorVersion?: string; + uuid?: () => string; + now?: () => Date; + }>, +): MarketProjectorStore { + const marketProjectorVersion = + input.marketProjectorVersion ?? MARKET_PROJECTOR_VERSION; + const nextUuid = input.uuid ?? randomUUID; + const now = input.now ?? (() => new Date()); + const gateway = createMarketProjectorDatabaseGateway({ + executor: input.executor, + }); + let activeLease: MarketProjectorLease | null = null; + + return Object.freeze({ + async tryAcquireLease() { + if (activeLease) throw invalidInput("postgres", "market-lease-active"); + const requestedAt = now(); + if (!Number.isFinite(requestedAt.valueOf())) { + throw invalidInput("postgres", "market-lease-time"); + } + const holderId = `market-projector:${nextUuid()}`; + const tokenHash = keccak256( + toBytes(`programmable:market-projector:lease-token:v1:${nextUuid()}`), + ); + const requestedExpiry = new Date(requestedAt.valueOf() + 90_000); + const inputCommitment = commitment("runtime-lease-acquire", { + holderId, + tokenHash, + requestedAt: requestedAt.toISOString(), + requestedExpiry: requestedExpiry.toISOString(), + }); + const rows = await gateway.transaction(async (transaction) => { + return transaction.query( + "select * from programmable_private.try_acquire_market_projector_runtime_lease_v1($1,$2::bytea,$3::timestamptz,$4::timestamptz,$5::bytea)", + [ + holderId, + hexToBytes(tokenHash), + requestedAt.toISOString(), + requestedExpiry.toISOString(), + hexToBytes(inputCommitment), + ], + ); + }); + if (rows.length !== 1 || typeof rows[0]?.acquired !== "boolean") { + throw validationError("postgres", "market-lease-cardinality"); + } + if (rows[0]!.acquired === false) return null; + activeLease = Object.freeze({ + holderId, + generation: integer(rows[0]!.lease_generation), + tokenHash, + acquiredAt: date(rows[0]!.acquired_at), + expiresAt: date(rows[0]!.expires_at), + }); + return activeLease; + }, + + async releaseLease(lease) { + if ( + !activeLease || + lease.holderId !== activeLease.holderId || + lease.generation !== activeLease.generation || + lease.tokenHash !== activeLease.tokenHash + ) { + throw invalidInput("postgres", "market-lease-release"); + } + const releasedAt = now(); + if (!Number.isFinite(releasedAt.valueOf())) { + throw invalidInput("postgres", "market-lease-time"); + } + const inputCommitment = commitment("runtime-lease-release", { + holderId: lease.holderId, + generation: lease.generation, + tokenHash: lease.tokenHash, + releasedAt: releasedAt.toISOString(), + }); + const rows = await gateway.transaction(async (transaction) => { + return transaction.query<{ released: unknown }>( + "select programmable_private.release_market_projector_runtime_lease_v1($1,$2::bigint,$3::bytea,$4::timestamptz,$5::bytea) as released", + [ + lease.holderId, + lease.generation, + hexToBytes(lease.tokenHash), + releasedAt.toISOString(), + hexToBytes(inputCommitment), + ], + ); + }); + if (rows.length !== 1 || rows[0]?.released !== true) { + throw validationError("postgres", "market-lease-release"); + } + activeLease = null; + }, + + async loadPlans() { + return gateway.transaction(async (transaction) => { + const plans: MarketPoolPlan[] = []; + for (const scope of RELEASE_SCOPES) { + const rows = await transaction.query( + "select * from programmable_private.list_market_projector_pools_v1($1::bigint,$2,$3,$4,$5,$6,$7::integer)", + [ + CHAIN_ID, + scope.releaseId, + scope.modelId, + scope.sourceGroup, + input.sourceProjectorVersion, + marketProjectorVersion, + MAXIMUM_PENDING_POOLS_PER_SCOPE, + ], + ); + for (const row of rows) { + const poolId = byteaBytes32(row.pool_id); + plans.push( + Object.freeze({ + scope, + epochId: uuid(row.epoch_id), + pointerGeneration: integer(row.pointer_generation), + sourceCheckpointId: uuid(row.source_checkpoint_id), + sourceCheckpointGeneration: integer( + row.source_checkpoint_generation, + ), + sourceReorgGeneration: integer(row.source_reorg_generation), + sourceCheckpointBlockNumber: integer( + row.source_checkpoint_block_number, + ), + sourceCheckpointBlockHash: byteaBytes32( + row.source_checkpoint_block_hash, + ), + sourceCheckpointBlockEvidenceId: uuid( + row.source_checkpoint_block_evidence_id, + ), + token: byteaAddress(row.token), + poolKey: Object.freeze({ + poolId, + currency0: byteaAddress(row.currency0), + currency1: byteaAddress(row.currency1), + hooks: byteaAddress(row.hook), + fee: Number(integer(row.pool_key_fee)), + tickSpacing: Number(integer(row.tick_spacing)), + token0Decimals: Number(integer(row.token0_decimals)), + token1Decimals: Number(integer(row.token1_decimals)), + }), + totalSupply: integer(row.total_supply), + launchBlockNumber: integer(row.launch_block_number), + launchBlockTimestamp: date(row.launch_block_timestamp), + cursor: parsePoolCursor(row), + }), + ); + } + } + return Object.freeze(plans); + }); + }, + + async listCloseAnchors({ + plan, + fromBlockExclusive, + toBlockInclusive, + limit, + }) { + return gateway.transaction(async (transaction) => { + const rows = await transaction.query( + "select * from programmable_private.list_market_close_anchors_v1($1::bigint,$2,$3,$4,$5,$6::bytea,$7::numeric,$8::numeric,$9::integer,$10::numeric)", + [ + CHAIN_ID, + plan.scope.releaseId, + plan.scope.modelId, + plan.scope.sourceGroup, + input.sourceProjectorVersion, + hexToBytes(plan.poolKey.poolId), + fromBlockExclusive, + toBlockInclusive, + limit, + null, + ], + ); + return Object.freeze( + rows.map((row) => + Object.freeze({ + occurrenceId: uuid(row.occurrence_id), + logicalEventId: uuid(row.logical_event_id), + blockEvidenceId: uuid(row.block_evidence_id), + blockNumber: integer(row.block_number), + blockHash: byteaBytes32(row.block_hash), + blockTimestamp: date(row.block_timestamp), + transactionHash: byteaBytes32(row.transaction_hash), + transactionIndex: integer(row.transaction_index), + blockGlobalLogIndex: integer(row.block_global_log_index), + }), + ), + ); + }); + }, + + async resolveGraphProvider(provider) { + return gateway.transaction(async (transaction) => { + const rows = await transaction.query<{ id: unknown }>( + "select programmable_private.resolve_market_graph_provider_v1($1,$2::bytea,$3::bytea) as id", + [ + provider.redactedIdentity, + hexToBytes(provider.deploymentCommitment), + hexToBytes(provider.schemaCommitment), + ], + ); + if (rows.length !== 1) + throw validationError("postgres", "graph-provider-cardinality"); + return uuid(rows[0]!.id, "graph-provider-id"); + }); + }, + + async commit(page) { + const startedAt = Date.now(); + return gateway.transaction(async (transaction) => { + if (!activeLease) { + throw validationError("postgres", "market-lease-missing"); + } + const leaseRows = await transaction.query<{ valid: unknown }>( + "select programmable_private.assert_market_projector_runtime_lease_v1($1,$2::bigint,$3::bytea) as valid", + [ + activeLease.holderId, + activeLease.generation, + hexToBytes(activeLease.tokenHash), + ], + ); + if (leaseRows.length !== 1 || leaseRows[0]?.valid !== true) { + throw validationError("postgres", "market-lease-expired"); + } + const plan = page.plan; + const pageHex = page.pageCommitment; + const runId = deterministicUuid("run", plan.epochId, pageHex); + const reconciliationId = deterministicUuid("reconciliation", runId); + const outcomeId = deterministicUuid("outcome", runId); + const telemetryId = deterministicUuid("telemetry", runId); + const cursorId = deterministicUuid( + "cursor", + plan.epochId, + plan.poolKey.poolId, + pageHex, + ); + const currentCursorGeneration = plan.cursor?.cursorGeneration ?? "0"; + const currentReorgGeneration = plan.cursor?.reorgGeneration ?? "0"; + const nextCursorGeneration = ( + BigInt(currentCursorGeneration) + 1n + ).toString(); + const nextReorgGeneration = page.isReorg + ? (BigInt(currentReorgGeneration) + 1n).toString() + : currentReorgGeneration; + const sourceFromBlock = page.isReorg + ? plan.launchBlockNumber + : (plan.cursor?.blockNumber ?? plan.launchBlockNumber); + const comparedCount = 1 + page.closes.length + page.candles.length; + const now = new Date(); + + await transaction.query( + "select programmable_private.open_run($1::uuid,'reconciliation',$2::bigint,$3,$4,$5,$6::uuid,$7::bigint,$8,$9::bytea,$10::timestamptz)", + [ + runId, + CHAIN_ID, + plan.scope.releaseId, + plan.scope.modelId, + plan.scope.sourceGroup, + plan.epochId, + plan.pointerGeneration, + marketProjectorVersion, + hexToBytes(pageHex), + now, + ], + ); + await transaction.query( + "select programmable_private.append_reconciliation_record($1::uuid,$2::uuid,$3,$4,$5::numeric,$6::numeric,$7::bigint,0,$8::bytea,array[]::bytea[],null,$9::timestamptz)", + [ + reconciliationId, + runId, + "market-source-match", + "info", + sourceFromBlock, + page.target.blockNumber, + comparedCount, + hexToBytes( + commitment("reconciliation", { + page: pageHex, + graphProviderId: page.graphProviderId, + block: page.target.blockNumber, + }), + ), + now, + ], + ); + + const globalIds = new Map(); + const ensureGlobal = async ( + evidenceId: string, + block: VerifiedChainlinkBlock, + ) => { + const cached = globalIds.get(evidenceId); + if (cached) return cached; + const existingRows = await transaction.query<{ id: unknown }>( + "select programmable_private.get_market_global_snapshot_v1($1::uuid,$2::uuid) as id", + [reconciliationId, evidenceId], + ); + const existing = existingRows[0]?.id; + if (existing !== null && existing !== undefined) { + const existingId = uuid(existing, "global-market-id"); + globalIds.set(evidenceId, existingId); + return existingId; + } + const contextRows = await transaction.query>( + "select * from programmable_private.get_market_block_evidence_context_v1($1::uuid,$2::uuid)", + [reconciliationId, evidenceId], + ); + if (contextRows.length !== 1) + throw validationError("postgres", "evidence-context-cardinality"); + const providerA = uuid(contextRows[0]!.provider_a_id); + const providerB = uuid(contextRows[0]!.provider_b_id); + if ( + integer(contextRows[0]!.block_number) !== block.blockNumber || + byteaBytes32(contextRows[0]!.block_hash) !== block.blockHash || + exactText( + contextRows[0]!.provider_a_identity, + IDENTIFIER_PATTERN, + "rpc-provider-identity", + ) !== input.rpcProviders[0].identity || + exactText( + contextRows[0]!.provider_b_identity, + IDENTIFIER_PATTERN, + "rpc-provider-identity", + ) !== input.rpcProviders[1].identity || + byteaBytes32(contextRows[0]!.provider_a_endpoint_commitment) !== + input.rpcProviders[0].endpointCommitment || + byteaBytes32(contextRows[0]!.provider_b_endpoint_commitment) !== + input.rpcProviders[1].endpointCommitment || + byteaBytes32(contextRows[0]!.provider_a_origin_commitment) !== + input.rpcProviders[0].endpointOriginCommitment || + byteaBytes32(contextRows[0]!.provider_b_origin_commitment) !== + input.rpcProviders[1].endpointOriginCommitment + ) + throw validationError("postgres", "evidence-context-mismatch"); + const id = deterministicUuid( + "global", + plan.epochId, + plan.pointerGeneration, + block.blockHash, + ); + const sourceCommitment = commitment("chainlink-query", { + feed: CHAINLINK_ETH_USD, + selector: LATEST_ROUND_DATA_SELECTOR, + blockNumber: block.blockNumber, + blockHash: block.blockHash, + }); + const resultCommitment = commitment("chainlink-result", { + sourceCommitment, + rawResult: block.rawResult, + }); + await transaction.query( + "select programmable_private.append_global_eth_usd_snapshot_v1($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,$6::numeric,$7::numeric,8,$8::timestamptz,$9::bytea,$10::bytea,$11::bytea,$12::bytea,$13::timestamptz)", + [ + id, + reconciliationId, + evidenceId, + providerA, + providerB, + block.feedRoundId, + block.answer, + block.feedUpdatedAt, + hexToBytes(block.rawResult), + hexToBytes(block.rawResult), + hexToBytes(sourceCommitment), + hexToBytes(resultCommitment), + block.blockTimestamp, + ], + ); + globalIds.set(evidenceId, id); + return id; + }; + + for (const close of page.closes) { + const globalId = await ensureGlobal( + close.anchor.blockEvidenceId, + close.global, + ); + const closeId = deterministicUuid( + "close", + plan.epochId, + plan.pointerGeneration, + plan.poolKey.poolId, + close.anchor.blockHash, + close.anchor.occurrenceId, + ); + const sourceQueryCommitment = commitment("close-query", { + deployment: page.graphProviderId, + poolId: plan.poolKey.poolId, + blockNumber: close.anchor.blockNumber, + blockHash: close.anchor.blockHash, + }); + const closeCommitment = commitment("close", { + sourceQueryCommitment, + occurrenceId: close.anchor.occurrenceId, + snapshot: close.snapshot, + token0Price: close.token0Price, + token1Price: close.token1Price, + feesUsd: close.feesUsd, + }); + await transaction.query( + "select programmable_private.append_market_block_close_v2($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::bytea,$6::uuid,$7::numeric,$8::numeric,$9::integer,$10::numeric,$11::numeric,$12::numeric,$13::numeric,$14::numeric,$15::numeric,$16::numeric,$17::bigint,$18::uuid,$19::bytea,$20::bytea,$21::timestamptz)", + [ + closeId, + reconciliationId, + page.graphProviderId, + close.anchor.blockEvidenceId, + hexToBytes(plan.poolKey.poolId), + close.anchor.occurrenceId, + close.snapshot.sqrtPriceX96, + close.snapshot.liquidity, + close.snapshot.tick, + close.token0Price, + close.token1Price, + close.snapshot.marketVolumeToken0, + close.snapshot.marketVolumeToken1, + close.snapshot.marketVolumeUsd, + close.feesUsd, + close.snapshot.totalValueLockedUsd, + close.snapshot.transactionCount, + globalId, + hexToBytes(sourceQueryCommitment), + hexToBytes(closeCommitment), + close.global.blockTimestamp, + ], + ); + } + + const targetGlobalId = await ensureGlobal( + page.targetEvidenceId, + page.target, + ); + const marketSnapshotId = deterministicUuid( + "snapshot", + plan.epochId, + plan.pointerGeneration, + plan.poolKey.poolId, + page.target.blockHash, + ); + const snapshotInput = commitment("snapshot", { + deployment: page.graphProviderId, + poolId: plan.poolKey.poolId, + blockNumber: page.target.blockNumber, + blockHash: page.target.blockHash, + snapshot: page.targetSnapshot, + }); + await transaction.query( + "select programmable_private.append_market_snapshot_v2($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::bytea,$6::numeric,$7::bytea,$8::numeric,$9::numeric,$10::numeric,$11::numeric,$12::numeric,null,$13::timestamptz,$14::bytea)", + [ + marketSnapshotId, + reconciliationId, + page.graphProviderId, + page.targetEvidenceId, + hexToBytes(plan.poolKey.poolId), + page.target.blockNumber, + hexToBytes(page.target.blockHash), + page.targetSnapshot.sqrtPriceX96, + page.targetSnapshot.liquidity, + page.targetSnapshot.marketVolumeToken0, + page.targetSnapshot.marketVolumeToken1, + page.targetSnapshot.marketVolumeUsd, + page.target.blockTimestamp, + hexToBytes(snapshotInput), + ], + ); + const snapshotDetail = commitment("snapshot-detail", { + snapshotId: marketSnapshotId, + globalId: targetGlobalId, + tick: page.targetSnapshot.tick, + token0Price: page.targetToken0Price, + token1Price: page.targetToken1Price, + tvlToken0: page.targetSnapshot.totalValueLockedToken0, + tvlToken1: page.targetSnapshot.totalValueLockedToken1, + tvlUsd: page.targetSnapshot.totalValueLockedUsd, + transactionCount: page.targetSnapshot.transactionCount, + }); + await transaction.query( + "select programmable_private.append_market_snapshot_details_v2($1::uuid,$2::uuid,$3,$4::bigint,$5::uuid,$6::integer,$7::numeric,$8::numeric,$9::numeric,$10::numeric,$11::numeric,$12::bigint,$13::bytea,$14::timestamptz)", + [ + marketSnapshotId, + reconciliationId, + marketProjectorVersion, + nextReorgGeneration, + targetGlobalId, + page.targetSnapshot.tick, + page.targetToken0Price, + page.targetToken1Price, + page.targetSnapshot.totalValueLockedToken0, + page.targetSnapshot.totalValueLockedToken1, + page.targetSnapshot.totalValueLockedUsd, + page.targetSnapshot.transactionCount, + hexToBytes(snapshotDetail), + page.target.blockTimestamp, + ], + ); + + for (const candle of page.candles) { + const candleId = deterministicUuid( + "candle", + plan.epochId, + plan.pointerGeneration, + plan.poolKey.poolId, + candle.interval, + candle.periodStart.toISOString(), + page.target.blockHash, + ); + const candleCommitment = commitment("candle", { + deployment: page.graphProviderId, + interval: candle.interval, + periodStart: candle.periodStart.toISOString(), + periodEnd: candle.periodEnd.toISOString(), + data: candle.data, + sourceBlockHash: page.target.blockHash, + }); + await transaction.query( + "select programmable_private.append_market_candle_v2($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::bytea,$6,$7::timestamptz,$8::timestamptz,$9::numeric,$10::numeric,$11::numeric,$12::numeric,$13::numeric,$14::numeric,$15::numeric,$16::bytea,$17::bytea)", + [ + candleId, + reconciliationId, + page.graphProviderId, + page.targetEvidenceId, + hexToBytes(plan.poolKey.poolId), + candle.interval, + candle.periodStart, + candle.periodEnd, + candle.data.open, + candle.data.high, + candle.data.low, + candle.data.close, + candle.data.marketVolumeToken0, + candle.data.marketVolumeToken1, + candle.data.marketVolumeUsd, + hexToBytes(page.target.blockHash), + hexToBytes(candleCommitment), + ], + ); + const closingRows = await transaction.query<{ id: unknown }>( + "select programmable_private.resolve_market_candle_close_v1($1::uuid,$2::bytea,$3::timestamptz,$4::timestamptz) as id", + [ + reconciliationId, + hexToBytes(plan.poolKey.poolId), + candle.periodStart, + candle.periodEnd, + ], + ); + if (closingRows.length !== 1) + throw validationError("postgres", "candle-close-cardinality"); + const closingId = uuid(closingRows[0]!.id, "candle-close-id"); + const detail = commitment("candle-detail", { + candleId, + closingId, + sourceBlockHash: page.target.blockHash, + feesUsd: candle.data.feesUsd, + transactionCount: candle.data.transactionCount, + }); + await transaction.query( + "select programmable_private.append_market_candle_details_v2($1::uuid,$2::uuid,$3,$4::bigint,$5::uuid,$6::numeric,$7::bigint,$8::bytea,$9::timestamptz)", + [ + candleId, + reconciliationId, + marketProjectorVersion, + nextReorgGeneration, + closingId, + candle.data.feesUsd, + candle.data.transactionCount, + hexToBytes(detail), + page.target.blockTimestamp, + ], + ); + } + + await transaction.query( + "select programmable_private.advance_market_projector_cursor_v1($1::uuid,$2::uuid,$3,$4,$5::bytea,$6::bigint,$7::bigint,$8::bigint,$9::bigint,$10::uuid,$11::bigint,$12::bigint,$13::uuid,$14::numeric,$15::bytea,$16,$17::timestamptz,$18::timestamptz,$19::bytea,$20::timestamptz)", + [ + cursorId, + reconciliationId, + input.sourceProjectorVersion, + marketProjectorVersion, + hexToBytes(plan.poolKey.poolId), + currentCursorGeneration, + nextCursorGeneration, + currentReorgGeneration, + nextReorgGeneration, + plan.sourceCheckpointId, + plan.sourceCheckpointGeneration, + plan.sourceReorgGeneration, + page.targetEvidenceId, + page.target.blockNumber, + hexToBytes(page.target.blockHash), + page.providerCursor, + page.nextHourCoverageEnd, + page.nextDayCoverageEnd, + hexToBytes(pageHex), + now, + ], + ); + + const lagBlocks = ( + BigInt(plan.sourceCheckpointBlockNumber) - + BigInt(page.target.blockNumber) + ).toString(); + const caughtUp = lagBlocks === "0"; + const telemetry = Object.freeze({ + closeCount: page.closes.length, + candleCount: page.candles.length, + lagBlocks, + caughtUp, + hourCovered: page.nextHourCoverageEnd?.toISOString() ?? null, + dayCovered: page.nextDayCoverageEnd?.toISOString() ?? null, + }); + await transaction.query( + "select programmable_private.append_run_telemetry($1::uuid,$2::uuid,$3,$4::timestamptz,$5::bigint,$6::bigint,$7::jsonb,$8::boolean)", + [ + telemetryId, + runId, + "market-page", + now, + finiteDuration(startedAt), + comparedCount, + JSON.stringify(telemetry), + page.isReorg, + ], + ); + const resultCommitment = commitment("result", { + page: pageHex, + telemetry, + }); + await transaction.query( + "select programmable_private.append_run_outcome($1::uuid,$2::uuid,'succeeded',$3::bytea,$4::timestamptz)", + [outcomeId, runId, hexToBytes(resultCommitment), now], + ); + return Object.freeze({ + status: caughtUp ? ("caught-up" as const) : ("committed" as const), + releaseId: plan.scope.releaseId, + poolId: plan.poolKey.poolId, + blockNumber: page.target.blockNumber, + lagBlocks, + closeCount: page.closes.length, + candleCount: page.candles.length, + caughtUp, + }); + }); + }, + + close: () => input.executor.close(), + }); +} + +function parseHexQuantity(value: unknown, operation: string): bigint { + if ( + typeof value !== "string" || + !/^0x(?:0|[1-9a-f][0-9a-f]*)$/u.test(value) + ) { + throw validationError("rpc", operation); + } + return BigInt(value); +} + +function parseRpcEnvelope(value: unknown, operation: string): unknown { + if ( + typeof value !== "object" || + value === null || + Array.isArray(value) || + (value as Record).jsonrpc !== "2.0" || + (value as Record).id !== 1 || + !("result" in value) || + "error" in value + ) + throw validationError("rpc", operation); + return (value as Record).result; +} + +function decodeChainlinkResult(value: unknown) { + if (typeof value !== "string" || !/^0x[0-9a-f]{320}$/u.test(value)) { + throw validationError("rpc", "chainlink-result"); + } + const words = Array.from({ length: 5 }, (_, index) => + BigInt(`0x${value.slice(2 + index * 64, 2 + (index + 1) * 64)}`), + ); + const signedAnswer = + words[1]! >= 2n ** 255n ? words[1]! - 2n ** 256n : words[1]!; + if ( + words[0]! <= 0n || + words[0]! >= 2n ** 80n || + signedAnswer <= 0n || + words[2]! <= 0n || + words[3]! < words[2]! || + words[4]! < words[0]! || + words[4]! >= 2n ** 80n || + words[2]! > 253_402_300_799n || + words[3]! > 253_402_300_799n + ) + throw validationError("rpc", "chainlink-round"); + return Object.freeze({ + rawResult: value as Hex, + feedRoundId: words[0]!.toString(), + answer: signedAnswer.toString(), + feedUpdatedAt: new Date(Number(words[3]!) * 1_000), + }); +} + +function exactRpcUrl( + value: unknown, + provider: "alchemy" | "quicknode", +): string { + if (typeof value !== "string") throw invalidInput("config", "rpc-url"); + let parsed: URL; + try { + parsed = new URL(value); + } catch { + throw invalidInput("config", "rpc-url"); + } + const alchemy = + parsed.hostname === "eth-mainnet.g.alchemy.com" && + /^\/v2\/[A-Za-z0-9_-]{8,256}$/u.test(parsed.pathname); + const quicknode = + /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+quiknode\.pro$/u.test( + parsed.hostname, + ) && /^\/[A-Za-z0-9_-]{8,256}\/?$/u.test(parsed.pathname); + if ( + parsed.protocol !== "https:" || + parsed.username || + parsed.password || + parsed.port || + parsed.search || + parsed.hash || + (provider === "alchemy" ? !alchemy : !quicknode) + ) + throw invalidInput("config", "rpc-url"); + return parsed.toString(); +} + +export function createDualRpcMarketReader( + input: Readonly<{ + endpoints: readonly [string, string]; + fetcher?: DataPipelineFetcher; + }>, +): MarketRpc { + const endpoints = Object.freeze([ + exactRpcUrl(input.endpoints[0], "alchemy"), + exactRpcUrl(input.endpoints[1], "quicknode"), + ] as const); + + async function request(endpoint: string, method: string, params: unknown[]) { + let lastError: unknown; + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + return await boundedJsonRequest({ + dependency: "rpc", + endpoint, + timeoutMs: 5_000, + maximumBodyBytes: 32 * 1024, + fetcher: input.fetcher, + body: { jsonrpc: "2.0", id: 1, method, params }, + }); + } catch (error) { + if (error instanceof DataPipelineError && !error.retryable) throw error; + lastError = error; + } + } + throw lastError; + } + + return Object.freeze({ + async readChainlinkBlock({ blockNumber, expectedBlockHash }) { + const number = BigInt(integer(blockNumber, "rpc-block-number")); + const results = await Promise.all( + endpoints.map(async (endpoint) => { + const [rawBlock, rawCall] = await Promise.all([ + request(endpoint, "eth_getBlockByHash", [expectedBlockHash, false]), + request(endpoint, "eth_call", [ + { to: CHAINLINK_ETH_USD, data: LATEST_ROUND_DATA_SELECTOR }, + { blockHash: expectedBlockHash, requireCanonical: true }, + ]), + ]); + const block = parseRpcEnvelope(rawBlock, "block"); + if ( + typeof block !== "object" || + block === null || + Array.isArray(block) + ) { + throw validationError("rpc", "block"); + } + const row = block as Record; + const parsedNumber = parseHexQuantity(row.number, "block-number"); + const hash = canonicalBytes32(row.hash); + const timestamp = parseHexQuantity(row.timestamp, "block-timestamp"); + if (parsedNumber !== number || hash !== expectedBlockHash) { + throw validationError("rpc", "block-identity"); + } + const chainlink = decodeChainlinkResult( + parseRpcEnvelope(rawCall, "eth-call"), + ); + const blockTimestamp = new Date(Number(timestamp) * 1_000); + if ( + !Number.isFinite(blockTimestamp.getTime()) || + chainlink.feedUpdatedAt > blockTimestamp || + blockTimestamp.getTime() - chainlink.feedUpdatedAt.getTime() > + 3_600_000 + ) + throw validationError("rpc", "chainlink-freshness"); + return Object.freeze({ + blockNumber, + blockHash: hash, + blockTimestamp, + ...chainlink, + }); + }), + ); + if ( + results[0]!.blockHash !== results[1]!.blockHash || + results[0]!.blockTimestamp.getTime() !== + results[1]!.blockTimestamp.getTime() || + results[0]!.rawResult !== results[1]!.rawResult + ) + throw validationError("rpc", "provider-mismatch"); + return results[0]!; + }, + }); +} + +function assertBeforeDeadline(deadlineAt: number) { + if (Date.now() >= deadlineAt) { + throw dataPipelineError({ + dependency: "uniswap", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); + } +} + +async function mapWithConcurrency( + values: readonly T[], + concurrency: number, + operation: (value: T, index: number) => Promise, +): Promise { + const output = new Array(values.length); + let nextIndex = 0; + const workers = Array.from( + { length: Math.min(concurrency, values.length) }, + async () => { + while (nextIndex < values.length) { + const index = nextIndex; + nextIndex += 1; + output[index] = await operation(values[index]!, index); + } + }, + ); + await Promise.all(workers); + return Object.freeze(output); +} + +async function preparePoolPage( + input: Readonly<{ + plan: MarketPoolPlan; + store: MarketProjectorStore; + analytics: MarketAnalytics; + rpc: MarketRpc; + graphProviderId: string; + deadlineAt: number; + }>, +): Promise { + const { plan } = input; + assertBeforeDeadline(input.deadlineAt); + const sourceReorg = BigInt(plan.sourceReorgGeneration); + const cursorReorg = BigInt(plan.cursor?.sourceReorgGeneration ?? "0"); + const epochChanged = + plan.cursor !== null && plan.epochId !== plan.cursor.epochId; + const pointerChanged = + plan.cursor !== null && + plan.pointerGeneration !== plan.cursor.pointerGeneration; + const sourceCheckpointAdvanced = + plan.cursor !== null && + BigInt(plan.sourceCheckpointGeneration) > + BigInt(plan.cursor.sourceCheckpointGeneration); + const isReorg = + plan.cursor !== null && + (epochChanged || pointerChanged || sourceReorg > cursorReorg); + if ( + plan.cursor && + !epochChanged && + (BigInt(plan.pointerGeneration) < BigInt(plan.cursor.pointerGeneration) || + (!pointerChanged && sourceReorg < cursorReorg)) + ) { + throw validationError("postgres", "stale-source-reorg"); + } + const sameBlockSourceAdvance = + sourceCheckpointAdvanced && + plan.cursor !== null && + plan.sourceCheckpointBlockNumber === plan.cursor.blockNumber; + const fromBlock = isReorg + ? (BigInt(plan.launchBlockNumber) - 1n).toString() + : sameBlockSourceAdvance + ? (BigInt(plan.cursor!.blockNumber) > 0n + ? BigInt(plan.cursor!.blockNumber) - 1n + : 0n + ).toString() + : (plan.cursor?.blockNumber ?? + (BigInt(plan.launchBlockNumber) - 1n).toString()); + const anchors = await input.store.listCloseAnchors({ + plan, + fromBlockExclusive: fromBlock, + toBlockInclusive: plan.sourceCheckpointBlockNumber, + limit: MAXIMUM_CLOSE_BLOCKS, + }); + const bounded = anchors.length === MAXIMUM_CLOSE_BLOCKS; + const terminalAnchor = bounded ? anchors[anchors.length - 1]! : null; + const targetBlockNumber = + terminalAnchor?.blockNumber ?? plan.sourceCheckpointBlockNumber; + const targetBlockHash = + terminalAnchor?.blockHash ?? plan.sourceCheckpointBlockHash; + const targetEvidenceId = + terminalAnchor?.blockEvidenceId ?? plan.sourceCheckpointBlockEvidenceId; + assertBeforeDeadline(input.deadlineAt); + const target = await input.rpc.readChainlinkBlock({ + blockNumber: targetBlockNumber, + expectedBlockHash: targetBlockHash, + }); + const targetSnapshot = exactResult( + await input.analytics.readPoolSnapshot({ + poolKey: plan.poolKey, + block: { number: targetBlockNumber, hash: targetBlockHash }, + }), + { number: targetBlockNumber, hash: targetBlockHash }, + ); + if (targetSnapshot.tick === null) + throw validationError("uniswap", "missing-tick"); + const targetPrices = prices(targetSnapshot, plan.poolKey); + + const closes = await mapWithConcurrency( + anchors, + PROVIDER_CONCURRENCY, + async (anchor): Promise => { + assertBeforeDeadline(input.deadlineAt); + const global = + anchor.blockHash === target.blockHash + ? target + : await input.rpc.readChainlinkBlock({ + blockNumber: anchor.blockNumber, + expectedBlockHash: anchor.blockHash, + }); + const snapshot = + anchor.blockHash === target.blockHash + ? targetSnapshot + : exactResult( + await input.analytics.readPoolSnapshot({ + poolKey: plan.poolKey, + block: { number: anchor.blockNumber, hash: anchor.blockHash }, + }), + { number: anchor.blockNumber, hash: anchor.blockHash }, + ); + if (snapshot.tick === null) + throw validationError("uniswap", "missing-close-tick"); + const closePrices = prices(snapshot, plan.poolKey); + const hourStart = floorDate(global.blockTimestamp, 3_600); + const partialHour = exactResult( + await input.analytics.readHourSeries({ + poolKey: plan.poolKey, + block: { number: anchor.blockNumber, hash: anchor.blockHash }, + from: dateSeconds(hourStart), + toExclusive: dateSeconds(plusSeconds(hourStart, 3_600)), + }), + { number: anchor.blockNumber, hash: anchor.blockHash }, + ); + const hour = partialHour.find( + (item) => item.periodStart === dateSeconds(hourStart), + ); + if (!hour) throw validationError("uniswap", "missing-close-fees"); + return Object.freeze({ + anchor, + global, + snapshot, + token0Price: closePrices.token0Price, + token1Price: closePrices.token1Price, + feesUsd: hour.feesUsd, + }); + }, + ); + + const hourEnd = floorDate(target.blockTimestamp, 3_600); + const dayEnd = floorDate(target.blockTimestamp, 86_400); + const hourStart = isReorg + ? floorDate(plan.launchBlockTimestamp, 3_600) + : (plan.cursor?.hourCoverageEnd ?? + floorDate(plan.launchBlockTimestamp, 3_600)); + const dayStart = isReorg + ? floorDate(plan.launchBlockTimestamp, 86_400) + : (plan.cursor?.dayCoverageEnd ?? + floorDate(plan.launchBlockTimestamp, 86_400)); + assertBeforeDeadline(input.deadlineAt); + const [hourSeries, daySeries] = await Promise.all([ + hourStart < hourEnd + ? input.analytics.readHourSeries({ + poolKey: plan.poolKey, + block: { number: target.blockNumber, hash: target.blockHash }, + from: dateSeconds(hourStart), + toExclusive: dateSeconds(hourEnd), + }) + : Promise.resolve(null), + dayStart < dayEnd + ? input.analytics.readDaySeries({ + poolKey: plan.poolKey, + block: { number: target.blockNumber, hash: target.blockHash }, + from: dateSeconds(dayStart), + toExclusive: dateSeconds(dayEnd), + }) + : Promise.resolve(null), + ]); + const candles: PreparedMarketCandle[] = []; + if (hourStart < hourEnd) { + for (const data of exactResult(hourSeries!, { + number: target.blockNumber, + hash: target.blockHash, + })) { + const periodStart = new Date(data.periodStart * 1_000); + candles.push( + Object.freeze({ + interval: "hour", + periodStart, + periodEnd: plusSeconds(periodStart, 3_600), + data, + }), + ); + } + } + if (dayStart < dayEnd) { + for (const data of exactResult(daySeries!, { + number: target.blockNumber, + hash: target.blockHash, + })) { + const periodStart = new Date(data.periodStart * 1_000); + candles.push( + Object.freeze({ + interval: "day", + periodStart, + periodEnd: plusSeconds(periodStart, 86_400), + data, + }), + ); + } + } + const nextHourCoverageEnd = + hourStart < hourEnd ? hourEnd : (plan.cursor?.hourCoverageEnd ?? null); + const nextDayCoverageEnd = + dayStart < dayEnd ? dayEnd : (plan.cursor?.dayCoverageEnd ?? null); + const providerCursor = `block:${target.blockNumber}:${target.blockHash.slice(2, 18)}`; + const pageCommitment = commitment("page", { + releaseId: plan.scope.releaseId, + modelId: plan.scope.modelId, + epochId: plan.epochId, + pointerGeneration: plan.pointerGeneration, + sourceCheckpointId: plan.sourceCheckpointId, + sourceCheckpointGeneration: plan.sourceCheckpointGeneration, + sourceReorgGeneration: plan.sourceReorgGeneration, + poolId: plan.poolKey.poolId, + graphProviderId: input.graphProviderId, + target, + targetSnapshot, + closes, + candles, + nextHourCoverageEnd: nextHourCoverageEnd?.toISOString() ?? null, + nextDayCoverageEnd: nextDayCoverageEnd?.toISOString() ?? null, + providerCursor, + isReorg, + }); + return Object.freeze({ + plan, + graphProviderId: input.graphProviderId, + targetEvidenceId, + target, + targetSnapshot, + targetToken0Price: targetPrices.token0Price, + targetToken1Price: targetPrices.token1Price, + closes: Object.freeze(closes), + candles: Object.freeze(candles), + nextHourCoverageEnd, + nextDayCoverageEnd, + providerCursor, + pageCommitment, + isReorg, + }); +} + +export async function runMarketProjectorCycle( + input: Readonly<{ + store: MarketProjectorStore; + analytics: MarketAnalytics; + rpc: MarketRpc; + graphProvider: Readonly<{ + redactedIdentity: string; + deploymentCommitment: HexBytes32; + schemaCommitment: HexBytes32; + }>; + deadlineMs?: number; + }>, +): Promise { + const startedAt = Date.now(); + const deadlineMs = input.deadlineMs ?? DEADLINE_MS; + if ( + !Number.isSafeInteger(deadlineMs) || + deadlineMs < CLOSE_RESERVE_MS + 1_000 || + deadlineMs > 80_000 + ) { + throw invalidInput("config", "market-projector-deadline"); + } + const deadlineAt = startedAt + deadlineMs - CLOSE_RESERVE_MS; + const graphProviderId = await input.store.resolveGraphProvider( + input.graphProvider, + ); + const plans = (await input.store.loadPlans()).filter((plan) => { + if (plan.cursor === null) return true; + if ( + plan.epochId !== plan.cursor.epochId || + plan.pointerGeneration !== plan.cursor.pointerGeneration + ) { + return true; + } + if ( + BigInt(plan.sourceReorgGeneration) > + BigInt(plan.cursor.sourceReorgGeneration) + ) { + return true; + } + if ( + BigInt(plan.sourceCheckpointGeneration) > + BigInt(plan.cursor.sourceCheckpointGeneration) + ) { + return true; + } + return ( + BigInt(plan.sourceCheckpointBlockNumber) > BigInt(plan.cursor.blockNumber) + ); + }); + if (plans.length === 0) { + return Object.freeze({ + status: "idle", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: true, + }); + } + const ranked = [...plans].sort((left, right) => { + const leftReorg = + left.cursor !== null && + (left.epochId !== left.cursor.epochId || + left.pointerGeneration !== left.cursor.pointerGeneration || + BigInt(left.sourceReorgGeneration) > + BigInt(left.cursor.sourceReorgGeneration)); + const rightReorg = + right.cursor !== null && + (right.epochId !== right.cursor.epochId || + right.pointerGeneration !== right.cursor.pointerGeneration || + BigInt(right.sourceReorgGeneration) > + BigInt(right.cursor.sourceReorgGeneration)); + if (leftReorg !== rightReorg) return leftReorg ? -1 : 1; + if ((left.cursor === null) !== (right.cursor === null)) { + return left.cursor === null ? -1 : 1; + } + const leftAdvancedAt = left.cursor?.advancedAt.getTime() ?? 0; + const rightAdvancedAt = right.cursor?.advancedAt.getTime() ?? 0; + if (leftAdvancedAt !== rightAdvancedAt) { + return leftAdvancedAt - rightAdvancedAt; + } + const leftLag = + BigInt(left.sourceCheckpointBlockNumber) - + BigInt(left.cursor?.blockNumber ?? left.launchBlockNumber); + const rightLag = + BigInt(right.sourceCheckpointBlockNumber) - + BigInt(right.cursor?.blockNumber ?? right.launchBlockNumber); + if (leftLag !== rightLag) return leftLag > rightLag ? -1 : 1; + const release = left.scope.releaseId.localeCompare(right.scope.releaseId); + return release !== 0 + ? release + : left.poolKey.poolId.localeCompare(right.poolKey.poolId); + }); + let last: MarketProjectorCycleResult | null = null; + let lastError: unknown; + let committedPools = 0; + for (const plan of ranked) { + if (committedPools >= MAXIMUM_POOLS_PER_CYCLE) break; + if (Date.now() >= deadlineAt) break; + try { + const page = await preparePoolPage({ + plan, + store: input.store, + analytics: input.analytics, + rpc: input.rpc, + graphProviderId, + deadlineAt, + }); + last = await input.store.commit(page); + committedPools += 1; + } catch (error) { + lastError = error; + console.warn("Market projector skipped one pool", { + releaseId: plan.scope.releaseId, + poolId: plan.poolKey.poolId, + error: safeMarketProjectorError(error), + }); + } + } + if (lastError !== undefined) throw lastError; + return ( + last ?? + Object.freeze({ + status: "idle", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }) + ); +} + +export type MarketProjectorRuntimeConfig = Readonly<{ + databaseUrl: string; + sslCaPem: string; + sourceProjectorVersion: string; + graphApiKey: string; + graphProvider: Readonly<{ + redactedIdentity: string; + deploymentCommitment: HexBytes32; + schemaCommitment: HexBytes32; + }>; + rpcEndpoints: readonly [string, string]; + rpcProviders: readonly [MarketRpcProviderEvidence, MarketRpcProviderEvidence]; +}>; + +export function loadMarketProjectorRuntimeConfig( + env: Environment = process.env, +): MarketProjectorRuntimeConfig { + if (BROWSER_FORBIDDEN.some((name) => env[name])) { + throw invalidInput("config", "browser-market-secret"); + } + const binding = getDataPipelineReleaseBinding(); + if ( + binding.releases.length !== RELEASE_SCOPES.length || + binding.releases.some( + (release, index) => + release.releaseVersion !== RELEASE_SCOPES[index]!.releaseId || + release.model !== RELEASE_SCOPES[index]!.modelId, + ) + ) + throw invalidInput("config", "market-release-scopes"); + const pipeline = loadDataPipelineConfig(env); + const sourceProjectorVersion = exactText( + env.PROGRAMMABLE_SOURCE_PROJECTOR_VERSION, + IDENTIFIER_PATTERN, + "source-projector-version", + ); + const redactedIdentity = exactText( + env.PROGRAMMABLE_UNISWAP_GRAPH_REDACTED_IDENTITY, + IDENTIFIER_PATTERN, + "graph-redacted-identity", + ); + const deploymentCommitment = canonicalBytes32( + exactText( + env.PROGRAMMABLE_UNISWAP_GRAPH_DEPLOYMENT_COMMITMENT, + HEX32_PATTERN, + "graph-deployment-commitment", + ), + ); + const schemaCommitment = canonicalBytes32( + exactText( + env.PROGRAMMABLE_UNISWAP_GRAPH_SCHEMA_COMMITMENT, + HEX32_PATTERN, + "graph-schema-commitment", + ), + ); + if ( + deploymentCommitment !== MARKET_GRAPH_DEPLOYMENT_COMMITMENT || + schemaCommitment !== MARKET_GRAPH_SCHEMA_COMMITMENT + ) { + throw invalidInput("config", "graph-release-provenance"); + } + if (!pipeline.uniswap.apiKey) throw invalidInput("config", "graph-api-key"); + const providers = createProductionDualRpcProviders(env); + assertProductionDualRpcProviders(providers); + return Object.freeze({ + databaseUrl: validatedPostgresConnectionString( + env.PROGRAMMABLE_RECONCILER_DATABASE_URL, + ), + sslCaPem: validatedPostgresSslCa(env.PROGRAMMABLE_POSTGRES_SSL_CA_PEM), + sourceProjectorVersion, + graphApiKey: pipeline.uniswap.apiKey, + graphProvider: Object.freeze({ + redactedIdentity, + deploymentCommitment, + schemaCommitment, + }), + rpcEndpoints: Object.freeze([ + exactRpcUrl(env.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL, "alchemy"), + exactRpcUrl(env.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL, "quicknode"), + ] as const), + rpcProviders: Object.freeze([ + Object.freeze({ + identity: providers[0].identity, + endpointCommitment: canonicalBytes32(providers[0].endpointCommitment), + endpointOriginCommitment: canonicalBytes32( + providers[0].endpointOriginCommitment, + ), + }), + Object.freeze({ + identity: providers[1].identity, + endpointCommitment: canonicalBytes32(providers[1].endpointCommitment), + endpointOriginCommitment: canonicalBytes32( + providers[1].endpointOriginCommitment, + ), + }), + ] as const), + }); +} + +export async function runConfiguredMarketProjectorCycle( + input: Readonly<{ + env?: Environment; + fetcher?: DataPipelineFetcher; + executor?: PostgresExecutor; + }> = {}, +): Promise { + const env = input.env ?? process.env; + const activation = env.PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE; + if (activation === undefined || activation === "false") { + return Object.freeze({ + status: "disabled", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }); + } + if (activation !== "true") { + throw invalidInput("config", "market-projector-active"); + } + const config = loadMarketProjectorRuntimeConfig(env); + const executor = + input.executor ?? + createPostgresExecutor({ + connectionString: config.databaseUrl, + sslCaPem: config.sslCaPem, + maxConnections: 1, + connectTimeoutMs: 2_000, + idleTimeoutMs: 5_000, + }); + const store = createPostgresMarketProjectorStore({ + executor, + sourceProjectorVersion: config.sourceProjectorVersion, + rpcProviders: config.rpcProviders, + }); + try { + const lease = await store.tryAcquireLease(); + if (!lease) { + return Object.freeze({ + status: "busy", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }); + } + try { + return await runMarketProjectorCycle({ + store, + analytics: createUniswapAnalyticsClient({ + gatewayBaseUrl: GRAPH_GATEWAY, + apiKey: config.graphApiKey, + fetcher: input.fetcher, + limits: { maximumPages: 24, maximumEntities: 6_000 }, + }), + rpc: createDualRpcMarketReader({ + endpoints: config.rpcEndpoints, + fetcher: input.fetcher, + }), + graphProvider: config.graphProvider, + }); + } finally { + await store.releaseLease(lease); + } + } finally { + if (!input.executor) await store.close(); + } +} + +export function safeMarketProjectorError(error: unknown) { + if (error instanceof DataPipelineError) { + return Object.freeze({ + dependency: error.dependency, + code: error.code, + retryable: error.retryable, + }); + } + return Object.freeze({ + dependency: "market-projector", + code: "internal_error", + retryable: false, + }); +} diff --git a/lib/data-pipeline/postgres-connection.server.ts b/lib/data-pipeline/postgres-connection.server.ts new file mode 100644 index 00000000..994c7d09 --- /dev/null +++ b/lib/data-pipeline/postgres-connection.server.ts @@ -0,0 +1,220 @@ +import "server-only"; + +import { X509Certificate } from "node:crypto"; + +import { invalidInput } from "./errors"; + +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1"]); +const POSTGRES_PREFIXES = ["postgresql://", "postgres://"] as const; + +function invalidConnectionString(): never { + throw invalidInput("postgres", "connection-string"); +} + +function decodedConnectionComponent(value: string): string { + try { + const decoded = decodeURIComponent(value); + if (decoded === "" || /[\u0000-\u0020\u007f]/u.test(decoded)) { + return invalidConnectionString(); + } + return decoded; + } catch { + return invalidConnectionString(); + } +} + +function isCanonicalIpv4(hostname: string): boolean { + const parts = hostname.split("."); + return ( + parts.length === 4 && + parts.every((part) => { + if (!/^(0|[1-9]\d{0,2})$/u.test(part)) return false; + return Number(part) <= 255; + }) + ); +} + +function isCanonicalDnsName(hostname: string): boolean { + if (hostname.length > 253) return false; + const labels = hostname.split("."); + return labels.every( + (label) => + label.length >= 1 && + label.length <= 63 && + /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label), + ); +} + +function validateRawAuthority(value: string): { + prefix: (typeof POSTGRES_PREFIXES)[number]; + hostname: string; + port: string; +} { + const prefix = POSTGRES_PREFIXES.find((candidate) => + value.startsWith(candidate), + ); + if (!prefix) return invalidConnectionString(); + + const remainder = value.slice(prefix.length); + const boundary = remainder.search(/[/?#]/u); + if (boundary <= 0) return invalidConnectionString(); + + const authority = remainder.slice(0, boundary); + if (authority.includes(",") || authority.includes("\\")) { + return invalidConnectionString(); + } + + const separator = authority.indexOf("@"); + if (separator <= 0 || separator !== authority.lastIndexOf("@")) { + return invalidConnectionString(); + } + + const userInfo = authority.slice(0, separator); + const passwordSeparator = userInfo.indexOf(":"); + if (passwordSeparator <= 0 || passwordSeparator === userInfo.length - 1) { + return invalidConnectionString(); + } + decodedConnectionComponent(userInfo.slice(0, passwordSeparator)); + decodedConnectionComponent(userInfo.slice(passwordSeparator + 1)); + + const hostAndPort = authority.slice(separator + 1); + let rawHostname: string; + let rawPort: string | undefined; + if (hostAndPort.startsWith("[")) { + // postgres.js 3.4.x splits bracketed addresses on colons before connecting. + // Reject them until the driver exposes an unambiguous parsed-host boundary. + return invalidConnectionString(); + } else { + const firstColon = hostAndPort.indexOf(":"); + if (firstColon >= 0) { + if (firstColon !== hostAndPort.lastIndexOf(":")) { + return invalidConnectionString(); + } + rawHostname = hostAndPort.slice(0, firstColon); + rawPort = hostAndPort.slice(firstColon + 1); + } else { + rawHostname = hostAndPort; + } + + if ( + rawHostname.includes("%") || + (!isCanonicalIpv4(rawHostname) && !isCanonicalDnsName(rawHostname)) + ) { + return invalidConnectionString(); + } + } + + if ( + rawPort === undefined || + !/^[1-9]\d{0,4}$/.test(rawPort) || + Number(rawPort) > 65_535 + ) { + return invalidConnectionString(); + } + return { prefix, hostname: rawHostname, port: rawPort }; +} + +export type PostgresConnectionTarget = { + connectionString: string; + hostname: string; + port: number; + isLoopback: boolean; + sslMode?: "disable" | "verify-full"; +}; + +export function validatedPostgresConnectionTarget( + value: unknown, +): PostgresConnectionTarget { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 4_096 || + /[\u0000-\u0020\u007f]/u.test(value) + ) { + return invalidConnectionString(); + } + + const authority = validateRawAuthority(value); + + let url: URL; + try { + url = new URL(value); + } catch { + return invalidConnectionString(); + } + + if ( + (url.protocol !== "postgresql:" && url.protocol !== "postgres:") || + `${url.protocol}//` !== authority.prefix || + url.username === "" || + url.password === "" || + url.hostname === "" || + url.pathname === "" || + url.pathname === "/" || + url.hash !== "" || + url.hostname !== authority.hostname || + url.port !== authority.port + ) { + return invalidConnectionString(); + } + + for (const key of url.searchParams.keys()) { + if (key !== "sslmode") { + return invalidConnectionString(); + } + } + + const sslModes = url.searchParams.getAll("sslmode"); + if (sslModes.length > 1) return invalidConnectionString(); + + if (LOOPBACK_HOSTS.has(url.hostname)) { + if ( + sslModes.length === 1 && + sslModes[0] !== "disable" && + sslModes[0] !== "verify-full" + ) { + return invalidConnectionString(); + } + return { + connectionString: value, + hostname: url.hostname, + port: Number(authority.port), + isLoopback: true, + sslMode: sslModes[0] as "disable" | "verify-full" | undefined, + }; + } + + if (sslModes.length !== 1 || sslModes[0] !== "verify-full") { + return invalidConnectionString(); + } + return { + connectionString: value, + hostname: url.hostname, + port: Number(authority.port), + isLoopback: false, + sslMode: "verify-full", + }; +} + +export function validatedPostgresConnectionString(value: unknown): string { + return validatedPostgresConnectionTarget(value).connectionString; +} + +export function validatedPostgresSslCa(value: unknown): string { + if ( + typeof value !== "string" || + value.length < 256 || + value.length > 32_768 || + !/^-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+-----END CERTIFICATE-----\r?\n?$/.test( + value, + ) + ) { + return invalidConnectionString(); + } + try { + new X509Certificate(value); + } catch { + return invalidConnectionString(); + } + return value; +} diff --git a/lib/data-pipeline/postgres-projector.ts b/lib/data-pipeline/postgres-projector.ts new file mode 100644 index 00000000..dfe49667 --- /dev/null +++ b/lib/data-pipeline/postgres-projector.ts @@ -0,0 +1,7547 @@ +import "server-only"; + +import { createHash, randomUUID } from "node:crypto"; + +import { concat, encodeAbiParameters, keccak256, toBytes } from "viem"; + +import { + canonicalizeFingerprintJson, + canonicalFingerprintPreimageV1, + canonicalFingerprintV1, + type CanonicalJsonValue, + type OccurrenceFingerprintReference, +} from "./canonical-fingerprint"; +import { + addressFromBytea, + bytes32FromBytea, + canonicalAddress, + canonicalBytes32, + canonicalRawData, + dataFromBytea, + hexToBytes, + parseNonnegativeIntegerText, + type HexAddress, + type HexBytes32, +} from "./codecs"; +import { classicV3InitialRewardCommitments } from "./classic-v3-reward-commitments"; +import type { + CandidateRpcProvider, + DualRpcCandidateWindowEvidence, + DualRpcDynamicRuntimeObservation, + DualRpcSafeHeadEvidence, + DualRpcRewardSnapshot, + ProjectorDynamicSourceTemplate, +} from "./dual-rpc"; +import type { EnvioCandidate } from "./envio"; +import { DataPipelineError } from "./errors"; +import { + postgresJson, + type PostgresExecutor, + type PostgresParameter, + type PostgresTransaction, +} from "./postgres"; +import type { ProjectorPlan, ProjectorStore } from "./projector"; +import type { + CanonicalDynamicSourceDeploymentEvidence, + PendingDynamicSourceActivation, +} from "./projector-dynamic-activation"; +import type { + ReorgGenesisAnchor, + ReorgHistoryAncestor, +} from "./projector-reorg"; +import type { + ReleaseProjectionPlan, + ReleaseProjectionStore, + StoredProjectionCandidate, + VerifiedReleaseProjection, +} from "./projector-projection"; +import type { + ProjectorCompletedLaunch, + ProjectorEventFact, + ProjectorKnownPool, + ProjectorOccurrenceFact, +} from "./projector-fold"; +import { + canonicalDynamicSourceLineage, + type VerifiedDynamicSourceLineage, +} from "./projector-identities"; +import { + deterministicProjectorUuid as deterministicUuid, + projectorOccurrenceUuid, +} from "./projector-ids"; +import { + foldProjectorRewardState, + type ProjectorRewardBaseline, + type ProjectorRewardEvent, + type ProjectorRewardModel, + type ProjectorRewardSnapshot, +} from "./projector-reward-fold"; +import { + expectedRewardRpcCallCount, + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1, +} from "./projector-reward-rpc-contract"; +import { + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE, +} from "./projector-runtime-limits"; +import { runtimeBytecodeEvidence } from "./runtime-bytecode"; +import { + projectionExecutionTraceCommitmentV1, + providerEvidenceV2, + providerEvidenceV3, +} from "./provider-evidence"; + +const PROJECTOR_LOGIN_ROLE = "programmable_projector_login"; +const PROJECTOR_CAPABILITY_ROLE = "programmable_projector"; +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; +const ENVIO_CONTROL_SCOPE = Object.freeze({ + chainId: "1", + releaseId: "envio-control", + modelId: "envio-control", + sourceGroup: "canonical-events", + projectorVersion: "envio-adapter-v1", +}); +const RELEASE_PROJECTOR_VERSION = "projector-v1"; +const IMMUTABLE_VALUES_DOMAIN = toBytes( + "programmable:data-pipeline:immutable-values:v1\0", +); +const ACTIVATION_MODEL_EVIDENCE_DOMAIN = + "programmable:classic-v3-activation-model-evidence:v1\0"; +const ACTIVATION_PAYLOAD_DOMAIN = + "programmable:classic-v3-dynamic-activation:v1\0"; + +export type ProjectorProviderDatabaseBinding = Readonly<{ + type: "rpc_provider" | "envio_deployment" | "uniswap_subgraph"; + redactedIdentity: string; + deploymentCommitment: HexBytes32; + schemaCommitment: HexBytes32; +}>; + +export type ProjectorReleaseDatabaseScope = Readonly<{ + releaseId: + | "classic-v2" + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + modelId: string; + sourceGroup: string; +}>; + +export type ProjectorRuntimeFence = Readonly<{ + holderId: string; + generation: string; + tokenHash: HexBytes32; +}>; + +export type ProjectorGenesisInitializationEvidence = Readonly<{ + anchorBlockNumber: string; + anchorBlockHash: HexBytes32; + safeHead: DualRpcSafeHeadEvidence; +}>; + +const PROJECTOR_RELEASE_SCOPES = Object.freeze([ + Object.freeze({ releaseId: "classic-v2", modelId: "classic", sourceGroup: "core" }), + Object.freeze({ releaseId: "classic-v3", modelId: "classic", sourceGroup: "core" }), + Object.freeze({ releaseId: "stock-paired-v1", modelId: "stock-paired", sourceGroup: "core" }), + Object.freeze({ releaseId: "stock-paired-v2", modelId: "stock-paired", sourceGroup: "core" }), + Object.freeze({ releaseId: "stock-paired-v3", modelId: "stock-paired", sourceGroup: "core" }), +] satisfies readonly ProjectorReleaseDatabaseScope[]); + +function canonicalReleaseScopes( + scopes: readonly ProjectorReleaseDatabaseScope[], +): readonly ProjectorReleaseDatabaseScope[] { + if ( + !Array.isArray(scopes) || + scopes.length !== PROJECTOR_RELEASE_SCOPES.length || + scopes.some((scope, index) => { + const expected = PROJECTOR_RELEASE_SCOPES[index]!; + return ( + scope?.releaseId !== expected.releaseId || + scope.modelId !== expected.modelId || + scope.sourceGroup !== expected.sourceGroup + ); + }) + ) { + return projectorValidationFailure(); + } + return PROJECTOR_RELEASE_SCOPES; +} + +function canonicalReleaseScope( + scope: ProjectorReleaseDatabaseScope, +): ProjectorReleaseDatabaseScope { + const expected = PROJECTOR_RELEASE_SCOPES.find( + ({ releaseId }) => releaseId === scope?.releaseId, + ); + if ( + !expected || + scope.modelId !== expected.modelId || + scope.sourceGroup !== expected.sourceGroup + ) { + return projectorValidationFailure(); + } + return expected; +} + +function canonicalRuntimeFence( + fence: ProjectorRuntimeFence, +): ProjectorRuntimeFence { + return Object.freeze({ + holderId: exactText( + fence?.holderId, + /^[a-z0-9][a-z0-9._-]{0,95}$/u, + 96, + ), + generation: integerText(fence?.generation), + tokenHash: canonicalBytes32(fence?.tokenHash), + }); +} + +async function assertRuntimeFence( + transaction: PostgresTransaction, + fence: ProjectorRuntimeFence, +): Promise { + const rows = await transaction.query<{ asserted: unknown }>( + "select programmable_private.assert_projector_runtime_lease_v1($1, $2::bigint, $3::bytea) as asserted", + [fence.holderId, fence.generation, hexToBytes(fence.tokenHash)], + ); + if (rows.length !== 1 || rows[0]?.asserted !== true) { + throw new ProjectorDatabaseError({ + sqlState: "40001", + disposition: "retry-serialization", + retryable: true, + }); + } +} + +export type ProjectorSqlStateScope = + | "batch" + | "candidate-local" + | "dynamic-parent" + | "gateway"; + +export type ProjectorSqlDisposition = + | "retry-serialization" + | "transient-no-candidate-penalty" + | "fatal-gateway-membership" + | "fatal-codec-or-caller" + | "immutable-replay-conflict" + | "quarantine-candidate" + | "abort-batch-invariant" + | "defer-dynamic-parent" + | "fatal-integrity" + | "idempotence-reread" + | "fatal-unknown"; + +export type ProjectorSqlClassification = Readonly<{ + sqlState: string | null; + disposition: ProjectorSqlDisposition; + retryable: boolean; +}>; + +function canonicalSqlState(value: unknown): string | null { + return typeof value === "string" && /^[0-9A-Z]{5}$/u.test(value) + ? value + : null; +} + +export function classifyProjectorSqlState(input: { + sqlState: unknown; + scope: ProjectorSqlStateScope; +}): ProjectorSqlClassification { + const sqlState = canonicalSqlState(input.sqlState); + if (sqlState === "40001" || sqlState === "40P01") { + return Object.freeze({ + sqlState, + disposition: "retry-serialization", + retryable: true, + }); + } + if ( + sqlState === "55P03" || + sqlState === "57014" || + sqlState === "57P01" || + sqlState?.startsWith("08") + ) { + return Object.freeze({ + sqlState, + disposition: "transient-no-candidate-penalty", + retryable: true, + }); + } + if (sqlState === "42501" || input.scope === "gateway") { + return Object.freeze({ + sqlState, + disposition: "fatal-gateway-membership", + retryable: false, + }); + } + if (sqlState === "22023" || sqlState === "22P02" || sqlState === "22003") { + return Object.freeze({ + sqlState, + disposition: "fatal-codec-or-caller", + retryable: false, + }); + } + if (sqlState === "23505") { + return Object.freeze({ + sqlState, + disposition: "immutable-replay-conflict", + retryable: false, + }); + } + if (sqlState === "23514") { + return Object.freeze({ + sqlState, + disposition: + input.scope === "candidate-local" + ? "quarantine-candidate" + : "abort-batch-invariant", + retryable: false, + }); + } + if (sqlState === "23503") { + return Object.freeze({ + sqlState, + disposition: + input.scope === "dynamic-parent" + ? "defer-dynamic-parent" + : "fatal-integrity", + retryable: input.scope === "dynamic-parent", + }); + } + if (sqlState === "55000") { + return Object.freeze({ + sqlState, + disposition: "idempotence-reread", + retryable: true, + }); + } + return Object.freeze({ + sqlState, + disposition: "fatal-unknown", + retryable: false, + }); +} + +function sqlStateFromUnknown(error: unknown): unknown { + if (error === null || typeof error !== "object") return null; + return Reflect.get(error, "code"); +} + +export class ProjectorDatabaseError extends Error { + readonly sqlState: string | null; + readonly disposition: ProjectorSqlDisposition; + readonly retryable: boolean; + + constructor(classification: ProjectorSqlClassification) { + super("Projector database operation failed"); + this.name = "ProjectorDatabaseError"; + this.sqlState = classification.sqlState; + this.disposition = classification.disposition; + this.retryable = classification.retryable; + } + + static fromUnknown( + error: unknown, + scope: ProjectorSqlStateScope, + ): ProjectorDatabaseError { + if (error instanceof ProjectorDatabaseError) return error; + return new ProjectorDatabaseError( + classifyProjectorSqlState({ + sqlState: sqlStateFromUnknown(error), + scope, + }), + ); + } + + toJSON() { + return { + name: this.name, + sqlState: this.sqlState, + disposition: this.disposition, + retryable: this.retryable, + }; + } +} + +function gatewayIdentityFailure(): ProjectorDatabaseError { + return new ProjectorDatabaseError( + classifyProjectorSqlState({ sqlState: null, scope: "gateway" }), + ); +} + +async function assertGatewayLogin( + transaction: PostgresTransaction, +): Promise { + const rows = await transaction.query<{ session_user: unknown }>( + "select session_user::text as session_user", + ); + if ( + rows.length !== 1 || + rows[0]?.session_user !== PROJECTOR_LOGIN_ROLE + ) { + throw gatewayIdentityFailure(); + } +} + +async function assumeAndVerifyCapabilityRole( + transaction: PostgresTransaction, +): Promise { + await transaction.query("set local role programmable_projector"); + await transaction.query("set local statement_timeout = '1000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '2000ms'", + ); + const rows = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + if ( + rows.length !== 1 || + rows[0]?.session_user !== PROJECTOR_LOGIN_ROLE || + rows[0]?.current_role !== PROJECTOR_CAPABILITY_ROLE + ) { + throw gatewayIdentityFailure(); + } +} + +export function createProjectorDatabaseGateway(input: { + executor: PostgresExecutor; +}) { + return Object.freeze({ + async transaction( + work: (transaction: PostgresTransaction) => Promise, + scope: ProjectorSqlStateScope = "batch", + ): Promise { + try { + return await input.executor.transaction(async (transaction) => { + await assertGatewayLogin(transaction); + await assumeAndVerifyCapabilityRole(transaction); + return work(transaction); + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw ProjectorDatabaseError.fromUnknown(error, scope); + } + }, + }); +} + +function projectorValidationFailure(): never { + throw new ProjectorDatabaseError({ + sqlState: null, + disposition: "fatal-codec-or-caller", + retryable: false, + }); +} + +function exactUuid(value: unknown): string { + if (typeof value !== "string" || !UUID_PATTERN.test(value)) { + return projectorValidationFailure(); + } + return value; +} + +function uuidBytes(value: unknown): Buffer { + return Buffer.from(exactUuid(value).replaceAll("-", ""), "hex"); +} + +export function projectionProviderBindingCommitmentV1(input: Readonly<{ + publicationId: string; + runId: string; + promotionMode: "exact_incremental"; + executionEvidenceId: string; + executionFingerprint: HexBytes32; + rewardEvidence: readonly Readonly<{ + evidenceId: string; + fingerprint: HexBytes32; + }>[]; + boundAt: string; +}>): HexBytes32 { + if (input.promotionMode !== "exact_incremental") { + return projectorValidationFailure(); + } + const boundAtMillis = Date.parse(input.boundAt); + if (!Number.isSafeInteger(boundAtMillis) || boundAtMillis < 0) { + return projectorValidationFailure(); + } + const mode = Buffer.from(input.promotionMode, "utf8"); + const modeLength = Buffer.alloc(4); + modeLength.writeInt32BE(mode.length); + const rewardCount = Buffer.alloc(4); + rewardCount.writeInt32BE(input.rewardEvidence.length); + const timestamp = Buffer.alloc(8); + timestamp.writeBigInt64BE(BigInt(boundAtMillis)); + const bytes32 = (value: HexBytes32) => + Buffer.from(canonicalBytes32(value).slice(2), "hex"); + const preimage = Buffer.concat([ + Buffer.from("programmable:projection-provider-binding:v1\0", "utf8"), + uuidBytes(input.publicationId), + uuidBytes(input.runId), + modeLength, + mode, + uuidBytes(input.executionEvidenceId), + bytes32(input.executionFingerprint), + rewardCount, + ...input.rewardEvidence.flatMap((evidence) => [ + uuidBytes(evidence.evidenceId), + bytes32(evidence.fingerprint), + ]), + timestamp, + ]); + return `0x${createHash("sha256").update(preimage).digest("hex")}` as HexBytes32; +} + +function integerText(value: unknown): string { + try { + if (typeof value === "bigint") return parseNonnegativeIntegerText(value.toString()); + if (typeof value === "number") { + if (!Number.isSafeInteger(value)) return projectorValidationFailure(); + return parseNonnegativeIntegerText(String(value)); + } + return parseNonnegativeIntegerText(value); + } catch { + return projectorValidationFailure(); + } +} + +function databaseBytes32(value: unknown): HexBytes32 { + try { + return bytes32FromBytea(value); + } catch { + return projectorValidationFailure(); + } +} + +function databaseAddress(value: unknown): HexAddress { + try { + return addressFromBytea(value); + } catch { + return projectorValidationFailure(); + } +} + +function exactText(value: unknown, pattern: RegExp, maximum = 128): string { + if (typeof value !== "string" || value.length > maximum || !pattern.test(value)) { + return projectorValidationFailure(); + } + return value; +} + +function exactRecord(value: unknown): Record { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + Object.getPrototypeOf(value) !== Object.prototype + ) { + return projectorValidationFailure(); + } + return value as Record; +} + +function exactArray(value: unknown, maximum = 10_000): readonly unknown[] { + if (!Array.isArray(value) || value.length > maximum) { + return projectorValidationFailure(); + } + return value; +} + +function exactTimestamp(value: unknown): string { + const text = value instanceof Date ? value.toISOString() : value; + if (typeof text !== "string") return projectorValidationFailure(); + const parsed = new Date(text); + if (!Number.isFinite(parsed.valueOf())) return projectorValidationFailure(); + return parsed.toISOString(); +} + +function canonicalProviderBindings( + values: readonly ProjectorProviderDatabaseBinding[], +): readonly ProjectorProviderDatabaseBinding[] { + if (!Array.isArray(values) || values.length < 3 || values.length > 8) { + return projectorValidationFailure(); + } + const identities = new Set(); + return Object.freeze( + values.map((value) => { + const type = exactText( + value.type, + /^(rpc_provider|envio_deployment|uniswap_subgraph)$/u, + ) as ProjectorProviderDatabaseBinding["type"]; + const redactedIdentity = exactText( + value.redactedIdentity, + /^[a-z0-9][a-z0-9._:/-]{0,127}$/u, + ); + if (identities.has(redactedIdentity)) return projectorValidationFailure(); + identities.add(redactedIdentity); + let deploymentCommitment: HexBytes32; + let schemaCommitment: HexBytes32; + try { + deploymentCommitment = canonicalBytes32(value.deploymentCommitment); + schemaCommitment = canonicalBytes32(value.schemaCommitment); + } catch { + return projectorValidationFailure(); + } + return Object.freeze({ + type, + redactedIdentity, + deploymentCommitment, + schemaCommitment, + }); + }), + ); +} + +type RuntimeState = Readonly<{ + epochId: string; + pointerGeneration: string; + providerDeploymentIds: readonly string[]; + reorgGeneration: string; +}>; + +function parseRuntimeState( + rows: readonly Record[], + providers: readonly ProjectorProviderDatabaseBinding[], +): RuntimeState { + if (rows.length !== 1) return projectorValidationFailure(); + const row = rows[0]!; + const ids = exactArray(row.provider_deployment_ids, 8).map(exactUuid); + const types = exactArray(row.provider_types, 8); + const identities = exactArray(row.provider_redacted_identities, 8); + if ( + ids.length !== providers.length || + types.length !== providers.length || + identities.length !== providers.length || + providers.some( + (provider, index) => + types[index] !== provider.type || + identities[index] !== provider.redactedIdentity, + ) + ) { + return projectorValidationFailure(); + } + return Object.freeze({ + epochId: exactUuid(row.epoch_id), + pointerGeneration: integerText(row.pointer_generation), + providerDeploymentIds: Object.freeze(ids), + reorgGeneration: integerText(row.reorg_generation), + }); +} + +async function readRuntimeState(input: { + transaction: PostgresTransaction; + scope: { + releaseId: string; + modelId: string; + sourceGroup: string; + projectorVersion: string; + }; + providers: readonly ProjectorProviderDatabaseBinding[]; +}): Promise { + const rows = await input.transaction.query( + "select * from programmable_private.get_projector_runtime_state_v1($1, $2, $3, $4, $5, $6::text[], $7::text[], $8::bytea[], $9::bytea[])", + [ + "1", + input.scope.releaseId, + input.scope.modelId, + input.scope.sourceGroup, + input.scope.projectorVersion, + input.providers.map(({ type }) => type), + input.providers.map(({ redactedIdentity }) => redactedIdentity), + input.providers.map(({ deploymentCommitment }) => + hexToBytes(deploymentCommitment), + ), + input.providers.map(({ schemaCommitment }) => hexToBytes(schemaCommitment)), + ], + ); + return parseRuntimeState(rows, input.providers); +} + +type ManifestSourceBinding = Readonly<{ + bindingId: string; + sourceName: string; + sourceRole: string; + sourceAddress: HexAddress; + inclusiveStartBlock: string; + abiEventSetCommitment: HexBytes32; + bindingCommitment: HexBytes32; +}>; + +type ManifestProjectionEventRule = Readonly<{ + ruleId: string; + projectionKind: string; + sourceRole: string; + eventType: string; + ruleCommitment: HexBytes32; +}>; + +type ManifestDynamicTemplate = Readonly<{ + templateId: string; + parentBindingId: string; + parentBindingCommitment: HexBytes32; + parentSourceRole: string; + factoryEventType: string; + deployedAddressField: string; + deployedSourceRole: string; + deployedArtifactCreationCodeCommitment: HexBytes32; + normalizedRuntimeCodeHash: HexBytes32; + expectedInstanceRuntimeCodeHash: HexBytes32 | null; + immutableReferencesCommitment: HexBytes32; + immutableBindingSpec: Record; + immutableBindingCommitment: HexBytes32; + runtimeCodeLength: string; + abiEventSetCommitment: HexBytes32; + templateCommitment: HexBytes32; +}>; + +type ParsedManifest = Readonly<{ + sources: readonly ManifestSourceBinding[]; + templates: readonly ManifestDynamicTemplate[]; + eventRules: readonly ManifestProjectionEventRule[]; +}>; + +function manifestHex(value: unknown): HexBytes32 { + try { + return canonicalBytes32(value); + } catch { + return projectorValidationFailure(); + } +} + +function parseManifest( + rows: readonly Record[], + state: Pick, +): ParsedManifest { + if (rows.length !== 1) return projectorValidationFailure(); + const row = rows[0]!; + if ( + exactUuid(row.epoch_id) !== state.epochId || + integerText(row.pointer_generation) !== state.pointerGeneration + ) { + return projectorValidationFailure(); + } + databaseBytes32(row.epoch_commitment); + databaseBytes32(row.artifact_creation_code_commitment); + const sources = exactArray(row.source_bindings, 256).map((entry) => { + const source = exactRecord(entry); + if (source.source_type !== "ethereum_contract" || source.source_address === null) { + return projectorValidationFailure(); + } + let sourceAddress: HexAddress; + try { + sourceAddress = canonicalAddress(source.source_address); + } catch { + return projectorValidationFailure(); + } + return Object.freeze({ + bindingId: exactUuid(source.binding_id), + sourceName: exactText(source.source_name, /^[A-Za-z][A-Za-z0-9]{0,95}$/u), + sourceRole: exactText(source.source_role, /^[a-z][a-z0-9_/-]{0,95}$/u), + sourceAddress, + inclusiveStartBlock: integerText(source.inclusive_start_block), + abiEventSetCommitment: manifestHex(source.abi_event_set_commitment), + bindingCommitment: manifestHex(source.binding_commitment), + }); + }); + const templates = exactArray(row.dynamic_source_templates, 128).map((entry) => { + const template = exactRecord(entry); + return Object.freeze({ + templateId: exactUuid(template.dynamic_source_template_id), + parentBindingId: exactUuid(template.parent_factory_release_binding_id), + parentBindingCommitment: manifestHex( + template.parent_factory_binding_commitment, + ), + parentSourceRole: exactText( + template.parent_source_role, + /^[a-z][a-z0-9_/-]{0,95}$/u, + ), + factoryEventType: exactText( + template.factory_event_type, + /^[A-Za-z][A-Za-z0-9]{0,95}$/u, + ), + deployedAddressField: exactText( + template.deployed_address_field, + /^[A-Za-z][A-Za-z0-9]{0,95}$/u, + ), + deployedSourceRole: exactText( + template.deployed_source_role, + /^(reward_vault|vesting_wallet)$/u, + ), + deployedArtifactCreationCodeCommitment: manifestHex( + template.deployed_artifact_creation_code_commitment, + ), + normalizedRuntimeCodeHash: manifestHex( + template.normalized_runtime_code_hash, + ), + expectedInstanceRuntimeCodeHash: + template.expected_instance_runtime_code_hash === null + ? null + : manifestHex(template.expected_instance_runtime_code_hash), + immutableReferencesCommitment: manifestHex( + template.immutable_references_commitment, + ), + immutableBindingSpec: exactRecord(template.immutable_binding_spec), + immutableBindingCommitment: manifestHex( + template.immutable_binding_commitment, + ), + runtimeCodeLength: integerText(template.runtime_code_length), + abiEventSetCommitment: manifestHex(template.abi_event_set_commitment), + templateCommitment: manifestHex(template.template_commitment), + }); + }); + const eventRules = exactArray(row.projection_event_rules, 512).map((entry) => { + const rule = exactRecord(entry); + return Object.freeze({ + ruleId: exactUuid(rule.projection_event_rule_id), + projectionKind: exactText( + rule.projection_kind, + /^[a-z][a-z0-9_/-]{0,95}$/u, + ), + sourceRole: exactText( + rule.source_role, + /^[a-z][a-z0-9_/-]{0,95}$/u, + ), + eventType: exactText( + rule.event_type, + /^[A-Za-z][A-Za-z0-9]{0,95}$/u, + ), + ruleCommitment: manifestHex(rule.rule_commitment), + }); + }); + exactArray(row.launch_completeness_requirements, 128); + return Object.freeze({ + sources: Object.freeze(sources), + templates: Object.freeze(templates), + eventRules: Object.freeze(eventRules), + }); +} + +function dynamicContractName(scope: ProjectorReleaseDatabaseScope) { + if (scope.releaseId === "classic-v3") return "ClassicV3RewardVault" as const; + if (scope.releaseId === "stock-paired-v1") return "StockV1RewardVault" as const; + if ( + scope.releaseId === "stock-paired-v2" || + scope.releaseId === "stock-paired-v3" + ) { + return "StockV2V3RewardVault" as const; + } + return null; +} + +function dynamicModel(scope: ProjectorReleaseDatabaseScope) { + return scope.releaseId === "classic-v3" ? "classic" as const : "stock-paired" as const; +} + +function immutableReferencesFromSpec(value: Record) { + const bindings = exactArray(value.bindings, 64); + return bindings.map((entry, index) => { + const binding = exactRecord(entry); + if (integerText(binding.ordinal) !== String(index)) { + return projectorValidationFailure(); + } + const start = Number(integerText(binding.offset)); + const length = Number(integerText(binding.length)); + if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length)) { + return projectorValidationFailure(); + } + return Object.freeze({ start, length }); + }); +} + +function parseDynamicAttestations(input: { + rows: readonly Record[]; + manifest: ParsedManifest; + scope: ProjectorReleaseDatabaseScope; +}): VerifiedDynamicSourceLineage[] { + const contractName = dynamicContractName(input.scope); + if (!contractName) { + if (input.rows.length !== 0) return projectorValidationFailure(); + return []; + } + return input.rows.map((row) => { + const templateId = exactUuid(row.dynamic_source_template_id); + const template = input.manifest.templates.find( + (candidate) => candidate.templateId === templateId, + ); + if (!template) return projectorValidationFailure(); + const parentBindingId = exactUuid(row.parent_factory_release_binding_id); + const parent = input.manifest.sources.find( + (candidate) => candidate.bindingId === parentBindingId, + ); + if ( + !parent || + template.parentBindingId !== parentBindingId || + databaseBytes32(row.parent_factory_binding_commitment) !== + parent.bindingCommitment || + template.parentBindingCommitment !== parent.bindingCommitment || + row.deployed_source_role !== template.deployedSourceRole || + databaseBytes32(row.normalized_runtime_code_hash) !== + template.normalizedRuntimeCodeHash || + databaseBytes32(row.immutable_references_commitment) !== + template.immutableReferencesCommitment || + databaseBytes32(row.immutable_binding_commitment) !== + template.immutableBindingCommitment || + databaseBytes32(row.abi_event_set_commitment) !== + template.abiEventSetCommitment || + databaseBytes32(row.template_commitment) !== template.templateCommitment || + integerText(row.runtime_code_length) !== template.runtimeCodeLength + ) { + return projectorValidationFailure(); + } + const exactRuntime = + row.expected_instance_runtime_code_hash === null + ? databaseBytes32(row.runtime_code_hash) + : databaseBytes32(row.expected_instance_runtime_code_hash); + if ( + template.expectedInstanceRuntimeCodeHash !== null && + exactRuntime !== template.expectedInstanceRuntimeCodeHash + ) { + return projectorValidationFailure(); + } + return canonicalDynamicSourceLineage({ + attestationId: exactUuid(row.dynamic_source_attestation_id), + sourceAddress: databaseAddress(row.deployed_source_address), + contractName, + model: dynamicModel(input.scope), + releaseVersion: input.scope.releaseId as VerifiedDynamicSourceLineage["releaseVersion"], + factoryAddress: parent.sourceAddress, + factoryContractName: parent.sourceName as VerifiedDynamicSourceLineage["factoryContractName"], + parentOccurrenceId: exactUuid(row.parent_factory_occurrence_id), + factoryBlockNumber: integerText(row.deployment_block_number), + expectedExactRuntimeCodeHash: exactRuntime, + expectedNormalizedRuntimeCodeHash: template.normalizedRuntimeCodeHash, + expectedImmutableReferencesCommitment: + template.immutableReferencesCommitment, + expectedRuntimeByteLength: template.runtimeCodeLength, + immutableReferences: immutableReferencesFromSpec(template.immutableBindingSpec), + }); + }); +} + +function dynamicTemplatesForPlan(input: { + manifest: ParsedManifest; + scope: ProjectorReleaseDatabaseScope; + state: RuntimeState; + envioProviderDeploymentId: string; + rpcProviderDeploymentIds: readonly [string, string]; +}): ProjectorDynamicSourceTemplate[] { + const descriptor = input.scope.releaseId === "classic-v3" + ? { + contractName: "ClassicV3RewardVault" as const, + model: "classic" as const, + releaseVersion: "classic-v3" as const, + parentFactoryContractName: "ClassicV3RewardVaultFactory" as const, + factoryEventName: "ClassicRewardVaultDeployed" as const, + } + : null; + if (descriptor === null) { + if (input.scope.modelId === "stock-paired") { + return []; + } + if (input.manifest.templates.length !== 0) { + return projectorValidationFailure(); + } + return []; + } + if (input.manifest.templates.length !== 1) { + return projectorValidationFailure(); + } + const template = input.manifest.templates[0]!; + const parent = input.manifest.sources.find( + ({ bindingId }) => bindingId === template.parentBindingId, + ); + if ( + !parent || + parent.sourceName !== descriptor.parentFactoryContractName || + parent.sourceRole !== "vault_factory" || + template.parentSourceRole !== parent.sourceRole || + template.parentBindingCommitment !== parent.bindingCommitment || + template.factoryEventType !== descriptor.factoryEventName || + template.deployedAddressField !== "vault" || + template.deployedSourceRole !== "reward_vault" + ) { + return projectorValidationFailure(); + } + return [ + Object.freeze({ + templateId: template.templateId, + contractName: descriptor.contractName, + model: descriptor.model, + releaseVersion: descriptor.releaseVersion, + parentFactoryAddress: parent.sourceAddress, + parentFactoryContractName: descriptor.parentFactoryContractName, + parentFactoryBindingId: parent.bindingId, + parentFactoryBindingCommitment: parent.bindingCommitment, + parentSourceRole: template.parentSourceRole, + factoryEventName: descriptor.factoryEventName, + deployedAddressField: "vault", + deployedSourceRole: "reward_vault", + deployedArtifactCreationCodeCommitment: + template.deployedArtifactCreationCodeCommitment, + expectedExactRuntimeCodeHash: + template.expectedInstanceRuntimeCodeHash, + expectedNormalizedRuntimeCodeHash: + template.normalizedRuntimeCodeHash, + expectedImmutableReferencesCommitment: + template.immutableReferencesCommitment, + expectedRuntimeByteLength: template.runtimeCodeLength, + immutableReferences: Object.freeze( + immutableReferencesFromSpec(template.immutableBindingSpec), + ), + immutableBindingSpec: Object.freeze(template.immutableBindingSpec), + immutableBindingCommitment: template.immutableBindingCommitment, + abiEventSetCommitment: template.abiEventSetCommitment, + templateCommitment: template.templateCommitment, + database: Object.freeze({ + scope: Object.freeze({ + releaseId: input.scope.releaseId, + modelId: input.scope.modelId, + sourceGroup: input.scope.sourceGroup, + }), + epochId: input.state.epochId, + pointerGeneration: input.state.pointerGeneration, + reorgGeneration: input.state.reorgGeneration, + envioProviderDeploymentId: input.envioProviderDeploymentId, + rpcProviderDeploymentIds: Object.freeze( + [...input.rpcProviderDeploymentIds], + ) as readonly [string, string], + }), + }), + ]; +} + +function parseProvisionalImmutableReferences(value: unknown) { + return Object.freeze( + exactArray(value, 64).map((entry) => { + const reference = exactRecord(entry); + const start = Number(integerText(reference.start)); + const length = Number(integerText(reference.length)); + if ( + !Number.isSafeInteger(start) || + !Number.isSafeInteger(length) || + start < 0 || + length < 1 + ) { + return projectorValidationFailure(); + } + return Object.freeze({ start, length }); + }), + ); +} + +function parseCurrentProvisionalDynamicSources(input: { + rows: readonly Record[]; + activationRows: readonly Record[]; + templates: readonly ProjectorDynamicSourceTemplate[]; + cursor: ProjectorPlan["cursor"]; + neutral: RuntimeState; + envioProviderDeploymentId: string; + rpcProviderDeploymentIds: readonly [string, string]; +}): VerifiedDynamicSourceLineage[] { + const activationByLineage = new Map< + string, + Readonly<{ + attestationId: string; + sourceAddress: HexAddress; + candidateId: string; + occurrenceId: string; + blockNumber: string; + blockHash: HexBytes32; + blockGlobalLogIndex: string; + }> + >(); + for (const row of input.activationRows) { + const provisionalLineageId = exactUuid(row.provisional_lineage_id); + if (activationByLineage.has(provisionalLineageId)) { + return projectorValidationFailure(); + } + activationByLineage.set( + provisionalLineageId, + Object.freeze({ + attestationId: exactUuid(row.dynamic_source_attestation_id), + sourceAddress: databaseAddress(row.deployed_source_address), + candidateId: exactText( + row.activation_candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ), + occurrenceId: exactUuid(row.activation_occurrence_id), + blockNumber: integerText(row.activation_block_number), + blockHash: databaseBytes32(row.activation_block_hash), + blockGlobalLogIndex: integerText( + row.activation_block_global_log_index, + ), + }), + ); + } + const consumedActivationLineages = new Set(); + const lineages = input.rows.map((row) => { + exactUuid(row.provisional_page_id); + const provisionalLineageId = exactUuid(row.provisional_lineage_id); + exactUuid(row.runtime_code_evidence_id); + const templateId = exactUuid(row.dynamic_source_template_id); + const template = input.templates.find( + (candidate) => candidate.templateId === templateId, + ); + const references = parseProvisionalImmutableReferences( + row.immutable_references, + ); + const factoryBlockNumber = integerText(row.factory_block_number); + const factoryBlockGlobalLogIndex = integerText( + row.factory_block_global_log_index, + ); + const snapshotBlockNumber = integerText(row.snapshot_block_number); + const exactRuntimeCodeHash = databaseBytes32( + row.expected_exact_runtime_code_hash, + ); + const normalizedRuntimeCodeHash = databaseBytes32( + row.expected_normalized_runtime_code_hash, + ); + const immutableReferencesCommitment = databaseBytes32( + row.expected_immutable_references_commitment, + ); + const runtimeByteLength = integerText(row.expected_runtime_byte_length); + const referenceShape = JSON.stringify(references); + const attestationId = exactUuid(row.dynamic_source_attestation_id); + const sourceAddress = databaseAddress(row.deployed_source_address); + const activation = activationByLineage.get(provisionalLineageId); + if (activation) consumedActivationLineages.add(provisionalLineageId); + const expectedCursorGeneration = integerText( + row.expected_cursor_generation, + ); + const expectedCursorBlockHash = databaseBytes32( + row.expected_cursor_block_hash, + ); + const cursorBindingValid = + expectedCursorGeneration === input.cursor.generation && + expectedCursorBlockHash === input.cursor.blockHash || + BigInt(input.cursor.generation) > BigInt(expectedCursorGeneration) && + BigInt(input.cursor.blockNumber) < BigInt(snapshotBlockNumber) || + activation !== undefined && + BigInt(input.cursor.generation) > BigInt(expectedCursorGeneration) && + (BigInt(input.cursor.blockNumber) > BigInt(activation.blockNumber) || + BigInt(input.cursor.blockNumber) === BigInt(activation.blockNumber) && + input.cursor.blockHash === activation.blockHash && + BigInt(input.cursor.blockGlobalLogIndex) >= + BigInt(activation.blockGlobalLogIndex)); + if ( + !template || + exactUuid(row.release_epoch_id) !== template.database.epochId || + integerText(row.release_pointer_generation) !== + template.database.pointerGeneration || + exactUuid(row.ingestion_epoch_id) !== input.neutral.epochId || + integerText(row.ingestion_pointer_generation) !== + input.neutral.pointerGeneration || + integerText(row.reorg_generation) !== + template.database.reorgGeneration || + !cursorBindingValid || + exactUuid(row.envio_provider_deployment_id) !== + input.envioProviderDeploymentId || + exactUuid(row.rpc_provider_a_id) !== input.rpcProviderDeploymentIds[0] || + exactUuid(row.rpc_provider_b_id) !== input.rpcProviderDeploymentIds[1] || + databaseBytes32(row.snapshot_block_hash) !== + databaseBytes32(row.factory_block_hash) || + snapshotBlockNumber !== factoryBlockNumber || + databaseBytes32(row.provisional_coverage_commitment) === + `0x${"00".repeat(32)}` || + databaseBytes32(row.parent_candidate_commitment) === + `0x${"00".repeat(32)}` || + row.contract_name !== template.contractName || + row.model !== template.model || + row.release_version !== template.releaseVersion || + databaseAddress(row.factory_address) !== + template.parentFactoryAddress || + row.factory_contract_name !== template.parentFactoryContractName || + exactRuntimeCodeHash !== template.expectedExactRuntimeCodeHash && + template.expectedExactRuntimeCodeHash !== null || + normalizedRuntimeCodeHash !== + template.expectedNormalizedRuntimeCodeHash || + immutableReferencesCommitment !== + template.expectedImmutableReferencesCommitment || + runtimeByteLength !== template.expectedRuntimeByteLength || + referenceShape !== JSON.stringify(template.immutableReferences) + || activation !== undefined && ( + activation.attestationId !== attestationId || + activation.sourceAddress !== sourceAddress || + BigInt(activation.blockNumber) < BigInt(factoryBlockNumber) || + activation.blockHash !== databaseBytes32(row.factory_block_hash) || + BigInt(activation.blockGlobalLogIndex) <= + BigInt(factoryBlockGlobalLogIndex) + ) + ) { + return projectorValidationFailure(); + } + return canonicalDynamicSourceLineage({ + attestationId, + sourceAddress, + contractName: template.contractName, + model: template.model, + releaseVersion: template.releaseVersion, + factoryAddress: template.parentFactoryAddress, + factoryContractName: template.parentFactoryContractName, + factoryCandidateId: exactText( + row.factory_candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ), + factoryBlockNumber, + factoryBlockGlobalLogIndex, + activationCandidateId: activation?.candidateId, + activationBlockNumber: activation?.blockNumber, + activationBlockHash: activation?.blockHash, + activationBlockGlobalLogIndex: activation?.blockGlobalLogIndex, + expectedExactRuntimeCodeHash: exactRuntimeCodeHash, + expectedNormalizedRuntimeCodeHash: normalizedRuntimeCodeHash, + expectedImmutableReferencesCommitment: immutableReferencesCommitment, + expectedRuntimeByteLength: runtimeByteLength, + immutableReferences: references, + }); + }); + if (consumedActivationLineages.size !== activationByLineage.size) { + return projectorValidationFailure(); + } + return lineages; +} + +function parseCursor(rows: readonly Record[]) { + if (rows.length !== 1) return projectorValidationFailure(); + const row = rows[0]!; + const generation = integerText(row.generation); + if (row.block_number === null || row.block_hash === null) { + return projectorValidationFailure(); + } + const isBlockBoundary = + row.block_global_log_index === null && row.candidate_id === null; + if ( + !isBlockBoundary && + (row.block_global_log_index === null || row.candidate_id === null) + ) { + return projectorValidationFailure(); + } + const blockGlobalLogIndex = isBlockBoundary + ? 0xffff_ffff + : Number(integerText(row.block_global_log_index)); + if ( + !Number.isSafeInteger(blockGlobalLogIndex) || + blockGlobalLogIndex < 0 || + blockGlobalLogIndex > 0xffff_ffff + ) { + return projectorValidationFailure(); + } + return Object.freeze({ + generation, + blockNumber: integerText(row.block_number), + blockHash: databaseBytes32(row.block_hash), + blockGlobalLogIndex, + candidateId: isBlockBoundary + ? "" + : exactText( + row.candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ), + isBlockBoundary, + }); +} + +function parseOptionalCursor(rows: readonly Record[]) { + if (rows.length !== 1) return projectorValidationFailure(); + const row = rows[0]!; + if ( + row.block_number === null && + row.block_hash === null && + row.block_global_log_index === null && + row.candidate_id === null + ) { + if (integerText(row.generation) !== "0") { + return projectorValidationFailure(); + } + return null; + } + return parseCursor(rows); +} + +/** + * Registers the immutable generation-zero predecessor used by the first raw + * backfill page. The database manifests independently determine the only + * acceptable anchor block; both production RPCs must already agree on that + * block and on a finalized safe head before this transaction can commit. + */ +export async function initializePostgresProjectorGenesis(input: { + executor: PostgresExecutor; + providers: readonly ProjectorProviderDatabaseBinding[]; + releaseScopes: readonly ProjectorReleaseDatabaseScope[]; + runtimeFence: ProjectorRuntimeFence; + evidence: ProjectorGenesisInitializationEvidence; + uuid?: () => string; + now?: () => Date; +}): Promise> { + const gateway = createProjectorDatabaseGateway({ executor: input.executor }); + const providers = canonicalProviderBindings(input.providers); + const releaseScopes = canonicalReleaseScopes(input.releaseScopes); + const runtimeFence = canonicalRuntimeFence(input.runtimeFence); + const envioIndexes = providers + .map((provider, index) => ({ provider, index })) + .filter(({ provider }) => provider.type === "envio_deployment"); + const rpcIndexes = providers + .map((provider, index) => ({ provider, index })) + .filter(({ provider }) => provider.type === "rpc_provider"); + if (envioIndexes.length !== 1 || rpcIndexes.length !== 2) { + return projectorValidationFailure(); + } + const anchorBlockNumber = integerText(input.evidence?.anchorBlockNumber); + const anchorBlockHash = canonicalBytes32(input.evidence?.anchorBlockHash); + const safeBlockNumber = integerText(input.evidence?.safeHead?.safeBlockNumber); + const safeBlockHash = canonicalBytes32(input.evidence?.safeHead?.safeBlockHash); + const cursorBlockHash = canonicalBytes32( + input.evidence?.safeHead?.cursorBlockHash, + ); + const providerHeads = input.evidence?.safeHead?.providerHeads?.map(integerText); + if ( + providerHeads?.length !== 2 || + anchorBlockHash !== cursorBlockHash || + BigInt(anchorBlockNumber) > BigInt(safeBlockNumber) || + providerHeads.some((head) => BigInt(head) < BigInt(safeBlockNumber) + 12n) + ) { + return projectorValidationFailure(); + } + const uuid = input.uuid ?? randomUUID; + const now = input.now ?? (() => new Date()); + + return gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + const neutral = await readRuntimeState({ + transaction, + scope: ENVIO_CONTROL_SCOPE, + providers, + }); + const envioProviderDeploymentId = + neutral.providerDeploymentIds[envioIndexes[0]!.index]!; + const rpcProviderDeploymentIds = rpcIndexes.map( + ({ index }) => neutral.providerDeploymentIds[index]!, + ) as [string, string]; + + const starts: bigint[] = []; + for (const scope of releaseScopes) { + const state = await readRuntimeState({ + transaction, + scope: { ...scope, projectorVersion: RELEASE_PROJECTOR_VERSION }, + providers, + }); + const manifestRows = await transaction.query( + "select * from programmable_private.get_projector_release_manifest_v1($1, $2, $3, $4, $5::uuid, $6)", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + state.epochId, + state.pointerGeneration, + ], + ); + const manifest = parseManifest(manifestRows, state); + starts.push(...manifest.sources.map(({ inclusiveStartBlock }) => + BigInt(inclusiveStartBlock) + )); + } + if (starts.length < 1) return projectorValidationFailure(); + const firstStartBlock = starts.reduce( + (minimum, current) => current < minimum ? current : minimum, + ); + if ( + firstStartBlock < 1n || + BigInt(anchorBlockNumber) !== firstStartBlock - 1n + ) { + return projectorValidationFailure(); + } + + const cursorQuery = + "select * from programmable_private.get_envio_ingestion_cursor_v1($1, $2::uuid, $3)"; + const cursorValues = ["1", envioProviderDeploymentId, "canonical-events"]; + const current = parseOptionalCursor( + await transaction.query(cursorQuery, cursorValues), + ); + if (current !== null) { + if ( + current.generation === "0" && + (current.blockNumber !== anchorBlockNumber || + current.blockHash !== anchorBlockHash || + !current.isBlockBoundary) + ) { + return projectorValidationFailure(); + } + return Object.freeze({ + status: "already-initialized" as const, + cursor: current, + }); + } + + const timestamp = now().toISOString(); + const ids = Object.freeze({ + run: exactUuid(uuid()), + observation: exactUuid(uuid()), + block: exactUuid(uuid()), + outcome: exactUuid(uuid()), + genesis: exactUuid(uuid()), + }); + const safeEvidence = providerEvidenceV2("safe_head", { + chain_id: "1", + epoch_id: neutral.epochId, + pointer_generation: neutral.pointerGeneration, + provider_a_id: rpcProviderDeploymentIds[0], + provider_b_id: rpcProviderDeploymentIds[1], + reported_chain_id_a: "1", + reported_chain_id_b: "1", + head_a: providerHeads[0]!, + head_b: providerHeads[1]!, + finality_depth: "12", + safe_block_number: safeBlockNumber, + safe_block_hash_a: safeBlockHash, + safe_block_hash_b: safeBlockHash, + }); + const requestCommitment = keccak256(toBytes(JSON.stringify([ + "projector-genesis-initialization-v1", + neutral.epochId, + neutral.pointerGeneration, + envioProviderDeploymentId, + ...rpcProviderDeploymentIds, + anchorBlockNumber, + anchorBlockHash, + safeBlockNumber, + safeBlockHash, + ...providerHeads, + ]))); + const resultCommitment = keccak256(toBytes(JSON.stringify([ + "projector-genesis-initialized-v1", + envioProviderDeploymentId, + anchorBlockNumber, + anchorBlockHash, + ]))); + exactIdResult(await transaction.query( + "select programmable_private.open_run($1::uuid, 'ingestion', '1', $2, $3, $4, $5::uuid, $6, $7, $8::bytea, $9::timestamptz) as id", + [ + ids.run, + ENVIO_CONTROL_SCOPE.releaseId, + ENVIO_CONTROL_SCOPE.modelId, + ENVIO_CONTROL_SCOPE.sourceGroup, + neutral.epochId, + neutral.pointerGeneration, + ENVIO_CONTROL_SCOPE.projectorVersion, + hexToBytes(requestCommitment), + timestamp, + ], + ), ids.run); + const observationId = await appendOrReuseSafeHeadObservation(transaction, [ + ids.observation, + ids.run, + rpcProviderDeploymentIds[0], + rpcProviderDeploymentIds[1], + "1", + "1", + providerHeads[0]!, + providerHeads[1]!, + 12, + safeBlockNumber, + hexToBytes(safeBlockHash), + hexToBytes(safeBlockHash), + safeEvidence.encodingVersion, + safeEvidence.canonicalPreimage, + hexToBytes(safeEvidence.contentFingerprint), + timestamp, + ]); + const blockEvidence = providerEvidenceV2("block", { + chain_id: "1", + epoch_id: neutral.epochId, + pointer_generation: neutral.pointerGeneration, + observation_id: observationId, + block_number: anchorBlockNumber, + provider_a_block_hash: anchorBlockHash, + provider_b_block_hash: anchorBlockHash, + }); + const blockEvidenceId = await appendOrReuseBlockEvidence( + transaction, + [ + ids.block, + observationId, + ids.run, + anchorBlockNumber, + hexToBytes(anchorBlockHash), + hexToBytes(anchorBlockHash), + blockEvidence.encodingVersion, + blockEvidence.canonicalPreimage, + hexToBytes(blockEvidence.contentFingerprint), + timestamp, + ], + ); + const genesisCommitment = keccak256(toBytes(JSON.stringify([ + "projector-genesis-anchor-v1", + envioProviderDeploymentId, + "canonical-events", + anchorBlockNumber, + anchorBlockHash, + blockEvidenceId, + ]))); + exactIdResult(await transaction.query( + "select programmable_private.append_run_outcome($1::uuid, $2::uuid, 'succeeded', $3::bytea, $4::timestamptz) as id", + [ids.outcome, ids.run, hexToBytes(resultCommitment), timestamp], + ), ids.outcome); + exactIdResult(await transaction.query( + "select programmable_private.register_envio_ingestion_genesis_v1($1::uuid, $2::uuid, $3::uuid, $4, $5::uuid, $6::bytea, $7::timestamptz) as id", + [ + ids.genesis, + ids.run, + envioProviderDeploymentId, + "canonical-events", + blockEvidenceId, + hexToBytes(genesisCommitment), + timestamp, + ], + ), ids.genesis); + + const cursor = parseCursor( + await transaction.query(cursorQuery, cursorValues), + ); + if ( + cursor.generation !== "0" || + cursor.blockNumber !== anchorBlockNumber || + cursor.blockHash !== anchorBlockHash || + !cursor.isBlockBoundary + ) { + return projectorValidationFailure(); + } + return Object.freeze({ status: "initialized" as const, cursor }); + }); +} + +function parseReorgTargets(rows: readonly Record[]): Readonly<{ + ancestors: readonly ReorgHistoryAncestor[]; + genesis: ReorgGenesisAnchor; + currentReorgGeneration: string; +}> { + if (rows.length < 1 || rows.length > 128) { + return projectorValidationFailure(); + } + const ancestors: ReorgHistoryAncestor[] = []; + let genesis: ReorgGenesisAnchor | undefined; + let currentReorgGeneration: string | undefined; + for (const row of rows) { + const rowReorgGeneration = integerText(row.current_reorg_generation); + if ( + currentReorgGeneration !== undefined && + currentReorgGeneration !== rowReorgGeneration + ) { + return projectorValidationFailure(); + } + currentReorgGeneration = rowReorgGeneration; + const blockNumber = integerText(row.block_number); + const blockHash = databaseBytes32(row.block_hash); + if (row.target_kind === "genesis") { + if ( + genesis !== undefined || + integerText(row.history_generation) !== "0" || + row.block_global_log_index !== null || + row.candidate_id !== null + ) { + return projectorValidationFailure(); + } + genesis = Object.freeze({ + kind: "genesis", + historyGeneration: "0", + genesisPointId: exactUuid(row.genesis_point_id), + blockNumber, + blockHash, + blockGlobalLogIndex: null, + candidateId: null, + }); + continue; + } + if (row.target_kind !== "history" || row.genesis_point_id !== null) { + return projectorValidationFailure(); + } + const blockGlobalLogIndex = row.block_global_log_index === null + ? null + : Number(integerText(row.block_global_log_index)); + if ( + blockGlobalLogIndex !== null && + (!Number.isSafeInteger(blockGlobalLogIndex) || + blockGlobalLogIndex < 0 || + blockGlobalLogIndex > 0xffff_ffff) + ) { + return projectorValidationFailure(); + } + const candidateId = row.candidate_id === null + ? null + : exactText( + row.candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ); + if ((blockGlobalLogIndex === null) !== (candidateId === null)) { + return projectorValidationFailure(); + } + ancestors.push(Object.freeze({ + kind: "history", + historyGeneration: integerText(row.history_generation), + blockNumber, + blockHash, + blockGlobalLogIndex, + candidateId, + })); + } + if (!genesis || currentReorgGeneration === undefined) { + return projectorValidationFailure(); + } + return Object.freeze({ + ancestors: Object.freeze(ancestors), + genesis, + currentReorgGeneration, + }); +} + +function exactIdResult( + rows: readonly Record[], + expected: string, +): void { + if (rows.length !== 1 || exactUuid(rows[0]?.id) !== expected) { + return projectorValidationFailure(); + } +} + +async function appendOrReuseSafeHeadObservation( + transaction: PostgresTransaction, + values: readonly PostgresParameter[], +): Promise { + const rows = await transaction.query( + "select programmable_private.append_or_reuse_safe_head_observation_v1($1::uuid, $2::uuid, $3::uuid, $4::uuid, $5, $6, $7::numeric, $8::numeric, $9, $10::numeric, $11::bytea, $12::bytea, $13, $14::bytea, $15::bytea, $16::timestamptz) as id", + values, + ); + if (rows.length !== 1) return projectorValidationFailure(); + return exactUuid(rows[0]?.id); +} + +async function appendOrReuseBlockEvidence( + transaction: PostgresTransaction, + values: readonly PostgresParameter[], +): Promise { + const rows = await transaction.query( + "select programmable_private.append_or_reuse_dual_rpc_block_evidence_v1($1::uuid, $2::uuid, $3::uuid, $4::numeric, $5::bytea, $6::bytea, $7, $8::bytea, $9::bytea, $10::timestamptz) as id", + values, + ); + if (rows.length !== 1) return projectorValidationFailure(); + return exactUuid(rows[0]?.id); +} + +function candidatePageJson(input: { + candidates: readonly EnvioCandidate[]; + evidence: DualRpcCandidateWindowEvidence; + firstSeenAt: string; +}) { + if ( + input.candidates.length !== input.evidence.candidates.length || + input.evidence.coveredCandidateCount !== input.candidates.length + ) { + return projectorValidationFailure(); + } + return input.candidates.map((candidate, index) => { + const verified = input.evidence.candidates[index]!; + if ( + verified.candidateId !== candidate.candidateId || + verified.candidateBlockHash !== candidate.blockHash || + verified.transactionHash !== candidate.transactionHash + ) { + return projectorValidationFailure(); + } + return { + candidateId: candidate.candidateId, + blockNumber: candidate.blockNumber, + blockHash: candidate.blockHash, + transactionHash: candidate.transactionHash, + transactionIndex: String(candidate.transactionIndex), + blockGlobalLogIndex: String(candidate.blockGlobalLogIndex), + sourceAddress: candidate.sourceAddress, + eventSignature: candidate.orderedTopics[0], + eventType: candidate.eventName, + orderedTopics: candidate.orderedTopics, + rawData: candidate.rawData, + decodedPayload: candidate.decodedPayload, + payloadHash: candidate.payloadHash, + providerCursor: candidate.candidateId, + contentCommitment: verified.rawLogCommitment, + firstSeenAt: input.firstSeenAt, + contractName: candidate.contractName, + }; + }); +} + +function sameStringPair( + left: readonly [string, string], + right: readonly [string, string], +) { + return left[0] === right[0] && left[1] === right[1]; +} + +function provisionalParentItem(input: { + plan: ProjectorPlan; + snapshotBlock: string; + evidence: DualRpcCandidateWindowEvidence; + candidate: EnvioCandidate; + verified: DualRpcCandidateWindowEvidence["candidates"][number]; + runtime: DualRpcDynamicRuntimeObservation; +}) { + const { candidate, verified, runtime } = input; + const matchingTemplates = input.plan.dynamicSourceTemplates.filter( + (template) => + template.templateId === runtime.template.templateId && + template.parentFactoryAddress === candidate.sourceAddress && + template.parentFactoryContractName === candidate.contractName && + template.factoryEventName === candidate.eventName, + ); + if (matchingTemplates.length !== 1) return projectorValidationFailure(); + const template = matchingTemplates[0]!; + const deployedAddress = candidate.decodedPayload[template.deployedAddressField]; + const configurationField = template.immutableBindingSpec + .factoryConfigurationField; + const configurationValue = + typeof configurationField === "string" + ? candidate.decodedPayload[configurationField] + : runtime.factoryConfigurationCommitment; + let sourceAddress: HexAddress; + let factoryConfigurationCommitment: HexBytes32; + try { + sourceAddress = canonicalAddress(deployedAddress); + factoryConfigurationCommitment = canonicalBytes32(configurationValue); + } catch { + return projectorValidationFailure(); + } + const runtimeCodeA = canonicalRawData(runtime.rawRuntimeCodeA); + const runtimeCodeB = canonicalRawData(runtime.rawRuntimeCodeB); + const reconstructedRuntimeCode = canonicalRawData( + runtime.reconstructedRuntimeCode, + ); + const runtimeCodeHashA = canonicalBytes32(runtime.runtimeCodeHashA); + const runtimeCodeHashB = canonicalBytes32(runtime.runtimeCodeHashB); + const normalizedRuntimeCodeHashA = canonicalBytes32( + runtime.normalizedRuntimeCodeHashA, + ); + const normalizedRuntimeCodeHashB = canonicalBytes32( + runtime.normalizedRuntimeCodeHashB, + ); + const immutableReferencesCommitment = canonicalBytes32( + runtime.immutableReferencesCommitment, + ); + const immutableValuesCommitment = canonicalBytes32( + runtime.immutableValuesCommitment, + ); + const reconstructedRuntimeCodeHash = canonicalBytes32( + runtime.reconstructedRuntimeCodeHash, + ); + const runtimeByteLength = String((runtimeCodeA.length - 2) / 2); + const immutableValues = runtime.immutableValues.map((value) => + canonicalRawData(value), + ); + let canonicalRuntimeEvidence: ReturnType; + let observedImmutableValues: readonly `0x${string}`[]; + let recomputedImmutableValuesCommitment: HexBytes32; + try { + canonicalRuntimeEvidence = runtimeBytecodeEvidence({ + runtimeBytecode: runtimeCodeA, + expectedByteLength: Number(runtimeByteLength), + immutableReferences: template.immutableReferences, + }); + const runtimeBytes = hexToBytes(runtimeCodeA); + observedImmutableValues = Object.freeze( + template.immutableReferences.map(({ start, length }) => + canonicalRawData( + `0x${Array.from( + runtimeBytes.slice(start, start + length), + (byte) => byte.toString(16).padStart(2, "0"), + ).join("")}`, + ), + ), + ); + recomputedImmutableValuesCommitment = canonicalBytes32( + keccak256( + concat([ + IMMUTABLE_VALUES_DOMAIN, + encodeAbiParameters([{ type: "bytes[]" }], [immutableValues]), + ]), + ), + ); + } catch { + return projectorValidationFailure(); + } + if ( + candidate.chainId !== 1 || + candidate.contractName !== template.parentFactoryContractName || + candidate.eventName !== template.factoryEventName || + candidate.releaseHint.model !== template.model || + candidate.releaseHint.releaseVersion !== template.releaseVersion || + candidate.blockNumber !== input.snapshotBlock || + verified.candidateId !== candidate.candidateId || + verified.sourceAddress !== candidate.sourceAddress || + verified.contractName !== candidate.contractName || + verified.eventName !== candidate.eventName || + verified.candidateBlockNumber !== candidate.blockNumber || + verified.candidateBlockHash !== candidate.blockHash || + verified.transactionHash !== candidate.transactionHash || + verified.payloadHash !== candidate.payloadHash || + verified.sourceKind !== "static" || + verified.model !== template.model || + verified.releaseVersion !== template.releaseVersion || + input.evidence.coverage.throughBlockNumber !== candidate.blockNumber || + input.evidence.coverage.throughBlockHash !== candidate.blockHash || + input.evidence.coverage.throughBlockGlobalLogIndex !== + String(0xffff_ffff) || + runtime.chainId !== 1 || + runtime.parentCandidateId !== candidate.candidateId || + runtime.sourceAddress !== sourceAddress || + runtime.deploymentBlockNumber !== candidate.blockNumber || + runtime.deploymentBlockHash !== candidate.blockHash || + !sameStringPair(runtime.providerIdentities, input.evidence.providerIdentities) || + !sameStringPair( + runtime.providerVendorGroups, + input.evidence.providerVendorGroups, + ) || + !sameStringPair( + runtime.providerEndpointCommitments, + input.evidence.providerEndpointCommitments, + ) || + !sameStringPair( + runtime.providerOriginCommitments, + input.evidence.providerOriginCommitments, + ) || + runtimeCodeA !== runtimeCodeB || + runtimeCodeA !== reconstructedRuntimeCode || + runtimeCodeHashA !== runtimeCodeHashB || + runtimeCodeHashA !== reconstructedRuntimeCodeHash || + canonicalRuntimeEvidence.exactRuntimeCodeHash !== runtimeCodeHashA || + normalizedRuntimeCodeHashA !== normalizedRuntimeCodeHashB || + canonicalRuntimeEvidence.normalizedRuntimeCodeHash !== + normalizedRuntimeCodeHashA || + normalizedRuntimeCodeHashA !== + template.expectedNormalizedRuntimeCodeHash || + canonicalRuntimeEvidence.immutableReferencesCommitment !== + immutableReferencesCommitment || + immutableReferencesCommitment !== + template.expectedImmutableReferencesCommitment || + JSON.stringify(runtime.immutableReferences) !== + JSON.stringify(template.immutableReferences) || + runtimeByteLength !== runtime.runtimeByteLengthA || + runtimeByteLength !== runtime.runtimeByteLengthB || + runtimeByteLength !== template.expectedRuntimeByteLength || + immutableValues.length !== observedImmutableValues.length || + immutableValues.some( + (value, index) => value !== observedImmutableValues[index], + ) || + immutableValuesCommitment !== recomputedImmutableValuesCommitment || + factoryConfigurationCommitment !== + canonicalBytes32(runtime.factoryConfigurationCommitment) || + (configurationField === null + ? runtime.deferredAllocationEvidenceCommitment === null + : runtime.deferredAllocationEvidenceCommitment !== null) || + runtime.providerCallCounts[0] !== 1 || + runtime.providerCallCounts[1] !== 1 || + (template.expectedExactRuntimeCodeHash !== null && + runtimeCodeHashA !== template.expectedExactRuntimeCodeHash) || + runtime.template.templateCommitment !== template.templateCommitment || + runtime.template.database.epochId !== template.database.epochId || + runtime.template.database.pointerGeneration !== + template.database.pointerGeneration || + runtime.template.database.reorgGeneration !== + template.database.reorgGeneration + ) { + return projectorValidationFailure(); + } + const eventSignature = candidate.orderedTopics[0]; + if (!eventSignature) return projectorValidationFailure(); + return Object.freeze({ + candidate, + verified, + runtime, + template, + sourceAddress, + factoryConfigurationCommitment, + runtimeCodeA, + runtimeCodeB, + runtimeCodeHashA, + runtimeCodeHashB, + normalizedRuntimeCodeHashA, + normalizedRuntimeCodeHashB, + immutableReferencesCommitment, + immutableValues, + immutableValuesCommitment, + reconstructedRuntimeCode, + reconstructedRuntimeCodeHash, + runtimeByteLength, + eventSignature, + }); +} + +function provisionalParentInputs(input: { + plan: ProjectorPlan; + snapshotBlock: string; + candidates: readonly EnvioCandidate[]; + evidence: DualRpcCandidateWindowEvidence; + runtimeObservations: readonly DualRpcDynamicRuntimeObservation[]; +}) { + const count = input.candidates.length; + if ( + count < 1 || + count > PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + input.evidence.coveredCandidateCount !== count || + input.evidence.candidates.length !== count || + input.runtimeObservations.length !== count + ) { + return projectorValidationFailure(); + } + const verifiedByCandidate = new Map( + input.evidence.candidates.map((verified) => [ + verified.candidateId, + verified, + ] as const), + ); + const runtimeByCandidate = new Map( + input.runtimeObservations.map((runtime) => [ + runtime.parentCandidateId, + runtime, + ] as const), + ); + if ( + verifiedByCandidate.size !== count || + runtimeByCandidate.size !== count || + new Set(input.candidates.map(({ candidateId }) => candidateId)).size !== count + ) { + return projectorValidationFailure(); + } + const parsed = input.candidates.map((candidate) => { + const verified = verifiedByCandidate.get(candidate.candidateId); + const runtime = runtimeByCandidate.get(candidate.candidateId); + if (verified === undefined || runtime === undefined) { + return projectorValidationFailure(); + } + return provisionalParentItem({ + plan: input.plan, + snapshotBlock: input.snapshotBlock, + evidence: input.evidence, + candidate, + verified, + runtime, + }); + }); + if ( + new Set(parsed.map(({ sourceAddress }) => sourceAddress)).size !== count + ) { + return projectorValidationFailure(); + } + return Object.freeze(parsed); +} + +const COMMIT_ENVIO_PAGE_SQL = `select programmable_private.commit_envio_ingestion_page_v1( + $1::uuid, $2::uuid, $3::uuid, $4::uuid, $5::text, + $6::bigint, $7::bigint, $8::numeric, + array( + select row( + page.item ->> 'candidateId', + (page.item ->> 'blockNumber')::numeric, + pg_catalog.decode(pg_catalog.substr(page.item ->> 'blockHash', 3), 'hex'), + pg_catalog.decode(pg_catalog.substr(page.item ->> 'transactionHash', 3), 'hex'), + (page.item ->> 'transactionIndex')::numeric, + (page.item ->> 'blockGlobalLogIndex')::numeric, + pg_catalog.decode(pg_catalog.substr(page.item ->> 'sourceAddress', 3), 'hex'), + pg_catalog.decode(pg_catalog.substr(page.item ->> 'eventSignature', 3), 'hex'), + page.item ->> 'eventType', + array( + select pg_catalog.decode(pg_catalog.substr(topic.value, 3), 'hex') + from pg_catalog.jsonb_array_elements_text(page.item -> 'orderedTopics') + with ordinality as topic(value, ordinal) + order by topic.ordinal + ), + pg_catalog.decode(pg_catalog.substr(page.item ->> 'rawData', 3), 'hex'), + page.item -> 'decodedPayload', + pg_catalog.decode(pg_catalog.substr(page.item ->> 'payloadHash', 3), 'hex'), + page.item ->> 'providerCursor', + pg_catalog.decode(pg_catalog.substr(page.item ->> 'contentCommitment', 3), 'hex'), + (page.item ->> 'firstSeenAt')::timestamptz, + page.item ->> 'contractName' + )::programmable_private.envio_candidate_page_item_v1 + from pg_catalog.jsonb_array_elements($9::jsonb) + with ordinality as page(item, ordinal) + order by page.ordinal + ), + $10::uuid, $11::uuid, $12::uuid, $13::uuid, + $14::bytea, $15::bytea[], $16::bytea[], $17::bytea, $18::bytea, + $19::smallint, $20::bytea, $21::bytea, $22::bytea, $23::timestamptz +)::text as generation`; + +export function createPostgresProjectorStore(input: { + executor: PostgresExecutor; + providers: readonly ProjectorProviderDatabaseBinding[]; + releaseScopes: readonly ProjectorReleaseDatabaseScope[]; + runtimeFence: ProjectorRuntimeFence; + streamId?: string; + uuid?: () => string; + now?: () => Date; +}): ProjectorStore { + const gateway = createProjectorDatabaseGateway({ executor: input.executor }); + const providers = canonicalProviderBindings(input.providers); + const envioIndexes = providers + .map((provider, index) => ({ provider, index })) + .filter(({ provider }) => provider.type === "envio_deployment"); + const rpcIndexes = providers + .map((provider, index) => ({ provider, index })) + .filter(({ provider }) => provider.type === "rpc_provider"); + if (envioIndexes.length !== 1 || rpcIndexes.length !== 2) { + return projectorValidationFailure(); + } + const streamId = exactText( + input.streamId ?? "canonical-events", + /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u, + ); + const releaseScopes = canonicalReleaseScopes(input.releaseScopes); + const runtimeFence = canonicalRuntimeFence(input.runtimeFence); + const uuid = input.uuid ?? randomUUID; + const now = input.now ?? (() => new Date()); + let latestPlan: ProjectorPlan | null = null; + const activationContexts = new Map< + string, + Readonly<{ + manifestArtifactCreationCodeCommitment: HexBytes32; + deployedArtifactCreationCodeCommitment: HexBytes32; + parentReceiptLogOrdinal: number; + }> + >(); + + return Object.freeze({ + async readPlan(): Promise { + return gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + const neutral = await readRuntimeState({ + transaction, + scope: ENVIO_CONTROL_SCOPE, + providers, + }); + const envioProviderDeploymentId = + neutral.providerDeploymentIds[envioIndexes[0]!.index]!; + const rpcProviderDeploymentIds = rpcIndexes.map( + ({ index }) => neutral.providerDeploymentIds[index]!, + ) as [string, string]; + const cursorRows = await transaction.query( + "select * from programmable_private.get_envio_ingestion_cursor_v1($1, $2::uuid, $3)", + ["1", envioProviderDeploymentId, streamId], + ); + const cursor = parseCursor(cursorRows); + const reorgRows = await transaction.query<{ generation: unknown }>( + "select programmable_private.get_projector_reorg_generation_v1()::text as generation", + ); + if (reorgRows.length !== 1) return projectorValidationFailure(); + const currentReorgGeneration = integerText( + reorgRows[0]?.generation, + ); + const dynamicSources: VerifiedDynamicSourceLineage[] = []; + const dynamicSourceTemplates: ProjectorDynamicSourceTemplate[] = []; + for (const scope of releaseScopes) { + const state = await readRuntimeState({ + transaction, + scope: { + ...scope, + projectorVersion: RELEASE_PROJECTOR_VERSION, + }, + providers, + }); + const manifestRows = await transaction.query( + "select * from programmable_private.get_projector_release_manifest_v1($1, $2, $3, $4, $5::uuid, $6)", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + state.epochId, + state.pointerGeneration, + ], + ); + const manifest = parseManifest(manifestRows, state); + const attestationRows = await transaction.query( + "select * from programmable_private.get_projector_dynamic_source_attestations_v1($1, $2, $3, $4, $5::uuid, $6)", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + state.epochId, + state.pointerGeneration, + ], + ); + dynamicSources.push( + ...parseDynamicAttestations({ + rows: attestationRows, + manifest, + scope, + }), + ); + dynamicSourceTemplates.push( + ...dynamicTemplatesForPlan({ + manifest, + scope, + state, + envioProviderDeploymentId, + rpcProviderDeploymentIds, + }), + ); + } + const provisionalRows = await transaction.query( + "select * from programmable_private.get_current_provisional_dynamic_sources_v1($1)", + [RELEASE_PROJECTOR_VERSION], + ); + const provisionalActivationRows = await transaction.query( + "select * from programmable_private.get_current_provisional_activation_boundaries_v1($1)", + [RELEASE_PROJECTOR_VERSION], + ); + const provisionalDynamicSources = parseCurrentProvisionalDynamicSources({ + rows: provisionalRows, + activationRows: provisionalActivationRows, + templates: dynamicSourceTemplates, + cursor, + neutral, + envioProviderDeploymentId, + rpcProviderDeploymentIds, + }); + const provisionalSourceAddresses = provisionalDynamicSources.map( + ({ sourceAddress }) => sourceAddress, + ); + if ( + new Set(dynamicSources.map(({ sourceAddress }) => sourceAddress)).size !== + dynamicSources.length || + new Set(provisionalSourceAddresses).size !== + provisionalSourceAddresses.length || + provisionalSourceAddresses.some((address) => + dynamicSources.some(({ sourceAddress }) => sourceAddress === address) + ) + ) { + return projectorValidationFailure(); + } + const plan = Object.freeze({ + cursor, + dynamicSources: Object.freeze([ + ...dynamicSources, + ...provisionalDynamicSources, + ]), + provisionalSourceAddresses: Object.freeze( + provisionalSourceAddresses, + ), + dynamicSourceTemplates: Object.freeze(dynamicSourceTemplates), + database: Object.freeze({ + epochId: neutral.epochId, + pointerGeneration: neutral.pointerGeneration, + reorgGeneration: currentReorgGeneration, + envioProviderDeploymentId, + rpcProviderDeploymentIds: Object.freeze( + rpcProviderDeploymentIds, + ) as readonly [string, string], + }), + }); + latestPlan = plan; + return plan; + }); + }, + + async resolvePendingDynamicSourceActivations( + resolveInput, + ): Promise { + const plan = latestPlan; + if ( + plan === null || + resolveInput.expectedCursorGeneration !== plan.cursor.generation || + resolveInput.expectedCursorBlockHash !== plan.cursor.blockHash || + resolveInput.expectedReorgGeneration !== + plan.database.reorgGeneration || + resolveInput.candidates.length > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + ) { + return projectorValidationFailure(); + } + const rows = await gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + return transaction.query( + "select * from programmable_private.resolve_pending_dynamic_source_activations_v1($1, $2::bigint, $3::bytea, $4::bigint)", + [ + RELEASE_PROJECTOR_VERSION, + plan.cursor.generation, + hexToBytes(plan.cursor.blockHash), + plan.database.reorgGeneration, + ], + ); + }); + const pending: PendingDynamicSourceActivation[] = []; + const seenSources = new Set(); + for (const row of rows) { + const parentCandidateId = exactText( + row.parent_candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ); + const parentMatches = resolveInput.candidates.filter( + ({ candidateId }) => candidateId === parentCandidateId, + ); + // Parent runtimes may be staged ahead of the durable ingestion cursor. + // A pending activation outside this exact candidate window is future + // work, not conflicting evidence. It is validated when its parent + // block becomes the current complete replay window. + if (parentMatches.length === 0) continue; + const sourceAddress = databaseAddress(row.source_address); + const templateId = exactUuid(row.dynamic_source_template_id); + const templates = plan.dynamicSourceTemplates.filter( + (template) => + template.templateId === templateId && + template.contractName === "ClassicV3RewardVault" && + template.database.epochId === exactUuid(row.release_epoch_id) && + template.database.pointerGeneration === + integerText(row.release_pointer_generation) && + template.database.reorgGeneration === + integerText(row.reorg_generation), + ); + const parentReceiptLogOrdinal = Number( + integerText(row.parent_receipt_log_ordinal), + ); + if ( + parentMatches.length !== 1 || + templates.length !== 1 || + !Number.isSafeInteger(parentReceiptLogOrdinal) || + parentReceiptLogOrdinal < 0 || + parentReceiptLogOrdinal > 0xffff_ffff || + seenSources.has(sourceAddress) + ) { + return projectorValidationFailure(); + } + const parent = parentMatches[0]!; + const template = templates[0]!; + const launchMatches = resolveInput.candidates.filter((candidate) => { + let rewardVault: HexAddress; + try { + rewardVault = canonicalAddress(candidate.decodedPayload.rewardVault); + } catch { + return false; + } + return ( + candidate.contractName === "ClassicV3Launcher" && + candidate.eventName === "MemeTokenLaunchedV2" && + rewardVault === sourceAddress && + candidate.blockNumber === parent.blockNumber && + candidate.blockHash === parent.blockHash && + candidate.transactionHash === parent.transactionHash && + candidate.blockGlobalLogIndex > parent.blockGlobalLogIndex + ); + }); + // The vault factory is permissionless. A direct factory deployment is + // not a Programmable launch and therefore has no launcher event to + // bind. Complete-window RPC verification still detects a genuinely + // missing Envio launch event before the cursor can advance. + if (launchMatches.length === 0) continue; + if (launchMatches.length !== 1) return projectorValidationFailure(); + const launch = launchMatches[0]!; + const parentOccurrenceId = projectorOccurrenceUuid({ + transactionHash: parent.transactionHash, + receiptLogOrdinal: String(parentReceiptLogOrdinal), + blockHash: parent.blockHash, + }); + const activationId = deterministicUuid( + "dynamic-source-activation", + exactUuid(row.release_epoch_id), + integerText(row.release_pointer_generation), + integerText(row.reorg_generation), + plan.cursor.generation, + parent.candidateId, + launch.candidateId, + sourceAddress, + ); + const providerVendorGroups = Object.freeze([ + exactText(row.provider_a_vendor, /^[a-z][a-z0-9_-]{0,31}$/u), + exactText(row.provider_b_vendor, /^[a-z][a-z0-9_-]{0,31}$/u), + ]) as readonly [string, string]; + const providerEndpointCommitments = Object.freeze([ + databaseBytes32(row.provider_a_endpoint_url_commitment), + databaseBytes32(row.provider_b_endpoint_url_commitment), + ]) as readonly [HexBytes32, HexBytes32]; + const databaseProviderIdentities = Object.freeze([ + exactText(row.provider_a_identity, /^[a-z0-9][a-z0-9._:/-]{0,95}$/u), + exactText(row.provider_b_identity, /^[a-z0-9][a-z0-9._:/-]{0,95}$/u), + ]) as readonly [string, string]; + if ( + databaseProviderIdentities[0] !== + `rpc:1:${providerVendorGroups[0]}` || + databaseProviderIdentities[1] !== + `rpc:1:${providerVendorGroups[1]}` + ) { + return projectorValidationFailure(); + } + const traceProviderIdentities = Object.freeze([ + `${providerVendorGroups[0]}-mainnet-${providerEndpointCommitments[0].slice(2, 34)}`, + `${providerVendorGroups[1]}-mainnet-${providerEndpointCommitments[1].slice(2, 34)}`, + ]) as readonly [string, string]; + const canonicalDeployment: CanonicalDynamicSourceDeploymentEvidence = + Object.freeze({ + provisionalPageId: exactUuid(row.provisional_page_id), + provisionalLineageId: exactUuid(row.provisional_lineage_id), + dynamicSourceAttestationId: exactUuid( + row.dynamic_source_attestation_id, + ), + runtimeCodeEvidenceId: exactUuid(row.runtime_code_evidence_id), + dynamicSourceTemplateId: templateId, + parentOccurrenceId, + parentCandidateId: parent.candidateId, + parentBlockNumber: parent.blockNumber, + parentBlockHash: parent.blockHash, + parentBlockGlobalLogIndex: parent.blockGlobalLogIndex, + parentTransactionHash: parent.transactionHash, + parentTransactionIndex: parent.transactionIndex, + parentSourceAddress: parent.sourceAddress, + parentContractName: parent.contractName, + parentEventName: parent.eventName, + parentPayloadHash: parent.payloadHash, + parentRawLogCommitment: databaseBytes32( + row.parent_candidate_commitment, + ), + canonicalStatusHistoryId: deterministicUuid( + "provisional-parent-status", + exactUuid(row.provisional_page_id), + parent.candidateId, + integerText(row.reorg_generation), + ), + safeHeadObservationId: exactUuid(row.safe_head_observation_id), + blockEvidenceId: exactUuid(row.target_block_evidence_id), + reorgGeneration: integerText(row.reorg_generation), + envioProviderDeploymentId: exactUuid( + row.envio_provider_deployment_id, + ), + rpcProviderDeploymentIds: Object.freeze([ + exactUuid(row.provider_a_id), + exactUuid(row.provider_b_id), + ]) as readonly [string, string], + providerIdentities: traceProviderIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments: Object.freeze([ + databaseBytes32(row.provider_a_endpoint_origin_commitment), + databaseBytes32(row.provider_b_endpoint_origin_commitment), + ]) as readonly [HexBytes32, HexBytes32], + }); + const ephemeralLineage = canonicalDynamicSourceLineage({ + attestationId: canonicalDeployment.dynamicSourceAttestationId, + sourceAddress, + contractName: template.contractName, + model: template.model, + releaseVersion: template.releaseVersion, + factoryAddress: template.parentFactoryAddress, + factoryContractName: template.parentFactoryContractName, + factoryCandidateId: parent.candidateId, + factoryBlockNumber: parent.blockNumber, + factoryBlockGlobalLogIndex: String(parent.blockGlobalLogIndex), + activationCandidateId: launch.candidateId, + activationBlockNumber: launch.blockNumber, + activationBlockHash: launch.blockHash, + activationBlockGlobalLogIndex: String( + launch.blockGlobalLogIndex, + ), + expectedExactRuntimeCodeHash: + template.expectedExactRuntimeCodeHash ?? + template.expectedNormalizedRuntimeCodeHash, + expectedNormalizedRuntimeCodeHash: + template.expectedNormalizedRuntimeCodeHash, + expectedImmutableReferencesCommitment: + template.expectedImmutableReferencesCommitment, + expectedRuntimeByteLength: template.expectedRuntimeByteLength, + immutableReferences: template.immutableReferences, + }); + activationContexts.set( + activationId, + Object.freeze({ + manifestArtifactCreationCodeCommitment: databaseBytes32( + row.manifest_artifact_creation_code_commitment, + ), + deployedArtifactCreationCodeCommitment: databaseBytes32( + row.deployed_artifact_creation_code_commitment, + ), + parentReceiptLogOrdinal, + }), + ); + seenSources.add(sourceAddress); + pending.push( + Object.freeze({ + activationId, + historicalParentCandidate: parent, + launchCandidate: launch, + sourceAddress, + template, + canonicalDeployment, + ephemeralLineage, + }), + ); + } + return Object.freeze(pending); + }, + + async stageVerifiedDynamicSourceActivations(stageInput): Promise { + const plan = latestPlan; + if ( + plan === null || + stageInput.blockComplete !== false || + stageInput.activations.length < 1 || + stageInput.activations.length > + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE || + stageInput.evidence.coveredCandidateCount !== + stageInput.evidence.candidates.length + ) { + return projectorValidationFailure(); + } + const timestamp = now().toISOString(); + const evidenceByCandidate = new Map( + stageInput.evidence.candidates.map((candidate) => [ + candidate.candidateId, + candidate, + ]), + ); + if (evidenceByCandidate.size !== stageInput.evidence.candidates.length) { + return projectorValidationFailure(); + } + const expectedKinds = Object.freeze([ + "classic-v3-initial-reward-configuration-v1", + "classic-v3-launch-reward-conservation-v1", + "classic-v3-runtime-activation-v1", + ]); + const activationPayloads: Record[] = []; + const modelEvidencePayloads: Record[] = []; + let activationBlockNumber: string | null = null; + let activationBlockHash: HexBytes32 | null = null; + let releaseEpochId: string | null = null; + let releasePointerGeneration: string | null = null; + let releaseReorgGeneration: string | null = null; + + for (const verifiedActivation of stageInput.activations) { + const pending = verifiedActivation.pending; + const context = activationContexts.get(pending.activationId); + const parentEvidence = evidenceByCandidate.get( + pending.historicalParentCandidate.candidateId, + ); + const launchEvidence = evidenceByCandidate.get( + pending.launchCandidate.candidateId, + ); + if (!context || !parentEvidence || !launchEvidence) { + return projectorValidationFailure(); + } + if (releaseEpochId === null) { + releaseEpochId = pending.template.database.epochId; + releasePointerGeneration = + pending.template.database.pointerGeneration; + releaseReorgGeneration = pending.template.database.reorgGeneration; + } else if ( + releaseEpochId !== pending.template.database.epochId || + releasePointerGeneration !== + pending.template.database.pointerGeneration || + releaseReorgGeneration !== + pending.template.database.reorgGeneration + ) { + return projectorValidationFailure(); + } + const evidenceKinds = verifiedActivation.modelVerificationEvidence + .map(({ evidenceKind }) => evidenceKind) + .sort(); + if ( + evidenceKinds.length !== 3 || + evidenceKinds.some((kind, index) => kind !== expectedKinds[index]) + ) { + return projectorValidationFailure(); + } + const evidenceByKind = new Map( + verifiedActivation.modelVerificationEvidence.map((entry) => [ + entry.evidenceKind, + entry, + ]), + ); + if (evidenceByKind.size !== 3) return projectorValidationFailure(); + for (const evidence of verifiedActivation.modelVerificationEvidence) { + const commitment = keccak256( + toBytes( + `${ACTIVATION_MODEL_EVIDENCE_DOMAIN}${canonicalizeFingerprintJson({ + activationId: pending.activationId, + evidenceKind: evidence.evidenceKind, + payload: evidence.payload, + })}`, + ), + ); + if ( + evidence.activationId !== pending.activationId || + canonicalBytes32(evidence.evidenceCommitment) !== commitment + ) { + return projectorValidationFailure(); + } + } + const initialEvidence = evidenceByKind.get( + "classic-v3-initial-reward-configuration-v1", + ); + const runtimeEvidence = evidenceByKind.get( + "classic-v3-runtime-activation-v1", + ); + if (!initialEvidence || !runtimeEvidence) { + return projectorValidationFailure(); + } + const initial = exactRecord(initialEvidence.payload); + const runtimePayload = exactRecord(runtimeEvidence.payload); + const runtimeObservation = exactRecord( + runtimePayload.runtimeObservation, + ); + const providerInitCodeHashes = exactArray( + initial.providerInitCodeHashes, + 2, + ).map(canonicalBytes32); + const providerConfigurationHashes = exactArray( + initial.providerFactoryConfigurationHashes, + 2, + ).map(canonicalBytes32); + const providerPredictedVaults = exactArray( + initial.providerPredictedVaults, + 2, + ).map(canonicalAddress); + const providerCtoAuthorities = exactArray( + initial.providerCtoAuthorities, + 2, + ).map(canonicalAddress); + const ctoAuthority = canonicalAddress(initial.ctoAuthority); + const sourceAddress = canonicalAddress(initial.vault); + const poolId = canonicalBytes32(initial.poolId); + const configurationHash = canonicalBytes32( + initial.factoryConfigurationHash, + ); + const activeConfigurationHash = canonicalBytes32( + initial.initialActiveConfigurationHash, + ); + const constructorArgumentsCommitment = canonicalBytes32( + initial.constructorArgumentsCommitment, + ); + const factoryInputCommitment = canonicalBytes32( + initial.factoryInputCommitment, + ); + const create2Salt = canonicalBytes32(initial.salt); + const locallyPredictedVault = canonicalAddress( + initial.locallyPredictedVault, + ); + const deployedArtifactCreationCodeCommitment = canonicalBytes32( + initial.deployedArtifactCreationCodeCommitment, + ); + const allocations = exactArray(initial.allocations, 5).map( + (value, index) => { + const allocation = exactRecord(value); + const allocationIndex = Number( + integerText(allocation.allocationIndex), + ); + const shareBps = integerText(allocation.shareBps); + if (allocationIndex !== index) return projectorValidationFailure(); + return Object.freeze({ + allocationIndex, + beneficiary: canonicalAddress(allocation.beneficiary), + shareBps, + }); + }, + ); + const allocationShares = allocations.map(({ shareBps }) => { + const share = BigInt(shareBps); + if (share < 1n || share > 10_000n) { + return projectorValidationFailure(); + } + return Number(share); + }); + const allocationBeneficiaries = allocations.map( + ({ beneficiary }) => beneficiary, + ); + if ( + allocations.length < 1 || + new Set(allocationBeneficiaries).size !== allocations.length || + allocationShares.reduce((sum, share) => sum + share, 0) !== 10_000 + ) { + return projectorValidationFailure(); + } + const parentFactory = canonicalAddress(initial.factory); + const parentEventVault = canonicalAddress( + pending.historicalParentCandidate.decodedPayload.vault, + ); + const parentEventPoolId = canonicalBytes32( + pending.historicalParentCandidate.decodedPayload.poolId, + ); + const parentEventFeeHook = canonicalAddress( + pending.historicalParentCandidate.decodedPayload.feeHook, + ); + const parentEventSalt = canonicalBytes32( + pending.historicalParentCandidate.decodedPayload.salt, + ); + const parentEventConfigurationHash = canonicalBytes32( + pending.historicalParentCandidate.decodedPayload.configurationHash, + ); + const launchEventVault = canonicalAddress( + pending.launchCandidate.decodedPayload.rewardVault, + ); + const launchEventPoolId = canonicalBytes32( + pending.launchCandidate.decodedPayload.poolId, + ); + const launchEventFeeHook = canonicalAddress( + pending.launchCandidate.decodedPayload.feeHook, + ); + const launchEventConfigurationHash = canonicalBytes32( + pending.launchCandidate.decodedPayload.rewardConfigurationHash, + ); + const localCommitments = classicV3InitialRewardCommitments({ + vault: sourceAddress, + feeHook: parentEventFeeHook, + poolId: parentEventPoolId, + ctoAuthority, + salt: parentEventSalt, + factoryConfigurationHash: configurationHash, + beneficiaries: allocationBeneficiaries, + sharesBps: allocationShares, + }); + if ( + parentFactory !== pending.historicalParentCandidate.sourceAddress || + parentEventVault !== sourceAddress || + parentEventPoolId !== poolId || + parentEventFeeHook !== launchEventFeeHook || + parentEventSalt !== create2Salt || + parentEventConfigurationHash !== configurationHash || + launchEventVault !== sourceAddress || + launchEventPoolId !== poolId || + launchEventConfigurationHash !== configurationHash || + localCommitments.factoryInputCommitment !== factoryInputCommitment || + localCommitments.constructorArgumentsCommitment !== + constructorArgumentsCommitment || + localCommitments.initialActiveConfigurationHash !== + activeConfigurationHash || + pending.sourceAddress !== sourceAddress || + locallyPredictedVault !== sourceAddress || + deployedArtifactCreationCodeCommitment !== + context.deployedArtifactCreationCodeCommitment || + providerInitCodeHashes[0] !== providerInitCodeHashes[1] || + providerConfigurationHashes[0] !== configurationHash || + providerConfigurationHashes[1] !== configurationHash || + providerPredictedVaults[0] !== sourceAddress || + providerPredictedVaults[1] !== sourceAddress || + providerCtoAuthorities[0] !== ctoAuthority || + providerCtoAuthorities[1] !== ctoAuthority || + JSON.stringify(initial.factoryProviderCallCounts) !== "[4,4]" || + runtimeObservation.sourceAddress !== sourceAddress || + runtimeObservation.activationBlockNumber !== + pending.launchCandidate.blockNumber || + runtimeObservation.activationBlockHash !== + pending.launchCandidate.blockHash || + runtimeObservation.activationBlockGlobalLogIndex !== + pending.launchCandidate.blockGlobalLogIndex || + !sameStringPair( + verifiedActivation.runtimeObservation.providerIdentities, + stageInput.evidence.providerIdentities, + ) || + !sameStringPair( + verifiedActivation.runtimeObservation.providerVendorGroups, + stageInput.evidence.providerVendorGroups, + ) || + !sameStringPair( + verifiedActivation.runtimeObservation.providerEndpointCommitments, + stageInput.evidence.providerEndpointCommitments, + ) || + !sameStringPair( + verifiedActivation.runtimeObservation.providerOriginCommitments, + stageInput.evidence.providerOriginCommitments, + ) + ) { + return projectorValidationFailure(); + } + const hookMatches = stageInput.candidates.filter((candidate) => { + try { + return ( + candidate.contractName === "ClassicV3Hook" && + candidate.eventName === "PoolRegistered" && + candidate.blockNumber === pending.launchCandidate.blockNumber && + candidate.blockHash === pending.launchCandidate.blockHash && + candidate.transactionHash === + pending.launchCandidate.transactionHash && + canonicalBytes32(candidate.decodedPayload.poolId) === poolId && + canonicalAddress(candidate.decodedPayload.rewardVault) === + sourceAddress + ); + } catch { + return false; + } + }); + if (hookMatches.length !== 1) return projectorValidationFailure(); + const hook = hookMatches[0]!; + const hookEvidence = evidenceByCandidate.get(hook.candidateId); + if (!hookEvidence) return projectorValidationFailure(); + const launchOccurrenceId = projectorOccurrenceUuid({ + transactionHash: pending.launchCandidate.transactionHash, + receiptLogOrdinal: String(launchEvidence.receiptLogOrdinal), + blockHash: pending.launchCandidate.blockHash, + }); + const hookOccurrenceId = projectorOccurrenceUuid({ + transactionHash: hook.transactionHash, + receiptLogOrdinal: String(hookEvidence.receiptLogOrdinal), + blockHash: hook.blockHash, + }); + const allocationHash = keccak256( + encodeAbiParameters( + [{ type: "address[]" }, { type: "uint16[]" }], + [ + allocations.map(({ beneficiary }) => beneficiary), + allocations.map(({ shareBps }) => Number(shareBps)), + ], + ), + ); + const predictResultHash = keccak256( + encodeAbiParameters([{ type: "address" }], [sourceAddress]), + ); + const payloadWithoutCommitment = Object.freeze({ + activationId: pending.activationId, + provisionalPageId: + pending.canonicalDeployment.provisionalPageId, + provisionalLineageId: + pending.canonicalDeployment.provisionalLineageId, + dynamicSourceAttestationId: + pending.canonicalDeployment.dynamicSourceAttestationId, + runtimeCodeEvidenceId: + pending.canonicalDeployment.runtimeCodeEvidenceId, + dynamicSourceTemplateId: + pending.canonicalDeployment.dynamicSourceTemplateId, + parentCandidateId: pending.historicalParentCandidate.candidateId, + parentOccurrenceId: + pending.canonicalDeployment.parentOccurrenceId, + parentBlockNumber: pending.historicalParentCandidate.blockNumber, + parentBlockHash: pending.historicalParentCandidate.blockHash, + parentBlockGlobalLogIndex: + pending.historicalParentCandidate.blockGlobalLogIndex, + parentReceiptLogOrdinal: context.parentReceiptLogOrdinal, + parentTransactionHash: + pending.historicalParentCandidate.transactionHash, + parentTransactionIndex: + pending.historicalParentCandidate.transactionIndex, + parentSourceAddress: + pending.historicalParentCandidate.sourceAddress, + parentPayloadHash: pending.historicalParentCandidate.payloadHash, + parentRawLogCommitment: + pending.canonicalDeployment.parentRawLogCommitment, + launchCandidateId: pending.launchCandidate.candidateId, + launchOccurrenceId, + launchBlockNumber: pending.launchCandidate.blockNumber, + launchBlockHash: pending.launchCandidate.blockHash, + launchBlockGlobalLogIndex: + pending.launchCandidate.blockGlobalLogIndex, + launchReceiptLogOrdinal: launchEvidence.receiptLogOrdinal, + launchTransactionHash: pending.launchCandidate.transactionHash, + hookCandidateId: hook.candidateId, + hookOccurrenceId, + hookReceiptLogOrdinal: hookEvidence.receiptLogOrdinal, + sourceAddress, + poolId, + ctoAuthority, + allocations, + allocationHash, + configurationHash, + activeConfigurationHash, + artifactCreationCodeCommitment: + context.manifestArtifactCreationCodeCommitment, + deployedArtifactCreationCodeCommitment, + factoryInputCommitment, + constructorArgumentsCommitment, + localInitCodeHash: providerInitCodeHashes[0]!, + create2Salt, + predictResultHash, + }); + const activationCommitment = keccak256( + toBytes( + `${ACTIVATION_PAYLOAD_DOMAIN}${canonicalizeFingerprintJson( + payloadWithoutCommitment as CanonicalJsonValue, + )}`, + ), + ); + activationPayloads.push({ + ...payloadWithoutCommitment, + activationCommitment, + }); + modelEvidencePayloads.push( + ...verifiedActivation.modelVerificationEvidence.map((evidence) => ({ + activationId: evidence.activationId, + evidenceKind: evidence.evidenceKind, + payload: evidence.payload, + evidenceCommitment: evidence.evidenceCommitment, + })), + ); + if (activationBlockNumber === null) { + activationBlockNumber = pending.launchCandidate.blockNumber; + activationBlockHash = pending.launchCandidate.blockHash; + } else if ( + activationBlockNumber !== pending.launchCandidate.blockNumber || + activationBlockHash !== pending.launchCandidate.blockHash + ) { + return projectorValidationFailure(); + } + } + if ( + activationBlockNumber === null || + activationBlockHash === null || + releaseEpochId === null || + releasePointerGeneration === null || + releaseReorgGeneration === null || + stageInput.evidence.coverage.throughBlockNumber !== + activationBlockNumber || + stageInput.evidence.coverage.throughBlockHash !== + activationBlockHash || + stageInput.evidence.coverage.throughBlockGlobalLogIndex !== + "4294967295" + ) { + return projectorValidationFailure(); + } + activationPayloads.sort((left, right) => + String(left.activationId).localeCompare(String(right.activationId)), + ); + modelEvidencePayloads.sort((left, right) => { + const activationOrder = String(left.activationId).localeCompare( + String(right.activationId), + ); + return activationOrder !== 0 + ? activationOrder + : String(left.evidenceKind).localeCompare( + String(right.evidenceKind), + ); + }); + const identity = [ + releaseEpochId, + releasePointerGeneration, + releaseReorgGeneration, + plan.database.epochId, + plan.database.pointerGeneration, + plan.database.reorgGeneration, + plan.cursor.generation, + ...stageInput.activations + .map(({ pending }) => pending.activationId) + .sort(), + ]; + const ids = Object.freeze({ + run: deterministicUuid("dynamic-activation-run", ...identity), + observation: deterministicUuid( + "dynamic-activation-observation", + ...identity, + ), + block: deterministicUuid("dynamic-activation-block", ...identity), + outcome: deterministicUuid("dynamic-activation-outcome", ...identity), + }); + const safeEvidence = providerEvidenceV2("safe_head", { + chain_id: "1", + epoch_id: plan.database.epochId, + pointer_generation: plan.database.pointerGeneration, + provider_a_id: plan.database.rpcProviderDeploymentIds[0], + provider_b_id: plan.database.rpcProviderDeploymentIds[1], + reported_chain_id_a: "1", + reported_chain_id_b: "1", + head_a: stageInput.evidence.providerHeads[0], + head_b: stageInput.evidence.providerHeads[1], + finality_depth: "12", + safe_block_number: stageInput.evidence.safeBlockNumber, + safe_block_hash_a: stageInput.evidence.safeBlockHash, + safe_block_hash_b: stageInput.evidence.safeBlockHash, + }); + const requestCommitment = keccak256( + toBytes( + canonicalizeFingerprintJson({ + kind: "dynamic-activation-stage-v1", + activations: activationPayloads, + modelEvidence: modelEvidencePayloads, + } as CanonicalJsonValue), + ), + ); + const resultCommitment = keccak256( + toBytes( + canonicalizeFingerprintJson({ + activationIds: stageInput.activations + .map(({ pending }) => pending.activationId) + .sort(), + }), + ), + ); + await gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + exactIdResult( + await transaction.query( + "select programmable_private.open_run($1::uuid, 'ingestion', '1', $2, $3, $4, $5::uuid, $6, $7, $8::bytea, $9::timestamptz) as id", + [ + ids.run, + ENVIO_CONTROL_SCOPE.releaseId, + ENVIO_CONTROL_SCOPE.modelId, + ENVIO_CONTROL_SCOPE.sourceGroup, + plan.database.epochId, + plan.database.pointerGeneration, + ENVIO_CONTROL_SCOPE.projectorVersion, + hexToBytes(requestCommitment), + timestamp, + ], + ), + ids.run, + ); + const observationId = await appendOrReuseSafeHeadObservation( + transaction, + [ + ids.observation, + ids.run, + plan.database.rpcProviderDeploymentIds[0], + plan.database.rpcProviderDeploymentIds[1], + "1", + "1", + stageInput.evidence.providerHeads[0], + stageInput.evidence.providerHeads[1], + 12, + stageInput.evidence.safeBlockNumber, + hexToBytes(stageInput.evidence.safeBlockHash), + hexToBytes(stageInput.evidence.safeBlockHash), + safeEvidence.encodingVersion, + safeEvidence.canonicalPreimage, + hexToBytes(safeEvidence.contentFingerprint), + timestamp, + ], + ); + const blockEvidence = providerEvidenceV2("block", { + chain_id: "1", + epoch_id: plan.database.epochId, + pointer_generation: plan.database.pointerGeneration, + observation_id: observationId, + block_number: activationBlockNumber, + provider_a_block_hash: activationBlockHash, + provider_b_block_hash: activationBlockHash, + }); + const blockEvidenceId = await appendOrReuseBlockEvidence( + transaction, + [ + ids.block, + observationId, + ids.run, + activationBlockNumber, + hexToBytes(activationBlockHash), + hexToBytes(activationBlockHash), + blockEvidence.encodingVersion, + blockEvidence.canonicalPreimage, + hexToBytes(blockEvidence.contentFingerprint), + timestamp, + ], + ); + const stagedRows = await transaction.query<{ staged_count: unknown }>( + "select programmable_private.stage_verified_dynamic_source_activations_v1($1::uuid, $2, $3::uuid, $4::bigint, $5::bigint, $6::bigint, $7::bytea, $8::uuid, $9::uuid, $10::uuid, $11::uuid, $12::uuid, $13::jsonb, $14::jsonb, $15::timestamptz) as staged_count", + [ + ids.run, + RELEASE_PROJECTOR_VERSION, + releaseEpochId, + releasePointerGeneration, + releaseReorgGeneration, + plan.cursor.generation, + hexToBytes(plan.cursor.blockHash), + plan.database.envioProviderDeploymentId, + plan.database.rpcProviderDeploymentIds[0], + plan.database.rpcProviderDeploymentIds[1], + observationId, + blockEvidenceId, + postgresJson(activationPayloads), + postgresJson(modelEvidencePayloads), + timestamp, + ], + ); + if ( + stagedRows.length !== 1 || + BigInt(integerText(stagedRows[0]!.staged_count)) !== + BigInt(stageInput.activations.length) + ) { + return projectorValidationFailure(); + } + exactIdResult( + await transaction.query( + "select programmable_private.append_run_outcome($1::uuid, $2::uuid, 'succeeded', $3::bytea, $4::timestamptz) as id", + [ + ids.outcome, + ids.run, + hexToBytes(resultCommitment), + timestamp, + ], + ), + ids.outcome, + ); + }); + }, + + async readReorgRecoveryState(recoveryInput) { + if ( + !Number.isSafeInteger(recoveryInput.maximumDepth) || + recoveryInput.maximumDepth < 1 || + recoveryInput.maximumDepth > 128 + ) { + return projectorValidationFailure(); + } + return gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + const runtime = await readRuntimeState({ + transaction, + scope: ENVIO_CONTROL_SCOPE, + providers, + }); + const envioProviderDeploymentId = + runtime.providerDeploymentIds[envioIndexes[0]!.index]!; + if ( + runtime.epochId !== recoveryInput.plan.database.epochId || + runtime.pointerGeneration !== + recoveryInput.plan.database.pointerGeneration || + envioProviderDeploymentId !== + recoveryInput.plan.database.envioProviderDeploymentId + ) { + return projectorValidationFailure(); + } + const rows = await transaction.query( + "select * from programmable_private.get_projector_reorg_targets_v1($1::uuid, $2, $3::integer)", + [ + envioProviderDeploymentId, + streamId, + recoveryInput.maximumDepth, + ], + ); + const parsed = parseReorgTargets(rows); + if ( + parsed.currentReorgGeneration !== + recoveryInput.plan.database.reorgGeneration + ) { + return projectorValidationFailure(); + } + return parsed; + }); + }, + + async recoverCanonicalReorg(recoveryInput) { + const { plan, recovery } = recoveryInput; + const timestamp = now().toISOString(); + const rpcProviderBindings = rpcIndexes.map(({ index }) => providers[index]!) as [ + ProjectorProviderDatabaseBinding, + ProjectorProviderDatabaseBinding, + ]; + const targetBlockHash = canonicalBytes32(recovery.targetBlockHash); + const safeBlockHash = canonicalBytes32(recovery.safeBlockHash); + const providerSafeBlockHashes = recovery.providerSafeBlockHashes.map( + canonicalBytes32, + ) as [HexBytes32, HexBytes32]; + const providerBlockHashes = recovery.providerBlockHashes.map( + canonicalBytes32, + ) as [HexBytes32, HexBytes32]; + const expectedCursorGeneration = integerText( + recovery.expectedGeneration, + ); + const nextCursorGeneration = integerText(recovery.nextGeneration); + const expectedReorgGeneration = integerText( + recovery.expectedReorgGeneration, + ); + const nextReorgGeneration = integerText(recovery.nextReorgGeneration); + const targetHistoryGeneration = integerText( + recovery.targetHistoryGeneration, + ); + const targetBlockNumber = integerText(recovery.targetBlockNumber); + const safeBlockNumber = integerText(recovery.safeBlockNumber); + if ( + recovery.action !== "rewind-and-replay" || + plan.cursor.generation !== expectedCursorGeneration || + plan.database.reorgGeneration !== expectedReorgGeneration || + nextCursorGeneration !== + (BigInt(expectedCursorGeneration) + 1n).toString() || + nextReorgGeneration !== + (BigInt(expectedReorgGeneration) + 1n).toString() || + BigInt(targetHistoryGeneration) >= BigInt(expectedCursorGeneration) || + targetBlockHash !== providerBlockHashes[0] || + targetBlockHash !== providerBlockHashes[1] || + safeBlockHash !== providerSafeBlockHashes[0] || + safeBlockHash !== providerSafeBlockHashes[1] || + recovery.finalityDepth !== "12" || + recovery.providerChainIds[0] !== 1 || + recovery.providerChainIds[1] !== 1 || + recovery.providerIdentities[0] !== + rpcProviderBindings[0].redactedIdentity || + recovery.providerIdentities[1] !== + rpcProviderBindings[1].redactedIdentity || + (recovery.targetBlockGlobalLogIndex === null) !== + (recovery.targetCandidateId === null) || + (targetHistoryGeneration === "0") !== + (recovery.genesisPointId !== null) + ) { + return projectorValidationFailure(); + } + const ids = Object.freeze({ + recovery: exactUuid(uuid()), + run: exactUuid(uuid()), + observation: exactUuid(uuid()), + block: exactUuid(uuid()), + outcome: exactUuid(uuid()), + }); + const safeEvidence = providerEvidenceV2("safe_head", { + chain_id: "1", + epoch_id: plan.database.epochId, + pointer_generation: plan.database.pointerGeneration, + provider_a_id: plan.database.rpcProviderDeploymentIds[0], + provider_b_id: plan.database.rpcProviderDeploymentIds[1], + reported_chain_id_a: "1", + reported_chain_id_b: "1", + head_a: integerText(recovery.providerHeads[0]), + head_b: integerText(recovery.providerHeads[1]), + finality_depth: "12", + safe_block_number: safeBlockNumber, + safe_block_hash_a: providerSafeBlockHashes[0], + safe_block_hash_b: providerSafeBlockHashes[1], + }); + const reasonCommitment = keccak256(toBytes(JSON.stringify([ + "projector-reorg-recovery-v1", + plan.cursor.generation, + plan.cursor.blockNumber, + plan.cursor.blockHash, + recovery, + ]))); + const requestCommitment = reasonCommitment; + const resultCommitment = keccak256(toBytes(JSON.stringify([ + nextCursorGeneration, + nextReorgGeneration, + targetHistoryGeneration, + targetBlockNumber, + targetBlockHash, + ]))); + return gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + exactIdResult(await transaction.query( + "select programmable_private.open_run($1::uuid, 'rewind', '1', $2, $3, $4, $5::uuid, $6, $7, $8::bytea, $9::timestamptz) as id", + [ + ids.run, + ENVIO_CONTROL_SCOPE.releaseId, + ENVIO_CONTROL_SCOPE.modelId, + ENVIO_CONTROL_SCOPE.sourceGroup, + plan.database.epochId, + plan.database.pointerGeneration, + ENVIO_CONTROL_SCOPE.projectorVersion, + hexToBytes(requestCommitment), + timestamp, + ], + ), ids.run); + const observationId = await appendOrReuseSafeHeadObservation(transaction, [ + ids.observation, + ids.run, + plan.database.rpcProviderDeploymentIds[0], + plan.database.rpcProviderDeploymentIds[1], + "1", + "1", + recovery.providerHeads[0], + recovery.providerHeads[1], + 12, + safeBlockNumber, + hexToBytes(providerSafeBlockHashes[0]), + hexToBytes(providerSafeBlockHashes[1]), + safeEvidence.encodingVersion, + safeEvidence.canonicalPreimage, + hexToBytes(safeEvidence.contentFingerprint), + timestamp, + ]); + const blockEvidence = providerEvidenceV2("block", { + chain_id: "1", + epoch_id: plan.database.epochId, + pointer_generation: plan.database.pointerGeneration, + observation_id: observationId, + block_number: targetBlockNumber, + provider_a_block_hash: providerBlockHashes[0], + provider_b_block_hash: providerBlockHashes[1], + }); + const blockEvidenceId = await appendOrReuseBlockEvidence( + transaction, + [ + ids.block, + observationId, + ids.run, + targetBlockNumber, + hexToBytes(providerBlockHashes[0]), + hexToBytes(providerBlockHashes[1]), + blockEvidence.encodingVersion, + blockEvidence.canonicalPreimage, + hexToBytes(blockEvidence.contentFingerprint), + timestamp, + ], + ); + exactIdResult(await transaction.query( + "select programmable_private.append_run_outcome($1::uuid, $2::uuid, 'succeeded', $3::bytea, $4::timestamptz) as id", + [ids.outcome, ids.run, hexToBytes(resultCommitment), timestamp], + ), ids.outcome); + const rows = await transaction.query<{ + cursor_generation: unknown; + reorg_generation: unknown; + release_checkpoint_count: unknown; + }>( + "select * from programmable_private.recover_projector_reorg_v1($1::uuid, $2::uuid, $3::uuid, $4::uuid, $5::uuid, $6::uuid, $7, $8::bigint, $9::bigint, $10::bigint, $11::bigint, $12::bigint, $13::numeric, $14::bytea, $15::numeric, $16, $17::uuid, $18, $19::bigint, $20::bytea, $21::bytea, $22::timestamptz)", + [ + ids.recovery, + ids.run, + ids.outcome, + observationId, + blockEvidenceId, + plan.database.envioProviderDeploymentId, + streamId, + expectedCursorGeneration, + nextCursorGeneration, + targetHistoryGeneration, + expectedReorgGeneration, + nextReorgGeneration, + targetBlockNumber, + hexToBytes(targetBlockHash), + recovery.targetBlockGlobalLogIndex, + recovery.targetCandidateId, + recovery.genesisPointId, + runtimeFence.holderId, + runtimeFence.generation, + hexToBytes(runtimeFence.tokenHash), + hexToBytes(reasonCommitment), + timestamp, + ], + ); + if (rows.length !== 1) return projectorValidationFailure(); + const generation = integerText(rows[0]?.cursor_generation); + const reorgGeneration = integerText(rows[0]?.reorg_generation); + const releaseCheckpointCount = Number( + integerText(rows[0]?.release_checkpoint_count), + ); + if ( + generation !== nextCursorGeneration || + reorgGeneration !== nextReorgGeneration || + !Number.isSafeInteger(releaseCheckpointCount) || + releaseCheckpointCount !== releaseScopes.length + ) { + return projectorValidationFailure(); + } + return Object.freeze({ + generation, + reorgGeneration, + releaseCheckpointCount, + }); + }); + }, + + async stageVerifiedDynamicParents(stageInput): Promise { + const timestamp = now().toISOString(); + const parsedItems = provisionalParentInputs(stageInput); + const first = parsedItems[0]!; + const { candidate: firstCandidate, template: firstTemplate } = first; + const scope = firstTemplate.database.scope; + if ( + stageInput.blockComplete !== false || + parsedItems.some( + ({ sourceAddress, template }) => + template.database.scope.releaseId !== scope.releaseId || + template.database.scope.modelId !== scope.modelId || + template.database.scope.sourceGroup !== scope.sourceGroup || + template.database.epochId !== firstTemplate.database.epochId || + template.database.pointerGeneration !== + firstTemplate.database.pointerGeneration || + template.database.reorgGeneration !== + firstTemplate.database.reorgGeneration || + template.database.envioProviderDeploymentId !== + stageInput.plan.database.envioProviderDeploymentId || + template.database.rpcProviderDeploymentIds[0] !== + stageInput.plan.database.rpcProviderDeploymentIds[0] || + template.database.rpcProviderDeploymentIds[1] !== + stageInput.plan.database.rpcProviderDeploymentIds[1] || + stageInput.plan.dynamicSources.some( + (known) => known.sourceAddress === sourceAddress, + ), + ) + ) { + return projectorValidationFailure(); + } + const identity = [ + firstTemplate.database.epochId, + firstTemplate.database.pointerGeneration, + firstTemplate.database.reorgGeneration, + stageInput.plan.cursor.generation, + firstCandidate.blockNumber, + firstCandidate.blockHash, + JSON.stringify( + parsedItems.map(({ candidate, sourceAddress, runtimeCodeHashA, template }) => [ + candidate.candidateId, + sourceAddress, + runtimeCodeHashA, + template.templateId, + ]), + ), + ] as const; + const ids = Object.freeze({ + run: deterministicUuid("provisional-dynamic-parent-run", ...identity), + observation: deterministicUuid( + "provisional-dynamic-parent-observation", + ...identity, + ), + block: deterministicUuid( + "provisional-dynamic-parent-block", + ...identity, + ), + page: deterministicUuid("provisional-dynamic-parent-page", ...identity), + outcome: deterministicUuid( + "provisional-dynamic-parent-outcome", + ...identity, + ), + }); + const safeEvidence = providerEvidenceV2("safe_head", { + chain_id: "1", + epoch_id: stageInput.plan.database.epochId, + pointer_generation: stageInput.plan.database.pointerGeneration, + provider_a_id: stageInput.plan.database.rpcProviderDeploymentIds[0], + provider_b_id: stageInput.plan.database.rpcProviderDeploymentIds[1], + reported_chain_id_a: "1", + reported_chain_id_b: "1", + head_a: stageInput.evidence.providerHeads[0], + head_b: stageInput.evidence.providerHeads[1], + finality_depth: "12", + safe_block_number: stageInput.evidence.safeBlockNumber, + safe_block_hash_a: stageInput.evidence.safeBlockHash, + safe_block_hash_b: stageInput.evidence.safeBlockHash, + }); + const records = parsedItems.map((parsed) => { + const itemIdentity = [ + ids.page, + parsed.candidate.candidateId, + parsed.sourceAddress, + parsed.runtimeCodeHashA, + parsed.template.templateId, + ] as const; + const itemIds = Object.freeze({ + runtime: deterministicUuid( + "provisional-dynamic-parent-runtime", + ...itemIdentity, + ), + lineage: deterministicUuid( + "provisional-dynamic-parent-lineage", + ...itemIdentity, + ), + attestation: deterministicUuid( + "provisional-dynamic-source-attestation", + ...itemIdentity, + ), + }); + const runtimeEvidence = providerEvidenceV2("runtime_code", { + chain_id: "1", + release_id: parsed.template.database.scope.releaseId, + model_id: parsed.template.database.scope.modelId, + source_group: parsed.template.database.scope.sourceGroup, + epoch_id: parsed.template.database.epochId, + pointer_generation: parsed.template.database.pointerGeneration, + source_address: parsed.sourceAddress, + deployment_block_evidence_id: ids.block, + deployment_block_number: parsed.candidate.blockNumber, + deployment_block_hash: parsed.candidate.blockHash, + provider_a_id: stageInput.plan.database.rpcProviderDeploymentIds[0], + provider_b_id: stageInput.plan.database.rpcProviderDeploymentIds[1], + runtime_code_hash_a: parsed.runtimeCodeHashA, + runtime_code_hash_b: parsed.runtimeCodeHashB, + runtime_code_a: parsed.runtimeCodeA, + runtime_code_b: parsed.runtimeCodeB, + normalized_runtime_code_hash_a: parsed.normalizedRuntimeCodeHashA, + normalized_runtime_code_hash_b: parsed.normalizedRuntimeCodeHashB, + immutable_references_commitment: + parsed.immutableReferencesCommitment, + immutable_values: parsed.immutableValues, + immutable_values_commitment: parsed.immutableValuesCommitment, + reconstructed_runtime_code: parsed.reconstructedRuntimeCode, + reconstructed_runtime_code_hash: + parsed.reconstructedRuntimeCodeHash, + }); + return Object.freeze({ + parsed, + ids: itemIds, + runtimeEvidence, + parentCandidate: Object.freeze({ + candidateId: parsed.candidate.candidateId, + blockNumber: parsed.candidate.blockNumber, + blockHash: parsed.candidate.blockHash, + transactionHash: parsed.candidate.transactionHash, + transactionIndex: String(parsed.candidate.transactionIndex), + blockGlobalLogIndex: String(parsed.candidate.blockGlobalLogIndex), + sourceAddress: parsed.candidate.sourceAddress, + eventSignature: parsed.eventSignature, + eventType: parsed.candidate.eventName, + decodedPayload: canonicalOccurrenceJson( + parsed.candidate.decodedPayload, + ), + payloadHash: parsed.candidate.payloadHash, + contentCommitment: parsed.verified.rawLogCommitment, + contractName: parsed.candidate.contractName, + }), + provisionalSource: Object.freeze({ + dynamicSourceAttestationId: itemIds.attestation, + parentCandidateId: parsed.candidate.candidateId, + provisionalLineageId: itemIds.lineage, + runtimeCodeEvidenceId: itemIds.runtime, + templateId: parsed.template.templateId, + }), + }); + }); + const executionTraceCommitment = + projectionExecutionTraceCommitmentV1( + stageInput.evidence.executionTrace, + ); + const requestCommitment = keccak256( + toBytes( + JSON.stringify([ + "provisional-dynamic-parent-v2", + ids.page, + firstTemplate.database, + stageInput.plan.cursor, + records.map(({ parentCandidate }) => parentCandidate), + records.map(({ provisionalSource }) => provisionalSource), + records.map( + ({ runtimeEvidence }) => runtimeEvidence.contentFingerprint, + ), + ]), + ), + ); + const resultCommitment = keccak256( + toBytes( + JSON.stringify([ + ids.page, + records.map(({ ids: itemIds }) => itemIds.runtime), + records.map(({ ids: itemIds }) => itemIds.attestation), + records.map( + ({ runtimeEvidence }) => runtimeEvidence.contentFingerprint, + ), + ]), + ), + ); + + await gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + exactIdResult( + await transaction.query( + "select programmable_private.open_run($1::uuid, $2, $3, $4, $5, $6, $7::uuid, $8, $9, $10::bytea, $11::timestamptz) as id", + [ + ids.run, + "ingestion", + "1", + ENVIO_CONTROL_SCOPE.releaseId, + ENVIO_CONTROL_SCOPE.modelId, + ENVIO_CONTROL_SCOPE.sourceGroup, + stageInput.plan.database.epochId, + stageInput.plan.database.pointerGeneration, + ENVIO_CONTROL_SCOPE.projectorVersion, + hexToBytes(requestCommitment), + timestamp, + ], + ), + ids.run, + ); + const observationId = await appendOrReuseSafeHeadObservation( + transaction, + [ + ids.observation, + ids.run, + stageInput.plan.database.rpcProviderDeploymentIds[0], + stageInput.plan.database.rpcProviderDeploymentIds[1], + "1", + "1", + stageInput.evidence.providerHeads[0], + stageInput.evidence.providerHeads[1], + 12, + stageInput.evidence.safeBlockNumber, + hexToBytes(stageInput.evidence.safeBlockHash), + hexToBytes(stageInput.evidence.safeBlockHash), + safeEvidence.encodingVersion, + safeEvidence.canonicalPreimage, + hexToBytes(safeEvidence.contentFingerprint), + timestamp, + ], + ); + const blockEvidence = providerEvidenceV2("block", { + chain_id: "1", + epoch_id: stageInput.plan.database.epochId, + pointer_generation: stageInput.plan.database.pointerGeneration, + observation_id: observationId, + block_number: firstCandidate.blockNumber, + provider_a_block_hash: firstCandidate.blockHash, + provider_b_block_hash: firstCandidate.blockHash, + }); + const blockEvidenceId = await appendOrReuseBlockEvidence( + transaction, + [ + ids.block, + observationId, + ids.run, + firstCandidate.blockNumber, + hexToBytes(firstCandidate.blockHash), + hexToBytes(firstCandidate.blockHash), + blockEvidence.encodingVersion, + blockEvidence.canonicalPreimage, + hexToBytes(blockEvidence.contentFingerprint), + timestamp, + ], + ); + for (const record of records) { + const { parsed, ids: itemIds, runtimeEvidence } = record; + exactIdResult( + await transaction.query( + "select programmable_private.append_dual_rpc_runtime_code_evidence($1::uuid, $2::uuid, $3::bytea, $4::uuid, $5::uuid, $6::uuid, $7::bytea, $8::bytea, $9::bytea, $10::bytea, $11::numeric, $12::numeric, $13::bytea, $14::bytea, $15::bytea, $16::bytea[], $17::bytea, $18::bytea, $19::bytea, $20, $21::bytea, $22::bytea, $23::bytea, $24::timestamptz) as id", + [ + itemIds.runtime, + ids.run, + hexToBytes(parsed.sourceAddress), + blockEvidenceId, + stageInput.plan.database.rpcProviderDeploymentIds[0], + stageInput.plan.database.rpcProviderDeploymentIds[1], + hexToBytes(parsed.runtimeCodeHashA), + hexToBytes(parsed.runtimeCodeHashB), + hexToBytes(parsed.runtimeCodeA), + hexToBytes(parsed.runtimeCodeB), + parsed.runtimeByteLength, + parsed.runtimeByteLength, + hexToBytes(parsed.normalizedRuntimeCodeHashA), + hexToBytes(parsed.normalizedRuntimeCodeHashB), + hexToBytes(parsed.immutableReferencesCommitment), + parsed.immutableValues.map(hexToBytes), + hexToBytes(parsed.immutableValuesCommitment), + hexToBytes(parsed.reconstructedRuntimeCode), + hexToBytes(parsed.reconstructedRuntimeCodeHash), + runtimeEvidence.encodingVersion, + runtimeEvidence.canonicalPreimage, + hexToBytes(runtimeEvidence.contentFingerprint), + hexToBytes(runtimeEvidence.contentFingerprint), + timestamp, + ], + ), + itemIds.runtime, + ); + } + exactIdResult( + await transaction.query( + "select programmable_private.stage_verified_dynamic_parents_v2($1::uuid, $2::uuid, $3, $4, $5, $6, $7::uuid, $8::bigint, $9::bigint, $10::bigint, $11::bytea, $12::uuid, $13, $14::uuid, $15::uuid, $16::uuid, $17::uuid, $18::numeric, $19::bytea, $20::bytea, $21::bytea[], $22::bytea[], $23::jsonb, $24::bytea, $25::jsonb, $26::jsonb, $27::timestamptz) as id", + [ + ids.page, + ids.run, + scope.releaseId, + scope.modelId, + scope.sourceGroup, + RELEASE_PROJECTOR_VERSION, + firstTemplate.database.epochId, + firstTemplate.database.pointerGeneration, + firstTemplate.database.reorgGeneration, + stageInput.plan.cursor.generation, + hexToBytes(stageInput.plan.cursor.blockHash), + stageInput.plan.database.envioProviderDeploymentId, + streamId, + stageInput.plan.database.rpcProviderDeploymentIds[0], + stageInput.plan.database.rpcProviderDeploymentIds[1], + observationId, + blockEvidenceId, + firstCandidate.blockNumber, + hexToBytes(firstCandidate.blockHash), + hexToBytes(stageInput.evidence.coverage.filterCommitment), + records.map(({ parsed }) => + hexToBytes(parsed.verified.rawLogCommitment) + ), + records.map(({ parsed }) => + hexToBytes(parsed.verified.rawLogCommitment) + ), + postgresJson(stageInput.evidence.executionTrace), + hexToBytes(executionTraceCommitment), + postgresJson( + records.map(({ parentCandidate }) => parentCandidate), + ), + postgresJson( + records.map(({ provisionalSource }) => provisionalSource), + ), + timestamp, + ], + ), + ids.page, + ); + exactIdResult( + await transaction.query( + "select programmable_private.stage_provisional_parent_receipt_ordinals_v1($1::uuid, $2::uuid, $3::text[], $4::numeric[], $5::timestamptz) as id", + [ + ids.page, + ids.run, + records.map(({ parsed }) => parsed.candidate.candidateId), + records.map(({ parsed }) => + String(parsed.verified.receiptLogOrdinal) + ), + timestamp, + ], + ), + ids.page, + ); + exactIdResult( + await transaction.query( + "select programmable_private.append_run_outcome($1::uuid, $2::uuid, 'succeeded', $3::bytea, $4::timestamptz) as id", + [ + ids.outcome, + ids.run, + hexToBytes(resultCommitment), + timestamp, + ], + ), + ids.outcome, + ); + }); + }, + + async commitVerifiedPage(commitInput): Promise<{ generation: string }> { + if (commitInput.blockComplete !== true) { + return projectorValidationFailure(); + } + const timestamp = now().toISOString(); + const ids = { + run: exactUuid(uuid()), + observation: exactUuid(uuid()), + block: exactUuid(uuid()), + outcome: exactUuid(uuid()), + coverage: exactUuid(uuid()), + }; + const plan = commitInput.plan; + const evidence = commitInput.evidence; + const lastCandidate = commitInput.candidates.at(-1); + const orderedLogCommitments = evidence.candidates.map( + ({ rawLogCommitment }) => canonicalBytes32(rawLogCommitment), + ); + const pageCommitment = canonicalBytes32( + evidence.coverage.providerLogCommitments[0], + ); + if ( + pageCommitment !== evidence.coverage.providerLogCommitments[1] || + (evidence.candidates.length > 0 && + (evidence.safeBlockNumber !== evidence.candidates[0]?.safeBlockNumber || + evidence.safeBlockHash !== evidence.candidates[0]?.safeBlockHash)) + ) { + return projectorValidationFailure(); + } + const finalBlockNumber = lastCandidate?.blockNumber ?? commitInput.snapshotBlock; + const finalBlockHash = lastCandidate?.blockHash ?? + canonicalBytes32(evidence.coverage.throughBlockHash); + const finalBlockGlobalLogIndex = lastCandidate?.blockGlobalLogIndex ?? + 0xffff_ffff; + const finalCandidateId = lastCandidate?.candidateId ?? "empty-page"; + if ( + evidence.coverage.throughBlockNumber !== finalBlockNumber || + (lastCandidate === undefined && + evidence.coverage.throughBlockGlobalLogIndex !== "4294967295") + ) { + return projectorValidationFailure(); + } + const safeEvidence = providerEvidenceV2("safe_head", { + chain_id: "1", + epoch_id: plan.database.epochId, + pointer_generation: plan.database.pointerGeneration, + provider_a_id: plan.database.rpcProviderDeploymentIds[0], + provider_b_id: plan.database.rpcProviderDeploymentIds[1], + reported_chain_id_a: "1", + reported_chain_id_b: "1", + head_a: evidence.providerHeads[0], + head_b: evidence.providerHeads[1], + finality_depth: "12", + safe_block_number: evidence.safeBlockNumber, + safe_block_hash_a: evidence.safeBlockHash, + safe_block_hash_b: evidence.safeBlockHash, + }); + const candidateBlocks = new Map(); + for (const candidate of commitInput.candidates) { + const blockHash = canonicalBytes32(candidate.blockHash); + const existing = candidateBlocks.get(candidate.blockNumber); + if (existing !== undefined && existing !== blockHash) { + return projectorValidationFailure(); + } + candidateBlocks.set(candidate.blockNumber, blockHash); + } + if (candidateBlocks.size === 0) { + candidateBlocks.set(finalBlockNumber, finalBlockHash); + } + if (candidateBlocks.get(finalBlockNumber) !== finalBlockHash) { + return projectorValidationFailure(); + } + const requestCommitment = keccak256( + toBytes( + JSON.stringify([ + plan.cursor.generation, + plan.cursor.candidateId, + commitInput.snapshotBlock, + pageCommitment, + ]), + ), + ); + const candidateJson = candidatePageJson({ + candidates: commitInput.candidates, + evidence, + firstSeenAt: timestamp, + }); + + return gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + await transaction.query( + "select programmable_private.open_run($1::uuid, $2, $3, $4, $5, $6, $7::uuid, $8, $9, $10::bytea, $11::timestamptz) as id", + [ + ids.run, + "ingestion", + "1", + ENVIO_CONTROL_SCOPE.releaseId, + ENVIO_CONTROL_SCOPE.modelId, + ENVIO_CONTROL_SCOPE.sourceGroup, + plan.database.epochId, + plan.database.pointerGeneration, + ENVIO_CONTROL_SCOPE.projectorVersion, + hexToBytes(requestCommitment), + timestamp, + ], + ); + const observationId = await appendOrReuseSafeHeadObservation( + transaction, + [ + ids.observation, + ids.run, + plan.database.rpcProviderDeploymentIds[0], + plan.database.rpcProviderDeploymentIds[1], + "1", + "1", + evidence.providerHeads[0], + evidence.providerHeads[1], + 12, + evidence.safeBlockNumber, + hexToBytes(evidence.safeBlockHash), + hexToBytes(evidence.safeBlockHash), + safeEvidence.encodingVersion, + safeEvidence.canonicalPreimage, + hexToBytes(safeEvidence.contentFingerprint), + timestamp, + ], + ); + const blockEvidenceRecords = Object.freeze( + [...candidateBlocks].map(([blockNumber, blockHash]) => { + const evidenceId = blockNumber === finalBlockNumber + ? ids.block + : exactUuid(uuid()); + return Object.freeze({ + evidenceId, + blockNumber, + blockHash, + evidence: providerEvidenceV2("block", { + chain_id: "1", + epoch_id: plan.database.epochId, + pointer_generation: plan.database.pointerGeneration, + observation_id: observationId, + block_number: blockNumber, + provider_a_block_hash: blockHash, + provider_b_block_hash: blockHash, + }), + }); + }), + ); + let finalBlockEvidenceId: string | undefined; + for (const blockRecord of blockEvidenceRecords) { + const storedBlockEvidenceId = await appendOrReuseBlockEvidence( + transaction, + [ + blockRecord.evidenceId, + observationId, + ids.run, + blockRecord.blockNumber, + hexToBytes(blockRecord.blockHash), + hexToBytes(blockRecord.blockHash), + blockRecord.evidence.encodingVersion, + blockRecord.evidence.canonicalPreimage, + hexToBytes(blockRecord.evidence.contentFingerprint), + timestamp, + ], + ); + if (blockRecord.blockNumber === finalBlockNumber) { + finalBlockEvidenceId = storedBlockEvidenceId; + } + } + if (!finalBlockEvidenceId) return projectorValidationFailure(); + const coverageEvidence = providerEvidenceV2("log_coverage", { + chain_id: "1", + epoch_id: plan.database.epochId, + pointer_generation: plan.database.pointerGeneration, + provider_deployment_id: plan.database.envioProviderDeploymentId, + stream_id: streamId, + expected_cursor_generation: plan.cursor.generation, + next_cursor_generation: (BigInt(plan.cursor.generation) + 1n).toString(), + previous_block_number: plan.cursor.blockNumber, + previous_block_global_log_index: plan.cursor.isBlockBoundary + ? null + : String(plan.cursor.blockGlobalLogIndex), + previous_candidate_id: plan.cursor.isBlockBoundary + ? null + : plan.cursor.candidateId, + from_block_number: evidence.coverage.fromBlockNumber, + to_block_number: finalBlockNumber, + final_block_hash: finalBlockHash, + final_block_global_log_index: String(finalBlockGlobalLogIndex), + final_candidate_id: finalCandidateId, + safe_head_observation_id: observationId, + final_block_evidence_id: finalBlockEvidenceId, + provider_a_id: plan.database.rpcProviderDeploymentIds[0], + provider_b_id: plan.database.rpcProviderDeploymentIds[1], + filter_commitment: evidence.coverage.filterCommitment, + ordered_log_commitments: orderedLogCommitments, + page_commitment: pageCommitment, + }); + const resultCommitment = keccak256( + encodeAbiParameters( + [{ type: "bytes32" }, { type: "bytes32" }], + [pageCommitment, coverageEvidence.contentFingerprint], + ), + ); + const commitRows = await transaction.query<{ generation: unknown }>( + COMMIT_ENVIO_PAGE_SQL, + [ + ids.outcome, + ids.coverage, + ids.run, + plan.database.envioProviderDeploymentId, + streamId, + plan.cursor.generation, + (BigInt(plan.cursor.generation) + 1n).toString(), + evidence.coverage.fromBlockNumber, + postgresJson(candidateJson), + observationId, + finalBlockEvidenceId, + plan.database.rpcProviderDeploymentIds[0], + plan.database.rpcProviderDeploymentIds[1], + hexToBytes(evidence.coverage.filterCommitment), + orderedLogCommitments.map(hexToBytes), + orderedLogCommitments.map(hexToBytes), + hexToBytes(pageCommitment), + hexToBytes(resultCommitment), + coverageEvidence.encodingVersion, + coverageEvidence.canonicalPreimage, + hexToBytes(coverageEvidence.contentFingerprint), + hexToBytes(coverageEvidence.contentFingerprint), + timestamp, + ], + ); + if (commitRows.length !== 1) return projectorValidationFailure(); + return Object.freeze({ + generation: integerText(commitRows[0]?.generation), + }); + }); + }, + }); +} + +type ProjectionRuntimeState = Readonly<{ + epochId: string; + pointerGeneration: string; + providerDeploymentIds: readonly string[]; + leaseGeneration: string; + checkpoint: ReleaseProjectionPlan["checkpoint"]; +}>; + +type ProjectionResolution = Readonly<{ + releaseBindingId: string | null; + dynamicSourceAttestationId: string | null; + abiEventSetCommitment: HexBytes32; + sourceRole: string; + projectionKind: string; +}>; + +type ProjectionPrivatePlan = Readonly<{ + runId: string; + leaseTokenHash: HexBytes32; + epochId: string; + pointerGeneration: string; + providerDeploymentIds: readonly string[]; + resolutions: ReadonlyMap; + manifest: ParsedManifest; +}>; + +function parseProjectionRuntimeState( + rows: readonly Record[], + providers: readonly ProjectorProviderDatabaseBinding[], +): ProjectionRuntimeState { + const base = parseRuntimeState(rows, providers); + const row = rows[0]!; + const leaseGeneration = integerText(row.lease_generation); + const checkpointGeneration = integerText(row.checkpoint_generation); + const reorgGeneration = integerText(row.reorg_generation); + const checkpointIdentityFields = [ + row.checkpoint_id, + row.checkpoint_block_number, + row.checkpoint_block_hash, + ]; + const checkpointCursorFields = [ + row.checkpoint_cursor_block_global_log_index, + row.checkpoint_cursor_candidate_id, + ]; + const checkpointAbsent = checkpointIdentityFields.every( + (value) => value === null, + ); + const checkpointIdentityComplete = checkpointIdentityFields.every( + (value) => value !== null, + ); + const checkpointCursorAbsent = checkpointCursorFields.every( + (value) => value === null, + ); + const checkpointCursorComplete = checkpointCursorFields.every( + (value) => value !== null, + ); + if ( + (!checkpointAbsent && !checkpointIdentityComplete) || + (!checkpointCursorAbsent && !checkpointCursorComplete) || + (checkpointAbsent && !checkpointCursorAbsent) + ) { + return projectorValidationFailure(); + } + let checkpoint: ReleaseProjectionPlan["checkpoint"] = null; + if (!checkpointAbsent && checkpointCursorComplete) { + exactUuid(row.checkpoint_id); + const blockGlobalLogIndex = Number( + integerText(row.checkpoint_cursor_block_global_log_index), + ); + if ( + !Number.isSafeInteger(blockGlobalLogIndex) || + blockGlobalLogIndex < 0 || + blockGlobalLogIndex > 0xffff_ffff + ) { + return projectorValidationFailure(); + } + checkpoint = Object.freeze({ + generation: checkpointGeneration, + reorgGeneration, + blockNumber: integerText(row.checkpoint_block_number), + blockHash: databaseBytes32(row.checkpoint_block_hash), + blockGlobalLogIndex, + candidateId: exactText( + row.checkpoint_cursor_candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ), + }); + } else if (!checkpointAbsent) { + exactUuid(row.checkpoint_id); + integerText(row.checkpoint_block_number); + databaseBytes32(row.checkpoint_block_hash); + if (checkpointGeneration === "0") return projectorValidationFailure(); + } else if (checkpointAbsent && + (checkpointGeneration !== "0" || reorgGeneration !== "0")) { + return projectorValidationFailure(); + } + return Object.freeze({ + epochId: base.epochId, + pointerGeneration: base.pointerGeneration, + providerDeploymentIds: base.providerDeploymentIds, + leaseGeneration, + checkpoint, + }); +} + +function parseProjectionCandidateRow( + row: Record, + expectedEnvioProviderId: string, +): Readonly<{ + candidate: StoredProjectionCandidate; + attemptCount: string; +}> { + if (exactUuid(row.provider_deployment_id) !== expectedEnvioProviderId) { + return projectorValidationFailure(); + } + const blockGlobalLogIndex = Number(integerText(row.block_global_log_index)); + const transactionIndex = Number(integerText(row.transaction_index)); + if ( + !Number.isSafeInteger(blockGlobalLogIndex) || + blockGlobalLogIndex < 0 || + blockGlobalLogIndex > 0xffff_ffff || + !Number.isSafeInteger(transactionIndex) || + transactionIndex < 0 || + transactionIndex > 0xffff_ffff + ) { + return projectorValidationFailure(); + } + const topics = exactArray(row.ordered_topics, 4).map(databaseBytes32); + const eventSignature = databaseBytes32(row.event_signature); + if (topics.length < 1 || topics[0] !== eventSignature) { + return projectorValidationFailure(); + } + const candidateId = exactText( + row.candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ); + const blockHash = databaseBytes32(row.block_hash); + const transactionHash = databaseBytes32(row.transaction_hash); + if ( + candidateId !== + `1:${blockHash}:${transactionHash}:${blockGlobalLogIndex}` + ) { + return projectorValidationFailure(); + } + databaseBytes32(row.content_commitment); + if (row.status !== "pending" && row.status !== "deferred") { + return projectorValidationFailure(); + } + return Object.freeze({ + candidate: Object.freeze({ + candidateId, + chainId: 1 as const, + blockNumber: integerText(row.block_number), + blockHash, + transactionHash, + transactionIndex, + blockGlobalLogIndex, + sourceAddress: databaseAddress(row.source_address), + contractName: exactText( + row.contract_name, + /^[A-Za-z][A-Za-z0-9]{0,95}$/u, + ), + eventName: exactText( + row.event_type, + /^[A-Za-z][A-Za-z0-9]{0,95}$/u, + ), + orderedTopics: topics, + rawData: dataFromBytea(row.raw_data), + decodedPayload: exactRecord(row.decoded_payload), + payloadHash: databaseBytes32(row.payload_hash), + }), + attemptCount: integerText(row.attempt_count), + }); +} + +function transactionAlignedProjectionPage>(rows: readonly T[], maximum: number): readonly T[] { + if (rows.length <= maximum) return Object.freeze([...rows]); + const boundaryTransaction = rows[maximum]!.candidate.transactionHash; + const page = rows.slice(0, maximum); + while ( + page.length > 0 && + page[page.length - 1]!.candidate.transactionHash === boundaryTransaction + ) { + page.pop(); + } + if (page.length === 0) return projectorValidationFailure(); + return Object.freeze(page); +} + +const REWARD_DELTA_PROJECTION_KINDS = Object.freeze(new Set([ + "creator-fee-checkpoint", + "beneficiary-claim", + "payout-change", + "reward-configuration-activation", +])); + +function isolateRewardTransaction>( + entries: readonly T[], + resolutions: ReadonlyMap, +): readonly T[] { + let transactionStart = 0; + while (transactionStart < entries.length) { + const transactionHash = entries[transactionStart]!.candidate.transactionHash; + let transactionEnd = transactionStart + 1; + while ( + transactionEnd < entries.length && + entries[transactionEnd]!.candidate.transactionHash === transactionHash + ) { + transactionEnd += 1; + } + const transaction = entries.slice(transactionStart, transactionEnd); + const rewardSources = new Set(); + for (const entry of transaction) { + const resolution = resolutions.get(entry.candidate.candidateId); + if ( + entry.action === "project" && + resolution && + REWARD_DELTA_PROJECTION_KINDS.has(resolution.projectionKind) + ) { + rewardSources.add(entry.candidate.sourceAddress); + } + } + if (rewardSources.size > 0) { + if (transactionStart > 0) { + return Object.freeze(entries.slice(0, transactionStart)); + } + // The caller supplies the complete remainder of this exact block when a + // reward transaction is first. Keeping every vault and every later + // reward occurrence is required because eth_call observes block-end + // state, not transaction-intermediate state. + return Object.freeze([...entries]); + } + transactionStart = transactionEnd; + } + return Object.freeze([...entries]); +} + +function rewardVaultsForProjection>( + entries: readonly T[], + resolutions: ReadonlyMap, +): readonly HexAddress[] { + const vaults = new Set(); + for (const entry of entries) { + const resolution = resolutions.get(entry.candidate.candidateId); + if ( + entry.action === "project" && + resolution?.sourceRole === "reward_vault" && + REWARD_DELTA_PROJECTION_KINDS.has(resolution.projectionKind) + ) { + vaults.add(entry.candidate.sourceAddress); + } + } + return Object.freeze([...vaults].sort()); +} + +function containsRewardProjection>( + entries: readonly T[], + resolutions: ReadonlyMap, +): boolean { + return rewardVaultsForProjection(entries, resolutions).length > 0; +} + +function projectionResolution(input: { + candidate: StoredProjectionCandidate; + manifest: ParsedManifest; + dynamicRows: readonly Record[]; +}): ProjectionResolution | null { + const staticMatches = input.manifest.sources.filter( + (source) => + source.sourceAddress === input.candidate.sourceAddress && + source.sourceName === input.candidate.contractName && + BigInt(source.inclusiveStartBlock) <= + BigInt(input.candidate.blockNumber) && + input.manifest.eventRules.some( + (rule) => + rule.sourceRole === source.sourceRole && + rule.eventType === input.candidate.eventName, + ), + ); + const dynamicMatches = input.dynamicRows.filter( + (row) => + databaseAddress(row.deployed_source_address) === + input.candidate.sourceAddress && + BigInt(integerText(row.deployment_block_number)) <= + BigInt(input.candidate.blockNumber) && + input.manifest.eventRules.some( + (rule) => + rule.sourceRole === row.deployed_source_role && + rule.eventType === input.candidate.eventName, + ), + ); + if (staticMatches.length + dynamicMatches.length > 1) { + return projectorValidationFailure(); + } + const staticMatch = staticMatches[0]; + if (staticMatch) { + const matchingRules = input.manifest.eventRules.filter( + (rule) => + rule.sourceRole === staticMatch.sourceRole && + rule.eventType === input.candidate.eventName, + ); + if (matchingRules.length !== 1) return projectorValidationFailure(); + const rule = matchingRules[0]!; + return Object.freeze({ + releaseBindingId: staticMatch.bindingId, + dynamicSourceAttestationId: null, + abiEventSetCommitment: staticMatch.abiEventSetCommitment, + sourceRole: staticMatch.sourceRole, + projectionKind: rule.projectionKind, + }); + } + const dynamicMatch = dynamicMatches[0]; + if (!dynamicMatch) return null; + const sourceRole = exactText( + dynamicMatch.deployed_source_role, + /^(reward_vault|vesting_wallet)$/u, + ); + const matchingRules = input.manifest.eventRules.filter( + (rule) => + rule.sourceRole === sourceRole && + rule.eventType === input.candidate.eventName, + ); + if (matchingRules.length !== 1) return projectorValidationFailure(); + const rule = matchingRules[0]!; + return Object.freeze({ + releaseBindingId: null, + dynamicSourceAttestationId: exactUuid( + dynamicMatch.dynamic_source_attestation_id, + ), + abiEventSetCommitment: databaseBytes32( + dynamicMatch.abi_event_set_commitment, + ), + sourceRole, + projectionKind: rule.projectionKind, + }); +} + +function poolIdsForProjection( + candidates: readonly StoredProjectionCandidate[], +): readonly HexBytes32[] { + const values = new Set(); + for (const candidate of candidates) { + const value = candidate.decodedPayload.poolId; + if (typeof value !== "string") continue; + try { + values.add(canonicalBytes32(value)); + } catch { + return projectorValidationFailure(); + } + } + return Object.freeze([...values].sort()); +} + +function parseKnownPool( + rows: readonly Record[], + releaseId: ProjectorReleaseDatabaseScope["releaseId"], + poolId: HexBytes32, +): ProjectorKnownPool | null { + if (rows.length === 0) return null; + if (rows.length !== 1) return projectorValidationFailure(); + const row = rows[0]!; + const token = databaseAddress(row.token); + const currency0 = databaseAddress(row.currency0); + const currency1 = databaseAddress(row.currency1); + if (token !== currency0 && token !== currency1) { + return projectorValidationFailure(); + } + const quote = token === currency0 ? currency1 : currency0; + const isClassic = releaseId.startsWith("classic-"); + return Object.freeze({ + releaseVersion: releaseId, + poolId, + token, + quoteAsset: isClassic ? null : quote, + rewardVault: + row.reward_vault === null ? null : databaseAddress(row.reward_vault), + }); +} + +/** + * Creates the release-scoped Postgres side of the projector. `readProjectionPlan` + * acquires a fenced lease and closes its transaction before any provider call. + * The final method re-enters Postgres only after the orchestrator has completed + * fresh Envio, dual-RPC and metadata verification. + */ +export function createPostgresReleaseProjectionStore(input: { + executor: PostgresExecutor; + providers: readonly ProjectorProviderDatabaseBinding[]; + scope: ProjectorReleaseDatabaseScope; + runtimeFence: ProjectorRuntimeFence; + rpcEvidenceBindings?: readonly [ + Omit, + Omit, + ]; + projectorVersion?: string; + holderId?: string; + uuid?: () => string; + now?: () => Date; +}): ReleaseProjectionStore { + const gateway = createProjectorDatabaseGateway({ executor: input.executor }); + const providers = canonicalProviderBindings(input.providers); + const rpcEvidenceBindings = input.rpcEvidenceBindings === undefined + ? null + : Object.freeze(input.rpcEvidenceBindings.map((binding) => Object.freeze({ + identity: exactText( + binding.identity, + /^[a-z0-9][a-z0-9-]{0,63}$/u, + ), + vendorGroup: exactText( + binding.vendorGroup, + /^[a-z0-9][a-z0-9-]{0,63}$/u, + ), + endpointCommitment: canonicalBytes32(binding.endpointCommitment), + endpointOriginCommitment: canonicalBytes32( + binding.endpointOriginCommitment, + ), + }))) as readonly [ + Omit, + Omit, + ]; + const scope = canonicalReleaseScope(input.scope); + const runtimeFence = canonicalRuntimeFence(input.runtimeFence); + const projectorVersion = exactText( + input.projectorVersion ?? "projector-v1", + /^[a-z0-9][a-z0-9._-]{0,95}$/u, + ); + const holderId = exactText( + input.holderId ?? "projector-runtime", + /^[a-z0-9][a-z0-9._-]{0,95}$/u, + ); + const uuid = input.uuid ?? randomUUID; + const now = input.now ?? (() => new Date()); + const envioIndex = providers.findIndex( + ({ type }) => type === "envio_deployment", + ); + const rpcIndexes = providers + .map((provider, index) => ({ provider, index })) + .filter(({ provider }) => provider.type === "rpc_provider") + .map(({ index }) => index); + if (envioIndex < 0 || rpcIndexes.length !== 2) { + return projectorValidationFailure(); + } + const privatePlans = new WeakMap(); + + return Object.freeze({ + async readProjectionPlan(): Promise { + return gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, runtimeFence); + const runtimeRows = await transaction.query( + "select * from programmable_private.get_projector_runtime_state_v1($1, $2, $3, $4, $5, $6::text[], $7::text[], $8::bytea[], $9::bytea[])", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + projectorVersion, + providers.map(({ type }) => type), + providers.map(({ redactedIdentity }) => redactedIdentity), + providers.map(({ deploymentCommitment }) => + hexToBytes(deploymentCommitment), + ), + providers.map(({ schemaCommitment }) => + hexToBytes(schemaCommitment), + ), + ], + ); + const runtime = parseProjectionRuntimeState(runtimeRows, providers); + const acquiredAt = now(); + if (!Number.isFinite(acquiredAt.valueOf())) { + return projectorValidationFailure(); + } + const expiresAt = new Date(acquiredAt.valueOf() + 90_000); + const leaseTokenHash = keccak256( + toBytes(`programmable:projector-lease:v1:${uuid()}`), + ); + const nextLeaseGeneration = ( + BigInt(runtime.leaseGeneration) + 1n + ).toString(); + const leaseInputCommitment = keccak256( + toBytes( + JSON.stringify([ + scope.releaseId, + scope.modelId, + scope.sourceGroup, + runtime.epochId, + runtime.pointerGeneration, + runtime.leaseGeneration, + nextLeaseGeneration, + holderId, + acquiredAt.toISOString(), + expiresAt.toISOString(), + ]), + ), + ); + const leaseRows = await transaction.query<{ acquired: unknown }>( + "select programmable_private.acquire_projector_lease($1, $2, $3, $4, $5, $6::uuid, $7::bigint, $8::bigint, $9::bigint, $10::bytea, $11, $12::timestamptz, $13::timestamptz, $14::bytea) as acquired", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + projectorVersion, + runtime.epochId, + runtime.pointerGeneration, + runtime.leaseGeneration, + nextLeaseGeneration, + hexToBytes(leaseTokenHash), + holderId, + acquiredAt.toISOString(), + expiresAt.toISOString(), + hexToBytes(leaseInputCommitment), + ], + ); + if (leaseRows.length !== 1 || leaseRows[0]?.acquired !== true) { + return projectorValidationFailure(); + } + const [ + manifestRows, + dynamicRows, + candidateRows, + ingestionCursorRows, + ] = await Promise.all([ + transaction.query( + "select * from programmable_private.get_projector_release_manifest_v1($1, $2, $3, $4, $5::uuid, $6)", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + runtime.epochId, + runtime.pointerGeneration, + ], + ), + transaction.query( + "select * from programmable_private.get_projector_dynamic_source_attestations_v1($1, $2, $3, $4, $5::uuid, $6)", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + runtime.epochId, + runtime.pointerGeneration, + ], + ), + transaction.query( + "select * from programmable_private.list_projector_candidate_page_v1($1, $2, $3, $4, $5::uuid, $6, $7, $8, $9::bytea, $10::numeric, $11::numeric, $12, $13, $14::timestamptz)", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + runtime.epochId, + runtime.pointerGeneration, + projectorVersion, + nextLeaseGeneration, + hexToBytes(leaseTokenHash), + runtime.checkpoint?.blockNumber ?? null, + runtime.checkpoint?.blockGlobalLogIndex ?? null, + runtime.checkpoint?.candidateId ?? null, + 33, + acquiredAt.toISOString(), + ], + ), + transaction.query( + "select * from programmable_private.get_envio_ingestion_cursor_v1($1, $2::uuid, $3)", + [ + "1", + runtime.providerDeploymentIds[envioIndex]!, + "canonical-events", + ], + ), + ]); + const ingestionCursor = parseCursor(ingestionCursorRows); + const manifest = parseManifest(manifestRows, { + epochId: runtime.epochId, + pointerGeneration: runtime.pointerGeneration, + }); + const dynamicSources = parseDynamicAttestations({ + rows: dynamicRows, + manifest, + scope, + }); + const parsedRows = candidateRows.map((row) => + parseProjectionCandidateRow( + row, + runtime.providerDeploymentIds[envioIndex]!, + ), + ); + if (parsedRows.length === 0) return null; + const resolutions = new Map(); + const resolveRows = ( + rows: readonly (typeof parsedRows)[number][], + ) => rows.map(({ candidate, attemptCount }) => { + const resolution = projectionResolution({ + candidate, + manifest, + dynamicRows, + }); + if (resolution) resolutions.set(candidate.candidateId, resolution); + return Object.freeze({ + candidate, + action: resolution ? "project" as const : "ignore" as const, + attemptCount, + }); + }); + let resolvedEntries = resolveRows(parsedRows); + const fetchAfter = async ( + after: StoredProjectionCandidate, + limit: number, + ) => { + const rows = await transaction.query( + "select * from programmable_private.list_projector_candidate_page_v1($1, $2, $3, $4, $5::uuid, $6, $7, $8, $9::bytea, $10::numeric, $11::numeric, $12, $13, $14::timestamptz)", + [ + "1", + scope.releaseId, + scope.modelId, + scope.sourceGroup, + runtime.epochId, + runtime.pointerGeneration, + projectorVersion, + nextLeaseGeneration, + hexToBytes(leaseTokenHash), + after.blockNumber, + after.blockGlobalLogIndex, + after.candidateId, + limit, + acquiredAt.toISOString(), + ], + ); + return resolveRows(rows.map((row) => + parseProjectionCandidateRow( + row, + runtime.providerDeploymentIds[envioIndex]!, + ) + )); + }; + const completePrefix = async ( + belongsToGroup: (entry: (typeof resolvedEntries)[number]) => boolean, + ) => { + while (true) { + const boundary = resolvedEntries.findIndex( + (entry) => !belongsToGroup(entry), + ); + if (boundary >= 0) { + return Object.freeze(resolvedEntries.slice(0, boundary)); + } + if ( + resolvedEntries.length > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + ) { + return projectorValidationFailure(); + } + const last = resolvedEntries.at(-1)?.candidate; + if (!last) return projectorValidationFailure(); + const remaining = + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + 1 - + resolvedEntries.length; + const next = await fetchAfter(last, Math.min(500, remaining)); + if (next.length === 0) { + const boundaryBlock = BigInt(ingestionCursor.blockNumber); + const groupBlock = BigInt(last.blockNumber); + if ( + !ingestionCursor.isBlockBoundary || + boundaryBlock < groupBlock + ) { + return null; + } + if ( + boundaryBlock === groupBlock && + ingestionCursor.blockHash !== last.blockHash + ) { + return projectorValidationFailure(); + } + return Object.freeze([...resolvedEntries]); + } + resolvedEntries = [...resolvedEntries, ...next]; + } + }; + + let batchKind: NonNullable = + "normal"; + let entries: readonly (typeof resolvedEntries)[number][]; + const first = resolvedEntries[0]!; + const firstTransactionHash = first.candidate.transactionHash; + const firstTransactionIsOversized = + resolvedEntries.length > PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE && + resolvedEntries[PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE]!.candidate + .transactionHash === firstTransactionHash; + if (firstTransactionIsOversized) { + const completedTransaction = await completePrefix( + (entry) => + entry.candidate.transactionHash === firstTransactionHash, + ); + if (completedTransaction === null) return null; + entries = completedTransaction; + batchKind = "oversized-transaction"; + if (containsRewardProjection(entries, resolutions)) { + const rewardBlockHash = first.candidate.blockHash; + const completedRewardBlock = await completePrefix( + (entry) => entry.candidate.blockHash === rewardBlockHash, + ); + if (completedRewardBlock === null) return null; + entries = completedRewardBlock; + batchKind = "reward-block"; + } + } else { + const page = transactionAlignedProjectionPage( + resolvedEntries, + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE, + ); + const isolated = isolateRewardTransaction(page, resolutions); + if ( + isolated.length === page.length && + containsRewardProjection(page, resolutions) + ) { + const rewardBlockHash = first.candidate.blockHash; + const completedRewardBlock = await completePrefix( + (entry) => entry.candidate.blockHash === rewardBlockHash, + ); + if (completedRewardBlock === null) return null; + entries = completedRewardBlock; + batchKind = "reward-block"; + } else { + entries = isolated; + } + } + if ( + entries.length < 1 || + entries.length > PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + ) { + return projectorValidationFailure(); + } + for (let index = 1; index < entries.length; index += 1) { + if ( + entries[index - 1]!.candidate.transactionHash === + entries[index]!.candidate.transactionHash && + entries[index - 1]!.action !== entries[index]!.action + ) { + return projectorValidationFailure(); + } + } + const selectedCandidateIds = new Set( + entries.map(({ candidate }) => candidate.candidateId), + ); + for (const candidateId of resolutions.keys()) { + if (!selectedCandidateIds.has(candidateId)) resolutions.delete(candidateId); + } + const runId = exactUuid(uuid()); + const requestCommitment = keccak256( + toBytes( + JSON.stringify([ + scope.releaseId, + runtime.epochId, + runtime.pointerGeneration, + nextLeaseGeneration, + batchKind, + entries.map(({ candidate, action }) => [ + candidate.candidateId, + action, + ]), + ]), + ), + ); + exactIdResult( + await transaction.query( + "select programmable_private.open_run($1::uuid, 'projection', 1, $2, $3, $4, $5::uuid, $6, $7, $8::bytea, $9::timestamptz) as id", + [ + runId, + scope.releaseId, + scope.modelId, + scope.sourceGroup, + runtime.epochId, + runtime.pointerGeneration, + projectorVersion, + hexToBytes(requestCommitment), + acquiredAt.toISOString(), + ], + ), + runId, + ); + const knownPools: ProjectorKnownPool[] = []; + for (const poolId of poolIdsForProjection( + entries + .filter(({ action }) => action === "project") + .map(({ candidate }) => candidate), + )) { + const known = parseKnownPool( + await transaction.query( + "select * from programmable_private.get_projector_pool_baseline_by_id_v1($1::uuid, $2::bytea)", + [runId, hexToBytes(poolId)], + ), + scope.releaseId, + poolId, + ); + if (known) knownPools.push(known); + } + const rewardVaults = rewardVaultsForProjection(entries, resolutions); + const rewardVerifications: NonNullable< + ReleaseProjectionPlan["rewardVerifications"] + >[number][] = []; + for (const rewardVault of rewardVaults) { + const state = parseProjectorRewardStateRows({ + activeRows: await transaction.query( + "select * from programmable_private.get_projector_reward_state_by_vault_v1($1::uuid, $2::bytea)", + [runId, hexToBytes(rewardVault)], + ), + balanceRows: await transaction.query( + "select * from programmable_private.get_projector_reward_balances_by_vault_v1($1::uuid, $2::bytea)", + [runId, hexToBytes(rewardVault)], + ), + scope, + vault: rewardVault, + }); + rewardVerifications.push(Object.freeze({ + model: state.model, + baseline: state.baseline, + })); + } + const plan = Object.freeze({ + scope, + entries: Object.freeze(entries), + dynamicSources: Object.freeze(dynamicSources), + knownPools: Object.freeze(knownPools), + lease: Object.freeze({ + generation: nextLeaseGeneration, + expiresAt: expiresAt.toISOString(), + }), + checkpoint: runtime.checkpoint, + rewardVerification: null, + rewardVerifications: Object.freeze(rewardVerifications), + batchKind, + }); + privatePlans.set( + plan, + Object.freeze({ + runId, + leaseTokenHash, + epochId: runtime.epochId, + pointerGeneration: runtime.pointerGeneration, + providerDeploymentIds: runtime.providerDeploymentIds, + resolutions, + manifest, + }), + ); + return plan; + }); + }, + + async commitVerifiedProjection( + projection: VerifiedReleaseProjection, + ): Promise> { + const privatePlan = privatePlans.get(projection.plan); + if (!privatePlan) return projectorValidationFailure(); + return commitPostgresVerifiedProjection({ + gateway, + providers, + rpcEvidenceBindings, + scope, + projectorVersion, + uuid, + now, + privatePlan, + projection, + runtimeFence, + }); + }, + }); +} + +const PROJECTION_ROUTE_KEYS = Object.freeze([ + "classic-v3-profile", + "creator-profile", + "explore-chart", + "explore-list", + "explore-token", + "launch-lookup", +] as const); + +const ZERO_ADDRESS: HexAddress = + "0x0000000000000000000000000000000000000000"; +const ZERO_BYTES32: HexBytes32 = + "0x0000000000000000000000000000000000000000000000000000000000000000"; +const PROJECTOR_MAXIMUM_REWARD_VERIFICATION_ACCOUNTS = 4_096; +const PROJECTOR_MAXIMUM_REWARD_VERIFICATION_CHUNKS = 86; +const PROJECTOR_MAXIMUM_REWARD_CALLS_PER_CHUNK = 128; +const PROJECTOR_MAXIMUM_REWARD_AGGREGATE_CALLS = + PROJECTOR_MAXIMUM_REWARD_VERIFICATION_CHUNKS * + PROJECTOR_MAXIMUM_REWARD_CALLS_PER_CHUNK; + +type OccurrenceWrite = Readonly<{ + occurrence: ProjectorOccurrenceFact; + fact: ProjectorEventFact; + occurrenceId: string; + logicalEventId: string; + resolutionId: string; + blockEvidenceId: string; +}>; + +type DetailedPoolBaseline = Readonly<{ + poolProjectionId: string; + launchProjectionId: string; + token: HexAddress; + creator: HexAddress; + rewardVault: HexAddress | null; + currency0: HexAddress; + currency1: HexAddress; + poolKeyFee: string; + tickSpacing: string; + hook: HexAddress; + poolFeeConfigurationId: string | null; + buySwapFeeBps: string | null; + sellSwapFeeBps: string | null; + buyCreatorFeeBps: string | null; + sellCreatorFeeBps: string | null; + launcherFeeBps: string | null; + transferTaxBps: string | null; + lpFeePips: string | null; + lastSourceOccurrenceId: string; +}>; + +type VerifiedRewardSeed = Readonly<{ + allocationFactId: string; + allocationEvidenceId: string; + factoryOccurrenceId: string; + vault: HexAddress; + beneficiaries: readonly HexAddress[]; + sharesBps: readonly string[]; + configurationHash: HexBytes32; + activeConfigurationHash: HexBytes32; +}>; + +type ProjectorRewardState = Readonly<{ + model: ProjectorRewardModel; + initialAllocationFactId: string; + initialAllocationEvidenceId: string; + baseline: ProjectorRewardBaseline; +}>; + +async function materializeDynamicActivationSeeds(input: { + transaction: PostgresTransaction; + runId: string; + scope: ProjectorReleaseDatabaseScope; + targetBlockNumber: string; + targetBlockHash: HexBytes32; + verifiedAt: string; +}): Promise[]> { + if (input.scope.releaseId !== "classic-v3") return Object.freeze([]); + const rows = await input.transaction.query( + "select * from programmable_private.get_dynamic_activation_seed_requests_v1($1::uuid, $2::numeric, $3::bytea)", + [ + input.runId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + ], + ); + const expectedPairs: Array<{ factId: string; evidenceId: string }> = []; + for (const row of rows) { + const activationId = exactUuid(row.activation_id); + const vault = databaseAddress(row.vault); + const beneficiaries = exactArray(row.ordered_beneficiaries, 5).map( + databaseAddress, + ); + const sharesBps = exactArray(row.ordered_shares_bps, 5).map(integerText); + const allocationHash = databaseBytes32(row.allocation_hash); + const configurationHash = databaseBytes32(row.configuration_hash); + const activeConfigurationHash = databaseBytes32( + row.active_configuration_hash, + ); + const artifactCreationCodeCommitment = databaseBytes32( + row.artifact_creation_code_commitment, + ); + const constructorArgumentsCommitment = databaseBytes32( + row.constructor_arguments_commitment, + ); + const localInitCodeHash = databaseBytes32(row.local_init_code_hash); + const create2Salt = databaseBytes32(row.create2_salt); + const predictResultHash = databaseBytes32(row.predict_result_hash); + const factoryTransactionHash = databaseBytes32( + row.factory_transaction_hash, + ); + const factoryReceiptLogOrdinal = integerText( + row.factory_receipt_log_ordinal, + ); + const factoryBlockHash = databaseBytes32(row.factory_block_hash); + const creationBlockNumber = integerText(row.creation_block_number); + const creationTransactionIndex = integerText( + row.creation_transaction_index, + ); + const required = exactArray(row.required_occurrences, 8).map((entry) => { + const occurrence = exactRecord(entry); + return Object.freeze({ + role: exactText( + occurrence.role, + /^(launcher|vault_factory|hook)$/u, + ), + occurrenceId: exactUuid(occurrence.occurrenceId), + transactionHash: canonicalBytes32(occurrence.transactionHash), + receiptLogOrdinal: integerText(occurrence.receiptLogOrdinal), + blockHash: canonicalBytes32(occurrence.blockHash), + contentFingerprint: canonicalBytes32( + occurrence.contentFingerprint, + ), + releaseBindingId: exactUuid(occurrence.releaseBindingId), + releaseBindingCommitment: canonicalBytes32( + occurrence.releaseBindingCommitment, + ), + }); + }); + if ( + beneficiaries.length < 1 || + beneficiaries.length !== sharesBps.length || + new Set(beneficiaries).size !== beneficiaries.length || + sharesBps.reduce((sum, share) => sum + BigInt(share), 0n) !== + 10_000n || + required.length !== 3 || + required.map(({ role }) => role).join(",") !== + "launcher,vault_factory,hook" + ) { + return projectorValidationFailure(); + } + const requiredReferences: OccurrenceFingerprintReference[] = required.map( + (occurrence) => ({ + transaction_hash: occurrence.transactionHash, + receipt_log_ordinal: occurrence.receiptLogOrdinal, + block_hash: occurrence.blockHash, + role: occurrence.role, + }), + ); + const allocationInput = { + chain_id: "1", + release_id: input.scope.releaseId, + model_id: input.scope.modelId, + vault, + factory_transaction_hash: factoryTransactionHash, + factory_receipt_log_ordinal: factoryReceiptLogOrdinal, + factory_block_hash: factoryBlockHash, + creation_block_number: creationBlockNumber, + creation_transaction_index: creationTransactionIndex, + ordered_beneficiaries: beneficiaries, + ordered_shares_bps: sharesBps, + allocation_hash: allocationHash, + configuration_hash: configurationHash, + active_configuration_hash: activeConfigurationHash, + artifact_creation_code_commitment: + artifactCreationCodeCommitment, + required_occurrences: requiredReferences, + }; + const allocationPreimage = canonicalFingerprintPreimageV1( + "allocation", + allocationInput, + ); + const allocationFingerprint = canonicalFingerprintV1( + "allocation", + allocationInput, + ); + const evidenceInput = { + allocation_fingerprint: allocationFingerprint, + recovery_method: "historical_getters", + evidence_version: "classic-v3-activation-v1", + top_level_destination: null, + method_selector: null, + transaction_input_hash: null, + constructor_arguments_commitment: constructorArgumentsCommitment, + local_init_code_hash: localInitCodeHash, + create2_salt: create2Salt, + local_create2_address: vault, + historical_enrichment_status: "matched", + getter_block_hash: factoryBlockHash, + getter_result_hash_a: activeConfigurationHash, + getter_result_hash_b: activeConfigurationHash, + predict_result_hash_a: predictResultHash, + predict_result_hash_b: predictResultHash, + predicted_vault_a: vault, + predicted_vault_b: vault, + selected_rpc_result_hash_a: configurationHash, + selected_rpc_result_hash_b: configurationHash, + selected_rpc_transaction_receipt_hash_a: null, + selected_rpc_transaction_receipt_hash_b: null, + extra_note: null, + required_occurrence_fingerprints: required.map( + ({ contentFingerprint }) => contentFingerprint, + ), + }; + const evidencePreimage = canonicalFingerprintPreimageV1( + "evidence", + evidenceInput, + ); + const evidenceFingerprint = canonicalFingerprintV1( + "evidence", + evidenceInput, + ); + const allocationFactId = deterministicUuid( + "dynamic-activation-allocation-fact", + activationId, + ); + const allocationEvidenceId = deterministicUuid( + "dynamic-activation-allocation-evidence", + activationId, + ); + const materialized = await input.transaction.query<{ + allocation_fact_id: unknown; + allocation_evidence_id: unknown; + }>( + "select * from programmable_private.materialize_dynamic_activation_seed_v1($1::uuid, $2::uuid, $3::uuid, $4::uuid, $5::uuid[], $6::text[], $7::bytea, $8::bytea, $9::bytea, $10::bytea, $11::timestamptz)", + [ + input.runId, + activationId, + allocationFactId, + allocationEvidenceId, + required.map(({ occurrenceId }) => occurrenceId), + required.map(({ role }) => role), + allocationPreimage, + hexToBytes(allocationFingerprint), + evidencePreimage, + hexToBytes(evidenceFingerprint), + input.verifiedAt, + ], + ); + if ( + materialized.length !== 1 || + exactUuid(materialized[0]!.allocation_fact_id) !== allocationFactId || + exactUuid(materialized[0]!.allocation_evidence_id) !== + allocationEvidenceId + ) { + return projectorValidationFailure(); + } + expectedPairs.push({ + factId: allocationFactId, + evidenceId: allocationEvidenceId, + }); + } + expectedPairs.sort((left, right) => left.factId.localeCompare(right.factId)); + const pairKeys = expectedPairs.map( + ({ factId, evidenceId }) => `${factId}:${evidenceId}`, + ); + if (new Set(pairKeys).size !== pairKeys.length) { + return projectorValidationFailure(); + } + return Object.freeze(expectedPairs.map((pair) => Object.freeze(pair))); +} + +function unixSecondsTimestamp(value: string): string { + const seconds = BigInt(integerText(value)); + if (seconds > 8_640_000_000_000n) return projectorValidationFailure(); + return exactTimestamp(new Date(Number(seconds) * 1_000)); +} + +function canonicalOccurrenceJson( + value: Readonly>, +): CanonicalJsonValue { + const normalize = (entry: unknown): CanonicalJsonValue => { + if ( + entry === null || + typeof entry === "string" || + typeof entry === "boolean" + ) { + return entry; + } + if (typeof entry === "number") { + if (!Number.isSafeInteger(entry)) return projectorValidationFailure(); + return entry; + } + if (Array.isArray(entry)) return entry.map(normalize); + if ( + typeof entry !== "object" || + Object.getPrototypeOf(entry) !== Object.prototype + ) { + return projectorValidationFailure(); + } + return Object.fromEntries( + Object.entries(entry as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, member]) => [key, normalize(member)]), + ); + }; + return normalize(value); +} + +function occurrencePair( + projection: VerifiedReleaseProjection, +): readonly Readonly<{ + occurrence: ProjectorOccurrenceFact; + fact: ProjectorEventFact; +}>[] { + if ( + projection.fold.occurrences.length !== projection.fold.facts.length || + projection.fold.occurrences.length !== + projection.plan.entries.filter(({ action }) => action === "project").length + ) { + return projectorValidationFailure(); + } + return Object.freeze( + projection.fold.occurrences.map((occurrence, index) => { + const fact = projection.fold.facts[index]!; + if (fact.sourceCandidateId !== occurrence.candidateId) { + return projectorValidationFailure(); + } + return Object.freeze({ occurrence, fact }); + }), + ); +} + +function parseDetailedPoolBaseline( + rows: readonly Record[], +): DetailedPoolBaseline | null { + if (rows.length === 0) return null; + if (rows.length !== 1) return projectorValidationFailure(); + const row = rows[0]!; + const nullableInteger = (value: unknown) => + value === null ? null : integerText(value); + return Object.freeze({ + poolProjectionId: exactUuid(row.pool_projection_id), + launchProjectionId: exactUuid(row.launch_projection_id), + token: databaseAddress(row.token), + creator: databaseAddress(row.creator), + rewardVault: + row.reward_vault === null ? null : databaseAddress(row.reward_vault), + currency0: databaseAddress(row.currency0), + currency1: databaseAddress(row.currency1), + poolKeyFee: integerText(row.pool_key_fee), + tickSpacing: integerText(row.tick_spacing), + hook: databaseAddress(row.hook), + poolFeeConfigurationId: + row.pool_fee_configuration_id === null + ? null + : exactUuid(row.pool_fee_configuration_id), + buySwapFeeBps: nullableInteger(row.buy_swap_fee_bps), + sellSwapFeeBps: nullableInteger(row.sell_swap_fee_bps), + buyCreatorFeeBps: nullableInteger(row.buy_creator_fee_bps), + sellCreatorFeeBps: nullableInteger(row.sell_creator_fee_bps), + launcherFeeBps: nullableInteger(row.launcher_fee_bps), + transferTaxBps: nullableInteger(row.transfer_tax_bps), + lpFeePips: nullableInteger(row.lp_fee_pips), + lastSourceOccurrenceId: exactUuid(row.last_source_occurrence_id), + }); +} + +function parseVerifiedRewardSeed( + rows: readonly Record[], + expectedVault: HexAddress, +): VerifiedRewardSeed | null { + if (rows.length === 0) return null; + if (rows.length !== 1) return projectorValidationFailure(); + const row = rows[0]!; + const beneficiaries = exactArray(row.ordered_beneficiaries, 5).map( + databaseAddress, + ); + const sharesBps = exactArray(row.ordered_shares_bps, 5).map(integerText); + if ( + beneficiaries.length < 1 || + beneficiaries.length !== sharesBps.length || + new Set(beneficiaries).size !== beneficiaries.length || + sharesBps.reduce((sum, share) => sum + BigInt(share), 0n) !== 10_000n || + databaseAddress(row.vault) !== expectedVault + ) { + return projectorValidationFailure(); + } + databaseBytes32(row.allocation_hash); + if (row.active_configuration_hash !== null) { + databaseBytes32(row.active_configuration_hash); + } + databaseBytes32(row.fact_content_fingerprint); + databaseBytes32(row.evidence_content_fingerprint); + exactText(row.evidence_version, /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/u); + exactText(row.recovery_method, /^[a-z][a-z0-9_/-]{0,95}$/u); + exactTimestamp(row.evidence_verified_at); + return Object.freeze({ + allocationFactId: exactUuid(row.allocation_fact_id), + allocationEvidenceId: exactUuid(row.allocation_evidence_id), + factoryOccurrenceId: exactUuid(row.factory_occurrence_id), + vault: expectedVault, + beneficiaries: Object.freeze(beneficiaries), + sharesBps: Object.freeze(sharesBps), + configurationHash: databaseBytes32(row.configuration_hash), + activeConfigurationHash: + row.active_configuration_hash === null + ? databaseBytes32(row.configuration_hash) + : databaseBytes32(row.active_configuration_hash), + }); +} + +function factScalar( + fact: ProjectorEventFact, + key: string, +): string { + const value = fact.values[key]; + if (typeof value !== "string") return projectorValidationFailure(); + return value; +} + +function factAddress(fact: ProjectorEventFact, key: string): HexAddress { + try { + return canonicalAddress(factScalar(fact, key)); + } catch { + return projectorValidationFailure(); + } +} + +function factBytes32(fact: ProjectorEventFact, key: string): HexBytes32 { + try { + return canonicalBytes32(factScalar(fact, key)); + } catch { + return projectorValidationFailure(); + } +} + +function rewardEventValues( + fact: ProjectorEventFact, +): Readonly> { + return Object.freeze( + Object.fromEntries( + Object.entries(fact.values).map(([key, value]) => { + if ( + typeof value === "string" || + (Array.isArray(value) && + value.every((entry) => typeof entry === "string")) + ) { + return [key, value] as const; + } + return projectorValidationFailure(); + }), + ), + ); +} + +function rewardModelForScope( + scope: ProjectorReleaseDatabaseScope, +): ProjectorRewardModel { + if (scope.releaseId === "classic-v3") return "classic-v3"; + if (scope.releaseId.startsWith("stock-paired-")) return "stock-paired"; + return projectorValidationFailure(); +} + +type ParsedRewardHeader = Readonly<{ + identity: string; + initialAllocationFactId: string; + initialAllocationEvidenceId: string; + poolId: HexBytes32; + configurationEpoch: string; + activeConfigurationHash: HexBytes32; + totalCreatorFeesReceived: string; +}>; + +function parseRewardHeader( + row: Record, + scope: ProjectorReleaseDatabaseScope, + expectedVault: HexAddress, +): ParsedRewardHeader { + const checkpointBlockHash = databaseBytes32(row.checkpoint_block_hash); + const baselineCommitment = databaseBytes32( + row.baseline_publication_commitment, + ); + const baselineBlockHash = databaseBytes32(row.baseline_promoted_block_hash); + const poolId = databaseBytes32(row.pool_id); + const activeConfigurationHash = databaseBytes32( + row.active_configuration_hash, + ); + const initialAllocationFactId = exactUuid(row.allocation_fact_id); + const initialAllocationEvidenceId = exactUuid(row.allocation_evidence_id); + const configurationEpoch = integerText(row.configuration_epoch); + const totalCreatorFeesReceived = integerText( + row.total_creator_fees_received, + ); + const identityValues = [ + integerText(row.chain_id), + exactText(row.release_id, /^[a-z0-9-]+$/u), + exactText(row.model_id, /^[a-z0-9-]+$/u), + exactText(row.source_group, /^[a-z0-9-]+$/u), + exactUuid(row.epoch_id), + integerText(row.pointer_generation), + exactUuid(row.checkpoint_id), + exactText(row.projector_version, /^[a-z0-9._-]+$/u), + integerText(row.checkpoint_generation), + integerText(row.reorg_generation), + integerText(row.checkpoint_block_number), + checkpointBlockHash, + exactUuid(row.reward_vault_projection_id), + initialAllocationFactId, + initialAllocationEvidenceId, + databaseAddress(row.vault), + poolId, + row.quote_asset === null ? null : databaseAddress(row.quote_asset), + databaseBytes32(row.configuration_hash), + activeConfigurationHash, + totalCreatorFeesReceived, + configurationEpoch, + exactUuid(row.baseline_projection_run_id), + baselineCommitment, + integerText(row.baseline_promoted_block_number), + baselineBlockHash, + exactUuid(row.vault_source_occurrence_id), + exactUuid(row.vault_source_logical_event_id), + databaseBytes32(row.vault_source_block_hash), + ]; + if ( + identityValues[0] !== "1" || + identityValues[1] !== scope.releaseId || + identityValues[2] !== scope.modelId || + identityValues[3] !== scope.sourceGroup || + identityValues[15] !== expectedVault + ) { + return projectorValidationFailure(); + } + return Object.freeze({ + identity: JSON.stringify(identityValues), + initialAllocationFactId, + initialAllocationEvidenceId, + poolId, + configurationEpoch, + activeConfigurationHash, + totalCreatorFeesReceived, + }); +} + +export function parseProjectorRewardStateRows(input: Readonly<{ + activeRows: readonly Record[]; + balanceRows: readonly Record[]; + scope: ProjectorReleaseDatabaseScope; + vault: HexAddress; +}>): ProjectorRewardState { + if ( + input.activeRows.length < 1 || + input.activeRows.length > 8 || + input.balanceRows.length < 1 || + input.balanceRows.length > 10_000 + ) { + return projectorValidationFailure(); + } + const header = parseRewardHeader( + input.activeRows[0]!, + input.scope, + input.vault, + ); + const model = rewardModelForScope(input.scope); + const activeBalances = new Map>(); + const allocations = input.activeRows.map((row, allocationIndex) => { + const candidateHeader = parseRewardHeader(row, input.scope, input.vault); + const parsedIndex = Number(integerText(row.allocation_index)); + const beneficiary = databaseAddress(row.beneficiary); + const payoutAddress = databaseAddress(row.payout_address); + const shareBps = integerText(row.share_bps); + const claimableAccrued = integerText(row.claimable_accrued); + const claimedTotal = integerText(row.claimed_total); + exactUuid(row.balance_projection_run_id); + databaseBytes32(row.balance_publication_commitment); + integerText(row.balance_promoted_block_number); + databaseBytes32(row.balance_promoted_block_hash); + exactUuid(row.allocation_source_occurrence_id); + exactUuid(row.allocation_source_logical_event_id); + databaseBytes32(row.allocation_source_block_hash); + exactUuid(row.balance_source_occurrence_id); + exactUuid(row.balance_source_logical_event_id); + databaseBytes32(row.balance_source_block_hash); + exactTimestamp(row.verified_at); + const existingActiveBalance = activeBalances.get(beneficiary); + if ( + candidateHeader.identity !== header.identity || + parsedIndex !== allocationIndex || + BigInt(shareBps) < 1n || + BigInt(shareBps) > 10_000n || + (model === "classic-v3" && beneficiary !== payoutAddress) || + (model === "stock-paired" && existingActiveBalance !== undefined) || + (existingActiveBalance !== undefined && + (existingActiveBalance.payoutAddress !== payoutAddress || + existingActiveBalance.claimableAccrued !== claimableAccrued || + existingActiveBalance.claimedTotal !== claimedTotal)) + ) { + return projectorValidationFailure(); + } + activeBalances.set(beneficiary, { + payoutAddress, + claimableAccrued, + claimedTotal, + }); + return Object.freeze({ + allocationIndex, + beneficiary, + payoutAddress, + shareBps, + }); + }); + const balances = input.balanceRows.map((row, balanceIndex) => { + const candidateHeader = parseRewardHeader(row, input.scope, input.vault); + const account = databaseAddress(row.account); + const payoutAddress = databaseAddress(row.payout_address); + exactUuid(row.account_reward_balance_id); + exactText(row.payout_source_kind, /^[a-z][a-z0-9_-]{0,95}$/u); + if (row.payout_configuration_epoch !== null) { + integerText(row.payout_configuration_epoch); + } + const claimableAccrued = integerText(row.claimable_accrued); + const claimedTotal = integerText(row.claimed_total); + exactUuid(row.balance_projection_run_id); + databaseBytes32(row.balance_publication_commitment); + integerText(row.balance_promoted_block_number); + databaseBytes32(row.balance_promoted_block_hash); + exactUuid(row.payout_projection_run_id); + databaseBytes32(row.payout_publication_commitment); + integerText(row.payout_promoted_block_number); + databaseBytes32(row.payout_promoted_block_hash); + exactUuid(row.payout_source_occurrence_id); + exactUuid(row.payout_source_logical_event_id); + databaseBytes32(row.payout_source_block_hash); + exactUuid(row.balance_source_occurrence_id); + exactUuid(row.balance_source_logical_event_id); + databaseBytes32(row.balance_source_block_hash); + exactTimestamp(row.verified_at); + const prior = balanceIndex === 0 + ? null + : databaseAddress(input.balanceRows[balanceIndex - 1]!.account); + if ( + candidateHeader.identity !== header.identity || + (prior !== null && prior.localeCompare(account) >= 0) || + (model === "classic-v3" && payoutAddress !== account) + ) { + return projectorValidationFailure(); + } + const active = activeBalances.get(account); + if ( + active && + (active.payoutAddress !== payoutAddress || + active.claimableAccrued !== claimableAccrued || + active.claimedTotal !== claimedTotal) + ) { + return projectorValidationFailure(); + } + return Object.freeze({ + account, + payoutAddress, + claimableAccrued, + claimedTotal, + }); + }); + if ( + [...activeBalances.keys()].some( + (account) => !balances.some((balance) => balance.account === account), + ) + ) { + return projectorValidationFailure(); + } + return Object.freeze({ + model, + initialAllocationFactId: header.initialAllocationFactId, + initialAllocationEvidenceId: header.initialAllocationEvidenceId, + baseline: Object.freeze({ + vault: input.vault, + poolId: header.poolId, + configurationEpoch: header.configurationEpoch, + activeConfigurationHash: header.activeConfigurationHash, + allocations: Object.freeze(allocations), + balances: Object.freeze(balances), + }), + }); +} + +async function stageCompletedLaunch(input: { + transaction: PostgresTransaction; + runId: string; + launch: ProjectorCompletedLaunch; + occurrenceWrites: ReadonlyMap; + targetBlockNumber: string; + targetBlockHash: HexBytes32; + verifiedAt: string; + scope: ProjectorReleaseDatabaseScope; + stagedPools: Map< + HexBytes32, + Readonly<{ + poolProjectionId: string; + launchProjectionId: string; + token: HexAddress; + creator: HexAddress; + rewardVault: HexAddress | null; + quoteAsset: HexAddress | null; + }> + >; + allocationPairs: Array<{ factId: string; evidenceId: string }>; + stagedRewardStates: Map; +}): Promise { + const occurrenceId = (candidateId: string) => { + const value = input.occurrenceWrites.get(candidateId)?.occurrenceId; + if (!value) return projectorValidationFailure(); + return value; + }; + const launchRole = input.launch.occurrenceRoles.find( + ({ sourceRole }) => sourceRole === "launcher", + ); + if (!launchRole || input.stagedPools.has(input.launch.poolId)) { + return projectorValidationFailure(); + } + const launchOccurrenceId = occurrenceId(launchRole.candidateId); + const launchProjectionId = deterministicUuid( + "launch-projection", + input.runId, + input.launch.token, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_launch_projection($1::uuid, $2::uuid, $3::bytea, $4::bytea, $5::bytea, $6::bytea, $7::bytea, $8::bytea, $9, $10, $11::numeric, $12::uuid, $13::numeric, $14::bytea, $15::timestamptz) as id", + [ + launchProjectionId, + input.runId, + hexToBytes(input.launch.token), + hexToBytes(input.launch.creator), + hexToBytes(input.launch.launchTransactionHash), + hexToBytes(input.launch.poolId), + input.launch.rewardVault === null + ? null + : hexToBytes(input.launch.rewardVault), + hexToBytes(input.launch.launchHash), + input.launch.tokenName, + input.launch.tokenSymbol, + input.launch.totalSupply, + launchOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + launchProjectionId, + ); + const poolProjectionId = deterministicUuid( + "pool-projection", + input.runId, + input.launch.poolId, + ); + const poolOccurrenceId = occurrenceId( + input.launch.pool.sourceCandidateId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_pool_projection($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::bytea, $6::numeric, $7::integer, $8::bytea, $9::uuid, $10::numeric, $11::bytea, $12::timestamptz) as id", + [ + poolProjectionId, + launchProjectionId, + input.runId, + hexToBytes(input.launch.pool.currency0), + hexToBytes(input.launch.pool.currency1), + input.launch.pool.poolKeyFee, + input.launch.pool.tickSpacing, + hexToBytes(input.launch.pool.hook), + poolOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + poolProjectionId, + ); + const feeConfigurationId = deterministicUuid( + "pool-fee-configuration", + input.runId, + input.launch.poolId, + ); + const feeOccurrenceId = occurrenceId( + input.launch.feeConfiguration.sourceCandidateId, + ); + if (input.scope.releaseId === "classic-v3") { + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_pool_fee_configuration_v2($1::uuid, $2::uuid, $3::uuid, $4::numeric, $5::numeric, $6::numeric, $7::numeric, $8::numeric, $9::numeric, $10::numeric, $11::numeric, $12::uuid, $13::numeric, $14::bytea, $15::timestamptz) as id", + [ + feeConfigurationId, + poolProjectionId, + input.runId, + input.launch.feeConfiguration.buySwapFeeBps, + input.launch.feeConfiguration.sellSwapFeeBps, + input.launch.feeConfiguration.buyCreatorFeeBps, + input.launch.feeConfiguration.sellCreatorFeeBps, + input.launch.feeConfiguration.launcherFeeBps, + input.launch.feeConfiguration.transferTaxBps, + input.launch.feeConfiguration.lpFeePips, + feeOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + feeConfigurationId, + ); + } else { + if ( + input.launch.feeConfiguration.buyCreatorFeeBps !== + input.launch.feeConfiguration.sellCreatorFeeBps + ) { + return projectorValidationFailure(); + } + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_pool_fee_configuration($1::uuid, $2::uuid, $3::uuid, $4::numeric, $5::numeric, $6::numeric, $7::numeric, $8::numeric, $9::numeric, $10::uuid, $11::numeric, $12::bytea, $13::timestamptz) as id", + [ + feeConfigurationId, + poolProjectionId, + input.runId, + input.launch.feeConfiguration.buySwapFeeBps, + input.launch.feeConfiguration.sellSwapFeeBps, + input.launch.feeConfiguration.buyCreatorFeeBps, + input.launch.feeConfiguration.launcherFeeBps, + input.launch.feeConfiguration.transferTaxBps, + input.launch.feeConfiguration.lpFeePips, + feeOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + feeConfigurationId, + ); + } + const liquidityOccurrenceId = occurrenceId( + input.launch.liquidity.sourceCandidateId, + ); + const liquidityFactId = deterministicUuid( + "launch-liquidity", + input.runId, + input.launch.token, + ); + const liquidityCommitment = keccak256( + toBytes( + JSON.stringify([ + input.launch.positionRecipient, + input.launch.positionTokenId, + input.launch.liquidity, + ]), + ), + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_launch_position_liquidity_v1($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::numeric, $6::numeric, $7::numeric, $8::numeric, $9::integer, $10::integer, $11::integer, $12::uuid, $13::bytea, $14::timestamptz) as id", + [ + liquidityFactId, + launchProjectionId, + input.runId, + hexToBytes(input.launch.positionRecipient), + input.launch.positionTokenId, + input.launch.liquidity.tokenLiquidityAmount, + input.launch.liquidity.lockedTokenDust, + input.launch.liquidity.initialSqrtPriceX96, + input.launch.liquidity.initialTick, + input.launch.liquidity.tickLower, + input.launch.liquidity.tickUpper, + liquidityOccurrenceId, + hexToBytes(liquidityCommitment), + input.verifiedAt, + ], + ), + liquidityFactId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_launch_projection_conditions($1::uuid, $2::boolean, $3::timestamptz) as id", + [launchProjectionId, input.launch.ethFunded, input.verifiedAt], + ), + launchProjectionId, + ); + for (const role of [...input.launch.occurrenceRoles].sort((left, right) => + left.sourceRole.localeCompare(right.sourceRole), + )) { + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_launch_occurrence_role($1::uuid, $2, $3::uuid, $4::timestamptz) as id", + [ + launchProjectionId, + role.sourceRole, + occurrenceId(role.candidateId), + input.verifiedAt, + ], + ), + launchProjectionId, + ); + } + + if (input.launch.custody) { + const custodyId = deterministicUuid( + "initial-buy-custody", + input.runId, + input.launch.token, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_initial_buy_custody_projection($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::smallint, $6::integer, $7::integer, $8::bytea, $9::uuid, $10::numeric, $11::bytea, $12::timestamptz) as id", + [ + custodyId, + launchProjectionId, + input.runId, + hexToBytes(input.launch.custody.address), + input.launch.custody.mode, + input.launch.custody.durationDays, + input.launch.custody.cliffDays, + hexToBytes(input.launch.custody.configurationHash), + occurrenceId(input.launch.custody.sourceCandidateId), + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + custodyId, + ); + if (input.launch.custody.vestingSourceCandidateId !== null) { + if ( + input.launch.custody.vestingStartTimestamp === null || + input.launch.custody.vestingEndTimestamp === null + ) { + return projectorValidationFailure(); + } + const vestingId = deterministicUuid( + "initial-buy-vesting", + input.runId, + input.launch.token, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_initial_buy_vesting_projection($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::bytea, $6::numeric, $7::timestamptz, $8::timestamptz, $9::uuid, $10::numeric, $11::bytea, $12::timestamptz) as id", + [ + vestingId, + custodyId, + input.runId, + hexToBytes(input.launch.creator), + hexToBytes(input.launch.token), + input.launch.initialBuy.tokenAmount, + unixSecondsTimestamp(input.launch.custody.vestingStartTimestamp), + unixSecondsTimestamp(input.launch.custody.vestingEndTimestamp), + occurrenceId(input.launch.custody.vestingSourceCandidateId), + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + vestingId, + ); + } + } + + const quoteAsset = + input.launch.model === "classic" + ? null + : input.launch.initialBuy.fundingAsset; + if (input.launch.rewardVault !== null) { + const seed = parseVerifiedRewardSeed( + await input.transaction.query( + "select * from programmable_private.get_projector_verified_reward_seed_v1($1::uuid, $2::bytea)", + [input.runId, hexToBytes(input.launch.rewardVault)], + ), + input.launch.rewardVault, + ); + if (!seed) { + throw new ProjectorDatabaseError({ + sqlState: "23514", + disposition: "abort-batch-invariant", + retryable: false, + }); + } + const factoryRole = input.launch.occurrenceRoles.find( + ({ sourceRole }) => sourceRole === "vault_factory", + ); + if ( + !factoryRole || + occurrenceId(factoryRole.candidateId) !== seed.factoryOccurrenceId + ) { + return projectorValidationFailure(); + } + const rewardVaultProjectionId = deterministicUuid( + "reward-vault-projection", + input.runId, + input.launch.rewardVault, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_reward_vault_projection($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::bytea, $6::bytea, $7::bytea, $8::uuid, $9::uuid, $10::numeric, $11::bytea, $12::timestamptz) as id", + [ + rewardVaultProjectionId, + launchProjectionId, + input.runId, + hexToBytes(input.launch.rewardVault), + hexToBytes(input.launch.poolId), + quoteAsset === null ? null : hexToBytes(quoteAsset), + hexToBytes(seed.configurationHash), + seed.allocationFactId, + seed.factoryOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + rewardVaultProjectionId, + ); + seed.beneficiaries.forEach((_beneficiary, index) => { + if (BigInt(seed.sharesBps[index]!) <= 0n) { + return projectorValidationFailure(); + } + }); + for (let index = 0; index < seed.beneficiaries.length; index += 1) { + const beneficiary = seed.beneficiaries[index]!; + const allocationId = deterministicUuid( + "reward-allocation-projection", + input.runId, + seed.allocationFactId, + String(index), + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_reward_allocation_projection($1::uuid, $2::uuid, $3::uuid, $4::uuid, $5::bigint, $6::integer, $7::bytea, $8::bytea, $9::numeric, $10::numeric, $11::numeric, $12::uuid, $13::numeric, $14::bytea, $15::timestamptz) as id", + [ + allocationId, + rewardVaultProjectionId, + input.runId, + seed.allocationFactId, + 1, + index, + hexToBytes(beneficiary), + hexToBytes(beneficiary), + seed.sharesBps[index]!, + input.targetBlockNumber, + null, + seed.factoryOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + allocationId, + ); + const balanceId = deterministicUuid( + "account-reward-balance", + input.runId, + input.launch.rewardVault, + beneficiary, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_account_reward_balance($1::uuid, $2::uuid, $3::bytea, $4::bytea, $5::numeric, $6::numeric, $7::uuid, $8::numeric, $9::bytea, $10::timestamptz) as id", + [ + balanceId, + input.runId, + hexToBytes(beneficiary), + hexToBytes(input.launch.rewardVault), + "0", + "0", + seed.factoryOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + balanceId, + ); + } + input.allocationPairs.push({ + factId: seed.allocationFactId, + evidenceId: seed.allocationEvidenceId, + }); + if (input.stagedRewardStates.has(input.launch.rewardVault)) { + return projectorValidationFailure(); + } + input.stagedRewardStates.set( + input.launch.rewardVault, + Object.freeze({ + model: rewardModelForScope(input.scope), + initialAllocationFactId: seed.allocationFactId, + initialAllocationEvidenceId: seed.allocationEvidenceId, + baseline: Object.freeze({ + vault: input.launch.rewardVault, + poolId: input.launch.poolId, + configurationEpoch: "1", + activeConfigurationHash: seed.activeConfigurationHash, + allocations: Object.freeze( + seed.beneficiaries.map((beneficiary, allocationIndex) => + Object.freeze({ + allocationIndex, + beneficiary, + payoutAddress: beneficiary, + shareBps: seed.sharesBps[allocationIndex]!, + }), + ), + ), + balances: Object.freeze( + seed.beneficiaries.map((beneficiary) => + Object.freeze({ + account: beneficiary, + payoutAddress: beneficiary, + claimableAccrued: "0", + claimedTotal: "0", + }), + ), + ), + }), + }), + ); + } + input.stagedPools.set( + input.launch.poolId, + Object.freeze({ + poolProjectionId, + launchProjectionId, + token: input.launch.token, + creator: input.launch.creator, + rewardVault: input.launch.rewardVault, + quoteAsset, + }), + ); +} + +async function stageIncrementalFacts(input: { + transaction: PostgresTransaction; + runId: string; + scope: ProjectorReleaseDatabaseScope; + occurrenceWrites: ReadonlyMap; + stagedPools: Map< + HexBytes32, + Readonly<{ + poolProjectionId: string; + launchProjectionId: string; + token: HexAddress; + creator: HexAddress; + rewardVault: HexAddress | null; + quoteAsset: HexAddress | null; + }> + >; + stagedRewardStates: ReadonlyMap; + allocationPairs: Array<{ factId: string; evidenceId: string }>; + targetBlockNumber: string; + targetBlockHash: HexBytes32; + verifiedAt: string; + verifiedRewardSnapshots: readonly ProjectorRewardSnapshot[]; +}): Promise[]; +}>> { + const handledByLaunch = new Set([ + "launch", + "liquidity", + "initial-buy", + "initial-buy-custody", + "pool-registration", + "fee-disclosure", + "reward-vault-deployment", + "vesting-wallet-deployment", + "eth-launch-coordinator", + ]); + const detailedPools = new Map(); + const poolState = async (poolId: HexBytes32) => { + const staged = input.stagedPools.get(poolId); + if (staged) return staged; + const cached = detailedPools.get(poolId); + if (cached) { + return Object.freeze({ + poolProjectionId: cached.poolProjectionId, + launchProjectionId: cached.launchProjectionId, + token: cached.token, + creator: cached.creator, + rewardVault: cached.rewardVault, + quoteAsset: + cached.token === cached.currency0 + ? cached.currency1 === ZERO_ADDRESS + ? null + : cached.currency1 + : cached.currency0 === ZERO_ADDRESS + ? null + : cached.currency0, + }); + } + const baseline = parseDetailedPoolBaseline( + await input.transaction.query( + "select * from programmable_private.get_projector_pool_baseline_by_id_v1($1::uuid, $2::bytea)", + [input.runId, hexToBytes(poolId)], + ), + ); + if (!baseline) return projectorValidationFailure(); + detailedPools.set(poolId, baseline); + return Object.freeze({ + poolProjectionId: baseline.poolProjectionId, + launchProjectionId: baseline.launchProjectionId, + token: baseline.token, + creator: baseline.creator, + rewardVault: baseline.rewardVault, + quoteAsset: + baseline.token === baseline.currency0 + ? baseline.currency1 === ZERO_ADDRESS + ? null + : baseline.currency1 + : baseline.currency0 === ZERO_ADDRESS + ? null + : baseline.currency0, + }); + }; + const feeDeltas = new Map< + string, + { + poolId: HexBytes32; + quoteAsset: HexAddress | null; + gross: bigint; + creator: bigint; + launcher: bigint; + lastOccurrenceId: string; + } + >(); + const rewardEvents = new Map(); + + for (const write of input.occurrenceWrites.values()) { + const { fact, occurrenceId } = write; + if (handledByLaunch.has(fact.kind)) continue; + if ( + fact.kind === "creator-fee-checkpoint" || + fact.kind === "beneficiary-claim" || + fact.kind === "payout-change" || + fact.kind === "reward-configuration-activation" + ) { + if (input.scope.releaseId === "classic-v2") { + return projectorValidationFailure(); + } + const vault = write.occurrence.sourceAddress; + const events = rewardEvents.get(vault) ?? []; + events.push( + Object.freeze({ + occurrenceId, + vault, + blockNumber: write.occurrence.blockNumber, + transactionIndex: String(write.occurrence.transactionIndex), + blockGlobalLogIndex: String( + write.occurrence.blockGlobalLogIndex, + ), + kind: fact.kind, + values: rewardEventValues(fact), + }), + ); + rewardEvents.set(vault, events); + } + if (fact.kind === "fee-accrual") { + const poolId = factBytes32(fact, "poolId"); + const pool = await poolState(poolId); + const quoteAsset = + typeof fact.values.quoteAsset === "string" + ? factAddress(fact, "quoteAsset") + : pool.quoteAsset; + if (quoteAsset !== pool.quoteAsset) return projectorValidationFailure(); + const gross = BigInt(factScalar(fact, "grossAmount")); + const creator = BigInt(factScalar(fact, "creatorFee")); + const launcher = BigInt(factScalar(fact, "launcherFee")); + const factId = deterministicUuid( + "fee-accrual-fact", + input.runId, + occurrenceId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_fee_accrual_fact($1::uuid, $2::uuid, $3::bytea, $4::bytea, $5::numeric, $6::numeric, $7::numeric, $8::uuid, $9::numeric, $10::bytea, $11::timestamptz) as id", + [ + factId, + input.runId, + hexToBytes(poolId), + quoteAsset === null ? null : hexToBytes(quoteAsset), + gross.toString(), + creator.toString(), + launcher.toString(), + occurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + factId, + ); + const key = `${poolId}:${quoteAsset ?? "native"}`; + const current = feeDeltas.get(key); + feeDeltas.set(key, { + poolId, + quoteAsset, + gross: (current?.gross ?? 0n) + gross, + creator: (current?.creator ?? 0n) + creator, + launcher: (current?.launcher ?? 0n) + launcher, + lastOccurrenceId: occurrenceId, + }); + continue; + } + if (fact.kind === "creator-hook-claim") { + const poolId = factBytes32(fact, "poolId"); + const pool = await poolState(poolId); + const claimId = deterministicUuid( + "creator-hook-claim", + input.runId, + occurrenceId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.append_creator_hook_claim_fact($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::bytea, $6::bytea, $7::bytea, $8::bytea, $9::bytea, $10::numeric, $11::timestamptz) as id", + [ + claimId, + input.runId, + occurrenceId, + hexToBytes(poolId), + typeof fact.values.rewardVault === "string" + ? hexToBytes(factAddress(fact, "rewardVault")) + : pool.rewardVault === null + ? null + : hexToBytes(pool.rewardVault), + typeof fact.values.creator === "string" + ? hexToBytes(factAddress(fact, "creator")) + : null, + typeof fact.values.recipient === "string" + ? hexToBytes(factAddress(fact, "recipient")) + : null, + pool.quoteAsset === null ? null : hexToBytes(pool.quoteAsset), + hexToBytes(factAddress(fact, "caller")), + factScalar(fact, "amount"), + input.verifiedAt, + ], + ), + claimId, + ); + continue; + } + if (fact.kind === "launcher-hook-claim") { + const claimId = deterministicUuid( + "launcher-hook-claim", + input.runId, + occurrenceId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.append_launcher_hook_claim_fact($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::bytea, $6::bytea, $7::bytea, $8::numeric, $9::timestamptz) as id", + [ + claimId, + input.runId, + occurrenceId, + hexToBytes(factAddress(fact, "treasury")), + hexToBytes(factAddress(fact, "recipient")), + typeof fact.values.quoteAsset === "string" + ? hexToBytes(factAddress(fact, "quoteAsset")) + : null, + hexToBytes(factAddress(fact, "caller")), + factScalar(fact, "amount"), + input.verifiedAt, + ], + ), + claimId, + ); + continue; + } + if (fact.kind === "creator-fee-checkpoint") { + const checkpointId = deterministicUuid( + "creator-fee-checkpoint", + input.runId, + occurrenceId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.append_creator_fee_checkpoint_fact($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::numeric, $6::numeric, $7::numeric, $8::timestamptz) as id", + [ + checkpointId, + input.runId, + occurrenceId, + hexToBytes(factBytes32(fact, "poolId")), + factScalar(fact, "configurationEpoch"), + factScalar(fact, "amount"), + factScalar(fact, "totalCreatorFeesReceived"), + input.verifiedAt, + ], + ), + checkpointId, + ); + continue; + } + if (fact.kind === "beneficiary-claim") { + const vault = write.occurrence.sourceAddress; + const beneficiary = factAddress(fact, "beneficiary"); + const recipient = + typeof fact.values.payoutAddress === "string" + ? factAddress(fact, "payoutAddress") + : beneficiary; + const claimId = deterministicUuid( + "beneficiary-claim", + input.runId, + occurrenceId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_claim_projection($1::uuid, $2::uuid, $3::bytea, $4, $5::bytea, $6::bytea, $7::numeric, $8::numeric, $9::numeric, $10::uuid, $11::numeric, $12::bytea, $13::timestamptz) as id", + [ + claimId, + input.runId, + hexToBytes(vault), + "beneficiary", + hexToBytes(beneficiary), + hexToBytes(recipient), + factScalar(fact, "amount"), + factScalar(fact, "beneficiaryTotalClaimed"), + factScalar(fact, "vaultTotalReceived"), + occurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + claimId, + ); + continue; + } + if (fact.kind === "payout-change") { + const vault = write.occurrence.sourceAddress; + const isClassic = input.scope.releaseId === "classic-v3"; + const previousPayoutAddress = factAddress( + fact, + isClassic ? "previousPayoutWallet" : "previousPayoutAddress", + ); + const newPayoutAddress = factAddress( + fact, + isClassic ? "newPayoutWallet" : "newPayoutAddress", + ); + const beneficiary = isClassic + ? previousPayoutAddress + : factAddress(fact, "beneficiary"); + const payoutChangeId = deterministicUuid( + "payout-change", + input.runId, + occurrenceId, + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_payout_change_projection($1::uuid, $2::uuid, $3::bytea, $4::bytea, $5::bytea, $6::bytea, $7::bigint, $8::uuid, $9::numeric, $10::bytea, $11::timestamptz) as id", + [ + payoutChangeId, + input.runId, + hexToBytes(vault), + hexToBytes(beneficiary), + hexToBytes(previousPayoutAddress), + hexToBytes(newPayoutAddress), + isClassic ? factScalar(fact, "configurationEpoch") : null, + occurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + payoutChangeId, + ); + continue; + } + if (fact.kind === "reward-configuration-activation") { + const activationId = deterministicUuid( + "reward-configuration-activation", + input.runId, + occurrenceId, + ); + const beneficiaries = fact.values.beneficiaries; + const shares = fact.values.sharesBps; + if (!Array.isArray(beneficiaries) || !Array.isArray(shares)) { + return projectorValidationFailure(); + } + exactIdResult( + await input.transaction.query( + "select programmable_private.append_reward_configuration_activation_fact($1::uuid, $2::uuid, $3::uuid, $4::bytea, $5::bytea, $6::numeric, $7::bytea, $8::bytea, $9::bytea[], $10::numeric[], $11::numeric, $12::timestamptz) as id", + [ + activationId, + input.runId, + occurrenceId, + hexToBytes(factBytes32(fact, "poolId")), + hexToBytes(factBytes32(fact, "approvalReference")), + factScalar(fact, "configurationEpoch"), + hexToBytes(factBytes32(fact, "previousConfigurationHash")), + hexToBytes(factBytes32(fact, "newConfigurationHash")), + beneficiaries.map((value) => { + if (typeof value !== "string") return projectorValidationFailure(); + return hexToBytes(canonicalAddress(value)); + }), + shares.map((value) => { + if (typeof value !== "string") return projectorValidationFailure(); + return integerText(value); + }), + factScalar(fact, "effectiveTotalCreatorFeesReceived"), + input.verifiedAt, + ], + ), + activationId, + ); + continue; + } + return projectorValidationFailure(); + } + + const verifiedSnapshots = new Map( + input.verifiedRewardSnapshots.map((snapshot) => [snapshot.vault, snapshot]), + ); + if (verifiedSnapshots.size !== input.verifiedRewardSnapshots.length) { + return projectorValidationFailure(); + } + const rewardDeltas: Array> = []; + for (const [vault, events] of rewardEvents) { + const occurrenceIds = orderedRewardOccurrenceIds(events, vault); + const eventByOccurrenceId = new Map( + events.map((event) => [event.occurrenceId, event]), + ); + const orderedEvents = occurrenceIds.map((occurrenceId) => { + const event = eventByOccurrenceId.get(occurrenceId); + if (!event) return projectorValidationFailure(); + return event; + }); + const staged = input.stagedRewardStates.get(vault); + const state = staged ?? parseProjectorRewardStateRows({ + activeRows: await input.transaction.query( + "select * from programmable_private.get_projector_reward_state_by_vault_v1($1::uuid, $2::bytea)", + [input.runId, hexToBytes(vault)], + ), + balanceRows: await input.transaction.query( + "select * from programmable_private.get_projector_reward_balances_by_vault_v1($1::uuid, $2::bytea)", + [input.runId, hexToBytes(vault)], + ), + scope: input.scope, + vault, + }); + if ( + state.model !== rewardModelForScope(input.scope) || + state.baseline.vault !== vault + ) { + return projectorValidationFailure(); + } + const snapshot = foldProjectorRewardState({ + model: state.model, + baseline: state.baseline, + events: orderedEvents, + }); + if ( + snapshot.activeConfigurationHash === null || + !verifiedSnapshots.has(vault) || + JSON.stringify(snapshot) !== JSON.stringify(verifiedSnapshots.get(vault)) + ) { + return projectorValidationFailure(); + } + const snapshotRows = await input.transaction.query<{ id: unknown }>( + "select programmable_private.stage_current_reward_snapshot_v2($1::uuid, $2::bytea, $3::bytea, $4::uuid, $5::bigint, $6::bytea, $7::numeric, $8::integer[], $9::bytea[], $10::bytea[], $11::numeric[], $12::bytea[], $13::bytea[], $14::numeric[], $15::numeric[], $16::uuid, $17::uuid[], $18::numeric, $19::bytea, $20::timestamptz) as id", + [ + input.runId, + hexToBytes(snapshot.vault), + hexToBytes(snapshot.poolId), + state.initialAllocationFactId, + snapshot.configurationEpoch, + hexToBytes(snapshot.activeConfigurationHash), + snapshot.totalCreatorFeesReceived, + snapshot.allocations.map(({ allocationIndex }) => allocationIndex), + snapshot.allocations.map(({ beneficiary }) => hexToBytes(beneficiary)), + snapshot.allocations.map(({ payoutAddress }) => hexToBytes(payoutAddress)), + snapshot.allocations.map(({ shareBps }) => shareBps), + snapshot.balances.map(({ account }) => hexToBytes(account)), + snapshot.balances.map(({ payoutAddress }) => hexToBytes(payoutAddress)), + snapshot.balances.map(({ claimableAccrued }) => claimableAccrued), + snapshot.balances.map(({ claimedTotal }) => claimedTotal), + snapshot.snapshotSourceOccurrenceId, + occurrenceIds, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ); + if (snapshotRows.length !== 1) return projectorValidationFailure(); + exactUuid(snapshotRows[0]!.id); + if ( + !input.allocationPairs.some( + ({ factId, evidenceId }) => + factId === state.initialAllocationFactId && + evidenceId === state.initialAllocationEvidenceId, + ) + ) { + input.allocationPairs.push({ + factId: state.initialAllocationFactId, + evidenceId: state.initialAllocationEvidenceId, + }); + } + rewardDeltas.push(Object.freeze({ + vault, + allocationFactId: state.initialAllocationFactId, + allocationEvidenceId: state.initialAllocationEvidenceId, + })); + } + + if (rewardEvents.size !== verifiedSnapshots.size) { + return projectorValidationFailure(); + } + + for (const delta of feeDeltas.values()) { + const baselineRows = await input.transaction.query( + "select * from programmable_private.get_projector_pool_fee_total_v1($1::uuid, $2::bytea, $3::bytea)", + [ + input.runId, + hexToBytes(delta.poolId), + delta.quoteAsset === null ? null : hexToBytes(delta.quoteAsset), + ], + ); + if (baselineRows.length > 1) return projectorValidationFailure(); + const baseline = baselineRows[0]; + const totalId = deterministicUuid( + "pool-fee-total", + input.runId, + delta.poolId, + delta.quoteAsset ?? "native", + ); + exactIdResult( + await input.transaction.query( + "select programmable_private.stage_pool_fee_total($1::uuid, $2::uuid, $3::bytea, $4::bytea, $5::numeric, $6::numeric, $7::numeric, $8::uuid, $9::numeric, $10::bytea, $11::timestamptz) as id", + [ + totalId, + input.runId, + hexToBytes(delta.poolId), + delta.quoteAsset === null ? null : hexToBytes(delta.quoteAsset), + ((baseline ? BigInt(integerText(baseline.gross_total)) : 0n) + + delta.gross).toString(), + ((baseline ? BigInt(integerText(baseline.creator_fee_total)) : 0n) + + delta.creator).toString(), + ((baseline ? BigInt(integerText(baseline.launcher_fee_total)) : 0n) + + delta.launcher).toString(), + delta.lastOccurrenceId, + input.targetBlockNumber, + hexToBytes(input.targetBlockHash), + input.verifiedAt, + ], + ), + totalId, + ); + } + return Object.freeze({ + rewardDeltas: Object.freeze( + rewardDeltas.sort((left, right) => left.vault.localeCompare(right.vault)), + ), + }); +} + +type RewardVerificationChunkManifest = Readonly<{ + chunkEndOffsets: readonly number[]; + providerAChunkCommitments: readonly HexBytes32[]; + providerBChunkCommitments: readonly HexBytes32[]; + providerAChunkCallCounts: readonly number[]; + providerBChunkCallCounts: readonly number[]; +}>; + +function orderedRewardOccurrenceIds( + events: readonly ProjectorRewardEvent[], + vault: HexAddress, +): readonly string[] { + const ordered = [...events].sort((left, right) => { + const blockOrder = BigInt(left.blockNumber) - BigInt(right.blockNumber); + if (blockOrder !== 0n) return blockOrder < 0n ? -1 : 1; + const transactionOrder = + BigInt(left.transactionIndex) - BigInt(right.transactionIndex); + if (transactionOrder !== 0n) return transactionOrder < 0n ? -1 : 1; + const logOrder = + BigInt(left.blockGlobalLogIndex) - BigInt(right.blockGlobalLogIndex); + if (logOrder !== 0n) return logOrder < 0n ? -1 : 1; + return left.occurrenceId.localeCompare(right.occurrenceId); + }); + const occurrenceIds = ordered.map((event) => { + if (event.vault !== vault) return projectorValidationFailure(); + return exactUuid(event.occurrenceId); + }); + if ( + occurrenceIds.length < 1 || + occurrenceIds.length > PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + new Set(occurrenceIds).size !== occurrenceIds.length + ) { + return projectorValidationFailure(); + } + return Object.freeze(occurrenceIds); +} + +function canonicalRewardVerificationChunkManifest(input: { + reward: DualRpcRewardSnapshot; + bindings: readonly [ + Omit, + Omit, + ]; +}): RewardVerificationChunkManifest { + const { reward, bindings } = input; + const matches = (actual: readonly unknown[], expected: readonly unknown[]) => + Array.isArray(actual) && + actual.length === expected.length && + actual.every((value, index) => value === expected[index]); + const expectedIdentities = bindings.map(({ identity }) => identity); + const expectedVendors = bindings.map(({ vendorGroup }) => vendorGroup); + const expectedEndpoints = bindings.map( + ({ endpointCommitment }) => endpointCommitment, + ); + const expectedOrigins = bindings.map( + ({ endpointOriginCommitment }) => endpointOriginCommitment, + ); + const verificationAccounts = reward.verificationAccounts; + const chunks = reward.chunks; + const maximumAccountsPerChunk = + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1.models[reward.model] + .maximumBalanceAccounts; + if ( + !Array.isArray(verificationAccounts) || + verificationAccounts.length < 1 || + verificationAccounts.length > + PROJECTOR_MAXIMUM_REWARD_VERIFICATION_ACCOUNTS || + verificationAccounts.some((account, index) => { + try { + return canonicalAddress(account) !== account || + (index > 0 && account <= verificationAccounts[index - 1]!); + } catch { + return true; + } + }) || + !Array.isArray(chunks) || + chunks.length < 1 || + chunks.length > PROJECTOR_MAXIMUM_REWARD_VERIFICATION_CHUNKS || + chunks.length !== + Math.ceil(verificationAccounts.length / maximumAccountsPerChunk) || + !matches(reward.providerIdentities, expectedIdentities) || + !matches(reward.providerVendorGroups, expectedVendors) || + !matches(reward.providerEndpointCommitments, expectedEndpoints) || + !matches(reward.providerOriginCommitments, expectedOrigins) || + !Array.isArray(reward.providerCallCounts) || + reward.providerCallCounts.length !== 2 || + !Array.isArray(reward.providerSnapshotCommitments) || + reward.providerSnapshotCommitments.length !== 2 || + reward.providerSnapshotCommitments[0] !== + reward.providerSnapshotCommitments[1] || + reward.providerSnapshotCommitments[0] === ZERO_BYTES32 + ) { + return projectorValidationFailure(); + } + try { + canonicalBytes32(reward.providerSnapshotCommitments[0]); + canonicalBytes32(reward.providerSnapshotCommitments[1]); + } catch { + return projectorValidationFailure(); + } + + const chunkEndOffsets: number[] = []; + const providerAChunkCommitments: HexBytes32[] = []; + const providerBChunkCommitments: HexBytes32[] = []; + const providerAChunkCallCounts: number[] = []; + const providerBChunkCallCounts: number[] = []; + let accountOffset = 0; + let aggregateCallCount = 0; + for (const [chunkIndex, chunk] of chunks.entries()) { + const remainingAccounts = verificationAccounts.length - accountOffset; + const expectedChunkSize = Math.min( + maximumAccountsPerChunk, + remainingAccounts, + ); + const expectedAccounts = verificationAccounts.slice( + accountOffset, + accountOffset + expectedChunkSize, + ); + const expectedCallCount = expectedRewardRpcCallCount( + reward.model, + reward.allocations.length, + expectedChunkSize, + ); + if ( + chunk.chunkIndex !== chunkIndex || + !Array.isArray(chunk.verificationAccounts) || + !matches(chunk.verificationAccounts, expectedAccounts) || + !Array.isArray(chunk.providerCallCounts) || + chunk.providerCallCounts.length !== 2 || + chunk.providerCallCounts[0] !== expectedCallCount || + chunk.providerCallCounts[1] !== expectedCallCount || + expectedCallCount < 1 || + expectedCallCount > PROJECTOR_MAXIMUM_REWARD_CALLS_PER_CHUNK || + !Array.isArray(chunk.providerSnapshotCommitments) || + chunk.providerSnapshotCommitments.length !== 2 || + chunk.providerSnapshotCommitments[0] !== + chunk.providerSnapshotCommitments[1] || + chunk.providerSnapshotCommitments[0] === ZERO_BYTES32 + ) { + return projectorValidationFailure(); + } + try { + canonicalBytes32(chunk.providerSnapshotCommitments[0]); + canonicalBytes32(chunk.providerSnapshotCommitments[1]); + } catch { + return projectorValidationFailure(); + } + accountOffset += expectedChunkSize; + aggregateCallCount += expectedCallCount; + chunkEndOffsets.push(accountOffset); + providerAChunkCommitments.push(chunk.providerSnapshotCommitments[0]); + providerBChunkCommitments.push(chunk.providerSnapshotCommitments[1]); + providerAChunkCallCounts.push(expectedCallCount); + providerBChunkCallCounts.push(expectedCallCount); + } + if ( + accountOffset !== verificationAccounts.length || + aggregateCallCount < 1 || + aggregateCallCount > PROJECTOR_MAXIMUM_REWARD_AGGREGATE_CALLS || + reward.providerCallCounts[0] !== aggregateCallCount || + reward.providerCallCounts[1] !== aggregateCallCount || + reward.rpcCallCount !== aggregateCallCount * 2 + ) { + return projectorValidationFailure(); + } + + const trace = reward.executionTrace; + if ( + !Number.isSafeInteger(trace.startedAtMs) || + trace.startedAtMs < 0 || + !Number.isSafeInteger(trace.completedAtMs) || + trace.completedAtMs < 0 || + !Number.isSafeInteger(trace.elapsedMs) || + trace.elapsedMs < 0 || + !Number.isSafeInteger(trace.hardDeadlineMs) || + trace.hardDeadlineMs < 1 || + trace.candidateBatchSize !== 0 || + trace.completedAtMs < trace.startedAtMs || + trace.elapsedMs !== trace.completedAtMs - trace.startedAtMs || + trace.elapsedMs > trace.hardDeadlineMs || + !Number.isSafeInteger(trace.maxCallsPerProvider) || + trace.maxCallsPerProvider < 1 || + trace.maxCallsPerProvider > PROJECTOR_MAXIMUM_REWARD_CALLS_PER_CHUNK || + providerAChunkCallCounts.some( + (count) => count > trace.maxCallsPerProvider, + ) || + !matches(trace.providerCallCounts, reward.providerCallCounts) || + !Array.isArray(trace.calls) || + trace.calls.length !== chunks.length * 2 || + trace.calls.some((call, callIndex) => { + const providerIndex = callIndex < chunks.length ? 0 : 1; + const binding = bindings[providerIndex]; + return !binding || + !Number.isSafeInteger(call.startedOffsetMs) || + call.startedOffsetMs < 0 || + call.startedOffsetMs > trace.elapsedMs || + !Number.isSafeInteger(call.durationMs) || + call.durationMs < 0 || + call.durationMs > trace.hardDeadlineMs || + call.operation !== "readRewardSnapshot" || + call.attempt !== 1 || + call.outcome !== "success" || + call.providerIdentity !== binding.identity || + call.providerVendorGroup !== binding.vendorGroup || + call.providerEndpointCommitment !== binding.endpointCommitment || + call.providerOriginCommitment !== binding.endpointOriginCommitment; + }) + ) { + return projectorValidationFailure(); + } + + return Object.freeze({ + chunkEndOffsets: Object.freeze(chunkEndOffsets), + providerAChunkCommitments: Object.freeze(providerAChunkCommitments), + providerBChunkCommitments: Object.freeze(providerBChunkCommitments), + providerAChunkCallCounts: Object.freeze(providerAChunkCallCounts), + providerBChunkCallCounts: Object.freeze(providerBChunkCallCounts), + }); +} + +function assertProjectionProviderEvidenceBindings(input: { + projection: VerifiedReleaseProjection; + bindings: readonly [ + Omit, + Omit, + ] | null; +}): void { + const bindings = input.bindings; + if (bindings === null) return projectorValidationFailure(); + const expectedIdentities = bindings.map(({ identity }) => identity); + const expectedVendors = bindings.map(({ vendorGroup }) => vendorGroup); + const expectedEndpoints = bindings.map( + ({ endpointCommitment }) => endpointCommitment, + ); + const expectedOrigins = bindings.map( + ({ endpointOriginCommitment }) => endpointOriginCommitment, + ); + const matches = (actual: readonly unknown[], expected: readonly unknown[]) => + actual.length === 2 && + actual.every((value, index) => value === expected[index]); + const evidence = input.projection.evidence; + const executionTrace = evidence.executionTrace; + if ( + !matches(evidence.providerIdentities, expectedIdentities) || + !matches(evidence.providerVendorGroups, expectedVendors) || + !matches(evidence.providerEndpointCommitments, expectedEndpoints) || + !matches(evidence.providerOriginCommitments, expectedOrigins) || + executionTrace.candidateBatchSize !== input.projection.plan.entries.length || + executionTrace.completedAtMs < executionTrace.startedAtMs || + executionTrace.elapsedMs !== + executionTrace.completedAtMs - executionTrace.startedAtMs || + executionTrace.elapsedMs > executionTrace.hardDeadlineMs || + executionTrace.providerCallCounts.some( + (count) => !Number.isSafeInteger(count) || count < 1 || count > 128, + ) + ) { + return projectorValidationFailure(); + } + const tracedCounts = bindings.map((binding) => + executionTrace.calls.filter((call) => { + if ( + call.providerIdentity !== binding.identity || + call.providerVendorGroup !== binding.vendorGroup || + call.providerEndpointCommitment !== binding.endpointCommitment || + call.providerOriginCommitment !== binding.endpointOriginCommitment + ) { + return false; + } + return true; + }).length + ); + if ( + !matches(tracedCounts, executionTrace.providerCallCounts) || + executionTrace.calls.some((call, callIndex) => { + const providerIndex = callIndex < executionTrace.providerCallCounts[0] + ? 0 + : 1; + const binding = bindings[providerIndex]; + return !binding || + call.providerIdentity !== binding.identity || + call.providerVendorGroup !== binding.vendorGroup || + call.providerEndpointCommitment !== binding.endpointCommitment || + call.providerOriginCommitment !== binding.endpointOriginCommitment; + }) || + evidence.candidates.some((candidate) => + !matches(candidate.providerIdentities, expectedIdentities) || + !matches(candidate.providerVendorGroups, expectedVendors) || + !matches(candidate.providerEndpointCommitments, expectedEndpoints) || + !matches(candidate.providerOriginCommitments, expectedOrigins) + ) + ) { + return projectorValidationFailure(); + } + const sortedRewardEvidence = [...(input.projection.rewardEvidence ?? [])].sort( + (left, right) => left.vault.localeCompare(right.vault), + ); + const sortedRewardSnapshots = [...(input.projection.rewardSnapshots ?? [])].sort( + (left, right) => left.vault.localeCompare(right.vault), + ); + if (sortedRewardEvidence.length !== sortedRewardSnapshots.length) { + return projectorValidationFailure(); + } + for (const [index, reward] of sortedRewardEvidence.entries()) { + const snapshot = sortedRewardSnapshots[index]; + if ( + !snapshot || + reward.vault !== snapshot.vault || + reward.poolId !== snapshot.poolId || + reward.configurationEpoch !== snapshot.configurationEpoch || + reward.configurationHash !== snapshot.activeConfigurationHash || + reward.totalCreatorFeesReceived !== + snapshot.totalCreatorFeesReceived || + JSON.stringify(reward.allocations) !== + JSON.stringify(snapshot.allocations) || + JSON.stringify(reward.balances) !== JSON.stringify(snapshot.balances) + ) { + return projectorValidationFailure(); + } + canonicalRewardVerificationChunkManifest({ reward, bindings }); + } +} + +async function commitPostgresVerifiedProjection(input: { + gateway: ReturnType; + providers: readonly ProjectorProviderDatabaseBinding[]; + rpcEvidenceBindings: readonly [ + Omit, + Omit, + ] | null; + scope: ProjectorReleaseDatabaseScope; + projectorVersion: string; + uuid: () => string; + now: () => Date; + privatePlan: ProjectionPrivatePlan; + projection: VerifiedReleaseProjection; + runtimeFence: ProjectorRuntimeFence; +}): Promise> { + const { projection, privatePlan } = input; + const timestamp = input.now(); + if ( + !Number.isFinite(timestamp.valueOf()) || + timestamp.toISOString() > projection.plan.lease.expiresAt || + projection.plan.scope.releaseId !== input.scope.releaseId || + projection.plan.scope.modelId !== input.scope.modelId || + projection.plan.scope.sourceGroup !== input.scope.sourceGroup + ) { + return projectorValidationFailure(); + } + const verifiedAt = timestamp.toISOString(); + assertProjectionProviderEvidenceBindings({ + projection, + bindings: input.rpcEvidenceBindings, + }); + const pairs = occurrencePair(projection); + const evidenceByCandidate = new Map( + projection.evidence.candidates.map((candidate) => [ + candidate.candidateId, + candidate, + ]), + ); + if ( + evidenceByCandidate.size !== projection.plan.entries.length || + projection.freshCandidates.length !== projection.plan.entries.length + ) { + return projectorValidationFailure(); + } + const lastEntry = projection.plan.entries.at(-1); + if (!lastEntry) return projectorValidationFailure(); + const targetBlockNumber = lastEntry.candidate.blockNumber; + const targetBlockHash = lastEntry.candidate.blockHash; + const targetCandidateId = lastEntry.candidate.candidateId; + const targetBlockGlobalLogIndex = + lastEntry.candidate.blockGlobalLogIndex; + const rpcProviderIds = input.providers + .map((provider, index) => ({ provider, index })) + .filter(({ provider }) => provider.type === "rpc_provider") + .map(({ index }) => privatePlan.providerDeploymentIds[index]!); + const envioProviderIds = input.providers + .map((provider, index) => ({ provider, index })) + .filter(({ provider }) => provider.type === "envio_deployment") + .map(({ index }) => privatePlan.providerDeploymentIds[index]!); + if (rpcProviderIds.length !== 2 || envioProviderIds.length !== 1) { + return projectorValidationFailure(); + } + const configuredProviderDeploymentIds = Object.freeze([ + envioProviderIds[0]!, + rpcProviderIds[0]!, + rpcProviderIds[1]!, + ]); + const rpcEvidenceBindings = input.rpcEvidenceBindings; + if (rpcEvidenceBindings === null) return projectorValidationFailure(); + + return input.gateway.transaction(async (transaction) => { + await assertRuntimeFence(transaction, input.runtimeFence); + const runtime = parseProjectionRuntimeState( + await transaction.query( + "select * from programmable_private.get_projector_runtime_state_v1($1, $2, $3, $4, $5, $6::text[], $7::text[], $8::bytea[], $9::bytea[])", + [ + "1", + input.scope.releaseId, + input.scope.modelId, + input.scope.sourceGroup, + input.projectorVersion, + input.providers.map(({ type }) => type), + input.providers.map(({ redactedIdentity }) => redactedIdentity), + input.providers.map(({ deploymentCommitment }) => + hexToBytes(deploymentCommitment), + ), + input.providers.map(({ schemaCommitment }) => + hexToBytes(schemaCommitment), + ), + ], + ), + input.providers, + ); + const expectedCheckpoint = projection.plan.checkpoint; + if ( + runtime.epochId !== privatePlan.epochId || + runtime.pointerGeneration !== privatePlan.pointerGeneration || + runtime.leaseGeneration !== projection.plan.lease.generation || + runtime.providerDeploymentIds.length !== + privatePlan.providerDeploymentIds.length || + runtime.providerDeploymentIds.some( + (id, index) => id !== privatePlan.providerDeploymentIds[index], + ) || + JSON.stringify(runtime.checkpoint) !== JSON.stringify(expectedCheckpoint) + ) { + throw new ProjectorDatabaseError({ + sqlState: "40001", + disposition: "retry-serialization", + retryable: true, + }); + } + + const proposedSafeObservationId = deterministicUuid( + "safe-head-observation", + privatePlan.runId, + projection.evidence.safeBlockNumber, + projection.evidence.safeBlockHash, + ); + const safeEvidence = providerEvidenceV2("safe_head", { + chain_id: "1", + epoch_id: privatePlan.epochId, + pointer_generation: privatePlan.pointerGeneration, + provider_a_id: rpcProviderIds[0]!, + provider_b_id: rpcProviderIds[1]!, + reported_chain_id_a: "1", + reported_chain_id_b: "1", + head_a: projection.evidence.providerHeads[0], + head_b: projection.evidence.providerHeads[1], + finality_depth: "12", + safe_block_number: projection.evidence.safeBlockNumber, + safe_block_hash_a: projection.evidence.safeBlockHash, + safe_block_hash_b: projection.evidence.safeBlockHash, + }); + const safeObservationId = await appendOrReuseSafeHeadObservation( + transaction, + [ + proposedSafeObservationId, + privatePlan.runId, + rpcProviderIds[0]!, + rpcProviderIds[1]!, + "1", + "1", + projection.evidence.providerHeads[0], + projection.evidence.providerHeads[1], + 12, + projection.evidence.safeBlockNumber, + hexToBytes(projection.evidence.safeBlockHash), + hexToBytes(projection.evidence.safeBlockHash), + safeEvidence.encodingVersion, + safeEvidence.canonicalPreimage, + hexToBytes(safeEvidence.contentFingerprint), + verifiedAt, + ], + ); + + const blockEvidenceIds = new Map(); + for (const evidence of projection.evidence.candidates) { + const key = `${evidence.candidateBlockNumber}:${evidence.candidateBlockHash}`; + if (blockEvidenceIds.has(key)) continue; + const blockEvidenceId = deterministicUuid( + "block-evidence", + privatePlan.runId, + evidence.candidateBlockNumber, + evidence.candidateBlockHash, + ); + const blockEvidence = providerEvidenceV2("block", { + chain_id: "1", + epoch_id: privatePlan.epochId, + pointer_generation: privatePlan.pointerGeneration, + observation_id: safeObservationId, + block_number: evidence.candidateBlockNumber, + provider_a_block_hash: evidence.candidateBlockHash, + provider_b_block_hash: evidence.candidateBlockHash, + }); + const storedBlockEvidenceId = await appendOrReuseBlockEvidence( + transaction, + [ + blockEvidenceId, + safeObservationId, + privatePlan.runId, + evidence.candidateBlockNumber, + hexToBytes(evidence.candidateBlockHash), + hexToBytes(evidence.candidateBlockHash), + blockEvidence.encodingVersion, + blockEvidence.canonicalPreimage, + hexToBytes(blockEvidence.contentFingerprint), + verifiedAt, + ], + ); + blockEvidenceIds.set(key, storedBlockEvidenceId); + } + const targetBlockEvidenceId = blockEvidenceIds.get( + `${targetBlockNumber}:${targetBlockHash}`, + ); + if (!targetBlockEvidenceId) return projectorValidationFailure(); + + const executionEvidenceId = deterministicUuid( + "projection-provider-execution-evidence", + privatePlan.runId, + ); + const executionTraceCommitment = projectionExecutionTraceCommitmentV1( + projection.evidence.executionTrace, + ); + const executionEvidence = providerEvidenceV3("projection_execution", { + chain_id: "1", + release_id: input.scope.releaseId, + model_id: input.scope.modelId, + source_group: input.scope.sourceGroup, + epoch_id: privatePlan.epochId, + pointer_generation: privatePlan.pointerGeneration, + run_id: privatePlan.runId, + provider_a_id: rpcProviderIds[0]!, + provider_b_id: rpcProviderIds[1]!, + provider_a_identity: rpcEvidenceBindings[0].identity, + provider_b_identity: rpcEvidenceBindings[1].identity, + provider_a_vendor_group: rpcEvidenceBindings[0].vendorGroup, + provider_b_vendor_group: rpcEvidenceBindings[1].vendorGroup, + provider_a_endpoint_commitment: + rpcEvidenceBindings[0].endpointCommitment, + provider_b_endpoint_commitment: + rpcEvidenceBindings[1].endpointCommitment, + provider_a_origin_commitment: + rpcEvidenceBindings[0].endpointOriginCommitment, + provider_b_origin_commitment: + rpcEvidenceBindings[1].endpointOriginCommitment, + provider_a_call_count: + projection.evidence.executionTrace.providerCallCounts[0], + provider_b_call_count: + projection.evidence.executionTrace.providerCallCounts[1], + candidate_batch_size: + projection.evidence.executionTrace.candidateBatchSize, + hard_deadline_ms: projection.evidence.executionTrace.hardDeadlineMs, + maximum_calls_per_provider: + projection.evidence.executionTrace.maxCallsPerProvider, + elapsed_ms: projection.evidence.executionTrace.elapsedMs, + execution_trace_commitment: executionTraceCommitment, + }); + exactIdResult( + await transaction.query( + "select programmable_private.append_projection_provider_execution_evidence_v1($1::uuid, $2::uuid, $3::uuid, $4::uuid[], $5::jsonb, $6::bytea, $7::smallint, $8::bytea, $9::bytea, $10::timestamptz) as id", + [ + executionEvidenceId, + privatePlan.runId, + safeObservationId, + configuredProviderDeploymentIds, + postgresJson(projection.evidence.executionTrace), + hexToBytes(executionTraceCommitment), + executionEvidence.encodingVersion, + executionEvidence.canonicalPreimage, + hexToBytes(executionEvidence.contentFingerprint), + verifiedAt, + ], + ), + executionEvidenceId, + ); + + const occurrenceWrites = new Map(); + const pairByCandidate = new Map( + pairs.map((pair) => [pair.occurrence.candidateId, pair]), + ); + for (const entry of projection.plan.entries) { + if (entry.action === "ignore") { + const decisionId = deterministicUuid( + "candidate-ignore", + privatePlan.runId, + entry.candidate.candidateId, + ); + const reasonCommitment = keccak256( + toBytes( + `programmable:ignore:v1\0${input.scope.releaseId}\0${entry.candidate.candidateId}\0outside-release-manifest`, + ), + ); + exactIdResult( + await transaction.query( + "select programmable_private.ignore_envio_candidate_v1($1::uuid, $2::uuid, $3, $4::bigint, $5, $6::bytea, $7::timestamptz) as id", + [ + decisionId, + privatePlan.runId, + entry.candidate.candidateId, + entry.attemptCount, + "outside-release-manifest", + hexToBytes(reasonCommitment), + verifiedAt, + ], + ), + decisionId, + ); + continue; + } + const pair = pairByCandidate.get(entry.candidate.candidateId); + const evidence = evidenceByCandidate.get(entry.candidate.candidateId); + const resolution = privatePlan.resolutions.get( + entry.candidate.candidateId, + ); + if (!pair || !evidence || !resolution) { + return projectorValidationFailure(); + } + const resolutionId = deterministicUuid( + "candidate-resolution", + privatePlan.runId, + entry.candidate.candidateId, + ); + const resolutionCommitment = keccak256( + toBytes( + JSON.stringify([ + input.scope.releaseId, + entry.candidate.candidateId, + resolution.releaseBindingId, + resolution.dynamicSourceAttestationId, + resolution.abiEventSetCommitment, + ]), + ), + ); + exactIdResult( + await transaction.query( + "select programmable_private.resolve_envio_candidate($1::uuid, $2::uuid, $3, $4::uuid, $5::uuid, $6::bytea, $7::bytea, $8::timestamptz) as id", + [ + resolutionId, + privatePlan.runId, + entry.candidate.candidateId, + resolution.releaseBindingId, + resolution.dynamicSourceAttestationId, + hexToBytes(resolution.abiEventSetCommitment), + hexToBytes(resolutionCommitment), + verifiedAt, + ], + ), + resolutionId, + ); + const logicalEventId = deterministicUuid( + "logical-event", + "1", + pair.occurrence.transactionHash, + pair.occurrence.receiptLogOrdinal, + ); + const occurrenceId = deterministicUuid( + "occurrence", + logicalEventId, + pair.occurrence.blockHash, + ); + const fingerprintInput = { + chain_id: "1", + transaction_hash: pair.occurrence.transactionHash, + receipt_log_ordinal: pair.occurrence.receiptLogOrdinal, + block_number: pair.occurrence.blockNumber, + block_hash: pair.occurrence.blockHash, + transaction_index: pair.occurrence.transactionIndex, + block_global_log_index: pair.occurrence.blockGlobalLogIndex, + source_address: pair.occurrence.sourceAddress, + event_signature: pair.occurrence.eventSignature, + ordered_topics: Array.from(pair.occurrence.orderedTopics), + raw_data: pair.occurrence.rawData, + decoded_payload: canonicalOccurrenceJson( + pair.occurrence.decodedPayload, + ), + payload_hash: pair.occurrence.payloadHash, + decoder_version: input.projectorVersion, + abi_event_set_commitment: resolution.abiEventSetCommitment, + release_id: input.scope.releaseId, + model_id: input.scope.modelId, + envio_candidate_id: pair.occurrence.candidateId, + provider_cursor: pair.occurrence.candidateId, + block_timestamp_unix: pair.occurrence.blockTimestamp, + }; + const canonicalPreimage = canonicalFingerprintPreimageV1( + "occurrence", + fingerprintInput, + ); + const fingerprint = canonicalFingerprintV1( + "occurrence", + fingerprintInput, + ); + const blockEvidenceId = blockEvidenceIds.get( + `${pair.occurrence.blockNumber}:${pair.occurrence.blockHash}`, + ); + if (!blockEvidenceId) return projectorValidationFailure(); + exactIdResult( + await transaction.query( + "select programmable_private.append_chain_event_occurrence($1::uuid, $2::uuid, $3::uuid, $4, $5::uuid, $6::numeric, $7::timestamptz, $8, $9::bytea, $10::uuid, $11::smallint, $12::bytea, $13::bytea, $14::timestamptz) as id", + [ + logicalEventId, + occurrenceId, + privatePlan.runId, + pair.occurrence.candidateId, + resolutionId, + pair.occurrence.receiptLogOrdinal, + unixSecondsTimestamp(pair.occurrence.blockTimestamp), + input.projectorVersion, + hexToBytes(resolution.abiEventSetCommitment), + blockEvidenceId, + 1, + canonicalPreimage, + hexToBytes(fingerprint), + verifiedAt, + ], + ), + occurrenceId, + ); + occurrenceWrites.set( + pair.occurrence.candidateId, + Object.freeze({ + occurrence: pair.occurrence, + fact: pair.fact, + occurrenceId, + logicalEventId, + resolutionId, + blockEvidenceId, + }), + ); + } + + const stagedPools = new Map< + HexBytes32, + Readonly<{ + poolProjectionId: string; + launchProjectionId: string; + token: HexAddress; + creator: HexAddress; + rewardVault: HexAddress | null; + quoteAsset: HexAddress | null; + }> + >(); + const allocationPairs: Array<{ + factId: string; + evidenceId: string; + }> = []; + const expectedActivationPairs = await materializeDynamicActivationSeeds({ + transaction, + runId: privatePlan.runId, + scope: input.scope, + targetBlockNumber, + targetBlockHash, + verifiedAt, + }); + const stagedRewardStates = new Map(); + for (const launch of projection.fold.launches) { + await stageCompletedLaunch({ + transaction, + runId: privatePlan.runId, + launch, + occurrenceWrites, + targetBlockNumber, + targetBlockHash, + verifiedAt, + scope: input.scope, + stagedPools, + allocationPairs, + stagedRewardStates, + }); + } + const launchPairKeys = allocationPairs.map( + ({ factId, evidenceId }) => `${factId}:${evidenceId}`, + ); + const expectedActivationPairKeys = expectedActivationPairs.map( + ({ factId, evidenceId }) => `${factId}:${evidenceId}`, + ); + const sortedLaunchPairKeys = [...launchPairKeys].sort(); + const sortedExpectedActivationPairKeys = [ + ...expectedActivationPairKeys, + ].sort(); + if ( + new Set(launchPairKeys).size !== launchPairKeys.length || + (input.scope.releaseId === "classic-v3" && + (launchPairKeys.length !== expectedActivationPairKeys.length || + sortedLaunchPairKeys.some( + (key, index) => key !== sortedExpectedActivationPairKeys[index], + ))) || + (input.scope.releaseId !== "classic-v3" && + expectedActivationPairKeys.length !== 0) + ) { + return projectorValidationFailure(); + } + + const chainOrderedOccurrenceIds = [...occurrenceWrites.values()] + .sort((left, right) => { + const blockOrder = BigInt(left.occurrence.blockNumber) - + BigInt(right.occurrence.blockNumber); + if (blockOrder !== 0n) return blockOrder < 0n ? -1 : 1; + const logOrder = BigInt(left.occurrence.blockGlobalLogIndex) - + BigInt(right.occurrence.blockGlobalLogIndex); + if (logOrder !== 0n) return logOrder < 0n ? -1 : 1; + return left.occurrenceId.localeCompare(right.occurrenceId); + }) + .map(({ occurrenceId }) => occurrenceId); + + const incrementalStage = await stageIncrementalFacts({ + transaction, + runId: privatePlan.runId, + scope: input.scope, + occurrenceWrites, + stagedPools, + stagedRewardStates, + allocationPairs, + targetBlockNumber, + targetBlockHash, + verifiedAt, + verifiedRewardSnapshots: projection.rewardSnapshots ?? [], + }); + + const rewardEvidenceByVault = new Map( + (projection.rewardEvidence ?? []).map((evidence) => [ + evidence.vault, + evidence, + ]), + ); + if ( + rewardEvidenceByVault.size !== (projection.rewardEvidence ?? []).length || + rewardEvidenceByVault.size !== incrementalStage.rewardDeltas.length + ) { + return projectorValidationFailure(); + } + const rewardProviderEvidence: Array> = []; + for (const rewardDelta of incrementalStage.rewardDeltas) { + const rewardEvidence = rewardEvidenceByVault.get(rewardDelta.vault); + if ( + !rewardEvidence || + rewardEvidence.blockNumber !== targetBlockNumber || + rewardEvidence.blockHash !== targetBlockHash || + rewardEvidence.model !== rewardModelForScope(input.scope) + ) { + return projectorValidationFailure(); + } + const foldedRows = await transaction.query<{ commitment: unknown }>( + "select programmable_private.get_staged_reward_folded_commitment_v1($1::uuid, $2::bytea) as commitment", + [privatePlan.runId, hexToBytes(rewardDelta.vault)], + ); + if (foldedRows.length !== 1) return projectorValidationFailure(); + const foldedSnapshotCommitment = databaseBytes32( + foldedRows[0]!.commitment, + ); + const rewardExecutionTraceCommitment = + projectionExecutionTraceCommitmentV1(rewardEvidence.executionTrace); + const rewardEvidenceId = deterministicUuid( + "reward-snapshot-provider-evidence", + privatePlan.runId, + rewardDelta.vault, + ); + const chunkManifest = canonicalRewardVerificationChunkManifest({ + reward: rewardEvidence, + bindings: input.rpcEvidenceBindings!, + }); + const encodedRewardEvidence = providerEvidenceV3("reward_snapshot", { + chain_id: "1", + release_id: input.scope.releaseId, + model_id: input.scope.modelId, + source_group: input.scope.sourceGroup, + epoch_id: privatePlan.epochId, + pointer_generation: privatePlan.pointerGeneration, + run_id: privatePlan.runId, + projection_execution_evidence_id: executionEvidenceId, + block_evidence_id: targetBlockEvidenceId, + vault: rewardEvidence.vault, + reward_model: rewardEvidence.model, + block_number: rewardEvidence.blockNumber, + block_hash: rewardEvidence.blockHash, + provider_a_id: rpcProviderIds[0]!, + provider_b_id: rpcProviderIds[1]!, + provider_a_snapshot_commitment: + rewardEvidence.providerSnapshotCommitments[0], + provider_b_snapshot_commitment: + rewardEvidence.providerSnapshotCommitments[1], + provider_a_call_count: rewardEvidence.providerCallCounts[0], + provider_b_call_count: rewardEvidence.providerCallCounts[1], + verification_accounts: rewardEvidence.verificationAccounts, + verification_account_chunk_end_offsets: + chunkManifest.chunkEndOffsets, + provider_a_verification_chunk_commitments: + chunkManifest.providerAChunkCommitments, + provider_b_verification_chunk_commitments: + chunkManifest.providerBChunkCommitments, + provider_a_verification_chunk_call_counts: + chunkManifest.providerAChunkCallCounts, + provider_b_verification_chunk_call_counts: + chunkManifest.providerBChunkCallCounts, + folded_snapshot_commitment: foldedSnapshotCommitment, + execution_trace_commitment: rewardExecutionTraceCommitment, + }); + exactIdResult( + await transaction.query( + "select programmable_private.append_reward_snapshot_provider_evidence_v1($1::uuid, $2::uuid, $3::uuid, $4::uuid, $5::bytea, $6, $7, $8::numeric, $9::bytea, $10::bytea, $11::bytea, $12::integer, $13::integer, $14::bytea[], $15::integer[], $16::bytea[], $17::bytea[], $18::integer[], $19::integer[], $20::bytea, $21::jsonb, $22::bytea, $23::smallint, $24::bytea, $25::bytea, $26::timestamptz) as id", + [ + rewardEvidenceId, + privatePlan.runId, + executionEvidenceId, + targetBlockEvidenceId, + hexToBytes(rewardEvidence.vault), + input.scope.modelId, + rewardEvidence.model, + rewardEvidence.blockNumber, + hexToBytes(rewardEvidence.blockHash), + hexToBytes(rewardEvidence.providerSnapshotCommitments[0]), + hexToBytes(rewardEvidence.providerSnapshotCommitments[1]), + rewardEvidence.providerCallCounts[0], + rewardEvidence.providerCallCounts[1], + rewardEvidence.verificationAccounts.map(hexToBytes), + chunkManifest.chunkEndOffsets, + chunkManifest.providerAChunkCommitments.map(hexToBytes), + chunkManifest.providerBChunkCommitments.map(hexToBytes), + chunkManifest.providerAChunkCallCounts, + chunkManifest.providerBChunkCallCounts, + hexToBytes(foldedSnapshotCommitment), + postgresJson(rewardEvidence.executionTrace), + hexToBytes(rewardExecutionTraceCommitment), + encodedRewardEvidence.encodingVersion, + encodedRewardEvidence.canonicalPreimage, + hexToBytes(encodedRewardEvidence.contentFingerprint), + verifiedAt, + ], + ), + rewardEvidenceId, + ); + rewardProviderEvidence.push(Object.freeze({ + evidenceId: rewardEvidenceId, + fingerprint: encodedRewardEvidence.contentFingerprint, + vault: rewardEvidence.vault, + })); + } + + const targetKey = [ + BigInt(targetBlockNumber), + BigInt(targetBlockGlobalLogIndex), + targetCandidateId, + ] as const; + const keyForDisposition = (row: Record) => [ + BigInt(integerText(row.block_number)), + BigInt(integerText(row.block_global_log_index)), + exactText( + row.candidate_id, + /^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(?:0|[1-9]\d*)$/u, + 192, + ), + ] as const; + const compareDispositionKey = ( + left: readonly [bigint, bigint, string], + right: readonly [bigint, bigint, string], + ) => left[0] < right[0] + ? -1 + : left[0] > right[0] + ? 1 + : left[1] < right[1] + ? -1 + : left[1] > right[1] + ? 1 + : left[2].localeCompare(right[2]); + const dispositionRows: Record[] = []; + let dispositionAfterBlock = expectedCheckpoint?.blockNumber ?? null; + let dispositionAfterLog = expectedCheckpoint?.blockGlobalLogIndex ?? null; + let dispositionAfterCandidate = expectedCheckpoint?.candidateId ?? null; + for ( + let pageIndex = 0; + pageIndex <= Math.ceil( + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP / 500, + ); + pageIndex += 1 + ) { + const page = await transaction.query( + "select * from programmable_private.list_projector_candidate_dispositions_v1($1, $2, $3, $4, $5::uuid, $6, $7, $8::bigint, $9::bytea, $10::numeric, $11::numeric, $12, $13, $14::timestamptz)", + [ + "1", + input.scope.releaseId, + input.scope.modelId, + input.scope.sourceGroup, + privatePlan.epochId, + privatePlan.pointerGeneration, + input.projectorVersion, + projection.plan.lease.generation, + hexToBytes(privatePlan.leaseTokenHash), + dispositionAfterBlock, + dispositionAfterLog, + dispositionAfterCandidate, + 500, + verifiedAt, + ], + ); + if (page.length === 0) break; + const previousKey = dispositionRows.length === 0 + ? null + : keyForDisposition(dispositionRows.at(-1)!); + const pageKeys = page.map(keyForDisposition); + if ( + pageKeys.some((key, index) => + (index > 0 && compareDispositionKey(pageKeys[index - 1]!, key) >= 0) || + (index === 0 && previousKey !== null && + compareDispositionKey(previousKey, key) >= 0) + ) + ) { + return projectorValidationFailure(); + } + dispositionRows.push(...page); + const pageLast = pageKeys.at(-1)!; + if (compareDispositionKey(pageLast, targetKey) >= 0) break; + if (page.length < 500) break; + dispositionAfterBlock = pageLast[0].toString(); + dispositionAfterLog = Number(pageLast[1]); + dispositionAfterCandidate = pageLast[2]; + } + if ( + dispositionRows.length > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + 500 + ) { + return projectorValidationFailure(); + } + const boundedDispositionRows = dispositionRows.filter((row) => + compareDispositionKey(keyForDisposition(row), targetKey) <= 0 + ); + const dispositionIds = boundedDispositionRows + .map((row) => exactUuid(row.decision_id)) + .sort(); + const dispositionCandidates = new Set( + boundedDispositionRows.map((row) => row.candidate_id), + ); + if ( + boundedDispositionRows.length !== projection.plan.entries.length || + dispositionCandidates.size !== projection.plan.entries.length || + projection.plan.entries.some( + ({ candidate }) => !dispositionCandidates.has(candidate.candidateId), + ) || + new Set(dispositionIds).size !== dispositionIds.length + ) { + return projectorValidationFailure(); + } + + const rewardDeltas = incrementalStage.rewardDeltas; + const promotionMode = "exact_incremental" as const; + const finalAllocationPairKeys = allocationPairs.map( + ({ factId, evidenceId }) => `${factId}:${evidenceId}`, + ); + if (rewardDeltas.length > 0) { + if ( + (projection.rewardSnapshots ?? []).length !== rewardDeltas.length || + (projection.rewardEvidence ?? []).length !== rewardDeltas.length || + rewardDeltas.some((rewardDelta) => + !allocationPairs.some((pair) => + pair.factId === rewardDelta.allocationFactId && + pair.evidenceId === rewardDelta.allocationEvidenceId + ) + ) + ) { + return projectorValidationFailure(); + } + } + if ( + new Set(finalAllocationPairKeys).size !== finalAllocationPairKeys.length + ) { + return projectorValidationFailure(); + } + const occurrenceIds = chainOrderedOccurrenceIds; + allocationPairs.sort((left, right) => + left.factId.localeCompare(right.factId), + ); + const checkpointGeneration = ( + BigInt(expectedCheckpoint?.generation ?? "0") + 1n + ).toString(); + const publicationId = deterministicUuid( + "projection-publication", + privatePlan.runId, + ); + const providerBindingId = deterministicUuid( + "projection-provider-binding", + privatePlan.runId, + ); + const orderedRewardProviderEvidence = [...rewardProviderEvidence] + .sort((left, right) => left.vault.localeCompare(right.vault)); + const rewardSnapshotEvidenceIds = orderedRewardProviderEvidence + .map(({ evidenceId }) => evidenceId); + const providerBindingCommitment = projectionProviderBindingCommitmentV1({ + publicationId, + runId: privatePlan.runId, + promotionMode, + executionEvidenceId, + executionFingerprint: executionEvidence.contentFingerprint, + rewardEvidence: orderedRewardProviderEvidence, + boundAt: verifiedAt, + }); + const resultCommitment = keccak256( + toBytes( + JSON.stringify([ + promotionMode, + input.scope.releaseId, + privatePlan.epochId, + privatePlan.pointerGeneration, + checkpointGeneration, + targetBlockNumber, + targetBlockHash, + targetCandidateId, + occurrenceIds, + dispositionIds, + allocationPairs, + { + evidenceId: executionEvidenceId, + fingerprint: executionEvidence.contentFingerprint, + }, + projection.rewardSnapshots ?? [], + projection.rewardEvidence ?? [], + rewardProviderEvidence, + { + providerBindingId, + providerBindingCommitment, + }, + PROJECTION_ROUTE_KEYS, + ]), + ), + ); + exactIdResult( + await transaction.query( + "select programmable_private.promote_projection_run_v3($1, $2::uuid, $3::uuid, $4::uuid, $5::uuid, $6, $7::bigint, $8::bytea, $9::bigint, $10::bigint, $11::bigint, $12::uuid, $13::uuid, $14::numeric, $15::bytea, $16::numeric, $17, $18::uuid[], $19::uuid[], $20::uuid[], $21::uuid[], $22::text[], $23::bytea, $24::uuid, $25::uuid[], $26::uuid, $27::bytea, $28::timestamptz) as id", + [ + promotionMode, + publicationId, + deterministicUuid("projection-checkpoint", privatePlan.runId), + deterministicUuid("projection-outcome", privatePlan.runId), + privatePlan.runId, + input.projectorVersion, + projection.plan.lease.generation, + hexToBytes(privatePlan.leaseTokenHash), + expectedCheckpoint?.generation ?? "0", + checkpointGeneration, + expectedCheckpoint?.reorgGeneration ?? "0", + safeObservationId, + targetBlockEvidenceId, + targetBlockNumber, + hexToBytes(targetBlockHash), + targetBlockGlobalLogIndex, + targetCandidateId, + occurrenceIds, + allocationPairs.map(({ factId }) => factId), + allocationPairs.map(({ evidenceId }) => evidenceId), + dispositionIds, + [...PROJECTION_ROUTE_KEYS], + hexToBytes(resultCommitment), + executionEvidenceId, + rewardSnapshotEvidenceIds, + providerBindingId, + hexToBytes(providerBindingCommitment), + verifiedAt, + ], + ), + publicationId, + ); + return Object.freeze({ checkpointGeneration }); + }); +} diff --git a/lib/data-pipeline/postgres-read-model.server.ts b/lib/data-pipeline/postgres-read-model.server.ts new file mode 100644 index 00000000..1399e5f4 --- /dev/null +++ b/lib/data-pipeline/postgres-read-model.server.ts @@ -0,0 +1,310 @@ +import "server-only"; + +import type { TokenChartRange } from "../onchain/chart"; +import type { ExploreSort } from "../onchain/types"; + +import type { PostgresTransaction } from "./postgres"; +import { + adaptIndexedChartV2, + adaptIndexedClassicV3ProfileV2, + adaptIndexedCreatorProfileV2, + adaptIndexedExploreListV2, + adaptIndexedLaunchLookupV2, + adaptIndexedStockPairedProfileV2, + adaptIndexedTokenDetailV2, + indexedRouteCacheHeaders, + type IndexedChartDataV2, + type IndexedClassicV3ProfileDataV2, + type IndexedCreatorProfileDataV2, + type IndexedExploreListDataV2, + type IndexedLaunchLookupDataV2, + type IndexedNotReadyReasonV2, + type IndexedRouteEnvelopeV2, + type IndexedRouteKeyV2, + type IndexedRowSourceV2, + type IndexedSnapshotIdentityV2, + type IndexedStockPairedProfileDataV2, + type IndexedTokenDetailDataV2, +} from "./route-adapters.server"; + +export type IndexedExploreReadRequest = { + chainId: 1; + query: string; + sort: ExploreSort; + page: number; + pageSize: number; +}; + +export type IndexedTokenReadRequest = { + chainId: 1; + address: string; +}; + +export type IndexedChartReadRequest = IndexedTokenReadRequest & { + range: TokenChartRange; +}; + +export type IndexedProfileReadRequest = { + chainId: 1; + account: string; +}; + +export type IndexedLaunchLookupRequest = IndexedProfileReadRequest & { + surface: "classic-v3" | "stock-paired"; + transactionHash: string; +}; + +/** + * Each implementation performs the complete route read through the supplied + * transaction: readiness, immutable pointers, payload, evidence commitments, + * totals and cursors. It must not acquire a client or start another + * transaction. The route coordinator owns the surrounding REPEATABLE READ, + * READ ONLY boundary. + */ +export type IndexedRouteSnapshotQueries = Readonly<{ + explore( + transaction: PostgresTransaction, + request: IndexedExploreReadRequest, + ): Promise>; + tokenDetail( + transaction: PostgresTransaction, + request: IndexedTokenReadRequest, + ): Promise>; + tokenChart( + transaction: PostgresTransaction, + request: IndexedChartReadRequest, + ): Promise>; + creatorProfile( + transaction: PostgresTransaction, + request: IndexedProfileReadRequest, + ): Promise>; + classicV3Profile( + transaction: PostgresTransaction, + request: IndexedProfileReadRequest, + ): Promise>; + stockPairedProfile( + transaction: PostgresTransaction, + request: IndexedProfileReadRequest, + ): Promise>; + launchLookup( + transaction: PostgresTransaction, + request: IndexedLaunchLookupRequest, + ): Promise>; +}>; + +export type IndexedRouteResponse = Readonly<{ + status: number; + body: T; + headers: Readonly>; +}>; + +export type AdaptedIndexedRouteSnapshotV2 = + | Readonly<{ + status: "not-ready"; + routeKey: IndexedRouteKeyV2; + reason: IndexedNotReadyReasonV2; + }> + | Readonly<{ + status: "ready"; + routeKey: IndexedRouteKeyV2; + snapshot: IndexedSnapshotIdentityV2; + /** + * Exact source rows used by the DTO adapter. The integration layer must + * derive `validatedRecordScopeEvidence` from this array and derive its + * coordinator `versions` from the matching snapshot pointers. + */ + recordSources: readonly IndexedRowSourceV2[]; + response: IndexedRouteResponse; + }>; + +function notReady( + routeKey: IndexedRouteKeyV2, + reason: IndexedNotReadyReasonV2, +): AdaptedIndexedRouteSnapshotV2 { + return Object.freeze({ status: "not-ready", routeKey, reason }); +} + +function ready(input: { + routeKey: IndexedRouteKeyV2; + snapshot: IndexedSnapshotIdentityV2; + recordSources: readonly IndexedRowSourceV2[]; + response: IndexedRouteResponse; +}): AdaptedIndexedRouteSnapshotV2 { + return Object.freeze({ + status: "ready", + routeKey: input.routeKey, + snapshot: input.snapshot, + recordSources: Object.freeze([...input.recordSources]), + response: Object.freeze(input.response), + }); +} + +/** + * Pure route adapters for use inside the coordinator's one transaction. + * These methods never own a connection or transaction. + */ +export function createPostgresPublicRouteSnapshotAdapters(input: { + queries: IndexedRouteSnapshotQueries; +}) { + return Object.freeze({ + async explore( + transaction: PostgresTransaction, + request: IndexedExploreReadRequest, + ) { + const envelope = await input.queries.explore(transaction, request); + if (envelope.status !== "ready") { + return notReady("explore-list", envelope.reason); + } + const body = adaptIndexedExploreListV2(envelope); + return ready({ + routeKey: "explore-list", + snapshot: envelope.snapshot, + recordSources: envelope.data.tokens.map((token) => token.source), + response: { + status: 200, + body, + headers: indexedRouteCacheHeaders("explore-list"), + }, + }); + }, + + async tokenDetail( + transaction: PostgresTransaction, + request: IndexedTokenReadRequest, + ) { + const envelope = await input.queries.tokenDetail(transaction, request); + if (envelope.status !== "ready") { + return notReady("explore-token", envelope.reason); + } + const body = adaptIndexedTokenDetailV2(envelope); + const found = body.token !== null; + return ready({ + routeKey: "explore-token", + snapshot: envelope.snapshot, + recordSources: envelope.data.token ? [envelope.data.token.source] : [], + response: { + status: found ? 200 : 404, + body, + headers: indexedRouteCacheHeaders( + "token-detail", + found ? "ready" : "not-found", + ), + }, + }); + }, + + async tokenChart( + transaction: PostgresTransaction, + request: IndexedChartReadRequest, + ) { + const envelope = await input.queries.tokenChart(transaction, request); + if (envelope.status !== "ready") { + return notReady("explore-chart", envelope.reason); + } + const body = adaptIndexedChartV2(envelope); + return ready({ + routeKey: "explore-chart", + snapshot: envelope.snapshot, + recordSources: [envelope.data.source], + response: { + status: 200, + body, + headers: indexedRouteCacheHeaders("token-chart"), + }, + }); + }, + + async creatorProfile( + transaction: PostgresTransaction, + request: IndexedProfileReadRequest, + ) { + const envelope = await input.queries.creatorProfile(transaction, request); + if (envelope.status !== "ready") { + return notReady("creator-profile", envelope.reason); + } + const body = adaptIndexedCreatorProfileV2(envelope); + return ready({ + routeKey: "creator-profile", + snapshot: envelope.snapshot, + recordSources: [ + ...envelope.data.tokens.map((token) => token.source), + ...envelope.data.claims.map((claim) => claim.source), + ], + response: { + status: 200, + body, + headers: indexedRouteCacheHeaders("creator-profile"), + }, + }); + }, + + async classicV3Profile( + transaction: PostgresTransaction, + request: IndexedProfileReadRequest, + ) { + const envelope = await input.queries.classicV3Profile( + transaction, + request, + ); + if (envelope.status !== "ready") { + return notReady("classic-v3-profile", envelope.reason); + } + const body = adaptIndexedClassicV3ProfileV2(envelope); + return ready({ + routeKey: "classic-v3-profile", + snapshot: envelope.snapshot, + recordSources: envelope.data.rewards.map((reward) => reward.source), + response: { + status: 200, + body, + headers: indexedRouteCacheHeaders("classic-v3-profile"), + }, + }); + }, + + async stockPairedProfile( + transaction: PostgresTransaction, + request: IndexedProfileReadRequest, + ) { + const envelope = await input.queries.stockPairedProfile( + transaction, + request, + ); + if (envelope.status !== "ready") { + return notReady("creator-profile", envelope.reason); + } + const body = adaptIndexedStockPairedProfileV2(envelope); + return ready({ + routeKey: "creator-profile", + snapshot: envelope.snapshot, + recordSources: envelope.data.rewards.map((reward) => reward.source), + response: { + status: 200, + body, + headers: indexedRouteCacheHeaders("stock-paired-profile"), + }, + }); + }, + + async launchLookup( + transaction: PostgresTransaction, + request: IndexedLaunchLookupRequest, + ) { + const envelope = await input.queries.launchLookup(transaction, request); + if (envelope.status !== "ready") { + return notReady("launch-lookup", envelope.reason); + } + const body = adaptIndexedLaunchLookupV2(envelope); + return ready({ + routeKey: "launch-lookup", + snapshot: envelope.snapshot, + recordSources: envelope.data.token ? [envelope.data.token.source] : [], + response: { + status: body.status === "pending" ? 202 : 200, + body, + headers: indexedRouteCacheHeaders("launch-lookup"), + }, + }); + }, + }); +} diff --git a/lib/data-pipeline/postgres-reconciler-route-corpus-store.ts b/lib/data-pipeline/postgres-reconciler-route-corpus-store.ts new file mode 100644 index 00000000..3c6a16e2 --- /dev/null +++ b/lib/data-pipeline/postgres-reconciler-route-corpus-store.ts @@ -0,0 +1,110 @@ +import "server-only"; + +import { hexToBytes, parseNonnegativeIntegerText } from "./codecs"; +import { assertReconcilerRouteSetForKeys } from "./classic-v3-reconciler-route-contract"; +import { validationError } from "./errors"; +import type { PostgresExecutor } from "./postgres"; +import { createReconcilerDatabaseGateway } from "./postgres-reconciler"; +import { + RECONCILER_ROUTE_KEYS, + type ReconcilerIndexedRouteStore, + type ReconcilerRouteDto, + type ReconcilerRouteKey, +} from "./reconciler-preparity"; + +const ROUTE_KEYS = new Set(RECONCILER_ROUTE_KEYS); + +function routeKey(value: unknown): ReconcilerRouteKey { + if (typeof value !== "string" || !ROUTE_KEYS.has(value)) { + throw validationError("postgres", "reconciler-corpus-route-key"); + } + return value as ReconcilerRouteKey; +} + +function comparedCount(value: unknown): number { + let parsed: bigint; + try { + parsed = BigInt(parseNonnegativeIntegerText(value, 19)); + } catch { + throw validationError("postgres", "reconciler-corpus-count"); + } + if (parsed < 1n || parsed > 1_000_000n) { + throw validationError("postgres", "reconciler-corpus-count"); + } + return Number(parsed); +} + +function json(value: unknown): ReconcilerRouteDto["dto"] { + let parsed = value; + if (typeof value === "string") { + try { + parsed = JSON.parse(value) as unknown; + } catch { + throw validationError("postgres", "reconciler-corpus-json"); + } + } + if (parsed === null || typeof parsed !== "object") { + throw validationError("postgres", "reconciler-corpus-json"); + } + return parsed as ReconcilerRouteDto["dto"]; +} + +export function createPostgresReconcilerRouteCorpusStore(input: { + executor: PostgresExecutor; +}): ReconcilerIndexedRouteStore { + const gateway = createReconcilerDatabaseGateway(input); + return Object.freeze({ + async readExactIndexedRouteCorpus({ + contract, + maximumEntityCount, + signal, + }) { + if (signal.aborted) { + throw validationError("postgres", "reconciler-corpus-aborted"); + } + return gateway.transaction(async (transaction) => { + const rows = await transaction.query<{ + route_key: unknown; + compared_count: unknown; + dto: unknown; + }>( + `select * + from programmable_private.get_reconciler_route_corpus_v1( + $1::bigint, $2::text, $3::text, $4::text, $5::uuid, + $6::bigint, $7::uuid, $8::numeric, $9::bytea, $10::integer + )`, + [ + contract.chainId, + contract.releaseId, + contract.modelId, + contract.sourceGroup, + contract.epochId, + contract.pointerGeneration, + contract.checkpointId, + contract.checkpointBlockNumber, + hexToBytes(contract.checkpointBlockHash), + maximumEntityCount, + ], + ); + if (signal.aborted || rows.length !== contract.routeKeys.length) { + throw validationError("postgres", "reconciler-corpus-cardinality"); + } + const parsed = rows.map((row) => Object.freeze({ + routeKey: routeKey(row.route_key), + comparedCount: comparedCount(row.compared_count), + dto: json(row.dto), + })); + if ( + parsed.some( + (row, index) => row.routeKey !== contract.routeKeys[index], + ) + ) { + throw validationError("postgres", "reconciler-corpus-order"); + } + return Object.freeze( + assertReconcilerRouteSetForKeys(parsed, contract.routeKeys), + ); + }); + }, + }); +} diff --git a/lib/data-pipeline/postgres-reconciler-store.ts b/lib/data-pipeline/postgres-reconciler-store.ts new file mode 100644 index 00000000..5fb65915 --- /dev/null +++ b/lib/data-pipeline/postgres-reconciler-store.ts @@ -0,0 +1,246 @@ +import "server-only"; + +import { + bytes32FromBytea, + canonicalBytes32, + hexToBytes, + parseNonnegativeIntegerText, + type HexBytes32, +} from "./codecs"; +import { validationError } from "./errors"; +import type { PostgresExecutor } from "./postgres"; +import { createReconcilerDatabaseGateway } from "./postgres-reconciler"; +import { + canonicalReconcilerCheckpointRequest, + canonicalReconcilerPreParityContract, + type ReconcilerCommitInput, + type ReconcilerCommitResult, + type ReconcilerPreParityStore, +} from "./reconciler-preparity"; + +function text(value: unknown, operation: string): string { + if (typeof value !== "string") { + throw validationError("postgres", operation); + } + return value; +} + +function integerText(value: unknown, operation: string): string { + if (typeof value === "bigint") { + if (value < 0n) throw validationError("postgres", operation); + return value.toString(); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) { + throw validationError("postgres", operation); + } + return String(value); + } + try { + return parseNonnegativeIntegerText(value, 19); + } catch { + throw validationError("postgres", operation); + } +} + +function bytes32(value: unknown, operation: string): HexBytes32 { + try { + if (typeof value === "string" && value.startsWith("0x")) { + return canonicalBytes32(value); + } + return bytes32FromBytea(value); + } catch { + throw validationError("postgres", operation); + } +} + +function json(value: unknown, operation: string): unknown { + if (typeof value !== "string") return value; + try { + return JSON.parse(value) as unknown; + } catch { + throw validationError("postgres", operation); + } +} + +function stringArray(value: unknown, operation: string): readonly string[] { + if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) { + throw validationError("postgres", operation); + } + return value; +} + +function commitResult( + value: unknown, + expectedRouteCount: number, +): ReconcilerCommitResult { + const parsed = json(value, "reconciler-commit-result"); + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw validationError("postgres", "reconciler-commit-result"); + } + const input = parsed as Record; + const routeCount = Number(integerText(input.routeCount, "route-count")); + const mismatchCount = Number( + integerText(input.mismatchCount, "mismatch-count"), + ); + if ( + routeCount !== expectedRouteCount || + mismatchCount < 0 || + mismatchCount > expectedRouteCount || + (input.status !== "succeeded" && input.status !== "failed") + ) { + throw validationError("postgres", "reconciler-commit-result"); + } + return Object.freeze({ + runId: text(input.runId, "run-id"), + reconciliationId: text(input.reconciliationId, "reconciliation-id"), + checkpointId: text(input.checkpointId, "checkpoint-id"), + checkpointBlockNumber: integerText( + input.checkpointBlockNumber, + "checkpoint-block-number", + ), + checkpointBlockHash: bytes32( + input.checkpointBlockHash, + "checkpoint-block-hash", + ), + routeCount, + mismatchCount, + status: input.status, + }); +} + +export function createPostgresReconcilerPreParityStore(input: { + executor: PostgresExecutor; +}): ReconcilerPreParityStore { + const gateway = createReconcilerDatabaseGateway(input); + return Object.freeze({ + async readExactContract(requestValue) { + const request = canonicalReconcilerCheckpointRequest(requestValue); + return gateway.transaction(async (transaction) => { + const rows = await transaction.query<{ + chain_id: unknown; + release_id: unknown; + model_id: unknown; + source_group: unknown; + projector_version: unknown; + epoch_id: unknown; + pointer_generation: unknown; + checkpoint_id: unknown; + checkpoint_generation: unknown; + reorg_generation: unknown; + checkpoint_block_number: unknown; + checkpoint_block_hash: unknown; + route_keys: unknown; + route_contract: unknown; + projection_contract: unknown; + current_entities: unknown; + }>( + `select * + from programmable_private.get_reconciler_preparity_contract_v1( + $1::bigint, $2::text, $3::text, $4::text, $5::uuid, + $6::bigint, $7::uuid, $8::numeric, $9::bytea, $10::integer + )`, + [ + request.chainId, + request.releaseId, + request.modelId, + request.sourceGroup, + request.epochId, + request.pointerGeneration, + request.checkpointId, + request.checkpointBlockNumber, + hexToBytes(request.checkpointBlockHash), + request.maximumEntityCount, + ], + ); + if (rows.length !== 1) { + throw validationError("postgres", "reconciler-contract-cardinality"); + } + const row = rows[0]!; + return canonicalReconcilerPreParityContract({ + chainId: integerText(row.chain_id, "chain-id"), + releaseId: text(row.release_id, "release-id"), + modelId: text(row.model_id, "model-id"), + sourceGroup: text(row.source_group, "source-group"), + projectorVersion: text(row.projector_version, "projector-version"), + epochId: text(row.epoch_id, "epoch-id"), + pointerGeneration: integerText( + row.pointer_generation, + "pointer-generation", + ), + checkpointId: text(row.checkpoint_id, "checkpoint-id"), + checkpointGeneration: integerText( + row.checkpoint_generation, + "checkpoint-generation", + ), + reorgGeneration: integerText( + row.reorg_generation, + "reorg-generation", + ), + checkpointBlockNumber: integerText( + row.checkpoint_block_number, + "checkpoint-block-number", + ), + checkpointBlockHash: bytes32( + row.checkpoint_block_hash, + "checkpoint-block-hash", + ), + routeKeys: stringArray(row.route_keys, "route-keys"), + routeContract: json(row.route_contract, "route-contract"), + projectionContract: json( + row.projection_contract, + "projection-contract", + ), + currentEntities: json(row.current_entities, "current-entities"), + }); + }); + }, + + async commitResult(result: ReconcilerCommitInput) { + return gateway.transaction(async (transaction) => { + const rows = await transaction.query<{ result: unknown }>( + `select programmable_private.commit_reconciler_preparity_result_v1( + $1::uuid, $2::uuid, $3::uuid[], $4::uuid[], $5::uuid, + $6::bigint, $7::text, $8::text, $9::text, $10::uuid, + $11::bigint, $12::uuid, $13::numeric, $14::bytea, $15::text, + $16::text[], $17::bytea[], $18::bytea[], $19::bytea[], + $20::bytea[], $21::bytea, $22::bytea, $23::bytea, + $24::timestamptz, $25::timestamptz, $26::timestamptz + ) as result`, + [ + result.runId, + result.reconciliationId, + result.parityRecordIds, + result.parityBindingIds, + result.outcomeId, + result.contract.chainId, + result.contract.releaseId, + result.contract.modelId, + result.contract.sourceGroup, + result.contract.epochId, + result.contract.pointerGeneration, + result.contract.checkpointId, + result.contract.checkpointBlockNumber, + hexToBytes(result.contract.checkpointBlockHash), + result.workerVersion, + result.routeKeys, + result.legacyDtoHashes.map(hexToBytes), + result.indexedDtoHashes.map(hexToBytes), + result.routeEvidenceCommitments.map(hexToBytes), + result.parityBindingCommitments.map(hexToBytes), + hexToBytes(result.requestCommitment), + hexToBytes(result.reconciliationEvidenceCommitment), + hexToBytes(result.resultCommitment), + result.startedAt, + result.comparedAt, + result.finishedAt, + ], + ); + if (rows.length !== 1) { + throw validationError("postgres", "reconciler-commit-cardinality"); + } + return commitResult(rows[0]!.result, result.routeKeys.length); + }); + }, + }); +} diff --git a/lib/data-pipeline/postgres-reconciler.ts b/lib/data-pipeline/postgres-reconciler.ts new file mode 100644 index 00000000..880055ec --- /dev/null +++ b/lib/data-pipeline/postgres-reconciler.ts @@ -0,0 +1,202 @@ +import "server-only"; + +import { DataPipelineError } from "./errors"; +import type { + PostgresExecutor, + PostgresTransaction, +} from "./postgres"; + +const RECONCILER_LOGIN_ROLE = "programmable_reconciler_login"; +const RECONCILER_CAPABILITY_ROLE = "programmable_reconciler"; + +export type ReconcilerSqlDisposition = + | "retry-serialization" + | "retry-transient" + | "fatal-gateway-membership" + | "fatal-codec-or-caller" + | "immutable-replay-conflict" + | "stale-checkpoint" + | "fatal-integrity" + | "fatal-unknown"; + +export type ReconcilerSqlClassification = Readonly<{ + sqlState: string | null; + disposition: ReconcilerSqlDisposition; + retryable: boolean; +}>; + +function canonicalSqlState(value: unknown): string | null { + return typeof value === "string" && /^[0-9A-Z]{5}$/u.test(value) + ? value + : null; +} + +export function classifyReconcilerSqlState( + sqlStateInput: unknown, +): ReconcilerSqlClassification { + const sqlState = canonicalSqlState(sqlStateInput); + if (sqlState === "40001" || sqlState === "40P01") { + return Object.freeze({ + sqlState, + disposition: "retry-serialization", + retryable: true, + }); + } + if ( + sqlState === "55P03" || + sqlState === "57014" || + sqlState === "57P01" || + sqlState?.startsWith("08") + ) { + return Object.freeze({ + sqlState, + disposition: "retry-transient", + retryable: true, + }); + } + if (sqlState === "42501") { + return Object.freeze({ + sqlState, + disposition: "fatal-gateway-membership", + retryable: false, + }); + } + if (sqlState === "22023" || sqlState === "22P02" || sqlState === "22003") { + return Object.freeze({ + sqlState, + disposition: "fatal-codec-or-caller", + retryable: false, + }); + } + if (sqlState === "23505") { + return Object.freeze({ + sqlState, + disposition: "immutable-replay-conflict", + retryable: false, + }); + } + if (sqlState === "55000") { + return Object.freeze({ + sqlState, + disposition: "stale-checkpoint", + retryable: true, + }); + } + if (sqlState?.startsWith("23")) { + return Object.freeze({ + sqlState, + disposition: "fatal-integrity", + retryable: false, + }); + } + return Object.freeze({ + sqlState, + disposition: "fatal-unknown", + retryable: false, + }); +} + +function sqlStateFromUnknown(error: unknown): unknown { + return error !== null && typeof error === "object" + ? Reflect.get(error, "code") + : null; +} + +export class ReconcilerDatabaseError extends Error { + readonly sqlState: string | null; + readonly disposition: ReconcilerSqlDisposition; + readonly retryable: boolean; + + constructor(classification: ReconcilerSqlClassification) { + super("Reconciler database operation failed"); + this.name = "ReconcilerDatabaseError"; + this.sqlState = classification.sqlState; + this.disposition = classification.disposition; + this.retryable = classification.retryable; + } + + static fromUnknown(error: unknown): ReconcilerDatabaseError { + if (error instanceof ReconcilerDatabaseError) return error; + return new ReconcilerDatabaseError( + classifyReconcilerSqlState(sqlStateFromUnknown(error)), + ); + } + + toJSON() { + return { + name: this.name, + sqlState: this.sqlState, + disposition: this.disposition, + retryable: this.retryable, + }; + } +} + +function gatewayIdentityFailure(): ReconcilerDatabaseError { + return new ReconcilerDatabaseError( + Object.freeze({ + sqlState: null, + disposition: "fatal-gateway-membership", + retryable: false, + }), + ); +} + +async function assertGatewayLogin( + transaction: PostgresTransaction, +): Promise { + const rows = await transaction.query<{ session_user: unknown }>( + "select session_user::text as session_user", + ); + if ( + rows.length !== 1 || + rows[0]?.session_user !== RECONCILER_LOGIN_ROLE + ) { + throw gatewayIdentityFailure(); + } +} + +async function assumeAndVerifyCapabilityRole( + transaction: PostgresTransaction, +): Promise { + await transaction.query("set local role programmable_reconciler"); + await transaction.query("set local statement_timeout = '3000ms'"); + await transaction.query("set local lock_timeout = '500ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '5000ms'", + ); + const rows = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + if ( + rows.length !== 1 || + rows[0]?.session_user !== RECONCILER_LOGIN_ROLE || + rows[0]?.current_role !== RECONCILER_CAPABILITY_ROLE + ) { + throw gatewayIdentityFailure(); + } +} + +export function createReconcilerDatabaseGateway(input: { + executor: PostgresExecutor; +}) { + return Object.freeze({ + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + try { + return await input.executor.transaction(async (transaction) => { + await assertGatewayLogin(transaction); + await assumeAndVerifyCapabilityRole(transaction); + return work(transaction); + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw ReconcilerDatabaseError.fromUnknown(error); + } + }, + }); +} diff --git a/lib/data-pipeline/postgres.ts b/lib/data-pipeline/postgres.ts new file mode 100644 index 00000000..067cfe4c --- /dev/null +++ b/lib/data-pipeline/postgres.ts @@ -0,0 +1,1351 @@ +import "server-only"; + +import postgres, { type Options, type Sql } from "postgres"; + +import { CircuitBreaker } from "./circuit"; +import { + addressFromBytea, + bytes32FromBytea, + canonicalAddress, + canonicalBytes32, + hexToBytes, + parseNonnegativeIntegerText, + parseUint256Text, + type HexAddress, + type HexBytes32, +} from "./codecs"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import { + validatedPostgresConnectionTarget, + validatedPostgresSslCa, +} from "./postgres-connection.server"; + +export type PostgresParameter = + | null + | boolean + | number + | string + | Date + | Uint8Array + | PostgresJsonParameter + | readonly PostgresParameter[]; + +export type PostgresJsonValue = + | null + | boolean + | number + | string + | readonly PostgresJsonValue[] + | Readonly<{ [key: string]: PostgresJsonValue }>; + +export type PostgresJsonParameter = Readonly<{ + kind: "programmable-postgres-json-v1"; + value: PostgresJsonValue; +}>; + +function isPostgresJsonValue( + value: unknown, + ancestors: Set = new Set(), +): value is PostgresJsonValue { + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + return true; + } + if (typeof value === "number") return Number.isFinite(value); + if (typeof value !== "object" || ancestors.has(value)) return false; + ancestors.add(value); + const valid = Array.isArray(value) + ? value.every((item) => isPostgresJsonValue(item, ancestors)) + : (Object.getPrototypeOf(value) === Object.prototype || + Object.getPrototypeOf(value) === null) && + Object.values(value).every((item) => + isPostgresJsonValue(item, ancestors) + ); + ancestors.delete(value); + return valid; +} + +export function postgresJson(value: unknown): PostgresJsonParameter { + try { + if (!isPostgresJsonValue(value) || JSON.stringify(value) === undefined) { + throw new TypeError("JSON value is not serializable"); + } + } catch { + throw invalidInput("postgres", "json-parameter"); + } + return Object.freeze({ kind: "programmable-postgres-json-v1", value }); +} + +function isPostgresJsonParameter( + value: PostgresParameter, +): value is PostgresJsonParameter { + return ( + value !== null && + typeof value === "object" && + !(value instanceof Date) && + !(value instanceof Uint8Array) && + Reflect.get(value, "kind") === "programmable-postgres-json-v1" + ); +} + +export type PostgresTransaction = { + query>( + text: string, + values?: readonly PostgresParameter[], + ): Promise; +}; + +export type PostgresExecutor = { + transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise; + close(): Promise; +}; + +type DriverSettings = { + maxConnections: number; + connectTimeoutMs: number; + idleTimeoutMs: number; +}; + +type NoCustomPostgresTypes = Record; + +export function postgresDriverOptions( + settings: DriverSettings, +): Pick< + Options, + | "prepare" + | "max" + | "connect_timeout" + | "idle_timeout" + | "fetch_types" + | "max_lifetime" + | "onnotice" + | "connection" +> { + if ( + !Number.isSafeInteger(settings.maxConnections) || + settings.maxConnections < 1 || + settings.maxConnections > 5 || + !Number.isSafeInteger(settings.connectTimeoutMs) || + settings.connectTimeoutMs < 100 || + settings.connectTimeoutMs > 5_000 || + !Number.isSafeInteger(settings.idleTimeoutMs) || + settings.idleTimeoutMs < 1_000 || + settings.idleTimeoutMs > 60_000 + ) { + throw invalidInput("postgres", "driver-settings"); + } + return { + prepare: false, + max: settings.maxConnections, + connect_timeout: Math.max(1, Math.ceil(settings.connectTimeoutMs / 1_000)), + idle_timeout: Math.max(1, Math.ceil(settings.idleTimeoutMs / 1_000)), + // postgres.js needs the server's element-to-array OID map to serialize + // typed parameters such as bytea[], uuid[] and numeric[] correctly. + fetch_types: true, + max_lifetime: 300, + onnotice: () => undefined, + connection: { + application_name: "programmable-read-model", + }, + }; +} + +type PostgresFactory = ( + connectionString: string, + options: Options, +) => Sql; + +export function createPostgresExecutor(input: { + connectionString: string; + maxConnections?: number; + connectTimeoutMs?: number; + idleTimeoutMs?: number; + sslCaPem?: string; + allowInsecureLoopback?: boolean; + postgresFactory?: PostgresFactory; +}): PostgresExecutor { + if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") { + throw invalidInput("postgres", "tls-override"); + } + const target = validatedPostgresConnectionTarget( + input.connectionString, + ); + const verifiedTls = + !target.isLoopback || target.sslMode === "verify-full"; + if (!verifiedTls && input.allowInsecureLoopback !== true) { + throw invalidInput("postgres", "loopback-tls"); + } + const options: Options = { + ...postgresDriverOptions({ + maxConnections: input.maxConnections ?? 2, + connectTimeoutMs: input.connectTimeoutMs ?? 1_000, + idleTimeoutMs: input.idleTimeoutMs ?? 5_000, + }), + ssl: verifiedTls + ? { + ca: validatedPostgresSslCa(input.sslCaPem), + rejectUnauthorized: true, + } + : false, + }; + const factory: PostgresFactory = + input.postgresFactory ?? + ((connectionString, driverOptions) => { + const connectionUrl = new URL(connectionString); + return postgres({ + ...driverOptions, + host: connectionUrl.hostname, + port: Number(connectionUrl.port), + database: connectionUrl.pathname.slice(1), + username: decodeURIComponent(connectionUrl.username), + password: decodeURIComponent(connectionUrl.password), + }) as unknown as Sql; + }); + const sql = factory( + target.connectionString, + options, + ); + + return { + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return sql.begin(async (transaction) => + work({ + async query>( + text: string, + values: readonly PostgresParameter[] = [], + ) { + const driverValues = values.map((value) => + Array.isArray(value) + ? sql.array([...value]) + : isPostgresJsonParameter(value) + ? sql.json(value.value) + : value as + | null + | boolean + | number + | string + | Date + | Uint8Array, + ); + const result = await transaction.unsafe( + text, + driverValues, + ); + return [...result]; + }, + }), + ) as Promise; + }, + async close() { + await sql.end({ timeout: 5 }); + }, + }; +} + +type DatabaseRow = Record; + +function integerText(value: unknown): string { + if (typeof value === "bigint") { + if (value < 0n) throw validationError("postgres", "integer"); + return value.toString(); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) { + throw validationError("postgres", "integer"); + } + return String(value); + } + try { + return parseNonnegativeIntegerText(value); + } catch { + throw validationError("postgres", "integer"); + } +} + +function uintText(value: unknown): string { + if (typeof value === "bigint") return parseUint256Text(value.toString()); + try { + return parseUint256Text(value); + } catch { + throw validationError("postgres", "uint256"); + } +} + +function nullableAddress(value: unknown): HexAddress | null { + if (value === null) return null; + try { + return addressFromBytea(value); + } catch { + throw validationError("postgres", "address"); + } +} + +function address(value: unknown): HexAddress { + const parsed = nullableAddress(value); + if (parsed === null) throw validationError("postgres", "address"); + return parsed; +} + +function nullableBytes32(value: unknown): HexBytes32 | null { + if (value === null) return null; + try { + return bytes32FromBytea(value); + } catch { + throw validationError("postgres", "bytes32"); + } +} + +function bytes32(value: unknown): HexBytes32 { + const parsed = nullableBytes32(value); + if (parsed === null) throw validationError("postgres", "bytes32"); + return parsed; +} + +function timestamp(value: unknown): string { + const date = + value instanceof Date + ? value + : typeof value === "string" + ? new Date(value) + : null; + if (date === null || Number.isNaN(date.valueOf())) { + throw validationError("postgres", "timestamp"); + } + return date.toISOString(); +} + +function nullableTimestamp(value: unknown): string | null { + return value === null ? null : timestamp(value); +} + +function text( + value: unknown, + pattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/, +): string { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 96 || + !pattern.test(value) + ) { + throw validationError("postgres", "text"); + } + return value; +} + +function descriptiveText( + value: unknown, + maximumBytes: number, + operation: string, +): string { + if ( + typeof value !== "string" || + Buffer.byteLength(value, "utf8") < 1 || + Buffer.byteLength(value, "utf8") > maximumBytes || + /[\u0000-\u001f\u007f]/u.test(value) + ) { + throw validationError("postgres", operation); + } + return value; +} + +function nullableDescriptiveText( + value: unknown, + maximumBytes: number, + operation: string, +): string | null { + return value === null + ? null + : descriptiveText(value, maximumBytes, operation); +} + +function decimalText(value: unknown, operation: string): string { + const candidate = + typeof value === "bigint" + ? value.toString() + : typeof value === "number" && Number.isFinite(value) + ? String(value) + : value; + if ( + typeof candidate !== "string" || + candidate.length < 1 || + candidate.length > 160 || + !/^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(candidate) + ) { + throw validationError("postgres", operation); + } + return candidate; +} + +function nullableDecimalText( + value: unknown, + operation: string, +): string | null { + return value === null ? null : decimalText(value, operation); +} + +function boundedInteger( + value: unknown, + maximum: number, + operation: string, +): number { + let parsed: bigint; + try { + if (typeof value === "number" && Number.isSafeInteger(value)) { + parsed = BigInt(value); + } else if (typeof value === "bigint") { + parsed = value; + } else { + parsed = BigInt(parseNonnegativeIntegerText(value)); + } + } catch { + throw validationError("postgres", operation); + } + if (parsed < 0n || parsed > BigInt(maximum)) { + throw validationError("postgres", operation); + } + return Number(parsed); +} + +function httpsUrl(value: unknown): string { + if ( + typeof value !== "string" || + value.length < 9 || + value.length > 512 + ) { + throw validationError("postgres", "project-link-url"); + } + try { + const parsed = new URL(value); + if ( + parsed.protocol !== "https:" || + parsed.hostname.length === 0 || + parsed.username.length > 0 || + parsed.password.length > 0 + ) { + throw new Error("invalid URL"); + } + } catch { + throw validationError("postgres", "project-link-url"); + } + return value; +} + +export type ProjectLink = { + kind: string; + url: string; + displayOrder: number; +}; + +function projectLinks(value: unknown): ProjectLink[] { + if (!Array.isArray(value) || value.length > 32) { + throw validationError("postgres", "project-links"); + } + const seenKinds = new Set(); + let previousOrder = -1; + return value.map((entry) => { + if ( + entry === null || + typeof entry !== "object" || + Array.isArray(entry) || + Object.getPrototypeOf(entry) !== Object.prototype || + Object.keys(entry).sort().join(",") !== "displayOrder,kind,url" + ) { + throw validationError("postgres", "project-link"); + } + const row = entry as Record; + if ( + typeof row.kind !== "string" || + !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u.test(row.kind) + ) { + throw validationError("postgres", "project-link-kind"); + } + const kind = row.kind; + const displayOrder = boundedInteger( + row.displayOrder, + 1_000, + "project-link-order", + ); + if (seenKinds.has(kind) || displayOrder < previousOrder) { + throw validationError("postgres", "project-link-order"); + } + seenKinds.add(kind); + previousOrder = displayOrder; + return { kind, url: httpsUrl(row.url), displayOrder }; + }); +} + +function nullableBps(value: unknown): number | null { + if (value === null) return null; + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < 0 || + value > 10_000 + ) { + throw validationError("postgres", "basis-points"); + } + return value; +} + +export type IndexedLaunch = { + chainId: string; + releaseVersion: string; + modelVersion: string; + token: HexAddress; + creator: HexAddress; + launchTransactionHash: HexBytes32; + poolId: HexBytes32 | null; + rewardVault: HexAddress | null; + launchHash: HexBytes32; + tokenName: string; + tokenSymbol: string; + totalSupply: string; + launchBlockTimestamp: string; + launchTransactionIndex: number; + launchReceiptLogOrdinal: number; + currency0: HexAddress; + currency1: HexAddress; + hook: HexAddress; + quoteAsset: HexAddress | null; + poolKeyFee: string; + tickSpacing: number; + totalSwapFeeBps: number | null; + buySwapFeeBps: number | null; + sellSwapFeeBps: number | null; + creatorFeeBps: number | null; + launcherFeeBps: number | null; + transferTaxBps: number | null; + lpFeePips: string; + project: { + name: string | null; + description: string | null; + logoReference: string | null; + revision: string; + createdAt: string; + links: ProjectLink[]; + } | null; + promotedBlockNumber: string; + promotedBlockHash: HexBytes32; + verifiedAt: string; +}; + +function parseLaunch(row: DatabaseRow): IndexedLaunch { + const links = projectLinks(row.project_links); + const hasMetadata = row.project_metadata_revision !== null; + if ( + hasMetadata !== (row.project_metadata_created_at !== null) || + (!hasMetadata && + (row.project_name !== null || + row.project_description !== null || + row.project_logo_reference !== null || + links.length > 0)) + ) { + throw validationError("postgres", "project-metadata"); + } + return { + chainId: integerText(row.chain_id), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + token: address(row.token), + creator: address(row.creator), + launchTransactionHash: bytes32(row.launch_transaction_hash), + poolId: nullableBytes32(row.pool_id), + rewardVault: nullableAddress(row.reward_vault), + launchHash: bytes32(row.launch_hash), + tokenName: descriptiveText(row.token_name, 128, "token-name"), + tokenSymbol: descriptiveText(row.token_symbol, 32, "token-symbol"), + totalSupply: uintText(row.total_supply), + launchBlockTimestamp: timestamp(row.launch_block_timestamp), + launchTransactionIndex: boundedInteger( + row.launch_transaction_index, + 0x7fff_ffff, + "launch-transaction-index", + ), + launchReceiptLogOrdinal: boundedInteger( + row.launch_receipt_log_ordinal, + 10_000, + "launch-receipt-log-ordinal", + ), + currency0: address(row.currency0), + currency1: address(row.currency1), + hook: address(row.hook), + quoteAsset: nullableAddress(row.quote_asset), + poolKeyFee: integerText(row.pool_key_fee), + tickSpacing: boundedInteger( + row.tick_spacing, + 0x7fff_ffff, + "tick-spacing", + ), + totalSwapFeeBps: nullableBps(row.total_swap_fee_bps), + buySwapFeeBps: nullableBps(row.buy_swap_fee_bps), + sellSwapFeeBps: nullableBps(row.sell_swap_fee_bps), + creatorFeeBps: nullableBps(row.creator_fee_bps), + launcherFeeBps: nullableBps(row.launcher_fee_bps), + transferTaxBps: nullableBps(row.transfer_tax_bps), + lpFeePips: integerText(row.lp_fee_pips), + project: hasMetadata + ? { + name: nullableDescriptiveText( + row.project_name, + 128, + "project-name", + ), + description: nullableDescriptiveText( + row.project_description, + 2_000, + "project-description", + ), + logoReference: nullableDescriptiveText( + row.project_logo_reference, + 512, + "project-logo", + ), + revision: integerText(row.project_metadata_revision), + createdAt: timestamp(row.project_metadata_created_at), + links, + } + : null, + promotedBlockNumber: integerText(row.promoted_block_number), + promotedBlockHash: bytes32(row.promoted_block_hash), + verifiedAt: timestamp(row.verified_at), + }; +} + +export type AccountRewardSummary = { + chainId: string; + account: HexAddress; + vault: HexAddress; + poolId: HexBytes32; + hook: HexAddress; + quoteAsset: HexAddress | null; + entitled: string; + claimed: string; + claimable: string; + releaseVersion: string; + modelVersion: string; + promotedBlockNumber: string; + promotedBlockHash: HexBytes32; + verifiedAt: string; +}; + +function parseReward( + row: DatabaseRow, +): AccountRewardSummary { + return { + chainId: integerText(row.chain_id), + account: address(row.account), + vault: address(row.vault), + poolId: bytes32(row.pool_id), + hook: address(row.hook), + quoteAsset: nullableAddress(row.quote_asset), + entitled: uintText(row.entitled), + claimed: uintText(row.claimed_total), + claimable: uintText(row.claimable_accrued), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + promotedBlockNumber: integerText(row.promoted_block_number), + promotedBlockHash: bytes32(row.promoted_block_hash), + verifiedAt: timestamp(row.verified_at), + }; +} + +function assertScope(condition: boolean, operation: string): asserts condition { + if (!condition) throw validationError("postgres", operation); +} + +export type MarketSnapshot = { + chainId: string; + releaseVersion: string; + modelVersion: string; + token: HexAddress; + poolId: HexBytes32; + sourceDeploymentCommitment: HexBytes32; + sourceSchemaCommitment: HexBytes32; + blockNumber: string; + blockHash: HexBytes32; + sqrtPriceX96: string; + liquidity: string; + marketVolumeToken0: string; + marketVolumeToken1: string; + marketVolumeUsd: string | null; + hookGrossVolume: string | null; + observedAt: string; + reconciliationEvidenceCommitment: HexBytes32; + reconciledAt: string; +}; + +function parseMarketSnapshot(row: DatabaseRow): MarketSnapshot { + return { + chainId: integerText(row.chain_id), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + token: address(row.token), + poolId: bytes32(row.pool_id), + sourceDeploymentCommitment: bytes32( + row.source_deployment_commitment, + ), + sourceSchemaCommitment: bytes32(row.source_schema_commitment), + blockNumber: integerText(row.block_number), + blockHash: bytes32(row.block_hash), + sqrtPriceX96: uintText(row.sqrt_price_x96), + liquidity: uintText(row.liquidity), + marketVolumeToken0: decimalText( + row.market_volume_token0, + "market-volume-token0", + ), + marketVolumeToken1: decimalText( + row.market_volume_token1, + "market-volume-token1", + ), + marketVolumeUsd: nullableDecimalText( + row.market_volume_usd, + "market-volume-usd", + ), + hookGrossVolume: + row.hook_gross_volume === null + ? null + : uintText(row.hook_gross_volume), + observedAt: timestamp(row.observed_at), + reconciliationEvidenceCommitment: bytes32( + row.reconciliation_evidence_commitment, + ), + reconciledAt: timestamp(row.reconciled_at), + }; +} + +export type MarketCandle = { + chainId: string; + releaseVersion: string; + modelVersion: string; + token: HexAddress; + poolId: HexBytes32; + sourceDeploymentCommitment: HexBytes32; + sourceSchemaCommitment: HexBytes32; + sourceBlockNumber: string; + sourceBlockHash: HexBytes32; + interval: "hour" | "day"; + periodStart: string; + periodEnd: string; + open: string; + high: string; + low: string; + close: string; + volumeToken0: string; + volumeToken1: string; + volumeUsd: string | null; + reconciliationEvidenceCommitment: HexBytes32; + reconciledAt: string; +}; + +function parseMarketCandle(row: DatabaseRow): MarketCandle { + if (row.interval !== "hour" && row.interval !== "day") { + throw validationError("postgres", "market-interval"); + } + const periodStart = timestamp(row.period_start); + const periodEnd = timestamp(row.period_end); + if (Date.parse(periodEnd) <= Date.parse(periodStart)) { + throw validationError("postgres", "market-period"); + } + return { + chainId: integerText(row.chain_id), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + token: address(row.token), + poolId: bytes32(row.pool_id), + sourceDeploymentCommitment: bytes32( + row.source_deployment_commitment, + ), + sourceSchemaCommitment: bytes32(row.source_schema_commitment), + sourceBlockNumber: integerText(row.source_block_number), + sourceBlockHash: bytes32(row.source_block_hash), + interval: row.interval, + periodStart, + periodEnd, + open: decimalText(row.open, "market-open"), + high: decimalText(row.high, "market-high"), + low: decimalText(row.low, "market-low"), + close: decimalText(row.close, "market-close"), + volumeToken0: decimalText(row.volume_token0, "market-volume-token0"), + volumeToken1: decimalText(row.volume_token1, "market-volume-token1"), + volumeUsd: nullableDecimalText(row.volume_usd, "market-volume-usd"), + reconciliationEvidenceCommitment: bytes32( + row.reconciliation_evidence_commitment, + ), + reconciledAt: timestamp(row.reconciled_at), + }; +} + +export type VaultHistoryRow = { + chainId: string; + releaseVersion: string; + modelVersion: string; + vault: HexAddress; + poolId: HexBytes32; + quoteAsset: HexAddress | null; + configurationHash: HexBytes32; + configurationEpoch: string; + allocationIndex: number; + beneficiary: HexAddress; + payoutAddress: HexAddress; + shareBps: number; + effectiveFromBlock: string; + effectiveToBlock: string | null; + promotedBlockNumber: string; + promotedBlockHash: HexBytes32; + verifiedAt: string; +}; + +function parseVaultHistory(row: DatabaseRow): VaultHistoryRow { + return { + chainId: integerText(row.chain_id), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + vault: address(row.vault), + poolId: bytes32(row.pool_id), + quoteAsset: + row.quote_asset === undefined + ? null + : nullableAddress(row.quote_asset), + configurationHash: bytes32(row.configuration_hash), + configurationEpoch: integerText(row.configuration_epoch), + allocationIndex: boundedInteger( + row.allocation_index, + 0x7fff_ffff, + "allocation-index", + ), + beneficiary: address(row.beneficiary), + payoutAddress: address(row.payout_address), + shareBps: boundedInteger(row.share_bps, 10_000, "share-bps"), + effectiveFromBlock: integerText(row.effective_from_block), + effectiveToBlock: + row.effective_to_block === null + ? null + : integerText(row.effective_to_block), + promotedBlockNumber: integerText(row.promoted_block_number), + promotedBlockHash: bytes32(row.promoted_block_hash), + verifiedAt: timestamp(row.verified_at), + }; +} + +function pagination(limit: number, offset = 0, maximumLimit = 100) { + if ( + !Number.isSafeInteger(limit) || + limit < 1 || + limit > maximumLimit || + !Number.isSafeInteger(offset) || + offset < 0 || + offset > 10_000 + ) { + throw invalidInput("postgres", "pagination"); + } + return { limit, offset }; +} + +function chain(value: string): string { + const parsed = parseNonnegativeIntegerText(value); + if (parsed === "0") throw invalidInput("postgres", "chain-id"); + return parsed; +} + +function inputAddress(value: string): HexAddress { + try { + return canonicalAddress(value); + } catch { + throw invalidInput("postgres", "address"); + } +} + +function inputBytes32(value: string): HexBytes32 { + try { + return canonicalBytes32(value); + } catch { + throw invalidInput("postgres", "bytes32"); + } +} + +function inputTimestamp(value: string, operation: string): Date { + if (typeof value !== "string") { + throw invalidInput("postgres", operation); + } + const parsed = new Date(value); + if (Number.isNaN(parsed.valueOf()) || parsed.toISOString() !== value) { + throw invalidInput("postgres", operation); + } + return parsed; +} + +const API_READER_LOGIN_ROLE = "programmable_api_reader_login"; +const API_READER_CAPABILITY_ROLE = "programmable_api_reader"; + +/** + * Establishes the narrow API-reader capability from the one approved login. + * Checking only current_role is insufficient because a privileged or + * accidentally configured session could SET ROLE and silently widen the + * application's database credential boundary. + */ +export async function establishPostgresApiReaderRole( + transaction: PostgresTransaction, +): Promise { + const loginRows = await transaction.query<{ session_user: unknown }>( + "select session_user::text as session_user", + ); + if ( + loginRows.length !== 1 || + loginRows[0]?.session_user !== API_READER_LOGIN_ROLE + ) { + throw validationError("postgres", "runtime-login-role"); + } + + await transaction.query(`set local role ${API_READER_CAPABILITY_ROLE}`); + const roleRows = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + if ( + roleRows.length !== 1 || + roleRows[0]?.session_user !== API_READER_LOGIN_ROLE || + roleRows[0]?.current_role !== API_READER_CAPABILITY_ROLE + ) { + throw validationError("postgres", "runtime-role"); + } +} + +export function createPostgresReadModel(input: { + executor: PostgresExecutor; + circuit?: CircuitBreaker; +}) { + const circuit = + input.circuit ?? new CircuitBreaker({ dependency: "postgres" }); + + async function run( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return circuit.execute(async () => { + try { + return await input.executor.transaction(async (transaction) => { + await establishPostgresApiReaderRole(transaction); + await transaction.query( + "set local statement_timeout = '1000ms'", + ); + await transaction.query("set local lock_timeout = '250ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '2000ms'", + ); + return work(transaction); + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "postgres", + code: "query_failed", + retryable: true, + countsTowardCircuit: true, + }); + } + }); + } + + return Object.freeze({ + async recentLaunches(options: { + chainId: string; + limit: number; + cursor?: { + blockNumber: string; + transactionHash: string; + token: string; + }; + }): Promise { + const chainId = chain(options.chainId); + const page = pagination(options.limit); + const cursor = options.cursor + ? { + blockNumber: parseNonnegativeIntegerText( + options.cursor.blockNumber, + ), + transactionHash: inputBytes32( + options.cursor.transactionHash, + ), + token: inputAddress(options.cursor.token), + } + : null; + return run(async (transaction) => { + const rows = await transaction.query( + "select * from programmable_private.get_recent_launches_v1($1, $2, $3, $4, $5)", + [ + chainId, + page.limit, + cursor?.blockNumber ?? null, + cursor ? hexToBytes(cursor.transactionHash) : null, + cursor ? hexToBytes(cursor.token) : null, + ], + ); + const launches = rows.map((row) => { + const launch = parseLaunch(row); + assertScope(launch.chainId === chainId, "recent-launch-scope"); + if (cursor) { + const block = BigInt(launch.promotedBlockNumber); + const cursorBlock = BigInt(cursor.blockNumber); + assertScope( + block < cursorBlock || + (block === cursorBlock && + launch.launchTransactionHash < cursor.transactionHash) || + (block === cursorBlock && + launch.launchTransactionHash === cursor.transactionHash && + launch.token > cursor.token), + "recent-launch-cursor", + ); + } + return launch; + }); + for (let index = 1; index < launches.length; index += 1) { + const previous = launches[index - 1]!; + const current = launches[index]!; + const previousBlock = BigInt(previous.promotedBlockNumber); + const currentBlock = BigInt(current.promotedBlockNumber); + assertScope( + currentBlock < previousBlock || + (currentBlock === previousBlock && + current.launchTransactionHash < + previous.launchTransactionHash) || + (currentBlock === previousBlock && + current.launchTransactionHash === + previous.launchTransactionHash && + current.token > previous.token), + "recent-launch-order", + ); + } + return launches; + }); + }, + + async launchByToken(options: { + chainId: string; + token: string; + }): Promise { + const chainId = chain(options.chainId); + const token = inputAddress(options.token); + return run(async (transaction) => { + const rows = await transaction.query( + "select * from programmable_private.get_launch_by_token_v1($1, $2)", + [chainId, hexToBytes(token)], + ); + if (rows.length > 1) throw validationError("postgres", "launch"); + if (!rows[0]) return null; + const launch = parseLaunch(rows[0]); + assertScope( + launch.chainId === chainId && launch.token === token, + "launch-token-scope", + ); + return launch; + }); + }, + + async publicProfile(options: { + chainId: string; + account: string; + limit: number; + offset: number; + }): Promise<{ + launches: IndexedLaunch[]; + rewards: AccountRewardSummary[]; + }> { + const chainId = chain(options.chainId); + const account = inputAddress(options.account); + const page = pagination(options.limit, options.offset); + return run(async (transaction) => { + const values = [ + chainId, + hexToBytes(account), + page.limit, + page.offset, + ]; + const launches = await transaction.query( + `select * + from programmable_private.launches_by_creator_v1 + where chain_id = $1 and creator = $2 + order by launch_block_timestamp desc, promoted_block_number desc, token + limit $3 offset $4`, + values, + ); + const rewards = await transaction.query( + `select * + from programmable_private.get_account_reward_summary_v1($1, $2) + limit $3 offset $4`, + values, + ); + return { + launches: launches.map((row) => { + const launch = parseLaunch(row); + assertScope( + launch.chainId === chainId && launch.creator === account, + "creator-launch-scope", + ); + return launch; + }), + rewards: rewards.map((row) => { + const reward = parseReward(row); + assertScope( + reward.chainId === chainId && reward.account === account, + "account-reward-scope", + ); + return reward; + }), + }; + }); + }, + + async marketSnapshot(options: { + chainId: string; + token: string; + }): Promise { + const chainId = chain(options.chainId); + const token = inputAddress(options.token); + return run(async (transaction) => { + const rows = await transaction.query( + `select * + from programmable_private.market_snapshots_v1 + where chain_id = $1 and token = $2 + order by block_number desc, observed_at desc + limit 1`, + [chainId, hexToBytes(token)], + ); + if (rows.length > 1) { + throw validationError("postgres", "market-snapshot"); + } + if (!rows[0]) return null; + const snapshot = parseMarketSnapshot(rows[0]); + assertScope( + snapshot.chainId === chainId && snapshot.token === token, + "market-snapshot-scope", + ); + return snapshot; + }); + }, + + async marketCandles(options: { + chainId: string; + token: string; + interval: "hour" | "day"; + from: string; + to: string; + limit: number; + }): Promise { + const chainId = chain(options.chainId); + const token = inputAddress(options.token); + if (options.interval !== "hour" && options.interval !== "day") { + throw invalidInput("postgres", "market-interval"); + } + const from = inputTimestamp(options.from, "market-from"); + const to = inputTimestamp(options.to, "market-to"); + if (to <= from) throw invalidInput("postgres", "market-period"); + const page = pagination(options.limit, 0, 1_000); + return run(async (transaction) => { + const rows = await transaction.query( + `select * + from programmable_private.market_candles_v1 + where chain_id = $1 + and token = $2 + and interval = $3 + and period_start >= $4 + and period_start < $5 + order by period_start asc, source_block_number asc + limit $6`, + [ + chainId, + hexToBytes(token), + options.interval, + from, + to, + page.limit, + ], + ); + return rows.map((row) => { + const candle = parseMarketCandle(row); + const periodStart = new Date(candle.periodStart); + assertScope( + candle.chainId === chainId && + candle.token === token && + candle.interval === options.interval && + periodStart >= from && + periodStart < to, + "market-candle-scope", + ); + return candle; + }); + }); + }, + + async classicVaultHistory(options: { + chainId: string; + vault: string; + limit: number; + }): Promise { + return vaultHistory( + "classic_v3_vault_history_v1", + options, + ); + }, + + async stockPairedVaultHistory(options: { + chainId: string; + vault: string; + limit: number; + }): Promise { + return vaultHistory( + "stock_paired_vault_history_v1", + options, + ); + }, + + async launchLookup(options: { + chainId: string; + transactionHash: string; + limit: number; + }) { + const chainId = chain(options.chainId); + const transactionHash = inputBytes32(options.transactionHash); + const page = pagination(options.limit); + return run(async (transaction) => { + const rows = await transaction.query( + `select * + from programmable_private.launch_lookup_v1 + where chain_id = $1 and launch_transaction_hash = $2 + order by promoted_block_number desc, token + limit $3`, + [chainId, hexToBytes(transactionHash), page.limit], + ); + return rows.map((row) => { + const result = { + chainId: integerText(row.chain_id), + token: address(row.token), + creator: address(row.creator), + transactionHash: bytes32(row.launch_transaction_hash), + poolId: nullableBytes32(row.pool_id), + rewardVault: nullableAddress(row.reward_vault), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + promotedBlockNumber: integerText(row.promoted_block_number), + promotedBlockHash: bytes32(row.promoted_block_hash), + }; + assertScope( + result.chainId === chainId && + result.transactionHash === transactionHash, + "launch-lookup-scope", + ); + return result; + }); + }); + }, + + async health() { + return run(async (transaction) => { + const checkpoints = await transaction.query( + "select * from programmable_private.checkpoint_summary_v1 order by chain_id, release_id, source_group, projector_version", + ); + const parity = await transaction.query( + "select * from programmable_private.parity_summary_v1 order by route_key, chain_id, release_id, model_id", + ); + const circuits = await transaction.query( + "select * from programmable_private.health_summary_v1 order by dependency", + ); + return { + checkpoints: checkpoints.map((row) => ({ + chainId: integerText(row.chain_id), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + sourceGroup: text( + row.source_group, + /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/, + ), + projectorVersion: text( + row.projector_version, + /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/, + ), + epochId: descriptiveText(row.epoch_id, 64, "epoch-id"), + pointerGeneration: integerText(row.pointer_generation), + leaseGeneration: integerText(row.lease_generation), + checkpointGeneration: integerText(row.checkpoint_generation), + reorgGeneration: integerText(row.reorg_generation), + blockNumber: integerText(row.block_number), + blockHash: bytes32(row.block_hash), + createdAt: timestamp(row.created_at), + })), + parity: parity.map((row) => ({ + routeKey: text( + row.route_key, + /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/, + ), + chainId: integerText(row.chain_id), + releaseVersion: text(row.release_id), + modelVersion: text(row.model_id), + comparisonCount: integerText(row.comparison_count), + matchingCount: integerText(row.matching_count), + mismatchCount: integerText(row.mismatch_count), + lastComparedAt: nullableTimestamp(row.last_compared_at), + lastResolvedAt: nullableTimestamp(row.last_resolved_at), + })), + circuits: circuits.map((row) => ({ + dependency: text( + row.dependency, + /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/, + ), + state: text( + row.circuit_status, + /^(closed|open|half_open|frozen)$/, + ), + observedAt: timestamp(row.observed_at), + failureCount: integerText(row.failure_count), + retryAfter: nullableTimestamp(row.retry_after), + })), + }; + }); + }, + + close: () => input.executor.close(), + circuitSnapshot: () => circuit.snapshot(), + }); + + async function vaultHistory( + view: + | "classic_v3_vault_history_v1" + | "stock_paired_vault_history_v1", + options: { chainId: string; vault: string; limit: number }, + ): Promise { + const chainId = chain(options.chainId); + const vault = inputAddress(options.vault); + const page = pagination(options.limit); + return run(async (transaction) => { + const rows = await transaction.query( + `select * + from programmable_private.${view} + where chain_id = $1 and vault = $2 + order by configuration_epoch desc, promoted_block_number desc + limit $3`, + [chainId, hexToBytes(vault), page.limit], + ); + return rows.map((row) => { + const history = parseVaultHistory(row); + assertScope( + history.chainId === chainId && history.vault === vault, + "vault-history-scope", + ); + return history; + }); + }); + } +} diff --git a/lib/data-pipeline/projector-dynamic-activation.ts b/lib/data-pipeline/projector-dynamic-activation.ts new file mode 100644 index 00000000..b5b4dae0 --- /dev/null +++ b/lib/data-pipeline/projector-dynamic-activation.ts @@ -0,0 +1,90 @@ +import "server-only"; + +import type { CanonicalJsonValue } from "./canonical-fingerprint"; +import type { + DualRpcCandidateWindowEvidence, + DualRpcDynamicRuntimeActivationObservation, + ProjectorDynamicSourceTemplate, +} from "./dual-rpc"; +import type { EnvioCandidate } from "./envio"; +import type { HexAddress, HexBytes32 } from "./codecs"; +import type { VerifiedDynamicSourceLineage } from "./projector-identities"; + +/** + * Canonical database provenance for a factory deployment that may be consumed + * by a later launch block. A raw Envio candidate is never sufficient: the + * resolver must bind the historical parent to its canonical occurrence, + * runtime evidence, template, provider tuple and current reorg generation. + */ +export type CanonicalDynamicSourceDeploymentEvidence = Readonly<{ + provisionalPageId: string; + provisionalLineageId: string; + dynamicSourceAttestationId: string; + runtimeCodeEvidenceId: string; + dynamicSourceTemplateId: string; + parentOccurrenceId: string; + parentCandidateId: string; + parentBlockNumber: string; + parentBlockHash: HexBytes32; + parentBlockGlobalLogIndex: number; + parentTransactionHash: HexBytes32; + parentTransactionIndex: number; + parentSourceAddress: HexAddress; + parentContractName: string; + parentEventName: string; + parentPayloadHash: HexBytes32; + parentRawLogCommitment: HexBytes32; + canonicalStatusHistoryId: string; + safeHeadObservationId: string; + blockEvidenceId: string; + reorgGeneration: string; + envioProviderDeploymentId: string; + rpcProviderDeploymentIds: readonly [string, string]; + providerIdentities: readonly [string, string]; + providerVendorGroups: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; +}>; + +/** A launch candidate paired with exactly one canonical historical parent. */ +export type PendingDynamicSourceActivation = Readonly<{ + activationId: string; + historicalParentCandidate: EnvioCandidate; + launchCandidate: EnvioCandidate; + sourceAddress: HexAddress; + template: ProjectorDynamicSourceTemplate; + canonicalDeployment: CanonicalDynamicSourceDeploymentEvidence; + /** + * Run-scoped lineage used to verify child logs. Its activation boundary is + * the launch log, so the child is accepted only strictly after that log. + */ + ephemeralLineage: VerifiedDynamicSourceLineage; +}>; + +export type ProjectorDynamicSourceActivationModelEvidence = Readonly<{ + activationId: string; + evidenceKind: string; + payload: CanonicalJsonValue; + evidenceCommitment: HexBytes32; +}>; + +export type VerifiedDynamicSourceActivation = Readonly<{ + pending: PendingDynamicSourceActivation; + runtimeObservation: DualRpcDynamicRuntimeActivationObservation; + modelVerificationEvidence: + readonly ProjectorDynamicSourceActivationModelEvidence[]; +}>; + +export type ResolvePendingDynamicSourceActivationsInput = Readonly<{ + candidates: readonly EnvioCandidate[]; + expectedCursorGeneration: string; + expectedCursorBlockHash: HexBytes32; + expectedReorgGeneration: string; +}>; + +export type StageVerifiedDynamicSourceActivationsInput = Readonly<{ + candidates: readonly EnvioCandidate[]; + evidence: DualRpcCandidateWindowEvidence; + activations: readonly VerifiedDynamicSourceActivation[]; + blockComplete: false; +}>; diff --git a/lib/data-pipeline/projector-fold.ts b/lib/data-pipeline/projector-fold.ts new file mode 100644 index 00000000..8bea7bfc --- /dev/null +++ b/lib/data-pipeline/projector-fold.ts @@ -0,0 +1,1742 @@ +import "server-only"; + +import type { DualRpcCandidateEvidence } from "./dual-rpc"; +import type { EnvioCandidate } from "./envio"; +import { PROGRAMMABLE_EVENT_SIGNATURES } from "./event-manifest"; +import { getDataPipelineReleaseBinding } from "./release-binding.server"; + +type HexAddress = `0x${string}`; +type HexBytes32 = `0x${string}`; +type HexData = `0x${string}`; +type ProjectorModel = "classic" | "stock-paired"; +type ProjectorRelease = + | "classic-v2" + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; +type SourceRole = + | "launcher" + | "hook" + | "vault_factory" + | "vesting_factory" + | "coordinator" + | "reward_vault"; + +type CanonicalValue = + | null + | boolean + | string + | readonly string[]; + +type FieldType = + | "address" + | "bytes32" + | "bool" + | "uint8" + | "uint16" + | "uint24" + | "uint64" + | "uint256" + | "int24" + | "address[]" + | "uint16[]"; + +export type ProjectorFactKind = + | "launch" + | "liquidity" + | "initial-buy" + | "initial-buy-custody" + | "pool-registration" + | "fee-disclosure" + | "fee-accrual" + | "creator-hook-claim" + | "launcher-hook-claim" + | "reward-vault-deployment" + | "vesting-wallet-deployment" + | "creator-fee-checkpoint" + | "beneficiary-claim" + | "payout-change" + | "reward-configuration-activation" + | "eth-launch-coordinator"; + +export type ProjectorProcedure = + | "stage_launch_projection" + | "stage_launch_position_liquidity_v1" + | "stage_initial_buy_custody_projection" + | "stage_pool_projection" + | "stage_pool_fee_configuration" + | "stage_pool_fee_configuration_v2" + | "stage_fee_accrual_fact" + | "append_creator_hook_claim_fact" + | "append_launcher_hook_claim_fact" + | "append_creator_fee_checkpoint_fact" + | "stage_claim_projection" + | "stage_payout_change_projection" + | "append_reward_configuration_activation_fact" + | null; + +type EventSpec = Readonly<{ + sourceRole: SourceRole; + kind: ProjectorFactKind; + procedure: ProjectorProcedure; + fields: Readonly>; +}>; + +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const MAX_UINT256 = (1n << 256n) - 1n; +const MIN_TICK = -887_272n; +const MAX_TICK = 887_272n; +const RELEASE_BINDING = getDataPipelineReleaseBinding(); + +const F = Object.freeze({ + address: "address" as const, + bytes32: "bytes32" as const, + bool: "bool" as const, + uint8: "uint8" as const, + uint16: "uint16" as const, + uint24: "uint24" as const, + uint64: "uint64" as const, + uint256: "uint256" as const, + int24: "int24" as const, + addresses: "address[]" as const, + uint16s: "uint16[]" as const, +}); + +function spec( + sourceRole: SourceRole, + kind: ProjectorFactKind, + procedure: ProjectorProcedure, + fields: Record, +): EventSpec { + return Object.freeze({ + sourceRole, + kind, + procedure, + fields: Object.freeze(fields), + }); +} + +const nativeLauncherClaim = { + treasury: F.address, + recipient: F.address, + caller: F.address, + amount: F.uint256, +}; +const stockLauncherClaim = { + treasury: F.address, + recipient: F.address, + quoteAsset: F.address, + caller: F.address, + amount: F.uint256, +}; +const stockLaunch = { + deployer: F.address, + token: F.address, + quoteAsset: F.address, + poolId: F.bytes32, + rewardVault: F.address, + positionRecipient: F.address, + positionTokenId: F.uint256, + launchHash: F.bytes32, +}; +const stockLiquidity = { + token: F.address, + quoteAsset: F.address, + totalSupply: F.uint256, + tokenLiquidityAmount: F.uint256, + lockedTokenDust: F.uint256, + initialTick: F.int24, + tickLower: F.int24, + tickUpper: F.int24, + lpFeePips: F.uint24, + launchHash: F.bytes32, +}; +const stockInitialBuy = { + deployer: F.address, + token: F.address, + quoteAsset: F.address, + poolId: F.bytes32, + quoteAmount: F.uint256, + tokenAmount: F.uint256, + launchHash: F.bytes32, +}; +const stockCoordinator = { + creator: F.address, + token: F.address, + quoteAsset: F.address, + initialBuyEthAmount: F.uint256, + initialBuyQuoteAmount: F.uint256, + initialBuyTokenAmount: F.uint256, + launchHash: F.bytes32, +}; +const stockPoolRegistered = { + poolId: F.bytes32, + token: F.address, + quoteAsset: F.address, + rewardVault: F.address, + registrar: F.address, + quoteIsCurrency0: F.bool, + rewardConfigurationHash: F.bytes32, + quoteConfigurationHash: F.bytes32, +}; +const stockFeeDisclosure = { + poolId: F.bytes32, + token: F.address, + quoteAsset: F.address, + rewardVault: F.address, + buySwapFeeBps: F.uint16, + sellSwapFeeBps: F.uint16, + creatorFeeBps: F.uint16, + launcherFeeBps: F.uint16, + transferTaxBps: F.uint16, + lpFeePips: F.uint24, +}; +const stockFeeAccrual = { + poolId: F.bytes32, + swapSender: F.address, + quoteAsset: F.address, + isBuy: F.bool, + grossQuoteAmount: F.uint256, + creatorFee: F.uint256, + launcherFee: F.uint256, +}; +const stockCreatorClaim = { + poolId: F.bytes32, + rewardVault: F.address, + quoteAsset: F.address, + caller: F.address, + amount: F.uint256, +}; +const stockVaultClaim = { + beneficiary: F.address, + payoutAddress: F.address, + quoteAsset: F.address, + amount: F.uint256, + beneficiaryTotalClaimed: F.uint256, + vaultTotalReceived: F.uint256, +}; +const stockVaultPayout = { + beneficiary: F.address, + previousPayoutAddress: F.address, + newPayoutAddress: F.address, +}; + +const EVENT_SPECS: Readonly>>> = + Object.freeze({ + ClassicV2Launcher: Object.freeze({ + MemeTokenLaunched: spec("launcher", "launch", "stage_launch_projection", { + creator: F.address, + token: F.address, + poolId: F.bytes32, + feeHook: F.address, + positionRecipient: F.address, + positionTokenId: F.uint256, + totalSwapFeeBps: F.uint16, + launchHash: F.bytes32, + }), + MemeLiquidityConfigured: spec( + "launcher", + "liquidity", + "stage_launch_position_liquidity_v1", + { + token: F.address, + totalSupply: F.uint256, + tokenLiquidityAmount: F.uint256, + lockedTokenDust: F.uint256, + initialTick: F.int24, + tickLower: F.int24, + tickUpper: F.int24, + lpFeePips: F.uint24, + launchHash: F.bytes32, + }, + ), + MemeCreatorInitialBuy: spec("launcher", "initial-buy", null, { + creator: F.address, + token: F.address, + poolId: F.bytes32, + nativeAmount: F.uint256, + tokenAmount: F.uint256, + launchHash: F.bytes32, + }), + }), + ClassicV2Hook: Object.freeze({ + PoolRegistered: spec("hook", "pool-registration", "stage_pool_projection", { + poolId: F.bytes32, + token: F.address, + creator: F.address, + registrar: F.address, + totalSwapFeeBps: F.uint16, + }), + PoolFeeDisclosure: spec( + "hook", + "fee-disclosure", + "stage_pool_fee_configuration", + { + poolId: F.bytes32, + token: F.address, + buySwapFeeBps: F.uint16, + sellSwapFeeBps: F.uint16, + launcherFeeBps: F.uint16, + transferTaxBps: F.uint16, + lpFeePips: F.uint24, + }, + ), + NativeSwapFeesAccrued: spec("hook", "fee-accrual", "stage_fee_accrual_fact", { + poolId: F.bytes32, + swapSender: F.address, + grossNativeAmount: F.uint256, + creatorFee: F.uint256, + launcherFee: F.uint256, + }), + CreatorFeesClaimed: spec( + "hook", + "creator-hook-claim", + "append_creator_hook_claim_fact", + { + poolId: F.bytes32, + creator: F.address, + recipient: F.address, + caller: F.address, + amount: F.uint256, + }, + ), + LauncherFeesClaimed: spec( + "hook", + "launcher-hook-claim", + "append_launcher_hook_claim_fact", + nativeLauncherClaim, + ), + }), + ClassicV3Launcher: Object.freeze({ + MemeTokenLaunchedV2: spec("launcher", "launch", "stage_launch_projection", { + deployer: F.address, + token: F.address, + poolId: F.bytes32, + feeHook: F.address, + rewardVault: F.address, + positionRecipient: F.address, + positionTokenId: F.uint256, + buySwapFeeBps: F.uint16, + sellSwapFeeBps: F.uint16, + rewardConfigurationHash: F.bytes32, + launchHash: F.bytes32, + }), + MemeLiquidityConfiguredV2: spec( + "launcher", + "liquidity", + "stage_launch_position_liquidity_v1", + { + token: F.address, + totalSupply: F.uint256, + tokenLiquidityAmount: F.uint256, + lockedTokenDust: F.uint256, + initialTick: F.int24, + tickLower: F.int24, + tickUpper: F.int24, + lpFeePips: F.uint24, + launchHash: F.bytes32, + }, + ), + MemeCreatorInitialBuyV2: spec("launcher", "initial-buy", null, { + deployer: F.address, + token: F.address, + poolId: F.bytes32, + nativeAmount: F.uint256, + tokenAmount: F.uint256, + launchHash: F.bytes32, + }), + MemeCreatorInitialBuyCustodyV2: spec( + "launcher", + "initial-buy-custody", + "stage_initial_buy_custody_projection", + { + deployer: F.address, + token: F.address, + custody: F.address, + mode: F.uint8, + durationDays: F.uint16, + cliffDays: F.uint16, + configurationHash: F.bytes32, + launchHash: F.bytes32, + }, + ), + }), + ClassicV3Hook: Object.freeze({ + PoolRegistered: spec("hook", "pool-registration", "stage_pool_projection", { + poolId: F.bytes32, + token: F.address, + rewardVault: F.address, + registrar: F.address, + buySwapFeeBps: F.uint16, + sellSwapFeeBps: F.uint16, + rewardConfigurationHash: F.bytes32, + }), + PoolFeeDisclosure: spec( + "hook", + "fee-disclosure", + "stage_pool_fee_configuration_v2", + { + poolId: F.bytes32, + token: F.address, + rewardVault: F.address, + buySwapFeeBps: F.uint16, + sellSwapFeeBps: F.uint16, + buyCreatorFeeBps: F.uint16, + sellCreatorFeeBps: F.uint16, + launcherFeeBps: F.uint16, + transferTaxBps: F.uint16, + lpFeePips: F.uint24, + }, + ), + NativeSwapFeesAccrued: spec("hook", "fee-accrual", "stage_fee_accrual_fact", { + poolId: F.bytes32, + swapSender: F.address, + isBuy: F.bool, + appliedTotalSwapFeeBps: F.uint16, + grossNativeAmount: F.uint256, + creatorFee: F.uint256, + launcherFee: F.uint256, + }), + CreatorFeesClaimed: spec( + "hook", + "creator-hook-claim", + "append_creator_hook_claim_fact", + { + poolId: F.bytes32, + rewardVault: F.address, + caller: F.address, + amount: F.uint256, + }, + ), + LauncherFeesClaimed: spec( + "hook", + "launcher-hook-claim", + "append_launcher_hook_claim_fact", + nativeLauncherClaim, + ), + }), + ClassicV3RewardVaultFactory: Object.freeze({ + ClassicRewardVaultDeployed: spec( + "vault_factory", + "reward-vault-deployment", + null, + { + vault: F.address, + poolId: F.bytes32, + feeHook: F.address, + salt: F.bytes32, + configurationHash: F.bytes32, + }, + ), + }), + ClassicV3VestingWalletFactory: Object.freeze({ + ClassicInitialBuyVestingWalletDeployed: spec( + "vesting_factory", + "vesting-wallet-deployment", + null, + { + wallet: F.address, + token: F.address, + beneficiary: F.address, + salt: F.bytes32, + configurationHash: F.bytes32, + }, + ), + }), + ClassicV3RewardVault: Object.freeze({ + CreatorFeesCheckpointed: spec( + "reward_vault", + "creator-fee-checkpoint", + "append_creator_fee_checkpoint_fact", + { + poolId: F.bytes32, + configurationEpoch: F.uint64, + amount: F.uint256, + totalCreatorFeesReceived: F.uint256, + }, + ), + BeneficiaryFeesClaimed: spec( + "reward_vault", + "beneficiary-claim", + "stage_claim_projection", + { + beneficiary: F.address, + amount: F.uint256, + beneficiaryTotalClaimed: F.uint256, + vaultTotalReceived: F.uint256, + }, + ), + PayoutWalletChanged: spec( + "reward_vault", + "payout-change", + "stage_payout_change_projection", + { + poolId: F.bytes32, + allocationIndex: F.uint256, + previousPayoutWallet: F.address, + newPayoutWallet: F.address, + shareBps: F.uint16, + configurationEpoch: F.uint64, + activeConfigurationHash: F.bytes32, + effectiveTotalCreatorFeesReceived: F.uint256, + }, + ), + CtoRewardConfigurationActivated: spec( + "reward_vault", + "reward-configuration-activation", + "append_reward_configuration_activation_fact", + { + poolId: F.bytes32, + approvalReference: F.bytes32, + configurationEpoch: F.uint64, + previousConfigurationHash: F.bytes32, + newConfigurationHash: F.bytes32, + beneficiaries: F.addresses, + sharesBps: F.uint16s, + effectiveTotalCreatorFeesReceived: F.uint256, + }, + ), + }), + StockV1Launcher: stockLauncherSpec(), + StockV2Launcher: stockLauncherSpec(), + StockV3Launcher: stockLauncherSpec(), + StockV1EthCoordinator: stockCoordinatorSpec(), + StockV2EthCoordinator: stockCoordinatorSpec(), + StockV3EthCoordinator: stockCoordinatorSpec(), + StockV1Hook: stockHookSpec(), + StockV2V3Hook: stockHookSpec(), + StockV1RewardVaultFactory: stockFactorySpec(), + StockV2V3RewardVaultFactory: stockFactorySpec(), + StockV1RewardVault: stockVaultSpec(), + StockV2V3RewardVault: stockVaultSpec(), + }); + +function stockLauncherSpec(): Readonly> { + return Object.freeze({ + StockPairedTokenLaunched: spec("launcher", "launch", "stage_launch_projection", stockLaunch), + StockPairedLiquidityConfigured: spec( + "launcher", + "liquidity", + "stage_launch_position_liquidity_v1", + stockLiquidity, + ), + StockPairedCreatorInitialBuy: spec("launcher", "initial-buy", null, stockInitialBuy), + }); +} + +function stockCoordinatorSpec(): Readonly> { + return Object.freeze({ + StockPairedEthTokenLaunched: spec( + "coordinator", + "eth-launch-coordinator", + null, + stockCoordinator, + ), + }); +} + +function stockHookSpec(): Readonly> { + return Object.freeze({ + PoolRegistered: spec("hook", "pool-registration", "stage_pool_projection", stockPoolRegistered), + PoolFeeDisclosure: spec( + "hook", + "fee-disclosure", + "stage_pool_fee_configuration", + stockFeeDisclosure, + ), + QuoteSwapFeesAccrued: spec("hook", "fee-accrual", "stage_fee_accrual_fact", stockFeeAccrual), + CreatorFeesClaimed: spec( + "hook", + "creator-hook-claim", + "append_creator_hook_claim_fact", + stockCreatorClaim, + ), + LauncherFeesClaimed: spec( + "hook", + "launcher-hook-claim", + "append_launcher_hook_claim_fact", + stockLauncherClaim, + ), + }); +} + +function stockFactorySpec(): Readonly> { + return Object.freeze({ + QuoteAssetFeeSplitVaultDeployed: spec( + "vault_factory", + "reward-vault-deployment", + null, + { + vault: F.address, + feeHook: F.address, + poolId: F.bytes32, + quoteAsset: F.address, + }, + ), + }); +} + +function stockVaultSpec(): Readonly> { + return Object.freeze({ + PayoutAddressUpdated: spec( + "reward_vault", + "payout-change", + "stage_payout_change_projection", + stockVaultPayout, + ), + BeneficiaryFeesClaimed: spec( + "reward_vault", + "beneficiary-claim", + "stage_claim_projection", + stockVaultClaim, + ), + }); +} + +function fail(reason: string): never { + throw new TypeError(`Projector fold rejected ${reason}`); +} + +function isPlainRecord(value: unknown): value is Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function exactKeys(value: Record, expected: readonly string[]) { + const actual = Object.keys(value).sort((a, b) => a.localeCompare(b)); + const sortedExpected = [...expected].sort((a, b) => a.localeCompare(b)); + return actual.length === sortedExpected.length && + actual.every((key, index) => key === sortedExpected[index]); +} + +function canonicalUnsigned(value: unknown, bits: number): string { + if (typeof value !== "string" || !/^(?:0|[1-9]\d*)$/u.test(value)) { + return fail("non-canonical unsigned payload value"); + } + const integer = BigInt(value); + const maximum = bits === 256 ? MAX_UINT256 : (1n << BigInt(bits)) - 1n; + if (integer > maximum) return fail("out-of-range unsigned payload value"); + return value; +} + +function canonicalSigned(value: unknown, bits: number): string { + if (typeof value !== "string" || !/^(?:0|[1-9]\d*|-[1-9]\d*)$/u.test(value)) { + return fail("non-canonical signed payload value"); + } + const integer = BigInt(value); + const boundary = 1n << BigInt(bits - 1); + if (integer < -boundary || integer >= boundary) { + return fail("out-of-range signed payload value"); + } + return value; +} + +function canonicalHex(value: unknown, bytes: number): string { + const pattern = new RegExp(`^0x[0-9a-f]{${bytes * 2}}$`, "u"); + if (typeof value !== "string" || !pattern.test(value)) { + return fail("non-canonical hexadecimal payload value"); + } + return value; +} + +function canonicalValue(type: FieldType, value: unknown): CanonicalValue { + switch (type) { + case "address": + return canonicalHex(value, 20); + case "bytes32": + return canonicalHex(value, 32); + case "bool": + if (typeof value !== "boolean") return fail("non-boolean payload value"); + return value; + case "uint8": + return canonicalUnsigned(value, 8); + case "uint16": + return canonicalUnsigned(value, 16); + case "uint24": + return canonicalUnsigned(value, 24); + case "uint64": + return canonicalUnsigned(value, 64); + case "uint256": + return canonicalUnsigned(value, 256); + case "int24": + return canonicalSigned(value, 24); + case "address[]": + case "uint16[]": { + if (!Array.isArray(value)) return fail("non-array payload value"); + return Object.freeze( + value.map((item) => + type === "address[]" + ? canonicalHex(item, 20) + : canonicalUnsigned(item, 16), + ), + ); + } + } +} + +function canonicalPayload( + input: unknown, + fields: Readonly>, +): Readonly> { + if (!isPlainRecord(input) || !exactKeys(input, Object.keys(fields))) { + return fail("payload fields"); + } + return Object.freeze( + Object.fromEntries( + Object.entries(fields) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([name, type]) => [name, canonicalValue(type, input[name])]), + ), + ); +} + +function canonicalAddress(value: string): HexAddress { + return canonicalHex(value, 20) as HexAddress; +} + +function canonicalBytes32(value: string): HexBytes32 { + return canonicalHex(value, 32) as HexBytes32; +} + +function canonicalData(value: string): HexData { + if (!/^0x(?:[0-9a-f]{2})*$/u.test(value)) return fail("raw data"); + return value as HexData; +} + +function exactRelease( + event: ProjectorFoldEvent, +): { model: ProjectorModel; releaseVersion: ProjectorRelease } { + const selectedModel = event.releaseContext?.model ?? event.evidence.model; + const selectedRelease = + event.releaseContext?.releaseVersion ?? event.evidence.releaseVersion; + if ( + (selectedModel !== "classic" && selectedModel !== "stock-paired") || + ![ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ].includes(selectedRelease) + ) { + return fail("unresolved release evidence"); + } + if ( + event.releaseContext && + ((event.evidence.model !== "unresolved" && + event.evidence.model !== event.releaseContext.model) || + (event.evidence.releaseVersion !== "unresolved" && + event.evidence.releaseVersion !== event.releaseContext.releaseVersion) || + (event.candidate.releaseHint.model !== "unresolved" && + event.candidate.releaseHint.model !== event.releaseContext.model) || + (event.candidate.releaseHint.releaseVersion !== "unresolved" && + event.candidate.releaseHint.releaseVersion !== + event.releaseContext.releaseVersion)) + ) { + return fail("release context conflicts with provider evidence"); + } + const release = RELEASE_BINDING.releases.find( + (item) => item.model === selectedModel && item.releaseVersion === selectedRelease, + ); + if ( + !release || + BigInt(event.candidate.blockNumber) < BigInt(release.activationBlock) || + (![...release.sourceContracts, ...release.dynamicContracts].includes( + event.candidate.contractName, + )) + ) { + return fail("release binding"); + } + return { + model: selectedModel, + releaseVersion: selectedRelease as ProjectorRelease, + }; +} + +export type ProjectorFoldEvent = Readonly<{ + candidate: EnvioCandidate; + evidence: DualRpcCandidateEvidence; + releaseContext?: Readonly<{ + model: ProjectorModel; + releaseVersion: ProjectorRelease; + }>; +}>; + +export type ProjectorOccurrenceFact = Readonly<{ + candidateId: string; + chainId: "1"; + releaseId: ProjectorRelease; + modelId: ProjectorModel; + sourceGroup: "core"; + blockNumber: string; + blockHash: HexBytes32; + blockTimestamp: string; + transactionHash: HexBytes32; + transactionIndex: string; + receiptLogOrdinal: string; + blockGlobalLogIndex: string; + sourceAddress: HexAddress; + eventSignature: HexBytes32; + eventType: string; + orderedTopics: readonly HexBytes32[]; + rawData: HexData; + decodedPayload: Readonly>; + payloadHash: HexBytes32; + dynamicSourceAttestationId: string | null; +}>; + +export type ProjectorEventFact = Readonly<{ + sourceCandidateId: string; + sourceRole: SourceRole; + kind: ProjectorFactKind; + procedure: ProjectorProcedure; + values: Readonly>; +}>; + +function assertEvidence(input: ProjectorFoldEvent) { + const { candidate, evidence } = input; + if ( + evidence.chainId !== 1 || + evidence.candidateId !== candidate.candidateId || + evidence.sourceAddress !== candidate.sourceAddress || + evidence.contractName !== candidate.contractName || + evidence.eventName !== candidate.eventName || + evidence.payloadHash !== candidate.payloadHash || + evidence.candidateBlockNumber !== candidate.blockNumber || + evidence.candidateBlockHash !== candidate.blockHash || + evidence.candidateBlockTimestamp !== candidate.blockTimestamp || + evidence.transactionHash !== candidate.transactionHash || + evidence.transactionIndex !== candidate.transactionIndex || + !Number.isSafeInteger(evidence.receiptLogOrdinal) || + evidence.receiptLogOrdinal < 0 || + evidence.receiptLogOrdinal > 0xffff_ffff + ) { + return fail("dual-RPC evidence mismatch"); + } + const dynamic = EVENT_SPECS[candidate.contractName]?.[candidate.eventName] + ?.sourceRole === "reward_vault"; + if ( + dynamic && + (evidence.sourceKind !== "dynamic-attested" || + typeof evidence.dynamicSourceAttestationId !== "string" || + !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test( + evidence.dynamicSourceAttestationId, + )) + ) { + return fail("dynamic source attestation"); + } + if (!dynamic && evidence.sourceKind !== "static") { + return fail("static source evidence"); + } +} + +export function translateProjectorEvent(input: ProjectorFoldEvent): Readonly<{ + occurrence: ProjectorOccurrenceFact; + fact: ProjectorEventFact; +}> { + assertEvidence(input); + const { candidate, evidence } = input; + const eventSpec = EVENT_SPECS[candidate.contractName]?.[candidate.eventName]; + if (!eventSpec) return fail("event outside frozen manifest"); + const release = exactRelease(input); + const decodedPayload = canonicalPayload(candidate.decodedPayload, eventSpec.fields); + const factValues = normalizedFactValues(eventSpec.kind, decodedPayload); + if ( + candidate.chainId !== 1 || + !/^(?:0|[1-9]\d*)$/u.test(candidate.blockNumber) || + !/^(?:0|[1-9]\d*)$/u.test(candidate.blockTimestamp) || + BigInt(candidate.blockNumber) > 9_223_372_036_854_775_807n || + BigInt(candidate.blockTimestamp) > (1n << 64n) - 1n || + !Number.isSafeInteger(candidate.transactionIndex) || + candidate.transactionIndex < 0 || + candidate.transactionIndex > 0xffff_ffff || + !Number.isSafeInteger(candidate.blockGlobalLogIndex) || + candidate.blockGlobalLogIndex < 0 || + candidate.blockGlobalLogIndex > 0xffff_ffff || + candidate.orderedTopics.length < 1 || + candidate.orderedTopics.length > 4 + ) { + return fail("occurrence placement"); + } + const candidatePattern = /^1:(0x[0-9a-f]{64}):(0x[0-9a-f]{64}):(0|[1-9]\d*)$/u.exec( + candidate.candidateId, + ); + if ( + !candidatePattern || + candidatePattern[1] !== candidate.blockHash || + candidatePattern[2] !== candidate.transactionHash || + candidatePattern[3] !== String(candidate.blockGlobalLogIndex) + ) { + return fail("candidate placement identity"); + } + const orderedTopics = Object.freeze( + candidate.orderedTopics.map((topic) => canonicalBytes32(topic)), + ); + const occurrence: ProjectorOccurrenceFact = Object.freeze({ + candidateId: candidate.candidateId, + chainId: "1", + releaseId: release.releaseVersion, + modelId: release.model, + sourceGroup: "core", + blockNumber: candidate.blockNumber, + blockHash: canonicalBytes32(candidate.blockHash), + blockTimestamp: candidate.blockTimestamp, + transactionHash: canonicalBytes32(candidate.transactionHash), + transactionIndex: String(candidate.transactionIndex), + receiptLogOrdinal: String(evidence.receiptLogOrdinal), + blockGlobalLogIndex: String(candidate.blockGlobalLogIndex), + sourceAddress: canonicalAddress(candidate.sourceAddress), + eventSignature: orderedTopics[0]!, + eventType: candidate.eventName, + orderedTopics, + rawData: canonicalData(candidate.rawData), + decodedPayload, + payloadHash: canonicalBytes32(candidate.payloadHash), + dynamicSourceAttestationId: evidence.dynamicSourceAttestationId ?? null, + }); + const fact = Object.freeze({ + sourceCandidateId: candidate.candidateId, + sourceRole: eventSpec.sourceRole, + kind: eventSpec.kind, + procedure: eventSpec.procedure, + values: factValues, + }); + validateFactInvariants(fact); + return Object.freeze({ occurrence, fact }); +} + +function normalizedFactValues( + kind: ProjectorFactKind, + payload: Readonly>, +): Readonly> { + if (kind !== "fee-accrual") return payload; + const grossAmount = + payload.grossNativeAmount ?? payload.grossQuoteAmount; + if (typeof grossAmount !== "string") return fail("fee accrual gross amount"); + return Object.freeze({ ...payload, grossAmount }); +} + +function uint(values: Readonly>, key: string) { + const value = values[key]; + if (typeof value !== "string" || !/^(?:0|[1-9]\d*)$/u.test(value)) { + return fail(`unsigned ${key}`); + } + return BigInt(value); +} + +function text(values: Readonly>, key: string) { + const value = values[key]; + if (typeof value !== "string") return fail(`scalar ${key}`); + return value; +} + +function bool(values: Readonly>, key: string) { + const value = values[key]; + if (typeof value !== "boolean") return fail(`boolean ${key}`); + return value; +} + +function validateFactInvariants(fact: ProjectorEventFact) { + const values = fact.values; + if (fact.kind === "fee-accrual") { + const gross = uint( + values, + "grossNativeAmount" in values ? "grossNativeAmount" : "grossQuoteAmount", + ); + if (uint(values, "creatorFee") + uint(values, "launcherFee") > gross) { + return fail("fee conservation"); + } + } + if (fact.kind === "fee-disclosure") { + const buy = uint(values, "buySwapFeeBps"); + const sell = uint(values, "sellSwapFeeBps"); + const launcher = uint(values, "launcherFeeBps"); + if (buy > 10_000n || sell > 10_000n || launcher > buy || launcher > sell) { + return fail("fee disclosure bounds"); + } + const buyCreator = values.buyCreatorFeeBps ?? values.creatorFeeBps; + const sellCreator = values.sellCreatorFeeBps ?? values.creatorFeeBps; + if ( + typeof buyCreator === "string" && + typeof sellCreator === "string" && + (BigInt(buyCreator) + launcher !== buy || BigInt(sellCreator) + launcher !== sell) + ) { + return fail("fee disclosure conservation"); + } + if (uint(values, "transferTaxBps") !== 0n) return fail("transfer tax"); + } + if (fact.kind === "liquidity") { + const supply = uint(values, "totalSupply"); + if ( + supply === 0n || + uint(values, "tokenLiquidityAmount") + uint(values, "lockedTokenDust") !== supply || + BigInt(text(values, "initialTick")) < MIN_TICK || + BigInt(text(values, "initialTick")) > MAX_TICK || + BigInt(text(values, "tickLower")) < MIN_TICK || + BigInt(text(values, "tickUpper")) > MAX_TICK || + BigInt(text(values, "tickLower")) >= BigInt(text(values, "tickUpper")) + ) { + return fail("liquidity invariant"); + } + } + if (fact.kind === "initial-buy") { + const funding = "nativeAmount" in values ? "nativeAmount" : "quoteAmount"; + if (uint(values, funding) === 0n || uint(values, "tokenAmount") === 0n) { + return fail("empty initial buy"); + } + } + if (fact.kind === "creator-fee-checkpoint") { + if (uint(values, "amount") > uint(values, "totalCreatorFeesReceived")) { + return fail("creator checkpoint total"); + } + } + if (fact.kind === "beneficiary-claim") { + if ( + uint(values, "amount") > uint(values, "beneficiaryTotalClaimed") || + uint(values, "beneficiaryTotalClaimed") > uint(values, "vaultTotalReceived") + ) { + return fail("beneficiary claim totals"); + } + } + if (fact.kind === "payout-change") { + const previous = text( + values, + "previousPayoutWallet" in values ? "previousPayoutWallet" : "previousPayoutAddress", + ); + const next = text( + values, + "newPayoutWallet" in values ? "newPayoutWallet" : "newPayoutAddress", + ); + if (previous === next) return fail("unchanged payout address"); + } + if (fact.kind === "reward-configuration-activation") { + const beneficiaries = values.beneficiaries; + const shares = values.sharesBps; + if ( + !Array.isArray(beneficiaries) || + !Array.isArray(shares) || + beneficiaries.length === 0 || + beneficiaries.length > 5 || + beneficiaries.length !== shares.length || + new Set(beneficiaries).size !== beneficiaries.length || + shares.reduce((sum, share) => sum + BigInt(share), 0n) !== 10_000n || + shares.some((share) => BigInt(share) === 0n) + ) { + return fail("reward configuration allocation"); + } + } +} + +export function projectorFoldManifestCoverage() { + const manifest = Object.entries(PROGRAMMABLE_EVENT_SIGNATURES) + .flatMap(([contractName, signatures]) => + signatures.map((signature) => { + const eventName = /^([A-Za-z][A-Za-z0-9]*)\(/u.exec(signature)?.[1]; + if (!eventName) return fail("malformed event manifest"); + return { contractName, eventName }; + }), + ) + .sort((left, right) => + `${left.contractName}:${left.eventName}`.localeCompare( + `${right.contractName}:${right.eventName}`, + ), + ); + const specs = Object.entries(EVENT_SPECS) + .flatMap(([contractName, events]) => + Object.keys(events).map((eventName) => ({ contractName, eventName })), + ) + .sort((left, right) => + `${left.contractName}:${left.eventName}`.localeCompare( + `${right.contractName}:${right.eventName}`, + ), + ); + if (JSON.stringify(manifest) !== JSON.stringify(specs)) { + return fail("manifest/spec drift"); + } + return Object.freeze(manifest.map((entry) => Object.freeze(entry))); +} + +/** + * Exact projector rule authority used by the hosted bootstrap planner. + * + * Contract names remain part of this boundary so a semantic source role is + * never reconstructed from a display name or naming convention. + */ +export function projectorFoldProjectionRules() { + projectorFoldManifestCoverage(); + return Object.freeze( + Object.entries(EVENT_SPECS) + .flatMap(([contractName, events]) => + Object.entries(events).map(([eventName, eventSpec]) => + Object.freeze({ + contractName, + eventName, + sourceRole: eventSpec.sourceRole, + projectionKind: eventSpec.kind, + }), + ), + ) + .sort((left, right) => + `${left.contractName}:${left.eventName}`.localeCompare( + `${right.contractName}:${right.eventName}`, + ), + ), + ); +} + +projectorFoldManifestCoverage(); + +type Registration = Readonly<{ + releaseVersion: ProjectorRelease; + sourceAddress: HexAddress; + candidateId: string; + values: Readonly>; +}>; +type Disclosure = Registration; +type VaultDeployment = Registration; +type PendingLaunch = { + releaseVersion: ProjectorRelease; + model: ProjectorModel; + transactionHash: HexBytes32; + launch: ProjectorEventFact; + launchOccurrence: ProjectorOccurrenceFact; + liquidity?: ProjectorEventFact; + liquidityOccurrence?: ProjectorOccurrenceFact; + initialBuy?: ProjectorEventFact; + custody?: ProjectorEventFact; + coordinator?: ProjectorEventFact; +}; + +export type ProjectorCompletedLaunch = Readonly<{ + releaseVersion: ProjectorRelease; + model: ProjectorModel; + token: HexAddress; + creator: HexAddress; + poolId: HexBytes32; + rewardVault: HexAddress | null; + launchHash: HexBytes32; + launchTransactionHash: HexBytes32; + tokenName: string; + tokenSymbol: string; + totalSupply: string; + positionRecipient: HexAddress; + positionTokenId: string; + pool: Readonly<{ + currency0: HexAddress; + currency1: HexAddress; + poolKeyFee: string; + tickSpacing: "200"; + hook: HexAddress; + sourceCandidateId: string; + }>; + feeConfiguration: Readonly<{ + buySwapFeeBps: string; + sellSwapFeeBps: string; + buyCreatorFeeBps: string; + sellCreatorFeeBps: string; + launcherFeeBps: string; + transferTaxBps: string; + lpFeePips: string; + sourceCandidateId: string; + }>; + liquidity: Readonly<{ + tokenLiquidityAmount: string; + lockedTokenDust: string; + initialSqrtPriceX96: string; + initialTick: string; + tickLower: string; + tickUpper: string; + sourceCandidateId: string; + }>; + initialBuy: Readonly<{ + fundingAsset: HexAddress; + fundingAmount: string; + tokenAmount: string; + sourceCandidateId: string; + }>; + custody: null | Readonly<{ + address: HexAddress; + mode: string; + durationDays: string; + cliffDays: string; + configurationHash: HexBytes32; + sourceCandidateId: string; + vestingSourceCandidateId: string | null; + vestingStartTimestamp: string | null; + vestingEndTimestamp: string | null; + }>; + ethFunded: boolean; + occurrenceRoles: readonly Readonly<{ + sourceRole: SourceRole; + candidateId: string; + }>[]; +}>; + +export type ProjectorKnownPool = Readonly<{ + releaseVersion: ProjectorRelease; + poolId: HexBytes32; + token: HexAddress; + quoteAsset: HexAddress | null; + rewardVault: HexAddress | null; +}>; + +export type ProjectorFoldResult = Readonly<{ + occurrences: readonly ProjectorOccurrenceFact[]; + facts: readonly ProjectorEventFact[]; + launches: readonly ProjectorCompletedLaunch[]; + knownPools: readonly ProjectorKnownPool[]; +}>; + +function eventOrderKey(occurrence: ProjectorOccurrenceFact) { + return [ + BigInt(occurrence.blockNumber), + BigInt(occurrence.transactionIndex), + BigInt(occurrence.blockGlobalLogIndex), + ] as const; +} + +function after(left: ProjectorOccurrenceFact, right: ProjectorOccurrenceFact) { + const a = eventOrderKey(left); + const b = eventOrderKey(right); + return a[0] > b[0] || + (a[0] === b[0] && + (a[1] > b[1] || + (a[1] === b[1] && + (a[2] > b[2] || + (a[2] === b[2] && left.candidateId > right.candidateId))))); +} + +function addressOrder(left: string, right: string) { + return BigInt(left) < BigInt(right) ? [left, right] : [right, left]; +} + +function launcherAddress(releaseVersion: ProjectorRelease) { + const contractName = + releaseVersion === "classic-v2" + ? "ClassicV2Launcher" + : releaseVersion === "classic-v3" + ? "ClassicV3Launcher" + : releaseVersion === "stock-paired-v1" + ? "StockV1Launcher" + : releaseVersion === "stock-paired-v2" + ? "StockV2Launcher" + : "StockV3Launcher"; + const source = RELEASE_BINDING.sources.find((item) => item.contractName === contractName); + if (!source) return fail("launcher source binding"); + return source.address; +} + +function tickToSqrtPriceX96(tickText: string): string { + const tick = BigInt(tickText); + if (tick < MIN_TICK || tick > MAX_TICK) return fail("tick math range"); + const absTick = tick < 0n ? -tick : tick; + let ratio = + absTick & 1n + ? 0xfffcb933bd6fad37aa2d162d1a594001n + : 0x100000000000000000000000000000000n; + const constants: readonly [bigint, bigint][] = [ + [0x2n, 0xfff97272373d413259a46990580e213an], + [0x4n, 0xfff2e50f5f656932ef12357cf3c7fdccn], + [0x8n, 0xffe5caca7e10e4e61c3624eaa0941cd0n], + [0x10n, 0xffcb9843d60f6159c9db58835c926644n], + [0x20n, 0xff973b41fa98c081472e6896dfb254c0n], + [0x40n, 0xff2ea16466c96a3843ec78b326b52861n], + [0x80n, 0xfe5dee046a99a2a811c461f1969c3053n], + [0x100n, 0xfcbe86c7900a88aedcffc83b479aa3a4n], + [0x200n, 0xf987a7253ac413176f2b074cf7815e54n], + [0x400n, 0xf3392b0822b70005940c7a398e4b70f3n], + [0x800n, 0xe7159475a2c29b7443b29c7fa6e889d9n], + [0x1000n, 0xd097f3bdfd2022b8845ad8f792aa5825n], + [0x2000n, 0xa9f746462d870fdf8a65dc1f90e061e5n], + [0x4000n, 0x70d869a156d2a1b890bb3df62baf32f7n], + [0x8000n, 0x31be135f97d08fd981231505542fcfa6n], + [0x10000n, 0x9aa508b5b7a84e1c677de54f3e99bc9n], + [0x20000n, 0x5d6af8dedb81196699c329225ee604n], + [0x40000n, 0x2216e584f5fa1ea926041bedfe98n], + [0x80000n, 0x48a170391f7dc42444e8fa2n], + ]; + for (const [mask, constant] of constants) { + if (absTick & mask) ratio = (ratio * constant) >> 128n; + } + if (tick > 0n) ratio = MAX_UINT256 / ratio; + const remainder = ratio & ((1n << 32n) - 1n); + return ((ratio >> 32n) + (remainder === 0n ? 0n : 1n)).toString(); +} + +function metadataFor( + input: Readonly>> | undefined, + token: string, +) { + const metadata = input?.[token]; + if ( + !metadata || + typeof metadata.name !== "string" || + typeof metadata.symbol !== "string" || + Buffer.byteLength(metadata.name, "utf8") < 1 || + Buffer.byteLength(metadata.name, "utf8") > 128 || + Buffer.byteLength(metadata.symbol, "utf8") < 1 || + Buffer.byteLength(metadata.symbol, "utf8") > 32 + ) { + return fail("missing token metadata enrichment"); + } + return metadata; +} + +function txMatches(pending: PendingLaunch, occurrence: ProjectorOccurrenceFact) { + return pending.transactionHash === occurrence.transactionHash; +} + +export function foldProjectorEvents(input: Readonly<{ + events: readonly ProjectorFoldEvent[]; + tokenMetadata?: Readonly>>; + knownPools?: readonly ProjectorKnownPool[]; +}>): ProjectorFoldResult { + const translated = input.events.map(translateProjectorEvent); + for (let index = 1; index < translated.length; index += 1) { + if (!after(translated[index]!.occurrence, translated[index - 1]!.occurrence)) { + return fail("event order"); + } + } + if (new Set(translated.map(({ occurrence }) => occurrence.candidateId)).size !== translated.length) { + return fail("duplicate occurrence"); + } + + const registrations = new Map(); + const disclosures = new Map(); + const vaults = new Map(); + const vestingWallets = new Map(); + const pendingByToken = new Map(); + const knownPools = new Map(); + for (const pool of input.knownPools ?? []) { + if (knownPools.has(pool.poolId)) return fail("duplicate known pool parent"); + knownPools.set(pool.poolId, Object.freeze({ ...pool })); + } + + for (const { occurrence, fact } of translated) { + const values = fact.values; + if (fact.kind === "pool-registration") { + const poolId = text(values, "poolId"); + if (registrations.has(poolId) || knownPools.has(poolId)) { + return fail("duplicate pool parent"); + } + if (text(values, "registrar") !== launcherAddress(occurrence.releaseId)) { + return fail("pool registrar parent"); + } + registrations.set(poolId, { + releaseVersion: occurrence.releaseId, + sourceAddress: occurrence.sourceAddress, + candidateId: occurrence.candidateId, + values, + }); + continue; + } + if (fact.kind === "fee-disclosure") { + const poolId = text(values, "poolId"); + const parent = registrations.get(poolId); + if (!parent || parent.releaseVersion !== occurrence.releaseId) { + return fail("fee disclosure parent"); + } + if ( + text(parent.values, "token") !== text(values, "token") || + ("rewardVault" in values && + text(parent.values, "rewardVault") !== text(values, "rewardVault")) || + ("quoteAsset" in values && + text(parent.values, "quoteAsset") !== text(values, "quoteAsset")) + ) { + return fail("fee disclosure relation"); + } + disclosures.set(poolId, { + releaseVersion: occurrence.releaseId, + sourceAddress: occurrence.sourceAddress, + candidateId: occurrence.candidateId, + values, + }); + continue; + } + if (fact.kind === "reward-vault-deployment") { + const vault = text(values, "vault"); + if (vault === ZERO_ADDRESS || vaults.has(vault)) return fail("reward vault parent"); + vaults.set(vault, { + releaseVersion: occurrence.releaseId, + sourceAddress: occurrence.sourceAddress, + candidateId: occurrence.candidateId, + values, + }); + continue; + } + if (fact.kind === "vesting-wallet-deployment") { + const wallet = text(values, "wallet"); + if (wallet === ZERO_ADDRESS || vestingWallets.has(wallet)) return fail("vesting parent"); + vestingWallets.set(wallet, { + releaseVersion: occurrence.releaseId, + sourceAddress: occurrence.sourceAddress, + candidateId: occurrence.candidateId, + values, + }); + continue; + } + if (fact.kind === "launch") { + const token = text(values, "token"); + const poolId = text(values, "poolId"); + const registration = registrations.get(poolId); + const disclosure = disclosures.get(poolId); + if (!registration || !disclosure) return fail("launch pool parent"); + if ( + registration.releaseVersion !== occurrence.releaseId || + text(registration.values, "token") !== token || + text(values, "feeHook") !== registration.sourceAddress + ) { + return fail("launch pool relation"); + } + if (pendingByToken.has(token)) return fail("duplicate launch parent"); + const rewardVault = values.rewardVault; + if (typeof rewardVault === "string") { + const vault = vaults.get(rewardVault); + if (!vault || vault.releaseVersion !== occurrence.releaseId) { + return fail("launch reward vault parent"); + } + if ( + text(vault.values, "poolId") !== poolId || + text(vault.values, "feeHook") !== registration.sourceAddress || + ("configurationHash" in vault.values && + text(vault.values, "configurationHash") !== + text(values, "rewardConfigurationHash")) + ) { + return fail("launch reward vault relation"); + } + } + pendingByToken.set(token, { + releaseVersion: occurrence.releaseId, + model: occurrence.modelId, + transactionHash: occurrence.transactionHash, + launch: fact, + launchOccurrence: occurrence, + }); + continue; + } + if (fact.kind === "liquidity") { + const token = text(values, "token"); + const pending = pendingByToken.get(token); + if (!pending || !txMatches(pending, occurrence) || pending.liquidity) { + return fail("liquidity parent"); + } + if (text(values, "launchHash") !== text(pending.launch.values, "launchHash")) { + return fail("liquidity launch relation"); + } + pending.liquidity = fact; + pending.liquidityOccurrence = occurrence; + continue; + } + if (fact.kind === "initial-buy") { + const token = text(values, "token"); + const pending = pendingByToken.get(token); + if (!pending || !txMatches(pending, occurrence) || pending.initialBuy) { + return fail("initial buy parent"); + } + const actor = "creator" in values ? "creator" : "deployer"; + const launchActor = "creator" in pending.launch.values ? "creator" : "deployer"; + if ( + text(values, "poolId") !== text(pending.launch.values, "poolId") || + text(values, "launchHash") !== text(pending.launch.values, "launchHash") || + text(values, actor) !== text(pending.launch.values, launchActor) + ) { + return fail("initial buy launch relation"); + } + pending.initialBuy = fact; + continue; + } + if (fact.kind === "initial-buy-custody") { + const token = text(values, "token"); + const pending = pendingByToken.get(token); + if (!pending || !txMatches(pending, occurrence) || pending.custody) { + return fail("custody parent"); + } + if ( + text(values, "launchHash") !== text(pending.launch.values, "launchHash") || + text(values, "deployer") !== text(pending.launch.values, "deployer") + ) { + return fail("custody launch relation"); + } + validateCustody(values, vestingWallets); + pending.custody = fact; + continue; + } + if (fact.kind === "eth-launch-coordinator") { + const token = text(values, "token"); + const pending = pendingByToken.get(token); + if (!pending || !txMatches(pending, occurrence) || pending.coordinator) { + return fail("coordinator launch parent"); + } + if ( + text(values, "creator") !== text(pending.launch.values, "deployer") || + text(values, "quoteAsset") !== text(pending.launch.values, "quoteAsset") || + text(values, "launchHash") !== text(pending.launch.values, "launchHash") || + !pending.initialBuy || + text(values, "initialBuyQuoteAmount") !== text(pending.initialBuy.values, "quoteAmount") || + text(values, "initialBuyTokenAmount") !== text(pending.initialBuy.values, "tokenAmount") || + uint(values, "initialBuyEthAmount") === 0n + ) { + return fail("coordinator launch relation"); + } + pending.coordinator = fact; + continue; + } + if ( + ["fee-accrual", "creator-hook-claim"].includes(fact.kind) && + !registrations.has(text(values, "poolId")) && + !knownPools.has(text(values, "poolId")) + ) { + return fail("fee event pool parent"); + } + } + + const launches: ProjectorCompletedLaunch[] = []; + for (const pending of pendingByToken.values()) { + launches.push( + completeLaunch({ + pending, + registrations, + disclosures, + vaults, + vestingWallets, + tokenMetadata: input.tokenMetadata, + }), + ); + } + for (const launch of launches) { + knownPools.set( + launch.poolId, + Object.freeze({ + releaseVersion: launch.releaseVersion, + poolId: launch.poolId, + token: launch.token, + quoteAsset: launch.model === "classic" ? null : launch.initialBuy.fundingAsset, + rewardVault: launch.rewardVault, + }), + ); + } + return Object.freeze({ + occurrences: Object.freeze(translated.map(({ occurrence }) => occurrence)), + facts: Object.freeze(translated.map(({ fact }) => fact)), + launches: Object.freeze(launches), + knownPools: Object.freeze([...knownPools.values()]), + }); +} + +function validateCustody( + values: Readonly>, + vestingWallets: ReadonlyMap, +) { + const mode = uint(values, "mode"); + const duration = uint(values, "durationDays"); + const cliff = uint(values, "cliffDays"); + const custody = text(values, "custody"); + if (mode > 3n) return fail("custody mode"); + if (mode === 0n) { + if (custody !== ZERO_ADDRESS || duration !== 0n || cliff !== 0n) { + return fail("unlocked custody schedule"); + } + return; + } + if (custody === ZERO_ADDRESS || duration < 1n || duration > 3650n) { + return fail("locked custody schedule"); + } + if (mode === 3n) { + if (cliff < 1n || cliff >= duration) return fail("custody cliff schedule"); + } else if (cliff !== 0n) { + return fail("custody cliff mode"); + } + const parent = vestingWallets.get(custody); + if ( + !parent || + text(parent.values, "token") !== text(values, "token") || + text(parent.values, "beneficiary") !== text(values, "deployer") || + text(parent.values, "configurationHash") !== text(values, "configurationHash") + ) { + return fail("locked custody vesting parent"); + } +} + +function completeLaunch(input: { + pending: PendingLaunch; + registrations: ReadonlyMap; + disclosures: ReadonlyMap; + vaults: ReadonlyMap; + vestingWallets: ReadonlyMap; + tokenMetadata: Readonly>> | undefined; +}): ProjectorCompletedLaunch { + const { pending } = input; + if (!pending.liquidity || !pending.liquidityOccurrence || !pending.initialBuy) { + return fail("incomplete launch"); + } + if (pending.releaseVersion === "classic-v3" && !pending.custody) { + return fail("incomplete Classic v3 custody launch"); + } + const launch = pending.launch.values; + const liquidity = pending.liquidity.values; + const initialBuy = pending.initialBuy.values; + const token = canonicalAddress(text(launch, "token")); + const poolId = canonicalBytes32(text(launch, "poolId")); + const registration = input.registrations.get(poolId); + const disclosure = input.disclosures.get(poolId); + if (!registration || !disclosure) return fail("incomplete launch pool"); + const metadata = metadataFor(input.tokenMetadata, token); + const creator = canonicalAddress(text(launch, "creator" in launch ? "creator" : "deployer")); + if ( + ("creator" in registration.values && text(registration.values, "creator") !== creator) || + ("totalSwapFeeBps" in launch && + (text(launch, "totalSwapFeeBps") !== text(registration.values, "totalSwapFeeBps") || + text(launch, "totalSwapFeeBps") !== text(disclosure.values, "buySwapFeeBps") || + text(launch, "totalSwapFeeBps") !== text(disclosure.values, "sellSwapFeeBps"))) || + ("buySwapFeeBps" in launch && + (text(launch, "buySwapFeeBps") !== text(disclosure.values, "buySwapFeeBps") || + text(launch, "sellSwapFeeBps") !== text(disclosure.values, "sellSwapFeeBps"))) || + text(liquidity, "lpFeePips") !== text(disclosure.values, "lpFeePips") + ) { + return fail("launch economics relation"); + } + const quoteAsset = + pending.model === "classic" + ? ZERO_ADDRESS + : text(launch, "quoteAsset"); + if ( + pending.model === "stock-paired" && + (text(liquidity, "quoteAsset") !== quoteAsset || + text(initialBuy, "quoteAsset") !== quoteAsset || + bool(registration.values, "quoteIsCurrency0") !== (BigInt(quoteAsset) < BigInt(token))) + ) { + return fail("stock quote relation"); + } + const [currency0, currency1] = addressOrder( + pending.model === "classic" ? ZERO_ADDRESS : quoteAsset, + token, + ) as [HexAddress, HexAddress]; + const launcher = text(disclosure.values, "launcherFeeBps"); + const buy = text(disclosure.values, "buySwapFeeBps"); + const sell = text(disclosure.values, "sellSwapFeeBps"); + const buyCreator = + typeof disclosure.values.buyCreatorFeeBps === "string" + ? disclosure.values.buyCreatorFeeBps + : typeof disclosure.values.creatorFeeBps === "string" + ? disclosure.values.creatorFeeBps + : (BigInt(buy) - BigInt(launcher)).toString(); + const sellCreator = + typeof disclosure.values.sellCreatorFeeBps === "string" + ? disclosure.values.sellCreatorFeeBps + : typeof disclosure.values.creatorFeeBps === "string" + ? disclosure.values.creatorFeeBps + : (BigInt(sell) - BigInt(launcher)).toString(); + const occurrenceRoles: { sourceRole: SourceRole; candidateId: string }[] = [ + { sourceRole: "launcher", candidateId: pending.launch.sourceCandidateId }, + ]; + const rewardVault = + typeof launch.rewardVault === "string" + ? canonicalAddress(launch.rewardVault) + : null; + if (rewardVault) { + const parent = input.vaults.get(rewardVault); + if (!parent) return fail("reward vault launch requirement"); + occurrenceRoles.push({ + sourceRole: "vault_factory", + candidateId: parent.candidateId, + }); + } + let custody: ProjectorCompletedLaunch["custody"] = null; + if (pending.custody) { + const values = pending.custody.values; + const mode = uint(values, "mode"); + const duration = uint(values, "durationDays"); + const cliff = uint(values, "cliffDays"); + const custodyAddress = canonicalAddress(text(values, "custody")); + let vestingSourceCandidateId: string | null = null; + let vestingStartTimestamp: string | null = null; + let vestingEndTimestamp: string | null = null; + if (mode !== 0n) { + const vesting = input.vestingWallets.get(custodyAddress); + if (!vesting) return fail("vesting launch requirement"); + vestingSourceCandidateId = vesting.candidateId; + occurrenceRoles.push({ sourceRole: "vesting_factory", candidateId: vesting.candidateId }); + const launchTime = BigInt(pending.launchOccurrence.blockTimestamp); + const start = + mode === 2n + ? launchTime + : launchTime + (mode === 3n ? cliff : duration) * 86_400n; + const end = launchTime + duration * 86_400n; + vestingStartTimestamp = start.toString(); + vestingEndTimestamp = end.toString(); + } + custody = Object.freeze({ + address: custodyAddress, + mode: mode.toString(), + durationDays: duration.toString(), + cliffDays: cliff.toString(), + configurationHash: canonicalBytes32(text(values, "configurationHash")), + sourceCandidateId: pending.custody.sourceCandidateId, + vestingSourceCandidateId, + vestingStartTimestamp, + vestingEndTimestamp, + }); + } + if (pending.coordinator) { + occurrenceRoles.push({ + sourceRole: "coordinator", + candidateId: pending.coordinator.sourceCandidateId, + }); + } + return Object.freeze({ + releaseVersion: pending.releaseVersion, + model: pending.model, + token, + creator, + poolId, + rewardVault, + launchHash: canonicalBytes32(text(launch, "launchHash")), + launchTransactionHash: pending.transactionHash, + tokenName: metadata.name, + tokenSymbol: metadata.symbol, + totalSupply: text(liquidity, "totalSupply"), + positionRecipient: canonicalAddress(text(launch, "positionRecipient")), + positionTokenId: text(launch, "positionTokenId"), + pool: Object.freeze({ + currency0: canonicalAddress(currency0), + currency1: canonicalAddress(currency1), + poolKeyFee: text(disclosure.values, "lpFeePips"), + tickSpacing: "200", + hook: registration.sourceAddress, + sourceCandidateId: registration.candidateId, + }), + feeConfiguration: Object.freeze({ + buySwapFeeBps: buy, + sellSwapFeeBps: sell, + buyCreatorFeeBps: buyCreator, + sellCreatorFeeBps: sellCreator, + launcherFeeBps: launcher, + transferTaxBps: text(disclosure.values, "transferTaxBps"), + lpFeePips: text(disclosure.values, "lpFeePips"), + sourceCandidateId: disclosure.candidateId, + }), + liquidity: Object.freeze({ + tokenLiquidityAmount: text(liquidity, "tokenLiquidityAmount"), + lockedTokenDust: text(liquidity, "lockedTokenDust"), + initialSqrtPriceX96: tickToSqrtPriceX96(text(liquidity, "initialTick")), + initialTick: text(liquidity, "initialTick"), + tickLower: text(liquidity, "tickLower"), + tickUpper: text(liquidity, "tickUpper"), + sourceCandidateId: pending.liquidity.sourceCandidateId, + }), + initialBuy: Object.freeze({ + fundingAsset: canonicalAddress(quoteAsset), + fundingAmount: text( + initialBuy, + "nativeAmount" in initialBuy ? "nativeAmount" : "quoteAmount", + ), + tokenAmount: text(initialBuy, "tokenAmount"), + sourceCandidateId: pending.initialBuy.sourceCandidateId, + }), + custody, + ethFunded: pending.model === "classic" || pending.coordinator !== undefined, + occurrenceRoles: Object.freeze(occurrenceRoles.map((item) => Object.freeze(item))), + }); +} diff --git a/lib/data-pipeline/projector-identities.ts b/lib/data-pipeline/projector-identities.ts new file mode 100644 index 00000000..1fbf119d --- /dev/null +++ b/lib/data-pipeline/projector-identities.ts @@ -0,0 +1,279 @@ +import "server-only"; + +import { + canonicalAddress, + canonicalBytes32, + parseNonnegativeIntegerText, + type HexAddress, + type HexBytes32, +} from "./codecs"; +import { validationError } from "./errors"; +import { canonicalUint32DecimalText } from "./provider-evidence"; +import { + canonicalImmutableReferences, + type ImmutableReference, +} from "./runtime-bytecode"; +import { getDataPipelineReleaseBinding } from "./release-binding.server"; + +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; +const CANDIDATE_ID_PATTERN = + /^1:(0x[0-9a-f]{64}):(0x[0-9a-f]{64}):(0|[1-9]\d*)$/u; +const ZERO_BYTES32 = `0x${"00".repeat(32)}`; +const DYNAMIC_FACTORY_CONTRACTS = Object.freeze({ + ClassicV3RewardVault: "ClassicV3RewardVaultFactory", + StockV1RewardVault: "StockV1RewardVaultFactory", + StockV2V3RewardVault: "StockV2V3RewardVaultFactory", +} as const); + +export type VerifiedDynamicSourceLineage = Readonly<{ + attestationId: string; + sourceAddress: HexAddress; + contractName: keyof typeof DYNAMIC_FACTORY_CONTRACTS; + model: "classic" | "stock-paired"; + releaseVersion: + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + factoryAddress: HexAddress; + factoryContractName: (typeof DYNAMIC_FACTORY_CONTRACTS)[keyof typeof DYNAMIC_FACTORY_CONTRACTS]; + parentOccurrenceId?: string; + factoryCandidateId?: string; + factoryBlockNumber: string; + factoryBlockGlobalLogIndex?: string; + activationCandidateId?: string; + activationOccurrenceId?: string; + activationBlockNumber?: string; + activationBlockHash?: HexBytes32; + activationBlockGlobalLogIndex?: string; + expectedExactRuntimeCodeHash: HexBytes32; + expectedNormalizedRuntimeCodeHash: HexBytes32; + expectedImmutableReferencesCommitment: HexBytes32; + expectedRuntimeByteLength: string; + immutableReferences: readonly ImmutableReference[]; +}>; + +function dynamicLineageError(): never { + throw validationError("rpc", "dynamic-source-lineage"); +} + +export function canonicalDynamicSourceLineage( + value: VerifiedDynamicSourceLineage, +): VerifiedDynamicSourceLineage { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return dynamicLineageError(); + } + const binding = getDataPipelineReleaseBinding(); + const contractName = value.contractName; + const expectedFactoryContractName = DYNAMIC_FACTORY_CONTRACTS[contractName]; + if ( + !expectedFactoryContractName || + value.factoryContractName !== expectedFactoryContractName || + !UUID_PATTERN.test(value.attestationId) + ) { + return dynamicLineageError(); + } + const release = binding.releases.find( + (candidate) => + candidate.model === value.model && + candidate.releaseVersion === value.releaseVersion && + candidate.dynamicContracts.includes(contractName) && + candidate.sourceContracts.includes(expectedFactoryContractName), + ); + const factory = binding.sources.find( + (source) => + source.contractName === expectedFactoryContractName && + source.address === value.factoryAddress, + ); + if (!release || !factory) return dynamicLineageError(); + let sourceAddress: HexAddress; + let factoryAddress: HexAddress; + let expectedExactRuntimeCodeHash: HexBytes32; + let expectedNormalizedRuntimeCodeHash: HexBytes32; + let expectedImmutableReferencesCommitment: HexBytes32; + let factoryBlockNumber: string; + let factoryBlockGlobalLogIndex: string | undefined; + let activationBlockNumber: string | undefined; + let activationBlockHash: HexBytes32 | undefined; + let activationBlockGlobalLogIndex: string | undefined; + let expectedRuntimeByteLength: string; + try { + sourceAddress = canonicalAddress(value.sourceAddress); + factoryAddress = canonicalAddress(value.factoryAddress); + expectedExactRuntimeCodeHash = canonicalBytes32( + value.expectedExactRuntimeCodeHash, + ); + expectedNormalizedRuntimeCodeHash = canonicalBytes32( + value.expectedNormalizedRuntimeCodeHash, + ); + expectedImmutableReferencesCommitment = canonicalBytes32( + value.expectedImmutableReferencesCommitment, + ); + factoryBlockNumber = parseNonnegativeIntegerText( + value.factoryBlockNumber, + ); + factoryBlockGlobalLogIndex = + value.factoryBlockGlobalLogIndex === undefined + ? undefined + : canonicalUint32DecimalText( + value.factoryBlockGlobalLogIndex, + "dynamic-factory-log-index", + ); + activationBlockNumber = value.activationBlockNumber === undefined + ? undefined + : parseNonnegativeIntegerText(value.activationBlockNumber); + activationBlockHash = value.activationBlockHash === undefined + ? undefined + : canonicalBytes32(value.activationBlockHash); + activationBlockGlobalLogIndex = + value.activationBlockGlobalLogIndex === undefined + ? undefined + : canonicalUint32DecimalText( + value.activationBlockGlobalLogIndex, + "dynamic-activation-log-index", + ); + expectedRuntimeByteLength = canonicalUint32DecimalText( + value.expectedRuntimeByteLength, + "dynamic-runtime-length", + ); + } catch { + return dynamicLineageError(); + } + const byteLength = Number(expectedRuntimeByteLength); + if ( + sourceAddress === factoryAddress || + expectedExactRuntimeCodeHash === ZERO_BYTES32 || + expectedNormalizedRuntimeCodeHash === ZERO_BYTES32 || + expectedImmutableReferencesCommitment === ZERO_BYTES32 || + byteLength < 1 || + byteLength > 24_576 || + BigInt(factoryBlockNumber) < BigInt(factory.startBlock) + ) { + return dynamicLineageError(); + } + let immutableReferences: readonly ImmutableReference[]; + try { + immutableReferences = canonicalImmutableReferences( + value.immutableReferences, + byteLength, + ); + } catch { + return dynamicLineageError(); + } + const hasCandidateParent = value.factoryCandidateId !== undefined; + const hasOccurrenceParent = value.parentOccurrenceId !== undefined; + if (hasCandidateParent === hasOccurrenceParent) { + return dynamicLineageError(); + } + if (hasCandidateParent) { + const parentMatch = CANDIDATE_ID_PATTERN.exec(value.factoryCandidateId!); + if ( + !parentMatch || + factoryBlockGlobalLogIndex === undefined || + BigInt(parentMatch[3]) !== BigInt(factoryBlockGlobalLogIndex) + ) { + return dynamicLineageError(); + } + } else if (!UUID_PATTERN.test(value.parentOccurrenceId!)) { + return dynamicLineageError(); + } + const activationShape = [ + activationBlockNumber, + activationBlockHash, + activationBlockGlobalLogIndex, + ]; + const hasActivationBoundary = activationShape.every( + (entry) => entry !== undefined, + ); + if ( + (!hasActivationBoundary && + activationShape.some((entry) => entry !== undefined)) || + (hasActivationBoundary && + ((value.activationCandidateId === undefined) === + (value.activationOccurrenceId === undefined))) + ) { + return dynamicLineageError(); + } + if (hasActivationBoundary) { + const activationAfterFactory = + BigInt(activationBlockNumber!) > BigInt(factoryBlockNumber) || + (BigInt(activationBlockNumber!) === BigInt(factoryBlockNumber) && + factoryBlockGlobalLogIndex !== undefined && + BigInt(activationBlockGlobalLogIndex!) > + BigInt(factoryBlockGlobalLogIndex)); + const activationMatch = value.activationCandidateId === undefined + ? null + : CANDIDATE_ID_PATTERN.exec(value.activationCandidateId); + if ( + !activationAfterFactory || + (value.activationOccurrenceId !== undefined && + !UUID_PATTERN.test(value.activationOccurrenceId)) || + (activationMatch !== null && + (activationMatch[1] !== activationBlockHash || + BigInt(activationMatch[3]!) !== + BigInt(activationBlockGlobalLogIndex!))) || + (value.activationCandidateId !== undefined && activationMatch === null) + ) { + return dynamicLineageError(); + } + } else if ( + value.activationCandidateId !== undefined || + value.activationOccurrenceId !== undefined + ) { + return dynamicLineageError(); + } + return Object.freeze({ + attestationId: value.attestationId, + sourceAddress, + contractName, + model: value.model, + releaseVersion: value.releaseVersion, + factoryAddress, + factoryContractName: expectedFactoryContractName, + ...(value.parentOccurrenceId + ? { parentOccurrenceId: value.parentOccurrenceId } + : {}), + ...(value.factoryCandidateId + ? { factoryCandidateId: value.factoryCandidateId } + : {}), + factoryBlockNumber, + ...(factoryBlockGlobalLogIndex === undefined + ? {} + : { factoryBlockGlobalLogIndex }), + ...(activationBlockNumber === undefined + ? {} + : { + ...(value.activationCandidateId + ? { activationCandidateId: value.activationCandidateId } + : {}), + ...(value.activationOccurrenceId + ? { activationOccurrenceId: value.activationOccurrenceId } + : {}), + activationBlockNumber, + activationBlockHash: activationBlockHash!, + activationBlockGlobalLogIndex: activationBlockGlobalLogIndex!, + }), + expectedExactRuntimeCodeHash, + expectedNormalizedRuntimeCodeHash, + expectedImmutableReferencesCommitment, + expectedRuntimeByteLength, + immutableReferences, + }); +} + +export function canonicalDynamicSourceLineages( + values: readonly VerifiedDynamicSourceLineage[] | undefined, +): ReadonlyMap { + if (values === undefined) return new Map(); + if (!Array.isArray(values) || values.length > 10_000) { + return dynamicLineageError(); + } + const result = new Map(); + for (const value of values) { + const lineage = canonicalDynamicSourceLineage(value); + if (result.has(lineage.sourceAddress)) return dynamicLineageError(); + result.set(lineage.sourceAddress, lineage); + } + return result; +} diff --git a/lib/data-pipeline/projector-ids.ts b/lib/data-pipeline/projector-ids.ts new file mode 100644 index 00000000..cfed6caf --- /dev/null +++ b/lib/data-pipeline/projector-ids.ts @@ -0,0 +1,36 @@ +import "server-only"; + +import { keccak256, toBytes } from "viem"; + +export function deterministicProjectorUuid( + domain: string, + ...values: readonly string[] +): string { + const digest = keccak256( + toBytes(`programmable:${domain}:v1\0${values.join("\0")}`), + ) + .slice(2, 34) + .split(""); + digest[12] = "8"; + digest[16] = ((Number.parseInt(digest[16]!, 16) & 0x3) | 0x8).toString(16); + const hex = digest.join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +export function projectorOccurrenceUuid(input: Readonly<{ + transactionHash: string; + receiptLogOrdinal: string; + blockHash: string; +}>): string { + const logicalEventId = deterministicProjectorUuid( + "logical-event", + "1", + input.transactionHash, + input.receiptLogOrdinal, + ); + return deterministicProjectorUuid( + "occurrence", + logicalEventId, + input.blockHash, + ); +} diff --git a/lib/data-pipeline/projector-projection.ts b/lib/data-pipeline/projector-projection.ts new file mode 100644 index 00000000..892f8c99 --- /dev/null +++ b/lib/data-pipeline/projector-projection.ts @@ -0,0 +1,566 @@ +import "server-only"; + +import { + readDualRpcRewardSnapshot, + readDualRpcTokenMetadata, + verifyEnvioCandidateBatchWithDualRpc, + type CandidateRpcProvider, + type DualRpcCandidateBatchEvidence, + type DualRpcRewardSnapshot, +} from "./dual-rpc"; +import type { EnvioCandidate } from "./envio"; +import { dataPipelineError, invalidInput, validationError } from "./errors"; +import { + foldProjectorEvents, + type ProjectorFoldResult, + type ProjectorKnownPool, +} from "./projector-fold"; +import type { VerifiedDynamicSourceLineage } from "./projector-identities"; +import { projectorOccurrenceUuid } from "./projector-ids"; +import { + foldProjectorRewardState, + type ProjectorRewardBaseline, + type ProjectorRewardEvent, + type ProjectorRewardModel, + type ProjectorRewardSnapshot, +} from "./projector-reward-fold"; +import type { ProjectorReleaseDatabaseScope } from "./postgres-projector"; +import { + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE, +} from "./projector-runtime-limits"; + +const MAXIMUM_PROJECTION_DEADLINE_MS = 75_000; +// A 32-row page can contain 32 distinct blocks, transactions and sources. +// The verifier rejects a page before making RPC calls unless the configured +// budget covers that worst-case shape. +const PROJECTOR_PROVIDER_CALL_BUDGET = 128; +const LAUNCH_EVENTS = new Set([ + "MemeTokenLaunched", + "MemeTokenLaunchedV2", + "StockPairedTokenLaunched", +]); + +export type ProjectionCandidateAction = "project" | "ignore"; + +export type StoredProjectionCandidate = Readonly< + Pick< + EnvioCandidate, + | "candidateId" + | "chainId" + | "blockNumber" + | "blockHash" + | "transactionHash" + | "transactionIndex" + | "blockGlobalLogIndex" + | "sourceAddress" + | "contractName" + | "eventName" + | "orderedTopics" + | "rawData" + | "decodedPayload" + | "payloadHash" + > +>; + +export type ReleaseProjectionCandidate = Readonly<{ + candidate: StoredProjectionCandidate; + action: ProjectionCandidateAction; + attemptCount: string; +}>; + +export type ReleaseProjectionPlan = Readonly<{ + scope: ProjectorReleaseDatabaseScope; + entries: readonly ReleaseProjectionCandidate[]; + dynamicSources: readonly VerifiedDynamicSourceLineage[]; + knownPools: readonly ProjectorKnownPool[]; + lease: Readonly<{ + generation: string; + expiresAt: string; + }>; + checkpoint: null | Readonly<{ + generation: string; + reorgGeneration: string; + blockNumber: string; + blockHash: `0x${string}`; + blockGlobalLogIndex: number; + candidateId: string; + }>; + rewardVerification: null | Readonly<{ + model: ProjectorRewardModel; + baseline: ProjectorRewardBaseline; + }>; + rewardVerifications?: readonly Readonly<{ + model: ProjectorRewardModel; + baseline: ProjectorRewardBaseline; + }>[]; + batchKind?: "normal" | "oversized-transaction" | "reward-block"; +}>; + +export type VerifiedReleaseProjection = Readonly<{ + plan: ReleaseProjectionPlan; + freshCandidates: readonly EnvioCandidate[]; + ignoredCandidateIds: readonly string[]; + evidence: DualRpcCandidateBatchEvidence; + fold: ProjectorFoldResult; + rewardSnapshot: ProjectorRewardSnapshot | null; + rewardSnapshots?: readonly ProjectorRewardSnapshot[]; + rewardEvidence?: readonly DualRpcRewardSnapshot[]; +}>; + +export type ReleaseProjectionStore = Readonly<{ + /** + * Owns and closes the first short database transaction. The returned plan + * includes an acquired lease, exact release manifest classification and a + * transaction-aligned page. + */ + readProjectionPlan(): Promise; + /** + * Owns and closes the final short database transaction. Implementations + * must re-check epoch, pointer, lease and checkpoint CAS before promotion. + */ + commitVerifiedProjection( + projection: VerifiedReleaseProjection, + ): Promise>; +}>; + +type ProjectionEnvio = Readonly<{ + readCandidate(candidateId: string): Promise; +}>; + +function projectionTimeout() { + return dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); +} + +async function withDeadline( + deadlineMs: number, + operation: () => Promise, +): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + operation(), + new Promise((_resolve, reject) => { + timer = setTimeout(() => reject(projectionTimeout()), deadlineMs); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +function canonicalJson(value: unknown): string { + if ( + value === null || + typeof value === "string" || + typeof value === "boolean" + ) { + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value)) { + throw validationError("envio", "projection-candidate-json"); + } + return JSON.stringify(value); + } + if (Array.isArray(value)) { + return `[${value.map(canonicalJson).join(",")}]`; + } + if ( + typeof value !== "object" || + Object.getPrototypeOf(value) !== Object.prototype + ) { + throw validationError("envio", "projection-candidate-json"); + } + const record = value as Record; + return `{${Object.keys(record) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`) + .join(",")}}`; +} + +function exactCandidateMatch( + expected: StoredProjectionCandidate, + fresh: EnvioCandidate | null, +): EnvioCandidate { + if ( + fresh === null || + fresh.candidateId !== expected.candidateId || + fresh.chainId !== expected.chainId || + fresh.blockNumber !== expected.blockNumber || + fresh.blockHash !== expected.blockHash || + fresh.transactionHash !== expected.transactionHash || + fresh.transactionIndex !== expected.transactionIndex || + fresh.blockGlobalLogIndex !== expected.blockGlobalLogIndex || + fresh.sourceAddress !== expected.sourceAddress || + fresh.contractName !== expected.contractName || + fresh.eventName !== expected.eventName || + fresh.rawData !== expected.rawData || + fresh.payloadHash !== expected.payloadHash || + fresh.orderedTopics.length !== expected.orderedTopics.length || + fresh.orderedTopics.some( + (topic, index) => topic !== expected.orderedTopics[index], + ) || + canonicalJson(fresh.decodedPayload) !== + canonicalJson(expected.decodedPayload) + ) { + throw validationError("envio", "projection-candidate-drift"); + } + return fresh; +} + +function assertPlan(plan: ReleaseProjectionPlan): void { + const batchKind = plan.batchKind ?? "normal"; + const maximum = batchKind === "normal" + ? PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE + : PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP; + if ( + !Array.isArray(plan.entries) || + plan.entries.length < 1 || + plan.entries.length > maximum || + !Array.isArray(plan.dynamicSources) || + !Array.isArray(plan.knownPools) || + !["normal", "oversized-transaction", "reward-block"].includes(batchKind) + ) { + throw invalidInput("postgres", "projection-plan"); + } + if ( + batchKind === "oversized-transaction" && + new Set(plan.entries.map(({ candidate }) => candidate.transactionHash)) + .size !== 1 + ) { + throw invalidInput("postgres", "projection-atomic-transaction"); + } + if ( + batchKind === "reward-block" && + new Set(plan.entries.map(({ candidate }) => candidate.blockHash)).size !== 1 + ) { + throw invalidInput("postgres", "projection-reward-block"); + } + const ids = new Set(); + let previous: + | readonly [bigint, number, string] + | undefined; + for (const entry of plan.entries) { + if ( + (entry.action !== "project" && entry.action !== "ignore") || + !/^(?:0|[1-9]\d*)$/u.test(entry.attemptCount) || + ids.has(entry.candidate.candidateId) + ) { + throw invalidInput("postgres", "projection-plan-entry"); + } + ids.add(entry.candidate.candidateId); + const key = [ + BigInt(entry.candidate.blockNumber), + entry.candidate.blockGlobalLogIndex, + entry.candidate.candidateId, + ] as const; + if ( + previous && + (key[0] < previous[0] || + (key[0] === previous[0] && + (key[1] < previous[1] || + (key[1] === previous[1] && key[2] <= previous[2])))) + ) { + throw invalidInput("postgres", "projection-plan-order"); + } + previous = key; + } + // The database page must never split a transaction. Otherwise a launch can + // be promoted without the sibling events that make it complete. + for (let index = 1; index < plan.entries.length; index += 1) { + const previousEntry = plan.entries[index - 1]!; + const current = plan.entries[index]!; + if ( + current.candidate.transactionHash === + previousEntry.candidate.transactionHash && + current.action !== previousEntry.action + ) { + throw invalidInput("postgres", "projection-transaction-classification"); + } + } +} + +function planRewardVerifications( + plan: ReleaseProjectionPlan, +): readonly Readonly<{ + model: ProjectorRewardModel; + baseline: ProjectorRewardBaseline; +}>[] { + const plural = plan.rewardVerifications; + if (plural !== undefined) { + if (!Array.isArray(plural) || plan.rewardVerification !== null) { + throw invalidInput("postgres", "reward-verification-plan"); + } + const vaults = plural.map(({ baseline }) => baseline.vault); + if (new Set(vaults).size !== vaults.length) { + throw invalidInput("postgres", "reward-verification-plan"); + } + return Object.freeze([...plural].sort((left, right) => + left.baseline.vault.localeCompare(right.baseline.vault) + )); + } + return plan.rewardVerification === null + ? Object.freeze([]) + : Object.freeze([plan.rewardVerification]); +} + +function launchMetadataRequests(candidates: readonly EnvioCandidate[]) { + const requests = new Map< + string, + Readonly<{ + token: `0x${string}`; + blockNumber: string; + blockHash: `0x${string}`; + }> + >(); + for (const candidate of candidates) { + if (!LAUNCH_EVENTS.has(candidate.eventName)) continue; + const token = candidate.decodedPayload.token; + if ( + typeof token !== "string" || + !/^0x[0-9a-f]{40}$/u.test(token) || + requests.has(token) + ) { + throw validationError("envio", "projection-launch-token"); + } + requests.set( + token, + Object.freeze({ + token: token as `0x${string}`, + blockNumber: candidate.blockNumber, + blockHash: candidate.blockHash, + }), + ); + } + return Object.freeze([...requests.values()]); +} + +const REWARD_FACT_KINDS = new Set([ + "creator-fee-checkpoint", + "beneficiary-claim", + "payout-change", + "reward-configuration-activation", +]); + +function projectedRewardEvents( + fold: ProjectorFoldResult, +): readonly ProjectorRewardEvent[] { + if (fold.facts.length !== fold.occurrences.length) { + throw validationError("rpc", "reward-fold-pairs"); + } + return Object.freeze( + fold.facts.flatMap((fact, index) => { + if (!REWARD_FACT_KINDS.has(fact.kind)) return []; + const occurrence = fold.occurrences[index]; + if (!occurrence || fact.sourceCandidateId !== occurrence.candidateId) { + throw validationError("rpc", "reward-fold-pairs"); + } + const values = Object.freeze( + Object.fromEntries( + Object.entries(fact.values).map(([key, value]) => { + if ( + typeof value === "string" || + (Array.isArray(value) && + value.every((entry) => typeof entry === "string")) + ) { + return [key, value] as const; + } + throw validationError("rpc", "reward-fold-values"); + }), + ), + ); + return [Object.freeze({ + occurrenceId: projectorOccurrenceUuid({ + transactionHash: occurrence.transactionHash, + receiptLogOrdinal: occurrence.receiptLogOrdinal, + blockHash: occurrence.blockHash, + }), + vault: occurrence.sourceAddress, + blockNumber: occurrence.blockNumber, + transactionIndex: occurrence.transactionIndex, + blockGlobalLogIndex: occurrence.blockGlobalLogIndex, + kind: fact.kind as ProjectorRewardEvent["kind"], + values, + })]; + }), + ); +} + +/** + * Executes one release-scoped projection cycle. Every Envio/RPC request is + * made after `readProjectionPlan` has closed its transaction and before + * `commitVerifiedProjection` opens the final transaction. + */ +export async function runReleaseProjectionCycle(input: { + store: ReleaseProjectionStore; + envio: ProjectionEnvio; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + deadlineMs?: number; + verifyBatch?: typeof verifyEnvioCandidateBatchWithDualRpc; + readMetadata?: typeof readDualRpcTokenMetadata; + readRewardSnapshot?: typeof readDualRpcRewardSnapshot; +}) { + const deadlineMs = input.deadlineMs ?? MAXIMUM_PROJECTION_DEADLINE_MS; + if ( + !Number.isSafeInteger(deadlineMs) || + deadlineMs < 10 || + deadlineMs > MAXIMUM_PROJECTION_DEADLINE_MS + ) { + throw invalidInput("config", "projection-deadline"); + } + const startedAt = Date.now(); + const verifyBatch = + input.verifyBatch ?? verifyEnvioCandidateBatchWithDualRpc; + const readMetadata = input.readMetadata ?? readDualRpcTokenMetadata; + const readRewardSnapshot = + input.readRewardSnapshot ?? readDualRpcRewardSnapshot; + const remaining = () => { + const value = deadlineMs - (Date.now() - startedAt); + if (value < 10) throw projectionTimeout(); + return value; + }; + return withDeadline(deadlineMs, async () => { + const plan = await input.store.readProjectionPlan(); + if (plan === null) { + return Object.freeze({ status: "idle" as const }); + } + assertPlan(plan); + const freshCandidates = Object.freeze( + await Promise.all( + plan.entries.map(async ({ candidate }) => + exactCandidateMatch( + candidate, + await input.envio.readCandidate(candidate.candidateId), + ), + ), + ), + ); + const projectedCandidates = freshCandidates.filter( + (_candidate, index) => plan.entries[index]!.action === "project", + ); + const evidence = await verifyBatch({ + candidates: freshCandidates, + providers: input.providers, + dynamicSources: plan.dynamicSources, + // Irrelevant candidates may be dynamic sources from another release. + // They still receive fresh dual-RPC placement evidence, while the fold + // below separately requires exact attested lineage for projected rows. + requireDynamicLineage: false, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxCallsPerProvider: PROJECTOR_PROVIDER_CALL_BUDGET, + }, + maximumCandidateCount: + (plan.batchKind ?? "normal") === "normal" + ? PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE + : PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + }); + const metadata = await readMetadata({ + tokens: launchMetadataRequests(projectedCandidates), + providers: input.providers, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxCallsPerProvider: PROJECTOR_PROVIDER_CALL_BUDGET, + }, + }); + const tokenMetadata = Object.fromEntries( + metadata.map(({ token, name, symbol }) => [token, { name, symbol }]), + ); + const evidenceByCandidate = new Map( + evidence.candidates.map((candidate) => [candidate.candidateId, candidate]), + ); + const fold = foldProjectorEvents({ + events: projectedCandidates.map((candidate) => { + const candidateEvidence = evidenceByCandidate.get(candidate.candidateId); + if (!candidateEvidence) { + throw validationError("rpc", "projection-evidence-coverage"); + } + return { + candidate, + evidence: candidateEvidence, + releaseContext: { + model: + plan.scope.releaseId.startsWith("classic-") + ? "classic" + : "stock-paired", + releaseVersion: plan.scope.releaseId, + }, + }; + }), + tokenMetadata, + knownPools: plan.knownPools, + }); + const rewardEvents = projectedRewardEvents(fold); + const verifications = planRewardVerifications(plan); + const rewardEventsByVault = new Map(); + for (const event of rewardEvents) { + const current = rewardEventsByVault.get(event.vault) ?? []; + current.push(event); + rewardEventsByVault.set(event.vault, current); + } + if (verifications.length !== rewardEventsByVault.size) { + throw validationError("rpc", "reward-verification-missing"); + } + const target = plan.entries.at(-1)?.candidate; + if (!target) throw validationError("rpc", "reward-target-block"); + const rewardSnapshots: ProjectorRewardSnapshot[] = []; + const rewardEvidence: DualRpcRewardSnapshot[] = []; + for (const verification of verifications) { + const vaultEvents = rewardEventsByVault.get( + verification.baseline.vault, + ); + if (!vaultEvents || vaultEvents.length < 1) { + throw validationError("rpc", "reward-verification-empty"); + } + const snapshot = foldProjectorRewardState({ + model: verification.model, + baseline: verification.baseline, + events: vaultEvents, + }); + const verifiedSnapshot = await readRewardSnapshot({ + model: verification.model, + baseline: verification.baseline, + expected: snapshot, + blockNumber: target.blockNumber, + blockHash: target.blockHash, + providers: input.providers, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxAttempts: 1, + maxCallsPerProvider: PROJECTOR_PROVIDER_CALL_BUDGET, + }, + }); + rewardSnapshots.push(snapshot); + rewardEvidence.push(verifiedSnapshot); + } + const verified = Object.freeze({ + plan, + freshCandidates, + ignoredCandidateIds: Object.freeze( + plan.entries + .filter(({ action }) => action === "ignore") + .map(({ candidate }) => candidate.candidateId), + ), + evidence, + fold, + rewardSnapshot: rewardSnapshots[0] ?? null, + rewardSnapshots: Object.freeze(rewardSnapshots), + rewardEvidence: Object.freeze(rewardEvidence), + }); + const committed = await input.store.commitVerifiedProjection(verified); + return Object.freeze({ + status: "committed" as const, + releaseId: plan.scope.releaseId, + projectedCandidateCount: projectedCandidates.length, + ignoredCandidateCount: verified.ignoredCandidateIds.length, + checkpointGeneration: committed.checkpointGeneration, + batchKind: plan.batchKind ?? "normal", + }); + }); +} diff --git a/lib/data-pipeline/projector-provider-commitments.ts b/lib/data-pipeline/projector-provider-commitments.ts new file mode 100644 index 00000000..fd5a00b0 --- /dev/null +++ b/lib/data-pipeline/projector-provider-commitments.ts @@ -0,0 +1,202 @@ +import { keccak256, toBytes } from "viem"; + +import type { HexBytes32 } from "./codecs"; +import { invalidInput } from "./errors"; +import { providerEvidenceContractCommitment } from "./provider-evidence"; +import { PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1 } from "./projector-reward-rpc-contract"; +import type { DataPipelineReleaseBinding } from "./release-binding.server"; +import { rpcProviderCommitment } from "./rpc-provider-commitments"; + +const RPC_METHOD_CONTRACT_V1 = Object.freeze({ + version: 1, + chainId: 1, + transport: Object.freeze({ + protocol: "ethereum-json-rpc", + batch: Object.freeze({ + maximumSize: 100, + waitMs: 0, + methods: Object.freeze([ + "eth_getBlockByNumber", + "eth_getTransactionReceipt", + "eth_getCode", + "eth_getLogs", + ]), + }), + redirects: "error", + retryCount: 0, + timeoutMs: 5_000, + }), + maximumCallsPerProvider: 128, + methods: Object.freeze([ + Object.freeze(["eth_chainId"]), + Object.freeze(["eth_blockNumber"]), + Object.freeze([ + "eth_getBlockByNumber", + "number,transactions=false,bounded-json-rpc-batch<=100", + ]), + Object.freeze([ + "eth_getTransactionReceipt", + "transaction-hash,bounded-json-rpc-batch<=100", + ]), + Object.freeze([ + "eth_getCode", + "address,eip-1898-block-hash,require-canonical=true,bounded-json-rpc-batch<=100", + ]), + Object.freeze([ + "eth_call", + "erc20-name-or-symbol,eip-1898-block-hash,require-canonical=true", + ]), + Object.freeze([ + "eth_call", + "reward-vault-snapshot", + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1, + ]), + Object.freeze([ + "eth_getLogs", + "address-set<=512,topic0-or-set<=64,single-block,bounded-json-rpc-batch<=100", + ]), + ]), + acceptedEvidence: Object.freeze([ + "safe_head", + "block", + "runtime_code", + "dynamic_attestation", + "log_coverage", + "reward_vault_snapshot", + ]), +}); +const ALCHEMY_HOST = "eth-mainnet.g.alchemy.com"; +const ALCHEMY_API_PATH = /^\/v2\/[A-Za-z0-9_-]{8,256}$/u; +const QUICKNODE_API_PATH = /^\/[A-Za-z0-9_-]{8,256}\/?$/u; +const QUICKNODE_HOST = + /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+quiknode\.pro$/u; + +function invalidEndpoint(): never { + throw invalidInput("config", "projector-provider-endpoint"); +} + +export function canonicalProjectorRpcEndpoint( + value: unknown, + provider: "alchemy" | "quicknode", +): string { + if (typeof value !== "string" || value.length < 1 || value.length > 1_024) { + return invalidEndpoint(); + } + let parsed: URL; + try { + parsed = new URL(value); + } catch { + return invalidEndpoint(); + } + if ( + parsed.protocol !== "https:" || + parsed.username !== "" || + parsed.password !== "" || + parsed.port !== "" || + parsed.search !== "" || + parsed.hash !== "" || + ((provider === "alchemy" && + (parsed.hostname !== ALCHEMY_HOST || + !ALCHEMY_API_PATH.test(parsed.pathname))) || + (provider === "quicknode" && + (!QUICKNODE_HOST.test(parsed.hostname) || + !QUICKNODE_API_PATH.test(parsed.pathname)))) + ) { + return invalidEndpoint(); + } + const credential = provider === "alchemy" + ? parsed.pathname.slice("/v2/".length) + : parsed.pathname.replace(/^\//u, "").replace(/\/$/u, ""); + if (credential === "docs-demo") return invalidEndpoint(); + return parsed.toString(); +} + +export function canonicalProjectorEnvioEndpoint( + value: unknown, + reviewedEndpoint: string, +): string { + if ( + typeof value !== "string" || + typeof reviewedEndpoint !== "string" || + value !== reviewedEndpoint || + value.length < 1 || + value.length > 256 || + !/^https:\/\/indexer\.hyperindex\.xyz\/[a-z0-9]{7,64}\/v1\/graphql$/u.test( + value, + ) + ) { + return invalidEndpoint(); + } + let parsed: URL; + try { + parsed = new URL(value); + } catch { + return invalidEndpoint(); + } + if ( + parsed.protocol !== "https:" || + parsed.username !== "" || + parsed.password !== "" || + parsed.hostname === "" || + parsed.search !== "" || + parsed.hash !== "" || + parsed.toString() !== value + ) { + return invalidEndpoint(); + } + return value; +} + +function commitment(domain: string, value: unknown): HexBytes32 { + return keccak256(toBytes(`${domain}\0${JSON.stringify(value)}`)); +} + +export function projectorRpcSchemaCommitment(): HexBytes32 { + return commitment("programmable:projector-rpc-schema:v1", [ + RPC_METHOD_CONTRACT_V1, + providerEvidenceContractCommitment(), + ]); +} + +export function projectorEnvioDeploymentCommitment(input: Readonly<{ + endpoint: string; + redactedIdentity: string; + binding: DataPipelineReleaseBinding; +}>): HexBytes32 { + return commitment("programmable:projector-envio-deployment:v1", [ + input.endpoint, + input.redactedIdentity, + input.binding.chainId, + input.binding.startBlock, + input.binding.confirmations, + input.binding.envio.deploymentLabel, + input.binding.envio.graphqlEndpoint, + input.binding.envio.sourceCommit, + input.binding.envio.configSha256, + input.binding.envio.handlerSha256, + input.binding.envio.sourceRegistrySha256, + input.binding.envio.eventSetSha256, + input.binding.envio.eventCount, + ]); +} + +export function projectorEnvioSchemaCommitment( + binding: DataPipelineReleaseBinding, +): HexBytes32 { + return commitment("programmable:projector-envio-schema:v1", [ + binding.chainId, + binding.envio.schemaVersion, + binding.envio.configSha256, + binding.envio.schemaSha256, + binding.envio.handlerSha256, + binding.envio.sourceRegistrySha256, + binding.envio.eventSetSha256, + binding.envio.eventCount, + ]); +} + +export function projectorRpcDeploymentCommitment( + canonicalEndpoint: string, +): HexBytes32 { + return rpcProviderCommitment("endpoint", canonicalEndpoint); +} diff --git a/lib/data-pipeline/projector-reorg.ts b/lib/data-pipeline/projector-reorg.ts new file mode 100644 index 00000000..36a7e757 --- /dev/null +++ b/lib/data-pipeline/projector-reorg.ts @@ -0,0 +1,745 @@ +import "server-only"; + +import type { + CandidateRpcBlock, + CandidateRpcProvider, +} from "./dual-rpc"; +import { + canonicalBytes32, + parseNonnegativeIntegerText, + type HexBytes32, +} from "./codecs"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import { assertProductionDualRpcProviders } from "./rpc-providers.server"; + +const PROVIDER_IDENTITY_PATTERN = /^[a-z0-9][a-z0-9:-]{0,63}$/u; +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; +const CANDIDATE_ID_PATTERN = + /^1:(0x[0-9a-f]{64}):(0x[0-9a-f]{64}):(0|[1-9]\d*)$/u; +const UINT32_MAXIMUM = 4_294_967_295; +const POSTGRES_BIGINT_MAXIMUM = 9_223_372_036_854_775_807n; +// This is not a caller preference. Safe-head persistence enforces the +// same Ethereum mainnet finality depth in migrations 002/007/008. +const FINALITY_DEPTH = 12n; +const DEFAULT_MAXIMUM_DEPTH = 64; +const MAXIMUM_DEPTH = 128; +const DEFAULT_MAXIMUM_PROVIDER_CALLS = 68; +const MAXIMUM_PROVIDER_CALLS = 128; +const DEFAULT_MAXIMUM_ATTEMPTS = 2; +const DEFAULT_DEADLINE_MS = 75_000; + +export type ReorgHistoryAncestor = Readonly<{ + kind: "history"; + historyGeneration: string; + blockNumber: string; + blockHash: HexBytes32; + blockGlobalLogIndex: number | null; + candidateId: string | null; +}>; + +export type ReorgGenesisAnchor = Readonly<{ + kind: "genesis"; + historyGeneration: "0"; + genesisPointId: string; + blockNumber: string; + blockHash: HexBytes32; + blockGlobalLogIndex: null; + candidateId: null; +}>; + +export type CanonicalReorgTarget = Readonly<{ + kind: "history" | "genesis"; + historyGeneration: string; + blockNumber: string; + blockHash: HexBytes32; + blockGlobalLogIndex: number | null; + candidateId: string | null; + genesisPointId: string | null; + providerIdentities: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + providerBlockHashes: readonly [HexBytes32, HexBytes32]; + providerBlockTimestamps: readonly [string, string]; + providerChainIds: readonly [1, 1]; + providerHeads: readonly [string, string]; + finalityDepth: "12"; + safeBlockNumber: string; + safeBlockHash: HexBytes32; + providerSafeBlockHashes: readonly [HexBytes32, HexBytes32]; + checkedDepth: number; +}>; + +export type EnvioCursorRecoveryPlan = Readonly<{ + action: "rewind-and-replay"; + expectedGeneration: string; + nextGeneration: string; + targetHistoryGeneration: string; + targetBlockNumber: string; + targetBlockHash: HexBytes32; + targetBlockGlobalLogIndex: number | null; + targetCandidateId: string | null; + genesisPointId: string | null; + expectedReorgGeneration: string; + nextReorgGeneration: string; + providerIdentities: readonly [string, string]; + providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + providerBlockHashes: readonly [HexBytes32, HexBytes32]; + providerBlockTimestamps: readonly [string, string]; + providerChainIds: readonly [1, 1]; + providerHeads: readonly [string, string]; + finalityDepth: "12"; + safeBlockNumber: string; + safeBlockHash: HexBytes32; + providerSafeBlockHashes: readonly [HexBytes32, HexBytes32]; + checkedDepth: number; +}>; + +type ReorgSearchPolicyInput = Readonly<{ + maximumDepth?: number; + maxProviderCalls?: number; + maxAttempts?: number; + deadlineMs?: number; +}>; + +type CanonicalSearchTarget = Readonly<{ + kind: "history" | "genesis"; + historyGeneration: string; + blockNumber: string; + blockHash: HexBytes32; + blockGlobalLogIndex: number | null; + candidateId: string | null; + genesisPointId: string | null; +}>; + +function timeoutError() { + return dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); +} + +function dependencyError() { + return dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); +} + +function canonicalInteger(value: unknown, operation: string): string { + try { + return parseNonnegativeIntegerText(value); + } catch { + throw invalidInput("rpc", operation); + } +} + +function canonicalPostgresBigint( + value: unknown, + operation: string, + allowZero = true, +): string { + const canonical = canonicalInteger(value, operation); + if ( + (!allowZero && canonical === "0") || + BigInt(canonical) > POSTGRES_BIGINT_MAXIMUM + ) { + throw invalidInput("rpc", operation); + } + return canonical; +} + +function canonicalProviderIdentity(value: unknown, operation: string): string { + if (typeof value !== "string" || !PROVIDER_IDENTITY_PATTERN.test(value)) { + throw invalidInput("rpc", operation); + } + return value; +} + +function canonicalHistoryAncestor( + value: ReorgHistoryAncestor, +): CanonicalSearchTarget { + if (value === null || typeof value !== "object" || value.kind !== "history") { + throw invalidInput("rpc", "reorg-ancestor"); + } + const historyGeneration = canonicalPostgresBigint( + value.historyGeneration, + "reorg-history-generation", + false, + ); + const blockNumber = canonicalPostgresBigint( + value.blockNumber, + "reorg-block-number", + ); + let blockHash: HexBytes32; + try { + blockHash = canonicalBytes32(value.blockHash); + } catch { + throw invalidInput("rpc", "reorg-block-hash"); + } + if ( + (value.blockGlobalLogIndex === null) !== (value.candidateId === null) || + (value.blockGlobalLogIndex !== null && + (!Number.isSafeInteger(value.blockGlobalLogIndex) || + value.blockGlobalLogIndex < 0 || + value.blockGlobalLogIndex > UINT32_MAXIMUM)) || + (value.candidateId !== null && typeof value.candidateId !== "string") + ) { + throw invalidInput("rpc", "reorg-cursor"); + } + if (value.candidateId !== null) { + const candidate = CANDIDATE_ID_PATTERN.exec(value.candidateId); + if ( + candidate === null || + candidate[1] !== blockHash || + Number(candidate[3]) !== value.blockGlobalLogIndex + ) { + throw invalidInput("rpc", "reorg-candidate-id"); + } + } + return Object.freeze({ + kind: "history", + historyGeneration, + blockNumber, + blockHash, + blockGlobalLogIndex: value.blockGlobalLogIndex, + candidateId: value.candidateId, + genesisPointId: null, + }); +} + +function canonicalGenesisAnchor( + value: ReorgGenesisAnchor, +): CanonicalSearchTarget { + if ( + value === null || + typeof value !== "object" || + value.kind !== "genesis" || + value.historyGeneration !== "0" || + typeof value.genesisPointId !== "string" || + !UUID_PATTERN.test(value.genesisPointId) || + value.blockGlobalLogIndex !== null || + value.candidateId !== null + ) { + throw invalidInput("rpc", "reorg-genesis"); + } + const blockNumber = canonicalPostgresBigint( + value.blockNumber, + "reorg-genesis-block-number", + ); + let blockHash: HexBytes32; + try { + blockHash = canonicalBytes32(value.blockHash); + } catch { + throw invalidInput("rpc", "reorg-genesis-block-hash"); + } + return Object.freeze({ + kind: "genesis", + historyGeneration: "0", + blockNumber, + blockHash, + blockGlobalLogIndex: null, + candidateId: null, + genesisPointId: value.genesisPointId, + }); +} + +function canonicalTargets(input: { + ancestors: readonly ReorgHistoryAncestor[]; + genesis?: ReorgGenesisAnchor; + maximumDepth: number; +}): readonly CanonicalSearchTarget[] { + if (!Array.isArray(input.ancestors)) { + throw invalidInput("rpc", "reorg-ancestors"); + } + const targets = input.ancestors.map(canonicalHistoryAncestor); + for (let index = 1; index < targets.length; index += 1) { + if ( + BigInt(targets[index - 1]!.historyGeneration) <= + BigInt(targets[index]!.historyGeneration) + ) { + throw invalidInput("rpc", "reorg-ancestor-order"); + } + } + if (input.genesis !== undefined) { + const genesis = canonicalGenesisAnchor(input.genesis); + if ( + targets.some( + (target) => BigInt(target.blockNumber) < BigInt(genesis.blockNumber), + ) + ) { + throw invalidInput("rpc", "reorg-genesis-order"); + } + targets.push(genesis); + } + if (targets.length < 1 || targets.length > input.maximumDepth) { + throw invalidInput("rpc", "reorg-depth"); + } + return Object.freeze(targets); +} + +function searchPolicy(input: ReorgSearchPolicyInput | undefined) { + const maximumDepth = input?.maximumDepth ?? DEFAULT_MAXIMUM_DEPTH; + const maxProviderCalls = + input?.maxProviderCalls ?? DEFAULT_MAXIMUM_PROVIDER_CALLS; + const maxAttempts = input?.maxAttempts ?? DEFAULT_MAXIMUM_ATTEMPTS; + const deadlineMs = input?.deadlineMs ?? DEFAULT_DEADLINE_MS; + if ( + !Number.isSafeInteger(maximumDepth) || + maximumDepth < 1 || + maximumDepth > MAXIMUM_DEPTH || + !Number.isSafeInteger(maxProviderCalls) || + maxProviderCalls < 1 || + maxProviderCalls > MAXIMUM_PROVIDER_CALLS || + !Number.isSafeInteger(maxAttempts) || + maxAttempts < 1 || + maxAttempts > 3 || + !Number.isSafeInteger(deadlineMs) || + deadlineMs < 10 || + deadlineMs > DEFAULT_DEADLINE_MS + ) { + throw invalidInput("rpc", "reorg-policy"); + } + return Object.freeze({ + maximumDepth, + maxProviderCalls, + maxAttempts, + deadlineAt: Date.now() + deadlineMs, + }); +} + +async function withinDeadline( + deadlineAt: number, + operation: () => Promise, +): Promise { + const remaining = deadlineAt - Date.now(); + if (remaining <= 0) throw timeoutError(); + let timeout: ReturnType | undefined; + try { + return await Promise.race([ + operation(), + new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(timeoutError()), remaining); + }), + ]); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +function canonicalProviderBlock( + value: CandidateRpcBlock, + expectedNumber: bigint, +): Readonly<{ hash: HexBytes32; timestamp: string }> { + if ( + value === null || + typeof value !== "object" || + value.number !== expectedNumber || + value.hash === null || + typeof value.timestamp !== "bigint" || + value.timestamp < 0n + ) { + throw validationError("rpc", "reorg-block"); + } + let hash: HexBytes32; + try { + hash = canonicalBytes32(value.hash); + } catch { + throw validationError("rpc", "reorg-block"); + } + return Object.freeze({ hash, timestamp: value.timestamp.toString() }); +} + +function immutablePair(first: T, second: T): readonly [T, T] { + return Object.freeze([first, second]) as readonly [T, T]; +} + +function exactPair(value: unknown, operation: string): readonly [unknown, unknown] { + if (!Array.isArray(value) || value.length !== 2) { + throw invalidInput("rpc", operation); + } + return immutablePair(value[0], value[1]); +} + +function canonicalRecoveryTarget( + value: CanonicalReorgTarget, +): CanonicalReorgTarget { + if (value === null || typeof value !== "object") { + throw invalidInput("rpc", "reorg-target"); + } + let base: CanonicalSearchTarget; + if (value.kind === "history") { + base = canonicalHistoryAncestor({ + kind: "history", + historyGeneration: value.historyGeneration, + blockNumber: value.blockNumber, + blockHash: value.blockHash, + blockGlobalLogIndex: value.blockGlobalLogIndex, + candidateId: value.candidateId, + }); + } else if (value.kind === "genesis") { + base = canonicalGenesisAnchor({ + kind: "genesis", + historyGeneration: value.historyGeneration as "0", + genesisPointId: value.genesisPointId as string, + blockNumber: value.blockNumber, + blockHash: value.blockHash, + blockGlobalLogIndex: value.blockGlobalLogIndex as null, + candidateId: value.candidateId as null, + }); + } else { + throw invalidInput("rpc", "reorg-target-kind"); + } + const identities = exactPair( + value.providerIdentities, + "reorg-target-providers", + ).map((identity) => + canonicalProviderIdentity(identity, "reorg-target-provider"), + ); + const endpoints = exactPair( + value.providerEndpointCommitments, + "reorg-target-endpoints", + ).map((commitment) => canonicalBytes32(commitment)); + const origins = exactPair( + value.providerOriginCommitments, + "reorg-target-origins", + ).map((commitment) => canonicalBytes32(commitment)); + const blockHashes = exactPair( + value.providerBlockHashes, + "reorg-target-block-hashes", + ).map((hash) => canonicalBytes32(hash)); + const timestamps = exactPair( + value.providerBlockTimestamps, + "reorg-target-block-timestamps", + ).map((timestamp) => + canonicalInteger(timestamp, "reorg-target-block-timestamp"), + ); + const chainIds = exactPair( + value.providerChainIds, + "reorg-target-chain-ids", + ); + const heads = exactPair( + value.providerHeads, + "reorg-target-heads", + ).map((head) => canonicalInteger(head, "reorg-target-head")); + const safeBlockNumber = canonicalInteger( + value.safeBlockNumber, + "reorg-target-safe-block", + ); + const safeBlockHash = canonicalBytes32(value.safeBlockHash); + const safeHashes = exactPair( + value.providerSafeBlockHashes, + "reorg-target-safe-hashes", + ).map((hash) => canonicalBytes32(hash)); + if ( + identities[0] === identities[1] || + endpoints[0] === endpoints[1] || + origins[0] === origins[1] || + blockHashes[0] !== base.blockHash || + blockHashes[1] !== base.blockHash || + timestamps[0] !== timestamps[1] || + chainIds[0] !== 1 || + chainIds[1] !== 1 || + value.finalityDepth !== "12" || + BigInt(heads[0]!) < FINALITY_DEPTH || + BigInt(heads[1]!) < FINALITY_DEPTH || + BigInt(heads[0]!) > POSTGRES_BIGINT_MAXIMUM || + BigInt(heads[1]!) > POSTGRES_BIGINT_MAXIMUM || + BigInt(safeBlockNumber) !== + (BigInt(heads[0]!) < BigInt(heads[1]!) + ? BigInt(heads[0]!) + : BigInt(heads[1]!)) - + FINALITY_DEPTH || + BigInt(base.blockNumber) > BigInt(safeBlockNumber) || + safeHashes[0] !== safeBlockHash || + safeHashes[1] !== safeBlockHash || + !Number.isSafeInteger(value.checkedDepth) || + value.checkedDepth < 1 || + value.checkedDepth > MAXIMUM_DEPTH + ) { + throw invalidInput("rpc", "reorg-target-evidence"); + } + return Object.freeze({ + ...base, + providerIdentities: immutablePair(identities[0]!, identities[1]!), + providerEndpointCommitments: immutablePair(endpoints[0]!, endpoints[1]!), + providerOriginCommitments: immutablePair(origins[0]!, origins[1]!), + providerBlockHashes: immutablePair(blockHashes[0]!, blockHashes[1]!), + providerBlockTimestamps: immutablePair(timestamps[0]!, timestamps[1]!), + providerChainIds: immutablePair(1 as const, 1 as const), + providerHeads: immutablePair(heads[0]!, heads[1]!), + finalityDepth: "12", + safeBlockNumber, + safeBlockHash, + providerSafeBlockHashes: immutablePair(safeHashes[0]!, safeHashes[1]!), + checkedDepth: value.checkedDepth, + }); +} + +export async function findCanonicalAncestorWithDualRpc(input: { + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + ancestors: readonly ReorgHistoryAncestor[]; + genesis?: ReorgGenesisAnchor; + policy?: ReorgSearchPolicyInput; +}): Promise { + assertProductionDualRpcProviders(input.providers); + const policy = searchPolicy(input.policy); + const targets = canonicalTargets({ + ancestors: input.ancestors, + genesis: input.genesis, + maximumDepth: policy.maximumDepth, + }); + const providerIdentities = immutablePair( + canonicalProviderIdentity(input.providers[0]?.identity, "reorg-provider"), + canonicalProviderIdentity(input.providers[1]?.identity, "reorg-provider"), + ); + const providerVendorGroups = immutablePair( + canonicalProviderIdentity( + input.providers[0]?.vendorGroup, + "reorg-provider-vendor", + ), + canonicalProviderIdentity( + input.providers[1]?.vendorGroup, + "reorg-provider-vendor", + ), + ); + let providerEndpointCommitments: readonly [HexBytes32, HexBytes32]; + let providerOriginCommitments: readonly [HexBytes32, HexBytes32]; + try { + providerEndpointCommitments = immutablePair( + canonicalBytes32(input.providers[0].endpointCommitment), + canonicalBytes32(input.providers[1].endpointCommitment), + ); + providerOriginCommitments = immutablePair( + canonicalBytes32(input.providers[0].endpointOriginCommitment), + canonicalBytes32(input.providers[1].endpointOriginCommitment), + ); + } catch { + throw invalidInput("rpc", "reorg-provider-commitment"); + } + if ( + providerIdentities[0] === providerIdentities[1] || + providerVendorGroups[0] === providerVendorGroups[1] || + input.providers[0].client === input.providers[1].client || + providerEndpointCommitments[0] === providerEndpointCommitments[1] || + providerOriginCommitments[0] === providerOriginCommitments[1] + ) { + throw invalidInput("rpc", "reorg-provider-independence"); + } + + const providerCallCounts = [0, 0]; + const callProvider = async ( + providerIndex: 0 | 1, + operation: () => Promise, + ): Promise => { + let lastError: unknown; + for (let attempt = 0; attempt < policy.maxAttempts; attempt += 1) { + if (providerCallCounts[providerIndex]! >= policy.maxProviderCalls) { + throw validationError("rpc", "reorg-provider-call-budget"); + } + providerCallCounts[providerIndex] += 1; + try { + return await withinDeadline(policy.deadlineAt, operation); + } catch (error) { + lastError = error; + if (error instanceof DataPipelineError && error.code === "timeout") { + throw error; + } + } + } + if (lastError instanceof DataPipelineError) throw lastError; + throw dependencyError(); + }; + + const paired = async ( + operation: (provider: CandidateRpcProvider) => Promise, + ): Promise => { + const results = await Promise.all([ + callProvider(0, () => operation(input.providers[0])), + callProvider(1, () => operation(input.providers[1])), + ]); + return immutablePair(results[0], results[1]); + }; + + const [chainIds, rawHeads] = await Promise.all([ + paired((provider) => provider.client.getChainId()), + paired((provider) => provider.client.getBlockNumber()), + ]); + if (chainIds[0] !== 1 || chainIds[1] !== 1) { + throw validationError("rpc", "reorg-chain-id"); + } + if ( + typeof rawHeads[0] !== "bigint" || + typeof rawHeads[1] !== "bigint" || + rawHeads[0] < FINALITY_DEPTH || + rawHeads[1] < FINALITY_DEPTH || + rawHeads[0] > POSTGRES_BIGINT_MAXIMUM || + rawHeads[1] > POSTGRES_BIGINT_MAXIMUM + ) { + throw validationError("rpc", "reorg-provider-head"); + } + const safeBlockNumber = + (rawHeads[0] < rawHeads[1] ? rawHeads[0] : rawHeads[1]) - FINALITY_DEPTH; + const rawSafeBlocks = await paired((provider) => + provider.client.getBlock({ blockNumber: safeBlockNumber }), + ); + const firstSafe = canonicalProviderBlock( + rawSafeBlocks[0], + safeBlockNumber, + ); + const secondSafe = canonicalProviderBlock( + rawSafeBlocks[1], + safeBlockNumber, + ); + if ( + firstSafe.hash !== secondSafe.hash || + firstSafe.timestamp !== secondSafe.timestamp + ) { + throw validationError("rpc", "reorg-safe-head-disagreement"); + } + + let checkedDepth = 0; + for (const target of targets) { + checkedDepth += 1; + const blockNumber = BigInt(target.blockNumber); + if (blockNumber > safeBlockNumber) continue; + const blocks = await paired((provider) => + provider.client.getBlock({ blockNumber }), + ); + const first = canonicalProviderBlock(blocks[0], blockNumber); + const second = canonicalProviderBlock(blocks[1], blockNumber); + if (first.hash !== second.hash || first.timestamp !== second.timestamp) { + throw validationError("rpc", "reorg-provider-disagreement"); + } + if (first.hash !== target.blockHash) continue; + + // Pin the safe head for the complete ancestor search. A reorg can happen + // after the initial safe-block sample but before the matching history + // target is read; returning that temporally mixed proof would make the + // database recovery decision depend on two different canonical views. + const finalRawSafeBlocks = await paired((provider) => + provider.client.getBlock({ blockNumber: safeBlockNumber }), + ); + const finalFirstSafe = canonicalProviderBlock( + finalRawSafeBlocks[0], + safeBlockNumber, + ); + const finalSecondSafe = canonicalProviderBlock( + finalRawSafeBlocks[1], + safeBlockNumber, + ); + if ( + finalFirstSafe.hash !== finalSecondSafe.hash || + finalFirstSafe.timestamp !== finalSecondSafe.timestamp || + finalFirstSafe.hash !== firstSafe.hash || + finalFirstSafe.timestamp !== firstSafe.timestamp + ) { + throw validationError("rpc", "reorg-safe-head-changed"); + } + + return Object.freeze({ + ...target, + providerIdentities, + providerEndpointCommitments, + providerOriginCommitments, + providerBlockHashes: immutablePair(first.hash, second.hash), + providerBlockTimestamps: immutablePair( + first.timestamp, + second.timestamp, + ), + providerChainIds: immutablePair(1 as const, 1 as const), + providerHeads: immutablePair( + rawHeads[0].toString(), + rawHeads[1].toString(), + ), + finalityDepth: "12", + safeBlockNumber: safeBlockNumber.toString(), + safeBlockHash: firstSafe.hash, + providerSafeBlockHashes: immutablePair( + firstSafe.hash, + secondSafe.hash, + ), + checkedDepth, + }); + } + + throw validationError("rpc", "reorg-no-canonical-ancestor"); +} + +export function buildEnvioCursorRecoveryPlan(input: { + expectedGeneration: string; + currentReorgGeneration: string; + target: CanonicalReorgTarget; +}): EnvioCursorRecoveryPlan { + const target = canonicalRecoveryTarget(input.target); + const expectedGeneration = canonicalPostgresBigint( + input.expectedGeneration, + "reorg-expected-generation", + false, + ); + const expectedReorgGeneration = canonicalPostgresBigint( + input.currentReorgGeneration, + "reorg-generation", + ); + const targetGeneration = canonicalPostgresBigint( + target.historyGeneration, + "reorg-target-generation", + ); + if ( + BigInt(targetGeneration) >= BigInt(expectedGeneration) || + BigInt(expectedGeneration) === POSTGRES_BIGINT_MAXIMUM || + BigInt(expectedReorgGeneration) === POSTGRES_BIGINT_MAXIMUM + ) { + throw invalidInput("rpc", "reorg-target-generation"); + } + if ( + (target.kind === "genesis" && + (targetGeneration !== "0" || + target.blockGlobalLogIndex !== null || + target.candidateId !== null || + target.genesisPointId === null)) || + (target.kind === "history" && + (targetGeneration === "0" || + (target.blockGlobalLogIndex === null) !== + (target.candidateId === null) || + target.genesisPointId !== null)) + ) { + throw invalidInput("rpc", "reorg-target-shape"); + } + + return Object.freeze({ + action: "rewind-and-replay", + expectedGeneration, + nextGeneration: (BigInt(expectedGeneration) + 1n).toString(), + targetHistoryGeneration: targetGeneration, + targetBlockNumber: target.blockNumber, + targetBlockHash: target.blockHash, + targetBlockGlobalLogIndex: target.blockGlobalLogIndex, + targetCandidateId: target.candidateId, + genesisPointId: target.genesisPointId, + expectedReorgGeneration, + nextReorgGeneration: (BigInt(expectedReorgGeneration) + 1n).toString(), + providerIdentities: target.providerIdentities, + providerEndpointCommitments: target.providerEndpointCommitments, + providerOriginCommitments: target.providerOriginCommitments, + providerBlockHashes: target.providerBlockHashes, + providerBlockTimestamps: target.providerBlockTimestamps, + providerChainIds: target.providerChainIds, + providerHeads: target.providerHeads, + finalityDepth: target.finalityDepth, + safeBlockNumber: target.safeBlockNumber, + safeBlockHash: target.safeBlockHash, + providerSafeBlockHashes: target.providerSafeBlockHashes, + checkedDepth: target.checkedDepth, + }); +} diff --git a/lib/data-pipeline/projector-reward-fold.ts b/lib/data-pipeline/projector-reward-fold.ts new file mode 100644 index 00000000..60093003 --- /dev/null +++ b/lib/data-pipeline/projector-reward-fold.ts @@ -0,0 +1,558 @@ +import "server-only"; + +import { + canonicalAddress, + canonicalBytes32, + parseNonnegativeIntegerText, + parseUint256Text, + type HexAddress, + type HexBytes32, +} from "./codecs"; + +const BASIS_POINTS = 10_000n; +const MAX_UINT256 = (1n << 256n) - 1n; +const MAX_UINT64 = (1n << 64n) - 1n; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; + +export type ProjectorRewardModel = + | "classic-v3" + | "stock-paired"; + +export type ProjectorRewardAllocation = Readonly<{ + allocationIndex: number; + beneficiary: HexAddress; + payoutAddress: HexAddress; + shareBps: string; +}>; + +export type ProjectorRewardBalance = Readonly<{ + account: HexAddress; + payoutAddress: HexAddress; + claimableAccrued: string; + claimedTotal: string; +}>; + +export type ProjectorRewardBaseline = Readonly<{ + vault: HexAddress; + poolId: HexBytes32; + configurationEpoch: string; + activeConfigurationHash: HexBytes32 | null; + allocations: readonly ProjectorRewardAllocation[]; + balances: readonly ProjectorRewardBalance[]; +}>; + +export type ProjectorRewardEvent = Readonly<{ + occurrenceId: string; + vault: HexAddress; + blockNumber: string; + transactionIndex: string; + blockGlobalLogIndex: string; + kind: + | "creator-fee-checkpoint" + | "beneficiary-claim" + | "payout-change" + | "reward-configuration-activation"; + values: Readonly>; +}>; + +export type ProjectorRewardSnapshot = Readonly<{ + vault: HexAddress; + poolId: HexBytes32; + configurationEpoch: string; + activeConfigurationHash: HexBytes32 | null; + totalCreatorFeesReceived: string; + allocations: readonly ProjectorRewardAllocation[]; + balances: readonly ProjectorRewardBalance[]; + snapshotSourceOccurrenceId: string; +}>; + +type MutableAllocation = { + allocationIndex: number; + beneficiary: HexAddress; + payoutAddress: HexAddress; + shareBps: bigint; +}; + +type MutableBalance = { + account: HexAddress; + payoutAddress: HexAddress; + claimableAccrued: bigint; + claimedTotal: bigint; +}; + +function rejected(reason: string): never { + throw new TypeError(`Projector reward fold rejected ${reason}`); +} + +function exactUuid(value: unknown): string { + if (typeof value !== "string" || !UUID.test(value)) return rejected("occurrence id"); + return value; +} + +function uint(value: unknown): bigint { + try { + return BigInt(parseUint256Text(value)); + } catch { + return rejected("uint256 value"); + } +} + +function uint64(value: unknown): bigint { + let parsed: bigint; + try { + parsed = BigInt(parseNonnegativeIntegerText(value, 20)); + } catch { + return rejected("uint64 value"); + } + if (parsed > MAX_UINT64) return rejected("uint64 value"); + return parsed; +} + +function index(value: unknown): number { + let parsed: bigint; + try { + parsed = BigInt(parseNonnegativeIntegerText(value, 3)); + } catch { + return rejected("allocation index"); + } + if (parsed > 7n) return rejected("allocation index"); + return Number(parsed); +} + +function stringValue( + values: Readonly>, + key: string, +): string { + const value = values[key]; + if (typeof value !== "string") return rejected(`missing ${key}`); + return value; +} + +function arrayValue( + values: Readonly>, + key: string, +): readonly string[] { + const value = values[key]; + if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { + return rejected(`missing ${key}`); + } + return value; +} + +function checkedAdd(left: bigint, right: bigint): bigint { + const sum = left + right; + if (sum > MAX_UINT256) return rejected("uint256 overflow"); + return sum; +} + +function validateAllocations( + model: ProjectorRewardModel, + allocations: readonly ProjectorRewardAllocation[], +): MutableAllocation[] { + const maximum = model === "classic-v3" ? 5 : 8; + if (allocations.length < 1 || allocations.length > maximum) { + return rejected("allocation count"); + } + const normalized = [...allocations] + .map((allocation) => { + const allocationIndex = index(String(allocation.allocationIndex)); + const shareBps = uint(allocation.shareBps); + if (shareBps < 1n || shareBps > BASIS_POINTS) { + return rejected("allocation share"); + } + return { + allocationIndex, + beneficiary: canonicalAddress(allocation.beneficiary), + payoutAddress: canonicalAddress(allocation.payoutAddress), + shareBps, + }; + }) + .sort((left, right) => left.allocationIndex - right.allocationIndex); + if ( + normalized.some((allocation, allocationIndex) => + allocation.allocationIndex !== allocationIndex + ) || + normalized.reduce((sum, allocation) => sum + allocation.shareBps, 0n) !== + BASIS_POINTS + ) { + return rejected("allocation set"); + } + if ( + model !== "classic-v3" && + new Set(normalized.map(({ beneficiary }) => beneficiary)).size !== + normalized.length + ) { + return rejected("immutable beneficiary uniqueness"); + } + if ( + model === "classic-v3" && + normalized.some( + ({ beneficiary, payoutAddress }) => beneficiary !== payoutAddress, + ) + ) { + return rejected("classic payout identity"); + } + return normalized; +} + +function validateBalances( + model: ProjectorRewardModel, + allocations: readonly MutableAllocation[], + balances: readonly ProjectorRewardBalance[], +): Map { + const normalized = new Map(); + for (const balance of balances) { + const account = canonicalAddress(balance.account); + if (normalized.has(account)) return rejected("duplicate balance"); + const payoutAddress = canonicalAddress(balance.payoutAddress); + if (model === "classic-v3" && payoutAddress !== account) { + return rejected("classic balance payout"); + } + normalized.set(account, { + account, + payoutAddress, + claimableAccrued: uint(balance.claimableAccrued), + claimedTotal: uint(balance.claimedTotal), + }); + } + for (const allocation of allocations) { + const balance = normalized.get(allocation.beneficiary); + if (!balance || balance.payoutAddress !== allocation.payoutAddress) { + return rejected("active beneficiary balance"); + } + } + if ( + model !== "classic-v3" && + [...normalized.keys()].some( + (account) => !allocations.some(({ beneficiary }) => beneficiary === account), + ) + ) { + return rejected("historical immutable beneficiary"); + } + return normalized; +} + +function totalReceived(balances: ReadonlyMap): bigint { + let total = 0n; + for (const balance of balances.values()) { + total = checkedAdd(total, balance.claimableAccrued); + total = checkedAdd(total, balance.claimedTotal); + } + return total; +} + +function entitlements( + amount: bigint, + allocations: readonly MutableAllocation[], +): readonly bigint[] { + let allocated = 0n; + return allocations.map((allocation, allocationIndex) => { + if (allocationIndex === allocations.length - 1) return amount - allocated; + const share = (amount * allocation.shareBps) / BASIS_POINTS; + allocated += share; + return share; + }); +} + +function ensureBalance( + balances: Map, + model: ProjectorRewardModel, + account: HexAddress, + payoutAddress: HexAddress, +): MutableBalance { + const current = balances.get(account); + if (current) { + if (model !== "classic-v3") current.payoutAddress = payoutAddress; + return current; + } + const created = { + account, + payoutAddress: model === "classic-v3" ? account : payoutAddress, + claimableAccrued: 0n, + claimedTotal: 0n, + }; + balances.set(account, created); + return created; +} + +function recomputeCumulativeBalances( + allocations: readonly MutableAllocation[], + balances: Map, + received: bigint, + mode: "assert" | "update", +) { + const entitlement = entitlements(received, allocations); + for (let allocationIndex = 0; allocationIndex < allocations.length; allocationIndex += 1) { + const allocation = allocations[allocationIndex]!; + const balance = ensureBalance( + balances, + "stock-paired", + allocation.beneficiary, + allocation.payoutAddress, + ); + const allocationEntitlement = entitlement[allocationIndex]!; + if (balance.claimedTotal > allocationEntitlement) { + return rejected("cumulative claimed total exceeds entitlement"); + } + const expectedClaimable = allocationEntitlement - balance.claimedTotal; + if (mode === "assert" && balance.claimableAccrued !== expectedClaimable) { + return rejected("cumulative baseline entitlement"); + } + balance.claimableAccrued = expectedClaimable; + } +} + +function validateEventOrder(events: readonly ProjectorRewardEvent[]) { + const occurrences = new Set(); + let previous: readonly [bigint, bigint, bigint] | null = null; + for (const event of events) { + const occurrenceId = exactUuid(event.occurrenceId); + if (occurrences.has(occurrenceId)) return rejected("duplicate event"); + occurrences.add(occurrenceId); + const key = [ + BigInt(parseNonnegativeIntegerText(event.blockNumber)), + BigInt(parseNonnegativeIntegerText(event.transactionIndex)), + BigInt(parseNonnegativeIntegerText(event.blockGlobalLogIndex)), + ] as const; + if ( + previous && + (key[0] < previous[0] || + (key[0] === previous[0] && key[1] < previous[1]) || + (key[0] === previous[0] && + key[1] === previous[1] && + key[2] <= previous[2])) + ) { + return rejected("event order"); + } + previous = key; + } +} + +export function foldProjectorRewardState(input: Readonly<{ + model: ProjectorRewardModel; + baseline: ProjectorRewardBaseline; + events: readonly ProjectorRewardEvent[]; +}>): ProjectorRewardSnapshot { + if (input.events.length < 1) return rejected("empty event set"); + const vault = canonicalAddress(input.baseline.vault); + const poolId = canonicalBytes32(input.baseline.poolId); + let configurationEpoch = uint64(input.baseline.configurationEpoch); + let activeConfigurationHash = + input.baseline.activeConfigurationHash === null + ? null + : canonicalBytes32(input.baseline.activeConfigurationHash); + if (input.model === "classic-v3" && activeConfigurationHash === null) { + return rejected("classic configuration hash"); + } + let allocations = validateAllocations(input.model, input.baseline.allocations); + const balances = validateBalances( + input.model, + allocations, + input.baseline.balances, + ); + let received = totalReceived(balances); + if (input.model !== "classic-v3") { + recomputeCumulativeBalances(allocations, balances, received, "assert"); + } + validateEventOrder(input.events); + + for (const event of input.events) { + if (canonicalAddress(event.vault) !== vault) return rejected("vault mismatch"); + const values = event.values; + if (event.kind === "creator-fee-checkpoint") { + if (input.model !== "classic-v3") return rejected("cumulative checkpoint"); + if (canonicalBytes32(stringValue(values, "poolId")) !== poolId) { + return rejected("checkpoint pool"); + } + if (uint64(stringValue(values, "configurationEpoch")) !== configurationEpoch) { + return rejected("checkpoint epoch"); + } + const amount = uint(stringValue(values, "amount")); + if (amount === 0n) return rejected("empty checkpoint"); + const expectedTotal = checkedAdd(received, amount); + if (uint(stringValue(values, "totalCreatorFeesReceived")) !== expectedTotal) { + return rejected("checkpoint total"); + } + const allocationAmounts = entitlements(amount, allocations); + allocations.forEach((allocation, allocationIndex) => { + const balance = ensureBalance( + balances, + "classic-v3", + allocation.beneficiary, + allocation.beneficiary, + ); + balance.claimableAccrued = checkedAdd( + balance.claimableAccrued, + allocationAmounts[allocationIndex]!, + ); + }); + received = expectedTotal; + continue; + } + + if (event.kind === "beneficiary-claim") { + const beneficiary = canonicalAddress(stringValue(values, "beneficiary")); + const balance = balances.get(beneficiary); + if (!balance) return rejected("unknown beneficiary claim"); + const vaultTotalReceived = uint(stringValue(values, "vaultTotalReceived")); + if (input.model !== "classic-v3") { + if (vaultTotalReceived < received) return rejected("cumulative received total regression"); + received = vaultTotalReceived; + recomputeCumulativeBalances(allocations, balances, received, "update"); + } else if (vaultTotalReceived !== received) { + return rejected("classic received total mismatch"); + } + const amount = uint(stringValue(values, "amount")); + const expectedClaimed = checkedAdd(balance.claimedTotal, amount); + if ( + amount === 0n || + balance.claimableAccrued !== amount || + uint(stringValue(values, "beneficiaryTotalClaimed")) !== expectedClaimed + ) { + return rejected("claim amount"); + } + if (typeof values.payoutAddress === "string") { + const payoutAddress = canonicalAddress(values.payoutAddress); + if (input.model === "classic-v3" || payoutAddress !== balance.payoutAddress) { + return rejected("claim payout"); + } + } + balance.claimableAccrued = 0n; + balance.claimedTotal = expectedClaimed; + continue; + } + + if (event.kind === "payout-change") { + if (input.model === "classic-v3") { + if (canonicalBytes32(stringValue(values, "poolId")) !== poolId) { + return rejected("payout pool"); + } + const allocationIndex = index(stringValue(values, "allocationIndex")); + const allocation = allocations[allocationIndex]; + if (!allocation) return rejected("payout allocation"); + const previous = canonicalAddress(stringValue(values, "previousPayoutWallet")); + const next = canonicalAddress(stringValue(values, "newPayoutWallet")); + const nextEpoch = uint64(stringValue(values, "configurationEpoch")); + if ( + previous !== allocation.beneficiary || + previous === next || + uint(stringValue(values, "shareBps")) !== allocation.shareBps || + nextEpoch !== configurationEpoch + 1n || + uint(stringValue(values, "effectiveTotalCreatorFeesReceived")) !== received + ) { + return rejected("classic payout transition"); + } + allocation.beneficiary = next; + allocation.payoutAddress = next; + ensureBalance(balances, "classic-v3", next, next); + configurationEpoch = nextEpoch; + activeConfigurationHash = canonicalBytes32( + stringValue(values, "activeConfigurationHash"), + ); + } else { + const beneficiary = canonicalAddress(stringValue(values, "beneficiary")); + const allocation = allocations.find( + (candidate) => candidate.beneficiary === beneficiary, + ); + const balance = balances.get(beneficiary); + const previous = canonicalAddress(stringValue(values, "previousPayoutAddress")); + const next = canonicalAddress(stringValue(values, "newPayoutAddress")); + if ( + !allocation || + !balance || + previous !== allocation.payoutAddress || + previous !== balance.payoutAddress || + previous === next + ) { + return rejected("immutable payout transition"); + } + allocation.payoutAddress = next; + balance.payoutAddress = next; + } + continue; + } + + if (input.model !== "classic-v3" || activeConfigurationHash === null) { + return rejected("immutable configuration activation"); + } + if (canonicalBytes32(stringValue(values, "poolId")) !== poolId) { + return rejected("activation pool"); + } + const nextEpoch = uint64(stringValue(values, "configurationEpoch")); + const previousHash = canonicalBytes32( + stringValue(values, "previousConfigurationHash"), + ); + const nextHash = canonicalBytes32(stringValue(values, "newConfigurationHash")); + const beneficiaries = arrayValue(values, "beneficiaries"); + const shares = arrayValue(values, "sharesBps"); + if ( + previousHash !== activeConfigurationHash || + nextEpoch !== configurationEpoch + 1n || + uint(stringValue(values, "effectiveTotalCreatorFeesReceived")) !== received || + beneficiaries.length !== shares.length + ) { + return rejected("configuration transition"); + } + allocations = validateAllocations( + "classic-v3", + beneficiaries.map((beneficiary, allocationIndex) => ({ + allocationIndex, + beneficiary: canonicalAddress(beneficiary), + payoutAddress: canonicalAddress(beneficiary), + shareBps: shares[allocationIndex]!, + })), + ); + if ( + new Set(allocations.map(({ beneficiary }) => beneficiary)).size !== + allocations.length + ) { + return rejected("activation beneficiary uniqueness"); + } + allocations.forEach(({ beneficiary }) => + ensureBalance(balances, "classic-v3", beneficiary, beneficiary), + ); + configurationEpoch = nextEpoch; + activeConfigurationHash = nextHash; + } + + const activeAccounts = new Set(allocations.map(({ beneficiary }) => beneficiary)); + const finalBalances = [...balances.values()] + .filter( + (balance) => + activeAccounts.has(balance.account) || + balance.claimableAccrued > 0n || + balance.claimedTotal > 0n, + ) + .sort((left, right) => left.account.localeCompare(right.account)); + if (totalReceived(new Map(finalBalances.map((balance) => [balance.account, balance]))) !== received) { + return rejected("reward conservation"); + } + return Object.freeze({ + vault, + poolId, + configurationEpoch: configurationEpoch.toString(), + activeConfigurationHash, + totalCreatorFeesReceived: received.toString(), + allocations: Object.freeze( + allocations.map((allocation) => + Object.freeze({ + allocationIndex: allocation.allocationIndex, + beneficiary: allocation.beneficiary, + payoutAddress: allocation.payoutAddress, + shareBps: allocation.shareBps.toString(), + }), + ), + ), + balances: Object.freeze( + finalBalances.map((balance) => + Object.freeze({ + account: balance.account, + payoutAddress: balance.payoutAddress, + claimableAccrued: balance.claimableAccrued.toString(), + claimedTotal: balance.claimedTotal.toString(), + }), + ), + ), + snapshotSourceOccurrenceId: exactUuid(input.events.at(-1)!.occurrenceId), + }); +} diff --git a/lib/data-pipeline/projector-reward-rpc-contract.ts b/lib/data-pipeline/projector-reward-rpc-contract.ts new file mode 100644 index 00000000..6a20059d --- /dev/null +++ b/lib/data-pipeline/projector-reward-rpc-contract.ts @@ -0,0 +1,79 @@ +import { toFunctionSelector } from "viem"; + +const call = ( + signature: string, + argumentShape: "none" | "allocation-index" | "account", +) => Object.freeze({ + signature, + selector: toFunctionSelector(signature), + argumentShape, + blockTag: "eip-1898-canonical-block-hash" as const, +}); + +/** + * Frozen, reviewable JSON-RPC surface for reward-vault snapshots. Runtime + * clients consume these exact function shapes and the provider schema + * commitment includes this object, so adding another onchain read necessarily + * changes the reviewed commitment. + */ +export const PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1 = Object.freeze({ + version: 1, + transportMethod: "eth_call", + retryCount: 0, + models: Object.freeze({ + "classic-v3": Object.freeze({ + maximumAllocations: 5, + maximumBalanceAccounts: 48, + fixed: Object.freeze([ + call("poolId()", "none"), + call("configurationEpoch()", "none"), + call("activeConfigurationHash()", "none"), + call("totalCreatorFeesReceived()", "none"), + call("totalCreatorFeesClaimed()", "none"), + call("beneficiaryCount()", "none"), + ]), + perAllocation: Object.freeze([ + call("beneficiaryAt(uint256)", "allocation-index"), + call("shareBpsAt(uint256)", "allocation-index"), + ]), + perBalanceAccount: Object.freeze([ + call("claimable(address)", "account"), + call("claimedBy(address)", "account"), + ]), + }), + "stock-paired": Object.freeze({ + maximumAllocations: 8, + maximumBalanceAccounts: 8, + fixed: Object.freeze([ + call("poolId()", "none"), + call("configurationHash()", "none"), + call("totalCreatorFeesReceived()", "none"), + call("totalCreatorFeesClaimed()", "none"), + call("beneficiaryCount()", "none"), + ]), + perAllocation: Object.freeze([ + call("beneficiaryAt(uint256)", "allocation-index"), + call("shareBpsOf(address)", "account"), + call("payoutAddressOf(address)", "account"), + ]), + perBalanceAccount: Object.freeze([ + call("claimable(address)", "account"), + call("claimedBy(address)", "account"), + ]), + }), + }), +}); + +export type ProjectorRewardRpcModel = + keyof typeof PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1.models; + +export function expectedRewardRpcCallCount( + model: ProjectorRewardRpcModel, + allocationCount: number, + balanceAccountCount: number, +): number { + const contract = PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1.models[model]; + return contract.fixed.length + + contract.perAllocation.length * allocationCount + + contract.perBalanceAccount.length * balanceAccountCount; +} diff --git a/lib/data-pipeline/projector-runtime-config.server.ts b/lib/data-pipeline/projector-runtime-config.server.ts new file mode 100644 index 00000000..19967b88 --- /dev/null +++ b/lib/data-pipeline/projector-runtime-config.server.ts @@ -0,0 +1,798 @@ +import "server-only"; + +import { + assertCandidateDatabaseBootstrapState, + assertCandidateDatabasePromotedState, + selectProjectorRuntimeBinding, + type ProjectorRuntimeBindingSelection, +} from "./candidate-projector-runtime-binding.server"; +import { createEnvioClient } from "./envio"; +import { invalidInput } from "./errors"; +import { createPostgresExecutor } from "./postgres"; +import { + createPostgresReleaseProjectionStore, + createPostgresProjectorStore, + type ProjectorProviderDatabaseBinding, + type ProjectorReleaseDatabaseScope, +} from "./postgres-projector"; +import { runProjectorCycle } from "./projector"; +import { runReleaseProjectionCycle } from "./projector-projection"; +import { createProjectorRuntimeLeaseController } from "./projector-runtime-lease.server"; +import { + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE, + PROJECTOR_PREFERRED_CANDIDATES_PER_COMMIT, + PROJECTOR_MAXIMUM_RUNTIME_ROUNDS, +} from "./projector-runtime-limits"; +import { + getDataPipelineReleaseBinding, + type DataPipelineReleaseBinding, +} from "./release-binding.server"; +import { + canonicalProjectorEnvioEndpoint, + projectorEnvioDeploymentCommitment, + projectorEnvioSchemaCommitment, + projectorRpcSchemaCommitment, +} from "./projector-provider-commitments"; +import { + assertProductionDualRpcProviders, + createProductionDualRpcProviders, + productionRpcProjectorCommitments, +} from "./rpc-providers.server"; +import { + validatedPostgresConnectionString, + validatedPostgresSslCa, +} from "./postgres-connection.server"; + +type Environment = Readonly>; + +const PROJECTOR_DEADLINE_MS = 75_000; +const INGESTION_DEADLINE_MS = 60_000; +const RELEASE_PROJECTION_DEADLINE_MS = 10_000; +const PROJECTOR_CLOSE_RESERVE_MS = 5_000; +const MINIMUM_OPERATION_DEADLINE_MS = 250; +const EXACT_RELEASE_SCOPES = Object.freeze([ + Object.freeze({ releaseId: "classic-v2", modelId: "classic", sourceGroup: "core" }), + Object.freeze({ releaseId: "classic-v3", modelId: "classic", sourceGroup: "core" }), + Object.freeze({ releaseId: "stock-paired-v1", modelId: "stock-paired", sourceGroup: "core" }), + Object.freeze({ releaseId: "stock-paired-v2", modelId: "stock-paired", sourceGroup: "core" }), + Object.freeze({ releaseId: "stock-paired-v3", modelId: "stock-paired", sourceGroup: "core" }), +] satisfies readonly ProjectorReleaseDatabaseScope[]); +const BROWSER_FORBIDDEN_NAMES = Object.freeze([ + "NEXT_PUBLIC_PROGRAMMABLE_PROJECTOR_ACTIVE", + "NEXT_PUBLIC_PROGRAMMABLE_PROJECTOR_DATABASE_URL", + "NEXT_PUBLIC_PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_URL", + "NEXT_PUBLIC_PROGRAMMABLE_POSTGRES_SSL_CA_PEM", + "NEXT_PUBLIC_PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN", + "NEXT_PUBLIC_PROGRAMMABLE_PROJECTOR_BINDING_MODE", + "NEXT_PUBLIC_PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT", +] as const); + +type StagedDynamicParentResult = Readonly<{ + status: "staged-dynamic-parent"; + candidateCount: number; + snapshotBlock: string; +}>; + +function parseStagedDynamicParentResult( + value: unknown, +): StagedDynamicParentResult | null { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return null; + } + const candidate = value as Record; + if (candidate.status !== "staged-dynamic-parent") return null; + if ( + typeof candidate.candidateCount !== "number" || + !Number.isSafeInteger(candidate.candidateCount) || + candidate.candidateCount < 1 || + candidate.candidateCount > PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + typeof candidate.snapshotBlock !== "string" || + !/^(?:0|[1-9]\d*)$/u.test(candidate.snapshotBlock) || + candidate.snapshotBlock.length > 78 || + Object.keys(candidate).length !== 3 + ) { + return invalidRuntimeConfig(); + } + return Object.freeze({ + status: "staged-dynamic-parent" as const, + candidateCount: candidate.candidateCount, + snapshotBlock: candidate.snapshotBlock, + }); +} + +function invalidRuntimeConfig(): never { + throw invalidInput("config", "projector-runtime-config"); +} + +export function projectorRuntimeActivationState( + env: Environment = process.env, +): "active" | "disabled" { + const value = env.PROGRAMMABLE_PROJECTOR_ACTIVE; + if (value === undefined || value === "" || value === "false") { + return "disabled"; + } + if (value === "true") return "active"; + return invalidRuntimeConfig(); +} + +function requiredText( + value: unknown, + pattern: RegExp, + maximum: number, +): string { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > maximum || + /[\u0000-\u001f\u007f]/u.test(value) || + !pattern.test(value) + ) { + return invalidRuntimeConfig(); + } + return value; +} + +function optionalSecret(value: unknown): string | undefined { + if (value === undefined || value === "") return undefined; + return requiredText(value, /^[^\s]+$/u, 2_048); +} + +function releaseScopes( + binding: DataPipelineReleaseBinding, +): readonly ProjectorReleaseDatabaseScope[] { + if ( + binding.releases.length !== EXACT_RELEASE_SCOPES.length || + binding.releases.some((release, index) => { + const expected = EXACT_RELEASE_SCOPES[index]!; + return ( + release.releaseVersion !== expected.releaseId || + release.model !== expected.modelId + ); + }) + ) { + return invalidRuntimeConfig(); + } + return EXACT_RELEASE_SCOPES; +} + +export type ProjectorRuntimeConfig = Readonly<{ + binding: ProjectorRuntimeBindingSelection; + database: Readonly<{ + projectorConnectionString: string; + runtimeConnectionString: string; + sslCaPem: string; + }>; + envio: Readonly<{ + endpoint: string; + token?: string; + releaseBinding: DataPipelineReleaseBinding; + }>; + providers: readonly ProjectorProviderDatabaseBinding[]; + releaseScopes: readonly ProjectorReleaseDatabaseScope[]; +}>; + +export function assertProjectorRuntimeProviderCommitments( + bindings: readonly ProjectorProviderDatabaseBinding[], + providers: ReturnType, +): void { + if ( + bindings.length !== 3 || + providers.length !== 2 || + bindings[1]?.type !== "rpc_provider" || + bindings[2]?.type !== "rpc_provider" || + providers[0].vendorGroup !== "alchemy" || + providers[1].vendorGroup !== "quicknode" || + providers[0].endpointCommitment !== bindings[1].deploymentCommitment || + providers[1].endpointCommitment !== bindings[2].deploymentCommitment || + bindings[1].schemaCommitment !== projectorRpcSchemaCommitment() || + bindings[2].schemaCommitment !== projectorRpcSchemaCommitment() + ) { + return invalidRuntimeConfig(); + } +} + +export function loadProjectorRuntimeConfigForBinding( + env: Environment, + canonicalBinding: DataPipelineReleaseBinding, +): ProjectorRuntimeConfig { + if (BROWSER_FORBIDDEN_NAMES.some((name) => env[name])) { + return invalidRuntimeConfig(); + } + const selection = selectProjectorRuntimeBinding({ + env, + canonicalBinding, + }); + const binding = selection.releaseBinding; + const envioEndpoint = canonicalProjectorEnvioEndpoint( + env.PROGRAMMABLE_ENVIO_GRAPHQL_URL, + binding.envio.graphqlEndpoint, + ); + const envioIdentity = requiredText( + env.PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY, + /^[a-z0-9][a-z0-9._:/-]{0,127}$/u, + 128, + ); + if (envioIdentity !== `envio:${binding.envio.deploymentLabel}`) { + return invalidRuntimeConfig(); + } + const rpcCommitments = productionRpcProjectorCommitments(env); + const derivedCommitments = Object.freeze({ + envioDeployment: projectorEnvioDeploymentCommitment({ + endpoint: envioEndpoint, + redactedIdentity: envioIdentity, + binding, + }), + envioSchema: projectorEnvioSchemaCommitment(binding), + alchemyDeployment: rpcCommitments.alchemy.deploymentCommitment, + alchemySchema: rpcCommitments.alchemy.schemaCommitment, + quicknodeDeployment: rpcCommitments.quicknode.deploymentCommitment, + quicknodeSchema: rpcCommitments.quicknode.schemaCommitment, + }); + const providers = Object.freeze([ + Object.freeze({ + type: "envio_deployment" as const, + redactedIdentity: envioIdentity, + deploymentCommitment: derivedCommitments.envioDeployment, + schemaCommitment: derivedCommitments.envioSchema, + }), + Object.freeze({ + type: "rpc_provider" as const, + redactedIdentity: "rpc:1:alchemy", + deploymentCommitment: derivedCommitments.alchemyDeployment, + schemaCommitment: derivedCommitments.alchemySchema, + }), + Object.freeze({ + type: "rpc_provider" as const, + redactedIdentity: "rpc:1:quicknode", + deploymentCommitment: derivedCommitments.quicknodeDeployment, + schemaCommitment: derivedCommitments.quicknodeSchema, + }), + ] satisfies readonly ProjectorProviderDatabaseBinding[]); + + return Object.freeze({ + binding: selection, + database: Object.freeze({ + projectorConnectionString: validatedPostgresConnectionString( + env.PROGRAMMABLE_PROJECTOR_DATABASE_URL, + ), + runtimeConnectionString: validatedPostgresConnectionString( + env.PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_URL, + ), + sslCaPem: validatedPostgresSslCa( + env.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + ), + }), + envio: Object.freeze({ + endpoint: envioEndpoint, + token: optionalSecret(env.PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN), + releaseBinding: binding, + }), + providers, + releaseScopes: releaseScopes(binding), + }); +} + +export function loadProjectorRuntimeConfig( + env: Environment = process.env, +): ProjectorRuntimeConfig { + return loadProjectorRuntimeConfigForBinding( + env, + getDataPipelineReleaseBinding(), + ); +} + +export type ProjectorRuntimeDependencies = Readonly<{ + createExecutor: typeof createPostgresExecutor; + createLeaseController: typeof createProjectorRuntimeLeaseController; + createProviders: typeof createProductionDualRpcProviders; + assertProviders: typeof assertProductionDualRpcProviders; + createEnvio: typeof createEnvioClient; + createStore: typeof createPostgresProjectorStore; + createReleaseStore: typeof createPostgresReleaseProjectionStore; + runCycle: typeof runProjectorCycle; + runReleaseCycle: typeof runReleaseProjectionCycle; + assertCandidateDatabase: typeof assertCandidateDatabaseBootstrapState; + assertPromotedDatabase: typeof assertCandidateDatabasePromotedState; + loadConfig?: typeof loadProjectorRuntimeConfig; +}>; + +const DEFAULT_DEPENDENCIES: ProjectorRuntimeDependencies = Object.freeze({ + createExecutor: createPostgresExecutor, + createLeaseController: createProjectorRuntimeLeaseController, + createProviders: createProductionDualRpcProviders, + assertProviders: assertProductionDualRpcProviders, + createEnvio: createEnvioClient, + createStore: createPostgresProjectorStore, + createReleaseStore: createPostgresReleaseProjectionStore, + runCycle: runProjectorCycle, + runReleaseCycle: runReleaseProjectionCycle, + assertCandidateDatabase: assertCandidateDatabaseBootstrapState, + assertPromotedDatabase: assertCandidateDatabasePromotedState, +}); + +export async function runConfiguredProjectorCycle( + input: Readonly<{ + env?: Environment; + dependencies?: ProjectorRuntimeDependencies; + ingestionOnly?: boolean; + preferredCandidatesPerCommit?: number; + }> = {}, +) { + const env = input.env ?? process.env; + const dependencies = input.dependencies ?? DEFAULT_DEPENDENCIES; + const ingestionOnly = input.ingestionOnly === true; + const preferredCandidatesPerCommit = + input.preferredCandidatesPerCommit ?? + PROJECTOR_PREFERRED_CANDIDATES_PER_COMMIT; + if ( + input.ingestionOnly !== undefined && + typeof input.ingestionOnly !== "boolean" || + !Number.isSafeInteger(preferredCandidatesPerCommit) || + preferredCandidatesPerCommit < + PROJECTOR_PREFERRED_CANDIDATES_PER_COMMIT || + preferredCandidatesPerCommit > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + preferredCandidatesPerCommit % + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE !== 0 || + (!ingestionOnly && + input.preferredCandidatesPerCommit !== undefined) + ) { + return invalidRuntimeConfig(); + } + if (projectorRuntimeActivationState(env) === "disabled") { + return Object.freeze({ + ok: true as const, + status: "disabled" as const, + readiness: Object.freeze({ + status: "disabled" as const, + activationReady: false as const, + lagging: true as const, + }), + }); + } + const config = dependencies.loadConfig + ? dependencies.loadConfig(env) + : loadProjectorRuntimeConfig(env); + const runtimeExecutor = dependencies.createExecutor({ + connectionString: config.database.runtimeConnectionString, + sslCaPem: config.database.sslCaPem, + maxConnections: 1, + connectTimeoutMs: 2_000, + idleTimeoutMs: 60_000, + }); + const leaseController = dependencies.createLeaseController({ + executor: runtimeExecutor, + }); + let executor: ReturnType | null = null; + let acquiredFence: Awaited< + ReturnType + >["fence"]; + try { + const acquisition = await leaseController.tryAcquire(); + if (acquisition.status === "busy") { + return Object.freeze({ + ok: true as const, + status: "busy" as const, + readiness: Object.freeze({ + status: "busy" as const, + activationReady: false as const, + lagging: true as const, + }), + }); + } + if (!acquisition.fence) return invalidRuntimeConfig(); + const runtimeFence = acquisition.fence; + acquiredFence = runtimeFence; + const writerExecutor = dependencies.createExecutor({ + connectionString: config.database.projectorConnectionString, + sslCaPem: config.database.sslCaPem, + maxConnections: 1, + connectTimeoutMs: 2_000, + idleTimeoutMs: 60_000, + }); + executor = writerExecutor; + if (config.binding.candidate) { + await dependencies.assertCandidateDatabase({ + executor: writerExecutor, + binding: config.binding.candidate, + }); + } else if (config.binding.promotedDatabase) { + await dependencies.assertPromotedDatabase({ + executor: writerExecutor, + binding: config.binding.promotedDatabase, + }); + } + const providers = dependencies.createProviders(env); + dependencies.assertProviders(providers); + assertProjectorRuntimeProviderCommitments(config.providers, providers); + const envio = dependencies.createEnvio(config.envio); + const startedAt = Date.now(); + const remainingRuntimeMs = () => + PROJECTOR_DEADLINE_MS - (Date.now() - startedAt); + const operationDeadline = (operationsRemainingInRound: number) => { + if ( + !Number.isSafeInteger(operationsRemainingInRound) || + operationsRemainingInRound < 1 + ) { + return invalidRuntimeConfig(); + } + const available = remainingRuntimeMs() - PROJECTOR_CLOSE_RESERVE_MS; + const fairShare = Math.floor(available / operationsRemainingInRound); + if (fairShare < MINIMUM_OPERATION_DEADLINE_MS) return null; + return Math.min( + RELEASE_PROJECTION_DEADLINE_MS, + fairShare, + ); + }; + const ingestionOperationDeadline = () => { + const available = remainingRuntimeMs() - PROJECTOR_CLOSE_RESERVE_MS; + if (available < MINIMUM_OPERATION_DEADLINE_MS) return null; + return Math.min(INGESTION_DEADLINE_MS, available); + }; + const ingestionStore = dependencies.createStore({ + executor: writerExecutor, + providers: config.providers, + releaseScopes: config.releaseScopes, + runtimeFence, + }); + const releaseStores = config.releaseScopes.map((scope) => + Object.freeze({ + scope, + store: dependencies.createReleaseStore({ + executor: writerExecutor, + providers: config.providers, + rpcEvidenceBindings: [ + { + identity: providers[0].identity, + vendorGroup: providers[0].vendorGroup, + endpointCommitment: providers[0].endpointCommitment, + endpointOriginCommitment: + providers[0].endpointOriginCommitment, + }, + { + identity: providers[1].identity, + vendorGroup: providers[1].vendorGroup, + endpointCommitment: providers[1].endpointCommitment, + endpointOriginCommitment: + providers[1].endpointOriginCommitment, + }, + ] as const, + scope, + runtimeFence, + }), + }), + ); + const ingestionState: { + failed: boolean; + committed: boolean; + committedEmpty: boolean; + stagedDynamicParent: boolean; + candidateCount: number; + pageCount: number; + snapshotBlock: string | null; + generation: string | null; + processedAtomicGroup: boolean; + } = { + failed: false, + committed: false, + committedEmpty: false, + stagedDynamicParent: false, + candidateCount: 0, + pageCount: 0, + snapshotBlock: null, + generation: null, + processedAtomicGroup: false, + }; + const projectionStates = config.releaseScopes.map((scope) => ({ + releaseId: scope.releaseId, + failed: false, + committed: false, + projectedCandidateCount: 0, + ignoredCandidateCount: 0, + pageCount: 0, + checkpointGeneration: null as string | null, + processedAtomicGroup: false, + })); + let madeAnyProgress = false; + let terminalSweepComplete = false; + let completedRounds = 0; + let stoppedForDeadline = false; + + for ( + let round = 0; + round < PROJECTOR_MAXIMUM_RUNTIME_ROUNDS; + round += 1 + ) { + const operationCount = ingestionOnly ? 1 : 1 + releaseStores.length; + if (operationDeadline(operationCount) === null) { + stoppedForDeadline = true; + break; + } + let madeProgress = false; + let operationsRemaining = operationCount; + let ingestionIdle = false; + let stagedDynamicParent = false; + let stopAfterIngestionFailure = false; + let idleProjectionCount = 0; + + const ingestionDeadline = ingestionOperationDeadline(); + if (ingestionDeadline === null) { + stoppedForDeadline = true; + break; + } + let observedIngestionStatus: unknown = null; + try { + const result = await dependencies.runCycle({ + store: ingestionStore, + envio, + providers, + deadlineMs: ingestionDeadline, + preferredCandidatesPerCommit, + }); + observedIngestionStatus = result.status; + const stagedResult = parseStagedDynamicParentResult(result); + if (stagedResult) { + ingestionState.pageCount += 1; + ingestionState.snapshotBlock = stagedResult.snapshotBlock; + ingestionState.candidateCount += stagedResult.candidateCount; + ingestionState.stagedDynamicParent = true; + stagedDynamicParent = true; + madeProgress = true; + } else { + if ( + !Number.isSafeInteger(result.candidateCount) || + result.candidateCount < 0 || + result.candidateCount > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + ) { + return invalidRuntimeConfig(); + } + ingestionState.pageCount += 1; + ingestionState.snapshotBlock = result.snapshotBlock; + ingestionState.candidateCount += result.candidateCount; + ingestionState.processedAtomicGroup ||= + result.candidateCount > PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE; + if (result.status === "committed") { + ingestionState.committed = true; + ingestionState.generation = result.generation; + madeProgress = true; + } else if (result.status === "committed-empty") { + ingestionState.committedEmpty = true; + ingestionState.generation = result.generation; + madeProgress = true; + } else { + ingestionIdle = true; + } + } + } catch { + ingestionState.failed = true; + stopAfterIngestionFailure = + observedIngestionStatus === "staged-dynamic-parent"; + } + operationsRemaining -= 1; + + if (stagedDynamicParent || stopAfterIngestionFailure) { + // The parent and its child must be replayed from the unchanged source + // cursor on the next invocation. Running release projections here + // would materialize against a cursor/checkpoint that did not advance. + completedRounds += 1; + if (stagedDynamicParent) madeAnyProgress = true; + terminalSweepComplete = false; + break; + } + + if (ingestionOnly) { + completedRounds += 1; + if (madeProgress) madeAnyProgress = true; + terminalSweepComplete = false; + break; + } + + for (let index = 0; index < releaseStores.length; index += 1) { + const binding = releaseStores[index]!; + const state = projectionStates[index]!; + if (state.processedAtomicGroup) { + // An atomic group is intentionally the last unit processed for this + // release in one invocation. It is deferred, not proven idle, so it + // must keep the terminal sweep and activation readiness false. + operationsRemaining -= 1; + continue; + } + const deadline = operationDeadline(operationsRemaining); + if (deadline === null) { + stoppedForDeadline = true; + break; + } + try { + const result = await dependencies.runReleaseCycle({ + store: binding.store, + envio, + providers, + deadlineMs: deadline, + }); + const projectedCandidateCount = + result.status === "committed" + ? result.projectedCandidateCount + : 0; + const ignoredCandidateCount = + result.status === "committed" + ? result.ignoredCandidateCount + : 0; + const maximumResultCount = + result.status === "committed" && + (result.batchKind ?? "normal") !== "normal" + ? PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + : PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE; + if ( + !Number.isSafeInteger(projectedCandidateCount) || + projectedCandidateCount < 0 || + !Number.isSafeInteger(ignoredCandidateCount) || + ignoredCandidateCount < 0 || + projectedCandidateCount + ignoredCandidateCount > + maximumResultCount + ) { + return invalidRuntimeConfig(); + } + state.pageCount += 1; + if (result.status === "committed") { + state.committed = true; + state.projectedCandidateCount += result.projectedCandidateCount; + state.ignoredCandidateCount += result.ignoredCandidateCount; + state.checkpointGeneration = result.checkpointGeneration; + state.processedAtomicGroup = + (result.batchKind ?? "normal") !== "normal"; + madeProgress = true; + } else { + idleProjectionCount += 1; + } + } catch { + state.failed = true; + } + operationsRemaining -= 1; + } + + if (stoppedForDeadline) break; + completedRounds += 1; + terminalSweepComplete = + ingestionIdle && idleProjectionCount === releaseStores.length; + if (madeProgress) madeAnyProgress = true; + + if (terminalSweepComplete || !madeProgress) break; + if ( + ingestionState.failed || + projectionStates.some(({ failed }) => failed) + ) { + break; + } + if (ingestionState.processedAtomicGroup) { + // One oversized exact block is the final ingestion unit for this + // invocation. Remaining normal pages are deferred to the next lease. + terminalSweepComplete = false; + break; + } + } + + if ( + !ingestionState.failed && + (ingestionState.pageCount < 1 || + ingestionState.snapshotBlock === null || + ((ingestionState.committed || ingestionState.committedEmpty) && + ingestionState.generation === null)) + ) { + ingestionState.failed = true; + } + for (const state of projectionStates) { + if (state.committed && state.checkpointGeneration === null) { + state.failed = true; + } + } + + const ingestion = ingestionState.failed + ? Object.freeze({ status: "failed" as const }) + : ingestionState.stagedDynamicParent + ? Object.freeze({ + status: "staged-dynamic-parent" as const, + candidateCount: ingestionState.candidateCount, + pageCount: ingestionState.pageCount, + snapshotBlock: ingestionState.snapshotBlock, + atomicGroupCount: 1 as const, + }) + : ingestionState.committed + ? Object.freeze({ + status: "committed" as const, + candidateCount: ingestionState.candidateCount, + pageCount: ingestionState.pageCount, + snapshotBlock: ingestionState.snapshotBlock, + generation: ingestionState.generation, + ...(ingestionState.processedAtomicGroup + ? { atomicGroupCount: 1 as const } + : {}), + }) + : ingestionState.committedEmpty + ? Object.freeze({ + status: "committed-empty" as const, + candidateCount: 0, + pageCount: ingestionState.pageCount, + snapshotBlock: ingestionState.snapshotBlock, + generation: ingestionState.generation, + }) + : Object.freeze({ + status: "idle" as const, + candidateCount: 0, + pageCount: ingestionState.pageCount, + snapshotBlock: ingestionState.snapshotBlock, + }); + const projections = projectionStates.map((state) => { + if (state.failed) { + return Object.freeze({ + releaseId: state.releaseId, + status: "failed" as const, + }); + } + if (!state.committed) { + if ( + (ingestionOnly || ingestionState.stagedDynamicParent) && + state.pageCount === 0 + ) { + return Object.freeze({ + releaseId: state.releaseId, + status: "deferred" as const, + pageCount: 0, + }); + } + return Object.freeze({ + releaseId: state.releaseId, + status: "idle" as const, + pageCount: state.pageCount, + }); + } + return Object.freeze({ + releaseId: state.releaseId, + status: "committed" as const, + projectedCandidateCount: state.projectedCandidateCount, + ignoredCandidateCount: state.ignoredCandidateCount, + pageCount: state.pageCount, + checkpointGeneration: state.checkpointGeneration, + ...(state.processedAtomicGroup ? { atomicGroupCount: 1 } : {}), + }); + }); + const failed = + ingestion.status === "failed" || + projections.some(({ status }) => status === "failed"); + const readinessStatus = failed + ? "incomplete" as const + : terminalSweepComplete + ? "caught-up" as const + : madeAnyProgress + ? "progressed" as const + : "incomplete" as const; + const readiness = Object.freeze({ + status: readinessStatus, + activationReady: readinessStatus === "caught-up", + lagging: readinessStatus !== "caught-up", + terminalSweepComplete, + stoppedForDeadline, + completedRounds, + snapshotBlock: ingestionState.snapshotBlock, + }); + return Object.freeze({ + ok: !failed, + ingestion, + projections: Object.freeze(projections), + readiness, + deadlineMs: PROJECTOR_DEADLINE_MS, + }); + } finally { + if (acquiredFence) { + try { + await leaseController.release(acquiredFence); + } catch { + // The database lease expires independently. Every writer transaction is + // fenced, so release remains a best-effort latency optimization. + } + } + if (executor) await executor.close(); + await runtimeExecutor.close(); + } +} diff --git a/lib/data-pipeline/projector-runtime-lease.server.ts b/lib/data-pipeline/projector-runtime-lease.server.ts new file mode 100644 index 00000000..19952511 --- /dev/null +++ b/lib/data-pipeline/projector-runtime-lease.server.ts @@ -0,0 +1,228 @@ +import "server-only"; + +import { randomBytes, randomUUID } from "node:crypto"; + +import { keccak256, toBytes } from "viem"; + +import { + bytes32FromBytea, + canonicalBytes32, + hexToBytes, + parseNonnegativeIntegerText, + type HexBytes32, +} from "./codecs"; +import { invalidInput } from "./errors"; +import type { + PostgresExecutor, + PostgresTransaction, +} from "./postgres"; +import type { ProjectorRuntimeFence } from "./postgres-projector"; + +const RUNTIME_LOGIN_ROLE = "programmable_projector_runtime_login"; +const RUNTIME_CAPABILITY_ROLE = "programmable_projector_runtime"; +const LEASE_TTL_MS = 85_000; +const ZERO_BYTES32 = `0x${"00".repeat(32)}`; + +type RuntimeLeaseAcquisition = Readonly<{ + status: "acquired" | "busy"; + fence?: ProjectorRuntimeFence; + acquiredAt: string; + expiresAt: string; +}>; + +function invalidRuntimeLease(): never { + throw invalidInput("postgres", "runtime-lease"); +} + +function timestamp(value: unknown): string { + const date = value instanceof Date + ? value + : typeof value === "string" + ? new Date(value) + : null; + if (date === null || !Number.isFinite(date.valueOf())) { + return invalidRuntimeLease(); + } + return date.toISOString(); +} + +function generation(value: unknown): string { + try { + return parseNonnegativeIntegerText( + typeof value === "bigint" ? value.toString() : value, + ); + } catch { + return invalidRuntimeLease(); + } +} + +function runtimeTokenHash(): HexBytes32 { + return keccak256(randomBytes(32)); +} + +async function assumeRuntimeRole( + transaction: PostgresTransaction, +): Promise { + const loginRows = await transaction.query<{ session_user: unknown }>( + "select session_user::text as session_user", + ); + if ( + loginRows.length !== 1 || + loginRows[0]?.session_user !== RUNTIME_LOGIN_ROLE + ) { + return invalidRuntimeLease(); + } + await transaction.query("set local role programmable_projector_runtime"); + await transaction.query("set local statement_timeout = '1000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '2000ms'", + ); + const roleRows = await transaction.query<{ + session_user: unknown; + current_role: unknown; + configured_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role, current_setting('role')::text as configured_role", + ); + if ( + roleRows.length !== 1 || + roleRows[0]?.session_user !== RUNTIME_LOGIN_ROLE || + roleRows[0]?.current_role !== RUNTIME_CAPABILITY_ROLE || + roleRows[0]?.configured_role !== RUNTIME_CAPABILITY_ROLE + ) { + return invalidRuntimeLease(); + } +} + +export function createProjectorRuntimeLeaseController(input: Readonly<{ + executor: PostgresExecutor; + now?: () => Date; + uuid?: () => string; + tokenHash?: () => HexBytes32; +}>) { + const now = input.now ?? (() => new Date()); + const uuid = input.uuid ?? randomUUID; + const tokenHash = input.tokenHash ?? runtimeTokenHash; + + return Object.freeze({ + async tryAcquire(): Promise { + const holderId = `projector-runtime-${uuid()}`; + if (!/^projector-runtime-[0-9a-f-]{36}$/u.test(holderId)) { + return invalidRuntimeLease(); + } + const requestedAt = now(); + if (!Number.isFinite(requestedAt.valueOf())) { + return invalidRuntimeLease(); + } + const requestedExpiresAt = new Date( + requestedAt.valueOf() + LEASE_TTL_MS, + ); + const leaseTokenHash = canonicalBytes32(tokenHash()); + if (leaseTokenHash === ZERO_BYTES32) return invalidRuntimeLease(); + const inputCommitment = keccak256( + toBytes( + JSON.stringify([ + holderId, + leaseTokenHash, + requestedAt.toISOString(), + requestedExpiresAt.toISOString(), + ]), + ), + ); + return input.executor.transaction(async (transaction) => { + await assumeRuntimeRole(transaction); + const rows = await transaction.query<{ + acquired: unknown; + lease_generation: unknown; + acquired_at: unknown; + expires_at: unknown; + }>( + "select * from programmable_private.try_acquire_projector_runtime_lease_v1($1, $2::bytea, $3::timestamptz, $4::timestamptz, $5::bytea)", + [ + holderId, + hexToBytes(leaseTokenHash), + requestedAt.toISOString(), + requestedExpiresAt.toISOString(), + hexToBytes(inputCommitment), + ], + ); + if (rows.length !== 1 || typeof rows[0]?.acquired !== "boolean") { + return invalidRuntimeLease(); + } + const row = rows[0]!; + const acquiredAt = timestamp(row.acquired_at); + const expiresAt = timestamp(row.expires_at); + const leaseGeneration = generation(row.lease_generation); + const duration = + new Date(expiresAt).valueOf() - new Date(acquiredAt).valueOf(); + if (duration < 1 || duration > 90_000) { + return invalidRuntimeLease(); + } + if (!row.acquired) { + return Object.freeze({ + status: "busy" as const, + acquiredAt, + expiresAt, + }); + } + if ( + Math.abs( + new Date(acquiredAt).valueOf() - requestedAt.valueOf(), + ) > 30_000 || + duration !== LEASE_TTL_MS + ) { + return invalidRuntimeLease(); + } + return Object.freeze({ + status: "acquired" as const, + fence: Object.freeze({ + holderId, + generation: leaseGeneration, + tokenHash: bytes32FromBytea(hexToBytes(leaseTokenHash)), + }), + acquiredAt, + expiresAt, + }); + }); + }, + + async release(fence: ProjectorRuntimeFence): Promise { + const releasedAt = now(); + if (!Number.isFinite(releasedAt.valueOf())) { + return invalidRuntimeLease(); + } + const inputCommitment = keccak256( + toBytes( + JSON.stringify([ + fence.holderId, + fence.generation, + fence.tokenHash, + releasedAt.toISOString(), + ]), + ), + ); + return input.executor.transaction(async (transaction) => { + await assumeRuntimeRole(transaction); + const rows = await transaction.query<{ released: unknown }>( + "select programmable_private.release_projector_runtime_lease_v1($1, $2::bigint, $3::bytea, $4::timestamptz, $5::bytea) as released", + [ + fence.holderId, + fence.generation, + hexToBytes(fence.tokenHash), + releasedAt.toISOString(), + hexToBytes(inputCommitment), + ], + ); + if (rows.length !== 1 || typeof rows[0]?.released !== "boolean") { + return invalidRuntimeLease(); + } + return rows[0].released; + }); + }, + }); +} + +export type ProjectorRuntimeLeaseController = ReturnType< + typeof createProjectorRuntimeLeaseController +>; diff --git a/lib/data-pipeline/projector-runtime-limits.ts b/lib/data-pipeline/projector-runtime-limits.ts new file mode 100644 index 00000000..b653fe25 --- /dev/null +++ b/lib/data-pipeline/projector-runtime-limits.ts @@ -0,0 +1,15 @@ +export const PROJECTOR_MAXIMUM_RUNTIME_ROUNDS = 8; +export const PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE = 32; +// Normal backlog work is deliberately smaller than the emergency atomic +// ceiling so one cycle stays inside the paid providers' sustained limits. +export const PROJECTOR_PREFERRED_CANDIDATES_PER_COMMIT = 32; +export const PROJECTOR_JSON_RPC_BATCH_SIZE = 20; +export const PROJECTOR_MAXIMUM_RPC_STARTS_PER_SECOND = 20; +// Normal pages remain deliberately small. A single Ethereum transaction, or a +// reward-bearing block that must be proven at block-end state, may be larger +// and is handled as one explicitly tagged atomic group instead of being split. +export const PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP = 4_096; +export const PROJECTOR_MAXIMUM_CANDIDATES_PER_CYCLE = + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP + + (PROJECTOR_MAXIMUM_RUNTIME_ROUNDS - 1) * + PROJECTOR_MAXIMUM_CANDIDATES_PER_PAGE; diff --git a/lib/data-pipeline/projector.ts b/lib/data-pipeline/projector.ts new file mode 100644 index 00000000..ee8dbdac --- /dev/null +++ b/lib/data-pipeline/projector.ts @@ -0,0 +1,960 @@ +import "server-only"; + +import type { + CandidateRpcProvider, + DualRpcCandidateWindowEvidence, + DualRpcDynamicRuntimeObservation, + ProjectorDynamicSourceTemplate, +} from "./dual-rpc"; +import { + readDualRpcSafeHead, + verifyDynamicRuntimeAtBlockWithDualRpc, + verifyDynamicRuntimesAtBlockWithDualRpc, + verifyEnvioCandidateWindowWithDualRpc, +} from "./dual-rpc"; +import { verifyClassicV3ActivationModel } from "./classic-v3-activation-model"; +import type { + EnvioCandidate, + EnvioCandidateCursor, +} from "./envio"; +import { DataPipelineError, dataPipelineError, invalidInput } from "./errors"; +import type { VerifiedDynamicSourceLineage } from "./projector-identities"; +import type { + PendingDynamicSourceActivation, + ResolvePendingDynamicSourceActivationsInput, + StageVerifiedDynamicSourceActivationsInput, +} from "./projector-dynamic-activation"; +import { + buildEnvioCursorRecoveryPlan, + findCanonicalAncestorWithDualRpc, + type EnvioCursorRecoveryPlan, + type ReorgGenesisAnchor, + type ReorgHistoryAncestor, +} from "./projector-reorg"; +import { + PROJECTOR_JSON_RPC_BATCH_SIZE, + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + PROJECTOR_PREFERRED_CANDIDATES_PER_COMMIT, +} from "./projector-runtime-limits"; +import { manifestEventSelectors } from "./event-manifest"; +import { getDataPipelineReleaseBinding } from "./release-binding.server"; + +/** + * A short page is the only Envio response that can prove the frozen window is + * exhausted. Page in bounded chunks while retaining the shared atomic-group + * ceiling for one block-complete commit. + */ +const PROJECTOR_PAGE_LIMIT = 32; +const MAXIMUM_PROVIDER_CALLS = 128; +// The paid secondary provider meters each request inside a JSON-RPC batch. +// Keep the real request starts below twenty seconds of sustained allowance so +// database work, provider jitter and the final atomic commit retain headroom. +const MAXIMUM_PROVIDER_RPC_STARTS = 400; +const COVERAGE_BLOCK_SPAN = 500; +const MAXIMUM_COVERAGE_REQUESTS = 9; +const MAXIMUM_DEADLINE_MS = 75_000; +const MAXIMUM_JSON_RPC_BATCH_SIZE = PROJECTOR_JSON_RPC_BATCH_SIZE; +const MAXIMUM_LOG_FILTER_ADDRESSES = 512; +const MAXIMUM_LOG_FILTER_TOPIC0 = 64; + +export type ProjectorCursor = EnvioCandidateCursor & { + generation: string; + blockHash: `0x${string}`; + /** + * PostgreSQL stores a genesis or verified-empty-page cursor at a block + * boundary with NULL log/candidate coordinates. The provider adapters use + * the equivalent uint32-max sentinel so the next window starts at the next + * block without inventing an Envio candidate. + */ + isBlockBoundary: boolean; +}; + +export type ProjectorPlan = Readonly<{ + cursor: ProjectorCursor; + dynamicSources: readonly VerifiedDynamicSourceLineage[]; + provisionalSourceAddresses: readonly `0x${string}`[]; + dynamicSourceTemplates: readonly ProjectorDynamicSourceTemplate[]; + database: Readonly<{ + epochId: string; + pointerGeneration: string; + reorgGeneration: string; + envioProviderDeploymentId: string; + rpcProviderDeploymentIds: readonly [string, string]; + }>; +}>; + +export type ProjectorStore = Readonly<{ + readPlan(): Promise; + readReorgRecoveryState(input: { + plan: ProjectorPlan; + maximumDepth: number; + }): Promise>; + recoverCanonicalReorg(input: { + plan: ProjectorPlan; + recovery: EnvioCursorRecoveryPlan; + }): Promise>; + resolvePendingDynamicSourceActivations( + input: ResolvePendingDynamicSourceActivationsInput, + ): Promise; + stageVerifiedDynamicSourceActivations( + input: StageVerifiedDynamicSourceActivationsInput, + ): Promise; + stageVerifiedDynamicParents(input: { + plan: ProjectorPlan; + snapshotBlock: string; + candidates: readonly EnvioCandidate[]; + evidence: DualRpcCandidateWindowEvidence; + runtimeObservations: readonly DualRpcDynamicRuntimeObservation[]; + blockComplete: false; + }): Promise; + commitVerifiedPage(input: { + plan: ProjectorPlan; + snapshotBlock: string; + candidates: readonly EnvioCandidate[]; + evidence: DualRpcCandidateWindowEvidence; + blockComplete: true; + }): Promise<{ generation: string }>; +}>; + +type ProjectorEnvio = Readonly<{ + readProgress(input: { + requiredBlock: string; + }): Promise<{ progressBlock: string }>; + readCandidatesWindow(input: { + cursor: EnvioCandidateCursor; + throughBlock: string; + limit: number; + }): Promise; +}>; + +type CaptureSafeHead = typeof readDualRpcSafeHead; +type VerifyWindow = typeof verifyEnvioCandidateWindowWithDualRpc; +type VerifyDynamicRuntime = typeof verifyDynamicRuntimeAtBlockWithDualRpc; +type VerifyDynamicRuntimes = typeof verifyDynamicRuntimesAtBlockWithDualRpc; +type FindCanonicalAncestor = typeof findCanonicalAncestorWithDualRpc; +type VerifyClassicV3Activation = typeof verifyClassicV3ActivationModel; + +type DynamicParentForReplay = Readonly<{ + parent: EnvioCandidate; + childSourceAddress: `0x${string}`; + template: ProjectorDynamicSourceTemplate; +}>; + +const MAXIMUM_DYNAMIC_PARENT_BLOCKS_PER_CYCLE = 8; +const MAXIMUM_DYNAMIC_ACTIVATION_BLOCKS_PER_CYCLE = 2; + +function dynamicParentsForReplay( + candidates: readonly EnvioCandidate[], + dynamicSources: readonly VerifiedDynamicSourceLineage[], + provisionalSourceAddresses: readonly `0x${string}`[], + templates: readonly ProjectorDynamicSourceTemplate[], +): readonly DynamicParentForReplay[] { + const known = new Set([ + ...dynamicSources.map(({ sourceAddress }) => sourceAddress), + ...provisionalSourceAddresses, + ]); + const selectedSources = new Set(); + const selectedParents = new Set(); + const matches: DynamicParentForReplay[] = []; + const selectedBlocks = new Set(); + for (const parent of candidates) { + const matchingTemplates = templates.filter( + (template) => + template.parentFactoryAddress === parent.sourceAddress && + template.parentFactoryContractName === parent.contractName && + template.factoryEventName === parent.eventName, + ); + if (matchingTemplates.length === 0) continue; + if (matchingTemplates.length !== 1) { + throw invalidInput("config", "dynamic-runtime-template"); + } + const template = matchingTemplates[0]!; + const deployedAddress = + parent.decodedPayload[template.deployedAddressField]; + if ( + typeof deployedAddress !== "string" || + !/^0x[0-9a-f]{40}$/u.test(deployedAddress) + ) { + continue; + } + const childSourceAddress = deployedAddress as `0x${string}`; + if (known.has(childSourceAddress)) continue; + if (!selectedBlocks.has(parent.blockNumber)) { + if (selectedBlocks.size >= MAXIMUM_DYNAMIC_PARENT_BLOCKS_PER_CYCLE) break; + selectedBlocks.add(parent.blockNumber); + } + if ( + selectedSources.has(childSourceAddress) || + selectedParents.has(parent.candidateId) + ) { + throw invalidInput("envio", "dynamic-parent-duplicate"); + } + selectedSources.add(childSourceAddress); + selectedParents.add(parent.candidateId); + matches.push(Object.freeze({ + parent, + childSourceAddress, + template, + })); + } + return Object.freeze(matches); +} + +function cursorAtStartOfBlock( + candidate: EnvioCandidate, + current: EnvioCandidateCursor, +): EnvioCandidateCursor { + if (current.blockNumber === candidate.blockNumber) return current; + return { + blockNumber: (BigInt(candidate.blockNumber) - 1n).toString(), + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "", + }; +} + +function dynamicSourcesWithActivationBoundaries( + current: readonly VerifiedDynamicSourceLineage[], + pending: readonly PendingDynamicSourceActivation[], +): readonly VerifiedDynamicSourceLineage[] { + const byAddress = new Map( + current.map((lineage) => [lineage.sourceAddress, lineage] as const), + ); + for (const activation of pending) { + byAddress.set( + activation.ephemeralLineage.sourceAddress, + activation.ephemeralLineage, + ); + } + return Object.freeze([...byAddress.values()]); +} + +function timeoutError() { + return dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); +} + +type CoverageFilterShape = Readonly<{ + addressCount: number; + topicCount: number; +}>; + +function coverageFilterShape(input: Readonly<{ + throughBlock: bigint; + dynamicSources: readonly VerifiedDynamicSourceLineage[]; +}>): CoverageFilterShape { + const binding = getDataPipelineReleaseBinding(); + const sourceContracts = binding.sources + .filter(({ startBlock }) => BigInt(startBlock) <= input.throughBlock) + .map(({ address, contractName }) => ({ address, contractName })) + .concat(input.dynamicSources.map(({ sourceAddress, contractName }) => ({ + address: sourceAddress, + contractName, + }))); + const contractNames = new Set(sourceContracts.map(({ contractName }) => contractName)); + return Object.freeze({ + addressCount: new Set(sourceContracts.map(({ address }) => address)).size, + topicCount: new Set( + [...contractNames].flatMap((contractName) => + manifestEventSelectors(contractName) + ), + ).size, + }); +} + +function estimatedProviderCallsForWindow(input: Readonly<{ + candidates: readonly EnvioCandidate[]; + cursor: EnvioCandidateCursor; + throughBlock: bigint; + filterShape: CoverageFilterShape; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; +}>): readonly [number, number] { + const { addressCount, topicCount } = input.filterShape; + if (addressCount < 1 || topicCount < 1) { + throw invalidInput("config", "coverage-filter"); + } + const cursorBlock = BigInt(input.cursor.blockNumber); + const effectiveFromBlock = + input.cursor.blockGlobalLogIndex === 0xffff_ffff && + input.cursor.candidateId === "" + ? cursorBlock + 1n + : cursorBlock; + const blockCount = input.throughBlock - effectiveFromBlock + 1n; + if (blockCount < 1n || blockCount > BigInt(Number.MAX_SAFE_INTEGER)) { + throw invalidInput("config", "coverage-window"); + } + const filterCount = Number(blockCount) * + Math.ceil(addressCount / MAXIMUM_LOG_FILTER_ADDRESSES) * + Math.ceil(topicCount / MAXIMUM_LOG_FILTER_TOPIC0); + if (!Number.isSafeInteger(filterCount) || filterCount < 1) { + throw invalidInput("config", "coverage-filter-budget"); + } + const uniqueCandidateBlocks = new Set( + input.candidates.map(({ blockNumber }) => blockNumber), + ).size; + const uniqueTransactions = new Set( + input.candidates.map(({ transactionHash }) => transactionHash), + ).size; + const uniqueCodeRequests = new Set( + input.candidates.map(({ blockHash, sourceAddress }) => + `${blockHash}:${sourceAddress}` + ), + ).size; + return input.providers.map(({ client }) => { + const blockCalls = client.getBlocks + ? Math.ceil((uniqueCandidateBlocks + 1) / MAXIMUM_JSON_RPC_BATCH_SIZE) + : uniqueCandidateBlocks + 1; + const receiptCalls = client.getTransactionReceipts + ? Math.ceil(uniqueTransactions / MAXIMUM_JSON_RPC_BATCH_SIZE) + : uniqueTransactions; + const codeCalls = client.getBytecodes + ? Math.ceil(uniqueCodeRequests / MAXIMUM_JSON_RPC_BATCH_SIZE) + : uniqueCodeRequests; + const logCalls = client.getLogsBatch + ? Math.ceil(filterCount / MAXIMUM_JSON_RPC_BATCH_SIZE) + : filterCount; + // chain/head, candidate blocks (including safe), receipts, code, exact + // through-block header and the bounded log filters. + return 2 + blockCalls + receiptCalls + codeCalls + 1 + logCalls; + }) as [number, number]; +} + +function estimatedProviderStartsForWindow(input: Readonly<{ + candidates: readonly EnvioCandidate[]; + cursor: EnvioCandidateCursor; + throughBlock: bigint; + filterShape: CoverageFilterShape; +}>): number { + const { addressCount, topicCount } = input.filterShape; + const cursorBlock = BigInt(input.cursor.blockNumber); + const effectiveFromBlock = + input.cursor.blockGlobalLogIndex === 0xffff_ffff && + input.cursor.candidateId === "" + ? cursorBlock + 1n + : cursorBlock; + const blockCount = input.throughBlock - effectiveFromBlock + 1n; + if ( + addressCount < 1 || + topicCount < 1 || + blockCount < 1n || + blockCount > BigInt(Number.MAX_SAFE_INTEGER) + ) { + throw invalidInput("config", "coverage-start-budget"); + } + const filterCount = Number(blockCount) * + Math.ceil(addressCount / MAXIMUM_LOG_FILTER_ADDRESSES) * + Math.ceil(topicCount / MAXIMUM_LOG_FILTER_TOPIC0); + const uniqueCandidateBlocks = new Set( + input.candidates.map(({ blockNumber }) => blockNumber), + ).size; + const uniqueTransactions = new Set( + input.candidates.map(({ transactionHash }) => transactionHash), + ).size; + const uniqueCodeRequests = new Set( + input.candidates.map(({ blockHash, sourceAddress }) => + `${blockHash}:${sourceAddress}` + ), + ).size; + const starts = + 2 + + uniqueCandidateBlocks + + 1 + + uniqueTransactions + + uniqueCodeRequests + + 1 + + filterCount; + if (!Number.isSafeInteger(starts) || starts < 1) { + throw invalidInput("config", "coverage-start-budget"); + } + return starts; +} + +function fitCompleteCoverageWindow(input: Readonly<{ + candidates: readonly EnvioCandidate[]; + cursor: EnvioCandidateCursor; + desiredThroughBlock: string; + dynamicSources: readonly VerifiedDynamicSourceLineage[]; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; +}>): Readonly<{ + candidates: readonly EnvioCandidate[]; + throughBlock: string; +}> { + // Unit-level callers may inject the verifier and deliberately omit concrete + // providers. Production configuration always supplies exactly two clients; + // only that path can be budgeted from transport capabilities. + if (input.providers.length !== 2) { + return Object.freeze({ + candidates: Object.freeze([...input.candidates]), + throughBlock: input.desiredThroughBlock, + }); + } + const cursorBlock = BigInt(input.cursor.blockNumber); + const minimumThrough = + input.cursor.blockGlobalLogIndex === 0xffff_ffff && + input.cursor.candidateId === "" + ? cursorBlock + 1n + : cursorBlock; + let low = minimumThrough; + let high = BigInt(input.desiredThroughBlock); + let selected: bigint | null = null; + const filterShape = coverageFilterShape({ + throughBlock: high, + dynamicSources: input.dynamicSources, + }); + while (low <= high) { + const middle = low + (high - low) / 2n; + const candidates = input.candidates.filter( + ({ blockNumber }) => BigInt(blockNumber) <= middle, + ); + const calls = estimatedProviderCallsForWindow({ + candidates, + cursor: input.cursor, + throughBlock: middle, + filterShape, + providers: input.providers, + }); + const starts = estimatedProviderStartsForWindow({ + candidates, + cursor: input.cursor, + throughBlock: middle, + filterShape, + }); + if ( + starts <= MAXIMUM_PROVIDER_RPC_STARTS && + calls.every((count) => count <= MAXIMUM_PROVIDER_CALLS) + ) { + selected = middle; + low = middle + 1n; + } else { + high = middle - 1n; + } + } + if (selected === null) { + throw invalidInput("rpc", "provider-call-budget"); + } + return Object.freeze({ + candidates: Object.freeze(input.candidates.filter( + ({ blockNumber }) => BigInt(blockNumber) <= selected!, + )), + throughBlock: selected.toString(), + }); +} + +async function withOverallDeadline( + deadlineMs: number, + operation: () => Promise, +): Promise { + let timeout: ReturnType | undefined; + try { + return await Promise.race([ + operation(), + new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(timeoutError()), deadlineMs); + }), + ]); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +export async function runProjectorCycle(input: { + store: ProjectorStore; + envio: ProjectorEnvio; + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + deadlineMs?: number; + preferredCandidatesPerCommit?: number; + captureSafeHead?: CaptureSafeHead; + verifyWindow?: VerifyWindow; + verifyDynamicRuntimes?: VerifyDynamicRuntimes; + verifyDynamicRuntime?: VerifyDynamicRuntime; + findCanonicalAncestor?: FindCanonicalAncestor; + verifyClassicV3Activation?: VerifyClassicV3Activation; +}) { + const deadlineMs = input.deadlineMs ?? MAXIMUM_DEADLINE_MS; + const preferredCandidatesPerCommit = + input.preferredCandidatesPerCommit ?? + PROJECTOR_PREFERRED_CANDIDATES_PER_COMMIT; + if ( + !Number.isSafeInteger(deadlineMs) || + deadlineMs < 10 || + deadlineMs > MAXIMUM_DEADLINE_MS + ) { + throw invalidInput("config", "projector-deadline"); + } + if ( + !Number.isSafeInteger(preferredCandidatesPerCommit) || + preferredCandidatesPerCommit < + PROJECTOR_PREFERRED_CANDIDATES_PER_COMMIT || + preferredCandidatesPerCommit > + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP || + preferredCandidatesPerCommit % PROJECTOR_PAGE_LIMIT !== 0 + ) { + throw invalidInput("config", "projector-candidate-window"); + } + const startedAt = Date.now(); + const remaining = () => { + const value = deadlineMs - (Date.now() - startedAt); + if (value < 10) throw timeoutError(); + return value; + }; + const captureSafeHead = input.captureSafeHead ?? readDualRpcSafeHead; + const verifyWindow = + input.verifyWindow ?? verifyEnvioCandidateWindowWithDualRpc; + const verifyDynamicRuntime = + input.verifyDynamicRuntime ?? verifyDynamicRuntimeAtBlockWithDualRpc; + const verifyDynamicRuntimes = + input.verifyDynamicRuntimes ?? + (input.verifyDynamicRuntime === undefined + ? verifyDynamicRuntimesAtBlockWithDualRpc + : null); + const findCanonicalAncestor = + input.findCanonicalAncestor ?? findCanonicalAncestorWithDualRpc; + const verifyClassicV3Activation = + input.verifyClassicV3Activation ?? verifyClassicV3ActivationModel; + + return withOverallDeadline(deadlineMs, async () => { + // Each store method owns and closes its database transaction. All Envio + // and RPC work deliberately occurs between these two calls. + const plan = await input.store.readPlan(); + let safeHead; + try { + safeHead = await captureSafeHead({ + providers: input.providers, + cursor: { + blockNumber: plan.cursor.blockNumber, + blockHash: plan.cursor.blockHash, + }, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxCallsPerProvider: MAXIMUM_PROVIDER_CALLS, + }, + }); + } catch (error) { + if ( + !(error instanceof DataPipelineError) || + error.code !== "validation_failed" || + error.safeMetadata?.operation !== "safe-head-cursor-orphaned" + ) { + throw error; + } + const recoveryState = await input.store.readReorgRecoveryState({ + plan, + maximumDepth: 128, + }); + if ( + recoveryState.currentReorgGeneration !== + plan.database.reorgGeneration + ) { + throw invalidInput("postgres", "reorg-generation"); + } + const target = await findCanonicalAncestor({ + providers: input.providers, + ancestors: recoveryState.ancestors, + genesis: recoveryState.genesis, + policy: { + maximumDepth: 128, + maxProviderCalls: MAXIMUM_PROVIDER_CALLS, + deadlineMs: remaining(), + }, + }); + const recovery = buildEnvioCursorRecoveryPlan({ + expectedGeneration: plan.cursor.generation, + currentReorgGeneration: recoveryState.currentReorgGeneration, + target, + }); + const recovered = await input.store.recoverCanonicalReorg({ + plan, + recovery, + }); + return { + status: "recovered-reorg" as const, + candidateCount: 0, + generation: recovered.generation, + reorgGeneration: recovered.reorgGeneration, + releaseCheckpointCount: recovered.releaseCheckpointCount, + snapshotBlock: recovery.targetBlockNumber, + }; + } + const progress = await input.envio.readProgress({ + requiredBlock: safeHead.safeBlockNumber, + }); + let snapshot = + BigInt(progress.progressBlock) < BigInt(safeHead.safeBlockNumber) + ? BigInt(progress.progressBlock) + : BigInt(safeHead.safeBlockNumber); + const cursorBlock = BigInt(plan.cursor.blockNumber); + if (snapshot < cursorBlock) { + return { + status: "idle" as const, + candidateCount: 0, + snapshotBlock: snapshot.toString(), + }; + } + if (plan.cursor.isBlockBoundary && snapshot === cursorBlock) { + return { + status: "idle" as const, + candidateCount: 0, + snapshotBlock: snapshot.toString(), + }; + } + const maximumCoverageBlock = + cursorBlock + + BigInt(COVERAGE_BLOCK_SPAN * MAXIMUM_COVERAGE_REQUESTS) - + 1n; + if (snapshot > maximumCoverageBlock) snapshot = maximumCoverageBlock; + const providerSnapshotBlock = snapshot.toString(); + const cursor: EnvioCandidateCursor = { + blockNumber: plan.cursor.blockNumber, + blockGlobalLogIndex: plan.cursor.blockGlobalLogIndex, + candidateId: plan.cursor.candidateId, + }; + const collected: EnvioCandidate[] = []; + let pageCursor = cursor; + let reachedTerminalBoundary = false; + let collectionCeiling = preferredCandidatesPerCommit; + // Read one sentinel beyond the atomic ceiling. An empty sentinel proves an + // exact-size block ended; a real sentinel lets us either cut back to the + // preceding complete block or fail closed when one block exceeds 4096. + while (collected.length <= collectionCeiling) { + const capacity = + collectionCeiling + 1 - collected.length; + const limit = Math.min(PROJECTOR_PAGE_LIMIT, capacity); + const page = await input.envio.readCandidatesWindow({ + cursor: pageCursor, + throughBlock: providerSnapshotBlock, + limit, + }); + collected.push(...page); + if (page.length < limit) { + reachedTerminalBoundary = true; + break; + } + const last = page[page.length - 1]!; + pageCursor = { + blockNumber: last.blockNumber, + blockGlobalLogIndex: last.blockGlobalLogIndex, + candidateId: last.candidateId, + }; + if ( + collectionCeiling === preferredCandidatesPerCommit && + collected.length > preferredCandidatesPerCommit + ) { + const boundaryBlock = collected.at(-1)!.blockNumber; + const completePrefixExists = collected.some( + ({ blockNumber }) => BigInt(blockNumber) < BigInt(boundaryBlock), + ); + if (completePrefixExists) break; + // Only a single oversized first block may exceed the preferred + // ceiling. Continue just far enough to prove its complete boundary, + // while retaining the hard 4096-candidate fail-closed limit. + collectionCeiling = PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP; + } + } + + // A returned candidate is never treated as proof that its block ended. A + // full page is cut back to the last preceding block boundary; a terminal + // Envio page may reach the frozen provider snapshot. Both cases are then + // independently scanned by two RPCs before the store may persist the + // `empty-page` boundary. + let candidates: readonly EnvioCandidate[] = collected; + let completeBlock = providerSnapshotBlock; + let exceededSingleBlockBudget = false; + if (!reachedTerminalBoundary) { + const finalBlock = collected[collected.length - 1]!.blockNumber; + const prefix = collected.filter( + (item) => BigInt(item.blockNumber) < BigInt(finalBlock), + ); + if (prefix.length === 0) { + const predecessor = BigInt(finalBlock) - 1n; + if (predecessor <= BigInt(cursor.blockNumber)) { + exceededSingleBlockBudget = true; + } else { + candidates = []; + completeBlock = predecessor.toString(); + } + } else { + candidates = prefix; + completeBlock = prefix[prefix.length - 1]!.blockNumber; + } + } + if (exceededSingleBlockBudget) { + throw dataPipelineError({ + dependency: "envio", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + }); + } + const provisionalParents = dynamicParentsForReplay( + candidates, + plan.dynamicSources, + plan.provisionalSourceAddresses, + plan.dynamicSourceTemplates, + ); + if (provisionalParents.length > 0) { + const groups = new Map(); + for (const provisionalParent of provisionalParents) { + const group = groups.get(provisionalParent.parent.blockNumber) ?? []; + group.push(provisionalParent); + groups.set(provisionalParent.parent.blockNumber, group); + } + const verifiedGroups = await Promise.all( + [...groups.entries()].map(async ([blockNumber, group]) => { + const parents = group.map(({ parent }) => parent); + const firstParent = parents[0]!; + const stageEvidence = await verifyWindow({ + candidates: parents, + cursor: cursorAtStartOfBlock(firstParent, cursor), + through: { + blockNumber, + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "empty-page", + }, + providers: input.providers, + dynamicSources: plan.dynamicSources, + coverageSourceAddresses: Object.freeze([ + ...new Set(parents.map(({ sourceAddress }) => sourceAddress)), + ]), + maximumCandidateCount: + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + coveragePolicy: { + maximumBlockSpan: COVERAGE_BLOCK_SPAN, + maximumRequests: 1, + }, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxCallsPerProvider: MAXIMUM_PROVIDER_CALLS, + }, + }); + const runtimeItems = group.map((provisionalParent) => ({ + parentCandidate: provisionalParent.parent, + sourceAddress: provisionalParent.childSourceAddress, + deploymentBlockNumber: provisionalParent.parent.blockNumber, + deploymentBlockHash: provisionalParent.parent.blockHash, + template: provisionalParent.template, + })); + const runtimeObservations: + readonly DualRpcDynamicRuntimeObservation[] = verifyDynamicRuntimes + ? await verifyDynamicRuntimes({ + items: runtimeItems, + parentEvidence: stageEvidence, + providers: input.providers, + deadlineMs: remaining(), + }) + : await Promise.all(runtimeItems.map((item) => + verifyDynamicRuntime({ + ...item, + parentEvidence: stageEvidence, + providers: input.providers, + deadlineMs: remaining(), + }) + )); + return { blockNumber, parents, stageEvidence, runtimeObservations }; + }), + ); + for (const group of verifiedGroups) { + await input.store.stageVerifiedDynamicParents({ + plan, + snapshotBlock: group.blockNumber, + candidates: group.parents, + evidence: group.stageEvidence, + runtimeObservations: group.runtimeObservations, + blockComplete: false, + }); + } + const lastGroup = verifiedGroups.at(-1)!; + return { + status: "staged-dynamic-parent" as const, + candidateCount: provisionalParents.length, + snapshotBlock: lastGroup.blockNumber, + }; + } + const fittedWindow = fitCompleteCoverageWindow({ + candidates, + cursor, + desiredThroughBlock: completeBlock, + dynamicSources: plan.dynamicSources, + providers: input.providers, + }); + candidates = fittedWindow.candidates; + completeBlock = fittedWindow.throughBlock; + // The durable non-empty cursor remains candidate-backed. Do not claim a + // trailing empty block in the same page: commit that reviewed candidate + // block first, then let the next empty-page cycle advance the verified + // block boundary. This preserves the database evidence shape and avoids a + // permanent candidate-at-N / safe-head-at-N+k validation loop. + const terminalCandidate = candidates.at(-1); + if ( + terminalCandidate !== undefined && + BigInt(completeBlock) > BigInt(terminalCandidate.blockNumber) + ) { + completeBlock = terminalCandidate.blockNumber; + } + const through: EnvioCandidateCursor = { + blockNumber: completeBlock, + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "empty-page", + }; + const pendingActivations = + await input.store.resolvePendingDynamicSourceActivations({ + candidates, + expectedCursorGeneration: plan.cursor.generation, + expectedCursorBlockHash: plan.cursor.blockHash, + expectedReorgGeneration: plan.database.reorgGeneration, + }); + const activationsByBlock = new Map< + string, + PendingDynamicSourceActivation[] + >(); + for (const pending of pendingActivations) { + const blockKey = `${pending.launchCandidate.blockNumber}:${pending.launchCandidate.blockHash}`; + const group = activationsByBlock.get(blockKey) ?? []; + group.push(pending); + activationsByBlock.set(blockKey, group); + } + const orderedActivationGroups = [...activationsByBlock.values()].sort( + (left, right) => { + const leftBlock = BigInt(left[0]!.launchCandidate.blockNumber); + const rightBlock = BigInt(right[0]!.launchCandidate.blockNumber); + return leftBlock < rightBlock ? -1 : leftBlock > rightBlock ? 1 : 0; + }, + ); + const selectedActivationGroups = orderedActivationGroups.slice( + 0, + MAXIMUM_DYNAMIC_ACTIVATION_BLOCKS_PER_CYCLE, + ); + if (selectedActivationGroups.length > 0) { + const verifiedGroups = await Promise.all( + selectedActivationGroups.map(async (group) => { + const activationBlock = group[0]!.launchCandidate.blockNumber; + const activationCandidates = candidates.filter( + (candidate) => + BigInt(candidate.blockNumber) <= BigInt(activationBlock), + ); + const dynamicSources = dynamicSourcesWithActivationBoundaries( + plan.dynamicSources, + pendingActivations.filter( + (pending) => + BigInt(pending.launchCandidate.blockNumber) <= + BigInt(activationBlock), + ), + ); + const activationEvidence = await verifyWindow({ + candidates: activationCandidates, + cursor, + through: { + blockNumber: activationBlock, + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "empty-page", + }, + providers: input.providers, + dynamicSources, + maximumCandidateCount: + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + coveragePolicy: { + maximumBlockSpan: COVERAGE_BLOCK_SPAN, + maximumRequests: MAXIMUM_COVERAGE_REQUESTS, + }, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxCallsPerProvider: MAXIMUM_PROVIDER_CALLS, + }, + }); + const verifiedActivations = await Promise.all( + group.map(async (pending) => { + const verification = await verifyClassicV3Activation({ + activationId: pending.activationId, + parentCandidate: pending.historicalParentCandidate, + launchCandidate: pending.launchCandidate, + sameBlockVaultEvents: activationCandidates.filter( + (candidate) => + candidate.blockNumber === activationBlock && + candidate.sourceAddress === pending.sourceAddress && + candidate.contractName === "ClassicV3RewardVault", + ), + candidateEvidence: activationEvidence, + sourceAddress: pending.sourceAddress, + template: pending.template, + canonicalDeployment: pending.canonicalDeployment, + providers: input.providers, + deadlineMs: remaining(), + }); + return Object.freeze({ + pending, + runtimeObservation: verification.runtimeObservation, + modelVerificationEvidence: + verification.modelVerificationEvidence, + }); + }), + ); + return Object.freeze({ + activationBlock, + activationCandidates, + activationEvidence, + verifiedActivations, + }); + }), + ); + for (const group of verifiedGroups) { + await input.store.stageVerifiedDynamicSourceActivations({ + candidates: group.activationCandidates, + evidence: group.activationEvidence, + activations: group.verifiedActivations, + blockComplete: false, + }); + } + const lastGroup = verifiedGroups.at(-1)!; + return { + status: "staged-dynamic-parent" as const, + candidateCount: verifiedGroups.reduce( + (sum, group) => sum + group.verifiedActivations.length, + 0, + ), + snapshotBlock: lastGroup.activationBlock, + }; + } + const evidence = await verifyWindow({ + candidates, + cursor, + through, + providers: input.providers, + dynamicSources: plan.dynamicSources, + maximumCandidateCount: + PROJECTOR_MAXIMUM_CANDIDATES_PER_ATOMIC_GROUP, + coveragePolicy: { + maximumBlockSpan: COVERAGE_BLOCK_SPAN, + maximumRequests: MAXIMUM_COVERAGE_REQUESTS, + }, + rpcPolicy: { + hardDeadlineMs: remaining(), + maxCallsPerProvider: MAXIMUM_PROVIDER_CALLS, + }, + }); + const committed = await input.store.commitVerifiedPage({ + plan, + snapshotBlock: completeBlock, + candidates, + evidence, + blockComplete: true, + }); + return { + status: candidates.length === 0 + ? "committed-empty" as const + : "committed" as const, + candidateCount: candidates.length, + generation: committed.generation, + snapshotBlock: completeBlock, + }; + }); +} diff --git a/lib/data-pipeline/provider-evidence.ts b/lib/data-pipeline/provider-evidence.ts new file mode 100644 index 00000000..ba706892 --- /dev/null +++ b/lib/data-pipeline/provider-evidence.ts @@ -0,0 +1,751 @@ +import "server-only"; + +import { encodeAbiParameters, keccak256, sha256, type Hex } from "viem"; + +import { + canonicalAddress, + canonicalBytes32, + canonicalRawData, + parseNonnegativeIntegerText, + type HexAddress, + type HexBytes32, + type HexData, +} from "./codecs"; +import { invalidInput, validationError } from "./errors"; + +const UINT32_MAXIMUM = 4_294_967_295n; +const PROVIDER_EVIDENCE_V2_PREFIX = Buffer.from( + "programmable:provider-evidence:v2\0", + "utf8", +); +const PROVIDER_EVIDENCE_V3_PREFIX = Buffer.from( + "programmable:provider-evidence:v3\0", + "utf8", +); +const PROJECTION_EXECUTION_TRACE_V1_PREFIX = Buffer.from( + "programmable:projection-execution-trace:v1\0", + "utf8", +); + +type ProviderEvidenceV2Subtype = + | "safe_head" + | "block" + | "runtime_code" + | "dynamic_attestation" + | "log_coverage"; +type ProviderEvidenceV3Subtype = + | "projection_execution" + | "reward_snapshot"; + +type ProviderEvidenceField = readonly [name: string, type: string]; + +function defineProviderEvidenceSchema< + const Fields extends readonly ProviderEvidenceField[], +>(fields: Fields): Fields { + return fields; +} + +const PROVIDER_EVIDENCE_V2_SCHEMAS: Readonly< + Record +> = Object.freeze({ + safe_head: defineProviderEvidenceSchema([ + ["chain_id", "u64"], + ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], + ["reported_chain_id_a", "u64"], + ["reported_chain_id_b", "u64"], + ["head_a", "u64"], + ["head_b", "u64"], + ["finality_depth", "u32"], + ["safe_block_number", "u64"], + ["safe_block_hash_a", "bytes32"], + ["safe_block_hash_b", "bytes32"], + ]), + block: defineProviderEvidenceSchema([ + ["chain_id", "u64"], + ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["observation_id", "uuid16"], + ["block_number", "u64"], + ["provider_a_block_hash", "bytes32"], + ["provider_b_block_hash", "bytes32"], + ]), + runtime_code: defineProviderEvidenceSchema([ + ["chain_id", "u64"], + ["release_id", "varutf8"], + ["model_id", "varutf8"], + ["source_group", "varutf8"], + ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["source_address", "bytes20"], + ["deployment_block_evidence_id", "uuid16"], + ["deployment_block_number", "u64"], + ["deployment_block_hash", "bytes32"], + ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], + ["runtime_code_hash_a", "bytes32"], + ["runtime_code_hash_b", "bytes32"], + ["runtime_code_a", "varbytes"], + ["runtime_code_b", "varbytes"], + ["normalized_runtime_code_hash_a", "bytes32"], + ["normalized_runtime_code_hash_b", "bytes32"], + ["immutable_references_commitment", "bytes32"], + ["immutable_values", "array"], + ["immutable_values_commitment", "bytes32"], + ["reconstructed_runtime_code", "varbytes"], + ["reconstructed_runtime_code_hash", "bytes32"], + ]), + dynamic_attestation: defineProviderEvidenceSchema([ + ["chain_id", "u64"], + ["release_id", "varutf8"], + ["model_id", "varutf8"], + ["source_group", "varutf8"], + ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["runtime_code_evidence_id", "uuid16"], + ["dynamic_source_template_id", "uuid16"], + ["parent_factory_occurrence_id", "uuid16"], + ["parent_factory_release_binding_id", "uuid16"], + ["parent_factory_binding_commitment", "bytes32"], + ["deployed_source_address", "bytes20"], + ["deployed_source_role", "varutf8"], + ["deployment_block_number", "u64"], + ["deployed_artifact_creation_code_commitment", "bytes32"], + ["expected_immutable_values_commitment", "bytes32"], + ["factory_configuration_commitment", "bytes32"], + ["constructor_arguments_commitment", "bytes32"], + ["local_init_code_hash", "bytes32"], + ["runtime_code_hash", "bytes32"], + ["abi_event_set_commitment", "bytes32"], + ]), + log_coverage: defineProviderEvidenceSchema([ + ["chain_id", "u64"], + ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["provider_deployment_id", "uuid16"], + ["stream_id", "varutf8"], + ["expected_cursor_generation", "u64"], + ["next_cursor_generation", "u64"], + ["previous_block_number", "u64"], + ["previous_block_global_log_index", "optional"], + ["previous_candidate_id", "optional"], + ["from_block_number", "u64"], + ["to_block_number", "u64"], + ["final_block_hash", "bytes32"], + ["final_block_global_log_index", "u32"], + ["final_candidate_id", "varutf8"], + ["safe_head_observation_id", "uuid16"], + ["final_block_evidence_id", "uuid16"], + ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], + ["filter_commitment", "bytes32"], + ["ordered_log_commitments", "array"], + ["page_commitment", "bytes32"], + ]), +}); + +const PROVIDER_EVIDENCE_V3_SCHEMAS: Readonly< + Record +> = Object.freeze({ + projection_execution: defineProviderEvidenceSchema([ + ["chain_id", "u64"], + ["release_id", "varutf8"], + ["model_id", "varutf8"], + ["source_group", "varutf8"], + ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["run_id", "uuid16"], + ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], + ["provider_a_identity", "varutf8"], + ["provider_b_identity", "varutf8"], + ["provider_a_vendor_group", "varutf8"], + ["provider_b_vendor_group", "varutf8"], + ["provider_a_endpoint_commitment", "bytes32"], + ["provider_b_endpoint_commitment", "bytes32"], + ["provider_a_origin_commitment", "bytes32"], + ["provider_b_origin_commitment", "bytes32"], + ["provider_a_call_count", "u32"], + ["provider_b_call_count", "u32"], + ["candidate_batch_size", "u32"], + ["hard_deadline_ms", "u32"], + ["maximum_calls_per_provider", "u32"], + ["elapsed_ms", "u32"], + ["execution_trace_commitment", "bytes32"], + ]), + reward_snapshot: defineProviderEvidenceSchema([ + ["chain_id", "u64"], + ["release_id", "varutf8"], + ["model_id", "varutf8"], + ["source_group", "varutf8"], + ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["run_id", "uuid16"], + ["projection_execution_evidence_id", "uuid16"], + ["block_evidence_id", "uuid16"], + ["vault", "bytes20"], + ["reward_model", "varutf8"], + ["block_number", "u64"], + ["block_hash", "bytes32"], + ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], + ["provider_a_snapshot_commitment", "bytes32"], + ["provider_b_snapshot_commitment", "bytes32"], + ["provider_a_call_count", "u32"], + ["provider_b_call_count", "u32"], + ["verification_accounts", "array"], + ["verification_account_chunk_end_offsets", "array"], + ["provider_a_verification_chunk_commitments", "array"], + ["provider_b_verification_chunk_commitments", "array"], + ["provider_a_verification_chunk_call_counts", "array"], + ["provider_b_verification_chunk_call_counts", "array"], + ["folded_snapshot_commitment", "bytes32"], + ["execution_trace_commitment", "bytes32"], + ]), +}); + +const PROVIDER_EVIDENCE_V2_TAGS: Readonly< + Record +> = + Object.freeze({ + safe_head: 1, + block: 2, + runtime_code: 3, + dynamic_attestation: 4, + log_coverage: 5, + }); +const PROVIDER_EVIDENCE_V3_TAGS: Readonly< + Record +> = Object.freeze({ + projection_execution: 6, + reward_snapshot: 7, + }); + +export function providerEvidenceContractCommitment(): HexBytes32 { + return keccak256( + encodeAbiParameters( + [ + { type: "string" }, + { type: "string" }, + ], + [ + "programmable:provider-evidence-contract:v2", + JSON.stringify([ + PROVIDER_EVIDENCE_V2_PREFIX.toString("hex"), + PROVIDER_EVIDENCE_V2_TAGS, + PROVIDER_EVIDENCE_V2_SCHEMAS, + ]), + ], + ), + ); +} + +export function providerEvidenceV3ContractCommitment(): HexBytes32 { + return keccak256( + encodeAbiParameters( + [ + { type: "string" }, + { type: "string" }, + ], + [ + "programmable:provider-evidence-contract:v3", + JSON.stringify([ + PROVIDER_EVIDENCE_V3_PREFIX.toString("hex"), + PROVIDER_EVIDENCE_V3_TAGS, + PROVIDER_EVIDENCE_V3_SCHEMAS, + ]), + ], + ), + ); +} + +function fixedUnsigned(value: unknown, width: 4 | 8): Buffer { + let parsed: bigint; + try { + parsed = BigInt(value as string | number | bigint); + } catch { + throw invalidInput("rpc", "provider-evidence-uint"); + } + if (parsed < 0n || parsed >= 1n << BigInt(width * 8)) { + throw invalidInput("rpc", "provider-evidence-uint"); + } + const result = Buffer.alloc(width); + if (width === 4) result.writeUInt32BE(Number(parsed)); + else result.writeBigUInt64BE(parsed); + return result; +} + +function exactHex(value: unknown, width?: number): Buffer { + if (typeof value !== "string" || !/^0x(?:[0-9a-f]{2})*$/u.test(value)) { + throw invalidInput("rpc", "provider-evidence-bytes"); + } + const result = Buffer.from(value.slice(2), "hex"); + if (width !== undefined && result.length !== width) { + throw invalidInput("rpc", "provider-evidence-bytes"); + } + return result; +} + +function exactUuid(value: unknown): Buffer { + if ( + typeof value !== "string" || + !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test( + value, + ) + ) { + throw invalidInput("rpc", "provider-evidence-uuid"); + } + return Buffer.from(value.replaceAll("-", ""), "hex"); +} + +function framed(value: Uint8Array): Buffer { + return Buffer.concat([fixedUnsigned(value.length, 4), value]); +} + +function exactFramedText(value: unknown): Buffer { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 512 || + /[\u0000-\u001f\u007f]/u.test(value) + ) { + throw invalidInput("rpc", "projection-execution-trace-text"); + } + return framed(Buffer.from(value, "utf8")); +} + +function exactSafeUnsignedNumber( + value: unknown, + width: 4 | 8, +): Buffer { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < 0 + ) { + throw invalidInput("rpc", "projection-execution-trace-uint"); + } + return fixedUnsigned(value, width); +} + +function isSafeUnsignedNumber( + value: unknown, + minimum: number, + maximum: number, +): value is number { + return typeof value === "number" && + Number.isSafeInteger(value) && + value >= minimum && + value <= maximum; +} + +function assertExactRecordFields( + value: unknown, + expectedFields: readonly string[], + operation: string, +): asserts value is Record { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + Object.getPrototypeOf(value) !== Object.prototype + ) { + throw invalidInput("rpc", operation); + } + const actualFields = Object.keys(value).sort(); + const expected = [...expectedFields].sort(); + if ( + actualFields.length !== expected.length || + actualFields.some((field, index) => field !== expected[index]) + ) { + throw invalidInput("rpc", operation); + } +} + +const PROJECTION_EXECUTION_TRACE_V1_FIELDS = Object.freeze([ + "startedAtMs", + "completedAtMs", + "candidateBatchSize", + "hardDeadlineMs", + "maxCallsPerProvider", + "elapsedMs", + "providerCallCounts", + "calls", +]); + +const PROJECTION_EXECUTION_CALL_V1_FIELDS = Object.freeze([ + "providerIdentity", + "providerVendorGroup", + "providerEndpointCommitment", + "providerOriginCommitment", + "operation", + "attempt", + "startedOffsetMs", + "durationMs", + "outcome", +]); + +const PROJECTION_EXECUTION_OPERATION_TAGS = Object.freeze({ + getChainId: 1, + getBlockNumber: 2, + getBlock: 3, + getTransactionReceipt: 4, + getBytecode: 5, + readRewardSnapshot: 6, +} as const); + +const PROJECTION_EXECUTION_OUTCOME_TAGS = Object.freeze({ + success: 1, + error: 2, +} as const); + +/** + * Canonical binary representation of one physical dual-RPC execution trace. + * Calls remain in their observed order; object-key order and JSON whitespace + * never influence the commitment that PostgreSQL recomputes. + */ +export function projectionExecutionTracePreimageV1( + trace: unknown, +): Uint8Array { + assertExactRecordFields( + trace, + PROJECTION_EXECUTION_TRACE_V1_FIELDS, + "projection-execution-trace", + ); + if ( + !Array.isArray(trace.providerCallCounts) || + trace.providerCallCounts.length !== 2 || + !Array.isArray(trace.calls) || + trace.calls.length < 1 || + trace.calls.length > 256 + ) { + throw invalidInput("rpc", "projection-execution-trace"); + } + const providerACallCount = trace.providerCallCounts[0]; + const providerBCallCount = trace.providerCallCounts[1]; + if ( + !isSafeUnsignedNumber(trace.startedAtMs, 0, Number.MAX_SAFE_INTEGER) || + !isSafeUnsignedNumber(trace.completedAtMs, 0, Number.MAX_SAFE_INTEGER) || + !isSafeUnsignedNumber(trace.candidateBatchSize, 0, 4_096) || + !isSafeUnsignedNumber(trace.hardDeadlineMs, 10, 75_000) || + !isSafeUnsignedNumber(trace.maxCallsPerProvider, 1, 128) || + !isSafeUnsignedNumber(trace.elapsedMs, 0, 75_000) || + trace.completedAtMs < trace.startedAtMs || + trace.completedAtMs - trace.startedAtMs !== trace.elapsedMs || + !isSafeUnsignedNumber(providerACallCount, 0, 11_008) || + !isSafeUnsignedNumber(providerBCallCount, 0, 11_008) + ) { + throw invalidInput("rpc", "projection-execution-trace-call-count"); + } + const calls = trace.calls.map((call) => { + assertExactRecordFields( + call, + PROJECTION_EXECUTION_CALL_V1_FIELDS, + "projection-execution-trace-call", + ); + const operation = + PROJECTION_EXECUTION_OPERATION_TAGS[ + call.operation as keyof typeof PROJECTION_EXECUTION_OPERATION_TAGS + ]; + const outcome = + PROJECTION_EXECUTION_OUTCOME_TAGS[ + call.outcome as keyof typeof PROJECTION_EXECUTION_OUTCOME_TAGS + ]; + if ( + operation === undefined || + outcome === undefined || + !isSafeUnsignedNumber(call.attempt, 1, 3) || + !isSafeUnsignedNumber(call.startedOffsetMs, 0, 75_000) || + !isSafeUnsignedNumber(call.durationMs, 0, 75_000) + ) { + throw invalidInput("rpc", "projection-execution-trace-call-enum"); + } + return Buffer.concat([ + exactFramedText(call.providerIdentity), + exactFramedText(call.providerVendorGroup), + exactHex(call.providerEndpointCommitment, 32), + exactHex(call.providerOriginCommitment, 32), + Buffer.from([operation]), + exactSafeUnsignedNumber(call.attempt, 4), + exactSafeUnsignedNumber(call.startedOffsetMs, 4), + exactSafeUnsignedNumber(call.durationMs, 4), + Buffer.from([outcome]), + ]); + }); + return Buffer.concat([ + PROJECTION_EXECUTION_TRACE_V1_PREFIX, + exactSafeUnsignedNumber(trace.startedAtMs, 8), + exactSafeUnsignedNumber(trace.completedAtMs, 8), + exactSafeUnsignedNumber(trace.candidateBatchSize, 4), + exactSafeUnsignedNumber(trace.hardDeadlineMs, 4), + exactSafeUnsignedNumber(trace.maxCallsPerProvider, 4), + exactSafeUnsignedNumber(trace.elapsedMs, 4), + exactSafeUnsignedNumber(providerACallCount, 4), + exactSafeUnsignedNumber(providerBCallCount, 4), + fixedUnsigned(calls.length, 4), + ...calls, + ]); +} + +export function projectionExecutionTraceCommitmentV1( + trace: unknown, +): HexBytes32 { + return sha256(projectionExecutionTracePreimageV1(trace)); +} + +function encodeProviderEvidenceType(type: string, value: unknown): Buffer { + if (type === "u32") return fixedUnsigned(value, 4); + if (type === "u64") return fixedUnsigned(value, 8); + if (type === "uuid16") return exactUuid(value); + if (type === "varutf8") { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 512 || + /[\u0000-\u001f\u007f]/u.test(value) + ) { + throw invalidInput("rpc", "provider-evidence-text"); + } + return framed(Buffer.from(value, "utf8")); + } + if (type === "varbytes") return framed(exactHex(value)); + const fixed = /^bytes(\d+)$/u.exec(type); + if (fixed) return exactHex(value, Number(fixed[1])); + const optional = /^optional<(.+)>$/u.exec(type); + if (optional) { + return value === null + ? Buffer.from([0]) + : Buffer.concat([ + Buffer.from([1]), + encodeProviderEvidenceType(optional[1]!, value), + ]); + } + const array = /^array<(.+)>$/u.exec(type); + if (array) { + if (!Array.isArray(value) || value.length > 4_096) { + throw invalidInput("rpc", "provider-evidence-array"); + } + return Buffer.concat([ + fixedUnsigned(value.length, 4), + ...value.map((item) => encodeProviderEvidenceType(array[1]!, item)), + ]); + } + throw invalidInput("rpc", "provider-evidence-schema"); +} + +export type ProviderEvidenceV2 = Readonly<{ + encodingVersion: 2; + canonicalPreimage: Uint8Array; + contentFingerprint: HexBytes32; +}>; + +export type ProviderEvidenceV3 = Readonly<{ + encodingVersion: 3; + canonicalPreimage: Uint8Array; + contentFingerprint: HexBytes32; +}>; + +function encodeProviderEvidence(input: Readonly<{ + encodingVersion: Version; + prefix: Buffer; + tag: number; + schema: readonly ProviderEvidenceField[]; + values: Readonly>; +}>): Readonly<{ + encodingVersion: Version; + canonicalPreimage: Uint8Array; + contentFingerprint: HexBytes32; +}> { + const { schema, values } = input; + if ( + values === null || + typeof values !== "object" || + Array.isArray(values) || + Object.getPrototypeOf(values) !== Object.prototype + ) { + throw invalidInput("rpc", "provider-evidence"); + } + const names = schema.map(([name]) => name); + const actualNames = Object.keys(values).sort(); + const expectedNames = [...names].sort(); + if ( + actualNames.length !== expectedNames.length || + actualNames.some((name, index) => name !== expectedNames[index]) + ) { + throw invalidInput("rpc", "provider-evidence-fields"); + } + const canonicalPreimage = Buffer.concat([ + input.prefix, + Buffer.from([input.tag]), + ...schema.map(([name, type]) => + encodeProviderEvidenceType(type, values[name]), + ), + ]); + return Object.freeze({ + encodingVersion: input.encodingVersion, + canonicalPreimage, + contentFingerprint: keccak256( + `0x${canonicalPreimage.toString("hex")}`, + ), + }); +} + +export function providerEvidenceV2( + subtype: ProviderEvidenceV2Subtype, + input: Readonly>, +): ProviderEvidenceV2 { + return encodeProviderEvidence({ + encodingVersion: 2, + prefix: PROVIDER_EVIDENCE_V2_PREFIX, + tag: PROVIDER_EVIDENCE_V2_TAGS[subtype], + schema: PROVIDER_EVIDENCE_V2_SCHEMAS[subtype], + values: input, + }); +} + +export function providerEvidenceV3( + subtype: ProviderEvidenceV3Subtype, + input: Readonly>, +): ProviderEvidenceV3 { + return encodeProviderEvidence({ + encodingVersion: 3, + prefix: PROVIDER_EVIDENCE_V3_PREFIX, + tag: PROVIDER_EVIDENCE_V3_TAGS[subtype], + schema: PROVIDER_EVIDENCE_V3_SCHEMAS[subtype], + values: input, + }); +} + +export function canonicalUint32DecimalText( + value: unknown, + operation = "uint32", +): string { + let text: string; + if (typeof value === "bigint") { + text = value.toString(); + } else if (typeof value === "number") { + if (!Number.isSafeInteger(value)) { + throw invalidInput("rpc", operation); + } + text = String(value); + } else { + try { + text = parseNonnegativeIntegerText(value); + } catch { + throw invalidInput("rpc", operation); + } + } + const parsed = BigInt(text); + if (parsed > UINT32_MAXIMUM) { + throw invalidInput("rpc", operation); + } + return text; +} + +export type CanonicalCoverageLog = Readonly<{ + address: HexAddress; + blockNumber: string; + blockHash: HexBytes32; + transactionHash: HexBytes32; + transactionIndex: string; + blockGlobalLogIndex: string; + topics: readonly HexBytes32[]; + data: HexData; + commitment: HexBytes32; +}>; + +export function canonicalCoverageLog(value: { + address: Hex; + blockNumber: bigint | null; + blockHash: Hex | null; + transactionHash: Hex | null; + transactionIndex: number | null; + logIndex: number | null; + removed?: boolean; + topics: readonly Hex[]; + data: Hex; +}): CanonicalCoverageLog { + if ( + value === null || + typeof value !== "object" || + value.blockNumber === null || + typeof value.blockNumber !== "bigint" || + value.blockNumber < 0n || + value.blockHash === null || + value.transactionHash === null || + value.transactionIndex === null || + value.logIndex === null || + value.removed !== false || + !Array.isArray(value.topics) || + value.topics.length < 1 || + value.topics.length > 4 + ) { + throw validationError("rpc", "coverage-log"); + } + let address: HexAddress; + let blockHash: HexBytes32; + let transactionHash: HexBytes32; + let data: HexData; + let topics: readonly HexBytes32[]; + let transactionIndex: string; + let blockGlobalLogIndex: string; + try { + address = canonicalAddress(value.address); + blockHash = canonicalBytes32(value.blockHash); + transactionHash = canonicalBytes32(value.transactionHash); + data = canonicalRawData(value.data); + topics = Object.freeze(value.topics.map(canonicalBytes32)); + transactionIndex = canonicalUint32DecimalText( + value.transactionIndex, + "coverage-transaction-index", + ); + blockGlobalLogIndex = canonicalUint32DecimalText( + value.logIndex, + "coverage-log-index", + ); + } catch { + throw validationError("rpc", "coverage-log"); + } + const blockNumber = value.blockNumber.toString(); + const commitment = keccak256( + encodeAbiParameters( + [ + { type: "address" }, + { type: "uint256" }, + { type: "bytes32" }, + { type: "bytes32" }, + { type: "uint32" }, + { type: "uint32" }, + { type: "bytes32[]" }, + { type: "bytes" }, + ], + [ + address, + BigInt(blockNumber), + blockHash, + transactionHash, + Number(transactionIndex), + Number(blockGlobalLogIndex), + [...topics], + data, + ], + ), + ); + return Object.freeze({ + address, + blockNumber, + blockHash, + transactionHash, + transactionIndex, + blockGlobalLogIndex, + topics, + data, + commitment, + }); +} + +export function coverageLogPlacementKey(log: CanonicalCoverageLog): string { + return `${log.blockNumber}:${log.blockGlobalLogIndex}`; +} diff --git a/lib/data-pipeline/public-route-queries.server.ts b/lib/data-pipeline/public-route-queries.server.ts new file mode 100644 index 00000000..3d0ad892 --- /dev/null +++ b/lib/data-pipeline/public-route-queries.server.ts @@ -0,0 +1,447 @@ +import "server-only"; + +import type { + IndexedChartDataV2, + IndexedClassicV3ProfileDataV2, + IndexedCreatorProfileDataV2, + IndexedExploreListDataV2, + IndexedLaunchLookupDataV2, + IndexedRouteEnvelopeV2, + IndexedRowSourceV2, + IndexedStockPairedProfileDataV2, + IndexedTokenDetailDataV2, +} from "./route-adapters.server"; +import type { + IndexedRouteSnapshotQueries, +} from "./postgres-read-model.server"; +import type { + PostgresParameter, + PostgresTransaction, +} from "./postgres"; +import type { ReviewedRouteScope } from "./route-coordinator.server"; + +const UUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const BYTES32 = /^0x[0-9a-f]{64}$/; + +type BoundaryRow = Record; +type ReadyEnvelope = Extract, { status: "ready" }>; + +const DISCOVERY_SCOPE = Object.freeze([ + Object.freeze({ model: "classic", releaseVersion: "classic-v2" }), + Object.freeze({ model: "classic", releaseVersion: "classic-v3" }), + Object.freeze({ model: "stock-paired", releaseVersion: "stock-paired-v1" }), + Object.freeze({ model: "stock-paired", releaseVersion: "stock-paired-v2" }), + Object.freeze({ model: "stock-paired", releaseVersion: "stock-paired-v3" }), +]) satisfies readonly ReviewedRouteScope[]; + +const CLASSIC_V3_SCOPE = Object.freeze([ + Object.freeze({ model: "classic", releaseVersion: "classic-v3" }), +]) satisfies readonly ReviewedRouteScope[]; + +const STOCK_SCOPE = Object.freeze([ + Object.freeze({ model: "stock-paired", releaseVersion: "stock-paired-v1" }), + Object.freeze({ model: "stock-paired", releaseVersion: "stock-paired-v2" }), + Object.freeze({ model: "stock-paired", releaseVersion: "stock-paired-v3" }), +]) satisfies readonly ReviewedRouteScope[]; + +function fail(field: string): never { + throw new Error(`Invalid indexed public route ${field}`); +} + +function object(value: unknown, field: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) fail(field); + return value as Record; +} + +function array(value: unknown, field: string): readonly unknown[] { + if (!Array.isArray(value)) fail(field); + return value; +} + +function nonnegativeInteger(value: unknown, field: string): string { + if (typeof value === "bigint") { + if (value < 0n) fail(field); + return value.toString(); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) fail(field); + return String(value); + } + if ( + typeof value !== "string" || + !/^(?:0|[1-9]\d*)$/.test(value) || + value.length > 78 + ) { + fail(field); + } + return value; +} + +function integerNumber(value: unknown, field: string): number { + const parsed = Number(nonnegativeInteger(value, field)); + if (!Number.isSafeInteger(parsed)) fail(field); + return parsed; +} + +function byteaBytes32(value: unknown, field: string): `0x${string}` { + if (value instanceof Uint8Array) { + if (value.byteLength !== 32) fail(field); + return `0x${Array.from(value, (byte) => + byte.toString(16).padStart(2, "0"), + ).join("")}`; + } + if (typeof value === "string") { + const normalized = value.startsWith("\\x") + ? `0x${value.slice(2).toLowerCase()}` + : value.toLowerCase(); + if (BYTES32.test(normalized)) return normalized as `0x${string}`; + } + fail(field); +} + +function scopeKey(scope: ReviewedRouteScope): string { + return `${scope.model}:${scope.releaseVersion}`; +} + +function parsedScope(value: unknown, field: string): ReviewedRouteScope { + const candidate = object(value, field); + const model = candidate.model; + const releaseVersion = candidate.releaseVersion; + if ( + (model !== "classic" && model !== "stock-paired") || + ![ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ].includes(String(releaseVersion)) || + (String(releaseVersion).startsWith("classic-") + ? model !== "classic" + : model !== "stock-paired") + ) { + fail(field); + } + return { + model, + releaseVersion: releaseVersion as ReviewedRouteScope["releaseVersion"], + }; +} + +function exactScopes( + value: unknown, + expected: readonly ReviewedRouteScope[], + field: string, +) { + const scopes = array(value, field).map((entry) => parsedScope(entry, field)); + const actualKeys = scopes.map(scopeKey).sort(); + const expectedKeys = expected.map(scopeKey).sort(); + if ( + actualKeys.length !== expectedKeys.length || + actualKeys.some((key, index) => key !== expectedKeys[index]) || + new Set(actualKeys).size !== actualKeys.length + ) { + fail(field); + } + return scopes; +} + +function matchingRecordScopes( + value: unknown, + expected: readonly ReviewedRouteScope[], +) { + const actualKeys = array(value, "record scope evidence") + .map((entry) => parsedScope(entry, "record scope evidence")) + .map(scopeKey) + .sort(); + const expectedKeys = expected.map(scopeKey).sort(); + if ( + actualKeys.length !== expectedKeys.length || + actualKeys.some((key, index) => key !== expectedKeys[index]) + ) { + fail("record scope evidence"); + } +} + +function sourceOf(value: unknown, field: string): IndexedRowSourceV2 { + const source = object(object(value, field).source, `${field} source`); + parsedScope( + { + model: source.modelVersion, + releaseVersion: source.releaseVersion, + }, + `${field} source scope`, + ); + return source as IndexedRowSourceV2; +} + +function routeEvidence( + value: unknown, + expectedScope: readonly ReviewedRouteScope[], +) { + const evidence = array(value, "route evidence"); + const scopes = evidence.map((entry, index) => { + const row = object(entry, `route evidence ${index}`); + for (const key of ["parityRecordId", "reconciliationId", "parityBindingId"]) { + if (typeof row[key] !== "string" || !UUID.test(row[key] as string)) { + fail(`route evidence ${key}`); + } + } + for (const key of ["parityEvidenceCommitment", "parityBindingCommitment"]) { + if ( + typeof row[key] !== "string" || + !BYTES32.test((row[key] as string).toLowerCase()) + ) { + fail(`route evidence ${key}`); + } + } + return parsedScope( + { + model: row.model ?? row.modelVersion, + releaseVersion: row.releaseVersion, + }, + `route evidence ${index} scope`, + ); + }); + exactScopes(scopes, expectedScope, "route evidence scope"); +} + +function readyEnvelope(value: unknown): ReadyEnvelope { + const envelope = object(value, "payload"); + if (envelope.status !== "ready") fail("payload status"); + object(envelope.snapshot, "payload snapshot"); + object(envelope.data, "payload data"); + return envelope as ReadyEnvelope; +} + +function expectedHttpStatus( + kind: + | "explore" + | "token" + | "chart" + | "creator" + | "classic-profile" + | "stock-profile" + | "launch", + envelope: ReadyEnvelope, +): number { + if (kind === "token") { + return (envelope.data as IndexedTokenDetailDataV2).token ? 200 : 404; + } + if (kind === "launch") { + const data = envelope.data as IndexedLaunchLookupDataV2; + return data.surface === "stock-paired" && data.resolution === "pending" + ? 202 + : 200; + } + return 200; +} + +function sourcesFor( + kind: + | "explore" + | "token" + | "chart" + | "creator" + | "classic-profile" + | "stock-profile" + | "launch", + envelope: ReadyEnvelope, +): readonly IndexedRowSourceV2[] { + if (kind === "explore") { + return (envelope.data as IndexedExploreListDataV2).tokens.map( + (token, index) => sourceOf(token, `explore token ${index}`), + ); + } + if (kind === "token") { + const token = (envelope.data as IndexedTokenDetailDataV2).token; + return token ? [sourceOf(token, "token detail")] : []; + } + if (kind === "chart") { + return [sourceOf({ source: (envelope.data as IndexedChartDataV2).source }, "chart")]; + } + if (kind === "creator") { + const data = envelope.data as IndexedCreatorProfileDataV2; + return [ + ...data.tokens.map((token, index) => + sourceOf(token, `creator token ${index}`), + ), + ...data.claims.map((claim, index) => + sourceOf(claim, `creator claim ${index}`), + ), + ]; + } + if (kind === "classic-profile") { + return (envelope.data as IndexedClassicV3ProfileDataV2).rewards.map( + (reward, index) => sourceOf(reward, `Classic reward ${index}`), + ); + } + if (kind === "stock-profile") { + return (envelope.data as IndexedStockPairedProfileDataV2).rewards.map( + (reward, index) => sourceOf(reward, `Stock reward ${index}`), + ); + } + const token = (envelope.data as IndexedLaunchLookupDataV2).token; + return token ? [sourceOf(token, "launch token")] : []; +} + +async function readEnvelope(input: { + transaction: PostgresTransaction; + sql: string; + values: readonly PostgresParameter[]; + kind: + | "explore" + | "token" + | "chart" + | "creator" + | "classic-profile" + | "stock-profile" + | "launch"; + scope: readonly ReviewedRouteScope[]; +}): Promise> { + const rows = await input.transaction.query( + input.sql, + input.values, + ); + if (rows.length === 0) { + return { status: "not-ready", reason: "reconciliation-incomplete" }; + } + if (rows.length !== 1) fail("row cardinality"); + const row = rows[0]!; + if (row.payload_complete !== true) fail("payload completeness"); + const envelope = readyEnvelope(row.payload); + const sources = sourcesFor(input.kind, envelope); + const recordCount = integerNumber(row.record_count, "record count"); + if (recordCount !== sources.length) fail("record count evidence"); + const recordScope = sources.map((source) => + parsedScope( + { + model: source.modelVersion, + releaseVersion: source.releaseVersion, + }, + "record source scope", + ), + ); + matchingRecordScopes(row.record_scopes, recordScope); + routeEvidence(row.route_evidence, input.scope); + + const snapshot = object(envelope.snapshot, "snapshot"); + const checkpointBlock = nonnegativeInteger( + row.comparison_checkpoint_block_number, + "comparison checkpoint block", + ); + const checkpointHash = byteaBytes32( + row.comparison_checkpoint_block_hash, + "comparison checkpoint hash", + ); + if ( + snapshot.blockNumber !== checkpointBlock || + typeof snapshot.blockHash !== "string" || + snapshot.blockHash.toLowerCase() !== checkpointHash + ) { + fail("comparison checkpoint binding"); + } + exactScopes( + array(snapshot.releasePointers, "snapshot pointers").map( + (pointer, index) => { + const parsed = object(pointer, `snapshot pointer ${index}`); + return parsedScope( + { + model: parsed.modelVersion, + releaseVersion: parsed.releaseVersion, + }, + `snapshot pointer ${index}`, + ); + }, + ), + input.scope, + "snapshot scope", + ); + if ( + integerNumber(row.http_status, "http status") !== + expectedHttpStatus(input.kind, envelope) + ) { + fail("HTTP status evidence"); + } + return envelope; +} + +export const postgresPublicRouteQueries: IndexedRouteSnapshotQueries = + Object.freeze({ + explore(transaction, request) { + return readEnvelope({ + transaction, + sql: `select * from programmable_private.get_public_explore_page_v1($1, $2, $3, $4, $5)`, + values: [ + request.chainId, + request.query, + request.sort, + request.page, + request.pageSize, + ], + kind: "explore", + scope: DISCOVERY_SCOPE, + }); + }, + tokenDetail(transaction, request) { + return readEnvelope({ + transaction, + sql: `select * from programmable_private.get_public_explore_token_v1($1, $2)`, + values: [request.chainId, request.address], + kind: "token", + scope: DISCOVERY_SCOPE, + }); + }, + tokenChart(transaction, request) { + return readEnvelope({ + transaction, + sql: `select * from programmable_private.get_public_token_chart_v1($1, $2, $3)`, + values: [request.chainId, request.address, request.range], + kind: "chart", + scope: DISCOVERY_SCOPE, + }); + }, + creatorProfile(transaction, request) { + return readEnvelope({ + transaction, + sql: `select * from programmable_private.get_public_creator_profile_v1($1, $2)`, + values: [request.chainId, request.account], + kind: "creator", + scope: DISCOVERY_SCOPE, + }); + }, + classicV3Profile(transaction, request) { + return readEnvelope({ + transaction, + sql: `select * from programmable_private.get_public_classic_v3_profile_v1($1, $2)`, + values: [request.chainId, request.account], + kind: "classic-profile", + scope: CLASSIC_V3_SCOPE, + }); + }, + stockPairedProfile(transaction, request) { + return readEnvelope({ + transaction, + sql: `select * from programmable_private.get_public_stock_paired_profile_v1($1, $2)`, + values: [request.chainId, request.account], + kind: "stock-profile", + scope: STOCK_SCOPE, + }); + }, + launchLookup(transaction, request) { + return readEnvelope({ + transaction, + sql: `select * from programmable_private.get_public_launch_lookup_v1($1, $2, $3, $4)`, + values: [ + request.chainId, + request.surface, + request.account, + request.transactionHash, + ], + kind: "launch", + scope: + request.surface === "classic-v3" ? CLASSIC_V3_SCOPE : STOCK_SCOPE, + }); + }, + }); diff --git a/lib/data-pipeline/public-route-readiness.server.ts b/lib/data-pipeline/public-route-readiness.server.ts new file mode 100644 index 00000000..ecfd91fd --- /dev/null +++ b/lib/data-pipeline/public-route-readiness.server.ts @@ -0,0 +1,648 @@ +import "server-only"; + +import { after } from "next/server"; + +import { + ALL_REVIEWED_ROUTE_SCOPES, + authorizeRouteReleaseProbe, + coordinateRouteRead, + validatedRecordScopeEvidence, + type AuthorizedReleaseProbe, + type CoordinatedRouteRead, + type IndexedRouteResult, + type IndexedProjectionVersion, + type IndexedRouteKey, + type IndexedRouteSnapshot, + type ReviewedRouteScope, + type RouteCheckpoint, + type RouteComparisonSchema, + type RouteReadiness, +} from "./route-coordinator.server"; +import type { PostgresTransaction } from "./postgres"; +import { + createPostgresPublicRouteSnapshotAdapters, + type AdaptedIndexedRouteSnapshotV2, +} from "./postgres-read-model.server"; +import { postgresPublicRouteQueries } from "./public-route-queries.server"; +import type { + IndexedRowSourceV2, + IndexedSnapshotIdentityV2, +} from "./route-adapters.server"; + +const SHADOW_PROBE_QUERY_PARAMETER = "__read_model_probe"; + +/** + * Converts one fresh, authenticated cache nonce into an unforgeable capability + * only after the private database consumes it atomically. Invalid, duplicated, + * stale, replayed and unauthenticated values remain visible to normal route + * query validation and can never reach the indexed read path. + */ +export async function preparePublicRouteRequest( + search: URLSearchParams, + headers: Headers, + route: IndexedRouteKey, +): Promise> { + const canonical = new URLSearchParams(search); + const nonces = canonical.getAll(SHADOW_PROBE_QUERY_PARAMETER); + let releaseProbe: AuthorizedReleaseProbe | null = null; + try { + releaseProbe = + nonces.length === 1 + ? await authorizeRouteReleaseProbe(headers, nonces[0]!, route) + : null; + } catch { + return Object.freeze({ + searchParams: canonical, + probeFailure: Response.json( + { error: "release_probe_temporarily_unavailable" }, + { + status: 503, + headers: { + "Cache-Control": "private, no-store", + "Retry-After": "1", + }, + }, + ), + }); + } + if (releaseProbe) { + canonical.delete(SHADOW_PROBE_QUERY_PARAMETER); + } + return Object.freeze({ + searchParams: canonical, + ...(releaseProbe ? { releaseProbe } : {}), + }); +} + +export const PUBLIC_INDEXED_ROUTE_READS = + createPostgresPublicRouteSnapshotAdapters({ + queries: postgresPublicRouteQueries, + }); + +export const CLASSIC_V3_ROUTE_SCOPE = Object.freeze([ + Object.freeze({ model: "classic", releaseVersion: "classic-v3" }), +]) satisfies readonly ReviewedRouteScope[]; + +export const STOCK_PAIRED_ROUTE_SCOPES = Object.freeze( + ALL_REVIEWED_ROUTE_SCOPES.filter( + (scope) => scope.model === "stock-paired", + ), +); + +export const PUBLIC_DISCOVERY_ROUTE_SCOPES = ALL_REVIEWED_ROUTE_SCOPES; + +const UUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._+:/-]{0,127}$/; + +type ReadinessRow = Record; + +function text(value: unknown, field: string): string { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 128 || + /[\u0000-\u001f\u007f]/u.test(value) + ) { + throw new Error(`Invalid route readiness ${field}`); + } + return value; +} + +function uuid(value: unknown, field: string): string { + const parsed = text(value, field).toLowerCase(); + if (!UUID.test(parsed)) throw new Error(`Invalid route readiness ${field}`); + return parsed; +} + +function nullableUuid(value: unknown): string | null { + return value === null || value === undefined + ? null + : uuid(value, "uuid"); +} + +function integer(value: unknown, field: string): string { + if (typeof value === "bigint") { + if (value < 0n) throw new Error(`Invalid route readiness ${field}`); + return value.toString(); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error(`Invalid route readiness ${field}`); + } + return String(value); + } + if ( + typeof value !== "string" || + !/^(?:0|[1-9]\d*)$/.test(value) || + value.length > 78 + ) { + throw new Error(`Invalid route readiness ${field}`); + } + return value; +} + +function nullableInteger(value: unknown): string | null { + return value === null || value === undefined + ? null + : integer(value, "integer"); +} + +function boolean(value: unknown, field: string): boolean { + if (typeof value !== "boolean") { + throw new Error(`Invalid route readiness ${field}`); + } + return value; +} + +function nullableBoolean(value: unknown): boolean | null { + return value === null || value === undefined + ? null + : boolean(value, "boolean"); +} + +function bytes32(value: unknown, field: string): `0x${string}` { + if (value instanceof Uint8Array) { + if (value.byteLength !== 32) { + throw new Error(`Invalid route readiness ${field}`); + } + return `0x${Array.from(value, (byte) => + byte.toString(16).padStart(2, "0"), + ).join("")}`; + } + if (typeof value === "string") { + if (/^\\x[0-9a-fA-F]{64}$/.test(value)) { + return `0x${value.slice(2).toLowerCase()}`; + } + if (/^0x[0-9a-fA-F]{64}$/.test(value)) { + return value.toLowerCase() as `0x${string}`; + } + } + throw new Error(`Invalid route readiness ${field}`); +} + +function nullableBytes32(value: unknown): `0x${string}` | null { + return value === null || value === undefined + ? null + : bytes32(value, "bytes32"); +} + +function releaseModel(scope: ReviewedRouteScope): string { + return scope.model; +} + +function scopeKey(scope: ReviewedRouteScope): string { + return `${scope.model}:${scope.releaseVersion}`; +} + +function exactProjectionVersion(row: ReadinessRow): IndexedProjectionVersion { + const projectorVersion = text(row.projector_version, "projector version"); + const sourceGroup = text(row.source_group, "source group"); + if (!IDENTIFIER.test(projectorVersion) || !IDENTIFIER.test(sourceGroup)) { + throw new Error("Invalid route readiness identifiers"); + } + const pointerGeneration = integer( + row.pointer_generation, + "pointer generation", + ); + const checkpointGeneration = integer( + row.checkpoint_generation, + "checkpoint generation", + ); + if (pointerGeneration === "0" || checkpointGeneration === "0") { + throw new Error("Invalid route readiness generation"); + } + const blockNumber = integer( + row.checkpoint_block_number, + "checkpoint block number", + ); + const safeBlockNumber = integer(row.safe_block_number, "safe block number"); + const confirmations = integer( + row.checkpoint_confirmations, + "checkpoint confirmations", + ); + if ( + BigInt(safeBlockNumber) < BigInt(blockNumber) || + BigInt(confirmations) > 1_024n + ) { + throw new Error("Invalid route readiness safe checkpoint"); + } + return Object.freeze({ + checkpointId: uuid(row.checkpoint_id, "checkpoint id"), + sourceGroup, + projectorVersion, + epochId: uuid(row.epoch_id, "epoch id"), + pointerGeneration, + checkpointGeneration, + reorgGeneration: integer(row.reorg_generation, "reorg generation"), + blockNumber, + blockHash: bytes32(row.checkpoint_block_hash, "checkpoint block hash"), + }); +} + +function exactParityStatus( + row: ReadinessRow, + version: IndexedProjectionVersion, +): "current" | "pending" | "stale" | "mismatch" | "missing" { + const parityRecordId = nullableUuid(row.parity_record_id); + const parityStatus = text(row.parity_status, "parity status").toLowerCase(); + if ( + !["current", "pending", "stale", "mismatch", "missing"].includes( + parityStatus, + ) + ) { + throw new Error("Invalid route readiness parity status"); + } + const parityIsMatch = nullableBoolean(row.parity_is_match); + const mismatchCount = nullableInteger(row.reconciliation_mismatch_count); + + if (parityStatus === "missing") { + if (parityRecordId !== null) { + throw new Error("Invalid missing route parity record"); + } + return "missing"; + } + if (!parityRecordId) { + throw new Error("Invalid route parity record"); + } + if ( + parityIsMatch === false || + (mismatchCount !== null && mismatchCount !== "0") || + parityStatus === "mismatch" + ) { + return "mismatch"; + } + if (parityStatus === "pending") { + return "pending"; + } + if (parityStatus === "stale") return "stale"; + + const parityCheckpointId = nullableUuid(row.parity_checkpoint_id); + const parityCheckpointGeneration = nullableInteger( + row.parity_checkpoint_generation, + ); + const parityReorgGeneration = nullableInteger(row.parity_reorg_generation); + const parityBlockNumber = nullableInteger(row.parity_block_number); + const parityBlockHash = nullableBytes32(row.parity_block_hash); + const parityBindingId = nullableUuid(row.parity_binding_id); + const parityBindingCommitment = nullableBytes32( + row.parity_binding_commitment, + ); + const parityEvidenceCommitment = nullableBytes32( + row.parity_evidence_commitment, + ); + const reconciliationId = nullableUuid(row.reconciliation_id); + const sourceFromBlock = nullableInteger(row.parity_source_from_block); + const sourceToBlock = nullableInteger(row.parity_source_to_block); + const sourceContainsCheckpoint = + sourceFromBlock !== null && + sourceToBlock !== null && + BigInt(sourceFromBlock) <= BigInt(version.blockNumber) && + BigInt(sourceToBlock) >= BigInt(version.blockNumber); + + if ( + parityIsMatch === true && + mismatchCount === "0" && + parityCheckpointId === version.checkpointId && + parityCheckpointGeneration === version.checkpointGeneration && + parityReorgGeneration === version.reorgGeneration && + parityBlockNumber === version.blockNumber && + parityBlockHash === version.blockHash && + parityBindingId !== null && + parityBindingCommitment !== null && + parityEvidenceCommitment !== null && + reconciliationId !== null && + sourceContainsCheckpoint && + parityStatus === "current" + ) { + return "current"; + } + return "stale"; +} + +function readinessMember( + scope: ReviewedRouteScope, + row: ReadinessRow | undefined, +): RouteReadiness[number] { + if (!row) { + return Object.freeze({ + ...scope, + eligibility: "ineligible" as const, + parity: "missing" as const, + }); + } + if ( + row.release_id !== scope.releaseVersion || + row.model_id !== releaseModel(scope) + ) { + throw new Error("Route readiness scope does not match"); + } + const eligible = + row.route_status === "eligible" && + row.eligibility_status === "eligible" && + row.route_mode === "indexed"; + if (!eligible) { + return Object.freeze({ + ...scope, + eligibility: "ineligible" as const, + parity: "missing" as const, + }); + } + const version = exactProjectionVersion(row); + const parity = exactParityStatus(row, version); + return Object.freeze({ + ...scope, + eligibility: "eligible" as const, + parity, + ...(parity === "current" ? { version } : {}), + }); +} + +/** Reads immutable activation evidence for the exact requested route scope. */ +export async function readExactRouteSnapshotReadiness(input: { + transaction: PostgresTransaction; + route: IndexedRouteKey; + chainId: 1; + scope: readonly ReviewedRouteScope[]; +}): Promise { + const releases = input.scope.map((scope) => scope.releaseVersion); + const models = input.scope.map((scope) => scope.model); + const rows = await input.transaction.query( + `select + route_key, chain_id, release_id, model_id, source_group, + route_status, eligibility_status, route_mode, + projector_version, epoch_id, pointer_generation, + checkpoint_id, checkpoint_generation, reorg_generation, + checkpoint_block_number, checkpoint_block_hash, + safe_block_number, checkpoint_confirmations, + parity_status, parity_record_id, reconciliation_id, parity_is_match, + parity_source_from_block, parity_source_to_block, + parity_evidence_commitment, reconciliation_mismatch_count, + parity_checkpoint_id, parity_checkpoint_generation, + parity_reorg_generation, parity_block_number, parity_block_hash, + parity_binding_id, parity_binding_commitment + from programmable_private.route_snapshot_readiness_v1 + where route_key = $1 + and chain_id = $2 + and release_id = any($3::text[]) + and model_id = any($4::text[]) + order by release_id, model_id, source_group`, + [input.route, input.chainId, releases, models], + ); + + const selected = new Map(); + for (const row of rows) { + if (row.route_key !== input.route || integer(row.chain_id, "chain") !== "1") { + throw new Error("Route readiness identity does not match"); + } + const key = `${text(row.model_id, "model")}:${text( + row.release_id, + "release", + )}`; + if (!input.scope.some((scope) => scopeKey(scope) === key)) { + throw new Error("Unsupported route readiness release"); + } + if (selected.has(key)) { + // More than one current source group is ambiguous and cannot be served. + throw new Error("Ambiguous route readiness source group"); + } + selected.set(key, row); + } + + const readiness = Object.freeze( + input.scope.map((scope) => + readinessMember(scope, selected.get(scopeKey(scope))), + ), + ); + return Object.freeze({ readiness }); +} + +type PublicAdaptedSnapshot = AdaptedIndexedRouteSnapshotV2; + +function sameScope( + left: ReviewedRouteScope, + right: ReviewedRouteScope, +) { + return ( + left.model === right.model && + left.releaseVersion === right.releaseVersion + ); +} + +function sourceScope(source: IndexedRowSourceV2): ReviewedRouteScope { + if ( + (source.modelVersion !== "classic" && + source.modelVersion !== "stock-paired") || + ![ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ].includes(source.releaseVersion) + ) { + throw new Error("Invalid indexed record scope"); + } + return Object.freeze({ + model: source.modelVersion, + releaseVersion: source.releaseVersion, + }); +} + +function exactPointerVersion( + snapshot: IndexedSnapshotIdentityV2, + scope: ReviewedRouteScope, +) { + const candidates = snapshot.releasePointers.filter( + (pointer) => + pointer.modelVersion === scope.model && + pointer.releaseVersion === scope.releaseVersion, + ); + if (candidates.length !== 1) { + throw new Error("Indexed snapshot scope is incomplete"); + } + const pointer = candidates[0]!; + return Object.freeze({ + checkpointId: pointer.checkpointId, + sourceGroup: pointer.sourceGroup, + projectorVersion: pointer.projectorVersion, + epochId: pointer.epochId, + pointerGeneration: pointer.pointerGeneration, + checkpointGeneration: pointer.checkpointGeneration, + reorgGeneration: pointer.reorgGeneration, + blockNumber: pointer.checkpointBlockNumber, + blockHash: pointer.checkpointBlockHash, + }); +} + +function sameVersion( + left: IndexedProjectionVersion, + right: IndexedProjectionVersion, +) { + return ( + left.checkpointId === right.checkpointId && + left.sourceGroup === right.sourceGroup && + left.projectorVersion === right.projectorVersion && + left.epochId === right.epochId && + left.pointerGeneration === right.pointerGeneration && + left.checkpointGeneration === right.checkpointGeneration && + left.reorgGeneration === right.reorgGeneration && + left.blockNumber === right.blockNumber && + left.blockHash.toLowerCase() === right.blockHash.toLowerCase() + ); +} + +function indexedResult( + adapted: Extract, + route: IndexedRouteKey, + scope: readonly ReviewedRouteScope[], + readiness: RouteReadiness, +): IndexedRouteResult { + if (adapted.routeKey !== route) { + throw new Error("Indexed adapter route does not match"); + } + if ( + adapted.snapshot.releasePointers.length !== scope.length || + scope.some( + (expected) => + !adapted.snapshot.releasePointers.some( + (pointer) => + pointer.modelVersion === expected.model && + pointer.releaseVersion === expected.releaseVersion, + ), + ) + ) { + throw new Error("Indexed adapter scope does not match"); + } + const versions = Object.freeze( + scope.map((member) => { + const pointerVersion = exactPointerVersion(adapted.snapshot, member); + const readyMember = readiness.find((candidate) => + sameScope(candidate, member), + ); + if ( + !readyMember || + readyMember.eligibility !== "eligible" || + readyMember.parity !== "current" || + !readyMember.version || + !sameVersion(pointerVersion, readyMember.version) + ) { + throw new Error("Indexed payload checkpoint does not match readiness"); + } + return Object.freeze({ + ...member, + version: pointerVersion, + }); + }), + ); + const scopeEvidence = validatedRecordScopeEvidence( + adapted.recordSources, + (source) => sourceScope(source), + ); + const headers = new Headers(adapted.response.headers); + headers.set("Content-Type", "application/json; charset=utf-8"); + return Object.freeze({ + source: "indexed" as const, + scope, + scopeEvidence, + versions, + comparisonCheckpoint: Object.freeze({ + blockNumber: adapted.snapshot.blockNumber, + blockHash: adapted.snapshot.blockHash, + }), + response: new Response(JSON.stringify(adapted.response.body), { + status: adapted.response.status, + headers, + }), + }); +} + +export async function readExactPublicRouteSnapshot(input: { + transaction: PostgresTransaction; + route: IndexedRouteKey; + scope: readonly ReviewedRouteScope[]; + indexed: (transaction: PostgresTransaction) => Promise; +}): Promise { + const readinessSnapshot = await readExactRouteSnapshotReadiness({ + transaction: input.transaction, + route: input.route, + chainId: 1, + scope: input.scope, + }); + if ( + readinessSnapshot.readiness.some( + (member) => + member.eligibility !== "eligible" || + member.parity !== "current" || + !member.version, + ) + ) { + return readinessSnapshot; + } + const adapted = await input.indexed(input.transaction); + if (adapted.status !== "ready") return readinessSnapshot; + return Object.freeze({ + readiness: readinessSnapshot.readiness, + indexed: indexedResult( + adapted, + input.route, + input.scope, + readinessSnapshot.readiness, + ), + }); +} + +export type PublicRouteReadInput = Readonly<{ + route: IndexedRouteKey; + scope: readonly ReviewedRouteScope[]; + releaseProbe?: AuthorizedReleaseProbe; + legacy: CoordinatedRouteRead["legacy"]; + indexed: ( + transaction: PostgresTransaction, + ) => Promise; + comparisonSchema?: RouteComparisonSchema; +}>; + +export function publicSnapshotCheckpoint( + value: RouteCheckpoint | null | undefined, +): RouteCheckpoint | undefined { + return value + ? Object.freeze({ + blockNumber: value.blockNumber, + blockHash: value.blockHash, + }) + : undefined; +} + +/** + * Central route wiring. Readiness and the route payload are read in one + * repeatable-read transaction. Route flags stay off until parity, load and + * production lifecycle evidence pass the release gate. + */ +export async function coordinatePublicRouteRead( + input: PublicRouteReadInput, +): Promise { + return coordinateRouteRead({ + route: input.route, + scope: input.scope, + ...(input.releaseProbe ? { releaseProbe: input.releaseProbe } : {}), + legacy: input.legacy, + indexedSnapshot: (transaction) => + readExactPublicRouteSnapshot({ + transaction, + route: input.route, + scope: input.scope, + indexed: input.indexed, + }), + ...(input.comparisonSchema + ? { comparisonSchema: input.comparisonSchema } + : {}), + scheduleShadowComparison(task) { + after(task); + }, + }); +} diff --git a/lib/data-pipeline/quicknode-stream-wake.server.ts b/lib/data-pipeline/quicknode-stream-wake.server.ts new file mode 100644 index 00000000..b00e2693 --- /dev/null +++ b/lib/data-pipeline/quicknode-stream-wake.server.ts @@ -0,0 +1,166 @@ +import "server-only"; + +import { createHmac, timingSafeEqual } from "node:crypto"; +import { gunzipSync } from "node:zlib"; + +const MAXIMUM_ENCODED_BODY_BYTES = 64 * 1024; +const MAXIMUM_DECODED_BODY_BYTES = 128 * 1024; +const MAXIMUM_TIMESTAMP_AGE_SECONDS = 5 * 60; +const MAXIMUM_FUTURE_SKEW_SECONDS = 30; +const ASCII_HEADER = /^[\x21-\x7e]+$/u; +const HEX_SIGNATURE = /^[0-9a-f]{64}$/iu; +const CANONICAL_TIMESTAMP = /^(?:0|[1-9]\d{0,11})$/u; + +type Environment = Readonly>; + +export type QuickNodeStreamWake = Readonly<{ + timestamp: string; + payloadBytes: number; +}>; + +export class QuickNodeStreamWakeError extends Error { + readonly status: 400 | 401 | 413 | 503; + + constructor(status: 400 | 401 | 413 | 503) { + super("QuickNode stream wake rejected"); + this.name = "QuickNodeStreamWakeError"; + this.status = status; + } +} + +function configuredSecret(env: Environment): string { + const secret = env.PROGRAMMABLE_QUICKNODE_STREAM_SECRET; + const length = secret ? Buffer.byteLength(secret, "utf8") : 0; + if (!secret || length < 32 || length > 1_024) { + throw new QuickNodeStreamWakeError(503); + } + return secret; +} + +function exactHeader( + request: Request, + name: string, + maximumLength: number, +): string { + const value = request.headers.get(name); + if ( + !value || + value.length > maximumLength || + !ASCII_HEADER.test(value) + ) { + throw new QuickNodeStreamWakeError(401); + } + return value; +} + +function assertFreshTimestamp(timestamp: string, nowMs: number) { + if (!CANONICAL_TIMESTAMP.test(timestamp)) { + throw new QuickNodeStreamWakeError(401); + } + const timestampSeconds = Number(timestamp); + const nowSeconds = Math.floor(nowMs / 1_000); + if ( + !Number.isSafeInteger(timestampSeconds) || + timestampSeconds < nowSeconds - MAXIMUM_TIMESTAMP_AGE_SECONDS || + timestampSeconds > nowSeconds + MAXIMUM_FUTURE_SKEW_SECONDS + ) { + throw new QuickNodeStreamWakeError(401); + } +} + +function decodedBody(request: Request, encoded: Uint8Array): Uint8Array { + const contentEncoding = request.headers.get("content-encoding") + ?.trim() + .toLowerCase(); + const hasGzipMagic = encoded[0] === 0x1f && encoded[1] === 0x8b; + if (contentEncoding && contentEncoding !== "identity" && contentEncoding !== "gzip") { + throw new QuickNodeStreamWakeError(400); + } + if (!hasGzipMagic) return encoded; + if (contentEncoding !== "gzip") { + throw new QuickNodeStreamWakeError(400); + } + try { + return gunzipSync(encoded, { + maxOutputLength: MAXIMUM_DECODED_BODY_BYTES, + }); + } catch { + throw new QuickNodeStreamWakeError(400); + } +} + +function parseJsonPayload(decoded: Uint8Array): string { + if ( + decoded.byteLength < 2 || + decoded.byteLength > MAXIMUM_DECODED_BODY_BYTES + ) { + throw new QuickNodeStreamWakeError(413); + } + let payload: string; + try { + payload = new TextDecoder("utf-8", { fatal: true }).decode(decoded); + } catch { + throw new QuickNodeStreamWakeError(400); + } + try { + const value: unknown = JSON.parse(payload); + if (value === null || typeof value !== "object") { + throw new Error("non-object payload"); + } + } catch { + throw new QuickNodeStreamWakeError(400); + } + return payload; +} + +export async function verifyQuickNodeStreamWake( + request: Request, + input: Readonly<{ + env?: Environment; + nowMs?: number; + }> = {}, +): Promise { + const secret = configuredSecret(input.env ?? process.env); + const nonce = exactHeader(request, "x-qn-nonce", 256); + const timestamp = exactHeader(request, "x-qn-timestamp", 32); + const signature = exactHeader(request, "x-qn-signature", 128); + assertFreshTimestamp(timestamp, input.nowMs ?? Date.now()); + if (!HEX_SIGNATURE.test(signature)) { + throw new QuickNodeStreamWakeError(401); + } + + const declaredLength = request.headers.get("content-length"); + if ( + declaredLength !== null && + (!/^(?:0|[1-9]\d{0,9})$/u.test(declaredLength) || + Number(declaredLength) > MAXIMUM_ENCODED_BODY_BYTES) + ) { + throw new QuickNodeStreamWakeError(413); + } + const encoded = new Uint8Array(await request.arrayBuffer()); + if ( + encoded.byteLength < 2 || + encoded.byteLength > MAXIMUM_ENCODED_BODY_BYTES + ) { + throw new QuickNodeStreamWakeError(413); + } + const decoded = decodedBody(request, encoded); + const payload = parseJsonPayload(decoded); + const expected = createHmac("sha256", secret) + .update(nonce, "utf8") + .update(timestamp, "utf8") + .update(payload, "utf8") + .digest(); + const provided = Buffer.from(signature, "hex"); + if ( + provided.length !== expected.length || + !timingSafeEqual(provided, expected) + ) { + throw new QuickNodeStreamWakeError(401); + } + + return Object.freeze({ + timestamp, + payloadBytes: Buffer.byteLength(payload, "utf8"), + }); +} diff --git a/lib/data-pipeline/read-model-performance-capture.server.ts b/lib/data-pipeline/read-model-performance-capture.server.ts new file mode 100644 index 00000000..0ec0d63f --- /dev/null +++ b/lib/data-pipeline/read-model-performance-capture.server.ts @@ -0,0 +1,1471 @@ +import "server-only"; + +import { + canonicalAddress, + canonicalBytes32, + parseNonnegativeIntegerText, + type HexAddress, + type HexBytes32, +} from "./codecs"; +import { selectProjectorRuntimeBinding } from "./candidate-projector-runtime-binding.server"; +import { loadDataPipelineConfig } from "./config"; +import { + verifyEnvioCandidateBatchWithDualRpc, + type CandidateRpcProvider, +} from "./dual-rpc"; +import { + createEnvioClient, + type EnvioCandidate, + type EnvioCandidateCursor, +} from "./envio"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import { + createPostgresExecutor, + type PostgresExecutor, +} from "./postgres"; +import { validatedPostgresConnectionString } from "./postgres-connection.server"; +import { getDataPipelineReleaseBinding } from "./release-binding.server"; +import { createProductionDualRpcProviders } from "./rpc-providers.server"; + +type Environment = Readonly>; + +const SMOKE_PROFILE_ID = "read-model-smoke-v1" as const; +const RELEASE_PROFILE_ID = "read-model-release-v1" as const; +const PROJECTOR_LOGIN_ROLE = "programmable_projector_login" as const; +const PROJECTOR_CAPABILITY_ROLE = "programmable_projector" as const; +const API_READER_LOGIN_ROLE = "programmable_api_reader_login" as const; +const API_READER_CAPABILITY_ROLE = "programmable_api_reader" as const; +const PERMISSION_DENIED_SQLSTATE = "42501" as const; +const HARD_DEADLINE_MS = 75_000; +const SMOKE_REQUIRED_CANDIDATE_COUNT = 8; +const RELEASE_REQUIRED_CANDIDATE_COUNT = 32; +const SMOKE_MAX_CALLS_PER_PROVIDER = 42; +const RELEASE_MAX_CALLS_PER_PROVIDER = 128; +const REQUIRED_KEY_COUNT = 100; +const REQUIRED_MODEL_LAUNCH_COUNT = 32; +const RELEASE_TOKEN_KEY_COUNT = 264; +const RELEASE_MINIMUM_ELIGIBLE_LAUNCH_COUNT = 264; +const MAXIMUM_ELIGIBLE_LAUNCH_COUNT = 400; +const ZERO_ADDRESS = `0x${"00".repeat(20)}`; +const ZERO_BYTES32 = `0x${"00".repeat(32)}`; +const SMOKE_REQUEST_KEYS = Object.freeze([ + "schemaVersion", + "profileId", + "gitHead", + "targetUrl", + "vercelDeploymentId", + "captureNonce", +] as const); +const RELEASE_REQUEST_KEYS = Object.freeze([ + ...SMOKE_REQUEST_KEYS, + "issuedAtMs", +] as const); +const CANDIDATE_ID_PATTERN = + /^1:(0x[0-9a-f]{64}):(0x[0-9a-f]{64}):(?:0|[1-9]\d*)$/u; + +type CaptureContract = Readonly<{ + profileId: typeof SMOKE_PROFILE_ID | typeof RELEASE_PROFILE_ID; + candidateCount: 8 | 32; + maximumCallsPerProvider: 42 | 128; + minimumEligibleLaunches: 200 | 264; + tokenKeyCount: 100 | 264; +}>; + +const SMOKE_CAPTURE_CONTRACT: CaptureContract = Object.freeze({ + profileId: SMOKE_PROFILE_ID, + candidateCount: SMOKE_REQUIRED_CANDIDATE_COUNT, + maximumCallsPerProvider: SMOKE_MAX_CALLS_PER_PROVIDER, + minimumEligibleLaunches: 200, + tokenKeyCount: REQUIRED_KEY_COUNT, +}); +const RELEASE_CAPTURE_CONTRACT: CaptureContract = Object.freeze({ + profileId: RELEASE_PROFILE_ID, + candidateCount: RELEASE_REQUIRED_CANDIDATE_COUNT, + maximumCallsPerProvider: RELEASE_MAX_CALLS_PER_PROVIDER, + minimumEligibleLaunches: RELEASE_MINIMUM_ELIGIBLE_LAUNCH_COUNT, + tokenKeyCount: RELEASE_TOKEN_KEY_COUNT, +}); + +type PerformanceCaptureRequest = Readonly<{ + schemaVersion: 1 | 2; + profileId: CaptureContract["profileId"]; + gitHead: string; + targetUrl: string; + vercelDeploymentId: string; + captureNonce: HexBytes32; + issuedAtMs?: number; +}>; + +type DatasetCounts = Readonly<{ + launches: number; + chainEvents: number; + marketSnapshots: number; + marketCandles: number; + accounts: number; + rewardRows: number; +}>; + +type DatasetKeys = Readonly<{ + tokenAddresses: readonly HexAddress[]; + accountAddresses: readonly HexAddress[]; + classicLaunches: readonly LaunchPathKey[]; + stockLaunches: readonly LaunchPathKey[]; + candidateIds: readonly string[]; +}>; + +const RELEASE_VERSIONS = Object.freeze([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const); +type ReleaseVersion = (typeof RELEASE_VERSIONS)[number]; +type ReleaseCounts = Readonly>; + +type EligibleLaunch = Readonly<{ + account: HexAddress; + transactionHash: HexBytes32; + tokenAddress: HexAddress; + releaseVersion: ReleaseVersion; +}>; + +type AccountEvidence = Readonly<{ + account: HexAddress; + profileRows: number; + rewardRows: number; +}>; + +type LaunchPathKey = Readonly<{ + account: HexAddress; + transactionHash: HexBytes32; +}>; + +type PerformanceDatasetSeed = Readonly<{ + generatedAt: string; + counts: DatasetCounts; + releaseCounts: ReleaseCounts; + eligibleLaunches: readonly EligibleLaunch[]; + accountEvidence: readonly AccountEvidence[]; + keys: DatasetKeys; +}>; + +export type PerformanceAccessEvidence = Readonly<{ + projectorSessionUser: typeof PROJECTOR_LOGIN_ROLE; + projectorCurrentRole: typeof PROJECTOR_CAPABILITY_ROLE; + projectorCurrentSettingRole: typeof PROJECTOR_CAPABILITY_ROLE; + apiReaderSessionUser: typeof API_READER_LOGIN_ROLE; + apiReaderCurrentRole: typeof API_READER_CAPABILITY_ROLE; + apiReaderCurrentSettingRole: typeof API_READER_CAPABILITY_ROLE; + apiReaderDeniedSqlstate: typeof PERMISSION_DENIED_SQLSTATE; + apiReaderFunctionExecute: false; + apiReaderViewSelect: false; +}>; + +type PerformanceDatasetCapture = Readonly<{ + dataset: PerformanceDatasetSeed; + accessEvidence: PerformanceAccessEvidence; +}>; + +export type PerformanceRpcTraceCall = Readonly<{ + providerIdentity: string; + providerVendorGroup: string; + providerEndpointCommitment: HexBytes32; + providerOriginCommitment: HexBytes32; + operation: + | "getChainId" + | "getBlockNumber" + | "getBlock" + | "getTransactionReceipt" + | "getBytecode"; + attempt: number; + startedOffsetMs: number; + durationMs: number; + outcome: "success" | "error"; +}>; + +type RpcTraceCapture = Readonly<{ + startedAtMs: number; + completedAtMs: number; + candidateBatchSize: number; + hardDeadlineMs: number; + maxCallsPerProvider: number; + elapsedMs: number; + providerCallCounts: readonly [number, number]; + calls: readonly PerformanceRpcTraceCall[]; + candidateEvidence: readonly RpcCandidateEvidence[]; +}>; + +type RpcCandidateEvidence = Readonly<{ + candidateId: string; + candidateBlockNumber: string; + candidateBlockHash: HexBytes32; + transactionHash: HexBytes32; + sourceAddress: HexAddress; +}>; + +function isRecord(value: unknown): value is Record { + return ( + value !== null && + typeof value === "object" && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function onlyKeys( + value: Record, + keys: readonly string[], +): boolean { + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + return ( + actual.length === expected.length && + actual.every((key, index) => key === expected[index]) + ); +} + +function captureInputFailure(): never { + throw invalidInput("config", "performance-capture-request"); +} + +function captureValidationFailure(operation: string): never { + throw validationError("config", operation); +} + +function exactString( + value: unknown, + pattern: RegExp, + maximum: number, +): string { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > maximum || + !pattern.test(value) + ) { + return captureInputFailure(); + } + return value; +} + +export function parseReadModelPerformanceCaptureRequest( + value: unknown, + env: Environment = process.env, +): PerformanceCaptureRequest { + if (!isRecord(value)) { + return captureInputFailure(); + } + const contract = + value.profileId === SMOKE_PROFILE_ID + ? SMOKE_CAPTURE_CONTRACT + : value.profileId === RELEASE_PROFILE_ID + ? RELEASE_CAPTURE_CONTRACT + : null; + if ( + contract === null || + !onlyKeys( + value, + contract === SMOKE_CAPTURE_CONTRACT + ? SMOKE_REQUEST_KEYS + : RELEASE_REQUEST_KEYS, + ) + ) { + return captureInputFailure(); + } + const gitHead = exactString(value.gitHead, /^[0-9a-f]{40}$/u, 40); + const deploymentId = exactString( + value.vercelDeploymentId, + /^dpl_[A-Za-z0-9]{20,128}$/u, + 132, + ); + const vercelHost = exactString( + env.VERCEL_URL, + /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*vercel\.app$/u, + 253, + ); + const expectedTargetUrl = new URL(`https://${vercelHost}`).toString(); + const targetUrl = exactString( + value.targetUrl, + /^https:\/\/[a-z0-9.-]+\.vercel\.app\/$/u, + 300, + ); + let parsedTarget: URL; + try { + parsedTarget = new URL(targetUrl); + } catch { + return captureInputFailure(); + } + if ( + value.schemaVersion !== + (contract === SMOKE_CAPTURE_CONTRACT ? 1 : 2) || + gitHead !== env.VERCEL_GIT_COMMIT_SHA || + targetUrl !== expectedTargetUrl || + parsedTarget.toString() !== targetUrl || + parsedTarget.pathname !== "/" || + parsedTarget.search !== "" || + parsedTarget.hash !== "" || + deploymentId !== env.VERCEL_DEPLOYMENT_ID + ) { + return captureInputFailure(); + } + let captureNonce: HexBytes32; + try { + captureNonce = canonicalBytes32(value.captureNonce); + } catch { + return captureInputFailure(); + } + let issuedAtMs: number | undefined; + if (contract === RELEASE_CAPTURE_CONTRACT) { + issuedAtMs = value.issuedAtMs as number; + const nowMs = Date.now(); + if ( + !Number.isSafeInteger(issuedAtMs) || + issuedAtMs < 1 || + issuedAtMs > nowMs + 30_000 || + nowMs - issuedAtMs > 60_000 + ) { + return captureInputFailure(); + } + } + return Object.freeze({ + schemaVersion: contract === SMOKE_CAPTURE_CONTRACT ? 1 : 2, + profileId: contract.profileId, + gitHead, + targetUrl, + vercelDeploymentId: deploymentId, + captureNonce, + ...(issuedAtMs === undefined ? {} : { issuedAtMs }), + }); +} + +function safeCount(value: unknown, minimum: number, operation: string): number { + let canonical: string; + try { + canonical = parseNonnegativeIntegerText( + typeof value === "bigint" + ? value.toString() + : typeof value === "number" && Number.isSafeInteger(value) + ? String(value) + : value, + ); + } catch { + return captureValidationFailure(operation); + } + const parsed = Number(canonical); + if (!Number.isSafeInteger(parsed) || parsed < minimum) { + return captureValidationFailure(operation); + } + return parsed; +} + +function exactTimestamp(value: unknown): string { + if (typeof value !== "string") { + return captureValidationFailure("performance-dataset-generated-at"); + } + const parsed = new Date(value); + if (Number.isNaN(parsed.valueOf()) || parsed.toISOString() !== value) { + return captureValidationFailure("performance-dataset-generated-at"); + } + return value; +} + +function exactAddresses( + value: unknown, + operation: string, + expectedCount: number, +): readonly HexAddress[] { + if (!Array.isArray(value) || value.length !== expectedCount) { + return captureValidationFailure(operation); + } + let addresses: HexAddress[]; + try { + addresses = value.map((entry) => canonicalAddress(entry)); + } catch { + return captureValidationFailure(operation); + } + if ( + addresses.some((address) => address === ZERO_ADDRESS) || + new Set(addresses).size !== expectedCount || + addresses.some( + (address, index) => index > 0 && address <= addresses[index - 1]!, + ) + ) { + return captureValidationFailure(operation); + } + return Object.freeze(addresses); +} + +function exactLaunches( + value: unknown, + operation: string, +): readonly LaunchPathKey[] { + if (!Array.isArray(value) || value.length !== REQUIRED_MODEL_LAUNCH_COUNT) { + return captureValidationFailure(operation); + } + const launches = value.map((entry) => { + if (!isRecord(entry) || !onlyKeys(entry, ["account", "transactionHash"])) { + return captureValidationFailure(operation); + } + let account: HexAddress; + let transactionHash: HexBytes32; + try { + account = canonicalAddress(entry.account); + transactionHash = canonicalBytes32(entry.transactionHash); + } catch { + return captureValidationFailure(operation); + } + if (account === ZERO_ADDRESS || transactionHash === ZERO_BYTES32) { + return captureValidationFailure(operation); + } + return Object.freeze({ account, transactionHash }); + }); + const identities = launches.map( + ({ account, transactionHash }) => `${account}:${transactionHash}`, + ); + if ( + new Set(identities).size !== REQUIRED_MODEL_LAUNCH_COUNT || + identities.some( + (identity, index) => index > 0 && identity <= identities[index - 1]!, + ) + ) { + return captureValidationFailure(operation); + } + return Object.freeze(launches); +} + +function exactCandidateIds( + value: unknown, + expectedCount: number, +): readonly string[] { + if (!Array.isArray(value) || value.length !== expectedCount) { + return captureValidationFailure("performance-candidate-ids"); + } + const ids = value.map((entry) => { + if (typeof entry !== "string" || !CANDIDATE_ID_PATTERN.test(entry)) { + return captureValidationFailure("performance-candidate-ids"); + } + return entry; + }); + if (new Set(ids).size !== expectedCount) { + return captureValidationFailure("performance-candidate-ids"); + } + return Object.freeze(ids); +} + +function exactReleaseCounts(value: unknown): ReleaseCounts { + if (!isRecord(value) || !onlyKeys(value, RELEASE_VERSIONS)) { + return captureValidationFailure("performance-release-counts"); + } + return Object.freeze( + Object.fromEntries( + RELEASE_VERSIONS.map((releaseVersion) => [ + releaseVersion, + safeCount( + value[releaseVersion], + 1, + "performance-release-count", + ), + ]), + ) as Record, + ); +} + +function exactEligibleLaunches( + value: unknown, + expectedCount: number, +): readonly EligibleLaunch[] { + if (!Array.isArray(value) || value.length !== expectedCount) { + return captureValidationFailure("performance-eligible-launches"); + } + const launches = value.map((entry) => { + if ( + !isRecord(entry) || + !onlyKeys(entry, [ + "account", + "transactionHash", + "tokenAddress", + "releaseVersion", + ]) || + !RELEASE_VERSIONS.includes(entry.releaseVersion as ReleaseVersion) + ) { + return captureValidationFailure("performance-eligible-launch"); + } + let account: HexAddress; + let transactionHash: HexBytes32; + let tokenAddress: HexAddress; + try { + account = canonicalAddress(entry.account); + transactionHash = canonicalBytes32(entry.transactionHash); + tokenAddress = canonicalAddress(entry.tokenAddress); + } catch { + return captureValidationFailure("performance-eligible-launch"); + } + if ( + account === ZERO_ADDRESS || + transactionHash === ZERO_BYTES32 || + tokenAddress === ZERO_ADDRESS || + entry.account !== account || + entry.transactionHash !== transactionHash || + entry.tokenAddress !== tokenAddress + ) { + return captureValidationFailure("performance-eligible-launch"); + } + return Object.freeze({ + account, + transactionHash, + tokenAddress, + releaseVersion: entry.releaseVersion as ReleaseVersion, + }); + }); + if ( + new Set(launches.map(({ transactionHash }) => transactionHash)).size !== + expectedCount || + new Set(launches.map(({ tokenAddress }) => tokenAddress)).size !== + expectedCount || + launches.some((launch, index) => { + if (index === 0) return false; + const previous = launches[index - 1]!; + const previousKey = `${previous.releaseVersion}:${previous.tokenAddress}:${previous.transactionHash}:${previous.account}`; + const currentKey = `${launch.releaseVersion}:${launch.tokenAddress}:${launch.transactionHash}:${launch.account}`; + return currentKey <= previousKey; + }) + ) { + return captureValidationFailure("performance-eligible-launch-identity"); + } + return Object.freeze(launches); +} + +function exactAccountEvidence( + value: unknown, + expectedAccounts: readonly HexAddress[], + counts: DatasetCounts, +): readonly AccountEvidence[] { + if (!Array.isArray(value) || value.length !== REQUIRED_KEY_COUNT) { + return captureValidationFailure("performance-account-evidence"); + } + let totalProfileRows = 0; + let totalRewardRows = 0; + const evidence = value.map((entry, index) => { + if ( + !isRecord(entry) || + !onlyKeys(entry, ["account", "profileRows", "rewardRows"]) + ) { + return captureValidationFailure("performance-account-evidence"); + } + let account: HexAddress; + try { + account = canonicalAddress(entry.account); + } catch { + return captureValidationFailure("performance-account-evidence"); + } + const profileRows = safeCount( + entry.profileRows, + 0, + "performance-account-profile-rows", + ); + const rewardRows = safeCount( + entry.rewardRows, + 0, + "performance-account-reward-rows", + ); + if ( + account !== expectedAccounts[index] || + entry.account !== account || + profileRows + rewardRows === 0 + ) { + return captureValidationFailure("performance-account-evidence"); + } + totalProfileRows += profileRows; + totalRewardRows += rewardRows; + return Object.freeze({ account, profileRows, rewardRows }); + }); + if ( + totalProfileRows > counts.launches || + totalRewardRows > counts.rewardRows + ) { + return captureValidationFailure("performance-account-evidence-counts"); + } + return Object.freeze(evidence); +} + +function validateDatasetSeed( + value: unknown, + contract: CaptureContract, +): PerformanceDatasetSeed { + if ( + !isRecord(value) || + !onlyKeys(value, [ + "generatedAt", + "counts", + "releaseCounts", + "eligibleLaunches", + "accountEvidence", + "keys", + ]) || + !isRecord(value.counts) || + !onlyKeys(value.counts, [ + "launches", + "chainEvents", + "marketSnapshots", + "marketCandles", + "accounts", + "rewardRows", + ]) || + !isRecord(value.keys) || + !onlyKeys(value.keys, [ + "tokenAddresses", + "accountAddresses", + "classicLaunches", + "stockLaunches", + "candidateIds", + ]) + ) { + return captureValidationFailure("performance-dataset-shape"); + } + const launches = safeCount( + value.counts.launches, + contract.minimumEligibleLaunches, + "performance-launch-count", + ); + if (launches > MAXIMUM_ELIGIBLE_LAUNCH_COUNT) { + return captureValidationFailure("performance-launch-count"); + } + const counts = Object.freeze({ + launches, + chainEvents: safeCount( + value.counts.chainEvents, + Math.max(600, 3 * launches), + "performance-chain-event-count", + ), + marketSnapshots: safeCount( + value.counts.marketSnapshots, + Math.max(200, launches), + "performance-market-snapshot-count", + ), + marketCandles: safeCount( + value.counts.marketCandles, + Math.max(200, launches), + "performance-market-candle-count", + ), + accounts: safeCount( + value.counts.accounts, + 100, + "performance-account-count", + ), + rewardRows: safeCount( + value.counts.rewardRows, + Math.max(200, launches), + "performance-reward-row-count", + ), + }); + const releaseCounts = exactReleaseCounts(value.releaseCounts); + const releaseTotal = RELEASE_VERSIONS.reduce( + (total, releaseVersion) => total + releaseCounts[releaseVersion], + 0, + ); + const classicTotal = + releaseCounts["classic-v2"] + releaseCounts["classic-v3"]; + const stockTotal = + releaseCounts["stock-paired-v1"] + + releaseCounts["stock-paired-v2"] + + releaseCounts["stock-paired-v3"]; + if ( + releaseTotal !== launches || + classicTotal < 32 || + classicTotal > 300 || + stockTotal < 32 || + stockTotal > 100 + ) { + return captureValidationFailure("performance-release-coverage"); + } + const eligibleLaunches = exactEligibleLaunches( + value.eligibleLaunches, + launches, + ); + for (const releaseVersion of RELEASE_VERSIONS) { + if ( + eligibleLaunches.filter( + (launch) => launch.releaseVersion === releaseVersion, + ).length !== releaseCounts[releaseVersion] + ) { + return captureValidationFailure("performance-release-coverage"); + } + } + const keys = Object.freeze({ + tokenAddresses: exactAddresses( + value.keys.tokenAddresses, + "performance-token-keys", + contract.tokenKeyCount, + ), + accountAddresses: exactAddresses( + value.keys.accountAddresses, + "performance-account-keys", + REQUIRED_KEY_COUNT, + ), + classicLaunches: exactLaunches( + value.keys.classicLaunches, + "performance-classic-launch-keys", + ), + stockLaunches: exactLaunches( + value.keys.stockLaunches, + "performance-stock-launch-keys", + ), + candidateIds: exactCandidateIds( + value.keys.candidateIds, + contract.candidateCount, + ), + }); + return Object.freeze({ + generatedAt: exactTimestamp(value.generatedAt), + counts, + releaseCounts, + eligibleLaunches, + accountEvidence: exactAccountEvidence( + value.accountEvidence, + keys.accountAddresses, + counts, + ), + keys, + }); +} + +const ACCESS_EVIDENCE_KEYS = Object.freeze([ + "projectorSessionUser", + "projectorCurrentRole", + "projectorCurrentSettingRole", + "apiReaderSessionUser", + "apiReaderCurrentRole", + "apiReaderCurrentSettingRole", + "apiReaderDeniedSqlstate", + "apiReaderFunctionExecute", + "apiReaderViewSelect", +] as const); + +function validateAccessEvidence(value: unknown): PerformanceAccessEvidence { + if ( + !isRecord(value) || + !onlyKeys(value, ACCESS_EVIDENCE_KEYS) || + value.projectorSessionUser !== PROJECTOR_LOGIN_ROLE || + value.projectorCurrentRole !== PROJECTOR_CAPABILITY_ROLE || + value.projectorCurrentSettingRole !== PROJECTOR_CAPABILITY_ROLE || + value.apiReaderSessionUser !== API_READER_LOGIN_ROLE || + value.apiReaderCurrentRole !== API_READER_CAPABILITY_ROLE || + value.apiReaderCurrentSettingRole !== API_READER_CAPABILITY_ROLE || + value.apiReaderDeniedSqlstate !== PERMISSION_DENIED_SQLSTATE || + value.apiReaderFunctionExecute !== false || + value.apiReaderViewSelect !== false + ) { + return captureValidationFailure("performance-access-evidence"); + } + return Object.freeze({ + projectorSessionUser: PROJECTOR_LOGIN_ROLE, + projectorCurrentRole: PROJECTOR_CAPABILITY_ROLE, + projectorCurrentSettingRole: PROJECTOR_CAPABILITY_ROLE, + apiReaderSessionUser: API_READER_LOGIN_ROLE, + apiReaderCurrentRole: API_READER_CAPABILITY_ROLE, + apiReaderCurrentSettingRole: API_READER_CAPABILITY_ROLE, + apiReaderDeniedSqlstate: PERMISSION_DENIED_SQLSTATE, + apiReaderFunctionExecute: false, + apiReaderViewSelect: false, + }); +} + +function sqlState(error: unknown): string | null { + if (error === null || typeof error !== "object") return null; + const code = Reflect.get(error, "code"); + return typeof code === "string" && /^[0-9A-Z]{5}$/u.test(code) + ? code + : null; +} + +function exactDatabaseIdentity( + rows: readonly Record[], + expectedSessionUser: string, + expectedCurrentRole: string, + operation: string, +) { + if ( + rows.length !== 1 || + rows[0]?.session_user !== expectedSessionUser || + rows[0]?.current_role !== expectedCurrentRole || + rows[0]?.current_setting_role !== expectedCurrentRole + ) { + return captureValidationFailure(operation); + } + return Object.freeze({ + sessionUser: expectedSessionUser, + currentRole: expectedCurrentRole, + currentSettingRole: expectedCurrentRole, + }); +} + +async function readApiReaderDenialEvidence( + executor: PostgresExecutor, +): Promise> { + return executor.transaction(async (transaction) => { + await transaction.query( + "set transaction isolation level repeatable read, read only", + ); + await transaction.query("set local role programmable_api_reader"); + await transaction.query("set local statement_timeout = '5000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '6000ms'", + ); + const identity = exactDatabaseIdentity( + await transaction.query( + "select session_user::text as session_user, current_role::text as current_role, current_setting('role', true)::text as current_setting_role", + ), + API_READER_LOGIN_ROLE, + API_READER_CAPABILITY_ROLE, + "performance-api-reader-role", + ); + const privilegeRows = await transaction.query<{ + function_execute: unknown; + view_select: unknown; + }>( + "select has_function_privilege(current_user, 'programmable_private.get_read_model_performance_dataset_v1(bigint)'::regprocedure, 'EXECUTE') as function_execute, has_table_privilege(current_user, 'programmable_private.read_model_performance_eligible_launches_v1', 'SELECT') as view_select", + ); + if ( + privilegeRows.length !== 1 || + privilegeRows[0]?.function_execute !== false || + privilegeRows[0]?.view_select !== false + ) { + return captureValidationFailure("performance-api-reader-privilege"); + } + + await transaction.query("savepoint performance_api_reader_denial"); + let deniedSqlstate: string | null = null; + try { + await transaction.query( + "select * from programmable_private.get_read_model_performance_dataset_v1($1)", + ["1"], + ); + } catch (error) { + deniedSqlstate = sqlState(error); + } + await transaction.query("rollback to savepoint performance_api_reader_denial"); + await transaction.query("release savepoint performance_api_reader_denial"); + if (deniedSqlstate !== PERMISSION_DENIED_SQLSTATE) { + return captureValidationFailure("performance-api-reader-denial"); + } + return Object.freeze({ + apiReaderSessionUser: identity.sessionUser as typeof API_READER_LOGIN_ROLE, + apiReaderCurrentRole: identity.currentRole as typeof API_READER_CAPABILITY_ROLE, + apiReaderCurrentSettingRole: + identity.currentSettingRole as typeof API_READER_CAPABILITY_ROLE, + apiReaderDeniedSqlstate: PERMISSION_DENIED_SQLSTATE, + apiReaderFunctionExecute: false as const, + apiReaderViewSelect: false as const, + }); + }); +} + +export async function readPerformanceDataset( + env: Environment, + dependencies: Readonly<{ + createExecutor?: typeof createPostgresExecutor; + }> = {}, +): Promise { + const config = loadDataPipelineConfig(env); + if ( + !config.postgres.connectionString || + !config.postgres.sslCaPem || + env.NEXT_PUBLIC_PROGRAMMABLE_PROJECTOR_DATABASE_URL + ) { + return captureInputFailure(); + } + let projectorConnectionString: string; + try { + projectorConnectionString = validatedPostgresConnectionString( + env.PROGRAMMABLE_PROJECTOR_DATABASE_URL, + ); + } catch { + return captureInputFailure(); + } + const createExecutor = dependencies.createExecutor ?? createPostgresExecutor; + const projectorExecutor = createExecutor({ + connectionString: projectorConnectionString, + sslCaPem: config.postgres.sslCaPem, + maxConnections: 1, + connectTimeoutMs: config.postgres.connectTimeoutMs, + idleTimeoutMs: config.postgres.idleTimeoutMs, + }); + const readerExecutor = createExecutor({ + connectionString: config.postgres.connectionString, + sslCaPem: config.postgres.sslCaPem, + maxConnections: 1, + connectTimeoutMs: config.postgres.connectTimeoutMs, + idleTimeoutMs: config.postgres.idleTimeoutMs, + }); + try { + const projectorCapture = await projectorExecutor.transaction( + async (transaction) => { + await transaction.query( + "set transaction isolation level repeatable read, read only", + ); + await transaction.query("set local role programmable_projector"); + await transaction.query("set local statement_timeout = '5000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '6000ms'", + ); + const identity = exactDatabaseIdentity( + await transaction.query( + "select session_user::text as session_user, current_role::text as current_role, current_setting('role', true)::text as current_setting_role", + ), + PROJECTOR_LOGIN_ROLE, + PROJECTOR_CAPABILITY_ROLE, + "performance-projector-role", + ); + const rows = await transaction.query( + "select * from programmable_private.get_read_model_performance_dataset_v1($1)", + ["1"], + ); + return Object.freeze({ identity, rows }); + }, + ); + const rows = projectorCapture.rows; + if (rows.length !== 1) { + return captureValidationFailure("performance-dataset-row"); + } + const row = rows[0]!; + const launchCount = safeCount( + row.launch_count, + SMOKE_CAPTURE_CONTRACT.minimumEligibleLaunches, + "performance-launch-count", + ); + if ( + safeCount( + row.eligible_launch_count, + SMOKE_CAPTURE_CONTRACT.minimumEligibleLaunches, + "performance-eligible-launch-count", + ) !== launchCount || + safeCount( + row.candidate_count, + SMOKE_CAPTURE_CONTRACT.candidateCount, + "performance-candidate-count", + ) !== SMOKE_CAPTURE_CONTRACT.candidateCount + ) { + return captureValidationFailure("performance-dataset-counts"); + } + const dataset = validateDatasetSeed({ + generatedAt: + row.generated_at instanceof Date + ? row.generated_at.toISOString() + : row.generated_at, + counts: { + launches: launchCount, + chainEvents: row.chain_event_count, + marketSnapshots: row.market_snapshot_count, + marketCandles: row.market_candle_count, + accounts: row.account_count, + rewardRows: row.reward_row_count, + }, + releaseCounts: row.release_coverage, + eligibleLaunches: row.eligible_launches, + accountEvidence: row.account_evidence, + keys: { + tokenAddresses: row.token_addresses, + accountAddresses: row.account_addresses, + classicLaunches: row.classic_launches, + stockLaunches: row.stock_launches, + candidateIds: row.candidate_ids, + }, + }, SMOKE_CAPTURE_CONTRACT); + const readerEvidence = await readApiReaderDenialEvidence(readerExecutor); + return Object.freeze({ + dataset, + accessEvidence: validateAccessEvidence({ + projectorSessionUser: projectorCapture.identity.sessionUser, + projectorCurrentRole: projectorCapture.identity.currentRole, + projectorCurrentSettingRole: + projectorCapture.identity.currentSettingRole, + ...readerEvidence, + }), + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "postgres", + code: "query_failed", + retryable: true, + countsTowardCircuit: true, + }); + } finally { + await Promise.allSettled([ + projectorExecutor.close(), + readerExecutor.close(), + ]); + } +} + +async function runProductionRpcTrace(input: { + providers: readonly [CandidateRpcProvider, CandidateRpcProvider]; + env: Environment; + contract: CaptureContract; + work: () => Promise; +}): Promise { + void input.providers; + void input.env; + const result = await input.work(); + if ( + !isRecord(result) || + !Array.isArray(result.candidates) || + result.candidates.length !== input.contract.candidateCount || + !isRecord(result.executionTrace) + ) { + return captureValidationFailure("performance-native-rpc-trace"); + } + return Object.freeze({ + ...(result.executionTrace as Omit), + candidateEvidence: Object.freeze( + result.candidates.map((candidate) => { + if (!isRecord(candidate)) { + return captureValidationFailure("performance-rpc-candidate-evidence"); + } + return Object.freeze({ + candidateId: candidate.candidateId, + candidateBlockNumber: candidate.candidateBlockNumber, + candidateBlockHash: candidate.candidateBlockHash, + transactionHash: candidate.transactionHash, + sourceAddress: candidate.sourceAddress, + }) as RpcCandidateEvidence; + }), + ), + }); +} + +function safeTraceInteger(value: unknown, maximum: number, operation: string) { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < 0 || + value > maximum + ) { + return captureValidationFailure(operation); + } + return value; +} + +function validateTrace( + trace: RpcTraceCapture, + providers: readonly [CandidateRpcProvider, CandidateRpcProvider], + expectedCandidateIds: readonly string[], + contract: CaptureContract, +): RpcTraceCapture { + const startedAtMs = safeTraceInteger( + trace.startedAtMs, + Number.MAX_SAFE_INTEGER, + "performance-rpc-started-at", + ); + const completedAtMs = safeTraceInteger( + trace.completedAtMs, + Number.MAX_SAFE_INTEGER, + "performance-rpc-completed-at", + ); + const elapsedMs = safeTraceInteger( + trace.elapsedMs, + HARD_DEADLINE_MS, + "performance-rpc-elapsed", + ); + if ( + completedAtMs < startedAtMs || + completedAtMs - startedAtMs !== elapsedMs || + trace.candidateBatchSize !== contract.candidateCount || + trace.hardDeadlineMs !== HARD_DEADLINE_MS || + trace.maxCallsPerProvider !== contract.maximumCallsPerProvider || + !Array.isArray(trace.providerCallCounts) || + trace.providerCallCounts.length !== 2 || + !Array.isArray(trace.calls) || + !Array.isArray(trace.candidateEvidence) || + trace.candidateEvidence.length !== contract.candidateCount || + providers.length !== 2 + ) { + return captureValidationFailure("performance-rpc-trace"); + } + const byIdentity = new Map(providers.map((provider) => [provider.identity, provider])); + const calls = trace.calls.map((call) => { + const provider = byIdentity.get(call.providerIdentity); + if ( + !provider || + call.providerVendorGroup !== provider.vendorGroup || + call.providerEndpointCommitment !== provider.endpointCommitment || + call.providerOriginCommitment !== provider.endpointOriginCommitment || + ![ + "getChainId", + "getBlockNumber", + "getBlock", + "getTransactionReceipt", + "getBytecode", + ].includes(call.operation) || + (call.outcome !== "success" && call.outcome !== "error") + ) { + return captureValidationFailure("performance-rpc-call"); + } + return Object.freeze({ + ...call, + attempt: safeTraceInteger(call.attempt, 128, "performance-rpc-attempt"), + startedOffsetMs: safeTraceInteger( + call.startedOffsetMs, + elapsedMs, + "performance-rpc-start", + ), + durationMs: safeTraceInteger( + call.durationMs, + HARD_DEADLINE_MS, + "performance-rpc-duration", + ), + }); + }); + let total = 0; + const counts: [number, number] = [0, 0]; + const expectedSuccessCounts = Object.freeze({ + getChainId: 1, + getBlockNumber: 1, + getBlock: contract.candidateCount + 1, + getTransactionReceipt: contract.candidateCount, + getBytecode: contract.candidateCount, + }); + for (const [index, provider] of providers.entries()) { + const count = safeTraceInteger( + trace.providerCallCounts[index], + contract.maximumCallsPerProvider, + "performance-rpc-count", + ); + if (count < 1) return captureValidationFailure("performance-rpc-count"); + counts[index] = count; + total += count; + for (const [operation, expected] of Object.entries(expectedSuccessCounts)) { + const successful = calls.filter( + (call) => + call.providerIdentity === provider.identity && + call.operation === operation && + call.outcome === "success", + ).length; + if (successful !== expected) { + return captureValidationFailure("performance-rpc-operation-count"); + } + } + } + if (total !== calls.length) { + return captureValidationFailure("performance-rpc-count"); + } + const candidateEvidence = trace.candidateEvidence.map((candidate, index) => { + if (!isRecord(candidate)) { + return captureValidationFailure("performance-rpc-candidate-evidence"); + } + const candidateId = candidate.candidateId; + const idMatch = + typeof candidateId === "string" + ? CANDIDATE_ID_PATTERN.exec(candidateId) + : null; + let candidateBlockNumber: string; + let candidateBlockHash: HexBytes32; + let transactionHash: HexBytes32; + let sourceAddress: HexAddress; + try { + candidateBlockNumber = parseNonnegativeIntegerText( + candidate.candidateBlockNumber, + ); + candidateBlockHash = canonicalBytes32(candidate.candidateBlockHash); + transactionHash = canonicalBytes32(candidate.transactionHash); + sourceAddress = canonicalAddress(candidate.sourceAddress); + } catch { + return captureValidationFailure("performance-rpc-candidate-evidence"); + } + if ( + !idMatch || + candidateId !== expectedCandidateIds[index] || + idMatch[1] !== candidateBlockHash || + idMatch[2] !== transactionHash || + candidate.candidateBlockNumber !== candidateBlockNumber || + candidate.candidateBlockHash !== candidateBlockHash || + candidate.transactionHash !== transactionHash || + candidate.sourceAddress !== sourceAddress + ) { + return captureValidationFailure("performance-rpc-candidate-evidence"); + } + return Object.freeze({ + candidateId, + candidateBlockNumber, + candidateBlockHash, + transactionHash, + sourceAddress, + }); + }); + if ( + new Set(candidateEvidence.map(({ candidateId }) => candidateId)).size !== + contract.candidateCount || + new Set( + candidateEvidence.map(({ candidateBlockNumber }) => candidateBlockNumber), + ).size !== contract.candidateCount || + new Set(candidateEvidence.map(({ transactionHash }) => transactionHash)) + .size !== contract.candidateCount || + new Set( + candidateEvidence.map( + ({ candidateBlockNumber, sourceAddress }) => + `${candidateBlockNumber}:${sourceAddress}`, + ), + ).size !== contract.candidateCount || + candidateEvidence.some( + ({ candidateBlockNumber }, index) => + index > 0 && + BigInt(candidateBlockNumber) <= + BigInt(candidateEvidence[index - 1]!.candidateBlockNumber), + ) + ) { + return captureValidationFailure("performance-rpc-candidate-coverage"); + } + return Object.freeze({ + startedAtMs, + completedAtMs, + candidateBatchSize: contract.candidateCount, + hardDeadlineMs: HARD_DEADLINE_MS, + maxCallsPerProvider: contract.maximumCallsPerProvider, + elapsedMs, + providerCallCounts: Object.freeze(counts), + calls: Object.freeze(calls), + candidateEvidence: Object.freeze(candidateEvidence), + }); +} + +async function withDeadline( + hardDeadlineMs: number, + work: () => Promise, +): Promise { + let timeout: ReturnType | undefined; + try { + return await Promise.race([ + work(), + new Promise((_resolve, reject) => { + timeout = setTimeout( + () => + reject( + dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }), + ), + hardDeadlineMs, + ); + }), + ]); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +export type PerformanceCaptureDependencies = Readonly<{ + readDataset(env: Environment): Promise; + createEnvio: typeof createEnvioClient; + createProviders: typeof createProductionDualRpcProviders; + verifyBatch: typeof verifyEnvioCandidateBatchWithDualRpc; + runRpcTrace: typeof runProductionRpcTrace; +}>; + +const DEFAULT_DEPENDENCIES: PerformanceCaptureDependencies = Object.freeze({ + readDataset: readPerformanceDataset, + createEnvio: createEnvioClient, + createProviders: createProductionDualRpcProviders, + verifyBatch: verifyEnvioCandidateBatchWithDualRpc, + runRpcTrace: runProductionRpcTrace, +}); + +async function readReleaseCandidates( + envio: ReturnType, +): Promise { + const selected: EnvioCandidate[] = []; + const seenBlocks = new Set(); + let cursor: EnvioCandidateCursor = Object.freeze({ + blockNumber: "0", + blockGlobalLogIndex: -1, + candidateId: "", + }); + for (let pageIndex = 0; pageIndex < 16; pageIndex += 1) { + const page = await envio.readCandidatesAfter({ cursor, limit: 32 }); + if (page.length === 0) break; + for (const candidate of page) { + if (!seenBlocks.has(candidate.blockNumber)) { + seenBlocks.add(candidate.blockNumber); + selected.push(candidate); + } + if (selected.length === RELEASE_CAPTURE_CONTRACT.candidateCount) { + return Object.freeze(selected); + } + } + const last = page.at(-1)!; + cursor = Object.freeze({ + blockNumber: last.blockNumber, + blockGlobalLogIndex: last.blockGlobalLogIndex, + candidateId: last.candidateId, + }); + if (page.length < 32) break; + } + return captureValidationFailure("performance-release-candidate-corpus"); +} + +function releaseDataset( + seed: PerformanceDatasetSeed, + candidates: readonly EnvioCandidate[], +): PerformanceDatasetSeed { + const tokenAddresses = [...new Set( + seed.eligibleLaunches.map(({ tokenAddress }) => tokenAddress), + )] + .sort() + .slice(0, RELEASE_CAPTURE_CONTRACT.tokenKeyCount); + return validateDatasetSeed( + { + ...seed, + keys: { + ...seed.keys, + tokenAddresses, + candidateIds: candidates.map(({ candidateId }) => candidateId), + }, + }, + RELEASE_CAPTURE_CONTRACT, + ); +} + +export async function captureReadModelPerformance( + requestBody: unknown, + options: Readonly<{ + env?: Environment; + dependencies?: PerformanceCaptureDependencies; + }> = {}, +) { + const env = options.env ?? process.env; + const dependencies = options.dependencies ?? DEFAULT_DEPENDENCIES; + const request = parseReadModelPerformanceCaptureRequest(requestBody, env); + const contract = + request.profileId === RELEASE_PROFILE_ID + ? RELEASE_CAPTURE_CONTRACT + : SMOKE_CAPTURE_CONTRACT; + const capturedDataset = await dependencies.readDataset(env); + const accessEvidence = validateAccessEvidence( + capturedDataset.accessEvidence, + ); + let dataset: PerformanceDatasetSeed | undefined; + if (contract === SMOKE_CAPTURE_CONTRACT) { + dataset = validateDatasetSeed( + capturedDataset.dataset, + SMOKE_CAPTURE_CONTRACT, + ); + } + const envioConfig = loadDataPipelineConfig(env).envio; + if (!envioConfig.endpoint) return captureInputFailure(); + const runtimeBinding = selectProjectorRuntimeBinding({ + env, + canonicalBinding: getDataPipelineReleaseBinding(), + }); + if ( + envioConfig.endpoint !== runtimeBinding.releaseBinding.envio.graphqlEndpoint + ) { + return captureInputFailure(); + } + const envio = dependencies.createEnvio({ + endpoint: envioConfig.endpoint, + token: envioConfig.token, + releaseBinding: runtimeBinding.releaseBinding, + }); + const providers = dependencies.createProviders(env); + const deadlineStartedAt = Date.now(); + + const trace = await withDeadline(HARD_DEADLINE_MS, async () => { + let freshCandidates: readonly EnvioCandidate[]; + if (contract === RELEASE_CAPTURE_CONTRACT) { + const candidates = await readReleaseCandidates(envio); + dataset = releaseDataset(capturedDataset.dataset, candidates); + freshCandidates = candidates; + } else { + const candidates = await Promise.all( + dataset!.keys.candidateIds.map((candidateId) => + envio.readCandidate(candidateId), + ), + ); + freshCandidates = candidates.map((candidate, index) => { + if ( + candidate === null || + candidate.candidateId !== dataset!.keys.candidateIds[index] + ) { + return captureValidationFailure("performance-envio-candidate"); + } + return candidate; + }); + } + const remaining = HARD_DEADLINE_MS - (Date.now() - deadlineStartedAt); + if (remaining < 10) { + return captureValidationFailure("performance-rpc-deadline"); + } + const rpcTrace = await dependencies.runRpcTrace({ + providers, + env, + contract, + work: async () => { + return dependencies.verifyBatch({ + candidates: freshCandidates, + providers, + rpcPolicy: { + hardDeadlineMs: remaining, + maxCallsPerProvider: contract.maximumCallsPerProvider, + }, + }); + }, + }); + return rpcTrace; + }); + const validatedTrace = validateTrace( + trace, + providers, + dataset!.keys.candidateIds, + contract, + ); + return Object.freeze({ + schemaVersion: 1 as const, + captureNonce: request.captureNonce, + datasetManifest: Object.freeze({ + schemaVersion: 1 as const, + profileId: contract.profileId, + generatedAt: dataset!.generatedAt, + counts: dataset!.counts, + releaseCounts: dataset!.releaseCounts, + eligibleLaunches: dataset!.eligibleLaunches, + accountEvidence: dataset!.accountEvidence, + keys: dataset!.keys, + accessEvidence, + }), + rpcTrace: Object.freeze({ + schemaVersion: 1 as const, + profileId: contract.profileId, + gitHead: request.gitHead, + targetUrl: request.targetUrl, + vercelDeploymentId: request.vercelDeploymentId, + captureNonce: request.captureNonce, + startedAtMs: validatedTrace.startedAtMs, + completedAtMs: validatedTrace.completedAtMs, + candidateBatchSize: contract.candidateCount, + hardDeadlineMs: HARD_DEADLINE_MS, + maxCallsPerProvider: contract.maximumCallsPerProvider, + elapsedMs: validatedTrace.elapsedMs, + providerCallCounts: validatedTrace.providerCallCounts, + calls: validatedTrace.calls, + candidateEvidence: validatedTrace.candidateEvidence, + }), + }); +} diff --git a/lib/data-pipeline/read-model.server.ts b/lib/data-pipeline/read-model.server.ts new file mode 100644 index 00000000..dc6e6901 --- /dev/null +++ b/lib/data-pipeline/read-model.server.ts @@ -0,0 +1,169 @@ +import "server-only"; + +import { CircuitBreaker } from "./circuit"; +import { INDEXED_ROUTE_FLAG_NAMES, loadDataPipelineConfig } from "./config"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, +} from "./errors"; +import { + createPostgresExecutor, + createPostgresReadModel, + establishPostgresApiReaderRole, + type PostgresExecutor, + type PostgresTransaction, +} from "./postgres"; +import { validatedPostgresConnectionTarget } from "./postgres-connection.server"; + +type BaseServerReadModel = ReturnType; + +export type ServerReadModel = BaseServerReadModel & { + /** + * Runs all readiness, payload, evidence, and version reads in one immutable + * database snapshot. Do not call ordinary read-model methods inside `work`. + */ + repeatableReadSnapshot( + work: (transaction: PostgresTransaction) => Promise, + ): Promise; +}; + +const READ_MODEL_SINGLETON = Symbol.for( + "programmable.data-pipeline.server-read-model.v1", +); +const REQUIRED_READ_MODEL_SINGLETON = Symbol.for( + "programmable.data-pipeline.server-read-model.required.v1", +); + +type SymbolRegistry = { + [key: symbol]: Promise | undefined; +}; + +function registry(): SymbolRegistry { + return globalThis as typeof globalThis & SymbolRegistry; +} + +function requiresReadModel( + config: ReturnType, +): boolean { + return ( + config.flags.INDEXED_READ_SHADOW_COMPARE_ENABLED || + INDEXED_ROUTE_FLAG_NAMES.some((name) => config.flags[name]) + ); +} + +function createServerReadModel(executor: PostgresExecutor): ServerReadModel { + const circuit = new CircuitBreaker({ dependency: "postgres" }); + const readModel = createPostgresReadModel({ executor, circuit }); + + return Object.freeze({ + ...readModel, + async repeatableReadSnapshot( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return circuit.execute(async () => { + try { + return await executor.transaction(async (transaction) => { + // PostgreSQL requires transaction characteristics before any read. + await transaction.query( + "set transaction isolation level repeatable read, read only", + ); + await establishPostgresApiReaderRole(transaction); + await transaction.query("set local statement_timeout = '1000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '2000ms'", + ); + return work(transaction); + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "postgres", + code: "query_failed", + retryable: true, + countsTowardCircuit: true, + }); + } + }); + }, + }); +} + +function constructServerReadModel(required: boolean): ServerReadModel | null { + const config = loadDataPipelineConfig(); + if (!required && !requiresReadModel(config)) return null; + + const connectionString = config.postgres.connectionString; + if (!connectionString) { + throw dataPipelineError({ + dependency: "config", + code: "invalid_config", + retryable: false, + countsTowardCircuit: false, + }); + } + + const target = validatedPostgresConnectionTarget(connectionString); + if (!target.isLoopback && !config.postgres.sslCaPem) { + throw dataPipelineError({ + dependency: "config", + code: "invalid_config", + retryable: false, + countsTowardCircuit: false, + }); + } + + const executor = createPostgresExecutor({ + connectionString, + sslCaPem: config.postgres.sslCaPem, + maxConnections: config.postgres.maxConnections, + connectTimeoutMs: config.postgres.connectTimeoutMs, + idleTimeoutMs: config.postgres.idleTimeoutMs, + allowInsecureLoopback: target.isLoopback, + }); + return createServerReadModel(executor); +} + +/** + * Returns the one process-wide read model. The promise itself is shared so + * concurrent cold-start requests cannot create competing Postgres pools. + */ +export function getServerReadModel( + options: Readonly<{ required?: boolean }> = {}, +): Promise { + const state = registry(); + if (options.required) { + const required = state[REQUIRED_READ_MODEL_SINGLETON]; + if (required) return required; + const ordinary = state[READ_MODEL_SINGLETON]; + const created = Promise.resolve(ordinary) + .then((existing) => existing ?? constructServerReadModel(true)); + state[REQUIRED_READ_MODEL_SINGLETON] = created; + state[READ_MODEL_SINGLETON] = created; + return created; + } + const existing = state[READ_MODEL_SINGLETON]; + if (existing) return existing; + + const created = Promise.resolve().then(() => constructServerReadModel(false)); + state[READ_MODEL_SINGLETON] = created; + return created; +} + +/** Test isolation only. Production lifecycle is owned by the server process. */ +export async function resetServerReadModelForTests(): Promise { + if (process.env.NODE_ENV !== "test") { + throw invalidInput("config", "read-model-reset"); + } + + const state = registry(); + const existing = state[READ_MODEL_SINGLETON]; + const required = state[REQUIRED_READ_MODEL_SINGLETON]; + delete state[READ_MODEL_SINGLETON]; + delete state[REQUIRED_READ_MODEL_SINGLETON]; + if (!existing && !required) return; + + const model = await (required ?? existing)!.catch(() => null); + if (model) await model.close(); +} diff --git a/lib/data-pipeline/reconciler-corpus-partitions.ts b/lib/data-pipeline/reconciler-corpus-partitions.ts new file mode 100644 index 00000000..f54062ed --- /dev/null +++ b/lib/data-pipeline/reconciler-corpus-partitions.ts @@ -0,0 +1,493 @@ +import "server-only"; + +import { keccak256, toBytes } from "viem"; + +import { + canonicalizeFingerprintJson, + type CanonicalJsonValue, +} from "./canonical-fingerprint"; +import { canonicalBytes32, type HexBytes32 } from "./codecs"; +import { invalidInput, validationError } from "./errors"; +import type { ReconcilerPreParityContract } from "./reconciler-preparity"; + +export const RECONCILER_CORPUS_PARTITION_SIZE = 128; +export const RECONCILER_CORPUS_MAXIMUM_TOTAL_COUNT = 10_000; +export const RECONCILER_CORPUS_PARTITION_VERSION = + "reconciler-corpus-partition-v1"; +export const RECONCILER_ENTITLEMENT_PARTITION_VERSION = + "reconciler-entitlement-partition-v1"; + +export type ReconcilerCorpusIdentity = Readonly<{ + tokenAddress: string; + poolId: HexBytes32; + launchTransactionHash: HexBytes32; + launchBlockNumber: string; + launchTransactionIndex: number; + launchLogIndex: number; +}>; + +export type ReconcilerCorpusPage = Readonly<{ + version: typeof RECONCILER_CORPUS_PARTITION_VERSION; + manifestCommitment: HexBytes32; + pageCommitment: HexBytes32; + pageIndex: number; + pageCount: number; + pageSize: number; + totalCount: number; + startIndex: number; + endIndexExclusive: number; + continuation: HexBytes32 | null; + identities: readonly ReconcilerCorpusIdentity[]; +}>; + +export type ReconcilerCorpusManifest = Readonly<{ + version: typeof RECONCILER_CORPUS_PARTITION_VERSION; + manifestCommitment: HexBytes32; + pageSize: number; + totalCount: number; + pageCount: number; + pages: readonly ReconcilerCorpusPage[]; +}>; + +function commitment(domain: string, value: CanonicalJsonValue): HexBytes32 { + return keccak256(toBytes( + `programmable:${domain}:v1\0${canonicalizeFingerprintJson(value)}`, + )); +} + +function canonicalIdentity( + value: ReconcilerCorpusIdentity, + operation: string, +): ReconcilerCorpusIdentity { + if ( + !/^0x[0-9a-f]{40}$/u.test(value.tokenAddress) || + !/^(0|[1-9][0-9]*)$/u.test(value.launchBlockNumber) || + !Number.isSafeInteger(value.launchTransactionIndex) || + value.launchTransactionIndex < 0 || + !Number.isSafeInteger(value.launchLogIndex) || + value.launchLogIndex < 0 + ) { + throw validationError("config", operation); + } + return Object.freeze({ + tokenAddress: value.tokenAddress, + poolId: canonicalBytes32(value.poolId), + launchTransactionHash: canonicalBytes32(value.launchTransactionHash), + launchBlockNumber: value.launchBlockNumber, + launchTransactionIndex: value.launchTransactionIndex, + launchLogIndex: value.launchLogIndex, + }); +} + +function identityJson(value: ReconcilerCorpusIdentity): CanonicalJsonValue { + return { + tokenAddress: value.tokenAddress, + poolId: value.poolId, + launchTransactionHash: value.launchTransactionHash, + launchBlockNumber: value.launchBlockNumber, + launchTransactionIndex: value.launchTransactionIndex, + launchLogIndex: value.launchLogIndex, + }; +} + +function projectedLaunchKeys( + currentEntities: CanonicalJsonValue, +): ReadonlySet { + if (!Array.isArray(currentEntities)) { + throw validationError("config", "reconciler-corpus-current-entities"); + } + const keys = new Set(); + for (const entity of currentEntities) { + if (entity === null || Array.isArray(entity) || typeof entity !== "object") { + throw validationError("config", "reconciler-corpus-current-entity"); + } + if (entity.entityKind !== "launch") continue; + if ( + typeof entity.entityKey !== "string" || + !/^0x[0-9a-f]{40}$/u.test(entity.entityKey) || + keys.has(entity.entityKey) + ) { + throw validationError("config", "reconciler-corpus-current-launch"); + } + keys.add(entity.entityKey); + } + return keys; +} + +function continuationCommitment( + manifestCommitment: HexBytes32, + nextPageIndex: number, +): HexBytes32 { + return commitment("reconciler-corpus-continuation", { + manifestCommitment, + nextPageIndex, + }); +} + +function pageCommitment(input: Readonly<{ + manifestCommitment: HexBytes32; + pageIndex: number; + pageCount: number; + pageSize: number; + totalCount: number; + startIndex: number; + endIndexExclusive: number; + continuation: HexBytes32 | null; + identities: readonly ReconcilerCorpusIdentity[]; +}>): HexBytes32 { + return commitment("reconciler-corpus-page", { + manifestCommitment: input.manifestCommitment, + pageIndex: input.pageIndex, + pageCount: input.pageCount, + pageSize: input.pageSize, + totalCount: input.totalCount, + startIndex: input.startIndex, + endIndexExclusive: input.endIndexExclusive, + continuation: input.continuation, + identities: input.identities.map(identityJson), + }); +} + +export function createReconcilerCorpusManifest(input: Readonly<{ + contract: ReconcilerPreParityContract; + identities: readonly ReconcilerCorpusIdentity[]; + pageSize?: number; +}>): ReconcilerCorpusManifest { + const pageSize = input.pageSize ?? RECONCILER_CORPUS_PARTITION_SIZE; + if ( + !Number.isSafeInteger(pageSize) || + pageSize < 1 || + pageSize > RECONCILER_CORPUS_PARTITION_SIZE + ) { + throw invalidInput("config", "reconciler-corpus-page-size"); + } + if ( + !Array.isArray(input.identities) || + input.identities.length < 1 || + input.identities.length > RECONCILER_CORPUS_MAXIMUM_TOTAL_COUNT + ) { + throw validationError("config", "reconciler-corpus-cardinality"); + } + const identities = Object.freeze(input.identities.map((identity) => + canonicalIdentity(identity, "reconciler-corpus-identity") + )); + const uniqueTokens = new Set(identities.map(({ tokenAddress }) => tokenAddress)); + const uniquePools = new Set(identities.map(({ poolId }) => poolId)); + const projectedLaunches = projectedLaunchKeys(input.contract.currentEntities); + if ( + uniqueTokens.size !== identities.length || + uniquePools.size !== identities.length || + projectedLaunches.size !== identities.length || + [...uniqueTokens].some((tokenAddress) => !projectedLaunches.has(tokenAddress)) + ) { + throw validationError("config", "reconciler-corpus-cardinality"); + } + + const pageCount = Math.ceil(identities.length / pageSize); + const manifestCommitment = commitment("reconciler-corpus-manifest", { + version: RECONCILER_CORPUS_PARTITION_VERSION, + chainId: input.contract.chainId, + releaseId: input.contract.releaseId, + modelId: input.contract.modelId, + sourceGroup: input.contract.sourceGroup, + projectorVersion: input.contract.projectorVersion, + epochId: input.contract.epochId, + pointerGeneration: input.contract.pointerGeneration, + checkpointId: input.contract.checkpointId, + checkpointGeneration: input.contract.checkpointGeneration, + reorgGeneration: input.contract.reorgGeneration, + checkpointBlockNumber: input.contract.checkpointBlockNumber, + checkpointBlockHash: input.contract.checkpointBlockHash, + routeKeys: [...input.contract.routeKeys], + routeContract: input.contract.routeContract, + projectionContract: input.contract.projectionContract, + currentEntities: input.contract.currentEntities, + pageSize, + totalCount: identities.length, + pageCount, + orderedLaunches: identities.map(identityJson), + }); + const pages = Array.from({ length: pageCount }, (_, pageIndex) => { + const startIndex = pageIndex * pageSize; + const endIndexExclusive = Math.min(startIndex + pageSize, identities.length); + const pageIdentities = Object.freeze( + identities.slice(startIndex, endIndexExclusive), + ); + const continuation = pageIndex + 1 < pageCount + ? continuationCommitment(manifestCommitment, pageIndex + 1) + : null; + return Object.freeze({ + version: RECONCILER_CORPUS_PARTITION_VERSION, + manifestCommitment, + pageCommitment: pageCommitment({ + manifestCommitment, + pageIndex, + pageCount, + pageSize, + totalCount: identities.length, + startIndex, + endIndexExclusive, + continuation, + identities: pageIdentities, + }), + pageIndex, + pageCount, + pageSize, + totalCount: identities.length, + startIndex, + endIndexExclusive, + continuation, + identities: pageIdentities, + } satisfies ReconcilerCorpusPage); + }); + return Object.freeze({ + version: RECONCILER_CORPUS_PARTITION_VERSION, + manifestCommitment, + pageSize, + totalCount: identities.length, + pageCount, + pages: Object.freeze(pages), + }); +} + +export function assembleReconcilerCorpusPages( + manifest: ReconcilerCorpusManifest, + pages: readonly ReconcilerCorpusPage[], +): readonly ReconcilerCorpusIdentity[] { + if ( + pages.length !== manifest.pageCount || + manifest.pages.length !== manifest.pageCount + ) { + throw validationError("config", "reconciler-corpus-page-cardinality"); + } + const assembled: ReconcilerCorpusIdentity[] = []; + for (let pageIndex = 0; pageIndex < pages.length; pageIndex += 1) { + const page = pages[pageIndex]!; + const expected = manifest.pages[pageIndex]!; + if ( + page.version !== RECONCILER_CORPUS_PARTITION_VERSION || + page.manifestCommitment !== manifest.manifestCommitment || + page.pageCommitment !== expected.pageCommitment || + page.pageIndex !== pageIndex || + page.pageCount !== manifest.pageCount || + page.pageSize !== manifest.pageSize || + page.totalCount !== manifest.totalCount || + page.startIndex !== expected.startIndex || + page.endIndexExclusive !== expected.endIndexExclusive || + page.continuation !== expected.continuation || + canonicalizeFingerprintJson(page.identities.map(identityJson)) !== + canonicalizeFingerprintJson(expected.identities.map(identityJson)) + ) { + throw validationError("config", "reconciler-corpus-page-binding"); + } + assembled.push(...page.identities); + } + if (assembled.length !== manifest.totalCount) { + throw validationError("config", "reconciler-corpus-page-completeness"); + } + return Object.freeze(assembled); +} + +export type ReconcilerEntitlementIdentity = Readonly<{ + tokenAddress: string; + vaultAddress: string; + account: string; +}>; + +export type ReconcilerEntitlementPage = Readonly<{ + version: typeof RECONCILER_ENTITLEMENT_PARTITION_VERSION; + manifestCommitment: HexBytes32; + pageCommitment: HexBytes32; + pageIndex: number; + pageCount: number; + pageSize: number; + totalCount: number; + startIndex: number; + endIndexExclusive: number; + continuation: HexBytes32 | null; + identities: readonly ReconcilerEntitlementIdentity[]; +}>; + +export type ReconcilerEntitlementManifest = Readonly<{ + version: typeof RECONCILER_ENTITLEMENT_PARTITION_VERSION; + manifestCommitment: HexBytes32; + parentManifestCommitment: HexBytes32; + parentPageCommitment: HexBytes32; + pageSize: number; + totalCount: number; + pageCount: number; + pages: readonly ReconcilerEntitlementPage[]; +}>; + +function canonicalEntitlementIdentity( + value: ReconcilerEntitlementIdentity, +): ReconcilerEntitlementIdentity { + if ( + !/^0x[0-9a-f]{40}$/u.test(value.tokenAddress) || + !/^0x[0-9a-f]{40}$/u.test(value.vaultAddress) || + !/^0x[0-9a-f]{40}$/u.test(value.account) + ) { + throw validationError("config", "reconciler-entitlement-identity"); + } + return Object.freeze({ + tokenAddress: value.tokenAddress, + vaultAddress: value.vaultAddress, + account: value.account, + }); +} + +function entitlementIdentityJson( + value: ReconcilerEntitlementIdentity, +): CanonicalJsonValue { + return { + tokenAddress: value.tokenAddress, + vaultAddress: value.vaultAddress, + account: value.account, + }; +} + +export function createReconcilerEntitlementManifest(input: Readonly<{ + contract: ReconcilerPreParityContract; + parentPage: ReconcilerCorpusPage; + identities: readonly ReconcilerEntitlementIdentity[]; + pageSize?: number; +}>): ReconcilerEntitlementManifest { + const pageSize = input.pageSize ?? RECONCILER_CORPUS_PARTITION_SIZE; + if ( + !Number.isSafeInteger(pageSize) || + pageSize < 1 || + pageSize > RECONCILER_CORPUS_PARTITION_SIZE || + !Array.isArray(input.identities) || + input.identities.length < 1 || + input.identities.length > RECONCILER_CORPUS_MAXIMUM_TOTAL_COUNT + ) { + throw invalidInput("config", "reconciler-entitlement-page-size"); + } + const identities = Object.freeze(input.identities.map( + canonicalEntitlementIdentity, + )); + const unique = new Set( + identities.map(({ vaultAddress, account }) => `${vaultAddress}:${account}`), + ); + const parentTokens = new Set( + input.parentPage.identities.map(({ tokenAddress }) => tokenAddress), + ); + if ( + unique.size !== identities.length || + identities.some(({ tokenAddress }) => !parentTokens.has(tokenAddress)) + ) { + throw validationError("config", "reconciler-entitlement-cardinality"); + } + const parentManifestCommitment = canonicalBytes32( + input.parentPage.manifestCommitment, + ); + const parentPageCommitment = canonicalBytes32( + input.parentPage.pageCommitment, + ); + const pageCount = Math.ceil(identities.length / pageSize); + const manifestCommitment = commitment("reconciler-entitlement-manifest", { + version: RECONCILER_ENTITLEMENT_PARTITION_VERSION, + chainId: input.contract.chainId, + releaseId: input.contract.releaseId, + modelId: input.contract.modelId, + epochId: input.contract.epochId, + checkpointId: input.contract.checkpointId, + checkpointGeneration: input.contract.checkpointGeneration, + reorgGeneration: input.contract.reorgGeneration, + checkpointBlockNumber: input.contract.checkpointBlockNumber, + checkpointBlockHash: input.contract.checkpointBlockHash, + parentManifestCommitment, + parentPageCommitment, + pageSize, + totalCount: identities.length, + pageCount, + orderedEntitlements: identities.map(entitlementIdentityJson), + }); + const pages = Array.from({ length: pageCount }, (_, pageIndex) => { + const startIndex = pageIndex * pageSize; + const endIndexExclusive = Math.min(startIndex + pageSize, identities.length); + const pageIdentities = Object.freeze( + identities.slice(startIndex, endIndexExclusive), + ); + const continuation = pageIndex + 1 < pageCount + ? commitment("reconciler-entitlement-continuation", { + manifestCommitment, + nextPageIndex: pageIndex + 1, + }) + : null; + const pageCommitmentValue = commitment("reconciler-entitlement-page", { + manifestCommitment, + pageIndex, + pageCount, + pageSize, + totalCount: identities.length, + startIndex, + endIndexExclusive, + continuation, + identities: pageIdentities.map(entitlementIdentityJson), + }); + return Object.freeze({ + version: RECONCILER_ENTITLEMENT_PARTITION_VERSION, + manifestCommitment, + pageCommitment: pageCommitmentValue, + pageIndex, + pageCount, + pageSize, + totalCount: identities.length, + startIndex, + endIndexExclusive, + continuation, + identities: pageIdentities, + } satisfies ReconcilerEntitlementPage); + }); + return Object.freeze({ + version: RECONCILER_ENTITLEMENT_PARTITION_VERSION, + manifestCommitment, + parentManifestCommitment, + parentPageCommitment, + pageSize, + totalCount: identities.length, + pageCount, + pages: Object.freeze(pages), + }); +} + +export function assembleReconcilerEntitlementPages( + manifest: ReconcilerEntitlementManifest, + pages: readonly ReconcilerEntitlementPage[], +): readonly ReconcilerEntitlementIdentity[] { + if ( + pages.length !== manifest.pageCount || + manifest.pages.length !== manifest.pageCount + ) { + throw validationError("config", "reconciler-entitlement-page-cardinality"); + } + const assembled: ReconcilerEntitlementIdentity[] = []; + for (let pageIndex = 0; pageIndex < pages.length; pageIndex += 1) { + const page = pages[pageIndex]!; + const expected = manifest.pages[pageIndex]!; + if ( + page.version !== RECONCILER_ENTITLEMENT_PARTITION_VERSION || + page.manifestCommitment !== manifest.manifestCommitment || + page.pageCommitment !== expected.pageCommitment || + page.pageIndex !== pageIndex || + page.pageCount !== manifest.pageCount || + page.pageSize !== manifest.pageSize || + page.totalCount !== manifest.totalCount || + page.startIndex !== expected.startIndex || + page.endIndexExclusive !== expected.endIndexExclusive || + page.continuation !== expected.continuation || + canonicalizeFingerprintJson(page.identities.map(entitlementIdentityJson)) !== + canonicalizeFingerprintJson(expected.identities.map( + entitlementIdentityJson, + )) + ) { + throw validationError("config", "reconciler-entitlement-page-binding"); + } + assembled.push(...page.identities); + } + if (assembled.length !== manifest.totalCount) { + throw validationError("config", "reconciler-entitlement-page-completeness"); + } + return Object.freeze(assembled); +} diff --git a/lib/data-pipeline/reconciler-exact-block-reader.server.ts b/lib/data-pipeline/reconciler-exact-block-reader.server.ts new file mode 100644 index 00000000..b2f01cd9 --- /dev/null +++ b/lib/data-pipeline/reconciler-exact-block-reader.server.ts @@ -0,0 +1,1346 @@ +import "server-only"; + +import { + getAddress, + isAddress, + keccak256, + type Address, + type Hex, +} from "viem"; + +import { + canonicalBytes32, + type HexBytes32, +} from "./codecs"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import { + RECONCILER_CORPUS_MAXIMUM_TOTAL_COUNT, + RECONCILER_CORPUS_PARTITION_SIZE, +} from "./reconciler-corpus-partitions"; +import { + canonicalProjectorRpcEndpoint, + projectorRpcDeploymentCommitment, +} from "./projector-provider-commitments"; +import type { + ReconcilerIndexedRouteStore, + ReconcilerLiveSource, + ReconcilerPreParityContract, + ReconcilerRouteDto, + ReconcilerRouteDtoReader, +} from "./reconciler-preparity"; +import { rpcProviderCommitment } from "./rpc-provider-commitments"; + +type Environment = Readonly>; +type Fetch = typeof fetch; + +const HEX_QUANTITY = /^0x(?:0|[1-9a-f][0-9a-f]*)$/u; +const HEX_DATA = /^0x(?:[0-9a-f]{2})*$/u; +const MAXIMUM_RESPONSE_BYTES = 8 * 1024 * 1024; +const DEFAULT_REQUEST_BUDGET = 512; +const DEFAULT_BATCH_SIZE = 32; +const MAXIMUM_BATCH_SIZE = 100; +const DEFAULT_LOGICAL_REQUEST_BUDGET = + DEFAULT_REQUEST_BUDGET * DEFAULT_BATCH_SIZE; +const DEFAULT_TIMEOUT_MS = 5_000; + +function quantity(value: bigint): Hex { + if (value < 0n) throw invalidInput("rpc", "negative-block-number"); + return `0x${value.toString(16)}` as Hex; +} + +function parseQuantity(value: unknown, operation: string): bigint { + if (typeof value !== "string" || !HEX_QUANTITY.test(value)) { + throw validationError("rpc", operation); + } + return BigInt(value); +} + +function data(value: unknown, operation: string): Hex { + if (typeof value !== "string" || !HEX_DATA.test(value)) { + throw validationError("rpc", operation); + } + return value as Hex; +} + +function address(value: unknown, operation: string): Address { + if (typeof value !== "string" || !isAddress(value)) { + throw validationError("rpc", operation); + } + return getAddress(value); +} + +function object(value: unknown, operation: string): Record { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw validationError("rpc", operation); + } + return value as Record; +} + +function exactBlockHash(value: unknown, operation: string): HexBytes32 { + try { + return canonicalBytes32(value); + } catch { + throw validationError("rpc", operation); + } +} + +export type ExactBlockRpcLog = Readonly<{ + address: Address; + blockNumber: bigint; + blockHash: HexBytes32; + transactionHash: HexBytes32; + transactionIndex: number; + logIndex: number; + topics: readonly Hex[]; + data: Hex; +}>; + +export type ExactBlockRpcReceiptLog = ExactBlockRpcLog & Readonly<{ + /** Zero-based position in the canonical transaction receipt log array. */ + receiptLogIndex: number; +}>; + +export type ExactBlockRpcReceipt = Readonly<{ + transactionHash: HexBytes32; + blockNumber: bigint; + blockHash: HexBytes32; + transactionIndex: number; + status: 1n; + logs: readonly ExactBlockRpcReceiptLog[]; +}>; + +export type ExactBlockRpcReceiptBinding = Readonly<{ + transactionHash: HexBytes32; + expectedBlockNumber: bigint; + expectedBlockHash: HexBytes32; +}>; + +export type ExactBlockRpcTransaction = Readonly<{ + transactionHash: HexBytes32; + blockNumber: bigint; + blockHash: HexBytes32; + transactionIndex: number; + from: Address; + to: Address; + input: Hex; + value: bigint; +}>; + +export type ExactBlockRpcTransactionBinding = Readonly<{ + transactionHash: HexBytes32; + expectedBlockNumber: bigint; + expectedBlockHash: HexBytes32; + expectedTo: Address; +}>; + +export type ExactBlockRpcTimestampBinding = Readonly<{ + blockNumber: bigint; + expectedHash?: HexBytes32; +}>; + +export type ExactBlockRpcCall = Readonly<{ + to: Address; + data: Hex; +}>; + +export type ExactBlockRpcPartitionBinding = Readonly<{ + manifestCommitment: HexBytes32; + pageCommitment: HexBytes32; + pageIndex: number; + pageCount: number; + pageSize: number; + totalCount: number; + startIndex: number; + endIndexExclusive: number; +}>; + +function safeQuantityNumber(value: unknown, operation: string): number { + const parsed = parseQuantity(value, operation); + if (parsed > BigInt(Number.MAX_SAFE_INTEGER)) { + throw validationError("rpc", operation); + } + return Number(parsed); +} + +function decodeRpcLog( + raw: unknown, + operation: string, +): ExactBlockRpcLog { + const row = object(raw, operation); + if ( + (row.removed !== undefined && row.removed !== false) || + !Array.isArray(row.topics) + ) { + throw validationError("rpc", `${operation}-canonical`); + } + return Object.freeze({ + address: address(row.address, `${operation}-address`), + blockNumber: parseQuantity(row.blockNumber, `${operation}-block-number`), + blockHash: exactBlockHash(row.blockHash, `${operation}-block-hash`), + transactionHash: exactBlockHash( + row.transactionHash, + `${operation}-transaction-hash`, + ), + transactionIndex: safeQuantityNumber( + row.transactionIndex, + `${operation}-transaction-index`, + ), + logIndex: safeQuantityNumber(row.logIndex, `${operation}-log-index`), + topics: Object.freeze(row.topics.map((topic) => + exactBlockHash(topic, `${operation}-topic`) as Hex + )), + data: data(row.data, `${operation}-data`), + }); +} + +export type ExactBlockRpcClient = Readonly<{ + endpointCommitment: HexBytes32; + endpointOriginCommitment: HexBytes32; + /** Physical HTTP requests reserved by this client. */ + requestCount(): number; + /** Individual JSON-RPC operations reserved by this client. */ + logicalRequestCount(): number; + /** + * Creates one independently bounded client for the next manifest-bound + * corpus page. The parent aggregates its counters and rejects reuse or + * out-of-order page issuance. + */ + createPartitionClient( + binding: ExactBlockRpcPartitionBinding, + ): ExactBlockRpcClient; + assertCheckpoint(input: { + blockNumber: bigint; + blockHash: HexBytes32; + signal: AbortSignal; + }): Promise; + call(input: { + to: Address; + data: Hex; + blockHash: HexBytes32; + signal: AbortSignal; + }): Promise; + callMany(input: { + calls: readonly ExactBlockRpcCall[]; + blockHash: HexBytes32; + signal: AbortSignal; + }): Promise; + getCodeHash(input: { + address: Address; + blockHash: HexBytes32; + signal: AbortSignal; + }): Promise; + getLogs(input: { + addresses: Address | readonly Address[]; + topics?: readonly (Hex | readonly Hex[] | null)[]; + fromBlock: bigint; + toBlock: bigint; + maximumLogs: number; + signal: AbortSignal; + }): Promise; + getBlockTimestamp(input: { + blockNumber: bigint; + expectedHash?: HexBytes32; + signal: AbortSignal; + }): Promise; + getBlockTimestamps(input: { + blocks: readonly ExactBlockRpcTimestampBinding[]; + signal: AbortSignal; + }): Promise; + getTransactionReceipt(input: { + transactionHash: HexBytes32; + expectedBlockNumber: bigint; + expectedBlockHash: HexBytes32; + signal: AbortSignal; + }): Promise; + getTransactionReceipts(input: { + receipts: readonly ExactBlockRpcReceiptBinding[]; + signal: AbortSignal; + }): Promise; + getTransaction(input: { + transactionHash: HexBytes32; + expectedBlockNumber: bigint; + expectedBlockHash: HexBytes32; + expectedTo: Address; + signal: AbortSignal; + }): Promise; + getTransactions(input: { + transactions: readonly ExactBlockRpcTransactionBinding[]; + signal: AbortSignal; + }): Promise; +}>; + +type JsonRpcRequest = Readonly<{ + jsonrpc: "2.0"; + id: number; + method: string; + params: readonly unknown[]; +}>; + +type ExactBlockRpcBudgetLedger = { + physicalRequests: number; + logicalRequests: number; +}; + +class ProviderLogLimitError extends Error { + constructor() { + super("Provider rejected the eth_getLogs range"); + this.name = "ProviderLogLimitError"; + } +} + +const EXPLICIT_LOG_LIMIT_PATTERNS = Object.freeze([ + /\beth_getlogs\b.{0,120}\b(?:block range|range|response size|result size|limit(?:ed)?|too (?:large|wide|many)|exceed(?:ed|s)?)\b/iu, + /\b(?:block range|response size|result size|query size)\b.{0,120}\b(?:limit(?:ed)?|too (?:large|wide)|exceed(?:ed|s)?|maximum|max)\b/iu, + /\b(?:too many|more than|maximum|max)\b.{0,120}\b(?:logs?|results?|blocks?)\b/iu, + /\bquery returned more than\b/iu, +]); + +function isExplicitProviderLogLimit(value: unknown): boolean { + let text: string; + if (typeof value === "string") { + text = value; + } else { + try { + text = JSON.stringify(value); + } catch { + return false; + } + } + return EXPLICIT_LOG_LIMIT_PATTERNS.some((pattern) => pattern.test(text)); +} + +function isSplittableLogFailure(error: unknown): boolean { + if (error instanceof ProviderLogLimitError) return true; + if (!(error instanceof DataPipelineError) || error.code !== "response_oversize") { + return false; + } + const operation = error.safeMetadata?.operation; + return operation === "reconciler-rpc-response" || + operation === "reconciler-rpc-log-count"; +} + +function assertCanonicalLogOrder( + logs: readonly ExactBlockRpcLog[], + operation: string, +): void { + for (let index = 1; index < logs.length; index += 1) { + const previous = logs[index - 1]!; + const current = logs[index]!; + if ( + current.blockNumber < previous.blockNumber || + (current.blockNumber === previous.blockNumber && + (current.transactionIndex < previous.transactionIndex || + (current.transactionIndex === previous.transactionIndex && + current.logIndex <= previous.logIndex))) + ) { + throw validationError("rpc", operation); + } + } +} + +function hasOwn(value: object, property: PropertyKey): boolean { + return Object.prototype.hasOwnProperty.call(value, property); +} + +async function responseText(response: Response): Promise { + const declared = response.headers.get("content-length"); + if (declared !== null && !/^(?:0|[1-9][0-9]*)$/u.test(declared)) { + throw validationError("rpc", "reconciler-rpc-content-length"); + } + if ( + declared !== null && + BigInt(declared) > BigInt(MAXIMUM_RESPONSE_BYTES) + ) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-response" }, + }); + } + if (!response.body) return ""; + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let byteLength = 0; + try { + while (true) { + const chunk = await reader.read(); + if (chunk.done) break; + byteLength += chunk.value.byteLength; + if (byteLength > MAXIMUM_RESPONSE_BYTES) { + try { + await reader.cancel(); + } catch { + // The size failure remains authoritative even if cancellation races + // with a provider closing the response stream. + } + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-response" }, + }); + } + chunks.push(chunk.value); + } + } finally { + reader.releaseLock(); + } + return new TextDecoder("utf-8", { fatal: true }).decode( + Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), byteLength), + ); +} + +export function createExactBlockRpcClient(input: { + endpoint: string; + endpointCommitment: HexBytes32; + endpointOriginCommitment: HexBytes32; + fetch?: Fetch; + maximumRequests?: number; + maximumLogicalRequests?: number; + maximumBatchSize?: number; + timeoutMs?: number; + /** @internal Root, corpus page, then one bounded nested-work page. */ + partitionDepth?: number; + /** @internal One budget shared by the root and every partition descendant. */ + budgetLedger?: ExactBlockRpcBudgetLedger; +}): ExactBlockRpcClient { + const fetchImplementation = input.fetch ?? fetch; + const maximumRequests = input.maximumRequests ?? DEFAULT_REQUEST_BUDGET; + const maximumLogicalRequests = input.maximumLogicalRequests ?? + DEFAULT_LOGICAL_REQUEST_BUDGET; + const maximumBatchSize = input.maximumBatchSize ?? DEFAULT_BATCH_SIZE; + const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const partitionDepth = input.partitionDepth ?? 0; + const budgetLedger = input.budgetLedger ?? { + physicalRequests: 0, + logicalRequests: 0, + }; + if ( + !Number.isSafeInteger(maximumRequests) || + maximumRequests < 1 || + maximumRequests > 10_000 || + !Number.isSafeInteger(maximumLogicalRequests) || + maximumLogicalRequests < 1 || + maximumLogicalRequests > 1_000_000 || + !Number.isSafeInteger(maximumBatchSize) || + maximumBatchSize < 1 || + maximumBatchSize > MAXIMUM_BATCH_SIZE || + !Number.isSafeInteger(timeoutMs) || + timeoutMs < 100 || + timeoutMs > 30_000 || + !Number.isSafeInteger(partitionDepth) || + partitionDepth < 0 || + partitionDepth > 2 + ) { + throw invalidInput("config", "reconciler-rpc-budget"); + } + let nextId = 1; + const partitionClients: ExactBlockRpcClient[] = []; + const issuedPartitions = new Set(); + let partitionManifestCommitment: HexBytes32 | null = null; + let partitionPageCount: number | null = null; + let partitionPageSize: number | null = null; + let partitionTotalCount: number | null = null; + let partitionNextStartIndex = 0; + let partitionSequenceClosed = false; + + const assertNotAborted = (signal: AbortSignal) => { + if (signal.aborted) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-aborted" }, + }); + } + }; + + // Reserve the whole operation before the first network request. This makes + // oversized batches atomic: they cannot spend a prefix of their budget and + // then fail midway through the input. + const reserve = (physicalCount: number, logicalCount: number) => { + const nextPhysicalCount = budgetLedger.physicalRequests + physicalCount; + const nextLogicalCount = budgetLedger.logicalRequests + logicalCount; + budgetLedger.physicalRequests = nextPhysicalCount; + budgetLedger.logicalRequests = nextLogicalCount; + if (nextPhysicalCount > maximumRequests) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-request-budget" }, + }); + } + if (nextLogicalCount > maximumLogicalRequests) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-logical-budget" }, + }); + } + }; + + const allocateRequest = ( + method: string, + params: readonly unknown[], + ): JsonRpcRequest => { + if (!Number.isSafeInteger(nextId)) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-id-budget" }, + }); + } + const request = Object.freeze({ + jsonrpc: "2.0" as const, + id: nextId, + method, + params, + }); + nextId += 1; + return request; + }; + + const post = async ( + body: JsonRpcRequest | readonly JsonRpcRequest[], + signal: AbortSignal, + ): Promise => { + assertNotAborted(signal); + const controller = new AbortController(); + const abort = () => controller.abort(); + signal.addEventListener("abort", abort, { once: true }); + const timer = setTimeout(abort, timeoutMs); + try { + const encodedBody = JSON.stringify(body); + if (Buffer.byteLength(encodedBody, "utf8") > MAXIMUM_RESPONSE_BYTES) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-request-body" }, + }); + } + const response = await fetchImplementation(input.endpoint, { + method: "POST", + redirect: "error", + headers: { "content-type": "application/json" }, + body: encodedBody, + signal: controller.signal, + }); + if (!response.ok) { + const isLogRequest = !Array.isArray(body) && + (body as JsonRpcRequest).method === "eth_getLogs"; + if (isLogRequest && response.status !== 408 && response.status !== 429) { + if (response.status === 413) { + throw new ProviderLogLimitError(); + } + const failureBody = await responseText(response); + if (isExplicitProviderLogLimit(failureBody)) { + throw new ProviderLogLimitError(); + } + } + throw dataPipelineError({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: response.status === 408 || response.status === 429 || + response.status >= 500, + countsTowardCircuit: true, + metadata: { + operation: "reconciler-rpc-http", + status: response.status, + }, + }); + } + try { + return JSON.parse(await responseText(response)) as unknown; + } catch (error) { + if (error instanceof DataPipelineError) { + throw error; + } + throw dataPipelineError({ + dependency: "rpc", + code: "invalid_json", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-json" }, + }); + } + } catch (error) { + if ( + error instanceof DataPipelineError || + error instanceof ProviderLogLimitError + ) { + throw error; + } + throw dataPipelineError({ + dependency: "rpc", + code: controller.signal.aborted ? "timeout" : "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-request" }, + }); + } finally { + clearTimeout(timer); + signal.removeEventListener("abort", abort); + } + }; + + const decodeSingleResponse = ( + decoded: unknown, + expectedId: number, + method: string, + ): unknown => { + const envelope = object(decoded, "reconciler-rpc-envelope"); + const hasResult = hasOwn(envelope, "result"); + const hasError = hasOwn(envelope, "error"); + if ( + envelope.jsonrpc !== "2.0" || + envelope.id !== expectedId || + hasResult === hasError + ) { + throw validationError("rpc", "reconciler-rpc-envelope"); + } + if (hasError) { + if ( + method === "eth_getLogs" && + isExplicitProviderLogLimit(envelope.error) + ) { + throw new ProviderLogLimitError(); + } + throw validationError("rpc", "reconciler-rpc-item-error"); + } + return envelope.result; + }; + + const rpc = async ( + method: string, + params: readonly unknown[], + signal: AbortSignal, + ): Promise => { + assertNotAborted(signal); + reserve(1, 1); + const request = allocateRequest(method, params); + return decodeSingleResponse( + await post(request, signal), + request.id, + method, + ); + }; + + const rpcBatch = async ( + requestsForBatch: readonly JsonRpcRequest[], + signal: AbortSignal, + ): Promise => { + const decoded = await post(requestsForBatch, signal); + if (!Array.isArray(decoded)) { + throw validationError("rpc", "reconciler-rpc-batch-envelope"); + } + const expectedIds = new Set(requestsForBatch.map((request) => request.id)); + const results = new Map(); + for (const rawEnvelope of decoded) { + const envelope = object(rawEnvelope, "reconciler-rpc-batch-item"); + if ( + envelope.jsonrpc !== "2.0" || + !Number.isSafeInteger(envelope.id) + ) { + throw validationError("rpc", "reconciler-rpc-batch-item"); + } + const id = envelope.id as number; + if (!expectedIds.has(id)) { + throw validationError("rpc", "reconciler-rpc-batch-id-unknown"); + } + if (results.has(id)) { + throw validationError("rpc", "reconciler-rpc-batch-id-duplicate"); + } + const hasResult = hasOwn(envelope, "result"); + const hasError = hasOwn(envelope, "error"); + if (hasResult === hasError) { + throw validationError("rpc", "reconciler-rpc-batch-item-shape"); + } + if (hasError) { + throw validationError("rpc", "reconciler-rpc-batch-item-error"); + } + results.set(id, envelope.result); + } + if (results.size !== requestsForBatch.length) { + throw validationError("rpc", "reconciler-rpc-batch-id-missing"); + } + return Object.freeze(requestsForBatch.map((request) => + results.get(request.id) + )); + }; + + const validateReceiptBinding = ( + binding: ExactBlockRpcReceiptBinding, + ) => { + if (binding.expectedBlockNumber < 0n) { + throw invalidInput("rpc", "reconciler-rpc-receipt-block"); + } + return Object.freeze({ + transactionHash: canonicalBytes32(binding.transactionHash), + expectedBlockNumber: binding.expectedBlockNumber, + expectedBlockHash: canonicalBytes32(binding.expectedBlockHash), + }); + }; + + const decodeReceipt = ( + raw: unknown, + binding: ReturnType, + ): ExactBlockRpcReceipt => { + const row = object(raw, "reconciler-rpc-receipt"); + const transactionHash = exactBlockHash( + row.transactionHash, + "reconciler-rpc-receipt-transaction-hash", + ); + const blockNumber = parseQuantity( + row.blockNumber, + "reconciler-rpc-receipt-block-number", + ); + const blockHash = exactBlockHash( + row.blockHash, + "reconciler-rpc-receipt-block-hash", + ); + const transactionIndex = safeQuantityNumber( + row.transactionIndex, + "reconciler-rpc-receipt-transaction-index", + ); + const status = parseQuantity( + row.status, + "reconciler-rpc-receipt-status", + ); + if ( + transactionHash !== binding.transactionHash || + blockNumber !== binding.expectedBlockNumber || + blockHash !== binding.expectedBlockHash || + status !== 1n || + !Array.isArray(row.logs) + ) { + throw validationError("rpc", "reconciler-rpc-receipt-binding"); + } + const logs = row.logs.map((rawLog, receiptLogIndex) => { + const log = decodeRpcLog(rawLog, "reconciler-rpc-receipt-log"); + if ( + log.transactionHash !== transactionHash || + log.blockNumber !== blockNumber || + log.blockHash !== blockHash || + log.transactionIndex !== transactionIndex + ) { + throw validationError("rpc", "reconciler-rpc-receipt-log-binding"); + } + return Object.freeze({ ...log, receiptLogIndex }); + }); + for (let index = 1; index < logs.length; index += 1) { + if (logs[index]!.logIndex <= logs[index - 1]!.logIndex) { + throw validationError("rpc", "reconciler-rpc-receipt-log-order"); + } + } + return Object.freeze({ + transactionHash, + blockNumber, + blockHash, + transactionIndex, + status: 1n as const, + logs: Object.freeze(logs), + }); + }; + + const validateTransactionBinding = ( + binding: ExactBlockRpcTransactionBinding, + ) => { + if (binding.expectedBlockNumber < 0n) { + throw invalidInput("rpc", "reconciler-rpc-transaction-block"); + } + return Object.freeze({ + transactionHash: canonicalBytes32(binding.transactionHash), + expectedBlockNumber: binding.expectedBlockNumber, + expectedBlockHash: canonicalBytes32(binding.expectedBlockHash), + expectedTo: getAddress(binding.expectedTo), + }); + }; + + const decodeTransaction = ( + raw: unknown, + binding: ReturnType, + ): ExactBlockRpcTransaction => { + const row = object(raw, "reconciler-rpc-transaction"); + const resolved = Object.freeze({ + transactionHash: exactBlockHash( + row.hash, + "reconciler-rpc-transaction-hash", + ), + blockNumber: parseQuantity( + row.blockNumber, + "reconciler-rpc-transaction-block-number", + ), + blockHash: exactBlockHash( + row.blockHash, + "reconciler-rpc-transaction-block-hash", + ), + transactionIndex: safeQuantityNumber( + row.transactionIndex, + "reconciler-rpc-transaction-index", + ), + from: address(row.from, "reconciler-rpc-transaction-from"), + to: address(row.to, "reconciler-rpc-transaction-to"), + input: data(row.input, "reconciler-rpc-transaction-input"), + value: parseQuantity(row.value, "reconciler-rpc-transaction-value"), + }); + if ( + resolved.transactionHash !== binding.transactionHash || + resolved.blockNumber !== binding.expectedBlockNumber || + resolved.blockHash !== binding.expectedBlockHash || + resolved.to !== binding.expectedTo + ) { + throw validationError("rpc", "reconciler-rpc-transaction-binding"); + } + return resolved; + }; + + const decodeBlock = (raw: unknown, blockNumber: bigint) => { + const result = object(raw, "reconciler-rpc-block"); + const number = parseQuantity(result.number, "reconciler-rpc-block-number"); + const hash = exactBlockHash(result.hash, "reconciler-rpc-block-hash"); + const timestamp = parseQuantity( + result.timestamp, + "reconciler-rpc-block-timestamp", + ); + if (number !== blockNumber) { + throw validationError("rpc", "reconciler-rpc-block-number-mismatch"); + } + return { number, hash, timestamp } as const; + }; + + const readBlock = async ( + blockNumber: bigint, + signal: AbortSignal, + ) => decodeBlock( + await rpc("eth_getBlockByNumber", [quantity(blockNumber), false], signal), + blockNumber, + ); + + return Object.freeze({ + endpointCommitment: canonicalBytes32(input.endpointCommitment), + endpointOriginCommitment: canonicalBytes32( + input.endpointOriginCommitment, + ), + requestCount: () => budgetLedger.physicalRequests, + logicalRequestCount: () => budgetLedger.logicalRequests, + createPartitionClient(binding) { + if (partitionDepth >= 2) { + throw invalidInput("rpc", "reconciler-rpc-nested-partition"); + } + const manifestCommitment = canonicalBytes32(binding.manifestCommitment); + const pageCommitment = canonicalBytes32(binding.pageCommitment); + if ( + !Number.isSafeInteger(binding.pageIndex) || + !Number.isSafeInteger(binding.pageCount) || + !Number.isSafeInteger(binding.pageSize) || + !Number.isSafeInteger(binding.totalCount) || + !Number.isSafeInteger(binding.startIndex) || + !Number.isSafeInteger(binding.endIndexExclusive) || + binding.pageCount < 1 || + binding.pageSize < 1 || + binding.pageSize > RECONCILER_CORPUS_PARTITION_SIZE || + binding.totalCount < 1 || + binding.totalCount > RECONCILER_CORPUS_MAXIMUM_TOTAL_COUNT || + binding.pageCount !== Math.ceil(binding.totalCount / binding.pageSize) || + binding.pageIndex !== partitionClients.length || + binding.pageIndex >= binding.pageCount || + binding.startIndex !== binding.pageIndex * binding.pageSize || + binding.endIndexExclusive !== Math.min( + binding.startIndex + binding.pageSize, + binding.totalCount, + ) || + partitionSequenceClosed + ) { + throw invalidInput("rpc", "reconciler-rpc-partition-binding"); + } + if (partitionManifestCommitment === null) { + if (binding.pageIndex !== 0 || binding.startIndex !== 0) { + throw invalidInput("rpc", "reconciler-rpc-partition-sequence"); + } + partitionManifestCommitment = manifestCommitment; + partitionPageCount = binding.pageCount; + partitionPageSize = binding.pageSize; + partitionTotalCount = binding.totalCount; + } else if ( + manifestCommitment !== partitionManifestCommitment || + binding.pageCount !== partitionPageCount || + binding.pageSize !== partitionPageSize || + binding.totalCount !== partitionTotalCount || + binding.startIndex !== partitionNextStartIndex + ) { + throw invalidInput("rpc", "reconciler-rpc-partition-sequence"); + } + const identity = `${manifestCommitment}:${pageCommitment}:${binding.pageIndex}`; + if (issuedPartitions.has(identity) || issuedPartitions.has(pageCommitment)) { + throw invalidInput("rpc", "reconciler-rpc-partition-reuse"); + } + issuedPartitions.add(identity); + issuedPartitions.add(pageCommitment); + const client = createExactBlockRpcClient({ + endpoint: input.endpoint, + endpointCommitment: input.endpointCommitment, + endpointOriginCommitment: input.endpointOriginCommitment, + fetch: fetchImplementation, + maximumRequests, + maximumLogicalRequests, + maximumBatchSize, + timeoutMs, + partitionDepth: partitionDepth + 1, + budgetLedger, + }); + partitionClients.push(client); + partitionNextStartIndex = binding.endIndexExclusive; + partitionSequenceClosed = binding.pageIndex + 1 === binding.pageCount; + return client; + }, + async assertCheckpoint({ blockNumber, blockHash, signal }) { + const block = await readBlock(blockNumber, signal); + if (block.hash !== canonicalBytes32(blockHash)) { + throw validationError("rpc", "reconciler-rpc-checkpoint-mismatch"); + } + return block.timestamp; + }, + async call({ to, data: callData, blockHash, signal }) { + return data( + await rpc( + "eth_call", + [ + { to: getAddress(to), data: data(callData, "reconciler-call-data") }, + { + blockHash: canonicalBytes32(blockHash), + requireCanonical: true, + }, + ], + signal, + ), + "reconciler-rpc-call-result", + ); + }, + async callMany({ calls, blockHash, signal }) { + assertNotAborted(signal); + const exactHash = canonicalBytes32(blockHash); + if (!Array.isArray(calls)) { + throw invalidInput("rpc", "reconciler-rpc-batch-calls"); + } + const validatedCalls = calls.map((call) => Object.freeze({ + to: address(call.to, "reconciler-rpc-batch-call-address"), + data: data(call.data, "reconciler-rpc-batch-call-data"), + })); + if (validatedCalls.length === 0) { + return Object.freeze([]); + } + const physicalCount = Math.ceil( + validatedCalls.length / maximumBatchSize, + ); + reserve(physicalCount, validatedCalls.length); + const results: Hex[] = []; + for ( + let offset = 0; + offset < validatedCalls.length; + offset += maximumBatchSize + ) { + const chunk = validatedCalls.slice(offset, offset + maximumBatchSize); + const requestsForBatch = chunk.map((call) => allocateRequest( + "eth_call", + [ + call, + { blockHash: exactHash, requireCanonical: true }, + ], + )); + const batchResults = await rpcBatch(requestsForBatch, signal); + for (const result of batchResults) { + results.push(data(result, "reconciler-rpc-batch-call-result")); + } + } + return Object.freeze(results); + }, + async getCodeHash({ address: contractAddress, blockHash, signal }) { + const code = data( + await rpc( + "eth_getCode", + [ + getAddress(contractAddress), + { + blockHash: canonicalBytes32(blockHash), + requireCanonical: true, + }, + ], + signal, + ), + "reconciler-rpc-code", + ); + if (code === "0x") { + throw validationError("rpc", "reconciler-rpc-code-empty"); + } + return keccak256(code); + }, + async getLogs({ + addresses, + topics, + fromBlock, + toBlock, + maximumLogs, + signal, + }) { + if ( + fromBlock < 0n || + toBlock < fromBlock || + !Number.isSafeInteger(maximumLogs) || + maximumLogs < 1 || + maximumLogs > 100_000 + ) { + throw invalidInput("rpc", "reconciler-log-request"); + } + const exactAddresses = Array.isArray(addresses) + ? addresses.map((item) => getAddress(item)) + : getAddress(addresses as Address); + const readRange = async ( + rangeFromBlock: bigint, + rangeToBlock: bigint, + ): Promise => { + try { + const result = await rpc( + "eth_getLogs", + [{ + address: exactAddresses, + fromBlock: quantity(rangeFromBlock), + toBlock: quantity(rangeToBlock), + ...(topics ? { topics } : {}), + }], + signal, + ); + if (!Array.isArray(result) || result.length > maximumLogs) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "reconciler-rpc-log-count" }, + }); + } + const logs = result.map((raw) => + decodeRpcLog(raw, "reconciler-rpc-log") + ); + if (logs.some((log) => + log.blockNumber < rangeFromBlock || + log.blockNumber > rangeToBlock + )) { + throw validationError("rpc", "reconciler-rpc-log-block-range"); + } + assertCanonicalLogOrder(logs, "reconciler-rpc-log-order"); + return Object.freeze(logs); + } catch (error) { + if (!isSplittableLogFailure(error)) throw error; + if (rangeFromBlock === rangeToBlock) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { + operation: "reconciler-rpc-log-single-block-oversize", + }, + }); + } + const midpoint = rangeFromBlock + + (rangeToBlock - rangeFromBlock) / 2n; + const left = await readRange(rangeFromBlock, midpoint); + const right = await readRange(midpoint + 1n, rangeToBlock); + const merged = Object.freeze([...left, ...right]); + assertCanonicalLogOrder( + merged, + "reconciler-rpc-log-split-order", + ); + return merged; + } + }; + return readRange(fromBlock, toBlock); + }, + async getBlockTimestamp({ blockNumber, expectedHash, signal }) { + const block = await readBlock(blockNumber, signal); + if (expectedHash && block.hash !== canonicalBytes32(expectedHash)) { + throw validationError("rpc", "reconciler-rpc-block-hash-mismatch"); + } + return block.timestamp; + }, + async getBlockTimestamps({ blocks, signal }) { + assertNotAborted(signal); + if (!Array.isArray(blocks)) { + throw invalidInput("rpc", "reconciler-rpc-block-timestamps"); + } + const bindings = blocks.map((binding) => { + if (binding.blockNumber < 0n) { + throw invalidInput("rpc", "reconciler-rpc-block-timestamp-number"); + } + return Object.freeze({ + blockNumber: binding.blockNumber, + expectedHash: binding.expectedHash === undefined + ? undefined + : canonicalBytes32(binding.expectedHash), + }); + }); + if (bindings.length === 0) return Object.freeze([]); + reserve(Math.ceil(bindings.length / maximumBatchSize), bindings.length); + const timestamps: bigint[] = []; + for ( + let offset = 0; + offset < bindings.length; + offset += maximumBatchSize + ) { + const chunk = bindings.slice(offset, offset + maximumBatchSize); + const requestsForBatch = chunk.map((binding) => allocateRequest( + "eth_getBlockByNumber", + [quantity(binding.blockNumber), false], + )); + const results = await rpcBatch(requestsForBatch, signal); + for (let index = 0; index < results.length; index += 1) { + const binding = chunk[index]!; + const block = decodeBlock(results[index], binding.blockNumber); + if ( + binding.expectedHash !== undefined && + block.hash !== binding.expectedHash + ) { + throw validationError( + "rpc", + "reconciler-rpc-block-hash-mismatch", + ); + } + timestamps.push(block.timestamp); + } + } + return Object.freeze(timestamps); + }, + async getTransactionReceipt({ + transactionHash, + expectedBlockNumber, + expectedBlockHash, + signal, + }) { + const binding = validateReceiptBinding({ + transactionHash, + expectedBlockNumber, + expectedBlockHash, + }); + return decodeReceipt( + await rpc( + "eth_getTransactionReceipt", + [binding.transactionHash], + signal, + ), + binding, + ); + }, + async getTransactionReceipts({ receipts, signal }) { + assertNotAborted(signal); + if (!Array.isArray(receipts)) { + throw invalidInput("rpc", "reconciler-rpc-receipts"); + } + const bindings = receipts.map(validateReceiptBinding); + if (bindings.length === 0) { + return Object.freeze([]); + } + reserve(Math.ceil(bindings.length / maximumBatchSize), bindings.length); + const resolved: ExactBlockRpcReceipt[] = []; + for ( + let offset = 0; + offset < bindings.length; + offset += maximumBatchSize + ) { + const chunk = bindings.slice(offset, offset + maximumBatchSize); + const requestsForBatch = chunk.map((binding) => allocateRequest( + "eth_getTransactionReceipt", + [binding.transactionHash], + )); + const results = await rpcBatch(requestsForBatch, signal); + for (let index = 0; index < results.length; index += 1) { + resolved.push(decodeReceipt(results[index], chunk[index]!)); + } + } + return Object.freeze(resolved); + }, + async getTransaction({ + transactionHash, + expectedBlockNumber, + expectedBlockHash, + expectedTo, + signal, + }) { + const binding = validateTransactionBinding({ + transactionHash, + expectedBlockNumber, + expectedBlockHash, + expectedTo, + }); + return decodeTransaction( + await rpc( + "eth_getTransactionByHash", + [binding.transactionHash], + signal, + ), + binding, + ); + }, + async getTransactions({ transactions, signal }) { + assertNotAborted(signal); + if (!Array.isArray(transactions)) { + throw invalidInput("rpc", "reconciler-rpc-transactions"); + } + const bindings = transactions.map(validateTransactionBinding); + if (bindings.length === 0) { + return Object.freeze([]); + } + reserve(Math.ceil(bindings.length / maximumBatchSize), bindings.length); + const resolved: ExactBlockRpcTransaction[] = []; + for ( + let offset = 0; + offset < bindings.length; + offset += maximumBatchSize + ) { + const chunk = bindings.slice(offset, offset + maximumBatchSize); + const requestsForBatch = chunk.map((binding) => allocateRequest( + "eth_getTransactionByHash", + [binding.transactionHash], + )); + const results = await rpcBatch(requestsForBatch, signal); + for (let index = 0; index < results.length; index += 1) { + resolved.push(decodeTransaction(results[index], chunk[index]!)); + } + } + return Object.freeze(resolved); + }, + }); +} + +export type ExactBlockRouteBuilder = (input: { + rpc: ExactBlockRpcClient; + contract: ReconcilerPreParityContract; + blockNumber: bigint; + blockHash: HexBytes32; + signal: AbortSignal; +}) => Promise; + +function endpointConfigurations(env: Environment) { + const values = [ + { + vendor: "alchemy" as const, + endpoint: canonicalProjectorRpcEndpoint( + env.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL, + "alchemy", + ), + }, + { + vendor: "quicknode" as const, + endpoint: canonicalProjectorRpcEndpoint( + env.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL, + "quicknode", + ), + }, + ]; + if (new URL(values[0]!.endpoint).origin === new URL(values[1]!.endpoint).origin) { + throw invalidInput("config", "reconciler-rpc-provider-independence"); + } + return values.map((value) => Object.freeze({ + ...value, + endpointCommitment: projectorRpcDeploymentCommitment(value.endpoint), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(value.endpoint).origin, + ), + })); +} + +function sourceEndpoint( + configurations: ReturnType, + source: ReconcilerLiveSource, +) { + const match = configurations.find((candidate) => + candidate.vendor === source.vendorGroup && + candidate.endpointCommitment === source.endpointCommitment && + candidate.endpointOriginCommitment === source.endpointOriginCommitment + ); + if (!match) { + throw invalidInput("rpc", "reconciler-live-source-binding"); + } + return match; +} + +export function createExactBlockReconcilerRouteDtoReader(input: { + env?: Environment; + indexedStore: ReconcilerIndexedRouteStore; + buildLiveRoutes: ExactBlockRouteBuilder; + fetch?: Fetch; + maximumRequestsPerProvider?: number; + maximumLogicalRequestsPerProvider?: number; + maximumBatchSize?: number; + timeoutMs?: number; +}): ReconcilerRouteDtoReader { + const configurations = endpointConfigurations(input.env ?? process.env); + return Object.freeze({ + async readLiveRoutes({ + source, + contract, + blockNumber, + blockHash, + signal, + }) { + const configuration = sourceEndpoint(configurations, source); + const rpc = createExactBlockRpcClient({ + endpoint: configuration.endpoint, + endpointCommitment: configuration.endpointCommitment, + endpointOriginCommitment: configuration.endpointOriginCommitment, + fetch: input.fetch, + maximumRequests: input.maximumRequestsPerProvider, + maximumLogicalRequests: input.maximumLogicalRequestsPerProvider, + maximumBatchSize: input.maximumBatchSize, + timeoutMs: input.timeoutMs, + }); + await rpc.assertCheckpoint({ blockNumber, blockHash, signal }); + const routes = await input.buildLiveRoutes({ + rpc, + contract, + blockNumber, + blockHash, + signal, + }); + const byKey = new Map(routes.map((route) => [route.routeKey, route])); + if (byKey.size !== routes.length) { + throw validationError("rpc", "reconciler-live-route-duplicate"); + } + const selected = contract.routeKeys.map((routeKey) => { + const route = byKey.get(routeKey); + if (!route) { + throw validationError("rpc", "reconciler-live-route-missing"); + } + return route; + }); + // A canonicality change while logs or EIP-1898 calls were in flight must + // invalidate the whole source read, not merely the affected entity. + await rpc.assertCheckpoint({ blockNumber, blockHash, signal }); + return Object.freeze(selected); + }, + readIndexedRoutes({ contract, signal }) { + return input.indexedStore.readExactIndexedRouteCorpus({ + contract, + maximumEntityCount: 10_000, + signal, + }); + }, + }); +} diff --git a/lib/data-pipeline/reconciler-preparity.server.ts b/lib/data-pipeline/reconciler-preparity.server.ts new file mode 100644 index 00000000..2f751eaa --- /dev/null +++ b/lib/data-pipeline/reconciler-preparity.server.ts @@ -0,0 +1,150 @@ +import "server-only"; + +import { buildClassicV2ExactBlockContribution } from "./classic-v2-reconciler-route-builder.server"; +import { dataPipelineError, invalidInput } from "./errors"; +import { assembleReconcilerRoutesFromContributions } from "./classic-v3-reconciler-route-contract"; +import { buildClassicV3ExactBlockRoutes } from "./classic-v3-reconciler-route-builder.server"; +import { createPostgresExecutor } from "./postgres"; +import { createPostgresReconcilerPreParityStore } from "./postgres-reconciler-store"; +import { createPostgresReconcilerRouteCorpusStore } from "./postgres-reconciler-route-corpus-store"; +import { + createExactBlockReconcilerRouteDtoReader, + type ExactBlockRouteBuilder, +} from "./reconciler-exact-block-reader.server"; +import { + canonicalReconcilerCheckpointRequest, + runReconcilerPreParityCycle, + type ReconcilerCheckpointRequest, + type ReconcilerCommitResult, + type ReconcilerRouteDtoReader, +} from "./reconciler-preparity"; +import { createProductionDualRpcProviders } from "./rpc-providers.server"; +import { + buildStockPairedV1ExactBlockContribution, + buildStockPairedV2ExactBlockContribution, + buildStockPairedV3ExactBlockContribution, + type StockPairedExactBlockContributionBuilder, +} from "./stock-paired-reconciler-route-builder.server"; + +type Environment = Readonly>; + +function configuredRouteReader( + routeDtoReader: ReconcilerRouteDtoReader | undefined, +): ReconcilerRouteDtoReader { + if (!routeDtoReader) { + // The exact-block route builders must be wired explicitly. Falling back to + // an existing public view would manufacture parity from the indexed side. + throw dataPipelineError({ + dependency: "uniswap", + code: "dependency_unavailable", + retryable: false, + countsTowardCircuit: false, + metadata: { operation: "reconciler-route-reader-unconfigured" }, + }); + } + return routeDtoReader; +} + +function requiredEnvironmentValue( + value: string | undefined, + operation: string, +): string { + if (typeof value !== "string" || value.length === 0) { + throw invalidInput("config", operation); + } + return value; +} + +function stockPairedExactBlockRoutes( + builder: StockPairedExactBlockContributionBuilder, +): ExactBlockRouteBuilder { + return async (input) => + assembleReconcilerRoutesFromContributions([await builder(input)]); +} + +const buildStockPairedV1ExactBlockRoutes = stockPairedExactBlockRoutes( + buildStockPairedV1ExactBlockContribution, +); +const buildStockPairedV2ExactBlockRoutes = stockPairedExactBlockRoutes( + buildStockPairedV2ExactBlockContribution, +); +const buildStockPairedV3ExactBlockRoutes = stockPairedExactBlockRoutes( + buildStockPairedV3ExactBlockContribution, +); +const buildClassicV2ExactBlockRoutes: ExactBlockRouteBuilder = async (input) => + assembleReconcilerRoutesFromContributions([ + await buildClassicV2ExactBlockContribution(input), + ]); + +function configuredExactBlockRouteBuilder( + releaseId: string, + modelId: string, +): ExactBlockRouteBuilder | undefined { + if (modelId === "classic" && releaseId === "classic-v2") { + return buildClassicV2ExactBlockRoutes; + } + if (modelId === "classic" && releaseId === "classic-v3") { + return buildClassicV3ExactBlockRoutes; + } + if (modelId !== "stock-paired") return undefined; + if (releaseId === "stock-paired-v1") { + return buildStockPairedV1ExactBlockRoutes; + } + if (releaseId === "stock-paired-v2") { + return buildStockPairedV2ExactBlockRoutes; + } + if (releaseId === "stock-paired-v3") { + return buildStockPairedV3ExactBlockRoutes; + } + return undefined; +} + +/** + * Runs one exact-checkpoint cycle. Production callers must supply the reviewed + * applicable-route live/indexed DTO implementation; there is deliberately no + * legacy or indexed-view fallback. + */ +export async function runConfiguredReconcilerPreParity(input: { + request: ReconcilerCheckpointRequest; + routeDtoReader?: ReconcilerRouteDtoReader; + exactBlockRouteBuilder?: ExactBlockRouteBuilder; + env?: Environment; +}): Promise { + const request = canonicalReconcilerCheckpointRequest(input.request); + const env = input.env ?? process.env; + const exactBlockRouteBuilder = input.exactBlockRouteBuilder ?? + configuredExactBlockRouteBuilder(request.releaseId, request.modelId); + if (!input.routeDtoReader && !exactBlockRouteBuilder) { + configuredRouteReader(undefined); + } + const executor = createPostgresExecutor({ + connectionString: requiredEnvironmentValue( + env.PROGRAMMABLE_RECONCILER_DATABASE_URL, + "reconciler-database-url", + ), + sslCaPem: requiredEnvironmentValue( + env.PROGRAMMABLE_RECONCILER_DATABASE_SSL_CA, + "reconciler-database-ca", + ), + maxConnections: 1, + connectTimeoutMs: 1_000, + idleTimeoutMs: 5_000, + }); + try { + const routeDtoReader = input.routeDtoReader ?? + createExactBlockReconcilerRouteDtoReader({ + env, + indexedStore: createPostgresReconcilerRouteCorpusStore({ executor }), + buildLiveRoutes: exactBlockRouteBuilder!, + }); + return await runReconcilerPreParityCycle({ + request, + store: createPostgresReconcilerPreParityStore({ executor }), + providers: createProductionDualRpcProviders(env), + routeDtoReader, + deadlineMs: 75_000, + }); + } finally { + await executor.close(); + } +} diff --git a/lib/data-pipeline/reconciler-preparity.ts b/lib/data-pipeline/reconciler-preparity.ts new file mode 100644 index 00000000..03754ab2 --- /dev/null +++ b/lib/data-pipeline/reconciler-preparity.ts @@ -0,0 +1,1018 @@ +import "server-only"; + +import { randomUUID } from "node:crypto"; + +import { keccak256, toBytes } from "viem"; + +import { + canonicalizeFingerprintJson, + type CanonicalJsonValue, +} from "./canonical-fingerprint"; +import { + canonicalBytes32, + parseNonnegativeIntegerText, + type HexBytes32, +} from "./codecs"; +import type { CandidateRpcProvider } from "./dual-rpc"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, + type DataPipelineDependency, +} from "./errors"; +import { assertProductionDualRpcProviders } from "./rpc-providers.server"; + +export const RECONCILER_ROUTE_KEYS = Object.freeze([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "classic-v3-profile", + "launch-lookup", +] as const); + +export type ReconcilerRouteKey = (typeof RECONCILER_ROUTE_KEYS)[number]; + +export const CLASSIC_V2_RECONCILER_ROUTE_KEYS = Object.freeze([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", +] as const satisfies readonly ReconcilerRouteKey[]); + +export const CLASSIC_V3_RECONCILER_ROUTE_KEYS = RECONCILER_ROUTE_KEYS; + +export const STOCK_PAIRED_RECONCILER_ROUTE_KEYS = Object.freeze([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "launch-lookup", +] as const satisfies readonly ReconcilerRouteKey[]); + +export function reconcilerRouteKeysForScope( + releaseId: string, + modelId: string, +): readonly ReconcilerRouteKey[] { + if (releaseId === "classic-v2" && modelId === "classic") { + return CLASSIC_V2_RECONCILER_ROUTE_KEYS; + } + if (releaseId === "classic-v3" && modelId === "classic") { + return CLASSIC_V3_RECONCILER_ROUTE_KEYS; + } + if ( + modelId === "stock-paired" && + (releaseId === "stock-paired-v1" || + releaseId === "stock-paired-v2" || + releaseId === "stock-paired-v3") + ) { + return STOCK_PAIRED_RECONCILER_ROUTE_KEYS; + } + throw invalidInput("config", "reconciler-release-model"); +} + +export type ReconcilerCheckpointRequest = Readonly<{ + chainId: "1"; + releaseId: string; + modelId: string; + sourceGroup: string; + epochId: string; + pointerGeneration: string; + checkpointId: string; + checkpointBlockNumber: string; + checkpointBlockHash: HexBytes32; + maximumEntityCount: number; +}>; + +export type ReconcilerPreParityContract = Readonly<{ + chainId: "1"; + releaseId: string; + modelId: string; + sourceGroup: string; + projectorVersion: string; + epochId: string; + pointerGeneration: string; + checkpointId: string; + checkpointGeneration: string; + reorgGeneration: string; + checkpointBlockNumber: string; + checkpointBlockHash: HexBytes32; + routeKeys: readonly ReconcilerRouteKey[]; + routeContract: CanonicalJsonValue; + projectionContract: CanonicalJsonValue; + currentEntities: CanonicalJsonValue; +}>; + +export type ReconcilerRouteDto = Readonly<{ + routeKey: ReconcilerRouteKey; + comparedCount: number; + dto: CanonicalJsonValue; +}>; + +export type ReconcilerLiveSource = Readonly<{ + identity: string; + vendorGroup: string; + endpointCommitment: HexBytes32; + endpointOriginCommitment: HexBytes32; +}>; + +export type ReconcilerRouteDtoReader = Readonly<{ + readLiveRoutes(input: { + source: ReconcilerLiveSource; + contract: ReconcilerPreParityContract; + blockNumber: bigint; + blockHash: HexBytes32; + signal: AbortSignal; + }): Promise; + readIndexedRoutes(input: { + contract: ReconcilerPreParityContract; + signal: AbortSignal; + }): Promise; +}>; + +export type ReconcilerIndexedRouteStore = Readonly<{ + readExactIndexedRouteCorpus(input: { + contract: ReconcilerPreParityContract; + maximumEntityCount: number; + signal: AbortSignal; + }): Promise; +}>; + +export type ReconcilerCommitInput = Readonly<{ + runId: string; + reconciliationId: string; + parityRecordIds: readonly string[]; + parityBindingIds: readonly string[]; + outcomeId: string; + contract: ReconcilerPreParityContract; + workerVersion: string; + routeKeys: readonly ReconcilerRouteKey[]; + legacyDtoHashes: readonly HexBytes32[]; + indexedDtoHashes: readonly HexBytes32[]; + routeEvidenceCommitments: readonly HexBytes32[]; + parityBindingCommitments: readonly HexBytes32[]; + requestCommitment: HexBytes32; + reconciliationEvidenceCommitment: HexBytes32; + resultCommitment: HexBytes32; + startedAt: string; + comparedAt: string; + finishedAt: string; +}>; + +export type ReconcilerCommitResult = Readonly<{ + runId: string; + reconciliationId: string; + checkpointId: string; + checkpointBlockNumber: string; + checkpointBlockHash: HexBytes32; + routeCount: number; + mismatchCount: number; + status: "succeeded" | "failed"; +}>; + +export type ReconcilerPreParityStore = Readonly<{ + readExactContract( + request: ReconcilerCheckpointRequest, + ): Promise; + commitResult(input: ReconcilerCommitInput): Promise; +}>; + +const IDENTIFIER_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u; +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; +const PROVIDER_IDENTITY_PATTERN = /^[a-z0-9][a-z0-9:-]{0,95}$/u; +const MAXIMUM_ENTITY_COUNT = 10_000; +const MAXIMUM_ROUTE_DTO_BYTES = 512 * 1024; +const MAXIMUM_ALL_ROUTE_DTO_BYTES = 4 * 1024 * 1024; +const MAXIMUM_JSON_NODES = 100_000; +const MAXIMUM_JSON_DEPTH = 64; +const MAXIMUM_COMPARED_COUNT = 1_000_000; +const MAXIMUM_DEADLINE_MS = 85_000; +const MINIMUM_DEADLINE_MS = 100; +const WORKER_VERSION_PATTERN = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u; + +function canonicalIdentifier( + value: unknown, + operation: string, +): string { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 96 || + !IDENTIFIER_PATTERN.test(value) + ) { + throw invalidInput("config", operation); + } + return value; +} + +function canonicalUuid(value: unknown, operation: string): string { + if (typeof value !== "string" || !UUID_PATTERN.test(value)) { + throw invalidInput("config", operation); + } + return value; +} + +function canonicalPositiveIntegerText( + value: unknown, + operation: string, +): string { + let parsed: string; + try { + parsed = parseNonnegativeIntegerText(value, 19); + } catch { + throw invalidInput("config", operation); + } + if (parsed === "0" || BigInt(parsed) > 9_223_372_036_854_775_807n) { + throw invalidInput("config", operation); + } + return parsed; +} + +function canonicalNonnegativeIntegerText( + value: unknown, + operation: string, +): string { + let parsed: string; + try { + parsed = parseNonnegativeIntegerText(value, 19); + } catch { + throw invalidInput("config", operation); + } + if (BigInt(parsed) > 9_223_372_036_854_775_807n) { + throw invalidInput("config", operation); + } + return parsed; +} + +function canonicalBlockNumber(value: unknown): string { + let parsed: string; + try { + parsed = parseNonnegativeIntegerText(value, 19); + } catch { + throw invalidInput("config", "checkpoint-block-number"); + } + if (BigInt(parsed) > 9_223_372_036_854_775_807n) { + throw invalidInput("config", "checkpoint-block-number"); + } + return parsed; +} + +export function canonicalReconcilerCheckpointRequest( + value: unknown, +): ReconcilerCheckpointRequest { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw invalidInput("config", "reconciler-checkpoint-request"); + } + const input = value as Record; + const exactKeys = [ + "chainId", + "releaseId", + "modelId", + "sourceGroup", + "epochId", + "pointerGeneration", + "checkpointId", + "checkpointBlockNumber", + "checkpointBlockHash", + "maximumEntityCount", + ].sort(); + if (Object.keys(input).sort().join("\0") !== exactKeys.join("\0")) { + throw invalidInput("config", "reconciler-checkpoint-request-fields"); + } + if (input.chainId !== "1") { + throw invalidInput("config", "chain-id"); + } + if ( + typeof input.maximumEntityCount !== "number" || + !Number.isSafeInteger(input.maximumEntityCount) || + input.maximumEntityCount < 1 || + input.maximumEntityCount > MAXIMUM_ENTITY_COUNT + ) { + throw invalidInput("config", "maximum-entity-count"); + } + const releaseId = canonicalIdentifier(input.releaseId, "release-id"); + const modelId = canonicalIdentifier(input.modelId, "model-id"); + reconcilerRouteKeysForScope(releaseId, modelId); + return Object.freeze({ + chainId: "1", + releaseId, + modelId, + sourceGroup: canonicalIdentifier(input.sourceGroup, "source-group"), + epochId: canonicalUuid(input.epochId, "epoch-id"), + pointerGeneration: canonicalPositiveIntegerText( + input.pointerGeneration, + "pointer-generation", + ), + checkpointId: canonicalUuid(input.checkpointId, "checkpoint-id"), + checkpointBlockNumber: canonicalBlockNumber( + input.checkpointBlockNumber, + ), + checkpointBlockHash: canonicalBytes32(input.checkpointBlockHash), + maximumEntityCount: input.maximumEntityCount, + }); +} + +function jsonValue( + value: unknown, + dependency: DataPipelineDependency, + operation: string, + state = { nodes: 0 }, + depth = 0, +): CanonicalJsonValue { + state.nodes += 1; + if (state.nodes > MAXIMUM_JSON_NODES || depth > MAXIMUM_JSON_DEPTH) { + throw validationError(dependency, operation); + } + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + if (typeof value === "string" && value.length > MAXIMUM_ROUTE_DTO_BYTES) { + throw validationError(dependency, operation); + } + return value; + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value)) { + throw validationError(dependency, operation); + } + return value; + } + if (Array.isArray(value)) { + if (value.length > MAXIMUM_JSON_NODES) { + throw validationError(dependency, operation); + } + return value.map((entry) => + jsonValue(entry, dependency, operation, state, depth + 1), + ); + } + if (typeof value !== "object") { + throw validationError(dependency, operation); + } + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) { + throw validationError(dependency, operation); + } + const output: Record = Object.create(null); + const entries = Object.entries(value as Record); + if (entries.length > MAXIMUM_JSON_NODES) { + throw validationError(dependency, operation); + } + for (const [key, entry] of entries) { + if (key.length > 256) throw validationError(dependency, operation); + output[key] = jsonValue( + entry, + dependency, + operation, + state, + depth + 1, + ); + } + return output; +} + +function canonicalJsonDocument( + value: unknown, + dependency: DataPipelineDependency, + operation: string, + maximumBytes: number, +): { value: CanonicalJsonValue; encoded: string; bytes: number } { + const canonical = jsonValue(value, dependency, operation); + let encoded: string; + try { + encoded = canonicalizeFingerprintJson(canonical); + } catch { + throw validationError(dependency, operation); + } + const bytes = Buffer.byteLength(encoded, "utf8"); + if (bytes > maximumBytes) { + throw dataPipelineError({ + dependency, + code: "response_oversize", + retryable: false, + countsTowardCircuit: false, + metadata: { operation, limit: maximumBytes }, + }); + } + return { value: canonical, encoded, bytes }; +} + +function routeKeys( + value: unknown, + releaseId: string, + modelId: string, +): readonly ReconcilerRouteKey[] { + const expected = reconcilerRouteKeysForScope(releaseId, modelId); + if ( + !Array.isArray(value) || + value.length !== expected.length || + value.some((routeKey, index) => routeKey !== expected[index]) + ) { + throw validationError("postgres", "reconciler-route-contract"); + } + return expected; +} + +export function canonicalReconcilerPreParityContract( + value: unknown, +): ReconcilerPreParityContract { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw validationError("postgres", "reconciler-contract"); + } + const input = value as Record; + if (input.chainId !== "1") { + throw validationError("postgres", "reconciler-contract-chain"); + } + const routeContract = canonicalJsonDocument( + input.routeContract, + "postgres", + "reconciler-route-contract", + MAXIMUM_ROUTE_DTO_BYTES, + ).value; + const projectionContract = canonicalJsonDocument( + input.projectionContract, + "postgres", + "reconciler-projection-contract", + MAXIMUM_ROUTE_DTO_BYTES, + ).value; + const currentEntities = canonicalJsonDocument( + input.currentEntities, + "postgres", + "reconciler-current-entities", + 2 * 1024 * 1024, + ).value; + const releaseId = canonicalIdentifier(input.releaseId, "release-id"); + const modelId = canonicalIdentifier(input.modelId, "model-id"); + return Object.freeze({ + chainId: "1", + releaseId, + modelId, + sourceGroup: canonicalIdentifier(input.sourceGroup, "source-group"), + projectorVersion: canonicalIdentifier( + input.projectorVersion, + "projector-version", + ), + epochId: canonicalUuid(input.epochId, "epoch-id"), + pointerGeneration: canonicalPositiveIntegerText( + input.pointerGeneration, + "pointer-generation", + ), + checkpointId: canonicalUuid(input.checkpointId, "checkpoint-id"), + checkpointGeneration: canonicalPositiveIntegerText( + input.checkpointGeneration, + "checkpoint-generation", + ), + reorgGeneration: canonicalNonnegativeIntegerText( + input.reorgGeneration, + "reorg-generation", + ), + checkpointBlockNumber: canonicalBlockNumber( + input.checkpointBlockNumber, + ), + checkpointBlockHash: canonicalBytes32(input.checkpointBlockHash), + routeKeys: routeKeys(input.routeKeys, releaseId, modelId), + routeContract, + projectionContract, + currentEntities, + }); +} + +function assertContractMatchesRequest( + contract: ReconcilerPreParityContract, + request: ReconcilerCheckpointRequest, +): void { + if ( + contract.chainId !== request.chainId || + contract.releaseId !== request.releaseId || + contract.modelId !== request.modelId || + contract.sourceGroup !== request.sourceGroup || + contract.epochId !== request.epochId || + contract.pointerGeneration !== request.pointerGeneration || + contract.checkpointId !== request.checkpointId || + contract.checkpointBlockNumber !== request.checkpointBlockNumber || + contract.checkpointBlockHash !== request.checkpointBlockHash + ) { + throw validationError("postgres", "reconciler-contract-scope"); + } +} + +function canonicalProviderPair( + providers: readonly CandidateRpcProvider[], +): readonly [CandidateRpcProvider, CandidateRpcProvider] { + assertProductionDualRpcProviders(providers); + if (providers.length !== 2) { + throw invalidInput("rpc", "reconciler-provider-count"); + } + const pair = providers as readonly [CandidateRpcProvider, CandidateRpcProvider]; + const identities = new Set(); + const vendors = new Set(); + const endpoints = new Set(); + const origins = new Set(); + for (const provider of pair) { + if ( + provider === null || + typeof provider !== "object" || + !PROVIDER_IDENTITY_PATTERN.test(provider.identity) || + !IDENTIFIER_PATTERN.test(provider.vendorGroup) || + typeof provider.client?.getChainId !== "function" || + typeof provider.client?.getBlock !== "function" + ) { + throw invalidInput("rpc", "reconciler-provider"); + } + identities.add(provider.identity); + vendors.add(provider.vendorGroup); + endpoints.add(canonicalBytes32(provider.endpointCommitment)); + origins.add(canonicalBytes32(provider.endpointOriginCommitment)); + } + if ( + identities.size !== 2 || + vendors.size !== 2 || + endpoints.size !== 2 || + origins.size !== 2 + ) { + throw invalidInput("rpc", "reconciler-provider-independence"); + } + return pair; +} + +function liveSource(provider: CandidateRpcProvider): ReconcilerLiveSource { + return Object.freeze({ + identity: provider.identity, + vendorGroup: provider.vendorGroup, + endpointCommitment: canonicalBytes32(provider.endpointCommitment), + endpointOriginCommitment: canonicalBytes32( + provider.endpointOriginCommitment, + ), + }); +} + +function dependencyUnavailable( + dependency: DataPipelineDependency, + operation: string, +): DataPipelineError { + return dataPipelineError({ + dependency, + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + metadata: { operation }, + }); +} + +async function exactProviderCheckpoint(input: { + provider: CandidateRpcProvider; + blockNumber: bigint; + blockHash: HexBytes32; +}): Promise { + try { + const [chainId, block] = await Promise.all([ + input.provider.client.getChainId(), + input.provider.client.getBlock({ blockNumber: input.blockNumber }), + ]); + if ( + chainId !== 1 || + block.number !== input.blockNumber || + block.hash === null || + canonicalBytes32(block.hash) !== input.blockHash || + typeof block.timestamp !== "bigint" || + block.timestamp < 0n + ) { + throw validationError("rpc", "reconciler-exact-checkpoint"); + } + return block.timestamp; + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dependencyUnavailable("rpc", "reconciler-exact-checkpoint"); + } +} + +type CanonicalRouteDto = ReconcilerRouteDto & { + encoded: string; + bytes: number; + hash: HexBytes32; +}; + +function commitment(domain: string, value: CanonicalJsonValue): HexBytes32 { + return keccak256( + toBytes( + `programmable:reconciler:${domain}:v1\0${canonicalizeFingerprintJson(value)}`, + ), + ) as HexBytes32; +} + +function canonicalRouteSet(input: { + value: unknown; + routeKeys: readonly ReconcilerRouteKey[]; + dependency: DataPipelineDependency; + operation: string; +}): readonly CanonicalRouteDto[] { + if (!Array.isArray(input.value) || input.value.length !== input.routeKeys.length) { + throw validationError(input.dependency, input.operation); + } + const byKey = new Map(); + let totalBytes = 0; + for (const raw of input.value) { + if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { + throw validationError(input.dependency, input.operation); + } + const item = raw as Record; + if ( + typeof item.routeKey !== "string" || + !input.routeKeys.includes(item.routeKey as ReconcilerRouteKey) || + typeof item.comparedCount !== "number" || + !Number.isSafeInteger(item.comparedCount) || + item.comparedCount < 1 || + item.comparedCount > MAXIMUM_COMPARED_COUNT || + byKey.has(item.routeKey as ReconcilerRouteKey) + ) { + throw validationError(input.dependency, input.operation); + } + const document = canonicalJsonDocument( + item.dto, + input.dependency, + input.operation, + MAXIMUM_ROUTE_DTO_BYTES, + ); + totalBytes += document.bytes; + if (totalBytes > MAXIMUM_ALL_ROUTE_DTO_BYTES) { + throw dataPipelineError({ + dependency: input.dependency, + code: "response_oversize", + retryable: false, + countsTowardCircuit: false, + metadata: { + operation: input.operation, + limit: MAXIMUM_ALL_ROUTE_DTO_BYTES, + }, + }); + } + const routeKey = item.routeKey as ReconcilerRouteKey; + const comparedCount = item.comparedCount; + const hash = commitment("route-dto", { + routeKey, + comparedCount, + dto: document.value, + }); + byKey.set(routeKey, { + routeKey, + comparedCount, + dto: document.value, + encoded: document.encoded, + bytes: document.bytes, + hash, + }); + } + return input.routeKeys.map((key) => { + const route = byKey.get(key); + if (!route) throw validationError(input.dependency, input.operation); + return route; + }); +} + +function canonicalTimestamp(value: Date, operation: string): string { + if (!(value instanceof Date) || Number.isNaN(value.valueOf())) { + throw invalidInput("config", operation); + } + return value.toISOString(); +} + +function monotonicTimestamps(now: () => Date) { + const startedAt = canonicalTimestamp(now(), "reconciler-started-at"); + return { + startedAt, + comparedAt() { + const value = canonicalTimestamp(now(), "reconciler-compared-at"); + if (value < startedAt) { + throw invalidInput("config", "reconciler-clock"); + } + return value; + }, + finishedAt(comparedAt: string) { + const value = canonicalTimestamp(now(), "reconciler-finished-at"); + if (value < comparedAt) { + throw invalidInput("config", "reconciler-clock"); + } + return value; + }, + }; +} + +function uniqueUuids( + uuidFactory: () => string, + routeCount: number, +): { + runId: string; + reconciliationId: string; + parityRecordIds: readonly string[]; + parityBindingIds: readonly string[]; + outcomeId: string; +} { + const valueCount = 3 + routeCount * 2; + const values = Array.from({ length: valueCount }, () => + canonicalUuid(uuidFactory(), "reconciler-generated-id"), + ); + if (new Set(values).size !== values.length) { + throw invalidInput("config", "reconciler-generated-id-collision"); + } + return { + runId: values[0]!, + reconciliationId: values[1]!, + parityRecordIds: values.slice(2, 2 + routeCount), + parityBindingIds: values.slice(2 + routeCount, 2 + routeCount * 2), + outcomeId: values[valueCount - 1]!, + }; +} + +type Deadline = Readonly<{ + signal: AbortSignal; + assertActive(): void; + assertCommitWindow(): void; +}>; + +async function withDeadline( + deadlineMs: number, + work: (deadline: Deadline) => Promise, +): Promise { + if ( + !Number.isSafeInteger(deadlineMs) || + deadlineMs < MINIMUM_DEADLINE_MS || + deadlineMs > MAXIMUM_DEADLINE_MS + ) { + throw invalidInput("config", "reconciler-deadline"); + } + const controller = new AbortController(); + let expired = false; + const expiresAt = Date.now() + deadlineMs; + const commitReserveMs = Math.min( + 5_000, + Math.max(20, Math.floor(deadlineMs / 5)), + ); + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => { + expired = true; + controller.abort(); + reject( + dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-cycle" }, + }), + ); + }, deadlineMs); + }); + const deadline = Object.freeze({ + signal: controller.signal, + assertActive() { + if (expired || controller.signal.aborted) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-cycle" }, + }); + } + }, + assertCommitWindow() { + if ( + expired || + controller.signal.aborted || + expiresAt - Date.now() < commitReserveMs + ) { + throw dataPipelineError({ + dependency: "rpc", + code: "timeout", + retryable: true, + countsTowardCircuit: true, + metadata: { operation: "reconciler-commit-window" }, + }); + } + }, + }); + try { + return await Promise.race([work(deadline), timeout]); + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} + +export async function runReconcilerPreParityCycle(input: { + request: ReconcilerCheckpointRequest; + store: ReconcilerPreParityStore; + providers: readonly CandidateRpcProvider[]; + routeDtoReader: ReconcilerRouteDtoReader; + workerVersion?: string; + deadlineMs?: number; + now?: () => Date; + uuidFactory?: () => string; +}): Promise { + const request = canonicalReconcilerCheckpointRequest(input.request); + const providers = canonicalProviderPair(input.providers); + const workerVersion = canonicalIdentifier( + input.workerVersion ?? "reconciler-preparity-v1", + "reconciler-worker-version", + ); + if (!WORKER_VERSION_PATTERN.test(workerVersion)) { + throw invalidInput("config", "reconciler-worker-version"); + } + if ( + !input.store || + typeof input.store.readExactContract !== "function" || + typeof input.store.commitResult !== "function" || + !input.routeDtoReader || + typeof input.routeDtoReader.readLiveRoutes !== "function" || + typeof input.routeDtoReader.readIndexedRoutes !== "function" + ) { + throw invalidInput("config", "reconciler-runtime-dependencies"); + } + const now = input.now ?? (() => new Date()); + const timestamps = monotonicTimestamps(now); + + return withDeadline(input.deadlineMs ?? 75_000, async (deadline) => { + const contract = canonicalReconcilerPreParityContract( + await input.store.readExactContract(request), + ); + deadline.assertActive(); + assertContractMatchesRequest(contract, request); + + const blockNumber = BigInt(contract.checkpointBlockNumber); + const [firstTimestamp, secondTimestamp] = await Promise.all( + providers.map((provider) => + exactProviderCheckpoint({ + provider, + blockNumber, + blockHash: contract.checkpointBlockHash, + }), + ), + ); + deadline.assertActive(); + if (firstTimestamp !== secondTimestamp) { + throw validationError("rpc", "reconciler-block-timestamp-consensus"); + } + + let firstLiveRaw: readonly ReconcilerRouteDto[]; + let secondLiveRaw: readonly ReconcilerRouteDto[]; + let indexedRaw: readonly ReconcilerRouteDto[]; + try { + [firstLiveRaw, secondLiveRaw, indexedRaw] = await Promise.all([ + input.routeDtoReader.readLiveRoutes({ + source: liveSource(providers[0]), + contract, + blockNumber, + blockHash: contract.checkpointBlockHash, + signal: deadline.signal, + }), + input.routeDtoReader.readLiveRoutes({ + source: liveSource(providers[1]), + contract, + blockNumber, + blockHash: contract.checkpointBlockHash, + signal: deadline.signal, + }), + input.routeDtoReader.readIndexedRoutes({ + contract, + signal: deadline.signal, + }), + ]); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dependencyUnavailable("uniswap", "reconciler-route-dto-read"); + } + deadline.assertActive(); + + const firstLive = canonicalRouteSet({ + value: firstLiveRaw, + routeKeys: contract.routeKeys, + dependency: "rpc", + operation: "reconciler-live-route-a", + }); + const secondLive = canonicalRouteSet({ + value: secondLiveRaw, + routeKeys: contract.routeKeys, + dependency: "rpc", + operation: "reconciler-live-route-b", + }); + const indexed = canonicalRouteSet({ + value: indexedRaw, + routeKeys: contract.routeKeys, + dependency: "postgres", + operation: "reconciler-indexed-route", + }); + + for (let index = 0; index < contract.routeKeys.length; index += 1) { + if ( + firstLive[index]!.hash !== secondLive[index]!.hash || + firstLive[index]!.comparedCount !== secondLive[index]!.comparedCount || + firstLive[index]!.comparedCount !== indexed[index]!.comparedCount + ) { + throw validationError("rpc", "reconciler-provider-route-consensus"); + } + } + + const routeEvidenceCommitments = contract.routeKeys.map( + (routeKey, index) => + commitment("route-evidence", { + routeKey, + chainId: contract.chainId, + checkpointId: contract.checkpointId, + checkpointBlockNumber: contract.checkpointBlockNumber, + checkpointBlockHash: contract.checkpointBlockHash, + comparedCount: firstLive[index]!.comparedCount, + liveDtoHash: firstLive[index]!.hash, + indexedDtoHash: indexed[index]!.hash, + providers: providers.map((provider) => liveSource(provider)), + }), + ); + const parityBindingCommitments = contract.routeKeys.map( + (routeKey, index) => + commitment("parity-binding", { + routeKey, + checkpointId: contract.checkpointId, + checkpointGeneration: contract.checkpointGeneration, + reorgGeneration: contract.reorgGeneration, + checkpointBlockNumber: contract.checkpointBlockNumber, + checkpointBlockHash: contract.checkpointBlockHash, + routeEvidenceCommitment: routeEvidenceCommitments[index]!, + }), + ); + const requestCommitment = commitment("request", { + request, + projectorVersion: contract.projectorVersion, + checkpointGeneration: contract.checkpointGeneration, + reorgGeneration: contract.reorgGeneration, + routeContract: contract.routeContract, + projectionContract: contract.projectionContract, + currentEntities: contract.currentEntities, + }); + const reconciliationEvidenceCommitment = commitment( + "reconciliation-evidence", + { + requestCommitment, + checkpointId: contract.checkpointId, + checkpointBlockNumber: contract.checkpointBlockNumber, + checkpointBlockHash: contract.checkpointBlockHash, + providers: providers.map((provider) => liveSource(provider)), + routeEvidenceCommitments, + }, + ); + const mismatchRoutes = contract.routeKeys.filter( + (_, index) => firstLive[index]!.hash !== indexed[index]!.hash, + ); + const resultCommitment = commitment("result", { + requestCommitment, + reconciliationEvidenceCommitment, + routeKeys: [...contract.routeKeys], + legacyDtoHashes: firstLive.map((route) => route.hash), + indexedDtoHashes: indexed.map((route) => route.hash), + routeEvidenceCommitments, + parityBindingCommitments, + mismatchRoutes, + status: mismatchRoutes.length === 0 ? "succeeded" : "failed", + }); + const comparedAt = timestamps.comparedAt(); + const finishedAt = timestamps.finishedAt(comparedAt); + const ids = uniqueUuids( + input.uuidFactory ?? randomUUID, + contract.routeKeys.length, + ); + + deadline.assertCommitWindow(); + const result = await input.store.commitResult({ + ...ids, + contract, + workerVersion, + routeKeys: contract.routeKeys, + legacyDtoHashes: firstLive.map((route) => route.hash), + indexedDtoHashes: indexed.map((route) => route.hash), + routeEvidenceCommitments, + parityBindingCommitments, + requestCommitment, + reconciliationEvidenceCommitment, + resultCommitment, + startedAt: timestamps.startedAt, + comparedAt, + finishedAt, + }); + deadline.assertActive(); + + if ( + result.runId !== ids.runId || + result.reconciliationId !== ids.reconciliationId || + result.checkpointId !== contract.checkpointId || + result.checkpointBlockNumber !== contract.checkpointBlockNumber || + result.checkpointBlockHash !== contract.checkpointBlockHash || + result.routeCount !== contract.routeKeys.length || + result.mismatchCount !== mismatchRoutes.length || + result.status !== (mismatchRoutes.length === 0 ? "succeeded" : "failed") + ) { + throw validationError("postgres", "reconciler-commit-result"); + } + return Object.freeze(result); + }); +} diff --git a/lib/data-pipeline/release-binding.server.ts b/lib/data-pipeline/release-binding.server.ts new file mode 100644 index 00000000..60aa9253 --- /dev/null +++ b/lib/data-pipeline/release-binding.server.ts @@ -0,0 +1,340 @@ +import "server-only"; + +import releaseBindingJson from "../../config/data-pipeline-release.v1.json"; + +type HexAddress = `0x${string}`; +type HexHash = `0x${string}`; + +const ZERO_SOURCE_COMMIT = "0".repeat(40); +const ZERO_SHA256 = `0x${"00".repeat(32)}`; + +const EXPECTED_MODEL_BY_RELEASE = Object.freeze({ + "classic-v2": "classic", + "classic-v3": "classic", + "stock-paired-v1": "stock-paired", + "stock-paired-v2": "stock-paired", + "stock-paired-v3": "stock-paired", +} as const); + +export type DataPipelineSourceBinding = { + contractName: string; + address: HexAddress; + startBlock: number; + runtimeCodeHash: HexHash; +}; + +export type DataPipelineModelRelease = { + model: string; + releaseVersion: string; + activationBlock: number; + sourceContracts: string[]; + dynamicContracts: string[]; +}; + +export type DataPipelineReleaseBinding = { + schemaVersion: 1; + chainId: 1; + startBlock: number; + confirmations: 12; + envio: { + deploymentLabel: string; + graphqlEndpoint: string; + schemaVersion: "1"; + sourceCommit: string; + configSha256: HexHash; + schemaSha256: HexHash; + handlerSha256: HexHash; + sourceRegistrySha256: HexHash; + eventSetSha256: HexHash; + eventCount: number; + }; + uniswapV4Subgraph: { + subgraphId: string; + deployment: string; + }; + sources: DataPipelineSourceBinding[]; + releases: DataPipelineModelRelease[]; +}; + +function invalidBinding(): never { + throw new Error("Invalid data pipeline release binding"); +} + +function isRecord(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function hasOnlyKeys(value: Record, keys: readonly string[]) { + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + return ( + actual.length === expected.length && + actual.every((key, index) => key === expected[index]) + ); +} + +function boundedString(value: unknown, pattern: RegExp, maximum: number) { + if ( + typeof value !== "string" || + value.length === 0 || + value.length > maximum || + !pattern.test(value) + ) { + return invalidBinding(); + } + return value; +} + +function positiveSafeInteger(value: unknown) { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) { + return invalidBinding(); + } + return value; +} + +function sha256(value: unknown): HexHash { + const commitment = boundedString( + value, + /^0x[0-9a-f]{64}$/, + 66, + ) as HexHash; + if (commitment === ZERO_SHA256) return invalidBinding(); + return commitment; +} + +function sourceCommit(value: unknown) { + const commitment = boundedString(value, /^[0-9a-f]{40}$/, 40); + if (commitment === ZERO_SOURCE_COMMIT) return invalidBinding(); + return commitment; +} + +function address(value: unknown): HexAddress { + return boundedString(value, /^0x[0-9a-f]{40}$/, 42) as HexAddress; +} + +function stringList(value: unknown) { + if (!Array.isArray(value) || value.length > 32) return invalidBinding(); + const output = value.map((item) => + boundedString(item, /^[A-Za-z][A-Za-z0-9]*$/, 96), + ); + if (new Set(output).size !== output.length) return invalidBinding(); + return output; +} + +export function parseDataPipelineReleaseBinding( + value: unknown, +): DataPipelineReleaseBinding { + if ( + !isRecord(value) || + !hasOnlyKeys(value, [ + "schemaVersion", + "chainId", + "startBlock", + "confirmations", + "envio", + "uniswapV4Subgraph", + "sources", + "releases", + ]) || + value.schemaVersion !== 1 || + value.chainId !== 1 || + value.confirmations !== 12 || + !isRecord(value.envio) || + !hasOnlyKeys(value.envio, [ + "deploymentLabel", + "graphqlEndpoint", + "schemaVersion", + "sourceCommit", + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "eventCount", + ]) || + value.envio.schemaVersion !== "1" || + !isRecord(value.uniswapV4Subgraph) || + !hasOnlyKeys(value.uniswapV4Subgraph, ["subgraphId", "deployment"]) || + !Array.isArray(value.sources) || + value.sources.length === 0 || + value.sources.length > 128 || + !Array.isArray(value.releases) || + value.releases.length === 0 || + value.releases.length > 64 + ) { + return invalidBinding(); + } + + const startBlock = positiveSafeInteger(value.startBlock); + const sources = value.sources.map((source): DataPipelineSourceBinding => { + if ( + !isRecord(source) || + !hasOnlyKeys(source, [ + "contractName", + "address", + "startBlock", + "runtimeCodeHash", + ]) + ) { + return invalidBinding(); + } + const sourceStartBlock = positiveSafeInteger(source.startBlock); + if (sourceStartBlock < startBlock) return invalidBinding(); + return { + contractName: boundedString( + source.contractName, + /^[A-Za-z][A-Za-z0-9]*$/, + 96, + ), + address: address(source.address), + startBlock: sourceStartBlock, + runtimeCodeHash: sha256(source.runtimeCodeHash), + }; + }); + if ( + new Set(sources.map(({ contractName }) => contractName)).size !== + sources.length || + new Set(sources.map(({ address: sourceAddress }) => sourceAddress)).size !== + sources.length + ) { + return invalidBinding(); + } + const sourceNames = new Set(sources.map(({ contractName }) => contractName)); + + const releases = value.releases.map((release): DataPipelineModelRelease => { + if ( + !isRecord(release) || + !hasOnlyKeys(release, [ + "model", + "releaseVersion", + "activationBlock", + "sourceContracts", + "dynamicContracts", + ]) + ) { + return invalidBinding(); + } + const sourceContracts = stringList(release.sourceContracts); + if ( + sourceContracts.length === 0 || + sourceContracts.some((contractName) => !sourceNames.has(contractName)) + ) { + return invalidBinding(); + } + const model = boundedString( + release.model, + /^[a-z][a-z0-9-]*$/, + 64, + ); + const releaseVersion = boundedString( + release.releaseVersion, + /^[a-z][a-z0-9-]*$/, + 64, + ); + if ( + !(releaseVersion in EXPECTED_MODEL_BY_RELEASE) || + EXPECTED_MODEL_BY_RELEASE[ + releaseVersion as keyof typeof EXPECTED_MODEL_BY_RELEASE + ] !== model + ) { + return invalidBinding(); + } + const activationBlock = positiveSafeInteger(release.activationBlock); + const maximumSourceStart = sourceContracts.reduce((maximum, name) => { + const source = sources.find((candidate) => candidate.contractName === name); + return source && source.startBlock > maximum + ? source.startBlock + : maximum; + }, 0); + if (activationBlock < maximumSourceStart) return invalidBinding(); + return { + model, + releaseVersion, + activationBlock, + sourceContracts, + dynamicContracts: stringList(release.dynamicContracts), + }; + }); + if ( + new Set(releases.map(({ releaseVersion }) => releaseVersion)).size !== + releases.length + ) { + return invalidBinding(); + } + + const sourceModels = new Map>(); + const dynamicModels = new Map>(); + for (const release of releases) { + for (const contractName of release.sourceContracts) { + const models = sourceModels.get(contractName) ?? new Set(); + models.add(release.model); + sourceModels.set(contractName, models); + } + for (const contractName of release.dynamicContracts) { + if (sourceNames.has(contractName)) return invalidBinding(); + const models = dynamicModels.get(contractName) ?? new Set(); + models.add(release.model); + dynamicModels.set(contractName, models); + } + } + if ( + sourceModels.size !== sourceNames.size || + [...sourceModels.values()].some((models) => models.size !== 1) || + [...dynamicModels.values()].some((models) => models.size !== 1) + ) { + return invalidBinding(); + } + + return { + schemaVersion: 1, + chainId: 1, + startBlock, + confirmations: 12, + envio: { + deploymentLabel: boundedString( + value.envio.deploymentLabel, + /^[a-z0-9][a-z0-9-]*$/, + 128, + ), + graphqlEndpoint: boundedString( + value.envio.graphqlEndpoint, + /^https:\/\/indexer\.hyperindex\.xyz\/[a-z0-9]{7,64}\/v1\/graphql$/, + 256, + ), + schemaVersion: "1", + sourceCommit: sourceCommit(value.envio.sourceCommit), + configSha256: sha256(value.envio.configSha256), + schemaSha256: sha256(value.envio.schemaSha256), + handlerSha256: sha256(value.envio.handlerSha256), + sourceRegistrySha256: sha256(value.envio.sourceRegistrySha256), + eventSetSha256: sha256(value.envio.eventSetSha256), + eventCount: positiveSafeInteger(value.envio.eventCount), + }, + uniswapV4Subgraph: { + subgraphId: boundedString( + value.uniswapV4Subgraph.subgraphId, + /^[1-9A-HJ-NP-Za-km-z]+$/, + 96, + ), + deployment: boundedString( + value.uniswapV4Subgraph.deployment, + /^[1-9A-HJ-NP-Za-km-z]+$/, + 96, + ), + }, + sources, + releases, + }; +} + +let cachedBinding: DataPipelineReleaseBinding | undefined; + +export function getDataPipelineReleaseBinding() { + cachedBinding ??= parseDataPipelineReleaseBinding(releaseBindingJson); + return cachedBinding; +} diff --git a/lib/data-pipeline/release-probe-nonce.server.ts b/lib/data-pipeline/release-probe-nonce.server.ts new file mode 100644 index 00000000..1f946138 --- /dev/null +++ b/lib/data-pipeline/release-probe-nonce.server.ts @@ -0,0 +1,190 @@ +import "server-only"; + +import { createHash } from "node:crypto"; + +import { CircuitBreaker } from "./circuit"; +import { loadDataPipelineConfig } from "./config"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, +} from "./errors"; +import { + createPostgresExecutor, + type PostgresExecutor, +} from "./postgres"; +import { validatedPostgresConnectionTarget } from "./postgres-connection.server"; + +const RELEASE_PROBE_ROLE = "programmable_release_probe_nonce"; +const RELEASE_PROBE_LOGIN = "programmable_release_probe_nonce_login"; +const RELEASE_PROBE_ROUTES = new Set([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "classic-v3-profile", + "launch-lookup", +]); +const RELEASE_PROBE_NONCE_CONSUMER = Symbol.for( + "programmable.data-pipeline.release-probe-nonce-consumer.v1", +); + +type ConsumerRegistry = { + [RELEASE_PROBE_NONCE_CONSUMER]?: Promise; +}; + +export type ReleaseProbeNonceInput = Readonly<{ + route: string; + nonce: string; + issuedAt: Date; + expiresAt: Date; +}>; + +export type ReleaseProbeNonceConsumer = Readonly<{ + consume(input: ReleaseProbeNonceInput): Promise; + close(): Promise; +}>; + +function registry(): ConsumerRegistry { + return globalThis as typeof globalThis & ConsumerRegistry; +} + +function validDate(value: Date): boolean { + return value instanceof Date && Number.isFinite(value.valueOf()); +} + +function validateInput(input: ReleaseProbeNonceInput): void { + if ( + !RELEASE_PROBE_ROUTES.has(input.route) || + typeof input.nonce !== "string" || + input.nonce.length < 1 || + input.nonce.length > 256 || + !validDate(input.issuedAt) || + !validDate(input.expiresAt) || + input.expiresAt.valueOf() <= input.issuedAt.valueOf() + ) { + throw invalidInput("postgres", "release-probe-nonce"); + } +} + +export function createReleaseProbeNonceConsumer(input: { + executor: PostgresExecutor; +}): ReleaseProbeNonceConsumer { + const circuit = new CircuitBreaker({ dependency: "postgres" }); + return Object.freeze({ + async consume(candidate: ReleaseProbeNonceInput): Promise { + validateInput(candidate); + const digest = createHash("sha256") + .update(candidate.nonce, "utf8") + .digest(); + + return circuit.execute(async () => { + try { + return await input.executor.transaction(async (transaction) => { + await transaction.query(`set local role ${RELEASE_PROBE_ROLE}`); + await transaction.query("set local statement_timeout = '1000ms'"); + await transaction.query("set local lock_timeout = '250ms'"); + await transaction.query( + "set local idle_in_transaction_session_timeout = '2000ms'", + ); + const identities = await transaction.query<{ + session_user: unknown; + active_role: unknown; + }>( + "select session_user::text as session_user, current_setting('role', true) as active_role", + ); + if ( + identities.length !== 1 || + identities[0]?.session_user !== RELEASE_PROBE_LOGIN || + identities[0]?.active_role !== RELEASE_PROBE_ROLE + ) { + throw validationError("postgres", "release-probe-role"); + } + + const rows = await transaction.query<{ consumed: unknown }>( + "select programmable_release_probe_private.consume_release_probe_nonce_v1($1::text, $2::bytea, $3::timestamptz, $4::timestamptz) as consumed", + [candidate.route, digest, candidate.issuedAt, candidate.expiresAt], + ); + if ( + rows.length !== 1 || + typeof rows[0]?.consumed !== "boolean" + ) { + throw validationError("postgres", "release-probe-consume"); + } + return rows[0].consumed; + }); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency: "postgres", + code: "query_failed", + retryable: true, + countsTowardCircuit: true, + }); + } + }); + }, + close: () => input.executor.close(), + }); +} + +function constructReleaseProbeNonceConsumer(): ReleaseProbeNonceConsumer { + const config = loadDataPipelineConfig(); + const connectionString = config.postgres.releaseProbeConnectionString; + if (!connectionString) { + throw dataPipelineError({ + dependency: "config", + code: "invalid_config", + retryable: false, + countsTowardCircuit: false, + }); + } + const target = validatedPostgresConnectionTarget(connectionString); + if (!target.isLoopback && !config.postgres.sslCaPem) { + throw dataPipelineError({ + dependency: "config", + code: "invalid_config", + retryable: false, + countsTowardCircuit: false, + }); + } + return createReleaseProbeNonceConsumer({ + executor: createPostgresExecutor({ + connectionString, + sslCaPem: config.postgres.sslCaPem, + maxConnections: 1, + connectTimeoutMs: config.postgres.connectTimeoutMs, + idleTimeoutMs: config.postgres.idleTimeoutMs, + allowInsecureLoopback: target.isLoopback, + }), + }); +} + +function getReleaseProbeNonceConsumer(): Promise { + const state = registry(); + const existing = state[RELEASE_PROBE_NONCE_CONSUMER]; + if (existing) return existing; + const created = Promise.resolve().then(constructReleaseProbeNonceConsumer); + state[RELEASE_PROBE_NONCE_CONSUMER] = created; + return created; +} + +export async function consumeReleaseProbeNonce( + input: ReleaseProbeNonceInput, +): Promise { + return (await getReleaseProbeNonceConsumer()).consume(input); +} + +/** Test isolation only. Production lifecycle is process-owned. */ +export async function resetReleaseProbeNonceConsumerForTests(): Promise { + if (process.env.NODE_ENV !== "test") { + throw invalidInput("config", "release-probe-consumer-reset"); + } + const state = registry(); + const existing = state[RELEASE_PROBE_NONCE_CONSUMER]; + delete state[RELEASE_PROBE_NONCE_CONSUMER]; + if (!existing) return; + const consumer = await existing.catch(() => null); + if (consumer) await consumer.close(); +} diff --git a/lib/data-pipeline/request.ts b/lib/data-pipeline/request.ts new file mode 100644 index 00000000..b9e6e05e --- /dev/null +++ b/lib/data-pipeline/request.ts @@ -0,0 +1,174 @@ +import "server-only"; + +import { + DataPipelineError, + dataPipelineError, + type DataPipelineDependency, +} from "./errors"; + +export type DataPipelineFetcher = ( + input: string, + init?: RequestInit, +) => Promise; + +async function readBoundedBody( + response: Response, + maximumBodyBytes: number, + dependency: DataPipelineDependency, +) { + const declared = response.headers.get("content-length"); + if ( + declared !== null && + (!/^(0|[1-9]\d*)$/.test(declared) || + declared.length > 12 || + BigInt(declared) > BigInt(maximumBodyBytes)) + ) { + throw dataPipelineError({ + dependency, + code: "response_oversize", + retryable: true, + countsTowardCircuit: true, + }); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw dataPipelineError({ + dependency, + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } + const decoder = new TextDecoder("utf-8", { fatal: true }); + const chunks: string[] = []; + let bytesRead = 0; + let completed = false; + try { + while (true) { + const result = await reader.read(); + if (result.done) break; + bytesRead += result.value.byteLength; + if (bytesRead > maximumBodyBytes) { + throw dataPipelineError({ + dependency, + code: "response_oversize", + retryable: true, + countsTowardCircuit: true, + }); + } + chunks.push(decoder.decode(result.value, { stream: true })); + } + chunks.push(decoder.decode()); + completed = true; + return chunks.join(""); + } catch (error) { + if (error instanceof DataPipelineError) throw error; + throw dataPipelineError({ + dependency, + code: "invalid_json", + retryable: true, + countsTowardCircuit: true, + }); + } finally { + if (!completed) await reader.cancel().catch(() => undefined); + reader.releaseLock(); + } +} + +export async function boundedJsonRequest(input: { + dependency: DataPipelineDependency; + endpoint: string; + timeoutMs: number; + maximumBodyBytes: number; + body: unknown; + headers?: Readonly>; + fetcher?: DataPipelineFetcher; +}): Promise { + const controller = new AbortController(); + let timeoutHandle: ReturnType | undefined; + const timeoutError = dataPipelineError({ + dependency: input.dependency, + code: "timeout", + retryable: true, + countsTowardCircuit: true, + }); + const timeout = new Promise((_resolve, reject) => { + timeoutHandle = setTimeout(() => { + controller.abort(); + reject(timeoutError); + }, input.timeoutMs); + }); + + const request = (async () => { + let response: Response; + try { + response = await (input.fetcher ?? fetch)(input.endpoint, { + method: "POST", + headers: { + "content-type": "application/json", + ...input.headers, + }, + body: JSON.stringify(input.body), + signal: controller.signal, + cache: "no-store", + }); + } catch { + if (controller.signal.aborted) throw timeoutError; + throw dataPipelineError({ + dependency: input.dependency, + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }); + } + if (!response.ok) { + throw dataPipelineError({ + dependency: input.dependency, + code: "dependency_unavailable", + retryable: response.status >= 500 || response.status === 429, + countsTowardCircuit: true, + metadata: { status: response.status }, + }); + } + + const raw = await readBoundedBody( + response, + input.maximumBodyBytes, + input.dependency, + ); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw dataPipelineError({ + dependency: input.dependency, + code: "invalid_json", + retryable: true, + countsTowardCircuit: true, + }); + } + if ( + typeof parsed === "object" && + parsed !== null && + !Array.isArray(parsed) && + "errors" in parsed && + Array.isArray((parsed as { errors?: unknown }).errors) && + (parsed as { errors: unknown[] }).errors.length > 0 + ) { + throw dataPipelineError({ + dependency: input.dependency, + code: "graphql_error", + retryable: true, + countsTowardCircuit: true, + }); + } + return parsed as T; + })(); + + try { + return await Promise.race([request, timeout]); + } finally { + if (timeoutHandle !== undefined) clearTimeout(timeoutHandle); + } +} diff --git a/lib/data-pipeline/route-activation.server.ts b/lib/data-pipeline/route-activation.server.ts new file mode 100644 index 00000000..6e05b233 --- /dev/null +++ b/lib/data-pipeline/route-activation.server.ts @@ -0,0 +1,25 @@ +import "server-only"; + +import { loadDataPipelineConfig } from "./config"; + +type ActivationEnvironment = Readonly< + Record +>; + +/** + * Action preparation is deliberately independent from every public-read flag. + * This is the only activation check used before action routes query Postgres. + */ +export function indexedLaunchLookupEnabled( + env: ActivationEnvironment = process.env, +): boolean { + return loadDataPipelineConfig(env).flags.INDEXED_LAUNCH_LOOKUP_ENABLED; +} + +/** Public GMGN/token-list feeds have their own release switch. */ +export function indexedPublicIndexerFeedEnabled( + env: ActivationEnvironment = process.env, +): boolean { + return loadDataPipelineConfig(env).flags + .INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED; +} diff --git a/lib/data-pipeline/route-adapters.server.ts b/lib/data-pipeline/route-adapters.server.ts new file mode 100644 index 00000000..0d7f1c51 --- /dev/null +++ b/lib/data-pipeline/route-adapters.server.ts @@ -0,0 +1,2334 @@ +import "server-only"; + +import { + formatUnits, + getAddress, + isAddress, + type Address, + type Hex, +} from "viem"; + +import type { TokenChartRange } from "../onchain/chart"; +import type { + CreatorClaim, + CreatorProfile, + ExplorePage, + ExploreSnapshot, + ExploreSort, +} from "../onchain/types"; +import type { ClassicV3ProfileRewards } from "../profile/classic-v3-rewards"; +import type { LauncherToken, TokenLink, TokenLinkKind } from "../tokens"; + +export const INDEXED_ROUTE_ADAPTER_VERSION = + "indexed-route-adapters-v2" as const; + +export type IndexedPublicRouteSurface = + | "explore-list" + | "token-detail" + | "token-chart" + | "creator-profile" + | "classic-v3-profile" + | "stock-paired-profile" + | "launch-lookup"; + +const CACHE_HEADERS = Object.freeze({ + explore: Object.freeze({ + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }), + token: Object.freeze({ + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }), + chart: Object.freeze({ + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }), + profile: Object.freeze({ + "Cache-Control": "private, max-age=0, s-maxage=15", + }), + noStore: Object.freeze({ "Cache-Control": "no-store" }), +}); + +export function indexedRouteCacheHeaders( + surface: IndexedPublicRouteSurface, + outcome: "ready" | "not-found" | "not-ready" | "error" = "ready", +): Readonly> { + if (outcome !== "ready") return CACHE_HEADERS.noStore; + if (surface === "explore-list") return CACHE_HEADERS.explore; + if (surface === "token-detail") return CACHE_HEADERS.token; + if (surface === "token-chart") return CACHE_HEADERS.chart; + if (surface === "creator-profile") return CACHE_HEADERS.profile; + return CACHE_HEADERS.noStore; +} + +export type IndexedRouteAdapterErrorCode = + | "invalid-input" + | "unsupported-release" + | "not-ready" + | "scope-mismatch" + | "snapshot-mismatch" + | "cursor-mismatch" + | "precision-loss" + | "projection-incomplete"; + +const ERROR_MESSAGES: Record = { + "invalid-input": "Indexed route input is invalid", + "unsupported-release": "Indexed launch release is unsupported", + "not-ready": "Indexed route data is not ready", + "scope-mismatch": "Indexed route scope does not match", + "snapshot-mismatch": "Indexed route snapshot does not match", + "cursor-mismatch": "Indexed route cursor does not match", + "precision-loss": "Indexed route value cannot be represented safely", + "projection-incomplete": "Indexed route projection is incomplete", +}; + +export class IndexedRouteAdapterError extends Error { + readonly code: IndexedRouteAdapterErrorCode; + readonly operation: string; + readonly retryable: boolean; + + constructor( + code: IndexedRouteAdapterErrorCode, + operation: string, + ) { + super(ERROR_MESSAGES[code]); + this.name = "IndexedRouteAdapterError"; + this.code = code; + this.operation = operation; + this.retryable = ![ + "invalid-input", + "unsupported-release", + "precision-loss", + ].includes(code); + } + + toJSON() { + return { + name: this.name, + code: this.code, + operation: this.operation, + retryable: this.retryable, + }; + } +} + +function fail( + code: IndexedRouteAdapterErrorCode, + operation: string, +): never { + throw new IndexedRouteAdapterError(code, operation); +} + +export type SupportedIndexedReleaseVersionV2 = + | "classic-v2" + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + +export type SupportedIndexedModelVersionV2 = "classic" | "stock-paired"; + +export type SupportedIndexedReleaseV2 = + | { + releaseVersion: "classic-v2"; + modelVersion: "classic"; + launchModel: "classic"; + } + | { + releaseVersion: "classic-v3"; + modelVersion: "classic"; + launchModel: "classic"; + launchModelVersion: "classic-v3"; + } + | { + releaseVersion: + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + modelVersion: "stock-paired"; + launchModel: "stock-paired"; + launchModelVersion: + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + }; + +const RELEASE_MODEL = Object.freeze({ + "classic-v2": "classic", + "classic-v3": "classic", + "stock-paired-v1": "stock-paired", + "stock-paired-v2": "stock-paired", + "stock-paired-v3": "stock-paired", +} satisfies Record< + SupportedIndexedReleaseVersionV2, + SupportedIndexedModelVersionV2 +>); + +const ALL_SUPPORTED_RELEASES = Object.freeze( + Object.keys(RELEASE_MODEL) as SupportedIndexedReleaseVersionV2[], +); + +export function assertSupportedIndexedReleaseV2(input: { + releaseVersion: string; + modelVersion: string; +}): SupportedIndexedReleaseV2 { + const releaseVersion = input.releaseVersion as SupportedIndexedReleaseVersionV2; + const expectedModel = RELEASE_MODEL[releaseVersion]; + if (!expectedModel || expectedModel !== input.modelVersion) { + fail("unsupported-release", "release-model"); + } + if (releaseVersion === "classic-v2") { + return { + releaseVersion, + modelVersion: "classic", + launchModel: "classic", + }; + } + if (releaseVersion === "classic-v3") { + return { + releaseVersion, + modelVersion: "classic", + launchModel: "classic", + launchModelVersion: "classic-v3", + }; + } + return { + releaseVersion, + modelVersion: "stock-paired", + launchModel: "stock-paired", + launchModelVersion: releaseVersion, + }; +} + +export type IndexedRouteKeyV2 = + | "explore-list" + | "explore-token" + | "explore-chart" + | "creator-profile" + | "classic-v3-profile" + | "launch-lookup"; + +const INDEXED_ROUTE_KEYS = new Set([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "classic-v3-profile", + "launch-lookup", +]); + +export type IndexedReleasePointerV2 = { + routeKey: IndexedRouteKeyV2; + chainId: 1 | 11_155_111; + releaseVersion: SupportedIndexedReleaseVersionV2; + modelVersion: SupportedIndexedModelVersionV2; + sourceGroup: string; + projectorVersion: string; + epochId: string; + pointerGeneration: string; + checkpointId: string; + checkpointGeneration: string; + reorgGeneration: string; + checkpointBlockNumber: string; + checkpointBlockHash: `0x${string}`; +}; + +export type IndexedSnapshotIdentityV2 = { + adapterVersion: typeof INDEXED_ROUTE_ADAPTER_VERSION; + snapshotCommitment: `0x${string}`; + chainId: 1 | 11_155_111; + blockNumber: string; + blockHash: `0x${string}`; + confirmations: number; + capturedAt: string; + releasePointers: readonly IndexedReleasePointerV2[]; + ethUsdQuote?: { + feedAddress: `0x${string}`; + roundId: string; + answer: string; + decimals: number; + updatedAt: string; + }; +}; + +export type IndexedRowSourceV2 = IndexedReleasePointerV2 & { + snapshotCommitment: `0x${string}`; + projectionRunId: string; + publicationCommitment: `0x${string}`; + promotedBlockNumber: string; + promotedBlockHash: `0x${string}`; +}; + +export type IndexedNotReadyReasonV2 = + | "route-disabled" + | "release-unverified" + | "snapshot-unavailable" + | "projection-lag" + | "reconciliation-incomplete"; + +export type IndexedRouteEnvelopeV2 = + | { + status: "ready"; + snapshot: IndexedSnapshotIdentityV2; + data: T; + } + | { + status: "not-ready"; + reason: IndexedNotReadyReasonV2; + }; + +export type IndexedProjectMetadataV2 = { + revision: string; + createdAt: string; + description: string | null; + imageUrl: string | null; + links: readonly { + kind: TokenLinkKind; + url: string; + displayOrder: number; + }[]; + extraData: `0x${string}`; +}; + +export type IndexedTokenProjectionV2 = { + source: IndexedRowSourceV2; + tokenAddress: `0x${string}`; + hookAddress: `0x${string}`; + poolId: `0x${string}`; + creatorAddress: `0x${string}`; + positionRecipient: `0x${string}` | null; + positionTokenId: string | null; + rewardVaultAddress: `0x${string}` | null; + launchHash: `0x${string}`; + launchBlockNumber: string; + launchTransactionHash: `0x${string}`; + launchTransactionIndex: number; + launchLogIndex: number; + launchedAt: string; + name: string; + symbol: string; + decimals: number; + totalSupplyRaw: string; + metadata: IndexedProjectMetadataV2 | null; + liquidity: { + tokenLiquidityAmountRaw: string | null; + lockedTokenDustRaw: string | null; + currentTick: number | null; + initialTick: number | null; + tickLower: number | null; + tickUpper: number | null; + activeLiquidity: string | null; + }; + fees: { + totalSwapFeeBps: number; + buySwapFeeBps: number; + sellSwapFeeBps: number; + buyCreatorFeeBps: number; + sellCreatorFeeBps: number; + launcherFeeBps: number; + transferTaxBps: number; + lpFeePips: number; + protocolFeePips: number; + }; + market: { + tokenPriceNativeWei: string | null; + marketCapNativeWei: string | null; + indexedMarketCapNativeWei: string | null; + indexedMarketCapUsdWad: string | null; + indexedValuationBlockNumber: string | null; + fdvUsdWad: string | null; + grossVolumeNativeWei: string | null; + creatorFeesGeneratedNativeWei: string | null; + launcherFeesGeneratedNativeWei: string | null; + creatorFeesAccruedNativeWei: string | null; + swapCount: number | null; + }; + quote: { + address: `0x${string}`; + symbol: string; + name: string; + decimals: number; + isCurrency0: boolean; + tokenPriceQuoteWad: string; + marketCapQuoteWad: string; + grossVolumeQuoteRaw: string; + creatorFeesGeneratedQuoteRaw: string; + programmableFeesGeneratedQuoteRaw: string; + creatorFeesAccruedQuoteRaw: string; + } | null; + initialBuy: { + nativeWei: string; + quoteRaw: string | null; + tokenRaw: string; + } | null; + uniswapV4Pool: { + source: "official-uniswap-v4-subgraph"; + indexedBlockNumber: string; + indexedBlockHash: `0x${string}`; + volumeUsdWad: string; + tvlUsdWad: string; + transactionCount: string; + liquidity: string; + sqrtPriceX96: string; + tick?: number; + feeTierPips: string; + } | null; +}; + +export type IndexedExploreCursorV2 = { + adapterVersion: typeof INDEXED_ROUTE_ADAPTER_VERSION; + snapshotCommitment: `0x${string}`; + normalizedQuery: string; + sort: ExploreSort; + pageSize: number; + valuationUnit: "usd-wad" | "native-wei" | null; + position: { + marketCapAtomic: string | null; + launchBlockNumber: string; + launchTransactionIndex: number; + launchLogIndex: number; + launchTransactionHash: `0x${string}`; + tokenAddress: `0x${string}`; + }; +}; + +export type IndexedExploreListDataV2 = { + request: { + query: string; + sort: ExploreSort; + requestedPage: number; + pageSize: number; + }; + page: { + resolvedPage: number; + totalCount: string; + valuationUnit: "usd-wad" | "native-wei" | null; + startAfter: IndexedExploreCursorV2 | null; + endAt: IndexedExploreCursorV2 | null; + }; + launcherFeesAccruedWei: string; + tokens: readonly IndexedTokenProjectionV2[]; +}; + +export type IndexedCreatorClaimV2 = { + source: IndexedRowSourceV2; + poolId: `0x${string}`; + tokenAddress: `0x${string}`; + creatorAddress: `0x${string}`; + recipientAddress: `0x${string}`; + callerAddress: `0x${string}`; + amountWei: string; + blockNumber: string; + transactionHash: `0x${string}`; + transactionIndex: number; + logIndex: number; + claimedAt: string; +}; + +export type IndexedClassicV3RewardProjectionV2 = { + source: IndexedRowSourceV2; + tokenAddress: `0x${string}`; + tokenName: string; + tokenSymbol: string; + poolId: `0x${string}`; + vaultAddress: `0x${string}`; + claimableWei: string; + claimedWei: string; + buySwapFeeBps: number; + sellSwapFeeBps: number; + platformFeeBps: number; + allocations: readonly { + allocationIndex: number; + beneficiary: `0x${string}`; + payoutAddress: `0x${string}`; + shareBps: number; + }[]; + launchTransactionHash: `0x${string}`; +}; + +export type IndexedStockPairedRewardProjectionV2 = { + source: IndexedRowSourceV2; + tokenAddress: `0x${string}`; + tokenName: string; + tokenSymbol: string; + imageUrl: string | null; + hookAddress: `0x${string}`; + poolId: `0x${string}`; + vaultAddress: `0x${string}`; + quoteAsset: `0x${string}`; + quoteAssetSymbol: string; + beneficiary: `0x${string}`; + payoutAddress: `0x${string}`; + shareBps: number; + claimableRaw: string; + claimedRaw: string; + generatedRaw: string; + creatorFeesPendingRaw: string; + beneficiaries: readonly { + beneficiary: `0x${string}`; + payoutAddress: `0x${string}`; + shareBps: number; + }[]; + buySwapFeeBps: number; + sellSwapFeeBps: number; + programmableFeeBps: number; + launchTransactionHash: `0x${string}`; + estimate: { + ethRaw: string; + usdRaw: string; + } | null; +}; + +export type IndexedTokenDetailDataV2 = { + address: string; + token: IndexedTokenProjectionV2 | null; +}; + +export type IndexedCreatorProfileDataV2 = { + account: string; + tokens: readonly IndexedTokenProjectionV2[]; + claims: readonly IndexedCreatorClaimV2[]; +}; + +export type IndexedClassicV3ProfileDataV2 = { + account: string; + chainId: 1 | 11_155_111; + rewards: readonly IndexedClassicV3RewardProjectionV2[]; +}; + +export type IndexedStockPairedProfileDataV2 = { + account: string; + chainId: 1; + rewards: readonly IndexedStockPairedRewardProjectionV2[]; +}; + +export type IndexedChartDataV2 = { + address: string; + range: TokenChartRange; + source: IndexedRowSourceV2; + poolId: string; + points: readonly { + blockNumber: string; + priceNativeWei: string; + priceUsdWad: string | null; + }[]; + swapCount: string; + volumeNativeWei: string; + volumeUsdWad: string | null; +}; + +export type IndexedLaunchLookupDataV2 = + | { + surface: "classic-v3"; + account: string; + transactionHash: string; + resolution: "found" | "not-found"; + token: IndexedTokenProjectionV2 | null; + } + | { + surface: "stock-paired"; + account: string; + transactionHash: string; + resolution: "found" | "pending"; + token: IndexedTokenProjectionV2 | null; + }; + +const UINT256_MAX = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; +const UUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +function integerText( + value: unknown, + operation: string, + maximumDigits = 78, +) { + if ( + typeof value !== "string" || + !/^(?:0|[1-9]\d*)$/.test(value) || + value.length > maximumDigits + ) { + fail("invalid-input", operation); + } + return value; +} + +function positiveIntegerText(value: unknown, operation: string) { + const parsed = integerText(value, operation); + if (parsed === "0") fail("invalid-input", operation); + return parsed; +} + +function uint256Text(value: unknown, operation: string) { + const parsed = integerText(value, operation, 79); + if (BigInt(parsed) > UINT256_MAX) fail("invalid-input", operation); + return BigInt(parsed).toString(); +} + +function nullableUint256Text(value: unknown, operation: string) { + return value === null ? null : uint256Text(value, operation); +} + +function safeCount(value: unknown, operation: string) { + const parsed = BigInt(integerText(value, operation)); + if (parsed > BigInt(Number.MAX_SAFE_INTEGER)) { + fail("precision-loss", operation); + } + return Number(parsed); +} + +function boundedInteger( + value: unknown, + minimum: number, + maximum: number, + operation: string, +) { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < minimum || + value > maximum + ) { + fail("invalid-input", operation); + } + return value; +} + +function bps(value: unknown, operation: string) { + return boundedInteger(value, 0, 10_000, operation); +} + +function canonicalAddress(value: unknown, operation: string): Address { + if (typeof value !== "string" || !isAddress(value)) { + fail("invalid-input", operation); + } + return getAddress(value); +} + +function canonicalBytes32(value: unknown, operation: string): Hex { + if (typeof value !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value)) { + fail("invalid-input", operation); + } + return value.toLowerCase() as Hex; +} + +function canonicalData(value: unknown, operation: string): Hex { + if ( + typeof value !== "string" || + !/^0x(?:[0-9a-fA-F]{2})*$/.test(value) || + value.length > 4_098 + ) { + fail("invalid-input", operation); + } + return value.toLowerCase() as Hex; +} + +function canonicalTimestamp(value: unknown, operation: string) { + if (typeof value !== "string") fail("invalid-input", operation); + const parsed = new Date(value); + if (Number.isNaN(parsed.valueOf()) || parsed.toISOString() !== value) { + fail("invalid-input", operation); + } + return value; +} + +function descriptiveText( + value: unknown, + maximumBytes: number, + operation: string, +) { + if ( + typeof value !== "string" || + value.trim().length === 0 || + Buffer.byteLength(value, "utf8") > maximumBytes || + /[\u0000-\u001f\u007f]/u.test(value) + ) { + fail("invalid-input", operation); + } + return value; +} + +function identifier(value: unknown, operation: string) { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 96 || + !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(value) + ) { + fail("invalid-input", operation); + } + return value; +} + +function projectorIdentifier(value: unknown, operation: string) { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 128 || + !/^[A-Za-z0-9][A-Za-z0-9._+:/-]*$/u.test(value) + ) { + fail("invalid-input", operation); + } + return value; +} + +function httpsUrl(value: unknown, operation: string) { + if (typeof value !== "string" || value.length > 512) { + fail("projection-incomplete", operation); + } + let parsed: URL; + try { + parsed = new URL(value); + } catch { + fail("projection-incomplete", operation); + } + if ( + parsed.protocol !== "https:" || + parsed.hostname.length === 0 || + parsed.username.length > 0 || + parsed.password.length > 0 + ) { + fail("projection-incomplete", operation); + } + return parsed; +} + +function metadataLinks( + links: IndexedProjectMetadataV2["links"], +): TokenLink[] { + if (!Array.isArray(links) || links.length > 3) { + fail("projection-incomplete", "metadata-links"); + } + const seenKinds = new Set(); + let previousOrder = -1; + return links.map((link) => { + if ( + !link || + !["website", "x", "telegram"].includes(link.kind) || + seenKinds.has(link.kind) || + !Number.isSafeInteger(link.displayOrder) || + link.displayOrder < 0 || + link.displayOrder <= previousOrder + ) { + fail("projection-incomplete", "metadata-link-order"); + } + const parsed = httpsUrl(link.url, "metadata-link-url"); + const hostname = parsed.hostname.toLowerCase(); + if ( + (link.kind === "x" && + !["x.com", "www.x.com", "twitter.com", "www.twitter.com"].includes( + hostname, + )) || + (link.kind === "telegram" && + !["t.me", "www.t.me", "telegram.me", "www.telegram.me"].includes( + hostname, + )) || + (link.kind !== "website" && parsed.pathname === "/") + ) { + fail("projection-incomplete", "metadata-link-origin"); + } + seenKinds.add(link.kind); + previousOrder = link.displayOrder; + return { kind: link.kind, url: parsed.toString() }; + }); +} + +function formatAtomic(value: string, decimals: number, operation: string) { + const atomic = uint256Text(value, operation); + boundedInteger(decimals, 0, 255, `${operation}-decimals`); + return formatUnits(BigInt(atomic), decimals); +} + +function validateSnapshot(snapshot: IndexedSnapshotIdentityV2) { + if (snapshot.adapterVersion !== INDEXED_ROUTE_ADAPTER_VERSION) { + fail("snapshot-mismatch", "adapter-version"); + } + const snapshotCommitment = canonicalBytes32( + snapshot.snapshotCommitment, + "snapshot-commitment", + ); + if (snapshot.chainId !== 1 && snapshot.chainId !== 11_155_111) { + fail("snapshot-mismatch", "snapshot-chain"); + } + const blockNumber = integerText(snapshot.blockNumber, "snapshot-block"); + const blockHash = canonicalBytes32(snapshot.blockHash, "snapshot-hash"); + const confirmations = boundedInteger( + snapshot.confirmations, + 0, + 1_024, + "snapshot-confirmations", + ); + canonicalTimestamp(snapshot.capturedAt, "snapshot-captured-at"); + if ( + !Array.isArray(snapshot.releasePointers) || + snapshot.releasePointers.length < 1 || + snapshot.releasePointers.length > 32 + ) { + fail("snapshot-mismatch", "snapshot-release-pointers"); + } + const pointerKeys = new Set(); + for (const pointer of snapshot.releasePointers) { + assertSupportedIndexedReleaseV2(pointer); + if ( + !INDEXED_ROUTE_KEYS.has(pointer.routeKey) || + pointer.chainId !== snapshot.chainId || + !UUID.test(pointer.epochId) || + !UUID.test(pointer.checkpointId) + ) { + fail("snapshot-mismatch", "snapshot-release-pointer"); + } + identifier(pointer.sourceGroup, "pointer-source-group"); + projectorIdentifier(pointer.projectorVersion, "pointer-projector-version"); + positiveIntegerText(pointer.pointerGeneration, "pointer-generation"); + positiveIntegerText( + pointer.checkpointGeneration, + "checkpoint-generation", + ); + integerText(pointer.reorgGeneration, "reorg-generation"); + const checkpointBlockNumber = integerText( + pointer.checkpointBlockNumber, + "checkpoint-block-number", + ); + const checkpointBlockHash = canonicalBytes32( + pointer.checkpointBlockHash, + "checkpoint-block-hash", + ); + if ( + checkpointBlockNumber !== blockNumber || + checkpointBlockHash !== blockHash + ) { + fail("snapshot-mismatch", "checkpoint-snapshot-boundary"); + } + const key = `${pointer.routeKey}:${pointer.releaseVersion}:${pointer.modelVersion}`; + if (pointerKeys.has(key)) { + fail("snapshot-mismatch", "snapshot-release-duplicate"); + } + pointerKeys.add(key); + } + + let ethUsdQuote: ExploreSnapshot["ethUsdQuote"]; + if (snapshot.ethUsdQuote) { + ethUsdQuote = { + feedAddress: canonicalAddress( + snapshot.ethUsdQuote.feedAddress, + "eth-usd-feed", + ), + roundId: positiveIntegerText(snapshot.ethUsdQuote.roundId, "eth-usd-round"), + answer: positiveIntegerText(snapshot.ethUsdQuote.answer, "eth-usd-answer"), + decimals: boundedInteger( + snapshot.ethUsdQuote.decimals, + 0, + 36, + "eth-usd-decimals", + ), + updatedAt: canonicalTimestamp( + snapshot.ethUsdQuote.updatedAt, + "eth-usd-updated-at", + ), + }; + } + + return { + snapshotCommitment, + publicSnapshot: { + chainId: snapshot.chainId, + blockNumber, + blockHash, + confirmations, + ...(ethUsdQuote ? { ethUsdQuote } : {}), + } satisfies ExploreSnapshot, + }; +} + +function assertSnapshotScope( + snapshot: IndexedSnapshotIdentityV2, + routeKey: IndexedRouteKeyV2, + releases: readonly SupportedIndexedReleaseVersionV2[], +) { + validateSnapshot(snapshot); + const expected = [...releases].sort(); + const actual = snapshot.releasePointers + .filter((pointer) => pointer.routeKey === routeKey) + .map((pointer) => pointer.releaseVersion) + .sort(); + if ( + actual.length !== expected.length || + actual.some((release, index) => release !== expected[index]) || + snapshot.releasePointers.some((pointer) => pointer.routeKey !== routeKey) + ) { + fail("snapshot-mismatch", "snapshot-route-release-scope"); + } +} + +function requireReady( + envelope: IndexedRouteEnvelopeV2, +): Extract, { status: "ready" }> { + if (envelope.status !== "ready") { + fail("not-ready", envelope.reason); + } + validateSnapshot(envelope.snapshot); + return envelope; +} + +function validateRowSource( + source: IndexedRowSourceV2, + snapshot: IndexedSnapshotIdentityV2, + routeKey: IndexedRouteKeyV2, +) { + const release = assertSupportedIndexedReleaseV2(source); + if (source.routeKey !== routeKey || source.chainId !== snapshot.chainId) { + fail("scope-mismatch", "row-route-scope"); + } + if ( + canonicalBytes32(source.snapshotCommitment, "row-snapshot") !== + canonicalBytes32(snapshot.snapshotCommitment, "snapshot") || + !UUID.test(source.epochId) + ) { + fail("snapshot-mismatch", "row-snapshot-scope"); + } + const pointer = snapshot.releasePointers.find( + (candidate) => + candidate.routeKey === source.routeKey && + candidate.releaseVersion === source.releaseVersion && + candidate.modelVersion === source.modelVersion && + candidate.sourceGroup === source.sourceGroup, + ); + if ( + !pointer || + pointer.chainId !== source.chainId || + pointer.epochId !== source.epochId || + pointer.projectorVersion !== source.projectorVersion || + pointer.pointerGeneration !== source.pointerGeneration || + pointer.checkpointId !== source.checkpointId || + pointer.checkpointGeneration !== source.checkpointGeneration || + pointer.reorgGeneration !== source.reorgGeneration || + pointer.checkpointBlockNumber !== source.checkpointBlockNumber || + pointer.checkpointBlockHash.toLowerCase() !== + source.checkpointBlockHash.toLowerCase() + ) { + fail("snapshot-mismatch", "row-release-pointer"); + } + positiveIntegerText(source.pointerGeneration, "row-pointer-generation"); + projectorIdentifier(source.projectorVersion, "row-projector-version"); + integerText(source.reorgGeneration, "row-reorg-generation"); + if (!UUID.test(source.projectionRunId)) { + fail("snapshot-mismatch", "row-projection-run"); + } + canonicalBytes32(source.publicationCommitment, "publication-commitment"); + const promotedBlock = BigInt( + integerText(source.promotedBlockNumber, "promoted-block"), + ); + const snapshotBlock = BigInt( + integerText(snapshot.blockNumber, "snapshot-block"), + ); + if (promotedBlock > snapshotBlock) { + fail("snapshot-mismatch", "future-publication"); + } + const promotedHash = canonicalBytes32( + source.promotedBlockHash, + "promoted-block-hash", + ); + if ( + promotedBlock === snapshotBlock && + promotedHash !== canonicalBytes32(snapshot.blockHash, "snapshot-hash") + ) { + fail("snapshot-mismatch", "publication-block-hash"); + } + return release; +} + +function nullableIntegerField( + value: number | null, + minimum: number, + maximum: number, + operation: string, +) { + return value === null + ? undefined + : boundedInteger(value, minimum, maximum, operation); +} + +function nullableAtomicFields( + value: string | null, + operation: string, +) { + if (value === null) return undefined; + const raw = uint256Text(value, operation); + return { raw, formatted: formatUnits(BigInt(raw), 18) }; +} + +function validateUniswapPool( + pool: IndexedTokenProjectionV2["uniswapV4Pool"], + snapshot: IndexedSnapshotIdentityV2, +): LauncherToken["uniswapV4Pool"] | undefined { + if (pool === null) return undefined; + if (pool.source !== "official-uniswap-v4-subgraph") { + fail("projection-incomplete", "uniswap-source"); + } + const indexedBlockNumber = integerText( + pool.indexedBlockNumber, + "uniswap-block", + ); + if (BigInt(indexedBlockNumber) > BigInt(snapshot.blockNumber)) { + fail("snapshot-mismatch", "uniswap-future-block"); + } + const indexedBlockHash = canonicalBytes32( + pool.indexedBlockHash, + "uniswap-block-hash", + ); + if ( + indexedBlockNumber === snapshot.blockNumber && + indexedBlockHash !== canonicalBytes32(snapshot.blockHash, "snapshot-hash") + ) { + fail("snapshot-mismatch", "uniswap-snapshot-hash"); + } + return { + source: pool.source, + indexedBlockNumber, + indexedBlockHash, + volumeUsdWad: uint256Text(pool.volumeUsdWad, "uniswap-volume-usd"), + tvlUsdWad: uint256Text(pool.tvlUsdWad, "uniswap-tvl-usd"), + transactionCount: integerText( + pool.transactionCount, + "uniswap-transaction-count", + ), + liquidity: uint256Text(pool.liquidity, "uniswap-liquidity"), + sqrtPriceX96: uint256Text(pool.sqrtPriceX96, "uniswap-sqrt-price"), + ...(pool.tick === undefined + ? {} + : { + tick: boundedInteger( + pool.tick, + -0x8000_0000, + 0x7fff_ffff, + "uniswap-tick", + ), + }), + feeTierPips: integerText(pool.feeTierPips, "uniswap-fee-tier"), + }; +} + +function adaptToken( + projection: IndexedTokenProjectionV2, + snapshot: IndexedSnapshotIdentityV2, + routeKey: IndexedRouteKeyV2, +): LauncherToken { + const release = validateRowSource(projection.source, snapshot, routeKey); + const tokenAddress = canonicalAddress(projection.tokenAddress, "token-address"); + const hookAddress = canonicalAddress(projection.hookAddress, "hook-address"); + const poolId = canonicalBytes32(projection.poolId, "pool-id"); + const creatorAddress = canonicalAddress( + projection.creatorAddress, + "creator-address", + ); + const positionRecipient = + projection.positionRecipient === null + ? undefined + : canonicalAddress(projection.positionRecipient, "position-recipient"); + const positionTokenId = + projection.positionTokenId === null + ? undefined + : uint256Text(projection.positionTokenId, "position-token-id"); + const rewardVaultAddress = + projection.rewardVaultAddress === null + ? undefined + : canonicalAddress(projection.rewardVaultAddress, "reward-vault"); + if ( + (release.releaseVersion === "classic-v2" && rewardVaultAddress) || + (release.releaseVersion !== "classic-v2" && !rewardVaultAddress) + ) { + fail("projection-incomplete", "release-reward-vault"); + } + + const launchHash = canonicalBytes32(projection.launchHash, "launch-hash"); + const launchBlockNumber = integerText( + projection.launchBlockNumber, + "launch-block", + ); + if ( + BigInt(launchBlockNumber) > BigInt(projection.source.promotedBlockNumber) + ) { + fail("snapshot-mismatch", "launch-publication-block"); + } + const launchTransactionHash = canonicalBytes32( + projection.launchTransactionHash, + "launch-transaction", + ); + const launchTransactionIndex = boundedInteger( + projection.launchTransactionIndex, + 0, + 0xffff_ffff, + "launch-transaction-index", + ); + const launchLogIndex = boundedInteger( + projection.launchLogIndex, + 0, + 0xffff_ffff, + "launch-log-index", + ); + const launchedAt = canonicalTimestamp(projection.launchedAt, "launched-at"); + const name = descriptiveText(projection.name, 128, "token-name"); + const symbol = descriptiveText(projection.symbol, 32, "token-symbol"); + const decimals = boundedInteger(projection.decimals, 0, 255, "token-decimals"); + const totalSupplyRaw = uint256Text(projection.totalSupplyRaw, "total-supply"); + + const fees = projection.fees; + const totalSwapFeeBps = bps(fees.totalSwapFeeBps, "total-swap-fee"); + const buySwapFeeBps = bps(fees.buySwapFeeBps, "buy-swap-fee"); + const sellSwapFeeBps = bps(fees.sellSwapFeeBps, "sell-swap-fee"); + const buyCreatorFeeBps = bps(fees.buyCreatorFeeBps, "buy-creator-fee"); + const sellCreatorFeeBps = bps(fees.sellCreatorFeeBps, "sell-creator-fee"); + const launcherFeeBps = bps(fees.launcherFeeBps, "launcher-fee"); + const transferTaxBps = bps(fees.transferTaxBps, "transfer-tax"); + const lpFeePips = boundedInteger(fees.lpFeePips, 0, 1_000_000, "lp-fee-pips"); + const protocolFeePips = boundedInteger( + fees.protocolFeePips, + 0, + 1_000_000, + "protocol-fee-pips", + ); + if ( + totalSwapFeeBps !== Math.max(buySwapFeeBps, sellSwapFeeBps) || + buyCreatorFeeBps + launcherFeeBps !== buySwapFeeBps || + sellCreatorFeeBps + launcherFeeBps !== sellSwapFeeBps || + launcherFeeBps !== 10 || + transferTaxBps !== 0 || + lpFeePips !== 0 + ) { + fail("projection-incomplete", "fee-disclosure"); + } + if ( + release.releaseVersion === "classic-v2" && + (buySwapFeeBps !== sellSwapFeeBps || + buyCreatorFeeBps !== sellCreatorFeeBps || + totalSwapFeeBps < 100 || + totalSwapFeeBps > 1_000 || + totalSwapFeeBps % 100 !== 0) + ) { + fail("projection-incomplete", "classic-v2-fees"); + } + if ( + release.launchModel === "stock-paired" && + (totalSwapFeeBps !== 100 || + buySwapFeeBps !== 100 || + sellSwapFeeBps !== 100 || + buyCreatorFeeBps !== 90 || + sellCreatorFeeBps !== 90) + ) { + fail("projection-incomplete", "stock-paired-fees"); + } + + const metadata = projection.metadata; + let description: string | undefined; + let imageUrl: string | undefined; + let links: TokenLink[] = []; + let metadataExtraData: Hex | undefined; + if (metadata) { + positiveIntegerText(metadata.revision, "metadata-revision"); + canonicalTimestamp(metadata.createdAt, "metadata-created-at"); + if (metadata.description !== null) { + const trimmed = metadata.description.trim(); + if (trimmed.length > 0) { + description = descriptiveText(trimmed, 2_000, "metadata-description"); + } + } + if (metadata.imageUrl !== null) { + imageUrl = httpsUrl(metadata.imageUrl, "metadata-image").toString(); + } + links = metadataLinks(metadata.links); + metadataExtraData = canonicalData(metadata.extraData, "metadata-extra-data"); + } + + const tokenLiquidityAmountRaw = nullableUint256Text( + projection.liquidity.tokenLiquidityAmountRaw, + "token-liquidity", + ); + const lockedTokenDustRaw = nullableUint256Text( + projection.liquidity.lockedTokenDustRaw, + "locked-token-dust", + ); + const activeLiquidity = nullableUint256Text( + projection.liquidity.activeLiquidity, + "active-liquidity", + ); + const currentTick = nullableIntegerField( + projection.liquidity.currentTick, + -0x8000_0000, + 0x7fff_ffff, + "current-tick", + ); + const initialTick = nullableIntegerField( + projection.liquidity.initialTick, + -0x8000_0000, + 0x7fff_ffff, + "initial-tick", + ); + const tickLower = nullableIntegerField( + projection.liquidity.tickLower, + -0x8000_0000, + 0x7fff_ffff, + "tick-lower", + ); + const tickUpper = nullableIntegerField( + projection.liquidity.tickUpper, + -0x8000_0000, + 0x7fff_ffff, + "tick-upper", + ); + if ( + (tickLower === undefined) !== (tickUpper === undefined) || + (tickLower !== undefined && tickUpper !== undefined && tickLower >= tickUpper) + ) { + fail("projection-incomplete", "tick-range"); + } + + const market = projection.market; + const tokenPrice = nullableAtomicFields( + market.tokenPriceNativeWei, + "token-price-native", + ); + const marketCap = nullableAtomicFields( + market.marketCapNativeWei, + "market-cap-native", + ); + const indexedMarketCap = nullableAtomicFields( + market.indexedMarketCapNativeWei, + "indexed-market-cap-native", + ); + const indexedMarketCapUsdWad = nullableUint256Text( + market.indexedMarketCapUsdWad, + "indexed-market-cap-usd", + ); + const indexedValuationBlockNumber = + market.indexedValuationBlockNumber === null + ? undefined + : integerText( + market.indexedValuationBlockNumber, + "indexed-valuation-block", + ); + if ( + indexedValuationBlockNumber !== undefined && + BigInt(indexedValuationBlockNumber) > BigInt(snapshot.blockNumber) + ) { + fail("snapshot-mismatch", "indexed-valuation-future-block"); + } + const fdvUsdWad = nullableUint256Text(market.fdvUsdWad, "fdv-usd"); + const grossVolume = nullableAtomicFields( + market.grossVolumeNativeWei, + "gross-volume-native", + ); + const creatorFeesGenerated = nullableAtomicFields( + market.creatorFeesGeneratedNativeWei, + "creator-fees-generated", + ); + const launcherFeesGenerated = nullableAtomicFields( + market.launcherFeesGeneratedNativeWei, + "launcher-fees-generated", + ); + const creatorFeesAccrued = nullableAtomicFields( + market.creatorFeesAccruedNativeWei, + "creator-fees-accrued", + ); + const swapCount = + market.swapCount === null + ? undefined + : boundedInteger( + market.swapCount, + 0, + Number.MAX_SAFE_INTEGER, + "swap-count", + ); + + let quoteFields: Partial = {}; + if (release.launchModel === "stock-paired") { + if (!projection.quote) fail("projection-incomplete", "stock-quote"); + const quote = projection.quote; + const quoteDecimals = boundedInteger( + quote.decimals, + 0, + 255, + "quote-decimals", + ); + const tokenPriceQuoteWad = uint256Text( + quote.tokenPriceQuoteWad, + "token-price-quote", + ); + const marketCapQuoteWad = uint256Text( + quote.marketCapQuoteWad, + "market-cap-quote", + ); + const grossVolumeQuoteRaw = uint256Text( + quote.grossVolumeQuoteRaw, + "gross-volume-quote", + ); + const creatorFeesGeneratedQuoteRaw = uint256Text( + quote.creatorFeesGeneratedQuoteRaw, + "creator-fees-generated-quote", + ); + const programmableFeesGeneratedQuoteRaw = uint256Text( + quote.programmableFeesGeneratedQuoteRaw, + "programmable-fees-generated-quote", + ); + const creatorFeesAccruedQuoteRaw = uint256Text( + quote.creatorFeesAccruedQuoteRaw, + "creator-fees-accrued-quote", + ); + quoteFields = { + quoteAssetAddress: canonicalAddress(quote.address, "quote-address"), + quoteAssetSymbol: descriptiveText(quote.symbol, 32, "quote-symbol"), + quoteAssetName: descriptiveText(quote.name, 128, "quote-name"), + quoteIsCurrency0: Boolean(quote.isCurrency0), + tokenPriceQuote: formatUnits(BigInt(tokenPriceQuoteWad), 18), + tokenPriceQuoteWad, + marketCapQuote: formatUnits(BigInt(marketCapQuoteWad), 18), + marketCapQuoteWad, + grossVolumeQuote: formatUnits(BigInt(grossVolumeQuoteRaw), quoteDecimals), + grossVolumeQuoteRaw, + creatorFeesGeneratedQuote: formatUnits( + BigInt(creatorFeesGeneratedQuoteRaw), + quoteDecimals, + ), + creatorFeesGeneratedQuoteRaw, + programmableFeesGeneratedQuote: formatUnits( + BigInt(programmableFeesGeneratedQuoteRaw), + quoteDecimals, + ), + programmableFeesGeneratedQuoteRaw, + creatorFeesAccruedQuote: formatUnits( + BigInt(creatorFeesAccruedQuoteRaw), + quoteDecimals, + ), + creatorFeesAccruedQuoteRaw, + }; + } else if (projection.quote !== null) { + fail("projection-incomplete", "classic-quote"); + } + + if (projection.initialBuy) { + uint256Text(projection.initialBuy.nativeWei, "initial-buy-native"); + uint256Text(projection.initialBuy.tokenRaw, "initial-buy-token"); + if (projection.initialBuy.quoteRaw !== null) { + uint256Text(projection.initialBuy.quoteRaw, "initial-buy-quote"); + } + } + + const uniswapV4Pool = validateUniswapPool( + projection.uniswapV4Pool, + snapshot, + ); + const creatorFeeBps = + buyCreatorFeeBps === sellCreatorFeeBps + ? buyCreatorFeeBps + : undefined; + + return { + id: `${snapshot.chainId}:${tokenAddress.toLowerCase()}`, + name, + symbol, + ...(description ? { description } : {}), + ...(imageUrl ? { imageUrl } : {}), + links, + tokenAddress, + hookAddress, + poolId, + creatorAddress, + ...(positionRecipient ? { positionRecipient } : {}), + ...(positionTokenId ? { positionTokenId } : {}), + ...(rewardVaultAddress ? { rewardVaultAddress } : {}), + launchHash, + launchBlockNumber, + launchTransactionHash, + launchTransactionIndex, + launchLogIndex, + launchedAt, + totalSupply: formatAtomic(totalSupplyRaw, decimals, "total-supply"), + totalSupplyRaw, + tokenDecimals: decimals, + ...(tokenLiquidityAmountRaw ? { tokenLiquidityAmountRaw } : {}), + ...(lockedTokenDustRaw ? { lockedTokenDustRaw } : {}), + ...(tokenPrice + ? { + tokenPriceEth: tokenPrice.formatted, + tokenPriceEthWei: tokenPrice.raw, + } + : {}), + ...(marketCap + ? { + marketCapEth: marketCap.formatted, + marketCapEthWei: marketCap.raw, + } + : {}), + ...(indexedMarketCap + ? { + indexedMarketCapEth: indexedMarketCap.formatted, + indexedMarketCapEthWei: indexedMarketCap.raw, + } + : {}), + ...(indexedMarketCapUsdWad ? { indexedMarketCapUsdWad } : {}), + ...(indexedValuationBlockNumber ? { indexedValuationBlockNumber } : {}), + ...(fdvUsdWad ? { fdvUsdWad } : {}), + ...(grossVolume + ? { + grossVolumeEth: grossVolume.formatted, + grossVolumeWei: grossVolume.raw, + } + : {}), + ...(creatorFeesGenerated + ? { + creatorFeesGeneratedEth: creatorFeesGenerated.formatted, + creatorFeesGeneratedWei: creatorFeesGenerated.raw, + } + : {}), + ...(launcherFeesGenerated + ? { + launcherFeesGeneratedEth: launcherFeesGenerated.formatted, + launcherFeesGeneratedWei: launcherFeesGenerated.raw, + } + : {}), + ...(creatorFeesAccrued + ? { + creatorFeesAccruedEth: creatorFeesAccrued.formatted, + creatorFeesAccruedWei: creatorFeesAccrued.raw, + } + : {}), + ...quoteFields, + ...(swapCount === undefined ? {} : { swapCount }), + ...(currentTick === undefined ? {} : { currentTick }), + ...(initialTick === undefined ? {} : { initialTick }), + ...(tickLower === undefined ? {} : { tickLower }), + ...(tickUpper === undefined ? {} : { tickUpper }), + ...(activeLiquidity ? { activeLiquidity } : {}), + protocolFeePips, + lpFeePips, + buyHookFeeBps: buySwapFeeBps, + sellHookFeeBps: sellSwapFeeBps, + ...(creatorFeeBps === undefined ? {} : { creatorFeeBps }), + buyCreatorFeeBps, + sellCreatorFeeBps, + ...(release.releaseVersion === "classic-v2" + ? {} + : { programmableFeeBps: launcherFeeBps }), + launcherFeeBps, + transferTaxBps, + totalSwapFeeBps, + launchModel: release.launchModel, + ...("launchModelVersion" in release && release.launchModelVersion + ? { launchModelVersion: release.launchModelVersion } + : {}), + ...(uniswapV4Pool ? { uniswapV4Pool } : {}), + liquidityPath: "meme", + ...(metadataExtraData ? { metadataExtraData } : {}), + } satisfies LauncherToken; +} + +function normalizedExploreQuery(value: string) { + return value.trim().toLowerCase().replace(/^\$/, ""); +} + +function validExploreSort(value: string): value is ExploreSort { + return ["newest", "oldest", "market-cap", "market-cap-asc"].includes( + value, + ); +} + +function marketCapFor( + projection: IndexedTokenProjectionV2, + unit: IndexedExploreCursorV2["valuationUnit"], +) { + if (unit === "usd-wad") { + return projection.market.indexedMarketCapUsdWad ?? projection.market.fdvUsdWad; + } + if (unit === "native-wei") { + return ( + projection.market.indexedMarketCapNativeWei ?? + projection.market.marketCapNativeWei + ); + } + return null; +} + +function cursorPosition( + projection: IndexedTokenProjectionV2, + valuationUnit: IndexedExploreCursorV2["valuationUnit"], +) { + return { + marketCapAtomic: nullableUint256Text( + marketCapFor(projection, valuationUnit), + "cursor-market-cap", + ), + launchBlockNumber: integerText( + projection.launchBlockNumber, + "cursor-launch-block", + ), + launchTransactionIndex: boundedInteger( + projection.launchTransactionIndex, + 0, + 0xffff_ffff, + "cursor-transaction-index", + ), + launchLogIndex: boundedInteger( + projection.launchLogIndex, + 0, + 0xffff_ffff, + "cursor-log-index", + ), + launchTransactionHash: canonicalBytes32( + projection.launchTransactionHash, + "cursor-transaction-hash", + ), + tokenAddress: canonicalAddress(projection.tokenAddress, "cursor-token"), + } satisfies IndexedExploreCursorV2["position"]; +} + +function compareAscendingLaunch( + left: IndexedExploreCursorV2["position"], + right: IndexedExploreCursorV2["position"], +) { + const leftBlock = BigInt(left.launchBlockNumber); + const rightBlock = BigInt(right.launchBlockNumber); + if (leftBlock !== rightBlock) return leftBlock < rightBlock ? -1 : 1; + if (left.launchTransactionIndex !== right.launchTransactionIndex) { + return left.launchTransactionIndex - right.launchTransactionIndex; + } + if (left.launchLogIndex !== right.launchLogIndex) { + return left.launchLogIndex - right.launchLogIndex; + } + const transactionComparison = left.launchTransactionHash.localeCompare( + right.launchTransactionHash, + ); + if (transactionComparison !== 0) return transactionComparison; + return left.tokenAddress + .toLowerCase() + .localeCompare(right.tokenAddress.toLowerCase()); +} + +function compareExplorePositions( + left: IndexedExploreCursorV2["position"], + right: IndexedExploreCursorV2["position"], + sort: ExploreSort, +) { + if (sort === "market-cap" || sort === "market-cap-asc") { + if (left.marketCapAtomic === null || right.marketCapAtomic === null) { + if (left.marketCapAtomic === null && right.marketCapAtomic !== null) return 1; + if (left.marketCapAtomic !== null && right.marketCapAtomic === null) return -1; + } else if (left.marketCapAtomic !== right.marketCapAtomic) { + const leftCap = BigInt(left.marketCapAtomic); + const rightCap = BigInt(right.marketCapAtomic); + if (sort === "market-cap") return leftCap > rightCap ? -1 : 1; + return leftCap < rightCap ? -1 : 1; + } + return -compareAscendingLaunch(left, right); + } + const launchComparison = compareAscendingLaunch(left, right); + return sort === "oldest" ? launchComparison : -launchComparison; +} + +function validateCursor( + cursor: IndexedExploreCursorV2, + input: { + snapshot: IndexedSnapshotIdentityV2; + normalizedQuery: string; + sort: ExploreSort; + pageSize: number; + valuationUnit: IndexedExploreCursorV2["valuationUnit"]; + }, +) { + if ( + cursor.adapterVersion !== INDEXED_ROUTE_ADAPTER_VERSION || + canonicalBytes32(cursor.snapshotCommitment, "cursor-snapshot") !== + canonicalBytes32(input.snapshot.snapshotCommitment, "snapshot") || + cursor.normalizedQuery !== input.normalizedQuery || + cursor.sort !== input.sort || + cursor.pageSize !== input.pageSize || + cursor.valuationUnit !== input.valuationUnit + ) { + fail("cursor-mismatch", "cursor-context"); + } + const position = cursor.position; + const canonical = { + marketCapAtomic: nullableUint256Text( + position.marketCapAtomic, + "cursor-market-cap", + ), + launchBlockNumber: integerText( + position.launchBlockNumber, + "cursor-launch-block", + ), + launchTransactionIndex: boundedInteger( + position.launchTransactionIndex, + 0, + 0xffff_ffff, + "cursor-transaction-index", + ), + launchLogIndex: boundedInteger( + position.launchLogIndex, + 0, + 0xffff_ffff, + "cursor-log-index", + ), + launchTransactionHash: canonicalBytes32( + position.launchTransactionHash, + "cursor-transaction-hash", + ), + tokenAddress: canonicalAddress(position.tokenAddress, "cursor-token"), + }; + if ( + (input.valuationUnit === null) !== + (canonical.marketCapAtomic === null) + ) { + fail("cursor-mismatch", "cursor-valuation"); + } + return canonical; +} + +function sameCursorPosition( + left: IndexedExploreCursorV2["position"], + right: IndexedExploreCursorV2["position"], +) { + return ( + left.marketCapAtomic === right.marketCapAtomic && + left.launchBlockNumber === right.launchBlockNumber && + left.launchTransactionIndex === right.launchTransactionIndex && + left.launchLogIndex === right.launchLogIndex && + left.launchTransactionHash.toLowerCase() === + right.launchTransactionHash.toLowerCase() && + left.tokenAddress.toLowerCase() === right.tokenAddress.toLowerCase() + ); +} + +export function adaptIndexedExploreListV2( + envelope: IndexedRouteEnvelopeV2, +): ExplorePage { + const ready = requireReady(envelope); + assertSnapshotScope( + ready.snapshot, + "explore-list", + ALL_SUPPORTED_RELEASES, + ); + const { request, page, tokens } = ready.data; + if (!validExploreSort(request.sort)) fail("invalid-input", "explore-sort"); + const requestedPage = boundedInteger( + request.requestedPage, + 1, + Number.MAX_SAFE_INTEGER, + "explore-page", + ); + const pageSize = boundedInteger(request.pageSize, 1, 100, "explore-page-size"); + const resolvedPage = boundedInteger( + page.resolvedPage, + 1, + Number.MAX_SAFE_INTEGER, + "explore-resolved-page", + ); + const total = safeCount(page.totalCount, "explore-total"); + const totalPages = Math.ceil(total / pageSize); + const expectedPage = totalPages === 0 ? 1 : Math.min(requestedPage, totalPages); + if (resolvedPage !== expectedPage) fail("cursor-mismatch", "explore-page-resolution"); + const marketSort = request.sort === "market-cap" || request.sort === "market-cap-asc"; + if ( + marketSort !== (page.valuationUnit !== null) || + (page.valuationUnit !== null && + page.valuationUnit !== "usd-wad" && + page.valuationUnit !== "native-wei") + ) { + fail("cursor-mismatch", "explore-valuation-unit"); + } + const expectedCount = Math.min( + pageSize, + Math.max(0, total - (resolvedPage - 1) * pageSize), + ); + if (!Array.isArray(tokens) || tokens.length !== expectedCount) { + fail("cursor-mismatch", "explore-page-count"); + } + const normalizedQuery = normalizedExploreQuery(request.query); + if ((resolvedPage === 1) !== (page.startAfter === null)) { + fail("cursor-mismatch", "explore-start-cursor"); + } + const context = { + snapshot: ready.snapshot, + normalizedQuery, + sort: request.sort, + pageSize, + valuationUnit: page.valuationUnit, + }; + const startPosition = page.startAfter + ? validateCursor(page.startAfter, context) + : null; + const positions = tokens.map((projection) => { + validateRowSource(projection.source, ready.snapshot, "explore-list"); + if ( + normalizedQuery && + !projection.name.toLowerCase().includes(normalizedQuery) && + !projection.symbol.toLowerCase().includes(normalizedQuery) && + !projection.tokenAddress.toLowerCase().includes(normalizedQuery) + ) { + fail("scope-mismatch", "explore-query"); + } + return cursorPosition(projection, page.valuationUnit); + }); + if ( + startPosition && + positions[0] && + compareExplorePositions(startPosition, positions[0], request.sort) >= 0 + ) { + fail("cursor-mismatch", "explore-exclusive-cursor"); + } + for (let index = 1; index < positions.length; index += 1) { + if ( + compareExplorePositions( + positions[index - 1]!, + positions[index]!, + request.sort, + ) >= 0 + ) { + fail("cursor-mismatch", "explore-page-order"); + } + } + if ((tokens.length === 0) !== (page.endAt === null)) { + fail("cursor-mismatch", "explore-end-cursor"); + } + if (page.endAt) { + const endPosition = validateCursor(page.endAt, context); + const lastPosition = positions.at(-1); + if (!lastPosition || !sameCursorPosition(endPosition, lastPosition)) { + fail("cursor-mismatch", "explore-end-position"); + } + } + + const launcherFeesAccruedWei = uint256Text( + ready.data.launcherFeesAccruedWei, + "launcher-fees-accrued", + ); + const { publicSnapshot } = validateSnapshot(ready.snapshot); + return { + status: "ready", + tokens: tokens.map((projection) => + adaptToken(projection, ready.snapshot, "explore-list"), + ), + page: resolvedPage, + pageSize, + total, + totalPages, + sort: request.sort, + query: request.query.trim(), + snapshot: publicSnapshot, + launcherFeesAccruedWei, + launcherFeesAccruedEth: formatUnits( + BigInt(launcherFeesAccruedWei), + 18, + ), + }; +} + +export function adaptIndexedTokenDetailV2( + envelope: IndexedRouteEnvelopeV2, +) { + const ready = requireReady(envelope); + assertSnapshotScope( + ready.snapshot, + "explore-token", + ALL_SUPPORTED_RELEASES, + ); + const address = canonicalAddress(ready.data.address, "token-detail-address"); + const token = ready.data.token + ? adaptToken(ready.data.token, ready.snapshot, "explore-token") + : null; + if (token && token.tokenAddress.toLowerCase() !== address.toLowerCase()) { + fail("scope-mismatch", "token-detail-scope"); + } + return { + status: "ready" as const, + token, + snapshot: validateSnapshot(ready.snapshot).publicSnapshot, + }; +} + +export function adaptIndexedChartV2( + envelope: IndexedRouteEnvelopeV2, +) { + const ready = requireReady(envelope); + assertSnapshotScope( + ready.snapshot, + "explore-chart", + ALL_SUPPORTED_RELEASES, + ); + const address = canonicalAddress(ready.data.address, "chart-address"); + canonicalBytes32(ready.data.poolId, "chart-pool"); + if (!["1h", "1d", "1w", "all"].includes(ready.data.range)) { + fail("invalid-input", "chart-range"); + } + const release = validateRowSource( + ready.data.source, + ready.snapshot, + "explore-chart", + ); + const swapCount = safeCount(ready.data.swapCount, "chart-swap-count"); + const volumeWei = uint256Text(ready.data.volumeNativeWei, "chart-volume"); + const volumeUsdWad = nullableUint256Text( + ready.data.volumeUsdWad, + "chart-volume-usd", + ); + + if (release.launchModel === "stock-paired") { + if ( + ready.data.points.length !== 0 || + swapCount !== 0 || + volumeWei !== "0" || + volumeUsdWad !== null + ) { + fail("not-ready", "stock-chart-public-contract"); + } + return { + status: "insufficient-history" as const, + address, + points: [], + swapCount: 0, + volumeWei: "0", + volumeEth: "0", + range: ready.data.range, + snapshotBlock: ready.snapshot.blockNumber, + }; + } + + const points = ready.data.points.map((point, index) => { + const blockNumber = integerText(point.blockNumber, "chart-point-block"); + if ( + BigInt(blockNumber) > BigInt(ready.snapshot.blockNumber) || + (index > 0 && + BigInt(blockNumber) <= + BigInt(ready.data.points[index - 1]!.blockNumber)) + ) { + fail("snapshot-mismatch", "chart-point-order"); + } + const priceNativeWei = positiveIntegerText( + point.priceNativeWei, + "chart-price-native", + ); + const priceUsdWad = nullableUint256Text( + point.priceUsdWad, + "chart-price-usd", + ); + return { + blockNumber, + priceEth: formatUnits(BigInt(priceNativeWei), 18), + ...(priceUsdWad + ? { priceUsd: formatUnits(BigInt(priceUsdWad), 18) } + : {}), + }; + }); + return { + status: points.length >= 2 ? ("ready" as const) : ("insufficient-history" as const), + address, + points, + swapCount, + volumeWei, + volumeEth: formatUnits(BigInt(volumeWei), 18), + ...(volumeUsdWad ? { volumeUsdWad } : {}), + range: ready.data.range, + snapshotBlock: ready.snapshot.blockNumber, + }; +} + +function adaptCreatorClaim( + claim: IndexedCreatorClaimV2, + snapshot: IndexedSnapshotIdentityV2, + account: Address, + tokenPools: ReadonlyMap, +): CreatorClaim { + const release = validateRowSource( + claim.source, + snapshot, + "creator-profile", + ); + if (release.releaseVersion !== "classic-v2") { + fail("unsupported-release", "creator-claim-release"); + } + const poolId = canonicalBytes32(claim.poolId, "claim-pool"); + const tokenAddress = canonicalAddress(claim.tokenAddress, "claim-token"); + const creatorAddress = canonicalAddress(claim.creatorAddress, "claim-creator"); + if ( + creatorAddress.toLowerCase() !== account.toLowerCase() || + tokenPools.get(poolId.toLowerCase()) !== tokenAddress.toLowerCase() + ) { + fail("scope-mismatch", "creator-claim-scope"); + } + const amountWei = uint256Text(claim.amountWei, "claim-amount"); + return { + poolId, + tokenAddress, + creatorAddress, + recipientAddress: canonicalAddress(claim.recipientAddress, "claim-recipient"), + callerAddress: canonicalAddress(claim.callerAddress, "claim-caller"), + amountWei, + amountEth: formatUnits(BigInt(amountWei), 18), + blockNumber: integerText(claim.blockNumber, "claim-block"), + transactionHash: canonicalBytes32(claim.transactionHash, "claim-transaction"), + transactionIndex: boundedInteger( + claim.transactionIndex, + 0, + 0xffff_ffff, + "claim-transaction-index", + ), + logIndex: boundedInteger(claim.logIndex, 0, 0xffff_ffff, "claim-log-index"), + claimedAt: canonicalTimestamp(claim.claimedAt, "claimed-at"), + }; +} + +export function adaptIndexedCreatorProfileV2( + envelope: IndexedRouteEnvelopeV2, +): CreatorProfile { + const ready = requireReady(envelope); + assertSnapshotScope( + ready.snapshot, + "creator-profile", + ALL_SUPPORTED_RELEASES, + ); + const account = canonicalAddress(ready.data.account, "profile-account"); + const tokens = ready.data.tokens.map((projection) => { + if ( + canonicalAddress(projection.creatorAddress, "profile-token-creator").toLowerCase() !== + account.toLowerCase() + ) { + fail("scope-mismatch", "profile-token-scope"); + } + return adaptToken(projection, ready.snapshot, "creator-profile"); + }); + const tokenPools = new Map( + tokens + .filter( + (entry) => + entry.launchModel === "classic" && + entry.launchModelVersion !== "classic-v3", + ) + .map((entry) => [ + entry.poolId.toLowerCase(), + entry.tokenAddress.toLowerCase(), + ]), + ); + const claims = ready.data.claims + .map((claim) => + adaptCreatorClaim(claim, ready.snapshot, account, tokenPools), + ) + .sort((left, right) => { + const leftBlock = BigInt(left.blockNumber); + const rightBlock = BigInt(right.blockNumber); + if (leftBlock !== rightBlock) return leftBlock > rightBlock ? -1 : 1; + if (left.transactionIndex !== right.transactionIndex) { + return right.transactionIndex - left.transactionIndex; + } + if (left.logIndex !== right.logIndex) return right.logIndex - left.logIndex; + return right.transactionHash.localeCompare(left.transactionHash); + }); + const pools = tokens + .filter((entry) => entry.launchModel !== "stock-paired") + .map((entry) => ({ + tokenAddress: entry.tokenAddress, + name: entry.name, + symbol: entry.symbol, + poolId: entry.poolId, + totalSwapFeeBps: entry.totalSwapFeeBps, + launchModel: "classic" as const, + claimableCreatorFeesWei: entry.creatorFeesAccruedWei ?? "0", + claimableCreatorFeesEth: entry.creatorFeesAccruedEth ?? "0", + generatedCreatorFeesWei: entry.creatorFeesGeneratedWei ?? "0", + generatedCreatorFeesEth: entry.creatorFeesGeneratedEth ?? "0", + })); + const claimable = pools.reduce( + (total, pool) => total + BigInt(pool.claimableCreatorFeesWei), + 0n, + ); + const generated = pools.reduce( + (total, pool) => total + BigInt(pool.generatedCreatorFeesWei), + 0n, + ); + const claimed = claims.reduce( + (total, claim) => total + BigInt(claim.amountWei), + 0n, + ); + return { + status: "ready", + account, + tokens, + pools, + claims, + totals: { + claimableWei: claimable.toString(), + claimableEth: formatUnits(claimable, 18), + generatedWei: generated.toString(), + generatedEth: formatUnits(generated, 18), + claimedWei: claimed.toString(), + claimedEth: formatUnits(claimed, 18), + }, + snapshot: validateSnapshot(ready.snapshot).publicSnapshot, + }; +} + +export function adaptIndexedClassicV3ProfileV2( + envelope: IndexedRouteEnvelopeV2, +): Extract { + const ready = requireReady(envelope); + assertSnapshotScope( + ready.snapshot, + "classic-v3-profile", + ["classic-v3"], + ); + const account = canonicalAddress(ready.data.account, "classic-profile-account"); + if (ready.data.chainId !== ready.snapshot.chainId) { + fail("scope-mismatch", "classic-profile-chain"); + } + const seenVaults = new Set(); + const rewards = ready.data.rewards.map((reward) => { + const release = validateRowSource( + reward.source, + ready.snapshot, + "classic-v3-profile", + ); + if (release.releaseVersion !== "classic-v3") { + fail("unsupported-release", "classic-profile-release"); + } + const vaultAddress = canonicalAddress(reward.vaultAddress, "reward-vault"); + if (seenVaults.has(vaultAddress.toLowerCase())) { + fail("projection-incomplete", "duplicate-reward-vault"); + } + seenVaults.add(vaultAddress.toLowerCase()); + if ( + !Array.isArray(reward.allocations) || + reward.allocations.length < 1 || + reward.allocations.length > 5 + ) { + fail("projection-incomplete", "reward-allocations"); + } + const beneficiaries = reward.allocations.map((allocation, index) => { + const allocationIndex = boundedInteger( + allocation.allocationIndex, + 0, + 4, + "reward-allocation-index", + ); + const beneficiary = canonicalAddress( + allocation.beneficiary, + "reward-beneficiary", + ); + const payoutAddress = canonicalAddress( + allocation.payoutAddress, + "reward-payout", + ); + const shareBps = bps(allocation.shareBps, "reward-share"); + if (allocationIndex !== index || shareBps === 0) { + fail("projection-incomplete", "reward-allocation-order"); + } + if (beneficiary.toLowerCase() !== payoutAddress.toLowerCase()) { + fail("not-ready", "classic-payout-semantics"); + } + return { allocationIndex, beneficiary, payoutAddress, shareBps }; + }); + if ( + beneficiaries.reduce((total, item) => total + item.shareBps, 0) !== + 10_000 || + new Set(beneficiaries.map((item) => item.beneficiary.toLowerCase())).size !== + beneficiaries.length + ) { + fail("projection-incomplete", "reward-allocation-total"); + } + const ownedAllocations = beneficiaries.filter( + (item) => item.payoutAddress.toLowerCase() === account.toLowerCase(), + ); + const claimableWei = uint256Text(reward.claimableWei, "reward-claimable"); + const claimedWei = uint256Text(reward.claimedWei, "reward-claimed"); + const buySwapFeeBps = bps(reward.buySwapFeeBps, "reward-buy-fee"); + const sellSwapFeeBps = bps(reward.sellSwapFeeBps, "reward-sell-fee"); + const platformFeeBps = bps(reward.platformFeeBps, "reward-platform-fee"); + if ( + buySwapFeeBps === 0 || + sellSwapFeeBps === 0 || + platformFeeBps !== 10 + ) { + fail("projection-incomplete", "reward-fee-disclosure"); + } + return { + tokenAddress: canonicalAddress(reward.tokenAddress, "reward-token"), + tokenName: descriptiveText(reward.tokenName, 128, "reward-token-name"), + tokenSymbol: descriptiveText(reward.tokenSymbol, 32, "reward-token-symbol"), + poolId: canonicalBytes32(reward.poolId, "reward-pool"), + vaultAddress, + beneficiary: account, + payoutAddress: account, + shareBps: ownedAllocations.reduce((total, item) => total + item.shareBps, 0), + ownedAllocations, + claimableWei, + claimableEth: formatUnits(BigInt(claimableWei), 18), + claimedWei, + claimedEth: formatUnits(BigInt(claimedWei), 18), + buySwapFeeBps, + sellSwapFeeBps, + platformFeeBps: 10 as const, + beneficiaries, + launchTransactionHash: canonicalBytes32( + reward.launchTransactionHash, + "reward-launch-transaction", + ), + }; + }); + return { + status: "ready", + account, + chainId: ready.data.chainId, + rewards, + }; +} + +export function adaptIndexedStockPairedProfileV2( + envelope: IndexedRouteEnvelopeV2, +) { + const ready = requireReady(envelope); + assertSnapshotScope( + ready.snapshot, + "creator-profile", + ["stock-paired-v1", "stock-paired-v2", "stock-paired-v3"], + ); + if (ready.snapshot.chainId !== 1 || ready.data.chainId !== 1) { + fail("scope-mismatch", "stock-profile-chain"); + } + const account = canonicalAddress(ready.data.account, "stock-profile-account"); + const seenVaults = new Set(); + const rewards = ready.data.rewards.map((reward) => { + const release = validateRowSource( + reward.source, + ready.snapshot, + "creator-profile", + ); + if (release.launchModel !== "stock-paired") { + fail("unsupported-release", "stock-profile-release"); + } + const vaultAddress = canonicalAddress(reward.vaultAddress, "stock-vault"); + if (seenVaults.has(vaultAddress.toLowerCase())) { + fail("projection-incomplete", "duplicate-stock-vault"); + } + seenVaults.add(vaultAddress.toLowerCase()); + const beneficiary = canonicalAddress( + reward.beneficiary, + "stock-beneficiary", + ); + if (beneficiary.toLowerCase() !== account.toLowerCase()) { + fail("scope-mismatch", "stock-beneficiary-scope"); + } + if ( + !Array.isArray(reward.beneficiaries) || + reward.beneficiaries.length < 1 || + reward.beneficiaries.length > 8 + ) { + fail("projection-incomplete", "stock-beneficiaries"); + } + const beneficiaries = reward.beneficiaries.map((allocation) => ({ + beneficiary: canonicalAddress( + allocation.beneficiary, + "stock-allocation-beneficiary", + ), + payoutAddress: canonicalAddress( + allocation.payoutAddress, + "stock-allocation-payout", + ), + shareBps: bps(allocation.shareBps, "stock-allocation-share"), + })); + if ( + beneficiaries.some((allocation) => allocation.shareBps === 0) || + beneficiaries.reduce((sum, allocation) => sum + allocation.shareBps, 0) !== + 10_000 || + new Set( + beneficiaries.map((allocation) => + allocation.beneficiary.toLowerCase(), + ), + ).size !== beneficiaries.length + ) { + fail("projection-incomplete", "stock-allocation-total"); + } + const shareBps = bps(reward.shareBps, "stock-account-share"); + if ( + shareBps === 0 || + !beneficiaries.some( + (allocation) => + allocation.beneficiary.toLowerCase() === account.toLowerCase() && + allocation.shareBps === shareBps, + ) + ) { + fail("scope-mismatch", "stock-account-allocation"); + } + const buySwapFeeBps = bps(reward.buySwapFeeBps, "stock-buy-fee"); + const sellSwapFeeBps = bps(reward.sellSwapFeeBps, "stock-sell-fee"); + const programmableFeeBps = bps( + reward.programmableFeeBps, + "stock-programmable-fee", + ); + if ( + buySwapFeeBps !== 100 || + sellSwapFeeBps !== 100 || + programmableFeeBps !== 10 + ) { + fail("projection-incomplete", "stock-fee-disclosure"); + } + const claimableRaw = uint256Text(reward.claimableRaw, "stock-claimable"); + const claimedRaw = uint256Text(reward.claimedRaw, "stock-claimed"); + const generatedRaw = uint256Text(reward.generatedRaw, "stock-generated"); + const creatorFeesPendingRaw = uint256Text( + reward.creatorFeesPendingRaw, + "stock-pending", + ); + const estimate = reward.estimate + ? { + ethRaw: uint256Text(reward.estimate.ethRaw, "stock-estimate-eth"), + usdRaw: uint256Text(reward.estimate.usdRaw, "stock-estimate-usd"), + } + : null; + return { + model: "stock-paired" as const, + tokenAddress: canonicalAddress(reward.tokenAddress, "stock-token"), + tokenName: descriptiveText(reward.tokenName, 128, "stock-token-name"), + tokenSymbol: descriptiveText(reward.tokenSymbol, 32, "stock-token-symbol"), + ...(reward.imageUrl === null + ? {} + : { imageUrl: httpsUrl(reward.imageUrl, "stock-image").toString() }), + hookAddress: canonicalAddress(reward.hookAddress, "stock-hook"), + poolId: canonicalBytes32(reward.poolId, "stock-pool"), + vaultAddress, + quoteAsset: canonicalAddress(reward.quoteAsset, "stock-quote"), + quoteAssetSymbol: descriptiveText( + reward.quoteAssetSymbol, + 32, + "stock-quote-symbol", + ), + beneficiary, + payoutAddress: canonicalAddress(reward.payoutAddress, "stock-payout"), + shareBps, + claimableRaw, + claimable: formatUnits(BigInt(claimableRaw), 18), + claimedRaw, + claimed: formatUnits(BigInt(claimedRaw), 18), + generatedRaw, + generated: formatUnits(BigInt(generatedRaw), 18), + creatorFeesPendingRaw, + beneficiaries, + buySwapFeeBps, + sellSwapFeeBps, + programmableFeeBps, + launchTransactionHash: canonicalBytes32( + reward.launchTransactionHash, + "stock-launch-transaction", + ), + ...(estimate + ? { + estimatedEthRaw: estimate.ethRaw, + estimatedEth: formatUnits(BigInt(estimate.ethRaw), 18), + estimatedUsdRaw: estimate.usdRaw, + estimatedUsd: formatUnits(BigInt(estimate.usdRaw), 6), + } + : {}), + }; + }); + return { + status: "ready" as const, + account, + chainId: 1 as const, + snapshotBlock: ready.snapshot.blockNumber, + rewards, + }; +} + +export function adaptIndexedLaunchLookupV2( + envelope: IndexedRouteEnvelopeV2, +) { + const ready = requireReady(envelope); + assertSnapshotScope( + ready.snapshot, + "launch-lookup", + ready.data.surface === "classic-v3" + ? ["classic-v3"] + : ["stock-paired-v1", "stock-paired-v2", "stock-paired-v3"], + ); + const account = canonicalAddress(ready.data.account, "lookup-account"); + const transactionHash = canonicalBytes32( + ready.data.transactionHash, + "lookup-transaction", + ); + if (ready.data.resolution !== "found") { + if (ready.data.token !== null) { + fail("projection-incomplete", "lookup-empty-resolution"); + } + return ready.data.surface === "stock-paired" + ? ({ status: "pending" as const, launch: null }) + : ({ status: "ready" as const, launch: null }); + } + if (!ready.data.token) fail("projection-incomplete", "lookup-token"); + const projection = ready.data.token; + const release = validateRowSource( + projection.source, + ready.snapshot, + "launch-lookup", + ); + if ( + canonicalAddress(projection.creatorAddress, "lookup-creator").toLowerCase() !== + account.toLowerCase() || + canonicalBytes32(projection.launchTransactionHash, "lookup-token-transaction") !== + transactionHash + ) { + fail("scope-mismatch", "lookup-provenance"); + } + const mapped = adaptToken(projection, ready.snapshot, "launch-lookup"); + if (ready.data.surface === "classic-v3") { + if (release.releaseVersion !== "classic-v3") { + fail("unsupported-release", "classic-lookup-release"); + } + return { + status: "ready" as const, + launch: { + tokenAddress: mapped.tokenAddress, + name: mapped.name, + symbol: mapped.symbol, + launchTransactionHash: transactionHash, + }, + }; + } + if (release.launchModel !== "stock-paired") { + fail("unsupported-release", "stock-lookup-release"); + } + if ( + !mapped.quoteAssetAddress || + !mapped.rewardVaultAddress || + !mapped.positionRecipient || + !mapped.positionTokenId || + !projection.initialBuy || + projection.initialBuy.quoteRaw === null + ) { + fail("projection-incomplete", "stock-lookup-fields"); + } + const initialBuyEthAmount = positiveIntegerText( + projection.initialBuy.nativeWei, + "stock-initial-buy-native", + ); + const initialBuyQuoteAmount = positiveIntegerText( + projection.initialBuy.quoteRaw, + "stock-initial-buy-quote", + ); + const initialBuyTokenAmount = positiveIntegerText( + projection.initialBuy.tokenRaw, + "stock-initial-buy-token", + ); + return { + status: "ready" as const, + launch: { + tokenAddress: mapped.tokenAddress, + name: mapped.name, + symbol: mapped.symbol, + quoteAsset: mapped.quoteAssetAddress, + poolId: mapped.poolId, + rewardVault: mapped.rewardVaultAddress, + positionRecipient: mapped.positionRecipient, + positionTokenId: mapped.positionTokenId, + creator: account, + initialBuyEthAmount, + initialBuyQuoteAmount, + initialBuyTokenAmount, + transactionHash, + }, + }; +} diff --git a/lib/data-pipeline/route-coordinator.server.ts b/lib/data-pipeline/route-coordinator.server.ts new file mode 100644 index 00000000..dbddf27c --- /dev/null +++ b/lib/data-pipeline/route-coordinator.server.ts @@ -0,0 +1,1734 @@ +import "server-only"; + +import { createHash, createHmac, timingSafeEqual } from "node:crypto"; +import { performance } from "node:perf_hooks"; + +import { provenanceHeaders, type ReadProvenance } from "./cache"; +import { canonicalBytes32, parseNonnegativeIntegerText } from "./codecs"; +import { loadDataPipelineConfig, type DataPipelineFlagName } from "./config"; +import { invalidInput } from "./errors"; +import { getServerReadModel, type ServerReadModel } from "./read-model.server"; +import { consumeReleaseProbeNonce } from "./release-probe-nonce.server"; +import type { PostgresTransaction } from "./postgres"; + +export const INDEXED_ROUTE_KEYS = [ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "classic-v3-profile", + "launch-lookup", +] as const; + +export type IndexedRouteKey = (typeof INDEXED_ROUTE_KEYS)[number]; +export type ReviewedModel = "classic" | "stock-paired"; +export type ReviewedRelease = + | "classic-v2" + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + +export type ReviewedRouteScope = Readonly<{ + model: ReviewedModel; + releaseVersion: ReviewedRelease; +}>; + +export const ALL_REVIEWED_ROUTE_SCOPES: readonly ReviewedRouteScope[] = + Object.freeze([ + Object.freeze({ model: "classic", releaseVersion: "classic-v2" }), + Object.freeze({ model: "classic", releaseVersion: "classic-v3" }), + Object.freeze({ + model: "stock-paired", + releaseVersion: "stock-paired-v1", + }), + Object.freeze({ + model: "stock-paired", + releaseVersion: "stock-paired-v2", + }), + Object.freeze({ + model: "stock-paired", + releaseVersion: "stock-paired-v3", + }), + ]); + +export type RouteCheckpoint = { + blockNumber: string; + blockHash: string; +}; + +export type IndexedProjectionVersion = RouteCheckpoint & { + checkpointId: string; + sourceGroup: string; + projectorVersion: string; + epochId: string; + pointerGeneration: string; + checkpointGeneration: string; + reorgGeneration: string; +}; + +export type RouteScopeProjectionVersion = ReviewedRouteScope & { + version: IndexedProjectionVersion; +}; + +const VALIDATED_RECORD_SCOPE_EVIDENCE = Symbol( + "programmable.validated-record-scope-evidence", +); +const VALIDATED_RECORD_SCOPE_EVIDENCE_INSTANCES = new WeakSet(); + +const AUTHORIZED_RELEASE_PROBE = Symbol( + "programmable.authorized-release-probe", +); +const AUTHORIZED_RELEASE_PROBE_ROUTE = Symbol( + "programmable.authorized-release-probe-route", +); +const AUTHORIZED_RELEASE_PROBE_INSTANCES = new WeakSet(); +const CONSUMED_RELEASE_PROBE_INSTANCES = new WeakSet(); + +const RELEASE_PROBE_NONCE = + /^(?[1-9]\d{12})-(?[0-9a-f]{64})-(?0|[1-9]\d{0,9})$/; +const RELEASE_PROBE_MAX_AGE_MS = 5 * 60 * 1_000; +const RELEASE_PROBE_MAX_FUTURE_SKEW_MS = 30 * 1_000; +const RELEASE_PROBE_SIGNATURE = /^[0-9a-f]{64}$/; +const RELEASE_PROBE_SIGNATURE_VERSION = "programmable-release-probe-v1"; + +export type AuthorizedReleaseProbe = Readonly<{ + readonly [AUTHORIZED_RELEASE_PROBE]: true; + readonly [AUTHORIZED_RELEASE_PROBE_ROUTE]: IndexedRouteKey; +}>; + +export type ValidatedRecordScopeEvidence = Readonly<{ + recordCount: number; + recordScopes: readonly ReviewedRouteScope[]; + readonly [VALIDATED_RECORD_SCOPE_EVIDENCE]: true; +}>; + +export type RouteComparisonSchema = Readonly<{ + addressFields?: readonly string[]; + hashFields?: readonly string[]; + integerFields?: readonly string[]; +}>; + +export type LegacyRouteResult = { + response: Response; + source: "rpc" | "blob"; + checkpoint?: RouteCheckpoint; +}; + +export type IndexedRouteResult = { + response: Response; + source: "indexed"; + scope: readonly ReviewedRouteScope[]; + /** + * Route adapters must derive this evidence from the same validated records + * used to construct `response`, before serializing the response body. + */ + scopeEvidence: ValidatedRecordScopeEvidence; + /** One exact immutable database checkpoint for every searched scope. */ + versions: readonly RouteScopeProjectionVersion[]; + /** Snapshot used only to compare an indexed response with the legacy path. */ + comparisonCheckpoint?: RouteCheckpoint; + projectionLag?: number; + reconciledAt?: string; +}; + +export type RouteScopeReadiness = ReviewedRouteScope & { + eligibility: "eligible" | "ineligible"; + parity: "current" | "pending" | "stale" | "mismatch" | "missing"; + version?: IndexedProjectionVersion; +}; + +export type RouteReadiness = readonly RouteScopeReadiness[]; + +export type IndexedRouteSnapshot = Readonly<{ + readiness: RouteReadiness; + /** May be omitted when the transaction proves the route is not current. */ + indexed?: IndexedRouteResult; +}>; + +export type RouteComparison = + | { + kind: "match"; + legacyHash: string; + indexedHash: string; + mismatchPaths: readonly []; + } + | { + kind: "mismatch"; + legacyHash: string; + indexedHash: string; + mismatchPaths: readonly string[]; + }; + +export type RouteComparisonEvent = + | (RouteComparison & { + route: IndexedRouteKey; + scope: readonly ReviewedRouteScope[]; + readiness: RouteReadiness; + blockNumber: string; + blockHash: string; + }) + | { + kind: "incomparable"; + route: IndexedRouteKey; + scope: readonly ReviewedRouteScope[]; + readiness?: RouteReadiness; + reason: + | "readiness-unavailable" + | "model-ineligible" + | "indexed-unavailable" + | "invalid-result" + | "checkpoint-missing" + | "checkpoint-mismatch" + | "body-oversize" + | "non-json" + | "invalid-json" + | "invalid-response"; + }; + +export type CoordinatedRouteRead = { + route: IndexedRouteKey; + scope: readonly ReviewedRouteScope[]; + legacy: () => Promise; + /** + * Must return readiness, the adapted payload, branded record-scope evidence, + * and exact scoped checkpoints from one repeatable-read transaction. Keeping + * this as one callback prevents same-checkpoint parity TOCTOU races. + */ + indexedSnapshot: ( + transaction: PostgresTransaction, + ) => Promise; + /** Present only after server-side probe-token authorization. */ + releaseProbe?: AuthorizedReleaseProbe; + comparisonSchema?: RouteComparisonSchema; + recordComparison?: (event: RouteComparisonEvent) => void | Promise; + /** + * Must enqueue the task outside the response critical path, for example + * with Next.js `after()` or a request-context `waitUntil()` implementation. + */ + scheduleShadowComparison?: ( + task: () => Promise, + ) => void | Promise; +}; + +const ROUTE_FLAGS = Object.freeze({ + "explore-list": "INDEXED_EXPLORE_LIST_READS_ENABLED", + "explore-token": "INDEXED_EXPLORE_TOKEN_READS_ENABLED", + "explore-chart": "INDEXED_EXPLORE_CHART_READS_ENABLED", + "creator-profile": "INDEXED_CREATOR_PROFILE_READS_ENABLED", + "classic-v3-profile": "INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED", + "launch-lookup": "INDEXED_LAUNCH_LOOKUP_ENABLED", +} satisfies Record); + +const RELEASE_MODELS = Object.freeze({ + "classic-v2": "classic", + "classic-v3": "classic", + "stock-paired-v1": "stock-paired", + "stock-paired-v2": "stock-paired", + "stock-paired-v3": "stock-paired", +} satisfies Record); + +const DISCOVERY_ROUTES: ReadonlySet = new Set([ + "explore-token", + "explore-chart", + "launch-lookup", +]); + +const PROJECTION_HEADERS = [ + "X-Programmable-Read-Source", + "X-Programmable-Projection-Block", + "X-Programmable-Projection-Hash", + "X-Programmable-Projection-Lag", + "X-Programmable-Reconciled-At", + "X-Programmable-Release-Version", +] as const; + +const SHARED_CACHE_HEADERS = [ + "Vercel-CDN-Cache-Control", + "CDN-Cache-Control", + "Surrogate-Control", + "Age", +] as const; + +const RELEASE_PROBE_HEADERS = [ + "x-programmable-shadow-probe", + "x-programmable-shadow-probe-signature", + "x-programmable-shadow-probe-token", + "x-programmable-shadow-overhead-ms", + "x-programmable-shadow-parity", + "x-programmable-live-fallback", +] as const; + +const MAX_COMPARISON_BODY_BYTES = 512 * 1024; +const MAX_CANONICAL_NODES = 20_000; +const MAX_CANONICAL_DEPTH = 64; +const MAX_MISMATCH_PATHS = 8; + +type ValidatedRouteIdentity = Readonly<{ + route: IndexedRouteKey; + scope: readonly ReviewedRouteScope[]; +}>; + +type CanonicalValue = + | null + | boolean + | number + | string + | CanonicalValue[] + | { [key: string]: CanonicalValue }; + +type CanonicalizationPolicy = Readonly<{ + addressFields: ReadonlySet; + hashFields: ReadonlySet; + integerFields: ReadonlySet; +}>; + +class ComparisonReadError extends Error { + readonly reason: Extract< + RouteComparisonEvent, + { kind: "incomparable" } + >["reason"]; + + constructor( + reason: Extract["reason"], + ) { + super("Route response is not comparable"); + this.name = "ComparisonReadError"; + this.reason = reason; + } +} + +function supportedRoute(value: unknown): value is IndexedRouteKey { + return ( + typeof value === "string" && + (INDEXED_ROUTE_KEYS as readonly string[]).includes(value) + ); +} + +function supportedModel(value: unknown): value is ReviewedModel { + return value === "classic" || value === "stock-paired"; +} + +function supportedRelease(value: unknown): value is ReviewedRelease { + return ( + typeof value === "string" && + Object.prototype.hasOwnProperty.call(RELEASE_MODELS, value) + ); +} + +function scopeKey(scope: ReviewedRouteScope): string { + return `${scope.model}:${scope.releaseVersion}`; +} + +function validatedScope(value: unknown): readonly ReviewedRouteScope[] { + if ( + !Array.isArray(value) || + value.length < 1 || + value.length > ALL_REVIEWED_ROUTE_SCOPES.length + ) { + throw invalidInput("config", "indexed-route-scope"); + } + const selected = new Map(); + for (const candidate of value) { + if ( + !candidate || + typeof candidate !== "object" || + !supportedModel((candidate as ReviewedRouteScope).model) || + !supportedRelease((candidate as ReviewedRouteScope).releaseVersion) + ) { + throw invalidInput("config", "indexed-route-scope"); + } + const scope = candidate as ReviewedRouteScope; + if (RELEASE_MODELS[scope.releaseVersion] !== scope.model) { + throw invalidInput("config", "indexed-route-scope"); + } + const key = scopeKey(scope); + if (selected.has(key)) { + throw invalidInput("config", "indexed-route-scope"); + } + selected.set( + key, + Object.freeze({ + model: scope.model, + releaseVersion: scope.releaseVersion, + }), + ); + } + return Object.freeze( + ALL_REVIEWED_ROUTE_SCOPES.filter((scope) => + selected.has(scopeKey(scope)), + ).map((scope) => selected.get(scopeKey(scope))!), + ); +} + +function sameScope( + left: readonly ReviewedRouteScope[], + right: readonly ReviewedRouteScope[], +): boolean { + return ( + left.length === right.length && + left.every((scope, index) => scopeKey(scope) === scopeKey(right[index]!)) + ); +} + +function validateIdentity(input: CoordinatedRouteRead): ValidatedRouteIdentity { + if (!supportedRoute(input.route)) { + throw invalidInput("config", "indexed-route-identity"); + } + const scope = validatedScope(input.scope); + if (input.route === "classic-v3-profile" && scope.length !== 1) { + throw invalidInput("config", "indexed-route-scope"); + } + if ( + input.route === "classic-v3-profile" && + (scope[0]?.model !== "classic" || scope[0]?.releaseVersion !== "classic-v3") + ) { + throw invalidInput("config", "indexed-route-identity"); + } + return Object.freeze({ route: input.route, scope }); +} + +function canonicalCheckpoint(value: RouteCheckpoint): RouteCheckpoint { + return Object.freeze({ + blockNumber: parseNonnegativeIntegerText(value.blockNumber), + blockHash: canonicalBytes32(value.blockHash), + }); +} + +function canonicalUuid(value: unknown, field: string): string { + if ( + typeof value !== "string" || + !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + value, + ) + ) { + throw invalidInput("config", field); + } + return value.toLowerCase(); +} + +function canonicalIdentifier( + value: unknown, + field: string, + allowPlus: boolean, +): string { + const pattern = allowPlus + ? /^[A-Za-z0-9][A-Za-z0-9._+:/-]*$/ + : /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/; + if ( + typeof value !== "string" || + Buffer.byteLength(value, "utf8") > 128 || + !pattern.test(value) + ) { + throw invalidInput("config", field); + } + return value; +} + +function canonicalPositiveInteger(value: string, field: string): string { + const canonical = parseNonnegativeIntegerText(value); + if (canonical === "0") throw invalidInput("config", field); + return canonical; +} + +function canonicalProjectionVersion( + value: IndexedProjectionVersion, +): IndexedProjectionVersion { + const checkpoint = canonicalCheckpoint(value); + return Object.freeze({ + ...checkpoint, + checkpointId: canonicalUuid(value.checkpointId, "projection-checkpoint"), + sourceGroup: canonicalIdentifier( + value.sourceGroup, + "projection-source-group", + false, + ), + projectorVersion: canonicalIdentifier( + value.projectorVersion, + "projection-projector-version", + true, + ), + epochId: canonicalUuid(value.epochId, "projection-epoch"), + pointerGeneration: canonicalPositiveInteger( + value.pointerGeneration, + "projection-pointer-generation", + ), + checkpointGeneration: canonicalPositiveInteger( + value.checkpointGeneration, + "projection-checkpoint-generation", + ), + reorgGeneration: parseNonnegativeIntegerText(value.reorgGeneration), + }); +} + +function sameProjectionVersion( + left: IndexedProjectionVersion, + right: IndexedProjectionVersion, +): boolean { + return ( + left.checkpointId === right.checkpointId && + left.sourceGroup === right.sourceGroup && + left.projectorVersion === right.projectorVersion && + left.blockNumber === right.blockNumber && + left.blockHash === right.blockHash && + left.epochId === right.epochId && + left.pointerGeneration === right.pointerGeneration && + left.checkpointGeneration === right.checkpointGeneration && + left.reorgGeneration === right.reorgGeneration + ); +} + +/** + * Route-specific adapters call this on their typed, validated records before + * constructing the JSON response. The coordinator accepts no unbranded scope + * assertion, and independently enforces the searched-scope allowlist. + */ +export function validatedRecordScopeEvidence( + records: readonly T[], + scopeOf: (record: T, index: number) => ReviewedRouteScope, +): ValidatedRecordScopeEvidence { + if (!Array.isArray(records) || records.length > 5_000) { + throw invalidInput("config", "indexed-record-scope-evidence"); + } + const selected = new Map(); + records.forEach((record, index) => { + const scope = validatedScope([scopeOf(record, index)])[0]!; + selected.set(scopeKey(scope), scope); + }); + const recordScopes = Object.freeze( + ALL_REVIEWED_ROUTE_SCOPES.filter((scope) => + selected.has(scopeKey(scope)), + ).map((scope) => selected.get(scopeKey(scope))!), + ); + const evidence = { + recordCount: records.length, + recordScopes, + } as ValidatedRecordScopeEvidence; + Object.defineProperty(evidence, VALIDATED_RECORD_SCOPE_EVIDENCE, { + value: true, + enumerable: false, + configurable: false, + writable: false, + }); + VALIDATED_RECORD_SCOPE_EVIDENCE_INSTANCES.add(evidence); + return Object.freeze(evidence); +} + +function validReleaseProbeToken(value: unknown): value is string { + return ( + typeof value === "string" && + Buffer.byteLength(value, "utf8") >= 32 && + Buffer.byteLength(value, "utf8") <= 512 && + /^[A-Za-z0-9._~+/=-]+$/u.test(value) + ); +} + +function releaseProbeSignaturePayload( + route: IndexedRouteKey, + nonce: string, +): string { + return `${RELEASE_PROBE_SIGNATURE_VERSION}\n${route}\n${nonce}`; +} + +/** + * Shared by private release tooling and tests. The secret never crosses the + * wire; only this route-bound HMAC is sent with the request. + */ +export function signRouteReleaseProbe(input: { + route: IndexedRouteKey; + nonce: string; + secret: string; +}): string { + if (!supportedRoute(input.route)) { + throw invalidInput("config", "release-probe-route"); + } + const route = input.route; + if (!RELEASE_PROBE_NONCE.test(input.nonce)) { + throw invalidInput("config", "release-probe-nonce"); + } + if (!validReleaseProbeToken(input.secret)) { + throw invalidInput("config", "release-probe-token"); + } + return createHmac("sha256", input.secret) + .update(releaseProbeSignaturePayload(route, input.nonce), "utf8") + .digest("hex"); +} + +/** + * Converts the exact internal probe headers into an unforgeable in-process + * capability after a distributed, atomic nonce consume. The capability stores + * no token and must never be serialized. + */ +export async function authorizeRouteReleaseProbe( + headers: Headers, + nonce: string, + route: IndexedRouteKey, +): Promise { + if (headers.get("x-programmable-shadow-probe") !== "1") return null; + if (headers.has("x-programmable-shadow-probe-token")) return null; + const supplied = headers.get("x-programmable-shadow-probe-signature"); + if (!supplied || !RELEASE_PROBE_SIGNATURE.test(supplied)) return null; + const match = RELEASE_PROBE_NONCE.exec(nonce); + if (!match?.groups) return null; + + const expectedToken = process.env.PROGRAMMABLE_SHADOW_PROBE_TOKEN; + if (expectedToken === undefined || expectedToken === "") return null; + if (!validReleaseProbeToken(expectedToken)) { + throw invalidInput("config", "release-probe-token"); + } + const expected = signRouteReleaseProbe({ + route, + nonce, + secret: expectedToken, + }); + const expectedDigest = Buffer.from(expected, "hex"); + const suppliedDigest = Buffer.from(supplied, "hex"); + if (!timingSafeEqual(expectedDigest, suppliedDigest)) return null; + + const now = Date.now(); + const issuedAt = Number(match.groups.issuedAt); + if ( + !Number.isSafeInteger(issuedAt) || + issuedAt < now - RELEASE_PROBE_MAX_AGE_MS || + issuedAt > now + RELEASE_PROBE_MAX_FUTURE_SKEW_MS + ) { + return null; + } + const expiresAt = issuedAt + RELEASE_PROBE_MAX_AGE_MS; + if ( + !(await consumeReleaseProbeNonce({ + route, + nonce, + issuedAt: new Date(issuedAt), + expiresAt: new Date(expiresAt), + })) + ) { + return null; + } + + const capability = {} as AuthorizedReleaseProbe; + Object.defineProperty(capability, AUTHORIZED_RELEASE_PROBE, { + value: true, + enumerable: false, + configurable: false, + writable: false, + }); + Object.defineProperty(capability, AUTHORIZED_RELEASE_PROBE_ROUTE, { + value: route, + enumerable: false, + configurable: false, + writable: false, + }); + AUTHORIZED_RELEASE_PROBE_INSTANCES.add(capability); + return Object.freeze(capability); +} + +function validatedReleaseProbe( + value: AuthorizedReleaseProbe | undefined, + route: IndexedRouteKey, +): AuthorizedReleaseProbe | null { + if (value === undefined) return null; + if ( + !value || + typeof value !== "object" || + value[AUTHORIZED_RELEASE_PROBE] !== true || + value[AUTHORIZED_RELEASE_PROBE_ROUTE] !== route || + !AUTHORIZED_RELEASE_PROBE_INSTANCES.has(value) || + CONSUMED_RELEASE_PROBE_INSTANCES.has(value) || + !Object.isFrozen(value) + ) { + throw invalidInput("config", "release-probe-capability"); + } + CONSUMED_RELEASE_PROBE_INSTANCES.add(value); + return value; +} + +function hasReleaseProbeHeaders(response: Response): boolean { + return RELEASE_PROBE_HEADERS.some((name) => response.headers.has(name)); +} + +function validResponse(value: unknown): value is Response { + return value instanceof Response; +} + +function validateLegacyResult(value: LegacyRouteResult): LegacyRouteResult { + if ( + !value || + !validResponse(value.response) || + (value.source !== "rpc" && value.source !== "blob") || + hasReleaseProbeHeaders(value.response) + ) { + throw new ComparisonReadError("invalid-result"); + } + return value; +} + +function validateIndexedResult( + value: IndexedRouteResult, + identity: ValidatedRouteIdentity, +): IndexedRouteResult { + if ( + !value || + !validResponse(value.response) || + value.source !== "indexed" || + hasReleaseProbeHeaders(value.response) + ) { + throw new ComparisonReadError("invalid-result"); + } + try { + if (!sameScope(validatedScope(value.scope), identity.scope)) { + throw new ComparisonReadError("invalid-result"); + } + const evidence = value.scopeEvidence; + if ( + !evidence || + typeof evidence !== "object" || + evidence[VALIDATED_RECORD_SCOPE_EVIDENCE] !== true || + !VALIDATED_RECORD_SCOPE_EVIDENCE_INSTANCES.has(evidence) || + !Object.isFrozen(evidence) || + !Object.isFrozen(evidence.recordScopes) || + !Number.isSafeInteger(evidence.recordCount) || + evidence.recordCount < 0 || + evidence.recordCount > 5_000 || + !Array.isArray(evidence.recordScopes) + ) { + throw new ComparisonReadError("invalid-result"); + } + const recordScopes = + evidence.recordScopes.length === 0 + ? (Object.freeze([]) as readonly ReviewedRouteScope[]) + : validatedScope(evidence.recordScopes); + for (const recordScope of recordScopes) { + if ( + !identity.scope.some( + (allowed) => scopeKey(allowed) === scopeKey(recordScope), + ) + ) { + throw new ComparisonReadError("invalid-result"); + } + } + if (DISCOVERY_ROUTES.has(identity.route) && recordScopes.length > 1) { + throw new ComparisonReadError("invalid-result"); + } + + const versions = validateScopedProjectionVersions(value.versions, identity); + const comparisonCheckpoint = value.comparisonCheckpoint + ? canonicalCheckpoint(value.comparisonCheckpoint) + : undefined; + if ( + comparisonCheckpoint && + versions.length === 1 && + (comparisonCheckpoint.blockNumber !== versions[0]!.version.blockNumber || + comparisonCheckpoint.blockHash !== versions[0]!.version.blockHash) + ) { + throw new ComparisonReadError("invalid-result"); + } + const result = Object.freeze({ + ...value, + scope: identity.scope, + scopeEvidence: evidence, + versions, + ...(comparisonCheckpoint ? { comparisonCheckpoint } : {}), + }); + indexedProvenance(result, identity.scope); + return result; + } catch { + throw new ComparisonReadError("invalid-result"); + } +} + +function validateScopedProjectionVersions( + value: readonly RouteScopeProjectionVersion[], + identity: ValidatedRouteIdentity, +): readonly RouteScopeProjectionVersion[] { + if (!Array.isArray(value) || value.length !== identity.scope.length) { + throw new ComparisonReadError("invalid-result"); + } + const selected = new Map(); + for (const candidate of value) { + const scope = validatedScope([candidate])[0]!; + const key = scopeKey(scope); + if ( + selected.has(key) || + !identity.scope.some((allowed) => scopeKey(allowed) === key) + ) { + throw new ComparisonReadError("invalid-result"); + } + selected.set( + key, + Object.freeze({ + ...scope, + version: canonicalProjectionVersion(candidate.version), + }), + ); + } + return Object.freeze( + identity.scope.map((scope) => selected.get(scopeKey(scope))!), + ); +} + +function validateReadiness( + value: RouteReadiness, + identity: ValidatedRouteIdentity, +): RouteReadiness { + if (!Array.isArray(value) || value.length !== identity.scope.length) { + throw new ComparisonReadError("invalid-result"); + } + const selected = new Map(); + for (const candidate of value) { + let scope: readonly ReviewedRouteScope[]; + try { + scope = validatedScope([candidate]); + } catch { + throw new ComparisonReadError("invalid-result"); + } + const member = scope[0]!; + const key = scopeKey(member); + if ( + selected.has(key) || + !identity.scope.some((allowed) => scopeKey(allowed) === key) || + (candidate.eligibility !== "eligible" && + candidate.eligibility !== "ineligible") || + !["current", "pending", "stale", "mismatch", "missing"].includes( + candidate.parity, + ) + ) { + throw new ComparisonReadError("invalid-result"); + } + let version: IndexedProjectionVersion | undefined; + if (candidate.parity === "current") { + if (!candidate.version) { + throw new ComparisonReadError("invalid-result"); + } + try { + version = canonicalProjectionVersion(candidate.version); + } catch { + throw new ComparisonReadError("invalid-result"); + } + } + selected.set( + key, + Object.freeze({ + ...member, + eligibility: candidate.eligibility, + parity: candidate.parity, + ...(version ? { version } : {}), + }), + ); + } + return Object.freeze( + identity.scope.map((scope) => selected.get(scopeKey(scope))!), + ); +} + +function validateSnapshotReadiness( + value: IndexedRouteSnapshot, + identity: ValidatedRouteIdentity, +): RouteReadiness { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new ComparisonReadError("invalid-result"); + } + return validateReadiness(value.readiness, identity); +} + +function currentReadinessVersions( + readiness: RouteReadiness, +): readonly RouteScopeProjectionVersion[] | null { + if ( + readiness.length < 1 || + readiness.some( + (member) => + member.eligibility !== "eligible" || + member.parity !== "current" || + !member.version, + ) + ) { + return null; + } + return Object.freeze( + readiness.map((member) => + Object.freeze({ + model: member.model, + releaseVersion: member.releaseVersion, + version: canonicalProjectionVersion(member.version!), + }), + ), + ); +} + +function sameScopedProjectionVersions( + readiness: readonly RouteScopeProjectionVersion[], + indexed: readonly RouteScopeProjectionVersion[], +): boolean { + return ( + readiness.length === indexed.length && + readiness.every((expected, index) => { + const actual = indexed[index]; + return ( + actual !== undefined && + scopeKey(expected) === scopeKey(actual) && + sameProjectionVersion(expected.version, actual.version) + ); + }) + ); +} + +function comparisonPolicy( + schema: RouteComparisonSchema = {}, +): CanonicalizationPolicy { + const fields = (value: readonly string[] | undefined) => { + if (!value) return new Set(); + if (!Array.isArray(value) || value.length > 128) { + throw invalidInput("config", "comparison-schema"); + } + const output = new Set(); + for (const field of value) { + if ( + typeof field !== "string" || + field.length < 1 || + field.length > 64 || + /[\u0000-\u001f\u007f]/u.test(field) + ) { + throw invalidInput("config", "comparison-schema"); + } + output.add(field); + } + return output; + }; + return Object.freeze({ + addressFields: fields(schema.addressFields), + hashFields: fields(schema.hashFields), + integerFields: fields(schema.integerFields), + }); +} + +function normalizeString( + value: string, + field: string | undefined, + policy: CanonicalizationPolicy, +): string { + if ( + field && + policy.addressFields.has(field) && + /^0x[0-9a-fA-F]{40}$/.test(value) + ) { + return value.toLowerCase(); + } + if ( + field && + policy.hashFields.has(field) && + /^0x[0-9a-fA-F]{64}$/.test(value) + ) { + return value.toLowerCase(); + } + if ( + field && + policy.integerFields.has(field) && + /^-?\d+$/.test(value) && + value.length <= 256 + ) { + return BigInt(value).toString(); + } + return value; +} + +function normalizeCanonicalValue( + value: unknown, + state: { nodes: number }, + depth: number, + policy: CanonicalizationPolicy, + field?: string, + arrayElement = false, +): CanonicalValue | undefined { + state.nodes += 1; + if (state.nodes > MAX_CANONICAL_NODES || depth > MAX_CANONICAL_DEPTH) { + throw invalidInput("config", "route-response-complexity"); + } + if (value === undefined) return arrayElement ? null : undefined; + if (value === null || typeof value === "boolean") return value; + if (typeof value === "string") return normalizeString(value, field, policy); + if (typeof value === "number") { + if ( + !Number.isFinite(value) || + (Number.isInteger(value) && !Number.isSafeInteger(value)) + ) { + throw invalidInput("config", "route-response-number"); + } + return Object.is(value, -0) ? 0 : value; + } + if (Array.isArray(value)) { + return value.map((entry) => + normalizeCanonicalValue(entry, state, depth + 1, policy, field, true)!, + ); + } + if (typeof value === "object") { + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) { + throw invalidInput("config", "route-response-object"); + } + const output = Object.create(null) as Record; + for (const key of Object.keys(value as Record).sort()) { + const normalized = normalizeCanonicalValue( + (value as Record)[key], + state, + depth + 1, + policy, + key, + ); + if (normalized !== undefined) output[key] = normalized; + } + return output; + } + throw invalidInput("config", "route-response-type"); +} + +function normalizedRouteResponse( + value: unknown, + schema: RouteComparisonSchema = {}, +): CanonicalValue { + const normalized = normalizeCanonicalValue( + value, + { nodes: 0 }, + 0, + comparisonPolicy(schema), + ); + if (normalized === undefined) { + throw invalidInput("config", "route-response-root"); + } + return normalized; +} + +export function canonicalizeRouteResponse( + value: unknown, + schema: RouteComparisonSchema = {}, +): string { + return JSON.stringify(normalizedRouteResponse(value, schema)); +} + +export function hashCanonicalRouteResponse( + value: unknown, + schema: RouteComparisonSchema = {}, +): string { + const digest = createHash("sha256") + .update(canonicalizeRouteResponse(value, schema), "utf8") + .digest("hex"); + return `0x${digest}`; +} + +function safePathSegment(key: string): string { + return /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/.test(key) ? `.${key}` : '[""]'; +} + +function collectMismatchPaths( + left: CanonicalValue, + right: CanonicalValue, + path: string, + output: string[], +): void { + if (output.length >= MAX_MISMATCH_PATHS) return; + if (Object.is(left, right)) return; + if (Array.isArray(left) && Array.isArray(right)) { + const length = Math.max(left.length, right.length); + for (let index = 0; index < length; index += 1) { + if (output.length >= MAX_MISMATCH_PATHS) return; + if (index >= left.length || index >= right.length) { + output.push(`${path}[${index}]`); + } else { + collectMismatchPaths( + left[index]!, + right[index]!, + `${path}[${index}]`, + output, + ); + } + } + return; + } + if ( + left !== null && + right !== null && + typeof left === "object" && + typeof right === "object" && + !Array.isArray(left) && + !Array.isArray(right) + ) { + const keys = Array.from( + new Set([...Object.keys(left), ...Object.keys(right)]), + ).sort(); + for (const key of keys) { + if (output.length >= MAX_MISMATCH_PATHS) return; + const nextPath = `${path}${safePathSegment(key)}`; + if (!(key in left) || !(key in right)) { + output.push(nextPath); + } else { + collectMismatchPaths(left[key]!, right[key]!, nextPath, output); + } + } + return; + } + output.push(path); +} + +export function compareRouteResponses( + legacy: unknown, + indexed: unknown, + schema: RouteComparisonSchema = {}, +): RouteComparison { + const normalizedLegacy = normalizedRouteResponse(legacy, schema); + const normalizedIndexed = normalizedRouteResponse(indexed, schema); + const legacyCanonical = JSON.stringify(normalizedLegacy); + const indexedCanonical = JSON.stringify(normalizedIndexed); + const legacyHash = `0x${createHash("sha256").update(legacyCanonical).digest("hex")}`; + const indexedHash = `0x${createHash("sha256").update(indexedCanonical).digest("hex")}`; + if (legacyHash === indexedHash) { + return Object.freeze({ + kind: "match" as const, + legacyHash, + indexedHash, + mismatchPaths: Object.freeze([]) as readonly [], + }); + } + const mismatchPaths: string[] = []; + collectMismatchPaths(normalizedLegacy, normalizedIndexed, "$", mismatchPaths); + return Object.freeze({ + kind: "mismatch" as const, + legacyHash, + indexedHash, + mismatchPaths: Object.freeze(mismatchPaths), + }); +} + +async function boundedJsonBody(response: Response): Promise { + const contentType = response.headers.get("Content-Type") ?? ""; + if ( + !/(?:^|\s|;)application\/(?:[a-z0-9.+-]*\+)?json(?:\s*;|$)/i.test( + contentType, + ) + ) { + throw new ComparisonReadError("non-json"); + } + const declaredLength = response.headers.get("Content-Length"); + if (declaredLength && /^\d+$/.test(declaredLength)) { + if (BigInt(declaredLength) > BigInt(MAX_COMPARISON_BODY_BYTES)) { + throw new ComparisonReadError("body-oversize"); + } + } + + let clone: Response; + try { + clone = response.clone(); + } catch { + throw new ComparisonReadError("invalid-response"); + } + if (!clone.body) { + if (clone.status === 204 || clone.status === 205) return null; + throw new ComparisonReadError("invalid-json"); + } + + const reader = clone.body.getReader(); + const chunks: Uint8Array[] = []; + let length = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + length += value.byteLength; + if (length > MAX_COMPARISON_BODY_BYTES) { + await reader.cancel().catch(() => undefined); + throw new ComparisonReadError("body-oversize"); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + + const bytes = new Uint8Array(length); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + let text: string; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + return JSON.parse(text) as unknown; + } catch (error) { + if (error instanceof ComparisonReadError) throw error; + throw new ComparisonReadError("invalid-json"); + } +} + +function copiedResponse(response: Response, headers: Headers): Response { + let clone: Response; + try { + clone = response.clone(); + } catch { + throw new ComparisonReadError("invalid-response"); + } + return new Response(clone.body, { + status: clone.status, + statusText: clone.statusText, + headers, + }); +} + +type ReleaseProbeObservation = Readonly<{ + shadowOverheadMs: number; + shadowParity: "match" | "mismatch" | "incomparable"; + liveFallback: boolean; +}>; + +function releaseProbeResponse( + response: Response, + capability: AuthorizedReleaseProbe, + observation: ReleaseProbeObservation, +): Response { + if (!AUTHORIZED_RELEASE_PROBE_INSTANCES.has(capability)) { + throw invalidInput("config", "release-probe-capability"); + } + const headers = new Headers(response.headers); + for (const name of RELEASE_PROBE_HEADERS) headers.delete(name); + for (const name of SHARED_CACHE_HEADERS) headers.delete(name); + // Vercel must add the cache observation itself after the application returns. + headers.delete("X-Vercel-Cache"); + headers.set("Cache-Control", "private, no-store"); + + if ( + !Number.isSafeInteger(observation.shadowOverheadMs) || + observation.shadowOverheadMs < 0 + ) { + throw invalidInput("config", "release-probe-overhead"); + } + headers.set( + "x-programmable-shadow-overhead-ms", + String(observation.shadowOverheadMs), + ); + headers.set("x-programmable-shadow-parity", observation.shadowParity); + headers.set( + "x-programmable-live-fallback", + observation.liveFallback ? "true" : "false", + ); + return copiedResponse(response, headers); +} + +function withoutProjectionHeaders(response: Response): Headers { + const headers = new Headers(response.headers); + for (const name of PROJECTION_HEADERS) headers.delete(name); + return headers; +} + +function fallbackResponse(result: LegacyRouteResult): Response { + const headers = withoutProjectionHeaders(result.response); + for (const name of SHARED_CACHE_HEADERS) headers.delete(name); + headers.set("Cache-Control", "private, no-store"); + for (const [name, value] of Object.entries( + provenanceHeaders({ source: result.source }), + )) { + headers.set(name, value); + } + return copiedResponse(result.response, headers); +} + +function indexedProvenance( + result: IndexedRouteResult, + scope: readonly ReviewedRouteScope[], +): ReadProvenance { + if (scope.length !== 1) { + return { + source: "indexed", + reconciledAt: result.reconciledAt, + }; + } + const version = result.versions[0]!.version; + return { + source: "indexed", + projectionBlock: version.blockNumber, + projectionHash: version.blockHash as `0x${string}`, + projectionLag: result.projectionLag, + reconciledAt: result.reconciledAt, + releaseVersion: scope[0]!.releaseVersion, + }; +} + +function indexedResponse( + result: IndexedRouteResult, + scope: readonly ReviewedRouteScope[], +): Response { + const headers = withoutProjectionHeaders(result.response); + const provenance: ReadProvenance = { + ...indexedProvenance(result, scope), + }; + for (const [name, value] of Object.entries(provenanceHeaders(provenance))) { + headers.set(name, value); + } + return copiedResponse(result.response, headers); +} + +function unavailableResponse(): Response { + return Response.json( + { error: "read_temporarily_unavailable" }, + { + status: 503, + headers: { + "Cache-Control": "private, no-store", + "Retry-After": "1", + }, + }, + ); +} + +async function safeRecord( + input: CoordinatedRouteRead, + event: RouteComparisonEvent, +): Promise { + if (!input.recordComparison) return; + try { + await input.recordComparison(event); + } catch { + // Comparison telemetry must never affect the public response. + } +} + +function incomparableEvent( + input: CoordinatedRouteRead, + identity: ValidatedRouteIdentity, + reason: Extract["reason"], + readiness?: RouteReadiness, +): RouteComparisonEvent { + return Object.freeze({ + kind: "incomparable" as const, + route: identity.route, + scope: identity.scope, + ...(readiness ? { readiness } : {}), + reason, + }); +} + +function normalizedSemanticHeaders(response: Response) { + const contentType = response.headers.get("Content-Type"); + const cacheControl = response.headers.get("Cache-Control"); + return { + contentType: contentType?.trim().toLowerCase() ?? null, + cacheControl: + cacheControl + ?.split(",") + .map((directive) => directive.trim().toLowerCase()) + .filter(Boolean) + .sort() + .join(",") ?? null, + }; +} + +function comparisonReason( + error: unknown, +): Extract["reason"] { + return error instanceof ComparisonReadError + ? error.reason + : "invalid-response"; +} + +async function recordedComparison( + input: CoordinatedRouteRead, + event: RouteComparisonEvent, +): Promise { + await safeRecord(input, event); + return event; +} + +async function compareShadowResults( + input: CoordinatedRouteRead, + identity: ValidatedRouteIdentity, + readiness: RouteReadiness, + legacy: LegacyRouteResult, + indexed: IndexedRouteResult, +): Promise { + if (!legacy.checkpoint || !indexed.comparisonCheckpoint) { + return recordedComparison( + input, + incomparableEvent(input, identity, "checkpoint-missing", readiness), + ); + } + + let legacyCheckpoint: RouteCheckpoint; + let indexedCheckpoint: RouteCheckpoint; + try { + legacyCheckpoint = canonicalCheckpoint(legacy.checkpoint); + indexedCheckpoint = canonicalCheckpoint(indexed.comparisonCheckpoint); + } catch { + return recordedComparison( + input, + incomparableEvent(input, identity, "invalid-result", readiness), + ); + } + if ( + legacyCheckpoint.blockNumber !== indexedCheckpoint.blockNumber || + legacyCheckpoint.blockHash !== indexedCheckpoint.blockHash + ) { + return recordedComparison( + input, + incomparableEvent(input, identity, "checkpoint-mismatch", readiness), + ); + } + + try { + const [legacyBody, indexedBody] = await Promise.all([ + boundedJsonBody(legacy.response), + boundedJsonBody(indexed.response), + ]); + const comparison = compareRouteResponses( + { + status: legacy.response.status, + headers: normalizedSemanticHeaders(legacy.response), + body: legacyBody, + }, + { + status: indexed.response.status, + headers: normalizedSemanticHeaders(indexed.response), + body: indexedBody, + }, + input.comparisonSchema, + ); + return recordedComparison( + input, + Object.freeze({ + ...comparison, + route: identity.route, + scope: identity.scope, + readiness, + blockNumber: legacyCheckpoint.blockNumber, + blockHash: legacyCheckpoint.blockHash, + }), + ); + } catch (error) { + return recordedComparison( + input, + incomparableEvent(input, identity, comparisonReason(error), readiness), + ); + } +} + +async function runShadowComparison( + input: CoordinatedRouteRead, + identity: ValidatedRouteIdentity, + readModel: ServerReadModel, + legacy: LegacyRouteResult, +): Promise { + let snapshot: IndexedRouteSnapshot; + let readiness: RouteReadiness; + try { + snapshot = await readModel.repeatableReadSnapshot(input.indexedSnapshot); + readiness = validateSnapshotReadiness(snapshot, identity); + } catch (error) { + return recordedComparison( + input, + incomparableEvent( + input, + identity, + error instanceof ComparisonReadError + ? error.reason + : "readiness-unavailable", + ), + ); + } + if (readiness.some((member) => member.eligibility !== "eligible")) { + return recordedComparison( + input, + incomparableEvent(input, identity, "model-ineligible", readiness), + ); + } + + if (!snapshot.indexed) { + return recordedComparison( + input, + incomparableEvent(input, identity, "indexed-unavailable", readiness), + ); + } + + let indexed: IndexedRouteResult; + try { + indexed = validateIndexedResult(snapshot.indexed, identity); + } catch (error) { + return recordedComparison( + input, + incomparableEvent( + input, + identity, + error instanceof ComparisonReadError + ? error.reason + : "indexed-unavailable", + readiness, + ), + ); + } + return compareShadowResults(input, identity, readiness, legacy, indexed); +} + +async function runSynchronousShadowProbe( + input: CoordinatedRouteRead, + identity: ValidatedRouteIdentity, + readModel: ServerReadModel, + legacy: LegacyRouteResult, + capability: AuthorizedReleaseProbe, +): Promise { + const startedAt = performance.now(); + let observedEvent: RouteComparisonEvent | undefined; + + let comparisonResponse: Response | undefined; + try { + comparisonResponse = legacy.response.clone(); + } catch { + const event = incomparableEvent(input, identity, "invalid-response"); + observedEvent = event; + await safeRecord(input, event); + } + if (comparisonResponse) { + observedEvent = await runShadowComparison(input, identity, readModel, { + ...legacy, + response: comparisonResponse, + }); + } + + const elapsed = Math.ceil(performance.now() - startedAt); + const parity = + observedEvent?.kind === "match" || observedEvent?.kind === "mismatch" + ? observedEvent.kind + : "incomparable"; + return releaseProbeResponse(fallbackResponse(legacy), capability, { + shadowOverheadMs: Math.max(0, elapsed), + shadowParity: parity, + liveFallback: false, + }); +} + +async function runSynchronousLiveProbe( + input: CoordinatedRouteRead, + identity: ValidatedRouteIdentity, + readiness: RouteReadiness, + indexed: IndexedRouteResult, + selectedResponse: Response, + capability: AuthorizedReleaseProbe, +): Promise { + const startedAt = performance.now(); + let observedEvent: RouteComparisonEvent | undefined; + + try { + const legacy = validateLegacyResult(await input.legacy()); + observedEvent = await compareShadowResults( + input, + identity, + readiness, + legacy, + indexed, + ); + } catch (error) { + observedEvent = incomparableEvent( + input, + identity, + comparisonReason(error), + readiness, + ); + await safeRecord(input, observedEvent); + } + + const elapsed = Math.ceil(performance.now() - startedAt); + const parity = + observedEvent?.kind === "match" || observedEvent?.kind === "mismatch" + ? observedEvent.kind + : "incomparable"; + return releaseProbeResponse(selectedResponse, capability, { + shadowOverheadMs: Math.max(0, elapsed), + shadowParity: parity, + liveFallback: false, + }); +} + +function scheduleShadowRead( + input: CoordinatedRouteRead, + identity: ValidatedRouteIdentity, + readModel: ServerReadModel, + legacy: LegacyRouteResult, +): void { + const scheduler = input.scheduleShadowComparison; + if (!scheduler) throw invalidInput("config", "shadow-scheduler"); + + const schedule = (task: () => Promise) => { + try { + const scheduled = scheduler(task); + if (scheduled && typeof scheduled.then === "function") { + void scheduled.catch(() => undefined); + } + } catch { + // Shadow telemetry must never replace or mutate the legacy response. + } + }; + + let comparisonResponse: Response; + try { + comparisonResponse = legacy.response.clone(); + } catch { + schedule(() => + safeRecord(input, incomparableEvent(input, identity, "invalid-response")), + ); + return; + } + + const comparisonLegacy: LegacyRouteResult = { + ...legacy, + response: comparisonResponse, + }; + const task = async () => { + try { + await runShadowComparison(input, identity, readModel, comparisonLegacy); + } catch { + await safeRecord( + input, + incomparableEvent(input, identity, "invalid-response"), + ); + } + }; + schedule(task); +} + +async function fallbackOrUnavailable( + input: CoordinatedRouteRead, + fallbackEnabled: boolean, +): Promise> { + if (!fallbackEnabled) { + return Object.freeze({ + response: unavailableResponse(), + usedFallback: false, + }); + } + try { + return Object.freeze({ + response: fallbackResponse(validateLegacyResult(await input.legacy())), + usedFallback: true, + }); + } catch { + return Object.freeze({ + response: unavailableResponse(), + usedFallback: false, + }); + } +} + +async function liveFallbackOrUnavailable( + input: CoordinatedRouteRead, + fallbackEnabled: boolean, + releaseProbe: AuthorizedReleaseProbe | null, +): Promise { + const startedAt = performance.now(); + const outcome = await fallbackOrUnavailable(input, fallbackEnabled); + if (!releaseProbe) return outcome.response; + const elapsed = Math.ceil(performance.now() - startedAt); + return releaseProbeResponse( + outcome.response, + releaseProbe, + { + shadowOverheadMs: Math.max(0, elapsed), + shadowParity: "incomparable", + liveFallback: outcome.usedFallback, + }, + ); +} + +export async function coordinateRouteRead( + input: CoordinatedRouteRead, +): Promise { + const identity = validateIdentity(input); + const releaseProbe = validatedReleaseProbe(input.releaseProbe, identity.route); + const config = loadDataPipelineConfig(); + const routeEnabled = config.flags[ROUTE_FLAGS[identity.route]]; + if (!routeEnabled && !releaseProbe) { + return validateLegacyResult(await input.legacy()).response; + } + + // Configuration errors are intentionally not converted into a fallback. + // Enabling an indexed route without its database must fail closed. + const readModel = await getServerReadModel({ required: Boolean(releaseProbe) }); + if (!readModel) throw invalidInput("config", "indexed-read-model"); + + if (!routeEnabled) { + const legacy = validateLegacyResult(await input.legacy()); + return runSynchronousShadowProbe( + input, + identity, + readModel, + legacy, + releaseProbe!, + ); + } + + if (config.flags.INDEXED_READ_SHADOW_COMPARE_ENABLED) { + if (!releaseProbe && !input.scheduleShadowComparison) { + throw invalidInput("config", "shadow-scheduler"); + } + const legacy = validateLegacyResult(await input.legacy()); + if (releaseProbe) { + return runSynchronousShadowProbe( + input, + identity, + readModel, + legacy, + releaseProbe, + ); + } + scheduleShadowRead(input, identity, readModel, legacy); + return legacy.response; + } + + let snapshot: IndexedRouteSnapshot; + let readiness: RouteReadiness; + try { + snapshot = await readModel.repeatableReadSnapshot(input.indexedSnapshot); + readiness = validateSnapshotReadiness(snapshot, identity); + } catch { + return liveFallbackOrUnavailable( + input, + config.flags.INDEXED_READ_LIVE_FALLBACK_ENABLED, + releaseProbe, + ); + } + const readyVersions = currentReadinessVersions(readiness); + if (!readyVersions) { + return liveFallbackOrUnavailable( + input, + config.flags.INDEXED_READ_LIVE_FALLBACK_ENABLED, + releaseProbe, + ); + } + if (!snapshot.indexed) { + return liveFallbackOrUnavailable( + input, + config.flags.INDEXED_READ_LIVE_FALLBACK_ENABLED, + releaseProbe, + ); + } + + let indexed: IndexedRouteResult; + let response: Response; + try { + indexed = validateIndexedResult(snapshot.indexed, identity); + if (!sameScopedProjectionVersions(readyVersions, indexed.versions)) { + throw new ComparisonReadError("checkpoint-mismatch"); + } + response = indexedResponse(indexed, identity.scope); + } catch { + return liveFallbackOrUnavailable( + input, + config.flags.INDEXED_READ_LIVE_FALLBACK_ENABLED, + releaseProbe, + ); + } + + if (!releaseProbe) return response; + return runSynchronousLiveProbe( + input, + identity, + readiness, + indexed, + response, + releaseProbe, + ); +} diff --git a/lib/data-pipeline/rpc-provider-commitments.ts b/lib/data-pipeline/rpc-provider-commitments.ts new file mode 100644 index 00000000..b99d794a --- /dev/null +++ b/lib/data-pipeline/rpc-provider-commitments.ts @@ -0,0 +1,13 @@ +import { keccak256, toBytes, type Hex } from "viem"; + +const DOMAINS = { + endpoint: "programmable:data-pipeline:rpc-endpoint:v1\0", + origin: "programmable:data-pipeline:rpc-origin:v1\0", +} as const; + +export function rpcProviderCommitment( + scope: keyof typeof DOMAINS, + canonicalValue: string, +): Hex { + return keccak256(toBytes(`${DOMAINS[scope]}${canonicalValue}`)); +} diff --git a/lib/data-pipeline/rpc-providers.server.ts b/lib/data-pipeline/rpc-providers.server.ts new file mode 100644 index 00000000..3c414f50 --- /dev/null +++ b/lib/data-pipeline/rpc-providers.server.ts @@ -0,0 +1,774 @@ +import "server-only"; + +import { + createPublicClient, + http, + numberToHex, + type Hex, + type RpcLog, +} from "viem"; +import { mainnet } from "viem/chains"; + +import type { + CandidateRpcClassicRewardFactorySnapshot, + CandidateRpcRewardSnapshot, + CandidateRpcClient, + CandidateRpcLog, + CandidateRpcLogFilter, + CandidateRpcProvider, + CandidateRpcReceipt, +} from "./dual-rpc"; +import { invalidInput } from "./errors"; +import { + canonicalProjectorRpcEndpoint, + projectorRpcDeploymentCommitment, + projectorRpcSchemaCommitment, +} from "./projector-provider-commitments"; +import { + expectedRewardRpcCallCount, + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1, + type ProjectorRewardRpcModel, +} from "./projector-reward-rpc-contract"; +import { + PROJECTOR_JSON_RPC_BATCH_SIZE, + PROJECTOR_MAXIMUM_RPC_STARTS_PER_SECOND, +} from "./projector-runtime-limits"; +import { rpcProviderCommitment } from "./rpc-provider-commitments"; + +type Environment = Readonly>; + +const BROWSER_FORBIDDEN_RPC_NAMES = [ + "NEXT_PUBLIC_PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL", + "NEXT_PUBLIC_PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL", +] as const; +const PRODUCTION_PROVIDER_PAIRS = new WeakSet(); +const MAXIMUM_PHYSICAL_RPC_CALLS_IN_FLIGHT_PER_PROVIDER = 8; +const RPC_RATE_WINDOW_MS = 1_000; +const ERC20_METADATA_ABI = [ + { + type: "function", + name: "name", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "string" }], + }, + { + type: "function", + name: "symbol", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "string" }], + }, +] as const; +const REWARD_VAULT_ABI = [ + { + type: "function", + name: "poolId", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "bytes32" }], + }, + { + type: "function", + name: "configurationEpoch", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint64" }], + }, + { + type: "function", + name: "activeConfigurationHash", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "bytes32" }], + }, + { + type: "function", + name: "configurationHash", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "bytes32" }], + }, + { + type: "function", + name: "totalCreatorFeesReceived", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "totalCreatorFeesClaimed", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "beneficiaryCount", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "beneficiaryAt", + stateMutability: "view", + inputs: [{ name: "index", type: "uint256" }], + outputs: [{ name: "", type: "address" }], + }, + { + type: "function", + name: "shareBpsAt", + stateMutability: "view", + inputs: [{ name: "index", type: "uint256" }], + outputs: [{ name: "", type: "uint16" }], + }, + { + type: "function", + name: "shareBpsOf", + stateMutability: "view", + inputs: [{ name: "beneficiary", type: "address" }], + outputs: [{ name: "", type: "uint16" }], + }, + { + type: "function", + name: "payoutAddressOf", + stateMutability: "view", + inputs: [{ name: "beneficiary", type: "address" }], + outputs: [{ name: "", type: "address" }], + }, + { + type: "function", + name: "claimable", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + }, + { + type: "function", + name: "claimedBy", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + }, +] as const; +const CLASSIC_REWARD_VAULT_FACTORY_ABI = [ + { + type: "function", + name: "configurationHashOf", + stateMutability: "view", + inputs: [{ name: "vault", type: "address" }], + outputs: [{ name: "", type: "bytes32" }], + }, + { + type: "function", + name: "ctoAuthority", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "address" }], + }, + { + type: "function", + name: "initCodeHash", + stateMutability: "view", + inputs: [ + { name: "feeHook", type: "address" }, + { name: "poolId", type: "bytes32" }, + { name: "beneficiaries", type: "address[]" }, + { name: "sharesBps", type: "uint16[]" }, + ], + outputs: [{ name: "", type: "bytes32" }], + }, + { + type: "function", + name: "predict", + stateMutability: "view", + inputs: [ + { name: "salt", type: "bytes32" }, + { name: "feeHook", type: "address" }, + { name: "poolId", type: "bytes32" }, + { name: "beneficiaries", type: "address[]" }, + { name: "sharesBps", type: "uint16[]" }, + ], + outputs: [{ name: "", type: "address" }], + }, +] as const; + +type RewardFunctionName = + (typeof REWARD_VAULT_ABI)[number]["name"]; + +export function boundedRpcExecutor(maximumInFlight: number) { + if ( + !Number.isSafeInteger(maximumInFlight) || + maximumInFlight < 1 || + maximumInFlight > PROJECTOR_MAXIMUM_RPC_STARTS_PER_SECOND + ) { + throw invalidInput("rpc", "rpc-concurrency"); + } + let inFlight = 0; + const waiters: Array<() => void> = []; + const starts: number[] = []; + let wakeup: ReturnType | undefined; + const prune = (now: number) => { + while ( + starts.length > 0 && + starts[0]! <= now - RPC_RATE_WINDOW_MS + ) { + starts.shift(); + } + }; + const schedule = () => { + const now = Date.now(); + prune(now); + while ( + waiters.length > 0 && + inFlight < maximumInFlight && + starts.length < PROJECTOR_MAXIMUM_RPC_STARTS_PER_SECOND + ) { + const next = waiters.shift()!; + inFlight += 1; + starts.push(now); + next(); + } + if ( + waiters.length > 0 && + inFlight < maximumInFlight && + starts.length >= PROJECTOR_MAXIMUM_RPC_STARTS_PER_SECOND && + wakeup === undefined + ) { + const delay = Math.max( + 1, + starts[0]! + RPC_RATE_WINDOW_MS - now, + ); + wakeup = setTimeout(() => { + wakeup = undefined; + schedule(); + }, delay); + } + }; + const acquire = async (): Promise => { + await new Promise((resolve) => { + waiters.push(resolve); + schedule(); + }); + }; + return async (operation: () => Promise): Promise => { + await acquire(); + try { + return await operation(); + } finally { + inFlight -= 1; + schedule(); + } + }; +} + +export function assertProductionDualRpcProviders( + providers: unknown, +): void { + const productionMarkerPresent = + process.env.NODE_ENV === "production" || + process.env.VERCEL_ENV === "production"; + if (process.env.NODE_ENV === "test" && !productionMarkerPresent) return; + if ( + providers === null || + (typeof providers !== "object" && typeof providers !== "function") || + !PRODUCTION_PROVIDER_PAIRS.has(providers) + ) { + throw invalidInput("rpc", "untrusted-provider-pair"); + } +} + +function candidateRpcClient(endpoint: string): CandidateRpcClient { + const client = createPublicClient({ + chain: mainnet, + transport: http(endpoint, { + batch: false, + fetchOptions: { redirect: "error" }, + retryCount: 0, + timeout: 5_000, + }), + }); + const batchClient = createPublicClient({ + chain: mainnet, + transport: http(endpoint, { + batch: { batchSize: PROJECTOR_JSON_RPC_BATCH_SIZE, wait: 0 }, + fetchOptions: { redirect: "error" }, + retryCount: 0, + timeout: 5_000, + }), + }); + const normalizeReceipt = ( + receipt: Awaited>, + ): CandidateRpcReceipt => ({ + status: receipt.status, + blockNumber: receipt.blockNumber, + blockHash: receipt.blockHash, + transactionHash: receipt.transactionHash, + transactionIndex: receipt.transactionIndex, + logs: receipt.logs.map((log) => ({ + address: log.address, + blockNumber: log.blockNumber, + blockHash: log.blockHash, + transactionHash: log.transactionHash, + transactionIndex: log.transactionIndex, + logIndex: log.logIndex, + removed: log.removed ?? false, + topics: log.topics as readonly Hex[], + data: log.data, + })), + }); + const normalizeRpcLog = (log: RpcLog): CandidateRpcLog => ({ + address: log.address, + blockNumber: log.blockNumber === null ? null : BigInt(log.blockNumber), + blockHash: log.blockHash, + transactionHash: log.transactionHash, + transactionIndex: + log.transactionIndex === null + ? null + : Number(BigInt(log.transactionIndex)), + logIndex: log.logIndex === null ? null : Number(BigInt(log.logIndex)), + removed: log.removed, + topics: log.topics as readonly Hex[], + data: log.data, + }); + const readFilteredLogs = async ( + rpcClient: typeof client, + filter: CandidateRpcLogFilter, + ): Promise => { + if ( + filter.addresses.length < 1 || + filter.addresses.length > 512 || + filter.topic0.length < 1 || + filter.topic0.length > 64 || + filter.fromBlock < 0n || + filter.toBlock < filter.fromBlock || + filter.toBlock - filter.fromBlock + 1n > 1n + ) { + throw invalidInput("rpc", "log-filter"); + } + const logs = await rpcClient.request({ + method: "eth_getLogs", + params: [{ + address: [...filter.addresses], + topics: [[...filter.topic0]], + fromBlock: numberToHex(filter.fromBlock), + toBlock: numberToHex(filter.toBlock), + }], + }); + return logs.map(normalizeRpcLog); + }; + // One limiter is shared by every operation issued through this provider + // client, including every subrequest carried by the batched transport. + // Reward snapshots and replay windows may fan out concurrently; without + // this provider-wide fence they can exceed paid-RPC burst limits even while + // their aggregate request counts remain bounded. + const rpc = boundedRpcExecutor( + MAXIMUM_PHYSICAL_RPC_CALLS_IN_FLIGHT_PER_PROVIDER, + ); + + return Object.freeze({ + getChainId: () => rpc(() => client.getChainId()), + getBlockNumber: () => rpc(() => client.getBlockNumber()), + async getBlock({ blockNumber }) { + const block = await rpc(() => client.getBlock({ blockNumber })); + return { + number: block.number, + hash: block.hash, + timestamp: block.timestamp, + }; + }, + async getBlocks({ blockNumbers }) { + if (blockNumbers.length < 1 || blockNumbers.length > 100) { + throw invalidInput("rpc", "block-batch-size"); + } + return Promise.all( + blockNumbers.map(async (blockNumber) => { + const block = await rpc(() => + batchClient.getBlock({ blockNumber }) + ); + return { + number: block.number, + hash: block.hash, + timestamp: block.timestamp, + }; + }), + ); + }, + async getTransactionReceipt({ hash }) { + const receipt = await rpc(() => + client.getTransactionReceipt({ hash }) + ); + return normalizeReceipt(receipt); + }, + async getTransactionReceipts({ hashes }) { + if (hashes.length < 1 || hashes.length > 100) { + throw invalidInput("rpc", "receipt-batch-size"); + } + return Promise.all( + hashes.map(async (hash) => + normalizeReceipt( + await rpc(() => batchClient.getTransactionReceipt({ hash })), + ) + ), + ); + }, + getBytecode: (request) => rpc(() => + "blockHash" in request && request.blockHash !== undefined + ? client.getBytecode({ + address: request.address, + blockHash: request.blockHash, + requireCanonical: true, + }) + : client.getBytecode({ + address: request.address, + blockNumber: request.blockNumber, + }) + ), + getBytecodes({ requests }) { + if (requests.length < 1 || requests.length > 100) { + throw invalidInput("rpc", "bytecode-batch-size"); + } + return Promise.all( + requests.map((request) => + rpc(() => batchClient.getBytecode(request)) + ), + ); + }, + async readErc20Metadata({ address, blockHash, requireCanonical }) { + const [name, symbol] = await Promise.all([ + rpc(() => client.readContract({ + address, + abi: ERC20_METADATA_ABI, + functionName: "name", + blockHash, + requireCanonical, + })), + rpc(() => client.readContract({ + address, + abi: ERC20_METADATA_ABI, + functionName: "symbol", + blockHash, + requireCanonical, + })), + ]); + return { name, symbol }; + }, + async readRewardSnapshot({ + model, + vault, + blockNumber, + blockHash, + balanceAccounts, + }): Promise { + const contract = PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1.models[model]; + if ( + !contract || + balanceAccounts.length < 1 || + balanceAccounts.length > contract.maximumBalanceAccounts + ) { + throw invalidInput("rpc", "reward-snapshot-request"); + } + let rpcCallCount = 0; + const decimal = (value: unknown): unknown => { + if (typeof value === "bigint") return value.toString(); + if ( + typeof value === "number" && + Number.isSafeInteger(value) && + value >= 0 + ) { + return value.toString(); + } + return value; + }; + const read = async ( + functionName: RewardFunctionName, + args: readonly unknown[] = [], + ): Promise => { + rpcCallCount += 1; + return rpc(() => client.readContract({ + address: vault, + abi: REWARD_VAULT_ABI, + functionName, + args, + blockHash, + requireCanonical: true, + } as never)); + }; + const [ + poolId, + configurationEpoch, + configurationHash, + totalCreatorFeesReceived, + totalCreatorFeesClaimed, + beneficiaryCountRaw, + ] = model === "classic-v3" + ? await Promise.all([ + read("poolId"), + read("configurationEpoch"), + read("activeConfigurationHash"), + read("totalCreatorFeesReceived"), + read("totalCreatorFeesClaimed"), + read("beneficiaryCount"), + ]) + : [ + ...await Promise.all([ + read("poolId"), + read("configurationHash"), + read("totalCreatorFeesReceived"), + read("totalCreatorFeesClaimed"), + read("beneficiaryCount"), + ]).then( + ([pool, hash, received, claimed, count]) => + [pool, null, hash, received, claimed, count] as const, + ), + ]; + if (typeof beneficiaryCountRaw !== "bigint") { + throw invalidInput("rpc", "reward-beneficiary-count"); + } + const beneficiaryCount = Number(beneficiaryCountRaw); + if ( + !Number.isSafeInteger(beneficiaryCount) || + beneficiaryCount < 1 || + beneficiaryCount > contract.maximumAllocations + ) { + throw invalidInput("rpc", "reward-beneficiary-count"); + } + const allocations = await Promise.all( + Array.from({ length: beneficiaryCount }, async (_value, index) => { + const beneficiary = await read("beneficiaryAt", [BigInt(index)]); + if (model === "classic-v3") { + const shareBps = await read("shareBpsAt", [BigInt(index)]); + return Object.freeze({ + allocationIndex: index, + beneficiary, + payoutAddress: beneficiary, + shareBps: decimal(shareBps), + }); + } + const [shareBps, payoutAddress] = await Promise.all([ + read("shareBpsOf", [beneficiary]), + read("payoutAddressOf", [beneficiary]), + ]); + return Object.freeze({ + allocationIndex: index, + beneficiary, + payoutAddress, + shareBps: decimal(shareBps), + }); + }), + ); + const payoutByAccount = new Map( + allocations.map(({ beneficiary, payoutAddress }) => [ + beneficiary, + payoutAddress, + ]), + ); + const balances = await Promise.all( + balanceAccounts.map(async (account) => { + const [claimableAccrued, claimedTotal] = await Promise.all([ + read("claimable", [account]), + read("claimedBy", [account]), + ]); + return Object.freeze({ + account, + payoutAddress: + model === "classic-v3" + ? account + : payoutByAccount.get(account) ?? account, + claimableAccrued: decimal(claimableAccrued), + claimedTotal: decimal(claimedTotal), + }); + }), + ); + if ( + rpcCallCount !== + expectedRewardRpcCallCount( + model as ProjectorRewardRpcModel, + beneficiaryCount, + balanceAccounts.length, + ) + ) { + throw invalidInput("rpc", "reward-call-contract"); + } + return Object.freeze({ + model, + vault, + blockNumber: blockNumber.toString(), + blockHash, + poolId, + configurationEpoch: decimal(configurationEpoch), + configurationHash, + totalCreatorFeesReceived: decimal(totalCreatorFeesReceived), + totalCreatorFeesClaimed: decimal(totalCreatorFeesClaimed), + beneficiaryCount: beneficiaryCountRaw.toString(), + allocations: Object.freeze(allocations), + balances: Object.freeze(balances), + rpcCallCount, + }); + }, + async readClassicRewardFactorySnapshot({ + factory, + vault, + blockNumber, + blockHash, + salt, + feeHook, + poolId, + beneficiaries, + sharesBps, + }): Promise { + const exactBlock = { blockHash, requireCanonical: true } as const; + const [configurationHash, ctoAuthority, initCodeHash, predictedVault] = + await Promise.all([ + rpc(() => client.readContract({ + address: factory, + abi: CLASSIC_REWARD_VAULT_FACTORY_ABI, + functionName: "configurationHashOf", + args: [vault], + ...exactBlock, + })), + rpc(() => client.readContract({ + address: factory, + abi: CLASSIC_REWARD_VAULT_FACTORY_ABI, + functionName: "ctoAuthority", + ...exactBlock, + })), + rpc(() => client.readContract({ + address: factory, + abi: CLASSIC_REWARD_VAULT_FACTORY_ABI, + functionName: "initCodeHash", + args: [feeHook, poolId, [...beneficiaries], [...sharesBps]], + ...exactBlock, + })), + rpc(() => client.readContract({ + address: factory, + abi: CLASSIC_REWARD_VAULT_FACTORY_ABI, + functionName: "predict", + args: [ + salt, + feeHook, + poolId, + [...beneficiaries], + [...sharesBps], + ], + ...exactBlock, + })), + ]); + return Object.freeze({ + factory, + vault, + blockNumber: blockNumber.toString(), + blockHash, + configurationHash, + ctoAuthority, + initCodeHash, + predictedVault, + rpcCallCount: 4, + }); + }, + getLogs(filter) { + return rpc(() => readFilteredLogs(client, filter)); + }, + async getLogsBatch({ requests }) { + if (requests.length < 1 || requests.length > 100) { + throw invalidInput("rpc", "log-batch-size"); + } + return Promise.all( + requests.map((filter) => + rpc(() => readFilteredLogs(batchClient, filter)) + ), + ); + }, + }); +} + +function productionRpcConfiguration(env: Environment) { + if (BROWSER_FORBIDDEN_RPC_NAMES.some((name) => env[name])) { + throw invalidInput("config", "browser-rpc-provider-url"); + } + const alchemyEndpoint = canonicalProjectorRpcEndpoint( + env.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL, + "alchemy", + ); + const quicknodeEndpoint = canonicalProjectorRpcEndpoint( + env.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL, + "quicknode", + ); + const alchemy = new URL(alchemyEndpoint); + const quicknode = new URL(quicknodeEndpoint); + if (alchemy.origin === quicknode.origin) { + throw invalidInput("config", "rpc-provider-independence"); + } + const schemaCommitment = projectorRpcSchemaCommitment(); + return Object.freeze({ + alchemy, + quicknode, + schemaCommitment, + alchemyDeploymentCommitment: projectorRpcDeploymentCommitment( + alchemy.toString(), + ), + quicknodeDeploymentCommitment: projectorRpcDeploymentCommitment( + quicknode.toString(), + ), + }); +} + +export function productionRpcProjectorCommitments( + env: Environment = process.env, +) { + const configuration = productionRpcConfiguration(env); + return Object.freeze({ + alchemy: Object.freeze({ + deploymentCommitment: configuration.alchemyDeploymentCommitment, + schemaCommitment: configuration.schemaCommitment, + }), + quicknode: Object.freeze({ + deploymentCommitment: configuration.quicknodeDeploymentCommitment, + schemaCommitment: configuration.schemaCommitment, + }), + }); +} + +/** + * Creates the only production provider pair accepted by the background + * projector. Provider identity is derived from strict endpoint validation, + * never from caller-supplied labels. + */ +export function createProductionDualRpcProviders( + env: Environment = process.env, +): readonly [CandidateRpcProvider, CandidateRpcProvider] { + const configuration = productionRpcConfiguration(env); + const { alchemy, quicknode } = configuration; + + const alchemyCommitment = configuration.alchemyDeploymentCommitment; + const quicknodeCommitment = configuration.quicknodeDeploymentCommitment; + const alchemyOriginCommitment = rpcProviderCommitment( + "origin", + alchemy.origin, + ); + const quicknodeOriginCommitment = rpcProviderCommitment( + "origin", + quicknode.origin, + ); + + const providers = Object.freeze([ + Object.freeze({ + identity: `alchemy-mainnet-${alchemyCommitment.slice(2, 34)}`, + vendorGroup: "alchemy", + endpointCommitment: alchemyCommitment, + endpointOriginCommitment: alchemyOriginCommitment, + client: candidateRpcClient(alchemy.toString()), + }), + Object.freeze({ + identity: `quicknode-mainnet-${quicknodeCommitment.slice(2, 34)}`, + vendorGroup: "quicknode", + endpointCommitment: quicknodeCommitment, + endpointOriginCommitment: quicknodeOriginCommitment, + client: candidateRpcClient(quicknode.toString()), + }), + ] as const); + PRODUCTION_PROVIDER_PAIRS.add(providers); + return providers; +} diff --git a/lib/data-pipeline/runtime-bytecode.ts b/lib/data-pipeline/runtime-bytecode.ts new file mode 100644 index 00000000..144389fa --- /dev/null +++ b/lib/data-pipeline/runtime-bytecode.ts @@ -0,0 +1,184 @@ +import { + bytesToHex, + concat, + encodeAbiParameters, + hexToBytes, + keccak256, + toBytes, + type Hex, +} from "viem"; + +import { invalidInput } from "./errors"; + +export type ImmutableReference = Readonly<{ + start: number; + length: number; +}>; + +const IMMUTABLE_REFERENCE_DOMAIN = toBytes( + "programmable:data-pipeline:immutable-references:v1\0", +); +const MAXIMUM_RUNTIME_BYTES = 24_576; +const MAXIMUM_IMMUTABLE_REFERENCES = 128; + +type CanonicalRuntimeBytecodeInput = Readonly<{ + runtimeBytecode: Hex; + runtimeBytes: Uint8Array; + expectedByteLength: number; + immutableReferences: readonly ImmutableReference[]; +}>; + +function runtimeBytes(value: Hex, expectedByteLength: number): Uint8Array { + if ( + typeof value !== "string" || + !/^0x(?:[0-9a-f]{2})+$/u.test(value) || + !Number.isSafeInteger(expectedByteLength) || + expectedByteLength < 1 || + expectedByteLength > MAXIMUM_RUNTIME_BYTES || + (value.length - 2) / 2 !== expectedByteLength + ) { + throw invalidInput("rpc", "runtime-bytecode"); + } + return hexToBytes(value); +} + +export function canonicalImmutableReferences( + value: readonly ImmutableReference[], + expectedByteLength: number, +): readonly ImmutableReference[] { + const referenceCount = Array.isArray(value) ? value.length : -1; + if ( + referenceCount < 1 || + referenceCount > MAXIMUM_IMMUTABLE_REFERENCES || + !Number.isSafeInteger(expectedByteLength) || + expectedByteLength < 1 || + expectedByteLength > MAXIMUM_RUNTIME_BYTES + ) { + throw invalidInput("rpc", "immutable-references"); + } + + let priorEnd = 0; + const references = new Array(referenceCount); + for (let index = 0; index < referenceCount; index += 1) { + const reference = value[index]; + if ( + reference === null || + typeof reference !== "object" || + Array.isArray(reference) + ) { + throw invalidInput("rpc", "immutable-references"); + } + const start = reference.start; + const length = reference.length; + if ( + !Number.isSafeInteger(start) || + !Number.isSafeInteger(length) || + start < 0 || + length < 1 || + length > 32 || + start + length > expectedByteLength || + (index > 0 && start < priorEnd) + ) { + throw invalidInput("rpc", "immutable-references"); + } + priorEnd = start + length; + references[index] = Object.freeze({ start, length }); + } + return Object.freeze(references); +} + +function canonicalRuntimeBytecodeInput(input: { + runtimeBytecode: Hex; + expectedByteLength: number; + immutableReferences: readonly ImmutableReference[]; +}): CanonicalRuntimeBytecodeInput { + if (input === null || typeof input !== "object") { + throw invalidInput("rpc", "runtime-bytecode"); + } + const runtimeBytecode = input.runtimeBytecode; + const expectedByteLength = input.expectedByteLength; + const immutableReferences = input.immutableReferences; + const bytes = runtimeBytes(runtimeBytecode, expectedByteLength); + return Object.freeze({ + runtimeBytecode: bytesToHex(bytes), + runtimeBytes: bytes, + expectedByteLength, + immutableReferences: canonicalImmutableReferences( + immutableReferences, + expectedByteLength, + ), + }); +} + +function immutableReferencesCommitmentFromCanonical( + references: readonly ImmutableReference[], + expectedByteLength: number, +): Hex { + return keccak256( + concat([ + IMMUTABLE_REFERENCE_DOMAIN, + encodeAbiParameters( + [ + { type: "uint32" }, + { type: "uint32[]" }, + { type: "uint32[]" }, + ], + [ + expectedByteLength, + references.map(({ start }) => start), + references.map(({ length }) => length), + ], + ), + ]), + ); +} + +function normalizedRuntimeBytecodeFromCanonical( + input: CanonicalRuntimeBytecodeInput, +): Hex { + const bytes = Uint8Array.from(input.runtimeBytes); + for (const { start, length } of input.immutableReferences) { + bytes.fill(0, start, start + length); + } + return bytesToHex(bytes); +} + +export function immutableReferencesCommitment( + value: readonly ImmutableReference[], + expectedByteLength: number, +): Hex { + const references = canonicalImmutableReferences(value, expectedByteLength); + return immutableReferencesCommitmentFromCanonical( + references, + expectedByteLength, + ); +} + +export function normalizeRuntimeBytecode(input: { + runtimeBytecode: Hex; + expectedByteLength: number; + immutableReferences: readonly ImmutableReference[]; +}): Hex { + return normalizedRuntimeBytecodeFromCanonical( + canonicalRuntimeBytecodeInput(input), + ); +} + +export function runtimeBytecodeEvidence(input: { + runtimeBytecode: Hex; + expectedByteLength: number; + immutableReferences: readonly ImmutableReference[]; +}) { + const canonical = canonicalRuntimeBytecodeInput(input); + const normalizedRuntimeBytecode = + normalizedRuntimeBytecodeFromCanonical(canonical); + return Object.freeze({ + exactRuntimeCodeHash: keccak256(canonical.runtimeBytecode), + normalizedRuntimeCodeHash: keccak256(normalizedRuntimeBytecode), + immutableReferencesCommitment: immutableReferencesCommitmentFromCanonical( + canonical.immutableReferences, + canonical.expectedByteLength, + ), + runtimeByteLength: canonical.expectedByteLength, + }); +} diff --git a/lib/data-pipeline/stock-paired-reconciler-contribution.ts b/lib/data-pipeline/stock-paired-reconciler-contribution.ts new file mode 100644 index 00000000..698dab67 --- /dev/null +++ b/lib/data-pipeline/stock-paired-reconciler-contribution.ts @@ -0,0 +1,422 @@ +import type { CanonicalJsonValue } from "./canonical-fingerprint"; +import { validationError } from "./errors"; + +export const STOCK_PAIRED_RECONCILER_CONTRIBUTION_CONTRACT = + "stock-paired-route-contribution-v1" as const; + +export type StockPairedReconcilerRelease = + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + +export type StockPairedReconcilerContribution = Readonly<{ + contractVersion: typeof STOCK_PAIRED_RECONCILER_CONTRIBUTION_CONTRACT; + releaseVersion: StockPairedReconcilerRelease; + modelId: "stock-paired"; + tokens: readonly CanonicalJsonValue[]; + charts: readonly CanonicalJsonValue[]; + profiles: readonly CanonicalJsonValue[]; + launches: readonly CanonicalJsonValue[]; +}>; + +type JsonRecord = Record; + +function fail(operation: string): never { + throw validationError("postgres", operation); +} + +function record(value: CanonicalJsonValue, operation: string): JsonRecord { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + fail(operation); + } + return value as JsonRecord; +} + +function exactKeys( + value: JsonRecord, + keys: readonly string[], + operation: string, +): void { + if (Object.keys(value).sort().join("\0") !== [...keys].sort().join("\0")) { + fail(operation); + } +} + +function text(value: CanonicalJsonValue, operation: string): string { + if (typeof value !== "string") fail(operation); + return value; +} + +function integerText(value: CanonicalJsonValue, operation: string): string { + const parsed = text(value, operation); + if (!/^(?:0|[1-9][0-9]{0,77})$/u.test(parsed)) fail(operation); + return parsed; +} + +function integer( + value: CanonicalJsonValue, + minimum: number, + maximum: number, + operation: string, +): number { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < minimum || + value > maximum + ) { + fail(operation); + } + return value; +} + +function hex( + value: CanonicalJsonValue, + bytes: number, + operation: string, +): string { + const parsed = text(value, operation); + if (!new RegExp(`^0x[0-9a-f]{${bytes * 2}}$`, "u").test(parsed)) { + fail(operation); + } + return parsed; +} + +function timestamp(value: CanonicalJsonValue, operation: string): string { + const parsed = text(value, operation); + if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(parsed)) { + fail(operation); + } + return parsed; +} + +function array( + value: CanonicalJsonValue, + operation: string, +): readonly CanonicalJsonValue[] { + if (!Array.isArray(value)) fail(operation); + return value; +} + +function assertIdentity( + row: JsonRecord, + releaseVersion: StockPairedReconcilerRelease, + operation: string, +): void { + if ( + row.releaseVersion !== releaseVersion || + row.modelId !== "stock-paired" + ) { + fail(operation); + } +} + +function token( + value: CanonicalJsonValue, + releaseVersion: StockPairedReconcilerRelease, +): JsonRecord { + const row = record(value, "stock-reconciler-token"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "tokenAddress", + "creatorAddress", + "launchTransactionHash", + "launchBlockNumber", + "launchTransactionIndex", + "launchLogIndex", + "launchedAt", + "poolId", + "hookAddress", + "quoteAssetAddress", + "rewardVaultAddress", + "positionRecipient", + "positionTokenId", + "launchHash", + "name", + "symbol", + "decimals", + "totalSupplyRaw", + "fees", + "liquidity", + ], "stock-reconciler-token-fields"); + assertIdentity(row, releaseVersion, "stock-reconciler-token-identity"); + hex(row.tokenAddress, 20, "stock-reconciler-token-address"); + hex(row.creatorAddress, 20, "stock-reconciler-token-creator"); + hex(row.launchTransactionHash, 32, "stock-reconciler-token-transaction"); + integerText(row.launchBlockNumber, "stock-reconciler-token-block"); + integer(row.launchTransactionIndex, 0, Number.MAX_SAFE_INTEGER, + "stock-reconciler-token-transaction-index"); + integer(row.launchLogIndex, 0, Number.MAX_SAFE_INTEGER, + "stock-reconciler-token-log-index"); + timestamp(row.launchedAt, "stock-reconciler-token-time"); + hex(row.poolId, 32, "stock-reconciler-token-pool"); + hex(row.hookAddress, 20, "stock-reconciler-token-hook"); + hex(row.quoteAssetAddress, 20, "stock-reconciler-token-quote"); + hex(row.rewardVaultAddress, 20, "stock-reconciler-token-vault"); + hex(row.positionRecipient, 20, "stock-reconciler-token-recipient"); + integerText(row.positionTokenId, "stock-reconciler-token-position"); + hex(row.launchHash, 32, "stock-reconciler-token-launch-hash"); + text(row.name, "stock-reconciler-token-name"); + text(row.symbol, "stock-reconciler-token-symbol"); + integer(row.decimals, 0, 255, "stock-reconciler-token-decimals"); + integerText(row.totalSupplyRaw, "stock-reconciler-token-supply"); + + const fees = record(row.fees, "stock-reconciler-token-fees"); + exactKeys(fees, [ + "buySwapFeeBps", + "sellSwapFeeBps", + "buyCreatorFeeBps", + "sellCreatorFeeBps", + "launcherFeeBps", + "transferTaxBps", + "lpFeePips", + ], "stock-reconciler-token-fee-fields"); + integer(fees.buySwapFeeBps, 0, 10_000, "stock-reconciler-buy-fee"); + integer(fees.sellSwapFeeBps, 0, 10_000, "stock-reconciler-sell-fee"); + integer(fees.buyCreatorFeeBps, 0, 10_000, + "stock-reconciler-buy-creator-fee"); + integer(fees.sellCreatorFeeBps, 0, 10_000, + "stock-reconciler-sell-creator-fee"); + integer(fees.launcherFeeBps, 0, 10_000, + "stock-reconciler-launcher-fee"); + integer(fees.transferTaxBps, 0, 10_000, + "stock-reconciler-transfer-tax"); + integer(fees.lpFeePips, 0, 1_000_000, + "stock-reconciler-lp-fee"); + + const liquidity = record(row.liquidity, "stock-reconciler-liquidity"); + exactKeys(liquidity, [ + "tokenLiquidityAmountRaw", + "lockedTokenDustRaw", + "initialTick", + "tickLower", + "tickUpper", + ], "stock-reconciler-liquidity-fields"); + integerText(liquidity.tokenLiquidityAmountRaw, + "stock-reconciler-token-liquidity"); + integerText(liquidity.lockedTokenDustRaw, + "stock-reconciler-locked-dust"); + integer(liquidity.initialTick, -887_272, 887_272, + "stock-reconciler-initial-tick"); + integer(liquidity.tickLower, -887_272, 887_272, + "stock-reconciler-lower-tick"); + integer(liquidity.tickUpper, -887_272, 887_272, + "stock-reconciler-upper-tick"); + return row; +} + +function chart( + value: CanonicalJsonValue, + releaseVersion: StockPairedReconcilerRelease, +): JsonRecord { + const row = record(value, "stock-reconciler-chart"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "tokenAddress", + "poolId", + "quoteAssetAddress", + "state", + "volume", + ], "stock-reconciler-chart-fields"); + assertIdentity(row, releaseVersion, "stock-reconciler-chart-identity"); + hex(row.tokenAddress, 20, "stock-reconciler-chart-token"); + hex(row.poolId, 32, "stock-reconciler-chart-pool"); + const quoteAssetAddress = hex( + row.quoteAssetAddress, + 20, + "stock-reconciler-chart-quote", + ); + const state = record(row.state, "stock-reconciler-chart-state"); + exactKeys(state, [ + "blockNumber", + "blockHash", + "transactionHash", + "transactionIndex", + "logIndex", + "sqrtPriceX96", + "liquidity", + "tick", + "lpFeePips", + ], "stock-reconciler-chart-state-fields"); + integerText(state.blockNumber, "stock-reconciler-chart-block"); + hex(state.blockHash, 32, "stock-reconciler-chart-block-hash"); + hex(state.transactionHash, 32, "stock-reconciler-chart-transaction"); + integer(state.transactionIndex, 0, Number.MAX_SAFE_INTEGER, + "stock-reconciler-chart-transaction-index"); + integer(state.logIndex, 0, Number.MAX_SAFE_INTEGER, + "stock-reconciler-chart-log-index"); + integerText(state.sqrtPriceX96, "stock-reconciler-chart-price"); + integerText(state.liquidity, "stock-reconciler-chart-liquidity"); + integer(state.tick, -887_272, 887_272, "stock-reconciler-chart-tick"); + integer(state.lpFeePips, 0, 1_000_000, + "stock-reconciler-chart-lp-fee"); + const volume = record(row.volume, "stock-reconciler-chart-volume"); + exactKeys(volume, [ + "quoteAssetAddress", + "grossQuoteRaw", + "creatorFeeQuoteRaw", + "launcherFeeQuoteRaw", + ], "stock-reconciler-chart-volume-fields"); + if ( + hex(volume.quoteAssetAddress, 20, "stock-reconciler-volume-quote") !== + quoteAssetAddress + ) { + fail("stock-reconciler-volume-quote-mismatch"); + } + integerText(volume.grossQuoteRaw, "stock-reconciler-chart-gross"); + integerText(volume.creatorFeeQuoteRaw, + "stock-reconciler-chart-creator-fee"); + integerText(volume.launcherFeeQuoteRaw, + "stock-reconciler-chart-launcher-fee"); + return row; +} + +function tokenReference( + value: CanonicalJsonValue, + releaseVersion: StockPairedReconcilerRelease, +): JsonRecord { + const row = record(value, "stock-reconciler-token-reference"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "tokenAddress", + "launchTransactionHash", + ], "stock-reconciler-token-reference-fields"); + assertIdentity(row, releaseVersion, + "stock-reconciler-token-reference-identity"); + hex(row.tokenAddress, 20, "stock-reconciler-token-reference-address"); + hex(row.launchTransactionHash, 32, + "stock-reconciler-token-reference-transaction"); + return row; +} + +function profile( + value: CanonicalJsonValue, + releaseVersion: StockPairedReconcilerRelease, +): JsonRecord { + const row = record(value, "stock-reconciler-profile"); + exactKeys(row, ["account", "tokens"], "stock-reconciler-profile-fields"); + hex(row.account, 20, "stock-reconciler-profile-account"); + array(row.tokens, "stock-reconciler-profile-tokens").forEach((entry) => + tokenReference(entry, releaseVersion)); + return row; +} + +function lookup( + value: CanonicalJsonValue, + releaseVersion: StockPairedReconcilerRelease, +): JsonRecord { + const row = record(value, "stock-reconciler-lookup"); + exactKeys(row, [ + "releaseVersion", + "modelId", + "account", + "launchTransactionHash", + "tokenAddress", + ], "stock-reconciler-lookup-fields"); + assertIdentity(row, releaseVersion, "stock-reconciler-lookup-identity"); + hex(row.account, 20, "stock-reconciler-lookup-account"); + hex(row.launchTransactionHash, 32, + "stock-reconciler-lookup-transaction"); + hex(row.tokenAddress, 20, "stock-reconciler-lookup-token"); + return row; +} + +export function assertStockPairedReconcilerContribution( + contribution: StockPairedReconcilerContribution, +): StockPairedReconcilerContribution { + const count = contribution.tokens.length; + if ( + contribution.contractVersion !== + STOCK_PAIRED_RECONCILER_CONTRIBUTION_CONTRACT || + contribution.modelId !== "stock-paired" || + !["stock-paired-v1", "stock-paired-v2", "stock-paired-v3"].includes( + contribution.releaseVersion, + ) || + count < 1 || + contribution.charts.length !== count || + contribution.launches.length !== count + ) { + fail("stock-reconciler-contribution-cardinality"); + } + const tokens = contribution.tokens.map((entry) => + token(entry, contribution.releaseVersion)); + const charts = contribution.charts.map((entry) => + chart(entry, contribution.releaseVersion)); + const launches = contribution.launches.map((entry) => + lookup(entry, contribution.releaseVersion)); + const profiles = contribution.profiles.map((entry) => + profile(entry, contribution.releaseVersion)); + const profileCount = contribution.profiles.reduce( + (sum: number, entry) => sum + array( + record(entry, "stock-reconciler-profile-count").tokens, + "stock-reconciler-profile-count-tokens", + ).length, + 0, + ); + if (profileCount !== count) { + fail("stock-reconciler-profile-cardinality"); + } + const tokenAddresses = new Set(); + const poolIds = new Set(); + const transactionHashes = new Set(); + const profiledTokens = new Set(); + const profileAccounts = new Set(); + for (let index = 0; index < count; index += 1) { + const currentToken = tokens[index]!; + const currentChart = charts[index]!; + const currentLaunch = launches[index]!; + if ( + tokenAddresses.has(currentToken.tokenAddress as string) || + poolIds.has(currentToken.poolId as string) || + transactionHashes.has(currentToken.launchTransactionHash as string) || + currentToken.tokenAddress !== currentChart.tokenAddress || + currentToken.poolId !== currentChart.poolId || + currentToken.quoteAssetAddress !== currentChart.quoteAssetAddress || + currentToken.tokenAddress !== currentLaunch.tokenAddress || + currentToken.launchTransactionHash !== + currentLaunch.launchTransactionHash || + currentToken.creatorAddress !== currentLaunch.account + ) { + fail("stock-reconciler-cross-route-identity"); + } + tokenAddresses.add(currentToken.tokenAddress as string); + poolIds.add(currentToken.poolId as string); + transactionHashes.add(currentToken.launchTransactionHash as string); + } + const tokensByAddress = new Map( + tokens.map((entry) => [entry.tokenAddress as string, entry]), + ); + for (const currentProfile of profiles) { + const account = currentProfile.account as string; + if (profileAccounts.has(account)) { + fail("stock-reconciler-profile-account-cardinality"); + } + profileAccounts.add(account); + const references = currentProfile.tokens as CanonicalJsonValue[]; + for (const reference of references) { + const currentReference = record( + reference, + "stock-reconciler-profile-reference", + ); + const tokenAddress = currentReference.tokenAddress as string; + const sourceToken = tokensByAddress.get(tokenAddress); + if ( + !sourceToken || + profiledTokens.has(tokenAddress) || + sourceToken.creatorAddress !== account || + sourceToken.launchTransactionHash !== + currentReference.launchTransactionHash + ) { + fail("stock-reconciler-profile-cross-route-identity"); + } + profiledTokens.add(tokenAddress); + } + } + if (profiledTokens.size !== count) { + fail("stock-reconciler-profile-token-cardinality"); + } + return contribution; +} diff --git a/lib/data-pipeline/stock-paired-reconciler-route-builder.server.ts b/lib/data-pipeline/stock-paired-reconciler-route-builder.server.ts new file mode 100644 index 00000000..638193e7 --- /dev/null +++ b/lib/data-pipeline/stock-paired-reconciler-route-builder.server.ts @@ -0,0 +1,1747 @@ +import "server-only"; + +import { + decodeEventLog, + decodeFunctionData, + decodeFunctionResult, + encodeAbiParameters, + encodeFunctionData, + getAddress, + isAddress, + keccak256, + parseAbi, + parseAbiItem, + parseAbiParameters, + toEventSelector, + type Abi, + type AbiEvent, + type Address, + type Hex, +} from "viem"; + +import { + getStockPairedExpectedInitialTickForRelease, + getStockPairedQuoteAssetForRelease, + stockFeeSplitVaultAbi, + stockPairedEthLaunchCoordinatorAbi, + stockPairedHookAbi, + stockQuoteRegistryAbi, + STOCK_PAIRED_CREATOR_FEE_BPS, + STOCK_PAIRED_PROGRAMMABLE_FEE_BPS, + STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, +} from "../stock-paired"; +import { + resolveVerifiedStockPairedRelease, + resolveVerifiedStockPairedV2Release, + resolveVerifiedStockPairedV3Release, + type VerifiedStockPairedRelease, +} from "../stock-paired-release"; +import { stateViewReadAbi, uerc20ReadAbi } from "../onchain/abis"; +import type { CanonicalJsonValue } from "./canonical-fingerprint"; +import { canonicalBytes32, type HexBytes32 } from "./codecs"; +import { dataPipelineError, invalidInput, validationError } from "./errors"; +import { + assembleReconcilerCorpusPages, + createReconcilerCorpusManifest, +} from "./reconciler-corpus-partitions"; +import type { + ExactBlockRpcClient, + ExactBlockRpcLog, + ExactBlockRpcReceipt, + ExactBlockRpcTransaction, +} from "./reconciler-exact-block-reader.server"; +import type { + ReconcilerPreParityContract, + ReconcilerRouteKey, +} from "./reconciler-preparity"; +import { + assertStockPairedReconcilerContribution, + STOCK_PAIRED_RECONCILER_CONTRIBUTION_CONTRACT, + type StockPairedReconcilerContribution, + type StockPairedReconcilerRelease, +} from "./stock-paired-reconciler-contribution"; + +export const STOCK_PAIRED_RECONCILER_LOG_BLOCK_RANGE = 10_000n; +export const STOCK_PAIRED_RECONCILER_ROUTE_KEYS = Object.freeze([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "launch-lookup", +] as const satisfies readonly ReconcilerRouteKey[]); +const MAXIMUM_LOGS_PER_REQUEST = 20_000; +const MAXIMUM_POOLS_PER_REQUEST = 64; +const CALLS_PER_LAUNCH = 27; +const MAXIMUM_USABLE_TICK = 887_200; +const MINIMUM_USABLE_TICK = -887_200; +const TOKEN_SUPPLY = 1_000_000_000n * 10n ** 18n; + +const launchedEvent = parseAbiItem( + "event StockPairedTokenLaunched(address indexed deployer,address indexed token,address indexed quoteAsset,bytes32 poolId,address rewardVault,address positionRecipient,uint256 positionTokenId,bytes32 launchHash)", +); +const liquidityEvent = parseAbiItem( + "event StockPairedLiquidityConfigured(address indexed token,address indexed quoteAsset,uint256 totalSupply,uint256 tokenLiquidityAmount,uint256 lockedTokenDust,int24 initialTick,int24 tickLower,int24 tickUpper,uint24 lpFeePips,bytes32 launchHash)", +); +const initialBuyEvent = parseAbiItem( + "event StockPairedCreatorInitialBuy(address indexed deployer,address indexed token,address indexed quoteAsset,bytes32 poolId,uint256 quoteAmount,uint256 tokenAmount,bytes32 launchHash)", +); +const ethLaunchEvent = parseAbiItem( + "event StockPairedEthTokenLaunched(address indexed creator,address indexed token,address indexed quoteAsset,uint256 initialBuyEthAmount,uint256 initialBuyQuoteAmount,uint256 initialBuyTokenAmount,bytes32 launchHash)", +); +const poolRegisteredEvent = parseAbiItem( + "event PoolRegistered(bytes32 indexed poolId,address indexed token,address indexed quoteAsset,address rewardVault,address registrar,bool quoteIsCurrency0,bytes32 rewardConfigurationHash,bytes32 quoteConfigurationHash)", +); +const feeDisclosureEvent = parseAbiItem( + "event PoolFeeDisclosure(bytes32 indexed poolId,address indexed token,address indexed quoteAsset,address rewardVault,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,uint16 creatorFeeBps,uint16 launcherFeeBps,uint16 transferTaxBps,uint24 lpFeePips)", +); +const feeAccruedEvent = parseAbiItem( + "event QuoteSwapFeesAccrued(bytes32 indexed poolId,address indexed swapSender,address indexed quoteAsset,bool isBuy,uint256 grossQuoteAmount,uint256 creatorFee,uint256 launcherFee)", +); +const vaultDeployedEvent = parseAbiItem( + "event QuoteAssetFeeSplitVaultDeployed(address indexed vault,address indexed feeHook,bytes32 indexed poolId,address quoteAsset)", +); +const swapEvent = parseAbiItem( + "event Swap(bytes32 indexed id,address indexed sender,int128 amount0,int128 amount1,uint160 sqrtPriceX96,uint128 liquidity,int24 tick,uint24 fee)", +); + +const launcherStateAbi = parseAbi([ + "function launchHashOf(address token) view returns (bytes32)", + "function rewardVaultOf(address token) view returns (address)", + "function quoteAssetOf(address token) view returns (address)", +]); +const rewardVaultFactoryStateAbi = parseAbi([ + "function isFactoryVault(address vault) view returns (bool)", + "function configurationHashOf(address vault) view returns (bytes32)", +]); +const positionForwarderFactoryStateAbi = parseAbi([ + "function isFactoryForwarder(address forwarder) view returns (bool)", + "function configurationHashOf(address forwarder) view returns (bytes32)", +]); + +const REWARD_CONFIGURATION_PARAMETERS = parseAbiParameters( + "uint256 chainId,address vault,address feeHook,address poolManager,address quoteAsset,bytes32 poolId,address[] beneficiaries,uint16[] sharesBps", +); +const POOL_KEY_PARAMETERS = parseAbiParameters( + "address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks", +); + +const LAUNCHER_EVENTS = Object.freeze([ + launchedEvent, + liquidityEvent, + initialBuyEvent, +]); +const HOOK_EVENTS = Object.freeze([ + poolRegisteredEvent, + feeDisclosureEvent, + feeAccruedEvent, +]); + +type Json = CanonicalJsonValue; +type JsonRecord = Record; + +type DecodedLog = Readonly<{ + eventName: string; + args: Readonly>; + log: ExactBlockRpcLog; +}>; + +type CallSpec = Readonly<{ + to: Address; + data: Hex; + decode: (data: Hex) => unknown; +}>; + +type StockLaunch = Readonly<{ + coordinator: Address; + token: Address; + quoteAsset: Address; + poolId: HexBytes32; + rewardVault: Address; + positionRecipient: Address; + positionTokenId: bigint; + launchHash: HexBytes32; + blockNumber: bigint; + blockHash: HexBytes32; + transactionHash: HexBytes32; + transactionIndex: number; + blockGlobalLogIndex: number; + log: ExactBlockRpcLog; +}>; + +type Companions = Readonly<{ + liquidity: DecodedLog; + initialBuy: DecodedLog; + ethLaunch: DecodedLog; + registration: DecodedLog; + disclosure: DecodedLog; + vaultDeployment: DecodedLog; +}>; + +type LaunchInput = Readonly<{ + creator: Address; + receiptLogIndex: number; + value: bigint; + name: string; + symbol: string; + creatorSalt: HexBytes32; + description: string; + website: string; + image: string; + extraData: Hex; + beneficiaries: readonly Address[]; + sharesBps: readonly number[]; + minimumQuoteAmountOut: bigint; + minimumInitialTokenOut: bigint; +}>; + +export type StockPairedExactBlockContributionBuilder = (input: Readonly<{ + rpc: ExactBlockRpcClient; + contract: ReconcilerPreParityContract; + blockNumber: bigint; + blockHash: HexBytes32; + signal: AbortSignal; +}>) => Promise; + +function fail(operation: string): never { + throw validationError("uniswap", operation); +} + +function lowerAddress(value: Address): string { + return value.toLowerCase(); +} + +function sameHex(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase(); +} + +function exactAddress(value: unknown, operation: string): Address { + if (typeof value !== "string" || !isAddress(value)) fail(operation); + return getAddress(value); +} + +function exactBytes32(value: unknown, operation: string): HexBytes32 { + try { + return canonicalBytes32(value); + } catch { + return fail(operation); + } +} + +function exactData(value: unknown, operation: string): Hex { + if (typeof value !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/u.test(value)) { + fail(operation); + } + return value.toLowerCase() as Hex; +} + +function exactText(value: unknown, operation: string): string { + if (typeof value !== "string") fail(operation); + return value; +} + +function record(value: unknown, operation: string): JsonRecord { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + fail(operation); + } + return value as JsonRecord; +} + +function array(value: unknown, operation: string): readonly unknown[] { + if (!Array.isArray(value)) fail(operation); + return value; +} + +function tuple( + value: unknown, + length: number, + operation: string, +): readonly unknown[] { + if (!Array.isArray(value) || value.length !== length) fail(operation); + return value; +} + +function integer(value: unknown, operation: string): bigint { + if (typeof value === "bigint") return value; + if (typeof value === "number" && Number.isSafeInteger(value)) { + return BigInt(value); + } + return fail(operation); +} + +function nonnegative(value: unknown, operation: string): bigint { + const parsed = integer(value, operation); + if (parsed < 0n) fail(operation); + return parsed; +} + +function safeInteger( + value: unknown, + minimum: number, + maximum: number, + operation: string, +): number { + const parsed = integer(value, operation); + if (parsed < BigInt(minimum) || parsed > BigInt(maximum)) fail(operation); + return Number(parsed); +} + +function callSpec( + to: Address, + abi: Abi, + functionName: string, + args: readonly unknown[] = [], +): CallSpec { + const request = { abi, functionName, args } as never; + return Object.freeze({ + to, + data: encodeFunctionData(request), + decode: (data: Hex) => decodeFunctionResult({ + abi, + functionName, + data, + } as never) as unknown, + }); +} + +export function stockPairedReconcilerBlockRanges( + fromBlock: bigint, + toBlock: bigint, +): readonly Readonly<{ fromBlock: bigint; toBlock: bigint }>[] { + if (fromBlock < 0n || toBlock < fromBlock) { + throw invalidInput("rpc", "stock-reconciler-log-range"); + } + const ranges: Array> = []; + for ( + let start = fromBlock; + start <= toBlock; + start += STOCK_PAIRED_RECONCILER_LOG_BLOCK_RANGE + ) { + const end = start + STOCK_PAIRED_RECONCILER_LOG_BLOCK_RANGE - 1n; + ranges.push(Object.freeze({ + fromBlock: start, + toBlock: end > toBlock ? toBlock : end, + })); + } + return Object.freeze(ranges); +} + +function eventMap(events: readonly AbiEvent[]) { + return new Map( + events.map((event) => [toEventSelector(event).toLowerCase(), event]), + ); +} + +function decodeKnownEvent( + events: ReadonlyMap, + log: ExactBlockRpcLog, +): DecodedLog { + const event = events.get((log.topics[0] ?? "").toLowerCase()); + if (!event) fail("stock-reconciler-log-selector"); + let decoded: ReturnType; + try { + decoded = decodeEventLog({ + abi: [event], + data: log.data, + topics: log.topics as [Hex, ...Hex[]], + strict: true, + }); + } catch { + return fail("stock-reconciler-log-decode"); + } + if ( + decoded.args === null || + typeof decoded.args !== "object" || + Array.isArray(decoded.args) + ) { + fail("stock-reconciler-log-args"); + } + return Object.freeze({ + eventName: decoded.eventName, + args: decoded.args as Readonly>, + log, + }); +} + +async function readUncappedLogs(input: Readonly<{ + rpc: ExactBlockRpcClient; + addresses: Address | readonly Address[]; + topics: readonly (Hex | readonly Hex[] | null)[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}>): Promise { + const logs = await input.rpc.getLogs({ + addresses: input.addresses, + topics: input.topics, + fromBlock: input.fromBlock, + toBlock: input.toBlock, + maximumLogs: MAXIMUM_LOGS_PER_REQUEST, + signal: input.signal, + }); + if (logs.length < MAXIMUM_LOGS_PER_REQUEST) return logs; + if (input.fromBlock === input.toBlock) { + throw dataPipelineError({ + dependency: "rpc", + code: "response_oversize", + retryable: false, + countsTowardCircuit: true, + metadata: { operation: "stock-reconciler-single-block-log-boundary" }, + }); + } + const midpoint = input.fromBlock + (input.toBlock - input.fromBlock) / 2n; + const [left, right] = await Promise.all([ + readUncappedLogs({ ...input, toBlock: midpoint }), + readUncappedLogs({ ...input, fromBlock: midpoint + 1n }), + ]); + return Object.freeze([...left, ...right]); +} + +async function readLogsInRanges(input: Readonly<{ + rpc: ExactBlockRpcClient; + addresses: Address | readonly Address[]; + events: readonly AbiEvent[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}>): Promise { + if (input.toBlock < input.fromBlock) return Object.freeze([]); + const selectors = eventMap(input.events); + const allowedAddresses = new Set( + (Array.isArray(input.addresses) ? input.addresses : [input.addresses]) + .map((address) => lowerAddress(address)), + ); + const output: DecodedLog[] = []; + for (const range of stockPairedReconcilerBlockRanges( + input.fromBlock, + input.toBlock, + )) { + const logs = await readUncappedLogs({ + rpc: input.rpc, + addresses: input.addresses, + topics: [[...selectors.keys()] as Hex[]], + fromBlock: range.fromBlock, + toBlock: range.toBlock, + signal: input.signal, + }); + if (logs.some((log) => + !allowedAddresses.has(lowerAddress(log.address)) || + !selectors.has((log.topics[0] ?? "").toLowerCase()) || + log.blockNumber < range.fromBlock || + log.blockNumber > range.toBlock + )) { + fail("stock-reconciler-log-filter-binding"); + } + output.push(...logs.map((log) => decodeKnownEvent(selectors, log))); + } + for (let index = 1; index < output.length; index += 1) { + const left = output[index - 1]!.log; + const right = output[index]!.log; + if ( + right.blockNumber < left.blockNumber || + (right.blockNumber === left.blockNumber && + (right.transactionIndex < left.transactionIndex || + (right.transactionIndex === left.transactionIndex && + right.logIndex <= left.logIndex))) + ) { + fail("stock-reconciler-log-order"); + } + } + return Object.freeze(output); +} + +async function readPoolSwapLogs(input: Readonly<{ + rpc: ExactBlockRpcClient; + poolManager: Address; + poolIds: readonly HexBytes32[]; + fromBlock: bigint; + toBlock: bigint; + signal: AbortSignal; +}>): Promise { + const selectorMap = eventMap([swapEvent]); + const output: DecodedLog[] = []; + for ( + let offset = 0; + offset < input.poolIds.length; + offset += MAXIMUM_POOLS_PER_REQUEST + ) { + const poolIds = input.poolIds.slice( + offset, + offset + MAXIMUM_POOLS_PER_REQUEST, + ); + for (const range of stockPairedReconcilerBlockRanges( + input.fromBlock, + input.toBlock, + )) { + const logs = await readUncappedLogs({ + rpc: input.rpc, + addresses: input.poolManager, + topics: [toEventSelector(swapEvent), poolIds], + fromBlock: range.fromBlock, + toBlock: range.toBlock, + signal: input.signal, + }); + const allowedPoolIds = new Set( + poolIds.map((poolId) => poolId.toLowerCase()), + ); + if (logs.some((log) => + !sameHex(log.address, input.poolManager) || + log.blockNumber < range.fromBlock || + log.blockNumber > range.toBlock || + !sameHex(log.topics[0] ?? "0x", toEventSelector(swapEvent)) || + !allowedPoolIds.has((log.topics[1] ?? "").toLowerCase()) + )) { + fail("stock-reconciler-swap-log-filter-binding"); + } + output.push(...logs.map((log) => decodeKnownEvent(selectorMap, log))); + } + } + output.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + return Object.freeze(output); +} + +async function readCalls( + rpc: ExactBlockRpcClient, + calls: readonly CallSpec[], + blockHash: HexBytes32, + signal: AbortSignal, +): Promise { + const results = await rpc.callMany({ + calls: calls.map(({ to, data }) => Object.freeze({ to, data })), + blockHash, + signal, + }); + if (results.length !== calls.length) fail("stock-reconciler-call-cardinality"); + return Object.freeze(results.map((result, index) => + calls[index]!.decode(result))); +} + +function configuredRelease( + releaseVersion: StockPairedReconcilerRelease, +): VerifiedStockPairedRelease { + const release = releaseVersion === "stock-paired-v1" + ? resolveVerifiedStockPairedRelease() + : releaseVersion === "stock-paired-v2" + ? resolveVerifiedStockPairedV2Release() + : resolveVerifiedStockPairedV3Release(); + if (!release || release.internalContractRelease !== releaseVersion) { + fail("stock-reconciler-release-manifest"); + } + return release; +} + +function resolveRelease( + contract: ReconcilerPreParityContract, + releaseVersion: StockPairedReconcilerRelease, +): VerifiedStockPairedRelease { + if ( + contract.chainId !== "1" || + contract.releaseId !== releaseVersion || + contract.modelId !== "stock-paired" || + contract.routeKeys.length !== STOCK_PAIRED_RECONCILER_ROUTE_KEYS.length || + contract.routeKeys.some( + (routeKey, index) => + routeKey !== STOCK_PAIRED_RECONCILER_ROUTE_KEYS[index], + ) + ) { + throw invalidInput("config", "stock-reconciler-release"); + } + return configuredRelease(releaseVersion); +} + +async function assertRuntime(input: Readonly<{ + rpc: ExactBlockRpcClient; + release: VerifiedStockPairedRelease; + blockHash: HexBytes32; + signal: AbortSignal; +}>): Promise { + const runtimeFields = [ + "quoteRegistry", + "positionPlanner", + "feeSplitVaultFactory", + "hookFactory", + "feeHook", + "launcher", + "ethLaunchCoordinator", + "positionForwarderFactory", + ] as const; + const runtime = [ + ...runtimeFields.map((label) => ({ + label, + address: input.release.addresses[label], + expectedHash: input.release.runtimeCodeHashes[label], + })), + ...Object.entries(input.release.officialDependencies) + .map(([label, dependency]) => ({ + label, + address: dependency.address, + expectedHash: dependency.runtimeCodeHash, + })), + { + label: "issuerBeacon", + address: input.release.issuerRuntime.beacon, + expectedHash: input.release.issuerRuntime.beaconRuntimeCodeHash, + }, + { + label: "issuerImplementation", + address: input.release.issuerRuntime.implementation, + expectedHash: input.release.issuerRuntime.implementationRuntimeCodeHash, + }, + ...(input.release.issuerRuntime.gmTokenManager && + input.release.issuerRuntime.gmTokenManagerRuntimeCodeHash + ? [{ + label: "issuerGmTokenManager", + address: input.release.issuerRuntime.gmTokenManager, + expectedHash: + input.release.issuerRuntime.gmTokenManagerRuntimeCodeHash, + }] + : []), + ]; + for (const item of runtime) { + const codeHash = await input.rpc.getCodeHash({ + address: item.address, + blockHash: input.blockHash, + signal: input.signal, + }); + if (!sameHex(codeHash, item.expectedHash)) { + fail(`stock-reconciler-runtime-${item.label}`); + } + } +} + +function oneByKey( + values: readonly DecodedLog[], + key: (value: DecodedLog) => string, + operation: string, +): ReadonlyMap { + const output = new Map(); + for (const value of values) { + const identity = key(value).toLowerCase(); + if (output.has(identity)) fail(operation); + output.set(identity, value); + } + return output; +} + +function launchRecords( + logs: readonly DecodedLog[], + release: VerifiedStockPairedRelease, +): readonly StockLaunch[] { + const launched = logs.filter(({ eventName }) => + eventName === "StockPairedTokenLaunched"); + if (launched.length < 1) { + fail("stock-reconciler-launch-cardinality"); + } + const tokens = new Set(); + const pools = new Set(); + const output = launched.map(({ args, log }) => { + const coordinator = exactAddress(args.deployer, + "stock-reconciler-launch-coordinator"); + const token = exactAddress(args.token, "stock-reconciler-launch-token"); + const quoteAsset = exactAddress( + args.quoteAsset, + "stock-reconciler-launch-quote", + ); + const poolId = exactBytes32(args.poolId, "stock-reconciler-launch-pool"); + if ( + !sameHex(coordinator, release.addresses.ethLaunchCoordinator) || + !getStockPairedQuoteAssetForRelease(release, quoteAsset) || + tokens.has(lowerAddress(token)) || + pools.has(poolId) + ) { + fail("stock-reconciler-launch-identity"); + } + tokens.add(lowerAddress(token)); + pools.add(poolId); + return Object.freeze({ + coordinator, + token, + quoteAsset, + poolId, + rewardVault: exactAddress(args.rewardVault, + "stock-reconciler-launch-vault"), + positionRecipient: exactAddress( + args.positionRecipient, + "stock-reconciler-launch-position-recipient", + ), + positionTokenId: nonnegative( + args.positionTokenId, + "stock-reconciler-position-token-id", + ), + launchHash: exactBytes32(args.launchHash, + "stock-reconciler-launch-hash"), + blockNumber: log.blockNumber, + blockHash: log.blockHash, + transactionHash: log.transactionHash, + transactionIndex: log.transactionIndex, + blockGlobalLogIndex: log.logIndex, + log, + }); + }); + output.sort((left, right) => + left.blockNumber === right.blockNumber + ? left.transactionIndex === right.transactionIndex + ? left.blockGlobalLogIndex - right.blockGlobalLogIndex + : left.transactionIndex - right.transactionIndex + : left.blockNumber < right.blockNumber ? -1 : 1 + ); + return Object.freeze(output); +} + +function sameTransaction(event: DecodedLog, launch: StockLaunch): boolean { + return sameHex(event.log.blockHash, launch.blockHash) && + sameHex(event.log.transactionHash, launch.transactionHash) && + event.log.blockNumber === launch.blockNumber && + event.log.transactionIndex === launch.transactionIndex; +} + +function companions(input: Readonly<{ + launches: readonly StockLaunch[]; + launcherLogs: readonly DecodedLog[]; + coordinatorLogs: readonly DecodedLog[]; + hookLogs: readonly DecodedLog[]; + factoryLogs: readonly DecodedLog[]; + release: VerifiedStockPairedRelease; +}>): ReadonlyMap { + const liquidity = oneByKey( + input.launcherLogs.filter(({ eventName }) => + eventName === "StockPairedLiquidityConfigured"), + ({ args }) => lowerAddress(exactAddress( + args.token, + "stock-reconciler-liquidity-token", + )), + "stock-reconciler-liquidity-cardinality", + ); + const initialBuy = oneByKey( + input.launcherLogs.filter(({ eventName }) => + eventName === "StockPairedCreatorInitialBuy"), + ({ args }) => lowerAddress(exactAddress( + args.token, + "stock-reconciler-initial-buy-token", + )), + "stock-reconciler-initial-buy-cardinality", + ); + const ethLaunch = oneByKey( + input.coordinatorLogs.filter(({ eventName }) => + eventName === "StockPairedEthTokenLaunched"), + ({ args }) => lowerAddress(exactAddress( + args.token, + "stock-reconciler-eth-launch-token", + )), + "stock-reconciler-eth-launch-cardinality", + ); + const registrations = oneByKey( + input.hookLogs.filter(({ eventName }) => eventName === "PoolRegistered"), + ({ args }) => exactBytes32( + args.poolId, + "stock-reconciler-registration-pool", + ), + "stock-reconciler-registration-cardinality", + ); + const disclosures = oneByKey( + input.hookLogs.filter(({ eventName }) => eventName === "PoolFeeDisclosure"), + ({ args }) => exactBytes32( + args.poolId, + "stock-reconciler-disclosure-pool", + ), + "stock-reconciler-disclosure-cardinality", + ); + const vaultDeployments = oneByKey( + input.factoryLogs.filter(({ eventName }) => + eventName === "QuoteAssetFeeSplitVaultDeployed"), + ({ args }) => lowerAddress(exactAddress( + args.vault, + "stock-reconciler-deployed-vault", + )), + "stock-reconciler-vault-deployment-cardinality", + ); + const output = new Map(); + for (const launch of input.launches) { + const tokenKey = lowerAddress(launch.token); + const values = { + liquidity: liquidity.get(tokenKey), + initialBuy: initialBuy.get(tokenKey), + ethLaunch: ethLaunch.get(tokenKey), + registration: registrations.get(launch.poolId), + disclosure: disclosures.get(launch.poolId), + vaultDeployment: vaultDeployments.get(lowerAddress(launch.rewardVault)), + }; + if ( + !values.liquidity || + !values.initialBuy || + !values.ethLaunch || + !values.registration || + !values.disclosure || + !values.vaultDeployment || + Object.values(values).some((value) => !sameTransaction(value!, launch)) + ) { + fail("stock-reconciler-companion-provenance"); + } + const all = Object.values(values) as DecodedLog[]; + if (all.some(({ args }) => + "launchHash" in args && + !sameHex(exactBytes32( + args.launchHash, + "stock-reconciler-companion-launch-hash", + ), launch.launchHash) + )) { + fail("stock-reconciler-companion-launch-hash"); + } + const { liquidity: liquidityLog, initialBuy: initialBuyLog, + ethLaunch: ethLaunchLog, registration, disclosure, vaultDeployment } = + values as Record; + if ( + !sameHex(exactAddress(liquidityLog.args.quoteAsset, + "stock-reconciler-liquidity-quote"), launch.quoteAsset) || + !sameHex(exactAddress(initialBuyLog.args.deployer, + "stock-reconciler-initial-buy-deployer"), launch.coordinator) || + !sameHex(exactAddress(initialBuyLog.args.quoteAsset, + "stock-reconciler-initial-buy-quote"), launch.quoteAsset) || + !sameHex(exactBytes32(initialBuyLog.args.poolId, + "stock-reconciler-initial-buy-pool"), launch.poolId) || + !sameHex(exactAddress(ethLaunchLog.args.quoteAsset, + "stock-reconciler-eth-launch-quote"), launch.quoteAsset) || + !sameHex(exactAddress(registration.args.token, + "stock-reconciler-registration-token"), launch.token) || + !sameHex(exactAddress(registration.args.quoteAsset, + "stock-reconciler-registration-quote"), launch.quoteAsset) || + !sameHex(exactAddress(registration.args.rewardVault, + "stock-reconciler-registration-vault"), launch.rewardVault) || + !sameHex(exactAddress(registration.args.registrar, + "stock-reconciler-registration-registrar"), input.release.addresses.launcher) || + !sameHex(exactAddress(disclosure.args.token, + "stock-reconciler-disclosure-token"), launch.token) || + !sameHex(exactAddress(disclosure.args.quoteAsset, + "stock-reconciler-disclosure-quote"), launch.quoteAsset) || + !sameHex(exactAddress(disclosure.args.rewardVault, + "stock-reconciler-disclosure-vault"), launch.rewardVault) || + !sameHex(exactAddress(vaultDeployment.args.feeHook, + "stock-reconciler-vault-hook"), input.release.addresses.feeHook) || + !sameHex(exactBytes32(vaultDeployment.args.poolId, + "stock-reconciler-vault-pool"), launch.poolId) || + !sameHex(exactAddress(vaultDeployment.args.quoteAsset, + "stock-reconciler-vault-quote"), launch.quoteAsset) + ) { + fail("stock-reconciler-companion-identity"); + } + output.set(tokenKey, Object.freeze({ + liquidity: liquidityLog, + initialBuy: initialBuyLog, + ethLaunch: ethLaunchLog, + registration, + disclosure, + vaultDeployment, + })); + } + return output; +} + +function receiptContains( + receipt: ExactBlockRpcReceipt, + expected: ExactBlockRpcLog, +): number | null { + const matches = receipt.logs.filter((log) => + sameHex(log.address, expected.address) && + sameHex(log.transactionHash, expected.transactionHash) && + log.logIndex === expected.logIndex && + sameHex(log.data, expected.data) && + log.topics.length === expected.topics.length && + log.topics.every((topic, index) => + sameHex(topic, expected.topics[index]!)) + ); + return matches.length === 1 ? matches[0]!.receiptLogIndex : null; +} + +function validatedLaunchInputs(input: Readonly<{ + launches: readonly StockLaunch[]; + companions: ReadonlyMap; + transactions: readonly ExactBlockRpcTransaction[]; + receipts: readonly ExactBlockRpcReceipt[]; + release: VerifiedStockPairedRelease; +}>): ReadonlyMap { + if ( + input.launches.length !== input.transactions.length || + input.launches.length !== input.receipts.length + ) { + fail("stock-reconciler-transaction-cardinality"); + } + const output = new Map(); + for (let index = 0; index < input.launches.length; index += 1) { + const launch = input.launches[index]!; + const transaction = input.transactions[index]!; + const receipt = input.receipts[index]!; + const companion = input.companions.get(lowerAddress(launch.token)); + if (!companion) fail("stock-reconciler-transaction-companion"); + if ( + !sameHex(transaction.transactionHash, launch.transactionHash) || + transaction.blockNumber !== launch.blockNumber || + !sameHex(transaction.blockHash, launch.blockHash) || + transaction.transactionIndex !== launch.transactionIndex || + !sameHex(transaction.to, input.release.addresses.ethLaunchCoordinator) || + !sameHex(receipt.transactionHash, launch.transactionHash) || + receipt.blockNumber !== launch.blockNumber || + !sameHex(receipt.blockHash, launch.blockHash) || + receipt.transactionIndex !== launch.transactionIndex + ) { + fail("stock-reconciler-transaction-binding"); + } + const launchReceiptIndex = receiptContains(receipt, launch.log); + if ( + launchReceiptIndex === null || + Object.values(companion).some(({ log }) => + receiptContains(receipt, log) === null) + ) { + fail("stock-reconciler-receipt-provenance"); + } + let decoded: ReturnType; + try { + decoded = decodeFunctionData({ + abi: stockPairedEthLaunchCoordinatorAbi, + data: transaction.input, + }); + } catch { + return fail("stock-reconciler-calldata-decode"); + } + if (decoded.functionName !== "launch" || decoded.args.length !== 1) { + fail("stock-reconciler-calldata-selector"); + } + const envelope = record(decoded.args[0], "stock-reconciler-envelope"); + const parameters = record(envelope.launch, "stock-reconciler-parameters"); + const metadata = record(parameters.metadata, "stock-reconciler-metadata"); + const beneficiaries = array( + parameters.rewardBeneficiaries, + "stock-reconciler-beneficiaries", + ).map((value) => exactAddress(value, "stock-reconciler-beneficiary")); + const sharesBps = array( + parameters.rewardSharesBps, + "stock-reconciler-shares", + ).map((value) => safeInteger(value, 1, 10_000, + "stock-reconciler-share")); + const ethArgs = companion.ethLaunch.args; + const creator = exactAddress(ethArgs.creator, + "stock-reconciler-eth-creator"); + const initialBuyEthAmount = nonnegative( + ethArgs.initialBuyEthAmount, + "stock-reconciler-initial-eth", + ); + const initialBuyQuoteAmount = nonnegative( + ethArgs.initialBuyQuoteAmount, + "stock-reconciler-initial-quote", + ); + const initialBuyTokenAmount = nonnegative( + ethArgs.initialBuyTokenAmount, + "stock-reconciler-initial-token", + ); + const minimumQuoteAmountOut = nonnegative( + envelope.minimumQuoteAmountOut, + "stock-reconciler-minimum-quote", + ); + const minimumInitialTokenOut = nonnegative( + envelope.minimumInitialTokenOut, + "stock-reconciler-minimum-token", + ); + if ( + !sameHex(creator, transaction.from) || + !sameHex(exactAddress(parameters.quoteAsset, + "stock-reconciler-calldata-quote"), launch.quoteAsset) || + transaction.value !== initialBuyEthAmount || + initialBuyEthAmount <= 0n || + initialBuyQuoteAmount < minimumQuoteAmountOut || + initialBuyTokenAmount < minimumInitialTokenOut || + nonnegative(parameters.initialBuyQuoteAmount, + "stock-reconciler-calldata-initial-quote") !== 0n || + minimumQuoteAmountOut === 0n || + minimumInitialTokenOut === 0n || + nonnegative(envelope.deadline, "stock-reconciler-deadline") <= 0n || + beneficiaries.length < 1 || + beneficiaries.length > 8 || + beneficiaries.length !== sharesBps.length || + new Set(beneficiaries.map(lowerAddress)).size !== beneficiaries.length || + sharesBps.reduce((sum, share) => sum + share, 0) !== 10_000 || + !sameHex(exactAddress(companion.ethLaunch.args.token, + "stock-reconciler-eth-token"), launch.token) + ) { + fail("stock-reconciler-calldata-provenance"); + } + const initialArgs = companion.initialBuy.args; + if ( + nonnegative(initialArgs.quoteAmount, + "stock-reconciler-event-initial-quote") !== initialBuyQuoteAmount || + nonnegative(initialArgs.tokenAmount, + "stock-reconciler-event-initial-token") !== initialBuyTokenAmount + ) { + fail("stock-reconciler-initial-buy-provenance"); + } + output.set(lowerAddress(launch.token), Object.freeze({ + creator, + receiptLogIndex: launchReceiptIndex, + value: transaction.value, + name: exactText(parameters.name, "stock-reconciler-name"), + symbol: exactText(parameters.symbol, "stock-reconciler-symbol"), + creatorSalt: exactBytes32(parameters.creatorSalt, + "stock-reconciler-creator-salt"), + description: exactText(metadata.description, + "stock-reconciler-description"), + website: exactText(metadata.website, "stock-reconciler-website"), + image: exactText(metadata.image, "stock-reconciler-image"), + extraData: exactData(metadata.extraData, "stock-reconciler-extra-data"), + beneficiaries: Object.freeze(beneficiaries), + sharesBps: Object.freeze(sharesBps), + minimumQuoteAmountOut, + minimumInitialTokenOut, + })); + } + return output; +} + +function poolIdentity( + token: Address, + quoteAsset: Address, + hook: Address, +): Readonly<{ poolId: HexBytes32; quoteIsCurrency0: boolean }> { + const quoteIsCurrency0 = BigInt(quoteAsset) < BigInt(token); + const currency0 = quoteIsCurrency0 ? quoteAsset : token; + const currency1 = quoteIsCurrency0 ? token : quoteAsset; + return Object.freeze({ + quoteIsCurrency0, + poolId: keccak256(encodeAbiParameters(POOL_KEY_PARAMETERS, [ + currency0, + currency1, + 0, + 200, + hook, + ])), + }); +} + +function rewardConfigurationHash(input: Readonly<{ + vault: Address; + hook: Address; + poolManager: Address; + quoteAsset: Address; + poolId: HexBytes32; + beneficiaries: readonly Address[]; + sharesBps: readonly number[]; +}>): HexBytes32 { + return keccak256(encodeAbiParameters(REWARD_CONFIGURATION_PARAMETERS, [ + 1n, + input.vault, + input.hook, + input.poolManager, + input.quoteAsset, + input.poolId, + [...input.beneficiaries], + [...input.sharesBps], + ])); +} + +function feeTotals( + logs: readonly DecodedLog[], + launch: StockLaunch, +): Readonly<{ gross: bigint; creator: bigint; launcher: bigint; count: number }> { + let gross = 0n; + let creator = 0n; + let launcher = 0n; + let count = 0; + for (const event of logs) { + if ( + event.eventName !== "QuoteSwapFeesAccrued" || + !sameHex(exactBytes32( + event.args.poolId, + "stock-reconciler-fee-pool", + ), launch.poolId) + ) { + continue; + } + const grossAmount = nonnegative( + event.args.grossQuoteAmount, + "stock-reconciler-gross-quote", + ); + const creatorAmount = nonnegative( + event.args.creatorFee, + "stock-reconciler-creator-fee", + ); + const launcherAmount = nonnegative( + event.args.launcherFee, + "stock-reconciler-launcher-fee", + ); + const actualTotalFee = creatorAmount + launcherAmount; + const floorTotalFee = + grossAmount * BigInt(STOCK_PAIRED_TOTAL_SWAP_FEE_BPS) / 10_000n; + const ceilingTotalFee = + (grossAmount * BigInt(STOCK_PAIRED_TOTAL_SWAP_FEE_BPS) + 9_999n) / + 10_000n; + const expectedLauncherFee = + grossAmount * BigInt(STOCK_PAIRED_PROGRAMMABLE_FEE_BPS) / 10_000n; + if ( + typeof event.args.isBuy !== "boolean" || + !sameHex(exactAddress( + event.args.quoteAsset, + "stock-reconciler-fee-quote", + ), launch.quoteAsset) || + actualTotalFee === 0n || + (actualTotalFee !== floorTotalFee && actualTotalFee !== ceilingTotalFee) || + launcherAmount !== ( + expectedLauncherFee > actualTotalFee + ? actualTotalFee + : expectedLauncherFee + ) || + creatorAmount !== actualTotalFee - launcherAmount + ) { + fail("stock-reconciler-fee-conservation"); + } + gross += grossAmount; + creator += creatorAmount; + launcher += launcherAmount; + count += 1; + } + return Object.freeze({ gross, creator, launcher, count }); +} + +function swapState( + logs: readonly DecodedLog[], + poolId: HexBytes32, +): Readonly<{ count: number; last: DecodedLog | null }> { + const matches = logs.filter(({ eventName, args }) => + eventName === "Swap" && + sameHex(exactBytes32(args.id, "stock-reconciler-swap-pool"), poolId) + ); + return Object.freeze({ count: matches.length, last: matches.at(-1) ?? null }); +} + +function isoTimestamp(timestamp: bigint): string { + if (timestamp < 0n || timestamp > 8_640_000_000_000n) { + fail("stock-reconciler-block-time"); + } + return new Date(Number(timestamp) * 1_000).toISOString(); +} + +function contributionDocument( + releaseVersion: StockPairedReconcilerRelease, + parts: Omit, +): StockPairedReconcilerContribution { + return assertStockPairedReconcilerContribution(Object.freeze({ + contractVersion: STOCK_PAIRED_RECONCILER_CONTRIBUTION_CONTRACT, + releaseVersion, + modelId: "stock-paired" as const, + ...parts, + })); +} + +async function buildContribution( + releaseVersion: StockPairedReconcilerRelease, + input: Parameters[0], +): Promise { + const release = resolveRelease(input.contract, releaseVersion); + if ( + input.contract.checkpointBlockNumber !== input.blockNumber.toString() || + !sameHex(input.contract.checkpointBlockHash, input.blockHash) + ) { + throw invalidInput("config", "stock-reconciler-checkpoint-binding"); + } + if (input.blockNumber < BigInt(release.startBlock)) { + fail("stock-reconciler-checkpoint-before-release"); + } + await assertRuntime({ + rpc: input.rpc, + release, + blockHash: input.blockHash, + signal: input.signal, + }); + + const fromBlock = BigInt(release.startBlock); + const [launcherLogs, coordinatorLogs, hookLogs, factoryLogs] = + await Promise.all([ + readLogsInRanges({ + rpc: input.rpc, + addresses: release.addresses.launcher, + events: LAUNCHER_EVENTS, + fromBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + readLogsInRanges({ + rpc: input.rpc, + addresses: release.addresses.ethLaunchCoordinator, + events: [ethLaunchEvent], + fromBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + readLogsInRanges({ + rpc: input.rpc, + addresses: release.addresses.feeHook, + events: HOOK_EVENTS, + fromBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + readLogsInRanges({ + rpc: input.rpc, + addresses: release.addresses.feeSplitVaultFactory, + events: [vaultDeployedEvent], + fromBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + ]); + const launches = launchRecords(launcherLogs, release); + const corpusManifest = createReconcilerCorpusManifest({ + contract: input.contract, + identities: launches.map((launch) => Object.freeze({ + tokenAddress: lowerAddress(launch.token), + poolId: launch.poolId, + launchTransactionHash: launch.transactionHash, + launchBlockNumber: launch.blockNumber.toString(), + launchTransactionIndex: launch.transactionIndex, + launchLogIndex: launch.blockGlobalLogIndex, + })), + }); + const launchCompanions = companions({ + launches, + launcherLogs, + coordinatorLogs, + hookLogs, + factoryLogs, + release, + }); + const launchInputs = new Map(); + const values: unknown[] = []; + const poolSwapLogs: DecodedLog[] = []; + const timestamps = new Map(); + const timestampHashes = new Map(); + const verifiedQuoteAssets = new Set(); + const completedCorpusPages: Array<(typeof corpusManifest.pages)[number]> = []; + for (const page of corpusManifest.pages) { + const pageRpc = input.rpc.createPartitionClient(page); + await pageRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + const pageLaunches = launches.slice(page.startIndex, page.endIndexExclusive); + const [transactions, receipts, pagePoolSwapLogs] = await Promise.all([ + pageRpc.getTransactions({ + transactions: pageLaunches.map((launch) => Object.freeze({ + transactionHash: launch.transactionHash, + expectedBlockNumber: launch.blockNumber, + expectedBlockHash: launch.blockHash, + expectedTo: release.addresses.ethLaunchCoordinator, + })), + signal: input.signal, + }), + pageRpc.getTransactionReceipts({ + receipts: pageLaunches.map((launch) => Object.freeze({ + transactionHash: launch.transactionHash, + expectedBlockNumber: launch.blockNumber, + expectedBlockHash: launch.blockHash, + })), + signal: input.signal, + }), + readPoolSwapLogs({ + rpc: pageRpc, + poolManager: release.officialDependencies.poolManager.address, + poolIds: pageLaunches.map(({ poolId }) => poolId), + fromBlock, + toBlock: input.blockNumber, + signal: input.signal, + }), + ]); + poolSwapLogs.push(...pagePoolSwapLogs); + const pageInputs = validatedLaunchInputs({ + launches: pageLaunches, + companions: launchCompanions, + transactions, + receipts, + release, + }); + for (const [key, launchInput] of pageInputs) { + if (launchInputs.has(key)) fail("stock-reconciler-launch-input-duplicate"); + launchInputs.set(key, launchInput); + } + for (const quoteAsset of new Set(pageLaunches.map(({ quoteAsset }) => quoteAsset))) { + const key = lowerAddress(quoteAsset); + if (verifiedQuoteAssets.has(key)) continue; + const codeHash = await pageRpc.getCodeHash({ + address: quoteAsset, + blockHash: input.blockHash, + signal: input.signal, + }); + if (!sameHex(codeHash, release.issuerRuntime.tokenRuntimeCodeHash)) { + fail("stock-reconciler-quote-runtime"); + } + verifiedQuoteAssets.add(key); + } + values.push(...await readCalls( + pageRpc, + pageLaunches.flatMap((launch) => { + const launchInput = pageInputs.get(lowerAddress(launch.token)); + if (!launchInput) fail("stock-reconciler-launch-input-missing"); + return [ + callSpec(launch.token, uerc20ReadAbi, "name"), + callSpec(launch.token, uerc20ReadAbi, "symbol"), + callSpec(launch.token, uerc20ReadAbi, "decimals"), + callSpec(launch.token, uerc20ReadAbi, "totalSupply"), + callSpec(launch.token, uerc20ReadAbi, "creator"), + callSpec(launch.token, uerc20ReadAbi, "metadata"), + callSpec(release.officialDependencies.stateView.address, + stateViewReadAbi, "getSlot0", [launch.poolId]), + callSpec(release.officialDependencies.stateView.address, + stateViewReadAbi, "getLiquidity", [launch.poolId]), + callSpec(release.addresses.feeHook, stockPairedHookAbi, + "feeDisclosure", [launch.poolId]), + callSpec(release.addresses.feeHook, stockPairedHookAbi, + "poolFeeConfig", [launch.poolId]), + callSpec(release.addresses.ethLaunchCoordinator, + stockPairedEthLaunchCoordinatorAbi, "predictTokenAddress", [ + launchInput.name, + launchInput.symbol, + launchInput.creator, + launchInput.creatorSalt, + ]), + callSpec(release.addresses.launcher, launcherStateAbi, + "launchHashOf", [launch.token]), + callSpec(release.addresses.launcher, launcherStateAbi, + "rewardVaultOf", [launch.token]), + callSpec(release.addresses.launcher, launcherStateAbi, + "quoteAssetOf", [launch.token]), + callSpec(release.addresses.feeSplitVaultFactory, + rewardVaultFactoryStateAbi, "isFactoryVault", [launch.rewardVault]), + callSpec(release.addresses.feeSplitVaultFactory, + rewardVaultFactoryStateAbi, "configurationHashOf", [launch.rewardVault]), + callSpec(launch.rewardVault, stockFeeSplitVaultAbi, "feeHook"), + callSpec(launch.rewardVault, stockFeeSplitVaultAbi, "poolId"), + callSpec(launch.rewardVault, stockFeeSplitVaultAbi, "quoteAsset"), + callSpec(launch.rewardVault, stockFeeSplitVaultAbi, "configurationHash"), + callSpec(launch.rewardVault, stockFeeSplitVaultAbi, "beneficiaryCount"), + callSpec(launch.rewardVault, stockFeeSplitVaultAbi, + "totalCreatorFeesReceived"), + callSpec(launch.rewardVault, stockFeeSplitVaultAbi, + "totalCreatorFeesClaimed"), + callSpec(release.addresses.quoteRegistry, stockQuoteRegistryAbi, + "isSupported", [launch.quoteAsset]), + callSpec(release.addresses.quoteRegistry, stockQuoteRegistryAbi, + "assertAssetReady", [launch.quoteAsset]), + callSpec(release.addresses.positionForwarderFactory, + positionForwarderFactoryStateAbi, "isFactoryForwarder", [ + launch.positionRecipient, + ]), + callSpec(release.addresses.positionForwarderFactory, + positionForwarderFactoryStateAbi, "configurationHashOf", [ + launch.positionRecipient, + ]), + ]; + }), + input.blockHash, + input.signal, + )); + const timestampBindings = []; + for (const launch of pageLaunches) { + const key = launch.blockNumber.toString(); + const knownHash = timestampHashes.get(key); + if (knownHash !== undefined && !sameHex(knownHash, launch.blockHash)) { + fail("stock-reconciler-launch-block-hash-conflict"); + } + if (!timestamps.has(key) && knownHash === undefined) { + timestampHashes.set(key, launch.blockHash); + timestampBindings.push({ + blockNumber: launch.blockNumber, + expectedHash: launch.blockHash, + }); + } + } + const pageTimestamps = await pageRpc.getBlockTimestamps({ + blocks: timestampBindings, + signal: input.signal, + }); + if (pageTimestamps.length !== timestampBindings.length) { + fail("stock-reconciler-launch-timestamp-cardinality"); + } + timestampBindings.forEach((binding, index) => { + timestamps.set(binding.blockNumber.toString(), pageTimestamps[index]!); + }); + await pageRpc.assertCheckpoint({ + blockNumber: input.blockNumber, + blockHash: input.blockHash, + signal: input.signal, + }); + completedCorpusPages.push(page); + } + assembleReconcilerCorpusPages(corpusManifest, completedCorpusPages); + poolSwapLogs.sort((left, right) => + left.log.blockNumber === right.log.blockNumber + ? left.log.transactionIndex === right.log.transactionIndex + ? left.log.logIndex - right.log.logIndex + : left.log.transactionIndex - right.log.transactionIndex + : left.log.blockNumber < right.log.blockNumber ? -1 : 1 + ); + + const tokens: Json[] = []; + const charts: Json[] = []; + const lookups: Json[] = []; + for (let index = 0; index < launches.length; index += 1) { + const launch = launches[index]!; + const launchInput = launchInputs.get(lowerAddress(launch.token)); + const companion = launchCompanions.get(lowerAddress(launch.token)); + if (!launchInput || !companion) { + fail("stock-reconciler-launch-evidence-missing"); + } + const offset = index * CALLS_PER_LAUNCH; + const name = exactText(values[offset], "stock-reconciler-current-name"); + const symbol = exactText(values[offset + 1], + "stock-reconciler-current-symbol"); + const decimals = safeInteger(values[offset + 2], 0, 255, + "stock-reconciler-current-decimals"); + const totalSupply = nonnegative(values[offset + 3], + "stock-reconciler-current-supply"); + const tokenCreator = exactAddress(values[offset + 4], + "stock-reconciler-token-creator"); + const metadata = tuple(values[offset + 5], 4, + "stock-reconciler-current-metadata"); + const slot0 = tuple(values[offset + 6], 4, + "stock-reconciler-slot0"); + nonnegative(values[offset + 7], "stock-reconciler-active-liquidity"); + const disclosure = tuple(values[offset + 8], 9, + "stock-reconciler-current-disclosure"); + const poolConfig = tuple(values[offset + 9], 7, + "stock-reconciler-current-pool-config"); + const predictedToken = tuple(values[offset + 10], 2, + "stock-reconciler-predicted-token"); + const currentLaunchHash = exactBytes32(values[offset + 11], + "stock-reconciler-current-launch-hash"); + const currentRewardVault = exactAddress(values[offset + 12], + "stock-reconciler-current-reward-vault"); + const currentQuoteAsset = exactAddress(values[offset + 13], + "stock-reconciler-current-quote-asset"); + const isFactoryVault = values[offset + 14]; + const factoryConfigurationHash = exactBytes32(values[offset + 15], + "stock-reconciler-factory-configuration-hash"); + const vaultHook = exactAddress(values[offset + 16], + "stock-reconciler-vault-hook"); + const vaultPoolId = exactBytes32(values[offset + 17], + "stock-reconciler-vault-pool"); + const vaultQuoteAsset = exactAddress(values[offset + 18], + "stock-reconciler-vault-quote"); + const vaultConfigurationHash = exactBytes32(values[offset + 19], + "stock-reconciler-vault-configuration-hash"); + const beneficiaryCount = safeInteger(values[offset + 20], 1, 8, + "stock-reconciler-beneficiary-count"); + const totalReceived = nonnegative(values[offset + 21], + "stock-reconciler-total-received"); + const totalClaimed = nonnegative(values[offset + 22], + "stock-reconciler-total-claimed"); + const registrySupported = values[offset + 23]; + const quoteConfigurationHash = exactBytes32(values[offset + 24], + "stock-reconciler-quote-configuration-hash"); + const isFactoryForwarder = values[offset + 25]; + const forwarderConfigurationHash = exactBytes32(values[offset + 26], + "stock-reconciler-forwarder-configuration-hash"); + + const pool = poolIdentity( + launch.token, + launch.quoteAsset, + release.addresses.feeHook, + ); + const expectedRewardHash = rewardConfigurationHash({ + vault: launch.rewardVault, + hook: release.addresses.feeHook, + poolManager: release.officialDependencies.poolManager.address, + quoteAsset: launch.quoteAsset, + poolId: launch.poolId, + beneficiaries: launchInput.beneficiaries, + sharesBps: launchInput.sharesBps, + }); + const registration = companion.registration.args; + if ( + name !== launchInput.name || + symbol !== launchInput.symbol || + decimals !== 18 || + totalSupply !== TOKEN_SUPPLY || + !sameHex(tokenCreator, release.addresses.launcher) || + exactText(metadata[0], "stock-reconciler-current-description") !== + launchInput.description || + exactText(metadata[1], "stock-reconciler-current-website") !== + launchInput.website || + exactText(metadata[2], "stock-reconciler-current-image") !== + launchInput.image || + !sameHex(exactData(metadata[3], "stock-reconciler-current-extra-data"), + launchInput.extraData) || + !sameHex(pool.poolId, launch.poolId) || + !sameHex(exactAddress(predictedToken[0], + "stock-reconciler-predicted-token-address"), launch.token) || + !sameHex(currentLaunchHash, launch.launchHash) || + !sameHex(currentRewardVault, launch.rewardVault) || + !sameHex(currentQuoteAsset, launch.quoteAsset) || + isFactoryVault !== true || + !sameHex(factoryConfigurationHash, expectedRewardHash) || + !sameHex(vaultConfigurationHash, expectedRewardHash) || + !sameHex(vaultHook, release.addresses.feeHook) || + !sameHex(vaultPoolId, launch.poolId) || + !sameHex(vaultQuoteAsset, launch.quoteAsset) || + beneficiaryCount !== launchInput.beneficiaries.length || + totalClaimed > totalReceived || + registrySupported !== true || + isFactoryForwarder !== true || + sameHex(forwarderConfigurationHash, `0x${"00".repeat(32)}`) || + !sameHex(exactBytes32(registration.rewardConfigurationHash, + "stock-reconciler-registration-reward-hash"), expectedRewardHash) || + !sameHex(exactBytes32(registration.quoteConfigurationHash, + "stock-reconciler-registration-quote-hash"), quoteConfigurationHash) || + registration.quoteIsCurrency0 !== pool.quoteIsCurrency0 + ) { + fail("stock-reconciler-current-provenance"); + } + + const [disclosedQuote, disclosedToken, buySwapFee, sellSwapFee, + creatorFee, launcherFee, transferTax, lpFee, disclosedVault] = disclosure; + const [configuredQuote, configuredToken, configuredVault, registrar, + quoteIsCurrency0, registered, pendingCreatorFees] = poolConfig; + const currentPendingCreatorFees = nonnegative( + pendingCreatorFees, + "stock-reconciler-pending-creator-fees", + ); + const buySwapFeeBps = safeInteger(buySwapFee, 0, 10_000, + "stock-reconciler-buy-fee"); + const sellSwapFeeBps = safeInteger(sellSwapFee, 0, 10_000, + "stock-reconciler-sell-fee"); + const creatorFeeBps = safeInteger(creatorFee, 0, 10_000, + "stock-reconciler-creator-fee"); + const launcherFeeBps = safeInteger(launcherFee, 0, 10_000, + "stock-reconciler-launcher-fee"); + const transferTaxBps = safeInteger(transferTax, 0, 10_000, + "stock-reconciler-transfer-tax"); + const lpFeePips = safeInteger(lpFee, 0, 1_000_000, + "stock-reconciler-lp-fee"); + if ( + !sameHex(exactAddress(disclosedQuote, + "stock-reconciler-disclosed-quote"), launch.quoteAsset) || + !sameHex(exactAddress(disclosedToken, + "stock-reconciler-disclosed-token"), launch.token) || + !sameHex(exactAddress(disclosedVault, + "stock-reconciler-disclosed-vault"), launch.rewardVault) || + !sameHex(exactAddress(configuredQuote, + "stock-reconciler-configured-quote"), launch.quoteAsset) || + !sameHex(exactAddress(configuredToken, + "stock-reconciler-configured-token"), launch.token) || + !sameHex(exactAddress(configuredVault, + "stock-reconciler-configured-vault"), launch.rewardVault) || + !sameHex(exactAddress(registrar, + "stock-reconciler-configured-registrar"), release.addresses.launcher) || + quoteIsCurrency0 !== pool.quoteIsCurrency0 || + registered !== true || + buySwapFeeBps !== STOCK_PAIRED_TOTAL_SWAP_FEE_BPS || + sellSwapFeeBps !== STOCK_PAIRED_TOTAL_SWAP_FEE_BPS || + creatorFeeBps !== STOCK_PAIRED_CREATOR_FEE_BPS || + launcherFeeBps !== STOCK_PAIRED_PROGRAMMABLE_FEE_BPS || + transferTaxBps !== 0 || + lpFeePips !== 0 + ) { + fail("stock-reconciler-current-fee-configuration"); + } + const disclosedEvent = companion.disclosure.args; + if ( + safeInteger(disclosedEvent.buySwapFeeBps, 0, 10_000, + "stock-reconciler-event-buy-fee") !== buySwapFeeBps || + safeInteger(disclosedEvent.sellSwapFeeBps, 0, 10_000, + "stock-reconciler-event-sell-fee") !== sellSwapFeeBps || + safeInteger(disclosedEvent.creatorFeeBps, 0, 10_000, + "stock-reconciler-event-creator-fee") !== creatorFeeBps || + safeInteger(disclosedEvent.launcherFeeBps, 0, 10_000, + "stock-reconciler-event-launcher-fee") !== launcherFeeBps || + safeInteger(disclosedEvent.transferTaxBps, 0, 10_000, + "stock-reconciler-event-transfer-tax") !== transferTaxBps || + safeInteger(disclosedEvent.lpFeePips, 0, 1_000_000, + "stock-reconciler-event-lp-fee") !== lpFeePips + ) { + fail("stock-reconciler-event-fee-configuration"); + } + + const liquidityArgs = companion.liquidity.args; + const tokenLiquidity = nonnegative( + liquidityArgs.tokenLiquidityAmount, + "stock-reconciler-token-liquidity", + ); + const lockedDust = nonnegative( + liquidityArgs.lockedTokenDust, + "stock-reconciler-locked-dust", + ); + const initialTick = safeInteger(liquidityArgs.initialTick, + -887_272, 887_272, "stock-reconciler-initial-tick"); + const tickLower = safeInteger(liquidityArgs.tickLower, + -887_272, 887_272, "stock-reconciler-lower-tick"); + const tickUpper = safeInteger(liquidityArgs.tickUpper, + -887_272, 887_272, "stock-reconciler-upper-tick"); + const expectedInitialTick = getStockPairedExpectedInitialTickForRelease( + release, + launch.quoteAsset, + pool.quoteIsCurrency0, + ); + if ( + nonnegative(liquidityArgs.totalSupply, + "stock-reconciler-liquidity-supply") !== totalSupply || + tokenLiquidity + lockedDust !== totalSupply || + expectedInitialTick === null || + initialTick !== expectedInitialTick || + (pool.quoteIsCurrency0 + ? tickLower !== MINIMUM_USABLE_TICK || tickUpper !== initialTick + : tickLower !== initialTick || tickUpper !== MAXIMUM_USABLE_TICK) || + safeInteger(liquidityArgs.lpFeePips, 0, 1_000_000, + "stock-reconciler-event-liquidity-fee") !== lpFeePips + ) { + fail("stock-reconciler-liquidity-provenance"); + } + + const currentSlot0 = { + sqrtPriceX96: nonnegative(slot0[0], "stock-reconciler-current-price"), + tick: safeInteger(slot0[1], -887_272, 887_272, + "stock-reconciler-current-tick"), + protocolFee: safeInteger(slot0[2], 0, 1_000_000, + "stock-reconciler-protocol-fee"), + lpFee: safeInteger(slot0[3], 0, 1_000_000, + "stock-reconciler-current-lp-fee"), + }; + const swaps = swapState(poolSwapLogs, launch.poolId); + const totals = feeTotals(hookLogs, launch); + const lastSwap = swaps.last; + if (!lastSwap || swaps.count < totals.count || totals.count < 1) { + fail("stock-reconciler-swap-coverage"); + } + const lastSwapPrice = nonnegative(lastSwap.args.sqrtPriceX96, + "stock-reconciler-last-swap-price"); + const lastSwapLiquidity = nonnegative(lastSwap.args.liquidity, + "stock-reconciler-last-swap-liquidity"); + const lastSwapTick = safeInteger(lastSwap.args.tick, -887_272, 887_272, + "stock-reconciler-last-swap-tick"); + if ( + currentSlot0.sqrtPriceX96 !== lastSwapPrice || + currentSlot0.tick !== lastSwapTick || + currentSlot0.lpFee !== lpFeePips || + totalReceived + currentPendingCreatorFees !== totals.creator + ) { + fail("stock-reconciler-current-pool-state"); + } + const timestamp = timestamps.get(launch.blockNumber.toString()); + if (timestamp === undefined) fail("stock-reconciler-launch-time-missing"); + + const tokenJson: Json = { + releaseVersion, + modelId: "stock-paired", + tokenAddress: lowerAddress(launch.token), + creatorAddress: lowerAddress(launchInput.creator), + launchTransactionHash: launch.transactionHash, + launchBlockNumber: launch.blockNumber.toString(), + launchTransactionIndex: launch.transactionIndex, + launchLogIndex: launchInput.receiptLogIndex, + launchedAt: isoTimestamp(timestamp), + poolId: launch.poolId, + hookAddress: lowerAddress(release.addresses.feeHook), + quoteAssetAddress: lowerAddress(launch.quoteAsset), + rewardVaultAddress: lowerAddress(launch.rewardVault), + positionRecipient: lowerAddress(launch.positionRecipient), + positionTokenId: launch.positionTokenId.toString(), + launchHash: launch.launchHash, + name, + symbol, + decimals, + totalSupplyRaw: totalSupply.toString(), + fees: { + buySwapFeeBps, + sellSwapFeeBps, + buyCreatorFeeBps: creatorFeeBps, + sellCreatorFeeBps: creatorFeeBps, + launcherFeeBps, + transferTaxBps, + lpFeePips, + }, + liquidity: { + tokenLiquidityAmountRaw: tokenLiquidity.toString(), + lockedTokenDustRaw: lockedDust.toString(), + initialTick, + tickLower, + tickUpper, + }, + }; + tokens.push(tokenJson); + charts.push({ + releaseVersion, + modelId: "stock-paired", + tokenAddress: lowerAddress(launch.token), + poolId: launch.poolId, + quoteAssetAddress: lowerAddress(launch.quoteAsset), + state: { + blockNumber: lastSwap.log.blockNumber.toString(), + blockHash: lastSwap.log.blockHash, + transactionHash: lastSwap.log.transactionHash, + transactionIndex: lastSwap.log.transactionIndex, + logIndex: lastSwap.log.logIndex, + sqrtPriceX96: lastSwapPrice.toString(), + liquidity: lastSwapLiquidity.toString(), + tick: lastSwapTick, + lpFeePips, + }, + volume: { + quoteAssetAddress: lowerAddress(launch.quoteAsset), + grossQuoteRaw: totals.gross.toString(), + creatorFeeQuoteRaw: totals.creator.toString(), + launcherFeeQuoteRaw: totals.launcher.toString(), + }, + }); + lookups.push({ + releaseVersion, + modelId: "stock-paired", + account: lowerAddress(launchInput.creator), + launchTransactionHash: launch.transactionHash, + tokenAddress: lowerAddress(launch.token), + }); + } + + const profileTokens = new Map(); + tokens.forEach((token, index) => { + const account = lowerAddress( + launchInputs.get(lowerAddress(launches[index]!.token))!.creator, + ); + const existing = profileTokens.get(account) ?? []; + const row = token as { + tokenAddress: string; + launchTransactionHash: string; + }; + existing.push({ + releaseVersion, + modelId: "stock-paired", + tokenAddress: row.tokenAddress, + launchTransactionHash: row.launchTransactionHash, + }); + profileTokens.set(account, existing); + }); + const profiles = [...profileTokens.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([account, profileTokenRows]) => ({ + account, + tokens: profileTokenRows, + })); + + return contributionDocument(releaseVersion, { + tokens: Object.freeze(tokens), + charts: Object.freeze(charts), + profiles: Object.freeze(profiles), + launches: Object.freeze(lookups), + }); +} + +export const buildStockPairedV1ExactBlockContribution: + StockPairedExactBlockContributionBuilder = + async (input) => buildContribution("stock-paired-v1", input); + +export const buildStockPairedV2ExactBlockContribution: + StockPairedExactBlockContributionBuilder = + async (input) => buildContribution("stock-paired-v2", input); + +export const buildStockPairedV3ExactBlockContribution: + StockPairedExactBlockContributionBuilder = + async (input) => buildContribution("stock-paired-v3", input); diff --git a/lib/data-pipeline/uniswap-parser-binding.ts b/lib/data-pipeline/uniswap-parser-binding.ts new file mode 100644 index 00000000..464f53b2 --- /dev/null +++ b/lib/data-pipeline/uniswap-parser-binding.ts @@ -0,0 +1,11 @@ +import "server-only"; + +// Generated from the exact bytes of lib/data-pipeline/uniswap.ts. The drift +// test must be updated together with this value whenever parser semantics or +// their surrounding source change, which makes the release commitment change. +export const UNISWAP_ANALYTICS_PARSER_BINDING = Object.freeze({ + contractVersion: "uniswap-analytics-parser-v1", + sourcePath: "lib/data-pipeline/uniswap.ts", + sourceCommitment: + "0xea73041ca7e18943b2b9d4d0265fa15c730471ebefda2898a1cb026f9a6f6204" as const, +}); diff --git a/lib/data-pipeline/uniswap.ts b/lib/data-pipeline/uniswap.ts new file mode 100644 index 00000000..7e018048 --- /dev/null +++ b/lib/data-pipeline/uniswap.ts @@ -0,0 +1,1162 @@ +import "server-only"; + +import { encodeAbiParameters, keccak256 } from "viem"; +import { CircuitBreaker } from "./circuit"; +import { + canonicalAddress, + canonicalBytes32, + parseNonnegativeIntegerText, + type HexAddress, + type HexBytes32, +} from "./codecs"; +import { loadDataPipelineConfig } from "./config"; +import { + DataPipelineError, + dataPipelineError, + invalidInput, + validationError, + type DataPipelineErrorCode, +} from "./errors"; +import { boundedJsonRequest, type DataPipelineFetcher } from "./request"; +import { getDataPipelineReleaseBinding } from "./release-binding.server"; +import { UNISWAP_ANALYTICS_PARSER_BINDING } from "./uniswap-parser-binding"; + +const RELEASE_BINDING = getDataPipelineReleaseBinding(); +export const OFFICIAL_V4_SUBGRAPH_ID = + RELEASE_BINDING.uniswapV4Subgraph.subgraphId; +export const OFFICIAL_V4_SUBGRAPH_DEPLOYMENT = + RELEASE_BINDING.uniswapV4Subgraph.deployment; +const OFFICIAL_V4_SUBGRAPH_GATEWAY_BASE_URL = "https://gateway.thegraph.com"; + +// Conservative query spans keep each fixed subgraph request bounded before +// entity pagination: six hours of swaps, 31 days of hourly candles, and one +// leap year of daily candles. Every split remains half-open: [from, to). +export const UNISWAP_SWAP_WINDOW_SECONDS = 21_600n; +export const UNISWAP_HOUR_WINDOW_SECONDS = 2_678_400; +export const UNISWAP_DAY_WINDOW_SECONDS = 31_622_400; + +const POOL_QUERY = ` + query ProgrammablePoolSnapshot($poolId: ID!, $block: Int!) { + _meta(block: { number: $block }) { + deployment + hasIndexingErrors + block { number hash } + } + pool( + id: $poolId + block: { number: $block } + subgraphError: deny + ) { + id + createdAtTimestamp + createdAtBlockNumber + token0 { id decimals } + token1 { id decimals } + hooks + feeTier + tickSpacing + liquidity + sqrtPrice + tick + txCount + volumeToken0 + volumeToken1 + volumeUSD + totalValueLockedToken0 + totalValueLockedToken1 + totalValueLockedUSD + } + } +`; + +const SWAP_QUERY = ` + query ProgrammableSwapPage( + $poolId: String! + $blockHash: Bytes! + $from: BigInt! + $toExclusive: BigInt! + $cursor: ID! + ) { + _meta(block: { hash: $blockHash }) { + deployment + hasIndexingErrors + block { number hash } + } + swaps( + first: 250 + orderBy: id + orderDirection: asc + block: { hash: $blockHash } + subgraphError: deny + where: { + pool: $poolId + timestamp_gte: $from + timestamp_lt: $toExclusive + id_gt: $cursor + } + ) { + id + transaction { id blockNumber timestamp } + timestamp + pool { id } + sender + origin + amount0 + amount1 + amountUSD + sqrtPriceX96 + tick + logIndex + } + } +`; + +const HOUR_QUERY = ` + query ProgrammablePoolHourSeries( + $poolId: String! + $blockHash: Bytes! + $from: Int! + $toExclusive: Int! + $cursor: ID! + ) { + _meta(block: { hash: $blockHash }) { + deployment + hasIndexingErrors + block { number hash } + } + poolHourDatas( + first: 250 + orderBy: id + orderDirection: asc + block: { hash: $blockHash } + subgraphError: deny + where: { + pool: $poolId + periodStartUnix_gte: $from + periodStartUnix_lt: $toExclusive + id_gt: $cursor + } + ) { + id + periodStartUnix + pool { id } + liquidity + sqrtPrice + token0Price + token1Price + tick + tvlUSD + volumeToken0 + volumeToken1 + volumeUSD + feesUSD + txCount + open + high + low + close + } + } +`; + +const DAY_QUERY = ` + query ProgrammablePoolDaySeries( + $poolId: String! + $blockHash: Bytes! + $from: Int! + $toExclusive: Int! + $cursor: ID! + ) { + _meta(block: { hash: $blockHash }) { + deployment + hasIndexingErrors + block { number hash } + } + poolDayDatas( + first: 250 + orderBy: id + orderDirection: asc + block: { hash: $blockHash } + subgraphError: deny + where: { + pool: $poolId + date_gte: $from + date_lt: $toExclusive + id_gt: $cursor + } + ) { + id + date + pool { id } + liquidity + sqrtPrice + token0Price + token1Price + tick + tvlUSD + volumeToken0 + volumeToken1 + volumeUSD + feesUSD + txCount + open + high + low + close + } + } +`; + +// This is the canonical provenance input for consumers that persist Graph +// facts. It deliberately references the exact documents executed below. Any +// parser behavior change must ship with a new parser contract version so a +// registered schema commitment cannot silently describe different semantics. +export const UNISWAP_ANALYTICS_QUERY_CONTRACT = Object.freeze({ + parser: UNISWAP_ANALYTICS_PARSER_BINDING, + queries: Object.freeze({ + poolSnapshot: POOL_QUERY, + swaps: SWAP_QUERY, + hourSeries: HOUR_QUERY, + daySeries: DAY_QUERY, + }), +}); + +export type VerifiedPoolKey = { + poolId: string; + currency0: string; + currency1: string; + fee: number; + tickSpacing: number; + hooks: string; + token0Decimals: number; + token1Decimals: number; +}; + +type CanonicalPoolKey = { + poolId: HexBytes32; + currency0: HexAddress; + currency1: HexAddress; + fee: number; + tickSpacing: number; + hooks: HexAddress; + token0Decimals: number; + token1Decimals: number; +}; + +export type AnalyticsProvenance = { + deployment: typeof OFFICIAL_V4_SUBGRAPH_DEPLOYMENT; + blockNumber: string; + blockHash: HexBytes32; +}; + +export type AnalyticsResult = + | { + status: "ready"; + data: T; + provenance: AnalyticsProvenance; + } + | { + status: "pending"; + reason: DataPipelineErrorCode; + }; + +type Meta = AnalyticsProvenance; + +function isRecord(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function onlyKeys(value: Record, keys: readonly string[]) { + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + return ( + actual.length === expected.length && + actual.every((key, index) => key === expected[index]) + ); +} + +function unsigned(value: unknown, maximumDigits = 78): string { + try { + return parseNonnegativeIntegerText(value, maximumDigits); + } catch { + throw validationError("uniswap", "integer"); + } +} + +function decimal(value: unknown, signed = false): string { + if ( + typeof value !== "string" || + value.length > 160 || + !(signed + ? /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value) + : /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) + ) { + throw validationError("uniswap", "decimal"); + } + return value; +} + +function safeInteger(value: unknown, minimum: number, maximum: number) { + const parsed = + typeof value === "number" + ? value + : typeof value === "string" && /^-?(0|[1-9]\d*)$/.test(value) + ? Number(value) + : Number.NaN; + if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) { + throw validationError("uniswap", "integer"); + } + return parsed; +} + +function id(value: unknown): string { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > 256 || + /[\u0000-\u001f\u007f]/.test(value) + ) { + throw validationError("uniswap", "entity-id"); + } + return value; +} + +function responseData( + response: unknown, + entityKey: string, +): { meta: unknown; entity: unknown } { + if ( + !isRecord(response) || + !onlyKeys(response, ["data"]) || + !isRecord(response.data) || + !onlyKeys(response.data, ["_meta", entityKey]) + ) { + throw validationError("uniswap", "response"); + } + return { + meta: response.data._meta, + entity: response.data[entityKey], + }; +} + +function parseMeta( + value: unknown, + block: { number: string; hash: string }, +): Meta { + if ( + !isRecord(value) || + !onlyKeys(value, ["deployment", "hasIndexingErrors", "block"]) || + value.deployment !== OFFICIAL_V4_SUBGRAPH_DEPLOYMENT || + value.hasIndexingErrors !== false || + !isRecord(value.block) || + !onlyKeys(value.block, ["number", "hash"]) + ) { + throw validationError("uniswap", "metadata"); + } + const number = unsigned( + typeof value.block.number === "number" + ? String(value.block.number) + : value.block.number, + ); + const hash = providerBytes32(value.block.hash); + if (number !== block.number || hash !== block.hash) { + throw validationError("uniswap", "metadata-block"); + } + return { + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + blockNumber: number, + blockHash: hash, + }; +} + +function providerAddress(value: unknown): HexAddress { + try { + return canonicalAddress(value); + } catch { + throw validationError("uniswap", "address"); + } +} + +function providerBytes32(value: unknown): HexBytes32 { + try { + return canonicalBytes32(value); + } catch { + throw validationError("uniswap", "bytes32"); + } +} + +function canonicalPoolKey(value: VerifiedPoolKey): CanonicalPoolKey { + let poolId: HexBytes32; + let currency0: HexAddress; + let currency1: HexAddress; + let hooks: HexAddress; + try { + poolId = canonicalBytes32(value.poolId); + currency0 = canonicalAddress(value.currency0); + currency1 = canonicalAddress(value.currency1); + hooks = canonicalAddress(value.hooks); + } catch { + throw invalidInput("uniswap", "pool-key"); + } + if ( + BigInt(currency0) >= BigInt(currency1) || + !Number.isSafeInteger(value.fee) || + (value.fee !== 0x80_00_00 && (value.fee < 0 || value.fee > 1_000_000)) || + !Number.isSafeInteger(value.tickSpacing) || + value.tickSpacing < 1 || + value.tickSpacing > 32_767 || + !Number.isSafeInteger(value.token0Decimals) || + value.token0Decimals < 0 || + value.token0Decimals > 255 || + !Number.isSafeInteger(value.token1Decimals) || + value.token1Decimals < 0 || + value.token1Decimals > 255 + ) { + throw invalidInput("uniswap", "pool-key"); + } + const recomputed = keccak256( + encodeAbiParameters( + [ + { type: "address" }, + { type: "address" }, + { type: "uint24" }, + { type: "int24" }, + { type: "address" }, + ], + [currency0, currency1, value.fee, value.tickSpacing, hooks], + ), + ); + if (recomputed !== poolId) throw invalidInput("uniswap", "pool-id"); + return { + poolId, + currency0, + currency1, + fee: value.fee, + tickSpacing: value.tickSpacing, + hooks, + token0Decimals: value.token0Decimals, + token1Decimals: value.token1Decimals, + }; +} + +function canonicalBlock(value: { number: string; hash: string }) { + let number: string; + let hash: HexBytes32; + try { + number = parseNonnegativeIntegerText(value.number, 10); + hash = canonicalBytes32(value.hash); + } catch { + throw invalidInput("uniswap", "block"); + } + const graphNumber = Number(number); + if ( + !Number.isSafeInteger(graphNumber) || + graphNumber < 0 || + graphNumber > 2_147_483_647 + ) { + throw invalidInput("uniswap", "block"); + } + return { number, hash, graphNumber }; +} + +export type PoolSnapshot = { + id: HexBytes32; + createdAtTimestamp: string; + createdAtBlockNumber: string; + token0: { id: HexAddress; decimals: number }; + token1: { id: HexAddress; decimals: number }; + hooks: HexAddress; + appliedFeeTier: string; + tickSpacing: number; + liquidity: string; + sqrtPriceX96: string; + tick: number | null; + transactionCount: string; + marketVolumeToken0: string; + marketVolumeToken1: string; + marketVolumeUsd: string; + totalValueLockedToken0: string; + totalValueLockedToken1: string; + totalValueLockedUsd: string; +}; + +function parsePool(value: unknown, key: CanonicalPoolKey): PoolSnapshot { + const keys = [ + "id", + "createdAtTimestamp", + "createdAtBlockNumber", + "token0", + "token1", + "hooks", + "feeTier", + "tickSpacing", + "liquidity", + "sqrtPrice", + "tick", + "txCount", + "volumeToken0", + "volumeToken1", + "volumeUSD", + "totalValueLockedToken0", + "totalValueLockedToken1", + "totalValueLockedUSD", + ] as const; + if ( + !isRecord(value) || + !onlyKeys(value, keys) || + !isRecord(value.token0) || + !onlyKeys(value.token0, ["id", "decimals"]) || + !isRecord(value.token1) || + !onlyKeys(value.token1, ["id", "decimals"]) + ) { + throw validationError("uniswap", "pool"); + } + const poolId = providerBytes32(value.id); + const token0 = providerAddress(value.token0.id); + const token1 = providerAddress(value.token1.id); + const hooks = providerAddress(value.hooks); + const token0Decimals = safeInteger(value.token0.decimals, 0, 255); + const token1Decimals = safeInteger(value.token1.decimals, 0, 255); + const tickSpacing = safeInteger(value.tickSpacing, 1, 0x7f_ffff); + if ( + poolId !== key.poolId || + token0 !== key.currency0 || + token1 !== key.currency1 || + hooks !== key.hooks || + token0Decimals !== key.token0Decimals || + token1Decimals !== key.token1Decimals || + tickSpacing !== key.tickSpacing + ) { + throw validationError("uniswap", "pool-key"); + } + return { + id: poolId, + createdAtTimestamp: unsigned(value.createdAtTimestamp), + createdAtBlockNumber: unsigned(value.createdAtBlockNumber), + token0: { id: token0, decimals: token0Decimals }, + token1: { id: token1, decimals: token1Decimals }, + hooks, + appliedFeeTier: unsigned(value.feeTier, 8), + tickSpacing, + liquidity: unsigned(value.liquidity), + sqrtPriceX96: unsigned(value.sqrtPrice), + tick: + value.tick === null ? null : safeInteger(value.tick, -887_272, 887_272), + transactionCount: unsigned(value.txCount), + marketVolumeToken0: decimal(value.volumeToken0), + marketVolumeToken1: decimal(value.volumeToken1), + marketVolumeUsd: decimal(value.volumeUSD), + totalValueLockedToken0: decimal(value.totalValueLockedToken0), + totalValueLockedToken1: decimal(value.totalValueLockedToken1), + totalValueLockedUsd: decimal(value.totalValueLockedUSD), + }; +} + +export type SwapAnalytics = { + id: string; + transactionHash: HexBytes32; + blockNumber: string; + transactionTimestamp: string; + timestamp: string; + poolId: HexBytes32; + sender: HexAddress; + origin: HexAddress; + amount0: string; + amount1: string; + marketAmountUsd: string; + sqrtPriceX96: string; + tick: number; + logIndex: string; +}; + +function parseSwap( + value: unknown, + key: CanonicalPoolKey, + from: bigint, + toExclusive: bigint, +): SwapAnalytics { + const keys = [ + "id", + "transaction", + "timestamp", + "pool", + "sender", + "origin", + "amount0", + "amount1", + "amountUSD", + "sqrtPriceX96", + "tick", + "logIndex", + ] as const; + if ( + !isRecord(value) || + !onlyKeys(value, keys) || + !isRecord(value.transaction) || + !onlyKeys(value.transaction, ["id", "blockNumber", "timestamp"]) || + !isRecord(value.pool) || + !onlyKeys(value.pool, ["id"]) + ) { + throw validationError("uniswap", "swap"); + } + const timestamp = unsigned(value.timestamp); + const numericTimestamp = BigInt(timestamp); + const poolId = providerBytes32(value.pool.id); + if ( + numericTimestamp < from || + numericTimestamp >= toExclusive || + poolId !== key.poolId + ) { + throw validationError("uniswap", "swap-window"); + } + return { + id: id(value.id), + transactionHash: providerBytes32(value.transaction.id), + blockNumber: unsigned(value.transaction.blockNumber), + transactionTimestamp: unsigned(value.transaction.timestamp), + timestamp, + poolId, + sender: providerAddress(value.sender), + origin: providerAddress(value.origin), + amount0: decimal(value.amount0, true), + amount1: decimal(value.amount1, true), + marketAmountUsd: decimal(value.amountUSD), + sqrtPriceX96: unsigned(value.sqrtPriceX96), + tick: safeInteger(value.tick, -887_272, 887_272), + logIndex: unsigned(value.logIndex, 10), + }; +} + +export type CandleAnalytics = { + id: string; + periodStart: number; + poolId: HexBytes32; + liquidity: string; + sqrtPriceX96: string; + token0Price: string; + token1Price: string; + tick: number; + tvlUsd: string; + marketVolumeToken0: string; + marketVolumeToken1: string; + marketVolumeUsd: string; + feesUsd: string; + transactionCount: string; + open: string; + high: string; + low: string; + close: string; +}; + +function parseCandle( + value: unknown, + key: CanonicalPoolKey, + timeField: "periodStartUnix" | "date", + from: number, + toExclusive: number, +): CandleAnalytics { + const keys = [ + "id", + timeField, + "pool", + "liquidity", + "sqrtPrice", + "token0Price", + "token1Price", + "tick", + "tvlUSD", + "volumeToken0", + "volumeToken1", + "volumeUSD", + "feesUSD", + "txCount", + "open", + "high", + "low", + "close", + ] as const; + if ( + !isRecord(value) || + !onlyKeys(value, keys) || + !isRecord(value.pool) || + !onlyKeys(value.pool, ["id"]) + ) { + throw validationError("uniswap", "candle"); + } + const periodStart = safeInteger(value[timeField], 0, 2_147_483_647); + const poolId = providerBytes32(value.pool.id); + if ( + periodStart < from || + periodStart >= toExclusive || + poolId !== key.poolId + ) { + throw validationError("uniswap", "candle-window"); + } + return { + id: id(value.id), + periodStart, + poolId, + liquidity: unsigned(value.liquidity), + sqrtPriceX96: unsigned(value.sqrtPrice), + token0Price: decimal(value.token0Price), + token1Price: decimal(value.token1Price), + tick: safeInteger(value.tick, -887_272, 887_272), + tvlUsd: decimal(value.tvlUSD), + marketVolumeToken0: decimal(value.volumeToken0), + marketVolumeToken1: decimal(value.volumeToken1), + marketVolumeUsd: decimal(value.volumeUSD), + feesUsd: decimal(value.feesUSD), + transactionCount: unsigned(value.txCount), + open: decimal(value.open), + high: decimal(value.high), + low: decimal(value.low), + close: decimal(value.close), + }; +} + +function gcd(left: bigint, right: bigint): bigint { + let a = left < 0n ? -left : left; + let b = right < 0n ? -right : right; + while (b !== 0n) { + const next = a % b; + a = b; + b = next; + } + return a; +} + +export function priceRatiosFromSqrtPriceX96(input: { + sqrtPriceX96: string; + token0Decimals: number; + token1Decimals: number; +}) { + let sqrt: bigint; + try { + sqrt = BigInt(parseNonnegativeIntegerText(input.sqrtPriceX96)); + } catch { + throw invalidInput("uniswap", "sqrt-price"); + } + if ( + sqrt === 0n || + !Number.isSafeInteger(input.token0Decimals) || + input.token0Decimals < 0 || + input.token0Decimals > 255 || + !Number.isSafeInteger(input.token1Decimals) || + input.token1Decimals < 0 || + input.token1Decimals > 255 + ) { + throw invalidInput("uniswap", "price-decimals"); + } + const directNumerator = sqrt * sqrt * 10n ** BigInt(input.token0Decimals); + const directDenominator = 2n ** 192n * 10n ** BigInt(input.token1Decimals); + const divisor = gcd(directNumerator, directDenominator); + const numerator = directNumerator / divisor; + const denominator = directDenominator / divisor; + return { + token1PerToken0: { + numerator: numerator.toString(), + denominator: denominator.toString(), + }, + token0PerToken1: { + numerator: denominator.toString(), + denominator: numerator.toString(), + }, + }; +} + +function windowBigInt(from: string, toExclusive: string) { + let canonicalFrom: string; + let canonicalTo: string; + try { + canonicalFrom = parseNonnegativeIntegerText(from); + canonicalTo = parseNonnegativeIntegerText(toExclusive); + } catch { + throw invalidInput("uniswap", "window"); + } + if (BigInt(canonicalFrom) >= BigInt(canonicalTo)) { + throw invalidInput("uniswap", "window"); + } + return { + from: canonicalFrom, + toExclusive: canonicalTo, + fromBigInt: BigInt(canonicalFrom), + toBigInt: BigInt(canonicalTo), + }; +} + +function windowInt(from: number, toExclusive: number) { + if ( + !Number.isSafeInteger(from) || + !Number.isSafeInteger(toExclusive) || + from < 0 || + toExclusive > 2_147_483_647 || + from >= toExclusive + ) { + throw invalidInput("uniswap", "window"); + } + return { from, toExclusive }; +} + +function* splitBigIntWindow( + window: ReturnType, + maximumSpan: bigint, +) { + let from = window.fromBigInt; + while (from < window.toBigInt) { + const candidate = from + maximumSpan; + const toExclusive = + candidate < window.toBigInt ? candidate : window.toBigInt; + yield { + from: from.toString(), + toExclusive: toExclusive.toString(), + fromBigInt: from, + toBigInt: toExclusive, + }; + from = toExclusive; + } +} + +function* splitIntWindow( + window: ReturnType, + maximumSpan: number, +) { + let from = window.from; + while (from < window.toExclusive) { + const toExclusive = Math.min(from + maximumSpan, window.toExclusive); + yield { from, toExclusive }; + from = toExclusive; + } +} + +function pending(error: unknown): { + status: "pending"; + reason: DataPipelineErrorCode; +} { + if (error instanceof DataPipelineError) { + return { status: "pending", reason: error.code }; + } + return { status: "pending", reason: "dependency_unavailable" }; +} + +export function createUniswapAnalyticsClient(options: { + gatewayBaseUrl: string; + apiKey: string; + fetcher?: DataPipelineFetcher; + circuit?: CircuitBreaker; + limits?: { + maximumPages: number; + maximumEntities: number; + }; +}) { + const isProduction = + process.env.NODE_ENV === "production" || + process.env.VERCEL_ENV === "production"; + if ( + isProduction && + options.gatewayBaseUrl !== OFFICIAL_V4_SUBGRAPH_GATEWAY_BASE_URL + ) { + throw dataPipelineError({ + dependency: "config", + code: "invalid_config", + retryable: false, + countsTowardCircuit: false, + }); + } + const config = loadDataPipelineConfig({ + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: options.gatewayBaseUrl, + PROGRAMMABLE_UNISWAP_GRAPH_API_KEY: options.apiKey, + }); + if (!config.uniswap.apiKey) { + throw invalidInput("config", "uniswap-config"); + } + const maximumPages = options.limits?.maximumPages ?? 40; + const maximumEntities = options.limits?.maximumEntities ?? 10_000; + if ( + !Number.isSafeInteger(maximumPages) || + maximumPages < 1 || + maximumPages > 40 || + !Number.isSafeInteger(maximumEntities) || + maximumEntities < 1 || + maximumEntities > 10_000 || + maximumEntities > maximumPages * 250 + ) { + throw invalidInput("uniswap", "pagination-limits"); + } + const endpoint = `${config.uniswap.gatewayBaseUrl}/api/subgraphs/id/${OFFICIAL_V4_SUBGRAPH_ID}`; + const circuit = + options.circuit ?? new CircuitBreaker({ dependency: "uniswap" }); + const request = (body: unknown) => + boundedJsonRequest({ + dependency: "uniswap", + endpoint, + timeoutMs: config.uniswap.timeoutMs, + maximumBodyBytes: config.uniswap.maximumBodyBytes, + fetcher: options.fetcher, + headers: { authorization: `Bearer ${config.uniswap.apiKey!}` }, + body, + }); + + async function execute( + operation: () => Promise<{ data: T; provenance: Meta }>, + ): Promise> { + try { + const result = await circuit.execute(operation); + return { + status: "ready", + data: result.data, + provenance: result.provenance, + }; + } catch (error) { + return pending(error); + } + } + + async function paginate(input: { + block: ReturnType; + query: string; + entityKey: "swaps" | "poolHourDatas" | "poolDayDatas"; + windows: Iterable<{ + variables: Record; + parse: (value: unknown) => T; + }>; + sort?: (left: T, right: T) => number; + }) { + return execute(async () => { + const collected: T[] = []; + let provenance: Meta | undefined; + let pagesConsumed = 0; + for (const window of input.windows) { + let cursor = ""; + while (true) { + if (pagesConsumed >= maximumPages) { + throw dataPipelineError({ + dependency: "uniswap", + code: "response_oversize", + retryable: true, + countsTowardCircuit: true, + }); + } + pagesConsumed += 1; + const response = await request({ + query: input.query, + variables: { ...window.variables, cursor }, + }); + const parsed = responseData(response, input.entityKey); + const currentMeta = parseMeta(parsed.meta, input.block); + if ( + provenance !== undefined && + (provenance.blockNumber !== currentMeta.blockNumber || + provenance.blockHash !== currentMeta.blockHash) + ) { + throw validationError("uniswap", "page-metadata"); + } + provenance = currentMeta; + if (!Array.isArray(parsed.entity) || parsed.entity.length > 250) { + throw validationError("uniswap", "page"); + } + let previousId = cursor; + for (const entity of parsed.entity) { + const item = window.parse(entity); + const itemId = + isRecord(entity) && typeof entity.id === "string" + ? entity.id + : ""; + if (itemId <= previousId) { + throw validationError("uniswap", "page-order"); + } + previousId = itemId; + collected.push(item); + if (collected.length > maximumEntities) { + throw dataPipelineError({ + dependency: "uniswap", + code: "response_oversize", + retryable: true, + countsTowardCircuit: true, + }); + } + } + if (parsed.entity.length < 250) break; + cursor = previousId; + if (collected.length >= maximumEntities) { + throw dataPipelineError({ + dependency: "uniswap", + code: "response_oversize", + retryable: true, + countsTowardCircuit: true, + }); + } + } + } + if (!provenance) throw validationError("uniswap", "page-metadata"); + if (input.sort) collected.sort(input.sort); + return { data: collected, provenance }; + }); + } + + return Object.freeze({ + async readPoolSnapshot(input: { + poolKey: VerifiedPoolKey; + block: { number: string; hash: string }; + }): Promise> { + const key = canonicalPoolKey(input.poolKey); + const block = canonicalBlock(input.block); + return execute(async () => { + const response = await request({ + query: POOL_QUERY, + variables: { + poolId: key.poolId, + block: block.graphNumber, + }, + }); + const parsed = responseData(response, "pool"); + const provenance = parseMeta(parsed.meta, block); + if (parsed.entity === null) { + throw validationError("uniswap", "pool-missing"); + } + return { + data: parsePool(parsed.entity, key), + provenance, + }; + }); + }, + + async readSwaps(input: { + poolKey: VerifiedPoolKey; + block: { number: string; hash: string }; + from: string; + toExclusive: string; + }): Promise> { + const key = canonicalPoolKey(input.poolKey); + const block = canonicalBlock(input.block); + const window = windowBigInt(input.from, input.toExclusive); + function* windows() { + for (const split of splitBigIntWindow( + window, + UNISWAP_SWAP_WINDOW_SECONDS, + )) { + yield { + variables: { + poolId: key.poolId, + blockHash: block.hash, + from: split.from, + toExclusive: split.toExclusive, + }, + parse: (value: unknown) => + parseSwap(value, key, split.fromBigInt, split.toBigInt), + }; + } + } + return paginate({ + block, + query: SWAP_QUERY, + entityKey: "swaps", + windows: windows(), + sort: (left, right) => { + const blockOrder = + BigInt(left.blockNumber) - BigInt(right.blockNumber); + if (blockOrder !== 0n) return blockOrder < 0n ? -1 : 1; + const transactionOrder = + left.transactionHash < right.transactionHash + ? -1 + : left.transactionHash > right.transactionHash + ? 1 + : 0; + if (transactionOrder !== 0) return transactionOrder; + const logOrder = BigInt(left.logIndex) - BigInt(right.logIndex); + return logOrder < 0n ? -1 : logOrder > 0n ? 1 : 0; + }, + }); + }, + + async readHourSeries(input: { + poolKey: VerifiedPoolKey; + block: { number: string; hash: string }; + from: number; + toExclusive: number; + }): Promise> { + const key = canonicalPoolKey(input.poolKey); + const block = canonicalBlock(input.block); + const window = windowInt(input.from, input.toExclusive); + function* windows() { + for (const split of splitIntWindow( + window, + UNISWAP_HOUR_WINDOW_SECONDS, + )) { + yield { + variables: { + poolId: key.poolId, + blockHash: block.hash, + ...split, + }, + parse: (value: unknown) => + parseCandle( + value, + key, + "periodStartUnix", + split.from, + split.toExclusive, + ), + }; + } + } + return paginate({ + block, + query: HOUR_QUERY, + entityKey: "poolHourDatas", + windows: windows(), + sort: (left, right) => { + if (left.periodStart !== right.periodStart) { + return left.periodStart - right.periodStart; + } + return left.id < right.id ? -1 : left.id > right.id ? 1 : 0; + }, + }); + }, + + async readDaySeries(input: { + poolKey: VerifiedPoolKey; + block: { number: string; hash: string }; + from: number; + toExclusive: number; + }): Promise> { + const key = canonicalPoolKey(input.poolKey); + const block = canonicalBlock(input.block); + const window = windowInt(input.from, input.toExclusive); + function* windows() { + for (const split of splitIntWindow( + window, + UNISWAP_DAY_WINDOW_SECONDS, + )) { + yield { + variables: { + poolId: key.poolId, + blockHash: block.hash, + ...split, + }, + parse: (value: unknown) => + parseCandle(value, key, "date", split.from, split.toExclusive), + }; + } + } + return paginate({ + block, + query: DAY_QUERY, + entityKey: "poolDayDatas", + windows: windows(), + sort: (left, right) => { + if (left.periodStart !== right.periodStart) { + return left.periodStart - right.periodStart; + } + return left.id < right.id ? -1 : left.id > right.id ? 1 : 0; + }, + }); + }, + + circuitSnapshot: () => circuit.snapshot(), + }); +} diff --git a/lib/launch-model-gating.ts b/lib/launch-model-gating.ts index c6ba1cf7..690fe88b 100644 --- a/lib/launch-model-gating.ts +++ b/lib/launch-model-gating.ts @@ -25,7 +25,6 @@ const DEEP_V1_KEEPER_EXECUTOR_RUNTIME_CODE_HASH = const implementedLaunchModels = new Set([ "classic", "classic-v3", - "deep", "stock-paired", ]); diff --git a/lib/onchain/durable-model.ts b/lib/onchain/durable-model.ts index f0fcd32d..7d241c38 100644 --- a/lib/onchain/durable-model.ts +++ b/lib/onchain/durable-model.ts @@ -166,6 +166,12 @@ export type DurableExploreRead = detail: string; }; +export function selectFreshDurableExploreModel( + read: DurableExploreRead, +): Extract | null { + return read.status === "ready" ? read.envelope.payload.model : null; +} + function contentHash(payload: unknown) { return keccak256(toBytes(JSON.stringify(payload))); } diff --git a/lib/onchain/explore-read-source.ts b/lib/onchain/explore-read-source.ts new file mode 100644 index 00000000..38476593 --- /dev/null +++ b/lib/onchain/explore-read-source.ts @@ -0,0 +1,65 @@ +import type { DurableExploreRead } from "./durable-model"; +import type { + ExploreReadModel, + ReadyOnchainDeployment, +} from "./types"; + +type ReadyExploreModel = Extract; + +export type ExploreReadSourceDependencies = { + readDurable: ( + config: ReadyOnchainDeployment, + ) => Promise; + selectFreshDurable: ( + read: DurableExploreRead, + ) => ReadyExploreModel | null; + readLive: ( + config: ReadyOnchainDeployment, + ) => Promise; + enrichWithUsd: ( + model: ExploreReadModel, + config: ReadyOnchainDeployment, + ) => Promise; + warn: (message: string, detail: unknown) => void; + error: (message: string, cause: unknown) => void; +}; + +async function enrichOrReturn( + model: ExploreReadModel, + config: ReadyOnchainDeployment, + dependencies: ExploreReadSourceDependencies, +): Promise { + try { + return await dependencies.enrichWithUsd(model, config); + } catch (cause) { + dependencies.error("ETH/USD enrichment failed", cause); + return model; + } +} + +export async function resolveExploreReadSource( + config: ReadyOnchainDeployment, + dependencies: ExploreReadSourceDependencies, +): Promise { + if (config.environment === "production") { + const durable = await dependencies.readDurable(config); + const durableModel = dependencies.selectFreshDurable(durable); + if (durableModel) { + return enrichOrReturn(durableModel, config, dependencies); + } + if (durable.status === "unavailable") { + dependencies.warn( + "Durable Explore index unavailable; using live RPCs", + durable.reason === "stale" + ? { + reason: durable.reason, + ageSeconds: Math.floor(durable.ageMs / 1_000), + } + : { reason: durable.reason, detail: durable.detail }, + ); + } + } + + const liveModel = await dependencies.readLive(config); + return enrichOrReturn(liveModel, config, dependencies); +} diff --git a/lib/onchain/read-model.ts b/lib/onchain/read-model.ts index 3bc318cc..726763aa 100644 --- a/lib/onchain/read-model.ts +++ b/lib/onchain/read-model.ts @@ -46,7 +46,11 @@ import type { ReadyOnchainDeployment, VerifiedLaunchRecord, } from "./types"; -import { readDurableExploreModel } from "./durable-model"; +import { + readDurableExploreModel, + selectFreshDurableExploreModel, +} from "./durable-model"; +import { resolveExploreReadSource } from "./explore-read-source"; import { isClassicV3ExploreReleaseReady, mergeClassicV3ExploreModel, @@ -984,39 +988,14 @@ export async function readExploreModel( } const value = (async () => { - if (config.environment === "production") { - const durable = await readDurableExploreModel(config); - if ( - durable.status === "ready" || - (durable.status === "unavailable" && - durable.reason === "stale") - ) { - if (durable.status === "unavailable") { - console.warn("Serving the last verified Explore index", { - reason: durable.reason, - ageSeconds: Math.floor(durable.ageMs / 1_000), - }); - } - const model = durable.envelope.payload.model; - try { - return await enrichExploreModelWithUsd(model, config); - } catch (error) { - console.error("ETH/USD enrichment failed", error); - return model; - } - } - console.warn("Durable Explore index unavailable; using live RPCs", { - reason: durable.reason, - detail: durable.detail, - }); - } - const model = await readReadyRegistryModel(config); - try { - return await enrichExploreModelWithUsd(model, config); - } catch (error) { - console.error("ETH/USD enrichment failed", error); - return model; - } + return resolveExploreReadSource(config, { + readDurable: readDurableExploreModel, + selectFreshDurable: selectFreshDurableExploreModel, + readLive: readReadyRegistryModel, + enrichWithUsd: enrichExploreModelWithUsd, + warn: console.warn, + error: console.error, + }); })().catch((error) => { if (cachedRead?.value === value) cachedRead = undefined; throw error; diff --git a/lib/server/action-rpc-quorum.server.ts b/lib/server/action-rpc-quorum.server.ts new file mode 100644 index 00000000..ff1f5540 --- /dev/null +++ b/lib/server/action-rpc-quorum.server.ts @@ -0,0 +1,391 @@ +import "server-only"; + +import type { Hex } from "viem"; + +import { rpcProviderCommitment } from "../data-pipeline/rpc-provider-commitments"; + +type Environment = Readonly>; +type SupportedChainId = 1 | 11_155_111; +type ActionRpcVendor = + | "alchemy" + | "quicknode" + | "infura" + | "drpc" + | "publicnode" + | "mevblocker" + | "sepolia-org" + | "blastapi" + | "ankr"; + +export type ActionRpcProvider = Readonly<{ + /** Server-only transport value. Deliberately non-enumerable at runtime. */ + endpoint: string; + identity: string; + vendorGroup: ActionRpcVendor; + endpointCommitment: Hex; + endpointOriginCommitment: Hex; +}>; + +type QuorumInput = Readonly<{ + chainId: SupportedChainId; + primary: string | null | undefined; + secondary?: string | null | undefined; + fallbacks?: readonly string[]; + maximumProviders?: number; +}>; + +export class ActionRpcQuorumError extends Error { + readonly code: + | "invalid-provider" + | "provider-not-independent" + | "quorum-unavailable"; + + constructor( + code: ActionRpcQuorumError["code"], + message = "Independent Ethereum RPC providers are unavailable", + ) { + super(message); + this.name = "ActionRpcQuorumError"; + this.code = code; + } + + toJSON() { + return { name: this.name, code: this.code }; + } +} + +const CREDENTIAL = /^[A-Za-z0-9_-]{8,256}$/u; +const QUICKNODE_HOST = + /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+quiknode\.pro$/u; +const BLAST_API_HOST = + /^(?:eth-mainnet|eth-sepolia)(?:\.public)?\.blastapi\.io$/u; + +function hasOnlySearchParameters(value: URL, allowed: readonly string[]) { + return [...value.searchParams.keys()].every((key) => allowed.includes(key)); +} + +function paidDrpcMatches(value: URL, chainId: SupportedChainId) { + if ( + value.hostname !== "lb.drpc.org" || + value.pathname !== "/ogrpc" || + !hasOnlySearchParameters(value, ["network", "dkey"]) || + value.searchParams.size !== 2 + ) { + return false; + } + const expectedNetwork = chainId === 1 ? "ethereum" : "sepolia"; + const key = value.searchParams.get("dkey") ?? ""; + return ( + value.searchParams.get("network") === expectedNetwork && + CREDENTIAL.test(key) + ); +} + +function providerVendor( + value: URL, + chainId: SupportedChainId, +): ActionRpcVendor | null { + const noQuery = value.search === ""; + const rootPath = value.pathname === "/"; + const expectedAlchemyHost = + chainId === 1 + ? "eth-mainnet.g.alchemy.com" + : "eth-sepolia.g.alchemy.com"; + if ( + value.hostname === expectedAlchemyHost && + noQuery && + /^\/v2\/[A-Za-z0-9_-]{8,256}$/u.test(value.pathname) + ) { + return "alchemy"; + } + if ( + QUICKNODE_HOST.test(value.hostname) && + noQuery && + /^\/[A-Za-z0-9_-]{8,256}\/?$/u.test(value.pathname) + ) { + return "quicknode"; + } + const expectedInfuraHost = + chainId === 1 ? "mainnet.infura.io" : "sepolia.infura.io"; + if ( + value.hostname === expectedInfuraHost && + noQuery && + /^\/v3\/[A-Za-z0-9_-]{8,256}$/u.test(value.pathname) + ) { + return "infura"; + } + const expectedDrpcHost = chainId === 1 ? "eth.drpc.org" : "sepolia.drpc.org"; + if ( + (value.hostname === expectedDrpcHost && rootPath && noQuery) || + paidDrpcMatches(value, chainId) + ) { + return "drpc"; + } + const expectedPublicnodeHost = + chainId === 1 + ? "ethereum-rpc.publicnode.com" + : "ethereum-sepolia-rpc.publicnode.com"; + if (value.hostname === expectedPublicnodeHost && rootPath && noQuery) { + return "publicnode"; + } + if ( + chainId === 1 && + value.hostname === "rpc.mevblocker.io" && + rootPath && + noQuery + ) { + return "mevblocker"; + } + if ( + chainId === 11_155_111 && + value.hostname === "rpc.sepolia.org" && + rootPath && + noQuery + ) { + return "sepolia-org"; + } + if ( + BLAST_API_HOST.test(value.hostname) && + value.hostname.startsWith(chainId === 1 ? "eth-mainnet" : "eth-sepolia") && + noQuery && + (rootPath || /^\/[A-Za-z0-9_-]{8,256}\/?$/u.test(value.pathname)) + ) { + return "blastapi"; + } + const expectedAnkrPath = chainId === 1 ? "eth" : "eth_sepolia"; + if ( + value.hostname === "rpc.ankr.com" && + noQuery && + new RegExp( + `^/${expectedAnkrPath}(?:/[A-Za-z0-9_-]{8,256})?/?$`, + "u", + ).test(value.pathname) + ) { + return "ankr"; + } + return null; +} + +function parseProvider( + endpoint: string | null | undefined, + chainId: SupportedChainId, +): ActionRpcProvider { + if (typeof endpoint !== "string" || endpoint.length < 1 || endpoint.length > 1_024) { + throw new ActionRpcQuorumError("invalid-provider"); + } + let parsed: URL; + try { + parsed = new URL(endpoint); + } catch { + throw new ActionRpcQuorumError("invalid-provider"); + } + if ( + parsed.protocol !== "https:" || + parsed.username !== "" || + parsed.password !== "" || + parsed.port !== "" || + parsed.hash !== "" + ) { + throw new ActionRpcQuorumError("invalid-provider"); + } + const vendorGroup = providerVendor(parsed, chainId); + if (!vendorGroup) { + throw new ActionRpcQuorumError("invalid-provider"); + } + + const canonicalEndpoint = parsed.toString(); + const endpointCommitment = rpcProviderCommitment( + "endpoint", + canonicalEndpoint, + ); + const endpointOriginCommitment = rpcProviderCommitment( + "origin", + parsed.origin.toLowerCase(), + ); + const provider = { + identity: `${vendorGroup}-${endpointCommitment.slice(2, 34)}`, + vendorGroup, + endpointCommitment, + endpointOriginCommitment, + } as ActionRpcProvider; + Object.defineProperty(provider, "endpoint", { + value: canonicalEndpoint, + enumerable: false, + configurable: false, + writable: false, + }); + return Object.freeze(provider); +} + +function sameEndpoint(left: ActionRpcProvider, right: ActionRpcProvider) { + return left.endpointCommitment === right.endpointCommitment; +} + +function sameProvider(left: ActionRpcProvider, right: ActionRpcProvider) { + return ( + left.vendorGroup === right.vendorGroup || + left.endpointOriginCommitment === right.endpointOriginCommitment + ); +} + +/** + * Resolves a server-side RPC quorum. The configured primary and secondary must + * represent different vendors and origins. Extra fallbacks can improve + * availability, but aliases of an already selected provider never add a vote. + */ +export function createActionRpcQuorum( + input: QuorumInput, +): readonly ActionRpcProvider[] { + const maximumProviders = input.maximumProviders ?? Number.MAX_SAFE_INTEGER; + if (!Number.isSafeInteger(maximumProviders) || maximumProviders < 2) { + throw new ActionRpcQuorumError("quorum-unavailable"); + } + + const primary = parseProvider(input.primary, input.chainId); + const selected: ActionRpcProvider[] = [primary]; + if (input.secondary) { + const secondary = parseProvider(input.secondary, input.chainId); + if (sameEndpoint(primary, secondary) || sameProvider(primary, secondary)) { + throw new ActionRpcQuorumError("provider-not-independent"); + } + selected.push(secondary); + } + + for (const endpoint of input.fallbacks ?? []) { + if (selected.length >= maximumProviders) break; + const fallback = parseProvider(endpoint, input.chainId); + if ( + selected.some( + (provider) => + sameEndpoint(provider, fallback) || sameProvider(provider, fallback), + ) + ) { + continue; + } + selected.push(fallback); + } + + if (selected.length < 2) { + throw new ActionRpcQuorumError("quorum-unavailable"); + } + return Object.freeze(selected.slice(0, maximumProviders)); +} + +export function tradeActionRpcProviders( + chainId: SupportedChainId, + env: Environment = process.env, +) { + if (chainId === 1) { + const primary = + env.ETHEREUM_RPC_URL ?? "https://ethereum-rpc.publicnode.com"; + const secondary = + env.ETHEREUM_RPC_URL_B ?? + env.ETHEREUM_RPC_URL_SECONDARY ?? + (primary === "https://ethereum-rpc.publicnode.com" + ? "https://rpc.mevblocker.io" + : "https://ethereum-rpc.publicnode.com"); + return createActionRpcQuorum({ + chainId, + primary, + secondary, + maximumProviders: 2, + }); + } + const primary = + env.SEPOLIA_RPC_URL ?? + "https://ethereum-sepolia-rpc.publicnode.com"; + const secondary = + env.SEPOLIA_RPC_URL_B ?? + env.SEPOLIA_RPC_URL_SECONDARY ?? + (primary === "https://ethereum-sepolia-rpc.publicnode.com" + ? "https://rpc.sepolia.org" + : "https://ethereum-sepolia-rpc.publicnode.com"); + return createActionRpcQuorum({ + chainId, + primary, + secondary, + maximumProviders: 2, + }); +} + +export function creatorClaimRpcProviders( + deployment: Readonly<{ + chainId: number; + rpcUrl: string; + rpcUrlSecondary: string | null; + }>, +) { + if (deployment.chainId !== 1 && deployment.chainId !== 11_155_111) { + throw new ActionRpcQuorumError("invalid-provider"); + } + const chainId = deployment.chainId as SupportedChainId; + return createActionRpcQuorum({ + chainId, + primary: deployment.rpcUrl, + secondary: deployment.rpcUrlSecondary, + fallbacks: + chainId === 1 + ? [ + "https://ethereum-rpc.publicnode.com", + "https://rpc.mevblocker.io", + ] + : [ + "https://ethereum-sepolia-rpc.publicnode.com", + "https://rpc.sepolia.org", + ], + maximumProviders: 2, + }); +} + +export function classicV3ActionRpcProviders( + environment: "production" | "rehearsal", + env: Environment = process.env, +) { + const chainId = environment === "production" ? 1 : 11_155_111; + const primary = + environment === "production" + ? env.ETHEREUM_RPC_URL ?? "https://eth.drpc.org" + : env.SEPOLIA_RPC_URL ?? "https://sepolia.drpc.org"; + const secondary = + environment === "production" + ? env.ETHEREUM_RPC_URL_B ?? + env.ETHEREUM_RPC_URL_SECONDARY ?? + "https://ethereum-rpc.publicnode.com" + : env.SEPOLIA_RPC_URL_B ?? + env.SEPOLIA_RPC_URL_SECONDARY ?? + "https://ethereum-sepolia-rpc.publicnode.com"; + return createActionRpcQuorum({ + chainId, + primary, + secondary, + fallbacks: + environment === "production" + ? ["https://rpc.mevblocker.io"] + : ["https://rpc.sepolia.org"], + maximumProviders: 2, + }); +} + +export function stockPairedActionRpcProviders( + env: Environment = process.env, +) { + const primary = + env.ETHEREUM_RPC_URL ?? "https://ethereum-rpc.publicnode.com"; + const secondary = + env.ETHEREUM_RPC_URL_B ?? + env.ETHEREUM_RPC_URL_SECONDARY ?? + (primary === "https://ethereum-rpc.publicnode.com" + ? "https://rpc.mevblocker.io" + : "https://ethereum-rpc.publicnode.com"); + return createActionRpcQuorum({ + chainId: 1, + primary, + secondary, + fallbacks: [ + "https://ethereum-rpc.publicnode.com", + "https://rpc.mevblocker.io", + "https://eth.drpc.org", + ], + maximumProviders: 5, + }); +} diff --git a/lib/stock-paired-access.ts b/lib/stock-paired-access.ts index f1d16a9a..d21d4d5f 100644 --- a/lib/stock-paired-access.ts +++ b/lib/stock-paired-access.ts @@ -1,41 +1,26 @@ -const STOCK_PAIRED_DEV_ACCOUNTS = new Set([ - "0x2bb333d48dfaf1596d9036671d2e43168994249e", -]); - export type StockPairedPublicLaunchRelease = { internalContractRelease: string; chainId: number; }; -export const STOCK_PAIRED_NEW_LAUNCHES_ENABLED = true; +export const STOCK_PAIRED_NEW_LAUNCHES_ENABLED = false; export function isStockPairedDevAccount( account: string | null | undefined, ) { - return Boolean( - STOCK_PAIRED_NEW_LAUNCHES_ENABLED && - account && - /^0x[a-fA-F0-9]{40}$/.test(account) && - STOCK_PAIRED_DEV_ACCOUNTS.has(account.toLowerCase()), - ); + void account; + return STOCK_PAIRED_NEW_LAUNCHES_ENABLED; } export function isStockPairedPublicLaunchEnabled( environment: "production" | "rehearsal", release: StockPairedPublicLaunchRelease | null, ) { - return Boolean( - STOCK_PAIRED_NEW_LAUNCHES_ENABLED && - environment === "production" && - release?.internalContractRelease === "stock-paired-v3" && - release.chainId === 1, - ); + void environment; + void release; + return STOCK_PAIRED_NEW_LAUNCHES_ENABLED; } export function isStockPairedLocalPreviewEnabled() { - return ( - STOCK_PAIRED_NEW_LAUNCHES_ENABLED && - process.env.NODE_ENV !== "production" && - process.env.NEXT_PUBLIC_STOCK_PAIRED_UI_PREVIEW === "true" - ); + return STOCK_PAIRED_NEW_LAUNCHES_ENABLED; } diff --git a/package-lock.json b/package-lock.json index 42ecc711..e41e4b6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ "@vercel/blob": "2.6.1", "lucide-react": "1.26.0", "next": "16.2.11", + "postgres": "3.4.7", "react": "19.2.8", "react-dom": "19.2.8", "server-only": "0.0.1", "viem": "2.55.5" }, "devDependencies": { + "@electric-sql/pglite": "0.5.4", "@types/node": "26.1.1", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", @@ -32,6 +34,9 @@ "eslint-config-next": "16.2.11", "typescript": "5.9.3", "vitest": "4.1.10" + }, + "engines": { + "node": ">=24.14.0 <25" } }, "node_modules/@adraffy/ens-normalize": { @@ -552,6 +557,13 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.4.tgz", + "integrity": "sha512-yYZUyyXrHU7tPlCjwZQJ6hIG9DscdCCn7Uk0mYKwC1FeHX286AbcmFveMiRBEak8e9iPupjsoVImN3yJZVed2g==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -9377,9 +9389,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -13923,9 +13935,9 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -14842,6 +14854,19 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/preact": { "version": "10.24.2", "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", diff --git a/package.json b/package.json index 5d4ae077..108c1b59 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "launcher-v4", "version": "0.1.0", "private": true, + "engines": { + "node": ">=24.14.0 <25" + }, "scripts": { "dev": "next dev", "dev:sepolia": "PROGRAMMABLE_ONCHAIN_NETWORK=rehearsal NEXT_PUBLIC_PROGRAMMABLE_ONCHAIN_NETWORK=rehearsal next dev", @@ -10,10 +13,12 @@ "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run", + "db:test:pglite": "node scripts/run-pglite-db-tests.mjs", + "projector:provider-commitments": "node scripts/compute-projector-provider-commitments.mjs", "verify:uniswap-launcher-sdk": "node scripts/verify-uniswap-liquidity-launcher-sdk.mjs", "brand:favicons": "node scripts/generate-programmable-favicons.mjs", "audit:prod": "npm audit --omit=dev --audit-level=moderate", - "verify": "npm run verify:uniswap-launcher-sdk && npm run contracts:bootstrap && npm run contracts:build && npm run lint && npm run typecheck && npm run test && npm run build", + "verify": "npm run verify:uniswap-launcher-sdk && npm run contracts:bootstrap && npm run contracts:build && npm run lint && npm run typecheck && npm run test && npm run perf:read-model:smoke && npm run perf:read-model:ops-gate && npm run perf:read-model:release-if-present && npm run build", "contracts:bootstrap": "cd contracts && ./scripts/bootstrap-deps.sh", "contracts:fmt": "cd contracts && forge fmt --check", "contracts:lint": "cd contracts && forge lint src script", @@ -118,7 +123,16 @@ "contracts:v2:sepolia:test2": "MEME_LAUNCH_FIXTURE_JSON=contracts/release/test2-classic-v2.json node scripts/serve-sepolia-meme-launch.mjs", "contracts:sepolia:lifecycle:verify": "node contracts/scripts/verify-sepolia-meme-lifecycle.mjs", "release:uniswap-hook:prepare": "node scripts/prepare-uniswap-hook-release.mjs", - "release:uniswap-hook:test": "vitest run tests/uniswap-hook-release.test.ts" + "release:uniswap-hook:test": "vitest run tests/uniswap-hook-release.test.ts", + "perf:read-model:smoke": "node scripts/perf/read-model-smoke.mjs", + "perf:read-model:gate": "node scripts/perf/read-model-gate.mjs --require-release-evidence", + "perf:read-model:release-if-present": "node scripts/perf/read-model-gate.mjs --require-release-evidence --if-present", + "perf:read-model:capture": "node scripts/perf/read-model-capture.mjs", + "perf:read-model:deploy-policy": "node scripts/perf/read-model-deploy-policy.mjs", + "perf:read-model:staged-deployment": "node scripts/perf/read-model-staged-deployment.mjs", + "perf:read-model:ops-gate": "node scripts/perf/read-model-ops-source-contracts.mjs", + "perf:read-model:production-binding": "node scripts/perf/read-model-production-binding.mjs", + "perf:read-model:post-promotion": "node scripts/perf/read-model-post-promotion.mjs" }, "dependencies": { "@privy-io/node": "^0.27.0", @@ -130,12 +144,14 @@ "@vercel/blob": "2.6.1", "lucide-react": "1.26.0", "next": "16.2.11", + "postgres": "3.4.7", "react": "19.2.8", "react-dom": "19.2.8", "server-only": "0.0.1", "viem": "2.55.5" }, "devDependencies": { + "@electric-sql/pglite": "0.5.4", "@types/node": "26.1.1", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", diff --git a/scripts/compute-projector-provider-commitments.mjs b/scripts/compute-projector-provider-commitments.mjs new file mode 100644 index 00000000..8da53f9b --- /dev/null +++ b/scripts/compute-projector-provider-commitments.mjs @@ -0,0 +1,77 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +import { createServer } from "vite"; + +const workspace = fileURLToPath(new URL("../", import.meta.url)); +const binding = JSON.parse( + await readFile( + new URL("../config/data-pipeline-release.v1.json", import.meta.url), + "utf8", + ), +); +const vite = await createServer({ + root: workspace, + configFile: false, + appType: "custom", + logLevel: "silent", + server: { middlewareMode: true }, + ssr: { noExternal: ["server-only"] }, + plugins: [{ + name: "projector-commitment-server-only-boundary", + enforce: "pre", + resolveId(id) { + return id === "server-only" ? "\0projector-server-only" : null; + }, + load(id) { + return id === "\0projector-server-only" ? "export {};" : null; + }, + }], +}); + +try { + const commitments = await vite.ssrLoadModule( + "/lib/data-pipeline/projector-provider-commitments.ts", + ); + const alchemy = commitments.canonicalProjectorRpcEndpoint( + process.env.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL, + "alchemy", + ); + const quicknode = commitments.canonicalProjectorRpcEndpoint( + process.env.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL, + "quicknode", + ); + const envioEndpoint = commitments.canonicalProjectorEnvioEndpoint( + process.env.PROGRAMMABLE_ENVIO_GRAPHQL_URL, + binding.envio.graphqlEndpoint, + ); + const expectedEnvioIdentity = `envio:${binding.envio.deploymentLabel}`; + const envioIdentity = + process.env.PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY ?? + expectedEnvioIdentity; + if (envioIdentity !== expectedEnvioIdentity) { + throw new Error("Envio identity does not match the reviewed release binding"); + } + const rpcSchema = commitments.projectorRpcSchemaCommitment(); + const output = { + envioDeploymentCommitment: + commitments.projectorEnvioDeploymentCommitment({ + endpoint: envioEndpoint, + redactedIdentity: envioIdentity, + binding, + }), + envioSchemaCommitment: + commitments.projectorEnvioSchemaCommitment(binding), + alchemyDeploymentCommitment: + commitments.projectorRpcDeploymentCommitment(alchemy), + alchemySchemaCommitment: rpcSchema, + quicknodeDeploymentCommitment: + commitments.projectorRpcDeploymentCommitment(quicknode), + quicknodeSchemaCommitment: rpcSchema, + }; + for (const [name, value] of Object.entries(output)) { + process.stdout.write(`${name}=${value}\n`); + } +} finally { + await vite.close(); +} diff --git a/scripts/data-pipeline/bootstrap-evidence.mjs b/scripts/data-pipeline/bootstrap-evidence.mjs new file mode 100644 index 00000000..f2195782 --- /dev/null +++ b/scripts/data-pipeline/bootstrap-evidence.mjs @@ -0,0 +1,2261 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import { + bytesToHex, + concat, + encodeAbiParameters, + hexToBytes, + keccak256, + parseAbiItem, + toBytes, + toEventSelector, + toFunctionSelector, +} from "viem"; + +import { + BOOTSTRAP_PLAN_KIND, + canonicalJson, + sha256, +} from "./hosted-db-operator-core.mjs"; + +const ADDRESS = /^0x[0-9a-f]{40}$/u; +const BYTES32 = /^0x[0-9a-f]{64}$/u; +const SELECTOR = /^0x[0-9a-f]{8}$/u; +const UUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u; +const NONZERO_BYTES32 = /^0x(?!0{64}$)[0-9a-f]{64}$/u; +const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u; +const POSITIVE_INTEGER_TEXT = /^[1-9][0-9]*$/u; +const NONNEGATIVE_INTEGER_TEXT = /^(?:0|[1-9][0-9]*)$/u; +const EXACT_RELEASES = Object.freeze([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +]); +const POOL_MANAGER_WORD = + "0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90"; +const COMMITMENT_DOMAIN = "programmable:data-pipeline:bootstrap-commitment:v1"; +const IMMUTABLE_REFERENCE_DOMAIN = toBytes( + "programmable:data-pipeline:immutable-references:v1\0", +); + +function exactCommitment(label, value) { + return sha256(`${COMMITMENT_DOMAIN}\0${label}\0${canonicalJson(value)}`); +} + +function deterministicUuid(label, value) { + const raw = exactCommitment(`uuid:${label}`, value).slice(2, 34).split(""); + raw[12] = "5"; + raw[16] = (8 + (Number.parseInt(raw[16], 16) & 3)).toString(16); + return `${raw.slice(0, 8).join("")}-${raw.slice(8, 12).join("")}-${raw.slice(12, 16).join("")}-${raw.slice(16, 20).join("")}-${raw.slice(20).join("")}`; +} + +function canonicalNonzeroBytes32(value, label) { + const result = canonicalBytes32(value, label); + if (!NONZERO_BYTES32.test(result)) throw new Error(`${label} is zero`); + return result; +} + +function normalizeAbiParameter(parameter) { + const normalized = { + name: parameter.name ?? "", + type: parameter.type, + indexed: parameter.indexed ?? false, + }; + if (Array.isArray(parameter.components)) { + normalized.components = parameter.components.map(normalizeAbiParameter); + } + return normalized; +} + +function normalizeAbiEvent(event) { + return { + type: "event", + name: event.name, + anonymous: event.anonymous ?? false, + inputs: event.inputs.map(normalizeAbiParameter), + }; +} + +function authorizedAbiEventEvidence(signatures, artifact, contractName) { + if (!Array.isArray(signatures) || signatures.length < 1) { + throw new Error(`authorized ABI event set is empty: ${contractName}`); + } + const artifactEvents = artifact.abi.filter(({ type }) => type === "event"); + const events = signatures.map((signature) => { + const parsed = parseAbiItem(`event ${signature}`); + const selector = toEventSelector(parsed); + const expected = normalizeAbiEvent(parsed); + const matches = artifactEvents.filter( + (candidate) => + candidate.name === parsed.name && + toEventSelector(candidate) === selector && + canonicalJson(normalizeAbiEvent(candidate)) === canonicalJson(expected), + ); + if (matches.length !== 1) { + throw new Error(`artifact ABI event drift: ${contractName}.${parsed.name}`); + } + return { signature, selector, abi: expected }; + }); + const eventNames = events.map(({ abi }) => abi.name); + if (new Set(eventNames).size !== eventNames.length) { + throw new Error(`overloaded event names are unsupported: ${contractName}`); + } + return Object.freeze({ + eventNames: Object.freeze(eventNames), + commitment: sha256( + Buffer.from( + `programmable:data-pipeline:abi-event-set:v1\0${canonicalJson({ + contractName, + events, + })}`, + "utf8", + ), + ), + }); +} + +function exactRecoverySelector(artifact, configured, contractName) { + const launchFunctions = artifact.abi.filter( + (item) => item.type === "function" && item.name === "launch", + ); + if (configured === null) { + if (launchFunctions.length !== 0) { + throw new Error(`recovery selector is missing: ${contractName}`); + } + return null; + } + if (!SELECTOR.test(configured ?? "")) { + throw new Error(`recovery selector is invalid: ${contractName}`); + } + if ( + launchFunctions.length !== 1 || + toFunctionSelector(launchFunctions[0]) !== configured + ) { + throw new Error(`recovery selector drift: ${contractName}`); + } + return configured; +} + +function canonicalAddress(value, label) { + const result = typeof value === "string" ? value.toLowerCase() : ""; + if (!ADDRESS.test(result)) throw new Error(`${label} is not an address`); + return result; +} + +function canonicalBytes32(value, label) { + const result = typeof value === "string" ? value.toLowerCase() : ""; + if (!BYTES32.test(result)) throw new Error(`${label} is not bytes32`); + return result; +} + +function eventName(signature) { + const match = /^([A-Za-z][A-Za-z0-9]*)\(/u.exec(signature); + if (!match) throw new Error("event declaration is invalid"); + return match[1]; +} + +function immutableReferences(artifact) { + const groups = artifact?.deployedBytecode?.immutableReferences; + if (groups === null || typeof groups !== "object" || Array.isArray(groups)) { + return []; + } + return Object.values(groups) + .flat() + .map(({ start, length }) => ({ start, length })) + .sort((left, right) => left.start - right.start || left.length - right.length); +} + +function immutableReferencesCommitment(references, runtimeCodeLength) { + return keccak256( + concat([ + IMMUTABLE_REFERENCE_DOMAIN, + encodeAbiParameters( + [{ type: "uint32" }, { type: "uint32[]" }, { type: "uint32[]" }], + [ + runtimeCodeLength, + references.map(({ start }) => start), + references.map(({ length }) => length), + ], + ), + ]), + ); +} + +function normalizedRuntimeHash(runtimeCode, references) { + const bytes = Uint8Array.from(hexToBytes(runtimeCode)); + for (const { start, length } of references) { + bytes.fill(0, start, start + length); + } + return keccak256(bytesToHex(bytes)); +} + +async function loadArtifact(workspace, artifactName) { + if (!/^[A-Z][A-Za-z0-9]{0,127}$/u.test(artifactName ?? "")) { + throw new Error("artifact name is invalid"); + } + const relativePath = `contracts/out/${artifactName}.sol/${artifactName}.json`; + const absolutePath = path.join(workspace, relativePath); + const bytes = await readFile(absolutePath); + const artifact = JSON.parse(bytes.toString("utf8")); + const creationCode = artifact?.bytecode?.object; + const runtimeCode = artifact?.deployedBytecode?.object; + if ( + !Array.isArray(artifact?.abi) || + typeof creationCode !== "string" || !/^0x(?:[0-9a-f]{2})+$/u.test(creationCode) || + typeof runtimeCode !== "string" || !/^0x(?:[0-9a-f]{2})+$/u.test(runtimeCode) || + artifact.bytecode?.linkReferences === null || + typeof artifact.bytecode?.linkReferences !== "object" || + Array.isArray(artifact.bytecode.linkReferences) || + Object.keys(artifact.bytecode.linkReferences).length !== 0 || + artifact.deployedBytecode?.linkReferences === null || + typeof artifact.deployedBytecode?.linkReferences !== "object" || + Array.isArray(artifact.deployedBytecode.linkReferences) || + Object.keys(artifact.deployedBytecode.linkReferences).length !== 0 + ) { + throw new Error(`artifact bytecode is invalid: ${artifactName}`); + } + const references = immutableReferences(artifact); + const runtimeCodeLength = (runtimeCode.length - 2) / 2; + return Object.freeze({ + artifactName, + abi: Object.freeze(artifact.abi), + relativePath, + fileSha256: sha256(bytes), + creationCodeHash: keccak256(creationCode), + runtimeTemplateHash: keccak256(runtimeCode), + normalizedRuntimeCodeHash: + references.length === 0 + ? keccak256(runtimeCode) + : normalizedRuntimeHash(runtimeCode, references), + immutableReferences: Object.freeze(references), + immutableReferencesCommitment: + references.length === 0 + ? null + : immutableReferencesCommitment(references, runtimeCodeLength), + runtimeCodeLength, + }); +} + +function manifestDeploymentEvidence(manifest, source) { + const verification = + manifest?.sourceVerification?.contracts?.[source.deploymentKey] ?? + manifest?.sourceVerification?.[source.deploymentKey]; + const transactionEntry = manifest?.transactions?.[source.deploymentKey]; + const transactionHash = + typeof transactionEntry === "string" + ? transactionEntry + : typeof transactionEntry?.transactionHash === "string" + ? transactionEntry.transactionHash + : typeof verification?.deploymentTransaction === "string" + ? verification.deploymentTransaction + : null; + const deploymentBlock = Number.isSafeInteger(verification?.deploymentBlock) + ? verification.deploymentBlock + : Number.isSafeInteger(transactionEntry?.blockNumber) + ? transactionEntry.blockNumber + : Number.isSafeInteger(manifest?.deploymentBlocks?.[source.deploymentKey]) + ? manifest.deploymentBlocks[source.deploymentKey] + : null; + return Object.freeze({ + releaseCommit: + typeof manifest.releaseCommit === "string" ? manifest.releaseCommit : null, + sourceCommitment: + typeof manifest.sourceCommitment === "string" + ? canonicalBytes32(manifest.sourceCommitment, "manifest source commitment") + : null, + transactionHash: + typeof transactionHash === "string" + ? canonicalBytes32(transactionHash, "deployment transaction") + : null, + deploymentBlock, + sourceVerificationStatus: + typeof verification?.status === "string" ? verification.status : null, + }); +} + +function providerBinding(provider, createdAt) { + const identity = { + providerType: provider.providerType, + redactedIdentity: provider.redactedIdentity, + deploymentCommitment: canonicalNonzeroBytes32( + provider.deploymentCommitment, + "provider deployment commitment", + ), + schemaCommitment: canonicalNonzeroBytes32( + provider.schemaCommitment, + "provider schema commitment", + ), + }; + const rpc = provider.providerType === "rpc_provider" + ? { + chainId: provider.chainId, + vendor: provider.vendor, + constructorVersion: provider.constructorVersion, + endpointUrlCommitment: canonicalNonzeroBytes32( + provider.endpointUrlCommitment, + "RPC endpoint URL commitment", + ), + endpointOriginCommitment: canonicalNonzeroBytes32( + provider.endpointOriginCommitment, + "RPC endpoint origin commitment", + ), + endpointEvidenceDomain: provider.endpointEvidenceDomain, + } + : null; + const providerDeploymentId = deterministicUuid("provider", { ...identity, rpc }); + const endpointEvidenceCommitment = rpc === null + ? null + : exactCommitment("rpc-endpoint-evidence", { + providerDeploymentId, + ...rpc, + }); + const inputCommitment = exactCommitment("provider-registration", { + providerDeploymentId, + ...identity, + rpc, + endpointEvidenceCommitment, + }); + return Object.freeze({ + ...provider, + providerDeploymentId, + ...(rpc === null ? {} : { endpointEvidenceCommitment }), + inputCommitment, + createdAt, + }); +} + +function projectionRule({ + epochId, + contractName, + sourceRole, + signature, + ordinal, + authority, +}) { + const type = eventName(signature); + const exact = authority.get(`${contractName}\0${type}`); + if (!exact || exact.sourceRole !== sourceRole) { + throw new Error(`event has no exact projector authority: ${contractName}.${type}`); + } + const projectionKind = exact.projectionKind; + if (!IDENTIFIER.test(projectionKind ?? "")) { + throw new Error(`projector kind is invalid: ${contractName}.${type}`); + } + const value = { epochId, projectionKind, sourceRole, eventType: type }; + return Object.freeze({ + ordinal, + projectionEventRuleId: deterministicUuid("projection-event-rule", value), + ...value, + ruleCommitment: exactCommitment("projection-event-rule", value), + }); +} + +function exactObjectKeys(value, expected, label) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + canonicalJson(Object.keys(value).sort()) !== + canonicalJson(expected.slice().sort()) + ) { + throw new Error(`${label} shape is invalid`); + } +} + +function validateCandidateEnvioEvidence(evidence) { + exactObjectKeys( + evidence, + [ + "path", + "fileSha256", + "status", + "deploymentLabel", + "graphqlEndpoint", + "schemaVersion", + "sourceCommit", + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "eventCount", + "redactedIdentity", + "deploymentCommitment", + "schemaCommitment", + "auditEvidenceCommitment", + "policyCommitment", + ], + "candidate Envio evidence", + ); + if ( + evidence.path !== "config/data-pipeline-envio-candidate.v1.json" || + !NONZERO_BYTES32.test(evidence.fileSha256 ?? "") || + evidence.status !== "deployed-synced-audited-not-promoted" || + evidence.schemaVersion !== "1" || + !/^[a-z0-9][a-z0-9-]{0,127}$/u.test(evidence.deploymentLabel ?? "") || + evidence.redactedIdentity !== `envio:${evidence.deploymentLabel}` || + !/^https:\/\/indexer\.hyperindex\.xyz\/[a-z0-9]{7,64}\/v1\/graphql$/u.test( + evidence.graphqlEndpoint ?? "", + ) || + !/^[0-9a-f]{40}$/u.test(evidence.sourceCommit ?? "") || + !NONZERO_BYTES32.test(evidence.configSha256 ?? "") || + !NONZERO_BYTES32.test(evidence.schemaSha256 ?? "") || + !NONZERO_BYTES32.test(evidence.handlerSha256 ?? "") || + !NONZERO_BYTES32.test(evidence.sourceRegistrySha256 ?? "") || + !NONZERO_BYTES32.test(evidence.eventSetSha256 ?? "") || + !Number.isSafeInteger(evidence.eventCount) || + evidence.eventCount < 1 || + !NONZERO_BYTES32.test(evidence.deploymentCommitment ?? "") || + !NONZERO_BYTES32.test(evidence.schemaCommitment ?? "") || + !NONZERO_BYTES32.test(evidence.auditEvidenceCommitment ?? "") || + !NONZERO_BYTES32.test(evidence.policyCommitment ?? "") + ) { + throw new Error("candidate Envio evidence is invalid"); + } +} + +function validateCanonicalReleaseCandidate(binding, candidateEnvioEvidence) { + const canonical = binding?.envio; + exactObjectKeys( + canonical, + [ + "deploymentLabel", + "graphqlEndpoint", + "schemaVersion", + "sourceCommit", + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "eventCount", + ], + "canonical Envio release binding", + ); + const candidate = { + deploymentLabel: candidateEnvioEvidence.deploymentLabel, + graphqlEndpoint: candidateEnvioEvidence.graphqlEndpoint, + schemaVersion: candidateEnvioEvidence.schemaVersion, + sourceCommit: candidateEnvioEvidence.sourceCommit, + configSha256: candidateEnvioEvidence.configSha256, + schemaSha256: candidateEnvioEvidence.schemaSha256, + handlerSha256: candidateEnvioEvidence.handlerSha256, + sourceRegistrySha256: candidateEnvioEvidence.sourceRegistrySha256, + eventSetSha256: candidateEnvioEvidence.eventSetSha256, + eventCount: candidateEnvioEvidence.eventCount, + }; + if ( + canonicalJson(canonical) !== canonicalJson(candidate) || + `envio:${canonical.deploymentLabel}` !== + candidateEnvioEvidence.redactedIdentity + ) { + throw new Error("canonical Envio release binding is not the audited candidate"); + } + return Object.freeze({ + identity: candidateEnvioEvidence.redactedIdentity, + endpoint: candidateEnvioEvidence.graphqlEndpoint, + }); +} + +function validateProviderSet(providers, candidateEnvioEvidence) { + validateCandidateEnvioEvidence(candidateEnvioEvidence); + if (!Array.isArray(providers) || providers.length !== 4) { + throw new Error("bootstrap provider set is incomplete"); + } + if ( + canonicalJson( + providers.map(({ providerType, vendor = null }) => ({ + providerType, + vendor, + })), + ) !== + canonicalJson([ + { providerType: "envio_deployment", vendor: null }, + { providerType: "rpc_provider", vendor: "alchemy" }, + { providerType: "rpc_provider", vendor: "quicknode" }, + { providerType: "uniswap_subgraph", vendor: null }, + ]) + ) { + throw new Error("bootstrap provider order is not canonical"); + } + const envio = providers.filter( + ({ providerType }) => providerType === "envio_deployment", + ); + const rpc = providers.filter( + ({ providerType }) => providerType === "rpc_provider", + ); + const graph = providers.filter( + ({ providerType }) => providerType === "uniswap_subgraph", + ); + if ( + envio.length !== 1 || + rpc.length !== 2 || + graph.length !== 1 || + envio[0].redactedIdentity !== candidateEnvioEvidence.redactedIdentity || + envio[0].deploymentCommitment !== + candidateEnvioEvidence.deploymentCommitment || + envio[0].schemaCommitment !== candidateEnvioEvidence.schemaCommitment + ) { + throw new Error("bootstrap provider set does not match candidate evidence"); + } + exactObjectKeys( + envio[0], + [ + "providerType", + "redactedIdentity", + "deploymentCommitment", + "schemaCommitment", + ], + "candidate Envio provider", + ); + exactObjectKeys( + graph[0], + [ + "providerType", + "redactedIdentity", + "deploymentCommitment", + "schemaCommitment", + "subgraphId", + "deployment", + ], + "Uniswap subgraph provider", + ); + const vendors = rpc.map(({ vendor }) => vendor).sort(); + if (canonicalJson(vendors) !== canonicalJson(["alchemy", "quicknode"])) { + throw new Error("bootstrap RPC vendors are not the canonical independent pair"); + } + for (const provider of rpc) { + exactObjectKeys( + provider, + [ + "providerType", + "redactedIdentity", + "vendor", + "chainId", + "constructorVersion", + "endpointUrlCommitment", + "endpointOriginCommitment", + "endpointEvidenceDomain", + "deploymentCommitment", + "schemaCommitment", + ], + "RPC provider", + ); + if ( + provider.chainId !== 1 || + provider.redactedIdentity !== `rpc:1:${provider.vendor}` || + provider.constructorVersion !== "rpc-provider-v1" || + provider.endpointEvidenceDomain !== "rpc-endpoint-commitments-v1" + ) { + throw new Error("bootstrap RPC endpoint evidence is invalid"); + } + canonicalNonzeroBytes32( + provider.endpointUrlCommitment, + "RPC endpoint URL commitment", + ); + canonicalNonzeroBytes32( + provider.endpointOriginCommitment, + "RPC endpoint origin commitment", + ); + } + if ( + graph[0].redactedIdentity !== + `uniswap-v4:ethereum:${graph[0].deployment}` || + !/^[1-9A-HJ-NP-Za-km-z]{8,96}$/u.test(graph[0].subgraphId ?? "") || + !/^[1-9A-HJ-NP-Za-km-z]{8,96}$/u.test(graph[0].deployment ?? "") + ) { + throw new Error("Uniswap subgraph evidence is invalid"); + } + if (new Set(providers.map(({ redactedIdentity }) => redactedIdentity)).size !== 4) { + throw new Error("bootstrap provider identities are not unique"); + } +} + +function validateCatalogAuthority(catalog, binding) { + exactObjectKeys( + catalog, + [ + "schemaVersion", + "chainId", + "sourceGroup", + "catalogVersion", + "createdAt", + "releaseBindingPath", + "dynamicBindingSpecs", + "releases", + ], + "bootstrap semantic catalog", + ); + if ( + catalog.schemaVersion !== 1 || + catalog.chainId !== binding.chainId || + catalog.sourceGroup !== "core" || + !IDENTIFIER.test(catalog.catalogVersion ?? "") || + catalog.releaseBindingPath !== "config/data-pipeline-release.v1.json" || + typeof catalog.createdAt !== "string" || + Number.isNaN(Date.parse(catalog.createdAt)) || + new Date(catalog.createdAt).toISOString() !== catalog.createdAt || + catalog.dynamicBindingSpecs === null || + typeof catalog.dynamicBindingSpecs !== "object" || + Array.isArray(catalog.dynamicBindingSpecs) || + !Array.isArray(catalog.releases) || + canonicalJson(catalog.releases.map(({ releaseId }) => releaseId)) !== + canonicalJson(EXACT_RELEASES) + ) { + throw new Error("bootstrap semantic catalog is invalid"); + } + for (const release of catalog.releases) { + exactObjectKeys( + release, + [ + "releaseId", + "modelId", + "activation", + "deploymentManifestPath", + "sources", + "dynamicSources", + "launchRequirements", + ], + "bootstrap semantic release", + ); + exactObjectKeys( + release.activation, + ["epochNumber", "expectedGeneration", "nextGeneration"], + "bootstrap release activation", + ); + if ( + release.activation.epochNumber !== 1 || + release.activation.expectedGeneration !== 0 || + release.activation.nextGeneration !== 1 || + !/^contracts\/deployments\/mainnet-[a-z0-9-]+\.json$/u.test( + release.deploymentManifestPath ?? "", + ) || + !Array.isArray(release.sources) || + !Array.isArray(release.dynamicSources) || + !Array.isArray(release.launchRequirements) + ) { + throw new Error(`bootstrap release authority is invalid: ${release.releaseId}`); + } + const expectedModel = release.releaseId.startsWith("classic-") + ? "classic" + : "stock-paired"; + if ( + release.modelId !== expectedModel || + release.sources.length < 1 || + release.launchRequirements.length < 1 || + new Set(release.sources.map(({ contractName }) => contractName)).size !== + release.sources.length || + new Set( + release.dynamicSources.map(({ contractName }) => contractName), + ).size !== release.dynamicSources.length + ) { + throw new Error(`bootstrap release coverage is invalid: ${release.releaseId}`); + } + for (const source of release.sources) { + exactObjectKeys( + source, + [ + "contractName", + "sourceRole", + "sourceType", + "artifact", + "deploymentKey", + "recoverySelector", + ], + "bootstrap static source", + ); + if ( + source.sourceType !== "ethereum_contract" || + !IDENTIFIER.test(source.contractName ?? "") || + !IDENTIFIER.test(source.sourceRole ?? "") || + !/^[A-Z][A-Za-z0-9]{0,127}$/u.test(source.artifact ?? "") || + !IDENTIFIER.test(source.deploymentKey ?? "") + ) { + throw new Error(`bootstrap source semantics are invalid: ${source.contractName}`); + } + } + for (const dynamic of release.dynamicSources) { + exactObjectKeys( + dynamic, + [ + "contractName", + "artifact", + "parentContractName", + "parentSourceRole", + "factoryEventType", + "deployedAddressField", + "deployedSourceRole", + "bindingSpec", + "factoryConfigurationField", + "bindingPolicy", + ], + "bootstrap dynamic source", + ); + if ( + dynamic.deployedAddressField !== "vault" || + dynamic.deployedSourceRole !== "reward_vault" || + !/^[A-Z][A-Za-z0-9]{0,127}$/u.test(dynamic.artifact ?? "") || + !IDENTIFIER.test(dynamic.parentContractName ?? "") || + !IDENTIFIER.test(dynamic.parentSourceRole ?? "") || + !IDENTIFIER.test(dynamic.factoryEventType ?? "") || + !IDENTIFIER.test(dynamic.bindingSpec ?? "") || + ![ + "factory-event-and-constants", + "factory-event-constants-and-deferred-allocation", + ].includes(dynamic.bindingPolicy) || + (dynamic.bindingPolicy === "factory-event-and-constants" && + typeof dynamic.factoryConfigurationField !== "string") || + (dynamic.bindingPolicy === + "factory-event-constants-and-deferred-allocation" && + dynamic.factoryConfigurationField !== null) + ) { + throw new Error(`bootstrap dynamic semantics are invalid: ${dynamic.contractName}`); + } + } + const requirementKeys = new Set(); + for (const requirement of release.launchRequirements) { + exactObjectKeys( + requirement, + ["occurrenceRole", "eventType", "requiredWhen"], + "bootstrap launch requirement", + ); + const key = `${requirement.occurrenceRole}\0${requirement.eventType}`; + if ( + !IDENTIFIER.test(requirement.occurrenceRole ?? "") || + !IDENTIFIER.test(requirement.eventType ?? "") || + !["always", "reward_vault", "locked_custody", "eth_funded"].includes( + requirement.requiredWhen, + ) || + requirementKeys.has(key) + ) { + throw new Error( + `bootstrap launch requirement is invalid: ${release.releaseId}`, + ); + } + requirementKeys.add(key); + } + } +} + +function validateDeploymentManifest(manifest, semantic) { + if ( + manifest?.chainId !== 1 || + manifest?.lifecycleEvidence?.status !== "verified-current-release" || + manifest?.lifecycleEvidence?.releaseEligible !== true || + !["match", "verified"].includes(manifest?.sourceVerification?.status) || + !/^[0-9a-f]{40}$/u.test(manifest?.releaseCommit ?? "") || + !NONZERO_BYTES32.test( + typeof manifest?.sourceCommitment === "string" + ? manifest.sourceCommitment.toLowerCase() + : "", + ) || + manifest.addresses === null || + typeof manifest.addresses !== "object" || + Array.isArray(manifest.addresses) + ) { + throw new Error(`deployment manifest is not release eligible: ${semantic.releaseId}`); + } +} + +function validateDynamicBindingSpec({ + spec, + dynamic, + artifact, + factoryEvent, + manifest, +}) { + exactObjectKeys( + spec, + [ + "factoryConfigurationField", + "normalizedRuntimeCodeHash", + "immutableReferencesCommitment", + "runtimeCodeLength", + "bindings", + ], + "dynamic immutable binding specification", + ); + if ( + dynamic.factoryConfigurationField !== spec.factoryConfigurationField || + !Array.isArray(spec.bindings) || + spec.bindings.length !== artifact.immutableReferences.length + ) { + throw new Error(`dynamic immutable binding coverage drift: ${dynamic.contractName}`); + } + const eventFields = new Map( + factoryEvent.inputs.map((input) => [input.name, input.type]), + ); + const reviewedConstants = new Set([POOL_MANAGER_WORD]); + if (typeof manifest.addresses.ctoAuthority === "string") { + reviewedConstants.add( + `0x${"0".repeat(24)}${canonicalAddress( + manifest.addresses.ctoAuthority, + "CTO authority", + ).slice(2)}`, + ); + } + let deferredConfiguration = 0; + let deferredBeneficiaryCount = 0; + for (const [index, binding] of spec.bindings.entries()) { + const expectedReference = artifact.immutableReferences[index]; + const expectedKeys = ["ordinal", "offset", "length", "source", "encoding"]; + if (binding?.source === "factory_event") expectedKeys.push("field"); + if (binding?.source === "constant") expectedKeys.push("value"); + if (binding?.source === "deferred_allocation_evidence") { + expectedKeys.push("evidenceRole"); + } + exactObjectKeys(binding, expectedKeys, "dynamic immutable binding"); + if ( + binding.ordinal !== String(index) || + binding.offset !== String(expectedReference.start) || + binding.length !== String(expectedReference.length) || + !["address", "bytes"].includes(binding.encoding) + ) { + throw new Error(`dynamic immutable binding offset drift: ${dynamic.contractName}`); + } + if (binding.source === "factory_event") { + const fieldType = eventFields.get(binding.field); + if ( + (binding.encoding === "address" && fieldType !== "address") || + (binding.encoding === "bytes" && fieldType !== "bytes32") + ) { + throw new Error(`dynamic factory binding type drift: ${dynamic.contractName}`); + } + } else if (binding.source === "constant") { + if ( + !reviewedConstants.has(binding.value) || + binding.value.length !== 2 + expectedReference.length * 2 + ) { + throw new Error(`dynamic immutable constant is not reviewed: ${dynamic.contractName}`); + } + } else if (binding.source === "deferred_allocation_evidence") { + if ( + binding.encoding !== "bytes" || + expectedReference.length !== 32 || + !["configuration_hash", "beneficiary_count"].includes( + binding.evidenceRole, + ) + ) { + throw new Error(`dynamic deferred evidence is invalid: ${dynamic.contractName}`); + } + if (binding.evidenceRole === "configuration_hash") { + deferredConfiguration += 1; + } else { + deferredBeneficiaryCount += 1; + } + } else if ( + binding.source !== "deployed_address" || + binding.encoding !== "address" + ) { + throw new Error(`dynamic immutable source is invalid: ${dynamic.contractName}`); + } + } + if ( + (spec.factoryConfigurationField === null && + (deferredConfiguration !== 1 || deferredBeneficiaryCount < 1)) || + (typeof spec.factoryConfigurationField === "string" && + (eventFields.get(spec.factoryConfigurationField) !== "bytes32" || + deferredConfiguration !== 0 || + deferredBeneficiaryCount !== 0)) + ) { + throw new Error(`dynamic configuration evidence is invalid: ${dynamic.contractName}`); + } +} + +export async function buildReviewedBootstrapPlan({ + workspace, + repositoryCommit, + binding, + bindingSha256, + providers, + eventSignatures, + projectionRules, + createdAt, + candidateEnvioEvidence, +}) { + if (!/^[0-9a-f]{40}$/u.test(repositoryCommit) || !BYTES32.test(bindingSha256)) { + throw new Error("bootstrap checkout evidence is invalid"); + } + if ( + typeof createdAt !== "string" || + Number.isNaN(Date.parse(createdAt)) || + new Date(createdAt).toISOString() !== createdAt + ) { + throw new Error("bootstrap creation time is invalid"); + } + const catalogPath = "config/data-pipeline-bootstrap.v1.json"; + const catalogBytes = await readFile(path.join(workspace, catalogPath)); + const catalog = JSON.parse(catalogBytes.toString("utf8")); + validateCatalogAuthority(catalog, binding); + if (catalog.createdAt !== createdAt) { + throw new Error("bootstrap creation time does not match the reviewed catalog"); + } + const sourceByName = new Map(binding.sources.map((source) => [source.contractName, source])); + const releaseById = new Map(binding.releases.map((release) => [release.releaseVersion, release])); + const projectionRuleAuthority = new Map( + projectionRules.map((rule) => [ + `${rule.contractName}\0${rule.eventName}`, + rule, + ]), + ); + if (projectionRuleAuthority.size !== projectionRules.length) { + throw new Error("projector rule authority is not unique"); + } + const artifactCache = new Map(); + const artifactEvidence = async (name) => { + if (!artifactCache.has(name)) { + artifactCache.set(name, await loadArtifact(workspace, name)); + } + return artifactCache.get(name); + }; + + validateProviderSet(providers, candidateEnvioEvidence); + const canonicalReleaseEnvio = validateCanonicalReleaseCandidate( + binding, + candidateEnvioEvidence, + ); + const providerBindings = providers.map((provider) => + providerBinding(provider, createdAt), + ); + const candidateEnvioProvider = providerBindings.find( + ({ providerType }) => providerType === "envio_deployment", + ); + if ( + !candidateEnvioProvider || + candidateEnvioProvider.redactedIdentity !== + candidateEnvioEvidence.redactedIdentity + ) { + throw new Error("candidate-only database requires the audited Envio candidate"); + } + const candidateInitializationInputCommitment = exactCommitment( + "candidate-database-initialization", + { + providerDeploymentId: candidateEnvioProvider.providerDeploymentId, + deploymentCommitment: candidateEnvioProvider.deploymentCommitment, + schemaCommitment: candidateEnvioProvider.schemaCommitment, + evidencePath: candidateEnvioEvidence.path, + evidenceFileSha256: candidateEnvioEvidence.fileSha256, + auditEvidenceCommitment: + candidateEnvioEvidence.auditEvidenceCommitment, + policyCommitment: candidateEnvioEvidence.policyCommitment, + sourceCommit: candidateEnvioEvidence.sourceCommit, + canonicalReleaseEnvioIdentity: canonicalReleaseEnvio.identity, + canonicalReleaseEnvioEndpoint: canonicalReleaseEnvio.endpoint, + initializedAt: createdAt, + }, + ); + const releases = []; + const usedDynamicBindingSpecs = new Set(); + for (const [releaseIndex, semantic] of catalog.releases.entries()) { + const bindingRelease = releaseById.get(semantic.releaseId); + if ( + !bindingRelease || bindingRelease.model !== semantic.modelId || + binding.releases[releaseIndex]?.releaseVersion !== semantic.releaseId || + JSON.stringify(bindingRelease.sourceContracts) !== + JSON.stringify(semantic.sources.map(({ contractName }) => contractName)) || + JSON.stringify(bindingRelease.dynamicContracts) !== + JSON.stringify(semantic.dynamicSources.map(({ contractName }) => contractName)) + ) { + throw new Error(`semantic release drift: ${semantic.releaseId}`); + } + const manifestBytes = await readFile( + path.join(workspace, semantic.deploymentManifestPath), + ); + const manifest = JSON.parse(manifestBytes.toString("utf8")); + validateDeploymentManifest(manifest, semantic); + const allArtifactEntries = await Promise.all([ + ...semantic.sources.map(async (source) => ({ + contractName: source.contractName, + artifact: await artifactEvidence(source.artifact), + })), + ...semantic.dynamicSources.map(async (source) => ({ + contractName: source.contractName, + artifact: await artifactEvidence(source.artifact), + })), + ]); + const artifactCreationCodeCommitment = exactCommitment( + "release-artifact-creation-set", + allArtifactEntries + .map(({ contractName, artifact }) => ({ + contractName, + artifactFileSha256: artifact.fileSha256, + creationCodeHash: artifact.creationCodeHash, + })) + .sort((left, right) => left.contractName.localeCompare(right.contractName)), + ); + const scope = { + chainId: 1, + releaseId: semantic.releaseId, + modelId: semantic.modelId, + sourceGroup: catalog.sourceGroup, + }; + const epochId = deterministicUuid("release-epoch", { + ...scope, + epochNumber: semantic.activation.epochNumber, + artifactCreationCodeCommitment, + }); + const sourceBindings = []; + for (const [sourceIndex, source] of semantic.sources.entries()) { + const pinned = sourceByName.get(source.contractName); + if (!pinned) throw new Error(`source binding is missing: ${source.contractName}`); + const address = canonicalAddress(pinned.address, "source address"); + const manifestAddress = canonicalAddress( + manifest.addresses[source.deploymentKey], + `deployment address: ${source.contractName}`, + ); + if (manifestAddress !== address) { + throw new Error(`deployment address drift: ${source.contractName}`); + } + const runtimeCodeHash = canonicalBytes32(pinned.runtimeCodeHash, "runtime code hash"); + const artifact = await artifactEvidence(source.artifact); + const signatures = eventSignatures[source.contractName]; + const abiEvidence = authorizedAbiEventEvidence( + signatures, + artifact, + source.contractName, + ); + const recoverySelector = exactRecoverySelector( + artifact, + source.recoverySelector, + source.contractName, + ); + const bindingValue = { + epochId, + sourceName: source.contractName, + sourceRole: source.sourceRole, + sourceType: source.sourceType, + sourceAddress: address, + recoverySelector, + inclusiveStartBlock: String(pinned.startBlock), + abiEventSetCommitment: abiEvidence.commitment, + artifactCreationCodeCommitment, + artifactFileSha256: artifact.fileSha256, + artifactCreationCodeHash: artifact.creationCodeHash, + runtimeCodeHash, + }; + const bindingId = deterministicUuid("release-source-binding", bindingValue); + sourceBindings.push(Object.freeze({ + ordinal: sourceIndex + 1, + bindingId, + ...bindingValue, + deploymentEvidence: manifestDeploymentEvidence(manifest, source), + bindingCommitment: exactCommitment("release-source-binding", { + bindingId, + ...bindingValue, + }), + inputCommitment: exactCommitment("release-source-binding-input", { + bindingId, + ...bindingValue, + }), + createdAt, + })); + } + const sourceBindingByName = new Map( + sourceBindings.map((source) => [source.sourceName, source]), + ); + const dynamicSourceTemplates = []; + for (const [dynamicIndex, dynamic] of semantic.dynamicSources.entries()) { + const parent = sourceBindingByName.get(dynamic.parentContractName); + const artifact = await artifactEvidence(dynamic.artifact); + const spec = catalog.dynamicBindingSpecs[dynamic.bindingSpec]; + const parentSemantic = semantic.sources.find( + ({ contractName }) => contractName === dynamic.parentContractName, + ); + if ( + !parent || + !parentSemantic || + parent.sourceRole !== dynamic.parentSourceRole || + !spec + ) { + throw new Error("dynamic template parent/spec is missing or mismatched"); + } + usedDynamicBindingSpecs.add(dynamic.bindingSpec); + const parentArtifact = await artifactEvidence(parentSemantic.artifact); + const factorySignature = eventSignatures[dynamic.parentContractName]?.find( + (signature) => signature.startsWith(`${dynamic.factoryEventType}(`), + ); + if (!factorySignature) { + throw new Error(`dynamic factory event is unauthorized: ${dynamic.contractName}`); + } + const factoryEvent = parseAbiItem(`event ${factorySignature}`); + authorizedAbiEventEvidence( + [factorySignature], + parentArtifact, + dynamic.parentContractName, + ); + if ( + factoryEvent.inputs.find( + (input) => input.name === dynamic.deployedAddressField, + )?.type !== "address" + ) { + throw new Error(`dynamic deployed address field is invalid: ${dynamic.contractName}`); + } + const references = spec.bindings.map(({ offset, length }) => ({ + start: Number(offset), + length: Number(length), + })); + if ( + JSON.stringify(references) !== JSON.stringify(artifact.immutableReferences) || + canonicalBytes32(spec.normalizedRuntimeCodeHash, "normalized runtime hash") !== + artifact.normalizedRuntimeCodeHash || + canonicalBytes32(spec.immutableReferencesCommitment, "immutable refs commitment") !== + artifact.immutableReferencesCommitment || + Number(spec.runtimeCodeLength) !== artifact.runtimeCodeLength + ) { + throw new Error(`dynamic artifact evidence drift: ${dynamic.contractName}`); + } + validateDynamicBindingSpec({ + spec, + dynamic, + artifact, + factoryEvent, + manifest, + }); + const immutableBindingSpec = { + factoryConfigurationField: dynamic.factoryConfigurationField, + bindings: spec.bindings, + }; + const immutableBindingCommitment = exactCommitment( + "dynamic-immutable-binding", + immutableBindingSpec, + ); + const dynamicAbiEvidence = authorizedAbiEventEvidence( + eventSignatures[dynamic.contractName], + artifact, + dynamic.contractName, + ); + const dynamicValue = { + epochId, + parentFactoryReleaseBindingId: parent.bindingId, + parentFactoryBindingCommitment: parent.bindingCommitment, + parentSourceRole: dynamic.parentSourceRole, + factoryEventType: dynamic.factoryEventType, + deployedAddressField: dynamic.deployedAddressField, + deployedSourceRole: dynamic.deployedSourceRole, + deployedArtifactCreationCodeCommitment: artifact.creationCodeHash, + deployedArtifactCreationCodeHash: artifact.creationCodeHash, + artifactFileSha256: artifact.fileSha256, + expectedInstanceRuntimeCodeHash: null, + normalizedRuntimeCodeHash: artifact.normalizedRuntimeCodeHash, + immutableReferencesCommitment: artifact.immutableReferencesCommitment, + immutableBindingSpec, + immutableBindingCommitment, + runtimeCodeLength: String(artifact.runtimeCodeLength), + abiEventSetCommitment: dynamicAbiEvidence.commitment, + evidencePolicy: dynamic.bindingPolicy, + }; + const dynamicSourceTemplateId = deterministicUuid( + "dynamic-source-template", + dynamicValue, + ); + dynamicSourceTemplates.push(Object.freeze({ + ordinal: dynamicIndex + 1, + dynamicSourceTemplateId, + contractName: dynamic.contractName, + ...dynamicValue, + templateCommitment: exactCommitment("dynamic-source-template", { + dynamicSourceTemplateId, + ...dynamicValue, + }), + createdAt, + })); + } + const projectionEventRules = []; + for (const source of [...sourceBindings, ...dynamicSourceTemplates.map((template) => ({ + sourceName: template.contractName, + sourceRole: template.deployedSourceRole, + }))]) { + for (const signature of eventSignatures[source.sourceName]) { + projectionEventRules.push(projectionRule({ + epochId, + contractName: source.sourceName, + sourceRole: source.sourceRole, + signature, + ordinal: projectionEventRules.length + 1, + authority: projectionRuleAuthority, + })); + } + } + const uniqueRuleKeys = new Set( + projectionEventRules.map(({ sourceRole, eventType }) => `${sourceRole}\0${eventType}`), + ); + if (uniqueRuleKeys.size !== projectionEventRules.length) { + throw new Error(`duplicate writer event rule: ${semantic.releaseId}`); + } + for (const requirement of semantic.launchRequirements) { + if ( + !uniqueRuleKeys.has( + `${requirement.occurrenceRole}\0${requirement.eventType}`, + ) + ) { + throw new Error( + `launch requirement has no exact projector rule: ${semantic.releaseId}`, + ); + } + } + const launchCompletenessRequirements = semantic.launchRequirements.map( + (requirement, index) => { + const value = { + epochId, + requirementOrdinal: index, + occurrenceRole: requirement.occurrenceRole, + eventType: requirement.eventType, + requiredWhen: requirement.requiredWhen, + }; + return Object.freeze({ + ordinal: index + 1, + launchRequirementId: deterministicUuid("launch-requirement", value), + ...value, + requirementCommitment: exactCommitment("launch-requirement", value), + createdAt, + }); + }, + ); + if ( + !Number.isSafeInteger(bindingRelease.activationBlock) || + bindingRelease.activationBlock < 1 + ) { + throw new Error(`release activation block is invalid: ${semantic.releaseId}`); + } + const epochValue = { + scope, + epochId, + epochNumber: String(semantic.activation.epochNumber), + activationBlock: String(bindingRelease.activationBlock), + artifactCreationCodeCommitment, + sourceBindings: sourceBindings.map(({ bindingCommitment }) => bindingCommitment), + dynamicSourceTemplates: dynamicSourceTemplates.map(({ templateCommitment }) => templateCommitment), + projectionEventRules: projectionEventRules.map(({ ruleCommitment }) => ruleCommitment), + launchCompletenessRequirements: launchCompletenessRequirements.map( + ({ requirementCommitment }) => requirementCommitment, + ), + }; + const epochCommitment = exactCommitment("release-epoch", epochValue); + releases.push(Object.freeze({ + ordinal: releaseIndex + 1, + scope, + activationBlock: String(bindingRelease.activationBlock), + epochId, + epochNumber: String(semantic.activation.epochNumber), + epochCommitment, + artifactCreationCodeCommitment, + createInputCommitment: exactCommitment("release-epoch-create-input", { + ...epochValue, + epochCommitment, + }), + sourceBindings: Object.freeze(sourceBindings), + dynamicSourceTemplates: Object.freeze(dynamicSourceTemplates), + projectionEventRules: Object.freeze(projectionEventRules), + launchCompletenessRequirements: Object.freeze(launchCompletenessRequirements), + activation: Object.freeze({ + expectedGeneration: String(semantic.activation.expectedGeneration), + nextGeneration: String(semantic.activation.nextGeneration), + inputCommitment: exactCommitment("release-epoch-activation", { + scope, + epochId, + epochCommitment, + expectedGeneration: String( + semantic.activation.expectedGeneration, + ), + nextGeneration: String(semantic.activation.nextGeneration), + }), + changedAt: createdAt, + }), + deploymentManifest: Object.freeze({ + path: semantic.deploymentManifestPath, + sha256: sha256(manifestBytes), + releaseCommit: manifest.releaseCommit ?? null, + sourceCommitment: manifest.sourceCommitment ?? null, + }), + })); + } + + if ( + canonicalJson(Object.keys(catalog.dynamicBindingSpecs).sort()) !== + canonicalJson([...usedDynamicBindingSpecs].sort()) + ) { + throw new Error("bootstrap dynamic binding specification is orphaned"); + } + + const payload = { + kind: BOOTSTRAP_PLAN_KIND, + schemaVersion: 2, + repositoryCommit, + createdAt, + catalog: { + path: catalogPath, + sha256: sha256(catalogBytes), + version: catalog.catalogVersion, + }, + releaseBinding: { + path: catalog.releaseBindingPath, + sha256: bindingSha256, + chainId: binding.chainId, + startBlock: String(binding.startBlock), + confirmations: binding.confirmations, + }, + providerBindings: Object.freeze(providerBindings), + releases: Object.freeze(releases), + candidateIsolation: Object.freeze({ + databaseMode: "candidate-only", + candidateEvidencePath: candidateEnvioEvidence.path, + candidateEvidenceSha256: candidateEnvioEvidence.fileSha256, + candidateAuditEvidenceCommitment: + candidateEnvioEvidence.auditEvidenceCommitment, + candidatePolicyCommitment: candidateEnvioEvidence.policyCommitment, + candidateSourceCommit: candidateEnvioEvidence.sourceCommit, + candidateEnvioIdentity: candidateEnvioProvider.redactedIdentity, + candidateEnvioProviderDeploymentId: + candidateEnvioProvider.providerDeploymentId, + candidateInitializationInputCommitment, + canonicalReleaseEnvioIdentity: canonicalReleaseEnvio.identity, + canonicalReleaseEnvioEndpoint: canonicalReleaseEnvio.endpoint, + legacyProductionDeploymentRegistered: false, + publicationAllowedBeforePromotion: false, + promotionPolicy: "atomic-attestation-then-vercel-cutover", + reason: "provider-neutral candidate identifiers are safe only because this database contains exactly one Envio deployment before promotion", + }), + execution: Object.freeze({ + mode: "reviewed-atomic-bootstrap", + targetDatabaseMode: "candidate-only", + ready: true, + exactReplayOnlyAfterFirstApply: true, + mixedGenerationPolicy: "reject", + expectedProductGenerations: Object.freeze( + releases.map(({ scope }) => ({ ...scope, before: "0", after: "1" })), + ), + runtimeStartGate: "separate-dual-rpc-genesis-evidence-required", + }), + }; + return Object.freeze({ + ...payload, + planSha256: sha256(canonicalJson(payload)), + }); +} + +function exactIsoTimestamp(value, label) { + if ( + typeof value !== "string" || + Number.isNaN(Date.parse(value)) || + new Date(value).toISOString() !== value + ) { + throw new Error(`${label} is invalid`); + } + return value; +} + +function validateReviewedProvider(provider, createdAt) { + const rpc = provider.providerType === "rpc_provider"; + const graph = provider.providerType === "uniswap_subgraph"; + exactObjectKeys( + provider, + rpc + ? [ + "providerType", + "redactedIdentity", + "vendor", + "chainId", + "constructorVersion", + "endpointUrlCommitment", + "endpointOriginCommitment", + "endpointEvidenceDomain", + "deploymentCommitment", + "schemaCommitment", + "providerDeploymentId", + "endpointEvidenceCommitment", + "inputCommitment", + "createdAt", + ] + : graph + ? [ + "providerType", + "redactedIdentity", + "deploymentCommitment", + "schemaCommitment", + "subgraphId", + "deployment", + "providerDeploymentId", + "inputCommitment", + "createdAt", + ] + : [ + "providerType", + "redactedIdentity", + "deploymentCommitment", + "schemaCommitment", + "providerDeploymentId", + "inputCommitment", + "createdAt", + ], + "reviewed provider binding", + ); + if ( + !["envio_deployment", "rpc_provider", "uniswap_subgraph"].includes( + provider.providerType, + ) || + !IDENTIFIER.test(provider.redactedIdentity ?? "") || + !UUID.test(provider.providerDeploymentId ?? "") || + provider.createdAt !== createdAt + ) { + throw new Error("reviewed provider identity is invalid"); + } + const identity = { + providerType: provider.providerType, + redactedIdentity: provider.redactedIdentity, + deploymentCommitment: canonicalNonzeroBytes32( + provider.deploymentCommitment, + "reviewed provider deployment commitment", + ), + schemaCommitment: canonicalNonzeroBytes32( + provider.schemaCommitment, + "reviewed provider schema commitment", + ), + }; + const rpcEvidence = rpc + ? { + chainId: provider.chainId, + vendor: provider.vendor, + constructorVersion: provider.constructorVersion, + endpointUrlCommitment: canonicalNonzeroBytes32( + provider.endpointUrlCommitment, + "reviewed RPC endpoint URL commitment", + ), + endpointOriginCommitment: canonicalNonzeroBytes32( + provider.endpointOriginCommitment, + "reviewed RPC endpoint origin commitment", + ), + endpointEvidenceDomain: provider.endpointEvidenceDomain, + } + : null; + const providerDeploymentId = deterministicUuid("provider", { + ...identity, + rpc: rpcEvidence, + }); + if (provider.providerDeploymentId !== providerDeploymentId) { + throw new Error("reviewed provider deterministic identity drifted"); + } + const endpointEvidenceCommitment = rpc + ? exactCommitment("rpc-endpoint-evidence", { + providerDeploymentId, + ...rpcEvidence, + }) + : null; + if ( + rpc && + provider.endpointEvidenceCommitment !== endpointEvidenceCommitment + ) { + throw new Error("reviewed RPC endpoint evidence drifted"); + } + const expectedInput = exactCommitment("provider-registration", { + providerDeploymentId, + ...identity, + rpc: rpcEvidence, + endpointEvidenceCommitment, + }); + if (provider.inputCommitment !== expectedInput) { + throw new Error("reviewed provider input commitment drifted"); + } +} + +function validatePlanImmutableBindingSpec(spec, runtimeCodeLength) { + exactObjectKeys( + spec, + ["factoryConfigurationField", "bindings"], + "reviewed immutable binding specification", + ); + if ( + !Array.isArray(spec.bindings) || + spec.bindings.length < 1 || + spec.bindings.length > 64 || + !( + spec.factoryConfigurationField === null || + /^[A-Za-z][A-Za-z0-9_]{0,63}$/u.test( + spec.factoryConfigurationField ?? "", + ) + ) + ) { + throw new Error("reviewed immutable binding specification is invalid"); + } + let previousEnd = 0; + let deferredConfiguration = 0; + let deferredBeneficiary = 0; + for (const [index, binding] of spec.bindings.entries()) { + const expectedKeys = ["ordinal", "offset", "length", "source", "encoding"]; + if (binding?.source === "factory_event") expectedKeys.push("field"); + if (binding?.source === "constant") expectedKeys.push("value"); + if (binding?.source === "deferred_allocation_evidence") { + expectedKeys.push("evidenceRole"); + } + exactObjectKeys(binding, expectedKeys, "reviewed immutable binding"); + const offset = Number(binding.offset); + const length = Number(binding.length); + if ( + binding.ordinal !== String(index) || + !NONNEGATIVE_INTEGER_TEXT.test(binding.offset ?? "") || + !POSITIVE_INTEGER_TEXT.test(binding.length ?? "") || + !Number.isSafeInteger(offset) || + !Number.isSafeInteger(length) || + length > 32 || + offset < previousEnd || + offset + length > Number(runtimeCodeLength) || + !["address", "bytes"].includes(binding.encoding) + ) { + throw new Error("reviewed immutable binding coordinates are invalid"); + } + if (binding.source === "factory_event") { + if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/u.test(binding.field ?? "")) { + throw new Error("reviewed factory-event binding is invalid"); + } + } else if (binding.source === "constant") { + if ( + !/^0x(?:[0-9a-f]{2})+$/u.test(binding.value ?? "") || + binding.value.length !== 2 + length * 2 + ) { + throw new Error("reviewed constant binding is invalid"); + } + } else if (binding.source === "deployed_address") { + if (binding.encoding !== "address" || ![20, 32].includes(length)) { + throw new Error("reviewed deployed-address binding is invalid"); + } + } else if (binding.source === "deferred_allocation_evidence") { + if ( + binding.encoding !== "bytes" || + length !== 32 || + !["configuration_hash", "beneficiary_count"].includes( + binding.evidenceRole, + ) + ) { + throw new Error("reviewed deferred binding is invalid"); + } + if (binding.evidenceRole === "configuration_hash") { + deferredConfiguration += 1; + } else { + deferredBeneficiary += 1; + } + } else { + throw new Error("reviewed immutable binding source is invalid"); + } + previousEnd = offset + length; + } + if ( + (spec.factoryConfigurationField === null && + (deferredConfiguration !== 1 || deferredBeneficiary < 1)) || + (spec.factoryConfigurationField !== null && + (deferredConfiguration !== 0 || deferredBeneficiary !== 0)) + ) { + throw new Error("reviewed immutable binding evidence policy is invalid"); + } +} + +function validateReviewedRelease(release, index, createdAt) { + exactObjectKeys( + release, + [ + "ordinal", + "scope", + "activationBlock", + "epochId", + "epochNumber", + "epochCommitment", + "artifactCreationCodeCommitment", + "createInputCommitment", + "sourceBindings", + "dynamicSourceTemplates", + "projectionEventRules", + "launchCompletenessRequirements", + "activation", + "deploymentManifest", + ], + "reviewed release bootstrap", + ); + exactObjectKeys( + release.scope, + ["chainId", "releaseId", "modelId", "sourceGroup"], + "reviewed release scope", + ); + const expectedReleaseId = EXACT_RELEASES[index]; + const expectedModel = expectedReleaseId.startsWith("classic-") + ? "classic" + : "stock-paired"; + if ( + release.ordinal !== index + 1 || + release.scope.chainId !== 1 || + release.scope.releaseId !== expectedReleaseId || + release.scope.modelId !== expectedModel || + release.scope.sourceGroup !== "core" || + !POSITIVE_INTEGER_TEXT.test(release.activationBlock ?? "") || + release.epochNumber !== "1" || + !UUID.test(release.epochId ?? "") || + !Array.isArray(release.sourceBindings) || + release.sourceBindings.length < 1 || + !Array.isArray(release.dynamicSourceTemplates) || + !Array.isArray(release.projectionEventRules) || + release.projectionEventRules.length < 1 || + !Array.isArray(release.launchCompletenessRequirements) || + release.launchCompletenessRequirements.length < 1 + ) { + throw new Error("reviewed release identity is invalid"); + } + exactObjectKeys( + release.deploymentManifest, + ["path", "sha256", "releaseCommit", "sourceCommitment"], + "reviewed deployment manifest evidence", + ); + if ( + !/^contracts\/deployments\/mainnet-[a-z0-9-]+\.json$/u.test( + release.deploymentManifest.path ?? "", + ) || + !NONZERO_BYTES32.test(release.deploymentManifest.sha256 ?? "") || + !/^[0-9a-f]{40}$/u.test(release.deploymentManifest.releaseCommit ?? "") || + !NONZERO_BYTES32.test( + release.deploymentManifest.sourceCommitment?.toLowerCase?.() ?? "", + ) + ) { + throw new Error("reviewed deployment manifest evidence is invalid"); + } + + const sourceNames = new Set(); + const sourceByBindingId = new Map(); + const artifactEntries = []; + for (const [sourceIndex, source] of release.sourceBindings.entries()) { + exactObjectKeys( + source, + [ + "ordinal", + "bindingId", + "epochId", + "sourceName", + "sourceRole", + "sourceType", + "sourceAddress", + "recoverySelector", + "inclusiveStartBlock", + "abiEventSetCommitment", + "artifactCreationCodeCommitment", + "artifactFileSha256", + "artifactCreationCodeHash", + "runtimeCodeHash", + "deploymentEvidence", + "bindingCommitment", + "inputCommitment", + "createdAt", + ], + "reviewed static source binding", + ); + if ( + source.ordinal !== sourceIndex + 1 || + source.epochId !== release.epochId || + !UUID.test(source.bindingId ?? "") || + !IDENTIFIER.test(source.sourceName ?? "") || + sourceNames.has(source.sourceName) || + !IDENTIFIER.test(source.sourceRole ?? "") || + source.sourceType !== "ethereum_contract" || + !ADDRESS.test(source.sourceAddress ?? "") || + !(source.recoverySelector === null || SELECTOR.test(source.recoverySelector)) || + !POSITIVE_INTEGER_TEXT.test(source.inclusiveStartBlock ?? "") || + source.createdAt !== createdAt + ) { + throw new Error("reviewed static source identity is invalid"); + } + for (const field of [ + "abiEventSetCommitment", + "artifactCreationCodeCommitment", + "artifactFileSha256", + "artifactCreationCodeHash", + "runtimeCodeHash", + "bindingCommitment", + "inputCommitment", + ]) { + canonicalNonzeroBytes32(source[field], `reviewed source ${field}`); + } + const bindingValue = { + epochId: release.epochId, + sourceName: source.sourceName, + sourceRole: source.sourceRole, + sourceType: source.sourceType, + sourceAddress: source.sourceAddress, + recoverySelector: source.recoverySelector, + inclusiveStartBlock: source.inclusiveStartBlock, + abiEventSetCommitment: source.abiEventSetCommitment, + artifactCreationCodeCommitment: + source.artifactCreationCodeCommitment, + artifactFileSha256: source.artifactFileSha256, + artifactCreationCodeHash: source.artifactCreationCodeHash, + runtimeCodeHash: source.runtimeCodeHash, + }; + if ( + source.bindingId !== + deterministicUuid("release-source-binding", bindingValue) || + source.bindingCommitment !== + exactCommitment("release-source-binding", { + bindingId: source.bindingId, + ...bindingValue, + }) || + source.inputCommitment !== + exactCommitment("release-source-binding-input", { + bindingId: source.bindingId, + ...bindingValue, + }) + ) { + throw new Error("reviewed static source commitment drifted"); + } + exactObjectKeys( + source.deploymentEvidence, + [ + "releaseCommit", + "sourceCommitment", + "transactionHash", + "deploymentBlock", + "sourceVerificationStatus", + ], + "reviewed source deployment evidence", + ); + if ( + source.deploymentEvidence.releaseCommit !== + release.deploymentManifest.releaseCommit || + source.deploymentEvidence.sourceCommitment?.toLowerCase?.() !== + release.deploymentManifest.sourceCommitment.toLowerCase() || + !( + source.deploymentEvidence.transactionHash === null || + BYTES32.test( + source.deploymentEvidence.transactionHash?.toLowerCase?.() ?? "", + ) + ) || + !( + source.deploymentEvidence.deploymentBlock === null || + (Number.isSafeInteger(source.deploymentEvidence.deploymentBlock) && + source.deploymentEvidence.deploymentBlock > 0) + ) || + !( + source.deploymentEvidence.sourceVerificationStatus === null || + IDENTIFIER.test(source.deploymentEvidence.sourceVerificationStatus) + ) + ) { + throw new Error("reviewed source deployment evidence drifted"); + } + sourceNames.add(source.sourceName); + sourceByBindingId.set(source.bindingId, source); + artifactEntries.push({ + contractName: source.sourceName, + artifactFileSha256: source.artifactFileSha256, + creationCodeHash: source.artifactCreationCodeHash, + }); + } + + const dynamicNames = new Set(); + for (const [dynamicIndex, template] of release.dynamicSourceTemplates.entries()) { + exactObjectKeys( + template, + [ + "ordinal", + "dynamicSourceTemplateId", + "contractName", + "epochId", + "parentFactoryReleaseBindingId", + "parentFactoryBindingCommitment", + "parentSourceRole", + "factoryEventType", + "deployedAddressField", + "deployedSourceRole", + "deployedArtifactCreationCodeCommitment", + "deployedArtifactCreationCodeHash", + "artifactFileSha256", + "expectedInstanceRuntimeCodeHash", + "normalizedRuntimeCodeHash", + "immutableReferencesCommitment", + "immutableBindingSpec", + "immutableBindingCommitment", + "runtimeCodeLength", + "abiEventSetCommitment", + "evidencePolicy", + "templateCommitment", + "createdAt", + ], + "reviewed dynamic source template", + ); + const parent = sourceByBindingId.get( + template.parentFactoryReleaseBindingId, + ); + if ( + template.ordinal !== dynamicIndex + 1 || + template.epochId !== release.epochId || + !UUID.test(template.dynamicSourceTemplateId ?? "") || + !IDENTIFIER.test(template.contractName ?? "") || + dynamicNames.has(template.contractName) || + !parent || + parent.bindingCommitment !== template.parentFactoryBindingCommitment || + parent.sourceRole !== template.parentSourceRole || + !IDENTIFIER.test(template.factoryEventType ?? "") || + template.deployedAddressField !== "vault" || + template.deployedSourceRole !== "reward_vault" || + template.expectedInstanceRuntimeCodeHash !== null || + !POSITIVE_INTEGER_TEXT.test(template.runtimeCodeLength ?? "") || + ![ + "factory-event-and-constants", + "factory-event-constants-and-deferred-allocation", + ].includes(template.evidencePolicy) || + template.createdAt !== createdAt + ) { + throw new Error("reviewed dynamic source identity is invalid"); + } + for (const field of [ + "parentFactoryBindingCommitment", + "deployedArtifactCreationCodeCommitment", + "deployedArtifactCreationCodeHash", + "artifactFileSha256", + "normalizedRuntimeCodeHash", + "immutableReferencesCommitment", + "immutableBindingCommitment", + "abiEventSetCommitment", + "templateCommitment", + ]) { + canonicalNonzeroBytes32(template[field], `reviewed template ${field}`); + } + if ( + template.deployedArtifactCreationCodeCommitment !== + template.deployedArtifactCreationCodeHash + ) { + throw new Error("reviewed dynamic creation-code evidence drifted"); + } + validatePlanImmutableBindingSpec( + template.immutableBindingSpec, + template.runtimeCodeLength, + ); + const immutableBindingCommitment = exactCommitment( + "dynamic-immutable-binding", + template.immutableBindingSpec, + ); + if (template.immutableBindingCommitment !== immutableBindingCommitment) { + throw new Error("reviewed immutable binding commitment drifted"); + } + const dynamicValue = { + epochId: template.epochId, + parentFactoryReleaseBindingId: + template.parentFactoryReleaseBindingId, + parentFactoryBindingCommitment: + template.parentFactoryBindingCommitment, + parentSourceRole: template.parentSourceRole, + factoryEventType: template.factoryEventType, + deployedAddressField: template.deployedAddressField, + deployedSourceRole: template.deployedSourceRole, + deployedArtifactCreationCodeCommitment: + template.deployedArtifactCreationCodeCommitment, + deployedArtifactCreationCodeHash: + template.deployedArtifactCreationCodeHash, + artifactFileSha256: template.artifactFileSha256, + expectedInstanceRuntimeCodeHash: + template.expectedInstanceRuntimeCodeHash, + normalizedRuntimeCodeHash: template.normalizedRuntimeCodeHash, + immutableReferencesCommitment: + template.immutableReferencesCommitment, + immutableBindingSpec: template.immutableBindingSpec, + immutableBindingCommitment: template.immutableBindingCommitment, + runtimeCodeLength: template.runtimeCodeLength, + abiEventSetCommitment: template.abiEventSetCommitment, + evidencePolicy: template.evidencePolicy, + }; + if ( + template.dynamicSourceTemplateId !== + deterministicUuid("dynamic-source-template", dynamicValue) || + template.templateCommitment !== + exactCommitment("dynamic-source-template", { + dynamicSourceTemplateId: template.dynamicSourceTemplateId, + ...dynamicValue, + }) + ) { + throw new Error("reviewed dynamic source commitment drifted"); + } + dynamicNames.add(template.contractName); + artifactEntries.push({ + contractName: template.contractName, + artifactFileSha256: template.artifactFileSha256, + creationCodeHash: template.deployedArtifactCreationCodeHash, + }); + } + + const artifactCreationCodeCommitment = exactCommitment( + "release-artifact-creation-set", + artifactEntries.sort((left, right) => + left.contractName.localeCompare(right.contractName), + ), + ); + if ( + release.artifactCreationCodeCommitment !== + artifactCreationCodeCommitment || + release.sourceBindings.some( + (source) => + source.artifactCreationCodeCommitment !== + artifactCreationCodeCommitment, + ) + ) { + throw new Error("reviewed release creation-code commitment drifted"); + } + const expectedEpochId = deterministicUuid("release-epoch", { + ...release.scope, + epochNumber: 1, + artifactCreationCodeCommitment, + }); + if (release.epochId !== expectedEpochId) { + throw new Error("reviewed release epoch identity drifted"); + } + + const ruleKeys = new Set(); + for (const [ruleIndex, rule] of release.projectionEventRules.entries()) { + exactObjectKeys( + rule, + [ + "ordinal", + "projectionEventRuleId", + "epochId", + "projectionKind", + "sourceRole", + "eventType", + "ruleCommitment", + ], + "reviewed projection event rule", + ); + const value = { + epochId: release.epochId, + projectionKind: rule.projectionKind, + sourceRole: rule.sourceRole, + eventType: rule.eventType, + }; + const key = `${rule.sourceRole}\0${rule.eventType}`; + if ( + rule.ordinal !== ruleIndex + 1 || + rule.epochId !== release.epochId || + !IDENTIFIER.test(rule.projectionKind ?? "") || + !IDENTIFIER.test(rule.sourceRole ?? "") || + !IDENTIFIER.test(rule.eventType ?? "") || + ruleKeys.has(key) || + rule.projectionEventRuleId !== + deterministicUuid("projection-event-rule", value) || + rule.ruleCommitment !== + exactCommitment("projection-event-rule", value) + ) { + throw new Error("reviewed projection event rule drifted"); + } + ruleKeys.add(key); + } + + for (const [requirementIndex, requirement] of + release.launchCompletenessRequirements.entries()) { + exactObjectKeys( + requirement, + [ + "ordinal", + "launchRequirementId", + "epochId", + "requirementOrdinal", + "occurrenceRole", + "eventType", + "requiredWhen", + "requirementCommitment", + "createdAt", + ], + "reviewed launch completeness requirement", + ); + const value = { + epochId: release.epochId, + requirementOrdinal: requirementIndex, + occurrenceRole: requirement.occurrenceRole, + eventType: requirement.eventType, + requiredWhen: requirement.requiredWhen, + }; + if ( + requirement.ordinal !== requirementIndex + 1 || + requirement.requirementOrdinal !== requirementIndex || + requirement.epochId !== release.epochId || + !ruleKeys.has( + `${requirement.occurrenceRole}\0${requirement.eventType}`, + ) || + !["always", "reward_vault", "locked_custody", "eth_funded"].includes( + requirement.requiredWhen, + ) || + requirement.createdAt !== createdAt || + requirement.launchRequirementId !== + deterministicUuid("launch-requirement", value) || + requirement.requirementCommitment !== + exactCommitment("launch-requirement", value) + ) { + throw new Error("reviewed launch completeness requirement drifted"); + } + } + + const epochValue = { + scope: release.scope, + epochId: release.epochId, + epochNumber: release.epochNumber, + activationBlock: release.activationBlock, + artifactCreationCodeCommitment, + sourceBindings: release.sourceBindings.map( + ({ bindingCommitment }) => bindingCommitment, + ), + dynamicSourceTemplates: release.dynamicSourceTemplates.map( + ({ templateCommitment }) => templateCommitment, + ), + projectionEventRules: release.projectionEventRules.map( + ({ ruleCommitment }) => ruleCommitment, + ), + launchCompletenessRequirements: + release.launchCompletenessRequirements.map( + ({ requirementCommitment }) => requirementCommitment, + ), + }; + const epochCommitment = exactCommitment("release-epoch", epochValue); + if ( + release.epochCommitment !== epochCommitment || + release.createInputCommitment !== + exactCommitment("release-epoch-create-input", { + ...epochValue, + epochCommitment, + }) + ) { + throw new Error("reviewed release epoch commitment drifted"); + } + exactObjectKeys( + release.activation, + ["expectedGeneration", "nextGeneration", "inputCommitment", "changedAt"], + "reviewed release activation", + ); + if ( + release.activation.expectedGeneration !== "0" || + release.activation.nextGeneration !== "1" || + release.activation.changedAt !== createdAt || + release.activation.inputCommitment !== + exactCommitment("release-epoch-activation", { + scope: release.scope, + epochId: release.epochId, + epochCommitment: release.epochCommitment, + expectedGeneration: "0", + nextGeneration: "1", + }) + ) { + throw new Error("reviewed release activation input drifted"); + } +} + +export function validateReviewedBootstrapPlan(plan) { + exactObjectKeys( + plan, + [ + "kind", + "schemaVersion", + "repositoryCommit", + "createdAt", + "catalog", + "releaseBinding", + "providerBindings", + "releases", + "candidateIsolation", + "execution", + "planSha256", + ], + "reviewed bootstrap plan", + ); + if ( + plan.kind !== BOOTSTRAP_PLAN_KIND || + plan.schemaVersion !== 2 || + !/^[0-9a-f]{40}$/u.test(plan.repositoryCommit ?? "") || + !BYTES32.test(plan.planSha256 ?? "") || + !Array.isArray(plan.providerBindings) || + plan.providerBindings.length !== 4 || + !Array.isArray(plan.releases) || + plan.releases.length !== EXACT_RELEASES.length + ) { + throw new Error("reviewed bootstrap plan is invalid"); + } + exactIsoTimestamp(plan.createdAt, "reviewed bootstrap creation time"); + const { planSha256, ...payload } = plan; + if (sha256(canonicalJson(payload)) !== planSha256) { + throw new Error("reviewed bootstrap plan commitment does not match"); + } + exactObjectKeys( + plan.catalog, + ["path", "sha256", "version"], + "reviewed bootstrap catalog evidence", + ); + if ( + plan.catalog.path !== "config/data-pipeline-bootstrap.v1.json" || + !NONZERO_BYTES32.test(plan.catalog.sha256 ?? "") || + !IDENTIFIER.test(plan.catalog.version ?? "") + ) { + throw new Error("reviewed bootstrap catalog evidence is invalid"); + } + exactObjectKeys( + plan.releaseBinding, + ["path", "sha256", "chainId", "startBlock", "confirmations"], + "reviewed release binding evidence", + ); + if ( + plan.releaseBinding.path !== "config/data-pipeline-release.v1.json" || + !NONZERO_BYTES32.test(plan.releaseBinding.sha256 ?? "") || + plan.releaseBinding.chainId !== 1 || + !POSITIVE_INTEGER_TEXT.test(plan.releaseBinding.startBlock ?? "") || + plan.releaseBinding.confirmations !== 12 + ) { + throw new Error("reviewed release binding evidence is invalid"); + } + + for (const provider of plan.providerBindings) { + validateReviewedProvider(provider, plan.createdAt); + } + const providerTypes = plan.providerBindings.map( + ({ providerType, vendor = null }) => ({ providerType, vendor }), + ); + if ( + canonicalJson(providerTypes) !== + canonicalJson([ + { providerType: "envio_deployment", vendor: null }, + { providerType: "rpc_provider", vendor: "alchemy" }, + { providerType: "rpc_provider", vendor: "quicknode" }, + { providerType: "uniswap_subgraph", vendor: null }, + ]) || + new Set( + plan.providerBindings.map(({ redactedIdentity }) => redactedIdentity), + ).size !== 4 + ) { + throw new Error("reviewed provider set is invalid"); + } + const [candidate, alchemy, quicknode, graph] = plan.providerBindings; + if ( + candidate.redactedIdentity !== + plan.candidateIsolation.candidateEnvioIdentity || + candidate.providerDeploymentId !== + plan.candidateIsolation.candidateEnvioProviderDeploymentId || + alchemy.chainId !== 1 || + quicknode.chainId !== 1 || + alchemy.redactedIdentity !== "rpc:1:alchemy" || + quicknode.redactedIdentity !== "rpc:1:quicknode" || + alchemy.constructorVersion !== "rpc-provider-v1" || + quicknode.constructorVersion !== "rpc-provider-v1" || + alchemy.endpointEvidenceDomain !== "rpc-endpoint-commitments-v1" || + quicknode.endpointEvidenceDomain !== "rpc-endpoint-commitments-v1" || + graph.redactedIdentity !== + `uniswap-v4:ethereum:${graph.deployment}` + ) { + throw new Error("reviewed provider semantics are invalid"); + } + + exactObjectKeys( + plan.candidateIsolation, + [ + "databaseMode", + "candidateEvidencePath", + "candidateEvidenceSha256", + "candidateAuditEvidenceCommitment", + "candidatePolicyCommitment", + "candidateSourceCommit", + "candidateEnvioIdentity", + "candidateEnvioProviderDeploymentId", + "candidateInitializationInputCommitment", + "canonicalReleaseEnvioIdentity", + "canonicalReleaseEnvioEndpoint", + "legacyProductionDeploymentRegistered", + "publicationAllowedBeforePromotion", + "promotionPolicy", + "reason", + ], + "reviewed candidate database isolation", + ); + if ( + plan.candidateIsolation.databaseMode !== "candidate-only" || + plan.candidateIsolation.candidateEvidencePath !== + "config/data-pipeline-envio-candidate.v1.json" || + !NONZERO_BYTES32.test( + plan.candidateIsolation.candidateEvidenceSha256 ?? "", + ) || + !NONZERO_BYTES32.test( + plan.candidateIsolation.candidateAuditEvidenceCommitment ?? "", + ) || + !NONZERO_BYTES32.test( + plan.candidateIsolation.candidatePolicyCommitment ?? "", + ) || + !/^[0-9a-f]{40}$/u.test( + plan.candidateIsolation.candidateSourceCommit ?? "", + ) || + plan.candidateIsolation.canonicalReleaseEnvioIdentity !== + candidate.redactedIdentity || + !/^https:\/\/indexer\.hyperindex\.xyz\/[a-z0-9]{7,64}\/v1\/graphql$/u.test( + plan.candidateIsolation.canonicalReleaseEnvioEndpoint ?? "", + ) || + plan.candidateIsolation.legacyProductionDeploymentRegistered !== false || + plan.candidateIsolation.publicationAllowedBeforePromotion !== false || + plan.candidateIsolation.promotionPolicy !== + "atomic-attestation-then-vercel-cutover" || + plan.candidateIsolation.reason !== + "provider-neutral candidate identifiers are safe only because this database contains exactly one Envio deployment before promotion" + ) { + throw new Error("reviewed candidate database isolation is invalid"); + } + const candidateInitializationInputCommitment = exactCommitment( + "candidate-database-initialization", + { + providerDeploymentId: candidate.providerDeploymentId, + deploymentCommitment: candidate.deploymentCommitment, + schemaCommitment: candidate.schemaCommitment, + evidencePath: plan.candidateIsolation.candidateEvidencePath, + evidenceFileSha256: + plan.candidateIsolation.candidateEvidenceSha256, + auditEvidenceCommitment: + plan.candidateIsolation.candidateAuditEvidenceCommitment, + policyCommitment: + plan.candidateIsolation.candidatePolicyCommitment, + sourceCommit: plan.candidateIsolation.candidateSourceCommit, + canonicalReleaseEnvioIdentity: + plan.candidateIsolation.canonicalReleaseEnvioIdentity, + canonicalReleaseEnvioEndpoint: + plan.candidateIsolation.canonicalReleaseEnvioEndpoint, + initializedAt: plan.createdAt, + }, + ); + if ( + plan.candidateIsolation.candidateInitializationInputCommitment !== + candidateInitializationInputCommitment + ) { + throw new Error("reviewed candidate initialization input drifted"); + } + + plan.releases.forEach((release, index) => + validateReviewedRelease(release, index, plan.createdAt), + ); + + exactObjectKeys( + plan.execution, + [ + "mode", + "targetDatabaseMode", + "ready", + "exactReplayOnlyAfterFirstApply", + "mixedGenerationPolicy", + "expectedProductGenerations", + "runtimeStartGate", + ], + "reviewed bootstrap execution gate", + ); + if ( + plan.execution.mode !== "reviewed-atomic-bootstrap" || + plan.execution.targetDatabaseMode !== "candidate-only" || + plan.execution.ready !== true || + plan.execution.exactReplayOnlyAfterFirstApply !== true || + plan.execution.mixedGenerationPolicy !== "reject" || + plan.execution.runtimeStartGate !== + "separate-dual-rpc-genesis-evidence-required" || + !Array.isArray(plan.execution.expectedProductGenerations) || + canonicalJson(plan.execution.expectedProductGenerations) !== + canonicalJson( + plan.releases.map(({ scope }) => ({ + ...scope, + before: "0", + after: "1", + })), + ) + ) { + throw new Error("reviewed bootstrap execution gate is invalid"); + } + return plan; +} diff --git a/scripts/data-pipeline/cutover-credentials.mjs b/scripts/data-pipeline/cutover-credentials.mjs new file mode 100644 index 00000000..451c4693 --- /dev/null +++ b/scripts/data-pipeline/cutover-credentials.mjs @@ -0,0 +1,1393 @@ +import { createHash } from "node:crypto"; +import { execFile } from "node:child_process"; +import { + constants as fsConstants, + chmod, + lstat, + mkdtemp, + open, + readFile, + rm, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; + +import postgres from "postgres"; + +import { + assertNoSecretOutput, + canonicalJson, + sha256, + validateDirectSupabaseTarget, +} from "./hosted-db-operator-core.mjs"; +import { + closeHostedDatabase, + openHostedDatabase, +} from "./hosted-db-postgres.mjs"; + +const executeFile = promisify(execFile); +const PROJECT_REF = /^[a-z0-9]{20}$/u; +const COMMIT = /^[0-9a-f]{40}$/u; +const OPERATION_ID = /^[a-z0-9][a-z0-9._-]{7,63}$/u; +const ISOLATION_ID = /^[a-z0-9][a-z0-9_-]{7,31}$/u; +const SHA256 = /^0x[0-9a-f]{64}$/u; +const PG_TOOL_VERSION = /\b(\d+)\.(?:\d+)(?:\.\d+)?\b/u; +const RESTORE_DATABASE_PREFIX = "programmable_restore_"; +const BACKUP_SCHEMAS = Object.freeze([ + "programmable_private", + "programmable_release_probe_private", + "supabase_migrations", +]); +const RESTORE_ROLE_NAMES = Object.freeze([ + "programmable_api_reader", + "programmable_api_reader_login", + "programmable_maintenance", + "programmable_migrator", + "programmable_operator", + "programmable_profile_binder", + "programmable_profile_recovery", + "programmable_profile_writer", + "programmable_projector", + "programmable_projector_login", + "programmable_projector_runtime", + "programmable_projector_runtime_login", + "programmable_reconciler", + "programmable_reconciler_login", + "programmable_release_probe_nonce", + "programmable_release_probe_nonce_login", +]); + +function freezeRoleSpec(spec) { + return Object.freeze(spec); +} + +export const ROLE_SPECS = Object.freeze([ + freezeRoleSpec({ + key: "apiReader", + loginRole: "programmable_api_reader_login", + capabilityRole: "programmable_api_reader", + }), + freezeRoleSpec({ + key: "projector", + loginRole: "programmable_projector_login", + capabilityRole: "programmable_projector", + }), + freezeRoleSpec({ + key: "projectorRuntime", + loginRole: "programmable_projector_runtime_login", + capabilityRole: "programmable_projector_runtime", + }), + freezeRoleSpec({ + key: "reconciler", + loginRole: "programmable_reconciler_login", + capabilityRole: "programmable_reconciler", + }), + freezeRoleSpec({ + key: "releaseProbe", + loginRole: "programmable_release_probe_nonce_login", + capabilityRole: "programmable_release_probe_nonce", + }), +]); + +function isPlainRecord(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function validateCaPem(value, label = "Postgres CA") { + if ( + typeof value !== "string" || + value.length < 64 || + value.length > 32_768 || + !value.includes("-----BEGIN CERTIFICATE-----") || + !value.includes("-----END CERTIFICATE-----") || + value.includes("PRIVATE KEY") + ) { + throw new Error(`${label} must be a server-only PEM certificate`); + } + return value; +} + +function readExactCredentials(credentials) { + if (!isPlainRecord(credentials)) { + throw new Error("exactly five login-role credentials are required"); + } + const expectedKeys = ROLE_SPECS.map(({ key }) => key).sort(); + const actualKeys = Object.keys(credentials).sort(); + if ( + actualKeys.length !== expectedKeys.length || + actualKeys.some((key, index) => key !== expectedKeys[index]) + ) { + throw new Error("exactly five login-role credentials are required"); + } + const values = new Map(); + const uniquePasswords = new Set(); + for (const spec of ROLE_SPECS) { + const descriptor = Object.getOwnPropertyDescriptor(credentials, spec.key); + const password = descriptor?.value; + if ( + !descriptor || + descriptor.get || + descriptor.set || + typeof password !== "string" || + password.length < 32 || + password.length > 256 || + [...password].some((character) => { + const code = character.codePointAt(0); + return code === undefined || code < 0x21 || code > 0x7e; + }) + ) { + throw new Error(`credential ${spec.key} is not a valid generated password`); + } + if (uniquePasswords.has(password)) { + throw new Error("login-role credentials must be unique"); + } + uniquePasswords.add(password); + values.set(spec.key, password); + } + return values; +} + +function errorCode(error) { + const code = error && typeof error === "object" ? error.code : undefined; + return typeof code === "string" && /^[A-Z0-9_]{2,16}$/u.test(code) + ? code + : undefined; +} + +function operationalFailure(label, error) { + const code = errorCode(error); + return new Error(`${label} failed${code ? ` (${code})` : ""}`); +} + +function validateDependencies(value, allowed) { + if (value === undefined) return Object.freeze({}); + if (!isPlainRecord(value)) throw new Error("dependencies must be an object"); + for (const key of Object.keys(value)) { + if (!allowed.includes(key) || typeof value[key] !== "function") { + throw new Error("dependencies contain an unsupported entry"); + } + } + return value; +} + +function staticRolePasswordSql(spec) { + return ` +do $credential_rotation$ +declare + credential_value text; +begin + credential_value := pg_catalog.current_setting( + 'programmable.credential_rotation', true + ); + if credential_value is null or pg_catalog.length(credential_value) < 32 then + raise exception 'credential rotation input is absent'; + end if; + execute pg_catalog.format( + 'alter role %I password %L valid until %L', + '${spec.loginRole}', + credential_value, + 'infinity' + ); + perform pg_catalog.set_config( + 'programmable.credential_rotation', '', true + ); +end +$credential_rotation$; +`; +} + +function isExactRoleFlagPosture(row, expectedLogin) { + return ( + row && + row.rolcanlogin === expectedLogin && + row.rolsuper === false && + row.rolcreatedb === false && + row.rolcreaterole === false && + row.rolinherit === false && + row.rolreplication === false && + row.rolbypassrls === false && + Number(row.rolconnlimit) === -1 && + (row.rolconfig === null || + (Array.isArray(row.rolconfig) && row.rolconfig.length === 0)) + ); +} + +async function readRolePosture(sql, { requirePasswords }) { + const names = ROLE_SPECS.flatMap(({ loginRole, capabilityRole }) => [ + loginRole, + capabilityRole, + ]); + const rows = await sql.unsafe( + ` + select + roles.rolname, + roles.rolcanlogin, + roles.rolsuper, + roles.rolcreatedb, + roles.rolcreaterole, + roles.rolinherit, + roles.rolreplication, + roles.rolbypassrls, + roles.rolconnlimit, + roles.rolconfig, + auth.rolpassword is not null as has_password + from pg_catalog.pg_roles as roles + join pg_catalog.pg_authid as auth + on auth.rolname = roles.rolname + where roles.rolname = any($1::text[]) + order by roles.rolname + `, + [names], + ); + const memberships = await sql.unsafe( + ` + select + member_role.rolname as member_role, + granted_role.rolname as granted_role, + membership.admin_option, + membership.inherit_option, + membership.set_option + from pg_catalog.pg_auth_members as membership + join pg_catalog.pg_roles as member_role + on member_role.oid = membership.member + join pg_catalog.pg_roles as granted_role + on granted_role.oid = membership.roleid + where member_role.rolname = any($1::text[]) + order by member_role.rolname, granted_role.rolname + `, + [ROLE_SPECS.map(({ loginRole }) => loginRole)], + ); + return { rows, memberships, requirePasswords }; +} + +async function readPoolerRolePosture(sql) { + const names = ROLE_SPECS.flatMap(({ loginRole, capabilityRole }) => [ + loginRole, + capabilityRole, + ]); + const rows = await sql.unsafe( + ` + select + rolname, + rolcanlogin, + rolsuper, + rolcreatedb, + rolcreaterole, + rolinherit, + rolreplication, + rolbypassrls, + rolconnlimit, + rolconfig, + false as has_password + from pg_catalog.pg_roles + where rolname = any($1::text[]) + order by rolname + `, + [names], + ); + const memberships = await sql.unsafe( + ` + select + member_role.rolname as member_role, + granted_role.rolname as granted_role, + membership.admin_option, + membership.inherit_option, + membership.set_option + from pg_catalog.pg_auth_members as membership + join pg_catalog.pg_roles as member_role + on member_role.oid = membership.member + join pg_catalog.pg_roles as granted_role + on granted_role.oid = membership.roleid + where member_role.rolname = any($1::text[]) + order by member_role.rolname, granted_role.rolname + `, + [ROLE_SPECS.map(({ loginRole }) => loginRole)], + ); + return { rows, memberships, requirePasswords: false }; +} + +function assertRolePosture(posture) { + if (!posture || !Array.isArray(posture.rows) || !Array.isArray(posture.memberships)) { + throw new Error("database role posture response is invalid"); + } + const rowByName = new Map(posture.rows.map((row) => [row?.rolname, row])); + if (rowByName.size !== ROLE_SPECS.length * 2) { + throw new Error("database role set does not match the reviewed role set"); + } + for (const spec of ROLE_SPECS) { + const login = rowByName.get(spec.loginRole); + const capability = rowByName.get(spec.capabilityRole); + if ( + !isExactRoleFlagPosture(login, true) || + !isExactRoleFlagPosture(capability, false) || + (posture.requirePasswords === true && login.has_password !== true) + ) { + throw new Error("database role posture does not match the reviewed posture"); + } + const memberships = posture.memberships.filter( + ({ member_role: memberRole }) => memberRole === spec.loginRole, + ); + if ( + memberships.length !== 1 || + memberships[0]?.granted_role !== spec.capabilityRole || + memberships[0]?.admin_option !== false || + memberships[0]?.inherit_option !== false || + memberships[0]?.set_option !== true + ) { + throw new Error("database role membership does not match the reviewed posture"); + } + } + return true; +} + +async function assertDirectOperatorIdentity(sql) { + const [identity] = await sql.unsafe(` + select + session_user::text as session_user, + current_user::text as current_user, + current_role::text as current_role, + pg_catalog.current_database()::text as database_name, + pg_catalog.inet_server_port()::integer as server_port + `); + if ( + identity?.session_user !== "postgres" || + identity?.current_user !== "postgres" || + identity?.current_role !== "postgres" || + identity?.database_name !== "postgres" || + Number(identity?.server_port) !== 5432 + ) { + throw new Error("direct database operator identity is not approved"); + } +} + +async function rotateLoginPassword(sql, spec, password) { + await sql.begin(async (transaction) => { + const [identity] = await transaction.unsafe(` + select session_user::text as session_user, + current_role::text as current_role + `); + if ( + identity?.session_user !== "postgres" || + identity?.current_role !== "postgres" + ) { + throw new Error("credential rotation operator identity changed"); + } + await transaction` + select pg_catalog.set_config( + 'programmable.credential_rotation', ${password}, true + ) + `; + await transaction.unsafe(staticRolePasswordSql(spec)).simple(); + }); +} + +export async function provisionLoginRoles(input) { + if (!isPlainRecord(input)) throw new Error("provisioning input is invalid"); + const credentials = readExactCredentials(input.credentials); + validateCaPem(input.sslCaPem); + const target = validateDirectSupabaseTarget( + input.databaseUrl, + input.expectedProjectRef, + ); + const dependencies = validateDependencies(input.dependencies, [ + "openHostedDatabase", + "closeHostedDatabase", + "assertDirectOperatorIdentity", + "readRolePosture", + "rotateLoginPassword", + ]); + const openDatabase = dependencies.openHostedDatabase ?? openHostedDatabase; + const closeDatabase = dependencies.closeHostedDatabase ?? closeHostedDatabase; + const assertIdentity = + dependencies.assertDirectOperatorIdentity ?? assertDirectOperatorIdentity; + const inspect = dependencies.readRolePosture ?? readRolePosture; + const rotate = dependencies.rotateLoginPassword ?? rotateLoginPassword; + let connection; + try { + connection = await openDatabase({ + databaseUrl: input.databaseUrl, + expectedProjectRef: input.expectedProjectRef, + sslCaPem: input.sslCaPem, + }); + if (canonicalJson(connection.target) !== canonicalJson(target)) { + throw new Error("direct database target identity changed"); + } + await assertIdentity(connection.sql); + assertRolePosture( + await inspect(connection.sql, { requirePasswords: false }), + ); + for (const spec of ROLE_SPECS) { + await rotate(connection.sql, spec, credentials.get(spec.key)); + } + assertRolePosture( + await inspect(connection.sql, { requirePasswords: true }), + ); + return Object.freeze({ + kind: "programmable-login-role-provisioning-result", + schemaVersion: 1, + target, + roles: Object.freeze( + ROLE_SPECS.map(({ loginRole, capabilityRole }) => + Object.freeze({ loginRole, capabilityRole, provisioned: true }), + ), + ), + }); + } catch (error) { + throw operationalFailure("login-role provisioning", error); + } finally { + if (connection?.sql) await closeDatabase(connection.sql).catch(() => {}); + } +} + +function validatePoolerTarget({ expectedProjectRef, poolerHost }) { + if (!PROJECT_REF.test(expectedProjectRef ?? "")) { + throw new Error("expected Supabase project ref is invalid"); + } + if ( + typeof poolerHost !== "string" || + !/^aws-[0-9]+-[a-z0-9-]+\.pooler\.supabase\.com$/u.test(poolerHost) + ) { + throw new Error("shared Supabase pooler host is invalid"); + } + return Object.freeze({ + projectRef: expectedProjectRef, + host: poolerHost, + port: 6543, + database: "postgres", + sslMode: "verify-full", + prepare: false, + }); +} + +function poolerConnectionUrl(target, spec, password) { + const url = new URL("postgresql://placeholder:placeholder@localhost/postgres"); + url.hostname = target.host; + url.port = String(target.port); + url.username = `${spec.loginRole}.${target.projectRef}`; + url.password = password; + url.searchParams.set("sslmode", "verify-full"); + return url; +} + +async function openPoolerDatabase({ target, spec, password, sslCaPem }) { + const connectionUrl = poolerConnectionUrl(target, spec, password); + const sql = postgres({ + host: connectionUrl.hostname, + port: Number(connectionUrl.port), + database: connectionUrl.pathname.slice(1), + username: decodeURIComponent(connectionUrl.username), + password: decodeURIComponent(connectionUrl.password), + ssl: { rejectUnauthorized: true, ca: sslCaPem }, + max: 1, + prepare: false, + connect_timeout: 8, + idle_timeout: 5, + max_lifetime: 30, + onnotice: () => {}, + connection: { + application_name: "programmable-pooler-role-verifier", + }, + }); + return { sql }; +} + +async function closePoolerDatabase(sql) { + await sql.end({ timeout: 3 }); +} + +function staticSetLocalRoleSql(spec) { + return `set local role ${spec.capabilityRole}`; +} + +async function verifyPoolerSession(sql, spec) { + return sql.begin(async (transaction) => { + const [before] = await transaction.unsafe(` + select + session_user::text as session_user, + current_role::text as current_role, + pg_catalog.current_database()::text as database_name + `); + if ( + before?.session_user !== spec.loginRole || + before?.current_role !== spec.loginRole || + before?.database_name !== "postgres" + ) { + throw new Error("pooler session login identity does not match"); + } + await transaction.unsafe(staticSetLocalRoleSql(spec)).simple(); + const [after] = await transaction.unsafe(` + select + session_user::text as session_user, + current_role::text as current_role, + pg_catalog.current_setting('role', true)::text as configured_role, + pg_catalog.current_database()::text as database_name + `); + if ( + after?.session_user !== spec.loginRole || + after?.current_role !== spec.capabilityRole || + after?.configured_role !== spec.capabilityRole || + after?.database_name !== "postgres" + ) { + throw new Error("pooler session capability identity does not match"); + } + return Object.freeze({ + loginRole: spec.loginRole, + capabilityRole: spec.capabilityRole, + verified: true, + }); + }); +} + +export async function verifyPoolerLogins(input) { + if (!isPlainRecord(input)) throw new Error("pooler verification input is invalid"); + const credentials = readExactCredentials(input.credentials); + const sslCaPem = validateCaPem(input.sslCaPem); + const target = validatePoolerTarget(input); + const dependencies = validateDependencies(input.dependencies, [ + "openPoolerDatabase", + "closePoolerDatabase", + "readPoolerRolePosture", + "verifyPoolerSession", + ]); + const openDatabase = dependencies.openPoolerDatabase ?? openPoolerDatabase; + const closeDatabase = dependencies.closePoolerDatabase ?? closePoolerDatabase; + const inspectPosture = + dependencies.readPoolerRolePosture ?? readPoolerRolePosture; + const verifySession = dependencies.verifyPoolerSession ?? verifyPoolerSession; + const roles = []; + try { + for (const spec of ROLE_SPECS) { + let connection; + try { + connection = await openDatabase({ + target, + spec, + password: credentials.get(spec.key), + sslCaPem, + options: Object.freeze({ prepare: false }), + }); + assertRolePosture(await inspectPosture(connection.sql)); + roles.push(await verifySession(connection.sql, spec)); + } finally { + if (connection?.sql) await closeDatabase(connection.sql).catch(() => {}); + } + } + if (roles.length !== ROLE_SPECS.length) { + throw new Error("not every reviewed pooler login was verified"); + } + return Object.freeze({ + kind: "programmable-pooler-login-verification-result", + schemaVersion: 1, + target, + roles: Object.freeze(roles), + }); + } catch (error) { + throw operationalFailure("pooler login verification", error); + } +} + +function decodeUrlComponent(value) { + try { + return decodeURIComponent(value); + } catch { + throw new Error("database credential encoding is invalid"); + } +} + +function parseSourceTarget(databaseUrl, expectedProjectRef) { + const safeTarget = validateDirectSupabaseTarget( + databaseUrl, + expectedProjectRef, + ); + const parsed = new URL(databaseUrl); + const password = decodeUrlComponent(parsed.password); + if (password.length < 1 || /[\u0000-\u001f\u007f]/u.test(password)) { + throw new Error("source database credential is invalid"); + } + return { safeTarget, password, username: "postgres" }; +} + +function parseRestoreTarget(databaseUrl, isolationId) { + if (!ISOLATION_ID.test(isolationId ?? "")) { + throw new Error("restore isolation id is invalid"); + } + let parsed; + try { + parsed = new URL(databaseUrl); + } catch { + throw new Error("isolated restore database URL is invalid"); + } + const parameters = [...parsed.searchParams.entries()]; + const expectedDatabase = `${RESTORE_DATABASE_PREFIX}${isolationId}`; + const port = Number(parsed.port); + if ( + !["postgres:", "postgresql:"].includes(parsed.protocol) || + !["127.0.0.1", "[::1]", "::1", "localhost"].includes(parsed.hostname) || + !Number.isInteger(port) || + port < 1 || + port > 65_535 || + parsed.pathname !== `/${expectedDatabase}` || + parsed.username !== "postgres" || + parsed.password.length < 1 || + parsed.hash !== "" || + parameters.length !== 1 || + parameters[0][0] !== "sslmode" || + parameters[0][1] !== "verify-full" + ) { + throw new Error( + "restore target must be an isolated loopback database with sslmode=verify-full", + ); + } + const password = decodeUrlComponent(parsed.password); + if (/[\u0000-\u001f\u007f]/u.test(password)) { + throw new Error("restore database credential is invalid"); + } + return { + safeTarget: Object.freeze({ + isolationId, + host: parsed.hostname, + port, + database: expectedDatabase, + sslMode: "verify-full", + }), + password, + username: "postgres", + }; +} + +function validateAbsoluteOutputPath(value, label) { + if ( + typeof value !== "string" || + !path.isAbsolute(value) || + value.length > 1024 || + path.basename(value) === "" || + value.includes("\u0000") + ) { + throw new Error(`${label} must be an absolute file path`); + } + return path.normalize(value); +} + +function backupRequestPayload({ + operationId, + repositoryCommit, + source, + restore, +}) { + return { + kind: "programmable-database-backup-restore-request", + schemaVersion: 1, + operationId, + repositoryCommit, + source, + restore, + schemas: BACKUP_SCHEMAS, + format: "targeted-schema-backup-v2", + }; +} + +function validateBackupRequest(input) { + if (!isPlainRecord(input)) throw new Error("backup and restore input is invalid"); + if (!OPERATION_ID.test(input.operationId ?? "")) { + throw new Error("backup operation id is invalid"); + } + if (!COMMIT.test(input.repositoryCommit ?? "")) { + throw new Error("repository commit must be an exact full commit hash"); + } + const sslCaPem = validateCaPem(input.sslCaPem, "source Postgres CA"); + const restoreSslCaPem = validateCaPem( + input.restoreSslCaPem, + "restore Postgres CA", + ); + const source = parseSourceTarget( + input.sourceDatabaseUrl, + input.expectedProjectRef, + ); + const restore = parseRestoreTarget( + input.restoreDatabaseUrl, + input.restoreIsolationId, + ); + const backupPath = validateAbsoluteOutputPath(input.backupPath, "backup path"); + const evidencePath = validateAbsoluteOutputPath( + input.evidencePath, + "evidence path", + ); + if (backupPath === evidencePath) { + throw new Error("backup and evidence paths must differ"); + } + const payload = backupRequestPayload({ + operationId: input.operationId, + repositoryCommit: input.repositoryCommit, + source: source.safeTarget, + restore: restore.safeTarget, + }); + return { + operationId: input.operationId, + repositoryCommit: input.repositoryCommit, + source, + restore, + sslCaPem, + restoreSslCaPem, + backupPath, + evidencePath, + requestSha256: sha256(canonicalJson(payload)), + }; +} + +async function safeExistingFile(filePath) { + let metadata; + try { + metadata = await lstat(filePath); + } catch (error) { + if (error?.code === "ENOENT") return null; + throw error; + } + if ( + !metadata.isFile() || + metadata.isSymbolicLink() || + (metadata.mode & 0o777) !== 0o600 + ) { + throw new Error("operator artifact is not a private regular file"); + } + return metadata; +} + +async function fileSha256(filePath) { + const contents = await readFile(filePath); + return { + bytes: contents.byteLength, + sha256: sha256(contents), + }; +} + +function validateStoredEvidence(value, request) { + if ( + !isPlainRecord(value) || + value.kind !== "programmable-database-backup-restore-evidence" || + value.schemaVersion !== 1 || + value.operationId !== request.operationId || + value.repositoryCommit !== request.repositoryCommit || + value.requestSha256 !== request.requestSha256 || + canonicalJson(value.source) !== canonicalJson(request.source.safeTarget) || + canonicalJson(value.restore) !== canonicalJson(request.restore.safeTarget) || + !isPlainRecord(value.backup) || + !SHA256.test(value.backup.sha256 ?? "") || + !SHA256.test(value.backup.archiveListSha256 ?? "") || + !["pg-custom-v1", "empty-target-schemas-v1"].includes( + value.backup.format, + ) || + !Number.isSafeInteger(value.backup.bytes) || + value.backup.bytes <= 0 || + !SHA256.test(value.sourceManifestSha256 ?? "") || + value.restoredManifestSha256 !== value.sourceManifestSha256 || + !Number.isSafeInteger(value.tableCount) || + value.tableCount < 0 || + !Number.isSafeInteger(value.rowCount) || + value.rowCount < 0 || + (value.tableCount === 0) !== + (value.backup.format === "empty-target-schemas-v1") || + !/^PostgreSQL 17\./u.test(value.postgresVersion ?? "") || + !Number.isFinite(Date.parse(value.createdAt ?? "")) + ) { + throw new Error("stored backup and restore evidence is invalid or conflicting"); + } + return value; +} + +async function readIdempotentEvidence(request) { + const [backupMetadata, evidenceMetadata] = await Promise.all([ + safeExistingFile(request.backupPath), + safeExistingFile(request.evidencePath), + ]); + if (!backupMetadata && !evidenceMetadata) return null; + if (!backupMetadata || !evidenceMetadata) { + throw new Error("partial backup evidence conflicts with the requested operation"); + } + let evidence; + try { + evidence = JSON.parse(await readFile(request.evidencePath, "utf8")); + } catch { + throw new Error("stored backup and restore evidence is invalid or conflicting"); + } + validateStoredEvidence(evidence, request); + const backup = await fileSha256(request.backupPath); + if ( + backup.sha256 !== evidence.backup.sha256 || + backup.bytes !== evidence.backup.bytes + ) { + throw new Error("stored backup artifact conflicts with its evidence"); + } + return Object.freeze({ + kind: "programmable-database-backup-restore-result", + schemaVersion: 1, + status: "current", + changed: false, + evidence: Object.freeze(evidence), + }); +} + +async function createPrivateFile(filePath, contents = "") { + const descriptor = await open( + filePath, + fsConstants.O_CREAT | + fsConstants.O_EXCL | + fsConstants.O_WRONLY | + (fsConstants.O_NOFOLLOW ?? 0), + 0o600, + ); + try { + if (contents !== "") await descriptor.writeFile(contents, "utf8"); + await descriptor.sync(); + } finally { + await descriptor.close(); + } + await chmod(filePath, 0o600); + const metadata = await lstat(filePath); + if (!metadata.isFile() || (metadata.mode & 0o777) !== 0o600) { + throw new Error("private operator artifact permissions are invalid"); + } +} + +async function createTemporaryCa(caPem) { + const directory = await mkdtemp(path.join(os.tmpdir(), "programmable-pg-ca-")); + await chmod(directory, 0o700); + const filePath = path.join(directory, "server-ca.crt"); + try { + await createPrivateFile(filePath, caPem); + return { directory, filePath }; + } catch (error) { + await rm(directory, { recursive: true, force: true }).catch(() => {}); + throw error; + } +} + +function safeChildEnvironment({ password, caPath, applicationName }) { + const environment = { + LANG: "C", + LC_ALL: "C", + PATH: process.env.PATH ?? "/usr/bin:/bin", + PGAPPNAME: applicationName, + PGCONNECT_TIMEOUT: "8", + PGPASSWORD: password, + PGSSLMODE: "verify-full", + PGSSLROOTCERT: caPath, + }; + if (process.platform === "win32" && process.env.SYSTEMROOT) { + environment.SYSTEMROOT = process.env.SYSTEMROOT; + } + return environment; +} + +async function runCommand(binary, args, options) { + const result = await executeFile(binary, args, { + cwd: options.cwd, + env: options.env, + encoding: "buffer", + maxBuffer: 32 * 1024 * 1024, + timeout: options.timeoutMs, + windowsHide: true, + }); + return { + stdout: Buffer.isBuffer(result.stdout) + ? result.stdout + : Buffer.from(result.stdout ?? ""), + stderr: Buffer.isBuffer(result.stderr) + ? result.stderr + : Buffer.from(result.stderr ?? ""), + }; +} + +function assertCommandContainsNoSecrets(args, secrets) { + const serialized = args.join("\u0000"); + for (const secret of secrets) { + if (typeof secret === "string" && secret.length > 0 && serialized.includes(secret)) { + throw new Error("database secret reached a child-process argument"); + } + } +} + +async function executeSafeCommand({ + runner, + binary, + args, + env, + timeoutMs, + secrets, +}) { + assertCommandContainsNoSecrets(args, secrets); + try { + const result = await runner(binary, Object.freeze([...args]), { + cwd: path.dirname(args.at(-1) ?? process.cwd()), + env: Object.freeze({ ...env }), + timeoutMs, + }); + return { + stdout: Buffer.isBuffer(result?.stdout) + ? result.stdout + : Buffer.from(result?.stdout ?? ""), + stderr: Buffer.isBuffer(result?.stderr) + ? result.stderr + : Buffer.from(result?.stderr ?? ""), + }; + } catch (error) { + throw operationalFailure("Postgres backup tool", error); + } +} + +function commandTargetArguments(target, username) { + return [ + "--host", + target.host, + "--port", + String(target.port), + "--username", + username, + "--dbname", + target.database, + "--no-password", + ]; +} + +function roleBootstrapSql() { + const body = RESTORE_ROLE_NAMES.map( + (role) => ` + if not exists ( + select 1 from pg_catalog.pg_roles where rolname = '${role}' + ) then + create role ${role} + nologin nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; + alter role ${role} + nologin nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls;`, + ).join("\n"); + return `do $programmable_restore_roles$ begin ${body}\nend $programmable_restore_roles$;`; +} + +function quoteIdentifier(value) { + if (typeof value !== "string" || value.length < 1 || value.length > 63) { + throw new Error("database catalog identifier is invalid"); + } + return `"${value.replaceAll('"', '""')}"`; +} + +async function captureDatabaseManifest(sql) { + // JSON serialization of timestamptz follows the session timezone. Normalize + // both the hosted source and isolated restore before hashing so identical + // instants cannot fail verification solely because the hosts use different + // timezone settings. + await sql.unsafe("set timezone = 'UTC'").simple(); + const objects = await sql.unsafe( + ` + select + namespace.nspname as schema_name, + class.relname as object_name, + class.relkind::text as object_kind + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = any($1::text[]) + order by namespace.nspname, class.relname, class.relkind + `, + [BACKUP_SCHEMAS], + ); + const functions = await sql.unsafe( + ` + select + namespace.nspname as schema_name, + procedure.proname as function_name, + procedure.prokind::text as function_kind, + pg_catalog.pg_get_function_identity_arguments(procedure.oid) + as identity_arguments + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = any($1::text[]) + order by namespace.nspname, procedure.proname, + pg_catalog.pg_get_function_identity_arguments(procedure.oid) + `, + [BACKUP_SCHEMAS], + ); + const types = await sql.unsafe( + ` + select + namespace.nspname as schema_name, + type.typname as type_name, + type.typtype::text as type_kind + from pg_catalog.pg_type as type + join pg_catalog.pg_namespace as namespace + on namespace.oid = type.typnamespace + where namespace.nspname = any($1::text[]) + and type.typname not like '\\_%' escape '\\' + order by namespace.nspname, type.typname + `, + [BACKUP_SCHEMAS], + ); + const tables = objects.filter(({ object_kind: kind }) => ["p", "r"].includes(kind)); + const tableEvidence = []; + let totalRows = 0; + for (const table of tables) { + const schema = quoteIdentifier(table.schema_name); + const name = quoteIdentifier(table.object_name); + const rows = await sql.unsafe(` + select pg_catalog.to_jsonb(row_value)::text as row_json + from ${schema}.${name} as row_value + order by pg_catalog.to_jsonb(row_value)::text collate "C" + `); + const hash = createHash("sha256"); + for (const row of rows) { + const value = row?.row_json; + if (typeof value !== "string") { + throw new Error("database row manifest response is invalid"); + } + const bytes = Buffer.from(value); + const length = Buffer.allocUnsafe(8); + length.writeBigUInt64BE(BigInt(bytes.byteLength)); + hash.update(length); + hash.update(bytes); + } + totalRows += rows.length; + tableEvidence.push({ + schema: table.schema_name, + table: table.object_name, + rows: rows.length, + rowsSha256: `0x${hash.digest("hex")}`, + }); + } + const payload = { + schemas: BACKUP_SCHEMAS, + objects, + functions, + types, + tables: tableEvidence, + }; + return Object.freeze({ + manifestSha256: sha256(canonicalJson(payload)), + tableCount: tables.length, + rowCount: totalRows, + }); +} + +async function openRestoreDatabase({ databaseUrl, sslCaPem }) { + const parsed = new URL(databaseUrl); + parsed.searchParams.delete("sslmode"); + const sql = postgres(parsed.toString(), { + ssl: { rejectUnauthorized: true, ca: sslCaPem }, + max: 1, + prepare: false, + connect_timeout: 8, + idle_timeout: 5, + max_lifetime: 60, + onnotice: () => {}, + connection: { + application_name: "programmable-isolated-restore-verifier", + }, + }); + return { sql }; +} + +async function assertRestoreTargetIsEmpty(sql, safeTarget) { + const [identity] = await sql.unsafe(` + select + session_user::text as session_user, + current_role::text as current_role, + pg_catalog.current_database()::text as database_name, + pg_catalog.inet_server_port()::integer as server_port, + pg_catalog.pg_is_in_recovery() as in_recovery + `); + if ( + identity?.session_user !== "postgres" || + identity?.current_role !== "postgres" || + identity?.database_name !== safeTarget.database || + Number(identity?.server_port) !== safeTarget.port || + identity?.in_recovery !== false + ) { + throw new Error("isolated restore database identity is not approved"); + } + const [footprint] = await sql.unsafe( + ` + select + (select pg_catalog.count(*)::integer + from pg_catalog.pg_namespace + where nspname = any($1::text[])) as schema_count, + (select pg_catalog.count(*)::integer + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = any($1::text[])) as object_count + `, + [BACKUP_SCHEMAS], + ); + if (Number(footprint?.schema_count) !== 0 || Number(footprint?.object_count) !== 0) { + throw new Error("isolated restore database is not empty"); + } +} + +function pgVersion(stdout) { + const value = Buffer.isBuffer(stdout) ? stdout.toString("utf8") : String(stdout ?? ""); + const match = PG_TOOL_VERSION.exec(value); + if (!match || Number(match[1]) !== 17) { + throw new Error("Postgres 17 client tools are required"); + } + return `PostgreSQL ${match[0]}`; +} + +async function writeEvidence(request, evidence) { + assertNoSecretOutput(evidence, [ + request.source.password, + request.restore.password, + request.sslCaPem, + request.restoreSslCaPem, + request.sourceDatabaseUrl, + request.restoreDatabaseUrl, + ].filter(Boolean)); + try { + await createPrivateFile( + request.evidencePath, + `${JSON.stringify(evidence, null, 2)}\n`, + ); + } catch (error) { + await rm(request.evidencePath, { force: true }).catch(() => {}); + throw error; + } +} + +export async function createBackupAndRestoreEvidence(input) { + const request = validateBackupRequest(input); + request.sourceDatabaseUrl = input.sourceDatabaseUrl; + request.restoreDatabaseUrl = input.restoreDatabaseUrl; + const dependencies = validateDependencies(input.dependencies, [ + "runCommand", + "openHostedDatabase", + "openRestoreDatabase", + "closeHostedDatabase", + "captureDatabaseManifest", + "assertRestoreTargetIsEmpty", + "now", + ]); + const runner = dependencies.runCommand ?? runCommand; + const openSource = dependencies.openHostedDatabase ?? openHostedDatabase; + const openRestore = dependencies.openRestoreDatabase ?? openRestoreDatabase; + const closeDatabase = dependencies.closeHostedDatabase ?? closeHostedDatabase; + const captureManifest = + dependencies.captureDatabaseManifest ?? captureDatabaseManifest; + const assertRestoreEmpty = + dependencies.assertRestoreTargetIsEmpty ?? assertRestoreTargetIsEmpty; + const now = dependencies.now ?? (() => new Date()); + let sourceConnection; + let restoreConnection; + let sourceCa; + let restoreCa; + let backupCreated = false; + try { + const existing = await readIdempotentEvidence(request); + if (existing) return existing; + sourceCa = await createTemporaryCa(request.sslCaPem); + restoreCa = await createTemporaryCa(request.restoreSslCaPem); + sourceConnection = await openSource({ + databaseUrl: input.sourceDatabaseUrl, + expectedProjectRef: input.expectedProjectRef, + sslCaPem: request.sslCaPem, + }); + restoreConnection = await openRestore({ + databaseUrl: input.restoreDatabaseUrl, + sslCaPem: request.restoreSslCaPem, + safeTarget: request.restore.safeTarget, + }); + await assertRestoreEmpty(restoreConnection.sql, request.restore.safeTarget); + const before = await captureManifest(sourceConnection.sql); + if ( + !SHA256.test(before?.manifestSha256 ?? "") || + !Number.isSafeInteger(before?.tableCount) || + before.tableCount < 0 || + !Number.isSafeInteger(before?.rowCount) || + before.rowCount < 0 + ) { + throw new Error("source database manifest is invalid"); + } + const secrets = [ + request.source.password, + request.restore.password, + input.sourceDatabaseUrl, + input.restoreDatabaseUrl, + request.sslCaPem, + request.restoreSslCaPem, + ]; + const sourceEnvironment = safeChildEnvironment({ + password: request.source.password, + caPath: sourceCa.filePath, + applicationName: "programmable-pg-backup", + }); + const restoreEnvironment = safeChildEnvironment({ + password: request.restore.password, + caPath: restoreCa.filePath, + applicationName: "programmable-pg-restore-test", + }); + const versionResult = await executeSafeCommand({ + runner, + binary: input.pgDumpBinary ?? "pg_dump", + args: ["--version"], + env: sourceEnvironment, + timeoutMs: 15_000, + secrets, + }); + const postgresVersion = pgVersion(versionResult.stdout); + let backupFormat; + let listResult; + if (before.tableCount === 0 && before.rowCount === 0) { + backupFormat = "empty-target-schemas-v1"; + const emptyArtifact = `${canonicalJson({ + kind: backupFormat, + schemaVersion: 1, + sourceManifestSha256: before.manifestSha256, + })}\n`; + await createPrivateFile(request.backupPath, emptyArtifact); + backupCreated = true; + listResult = { + stdout: Buffer.from(emptyArtifact), + stderr: Buffer.alloc(0), + }; + } else { + backupFormat = "pg-custom-v1"; + await createPrivateFile(request.backupPath); + backupCreated = true; + const dumpArguments = [ + "--format=custom", + "--compress=6", + "--serializable-deferrable", + "--no-owner", + "--no-privileges", + ...BACKUP_SCHEMAS.flatMap((schema) => ["--schema", schema]), + ...commandTargetArguments(request.source.safeTarget, request.source.username), + "--file", + request.backupPath, + ]; + await executeSafeCommand({ + runner, + binary: input.pgDumpBinary ?? "pg_dump", + args: dumpArguments, + env: sourceEnvironment, + timeoutMs: 15 * 60_000, + secrets, + }); + await chmod(request.backupPath, 0o600); + } + const after = await captureManifest(sourceConnection.sql); + if ( + after?.manifestSha256 !== before.manifestSha256 || + after?.tableCount !== before.tableCount || + after?.rowCount !== before.rowCount + ) { + throw new Error("source database changed during the backup window"); + } + if (backupFormat === "pg-custom-v1") { + listResult = await executeSafeCommand({ + runner, + binary: input.pgRestoreBinary ?? "pg_restore", + args: ["--list", request.backupPath], + env: sourceEnvironment, + timeoutMs: 60_000, + secrets, + }); + } + if (listResult.stdout.byteLength < 1) { + throw new Error("Postgres backup archive listing is empty"); + } + if (backupFormat === "pg-custom-v1") { + await executeSafeCommand({ + runner, + binary: input.psqlBinary ?? "psql", + args: [ + "--no-psqlrc", + "--quiet", + "--set", + "ON_ERROR_STOP=1", + ...commandTargetArguments(request.restore.safeTarget, request.restore.username), + "--command", + roleBootstrapSql(), + ], + env: restoreEnvironment, + timeoutMs: 60_000, + secrets, + }); + await executeSafeCommand({ + runner, + binary: input.pgRestoreBinary ?? "pg_restore", + args: [ + "--exit-on-error", + "--single-transaction", + "--no-owner", + "--no-privileges", + ...commandTargetArguments(request.restore.safeTarget, request.restore.username), + request.backupPath, + ], + env: restoreEnvironment, + timeoutMs: 15 * 60_000, + secrets, + }); + } + const restored = await captureManifest(restoreConnection.sql); + if ( + restored?.manifestSha256 !== before.manifestSha256 || + restored?.tableCount !== before.tableCount || + restored?.rowCount !== before.rowCount + ) { + throw new Error("isolated restore does not match the source manifest"); + } + const backup = await fileSha256(request.backupPath); + if (backup.bytes < 1) throw new Error("Postgres backup archive is empty"); + const createdAt = now(); + if (!(createdAt instanceof Date) || !Number.isFinite(createdAt.getTime())) { + throw new Error("backup evidence timestamp is invalid"); + } + const evidence = Object.freeze({ + kind: "programmable-database-backup-restore-evidence", + schemaVersion: 1, + operationId: request.operationId, + repositoryCommit: request.repositoryCommit, + requestSha256: request.requestSha256, + source: request.source.safeTarget, + restore: request.restore.safeTarget, + backup: Object.freeze({ + format: backupFormat, + sha256: backup.sha256, + bytes: backup.bytes, + archiveListSha256: sha256(listResult.stdout), + }), + sourceManifestSha256: before.manifestSha256, + restoredManifestSha256: restored.manifestSha256, + tableCount: before.tableCount, + rowCount: before.rowCount, + postgresVersion, + createdAt: createdAt.toISOString(), + }); + await writeEvidence(request, evidence); + return Object.freeze({ + kind: "programmable-database-backup-restore-result", + schemaVersion: 1, + status: "created", + changed: true, + evidence, + }); + } catch (error) { + if (backupCreated) { + await rm(request.backupPath, { force: true }).catch(() => {}); + } + throw operationalFailure("database backup and isolated restore", error); + } finally { + if (sourceConnection?.sql) await closeDatabase(sourceConnection.sql).catch(() => {}); + if (restoreConnection?.sql) await closeDatabase(restoreConnection.sql).catch(() => {}); + if (sourceCa?.directory) { + await rm(sourceCa.directory, { recursive: true, force: true }).catch(() => {}); + } + if (restoreCa?.directory) { + await rm(restoreCa.directory, { recursive: true, force: true }).catch(() => {}); + } + } +} diff --git a/scripts/data-pipeline/cutover-credentials.test.mjs b/scripts/data-pipeline/cutover-credentials.test.mjs new file mode 100644 index 00000000..eac39732 --- /dev/null +++ b/scripts/data-pipeline/cutover-credentials.test.mjs @@ -0,0 +1,779 @@ +import assert from "node:assert/strict"; +import { lstat, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + ROLE_SPECS, + createBackupAndRestoreEvidence, + provisionLoginRoles, + verifyPoolerLogins, +} from "./cutover-credentials.mjs"; + +const PROJECT_REF = "mnnvlrqwhfoppogslsje"; +const COMMIT = "a".repeat(40); +const SOURCE_PASSWORD = "Source_password_0123456789_ABCDEFGHIJK"; +const RESTORE_PASSWORD = "Restore_password_0123456789_ABCDEFGHIJ"; +const CA = `-----BEGIN CERTIFICATE-----\n${"A".repeat(96)}\n-----END CERTIFICATE-----`; +const RESTORE_CA = `-----BEGIN CERTIFICATE-----\n${"B".repeat(96)}\n-----END CERTIFICATE-----`; + +function credentials(prefix = "credential") { + return Object.fromEntries( + ROLE_SPECS.map(({ key }, index) => [ + key, + `${prefix}_${String(index).padStart(2, "0")}_${"X".repeat(34)}`, + ]), + ); +} + +function rolePosture(requirePasswords = true) { + const rows = ROLE_SPECS.flatMap(({ loginRole, capabilityRole }) => [ + { + rolname: loginRole, + rolcanlogin: true, + rolsuper: false, + rolcreatedb: false, + rolcreaterole: false, + rolinherit: false, + rolreplication: false, + rolbypassrls: false, + rolconnlimit: -1, + rolconfig: null, + has_password: requirePasswords, + }, + { + rolname: capabilityRole, + rolcanlogin: false, + rolsuper: false, + rolcreatedb: false, + rolcreaterole: false, + rolinherit: false, + rolreplication: false, + rolbypassrls: false, + rolconnlimit: -1, + rolconfig: null, + has_password: false, + }, + ]); + const memberships = ROLE_SPECS.map(({ loginRole, capabilityRole }) => ({ + member_role: loginRole, + granted_role: capabilityRole, + admin_option: false, + inherit_option: false, + set_option: true, + })); + return { rows, memberships, requirePasswords }; +} + +function pending(value, simpleEffect) { + const promise = Promise.resolve(value); + promise.simple = async () => { + if (simpleEffect) simpleEffect(); + return value; + }; + return promise; +} + +function directProvisioningSql(secretObservations) { + let rotationCount = 0; + const sql = { + unsafe(query) { + if (query.includes("current_user::text")) { + return pending([ + { + session_user: "postgres", + current_user: "postgres", + current_role: "postgres", + database_name: "postgres", + server_port: 5432, + }, + ]); + } + if (query.includes("pg_catalog.pg_authid")) { + return pending(rolePosture(rotationCount === ROLE_SPECS.length).rows); + } + if (query.includes("from pg_catalog.pg_auth_members")) { + return pending(rolePosture().memberships); + } + throw new Error("unexpected direct SQL"); + }, + async begin(callback) { + const transaction = async (strings, ...values) => { + assert.match(strings.join(""), /set_config/u); + assert.equal(values.length, 1); + secretObservations.push(values[0]); + return []; + }; + transaction.unsafe = (query) => { + if (query.includes("select session_user::text")) { + return pending([{ session_user: "postgres", current_role: "postgres" }]); + } + assert.match(query, /do \$credential_rotation\$/u); + for (const secret of secretObservations) { + assert.equal(query.includes(secret), false); + } + return pending([], () => { + rotationCount += 1; + }); + }; + return callback(transaction); + }, + }; + return sql; +} + +test("ROLE_SPECS is the exact immutable five-role contract", () => { + assert.equal(Object.isFrozen(ROLE_SPECS), true); + assert.deepEqual( + ROLE_SPECS.map(({ key, loginRole, capabilityRole }) => ({ + key, + loginRole, + capabilityRole, + })), + [ + { + key: "apiReader", + loginRole: "programmable_api_reader_login", + capabilityRole: "programmable_api_reader", + }, + { + key: "projector", + loginRole: "programmable_projector_login", + capabilityRole: "programmable_projector", + }, + { + key: "projectorRuntime", + loginRole: "programmable_projector_runtime_login", + capabilityRole: "programmable_projector_runtime", + }, + { + key: "reconciler", + loginRole: "programmable_reconciler_login", + capabilityRole: "programmable_reconciler", + }, + { + key: "releaseProbe", + loginRole: "programmable_release_probe_nonce_login", + capabilityRole: "programmable_release_probe_nonce", + }, + ], + ); + assert.equal(ROLE_SPECS.every(Object.isFrozen), true); +}); + +test("provisionLoginRoles sends every password only as a bound value", async () => { + const values = credentials(); + const observedSecrets = []; + const sql = directProvisioningSql(observedSecrets); + let closed = 0; + const result = await provisionLoginRoles({ + databaseUrl: `postgresql://postgres:${SOURCE_PASSWORD}@db.${PROJECT_REF}.supabase.co:5432/postgres?sslmode=verify-full`, + expectedProjectRef: PROJECT_REF, + sslCaPem: CA, + credentials: values, + dependencies: { + openHostedDatabase: async () => ({ + sql, + target: { + projectRef: PROJECT_REF, + host: `db.${PROJECT_REF}.supabase.co`, + port: 5432, + database: "postgres", + sslMode: "verify-full", + }, + }), + closeHostedDatabase: async () => { + closed += 1; + }, + }, + }); + assert.deepEqual(observedSecrets, ROLE_SPECS.map(({ key }) => values[key])); + assert.equal(closed, 1); + assert.equal(result.roles.length, 5); + const serialized = JSON.stringify(result); + for (const password of Object.values(values)) { + assert.equal(serialized.includes(password), false); + } +}); + +test("provisionLoginRoles rejects missing, extra, duplicate and weak credentials", async () => { + const base = { + databaseUrl: `postgresql://postgres:${SOURCE_PASSWORD}@db.${PROJECT_REF}.supabase.co:5432/postgres?sslmode=verify-full`, + expectedProjectRef: PROJECT_REF, + sslCaPem: CA, + dependencies: { + openHostedDatabase: async () => { + throw new Error("must not open"); + }, + }, + }; + const missing = credentials(); + delete missing.releaseProbe; + await assert.rejects( + provisionLoginRoles({ ...base, credentials: missing }), + /exactly five/u, + ); + await assert.rejects( + provisionLoginRoles({ ...base, credentials: { ...credentials(), extra: "X".repeat(40) } }), + /exactly five/u, + ); + const duplicate = credentials(); + duplicate.projector = duplicate.apiReader; + await assert.rejects( + provisionLoginRoles({ ...base, credentials: duplicate }), + /must be unique/u, + ); + await assert.rejects( + provisionLoginRoles({ + ...base, + credentials: { ...credentials(), apiReader: "short" }, + }), + /not a valid generated password/u, + ); +}); + +test("provisionLoginRoles fails closed on excess membership and redacts dependency errors", async () => { + const values = credentials("private"); + const posture = rolePosture(false); + posture.memberships.push({ + member_role: ROLE_SPECS[0].loginRole, + granted_role: "pg_read_all_data", + admin_option: false, + inherit_option: false, + set_option: true, + }); + let closed = false; + await assert.rejects( + provisionLoginRoles({ + databaseUrl: `postgresql://postgres:${SOURCE_PASSWORD}@db.${PROJECT_REF}.supabase.co:5432/postgres?sslmode=verify-full`, + expectedProjectRef: PROJECT_REF, + sslCaPem: CA, + credentials: values, + dependencies: { + openHostedDatabase: async () => ({ sql: {}, target: {} }), + closeHostedDatabase: async () => { + closed = true; + }, + assertDirectOperatorIdentity: async () => {}, + readRolePosture: async () => posture, + rotateLoginPassword: async () => { + throw new Error(values.apiReader); + }, + }, + }), + (error) => { + assert.equal(error.message, "login-role provisioning failed"); + assert.equal(error.message.includes(values.apiReader), false); + return true; + }, + ); + assert.equal(closed, true); +}); + +function poolerSql(spec, setRoleStatements) { + let activeRole = spec.loginRole; + return { + unsafe(query) { + if (query.includes("from pg_catalog.pg_roles")) { + return pending(rolePosture().rows); + } + if (query.includes("from pg_catalog.pg_auth_members")) { + return pending(rolePosture().memberships); + } + throw new Error("unexpected pooler posture SQL"); + }, + async begin(callback) { + const transaction = { + unsafe(query) { + if (query.trim().startsWith("set local role")) { + return pending([], () => { + setRoleStatements.push(query.trim()); + activeRole = spec.capabilityRole; + }); + } + if (query.includes("configured_role")) { + return pending([ + { + session_user: spec.loginRole, + current_role: activeRole, + configured_role: activeRole, + database_name: "postgres", + }, + ]); + } + return pending([ + { + session_user: spec.loginRole, + current_role: activeRole, + database_name: "postgres", + }, + ]); + }, + }; + return callback(transaction); + }, + }; +} + +test("verifyPoolerLogins checks all five transaction-pooler identities with SET LOCAL ROLE", async () => { + const values = credentials("pooler"); + const opens = []; + const closes = []; + const setRoleStatements = []; + const result = await verifyPoolerLogins({ + expectedProjectRef: PROJECT_REF, + poolerHost: "aws-0-eu-central-1.pooler.supabase.com", + sslCaPem: CA, + credentials: values, + dependencies: { + openPoolerDatabase: async (entry) => { + opens.push(entry); + return { sql: poolerSql(entry.spec, setRoleStatements) }; + }, + closePoolerDatabase: async (sql) => { + closes.push(sql); + }, + }, + }); + assert.equal(opens.length, 5); + assert.equal(closes.length, 5); + assert.equal(opens.every(({ options }) => options.prepare === false), true); + assert.deepEqual( + setRoleStatements, + ROLE_SPECS.map(({ capabilityRole }) => `set local role ${capabilityRole}`), + ); + assert.equal(result.target.port, 6543); + assert.equal(result.target.sslMode, "verify-full"); + assert.equal(result.target.prepare, false); + assert.equal(result.roles.length, 5); + const serialized = JSON.stringify(result); + for (const password of Object.values(values)) { + assert.equal(serialized.includes(password), false); + } +}); + +test("verifyPoolerLogins rejects an unreviewed host and an identity mismatch", async () => { + await assert.rejects( + verifyPoolerLogins({ + expectedProjectRef: PROJECT_REF, + poolerHost: "attacker.example", + sslCaPem: CA, + credentials: credentials(), + }), + /pooler host is invalid/u, + ); + const secret = credentials("identity_secret"); + await assert.rejects( + verifyPoolerLogins({ + expectedProjectRef: PROJECT_REF, + poolerHost: "aws-0-eu-central-1.pooler.supabase.com", + sslCaPem: CA, + credentials: secret, + dependencies: { + openPoolerDatabase: async () => ({ sql: {} }), + closePoolerDatabase: async () => {}, + readPoolerRolePosture: async () => rolePosture(), + verifyPoolerSession: async () => { + throw new Error(`wrong identity ${secret.apiReader}`); + }, + }, + }), + (error) => { + assert.equal(error.message, "pooler login verification failed"); + assert.equal(error.message.includes(secret.apiReader), false); + return true; + }, + ); +}); + +async function backupFixture() { + const directory = await mkdtemp(path.join(os.tmpdir(), "cutover-credentials-test-")); + const backupPath = path.join(directory, "read-model.dump"); + const evidencePath = path.join(directory, "read-model.evidence.json"); + const sourceDatabaseUrl = `postgresql://postgres:${encodeURIComponent( + SOURCE_PASSWORD, + )}@db.${PROJECT_REF}.supabase.co:5432/postgres?sslmode=verify-full`; + const restoreDatabaseUrl = `postgresql://postgres:${encodeURIComponent( + RESTORE_PASSWORD, + )}@127.0.0.1:55432/programmable_restore_cutover01?sslmode=verify-full`; + return { + directory, + input: { + operationId: "production-cutover-01", + repositoryCommit: COMMIT, + sourceDatabaseUrl, + expectedProjectRef: PROJECT_REF, + sslCaPem: CA, + restoreDatabaseUrl, + restoreIsolationId: "cutover01", + restoreSslCaPem: RESTORE_CA, + backupPath, + evidencePath, + }, + }; +} + +function manifest(value = "c") { + return { + manifestSha256: `0x${value.repeat(64)}`, + tableCount: 27, + rowCount: 265, + }; +} + +function backupDependencies(fixture, overrides = {}) { + const calls = []; + const caPaths = new Set(); + let closeCount = 0; + const dependencies = { + openHostedDatabase: async ({ databaseUrl, expectedProjectRef, sslCaPem }) => { + assert.equal(databaseUrl, fixture.input.sourceDatabaseUrl); + assert.equal(expectedProjectRef, PROJECT_REF); + assert.equal(sslCaPem, CA); + return { + sql: { side: "source" }, + target: { + projectRef: PROJECT_REF, + host: `db.${PROJECT_REF}.supabase.co`, + port: 5432, + database: "postgres", + sslMode: "verify-full", + }, + }; + }, + openRestoreDatabase: async ({ databaseUrl, sslCaPem, safeTarget }) => { + assert.equal(databaseUrl, fixture.input.restoreDatabaseUrl); + assert.equal(sslCaPem, RESTORE_CA); + assert.equal(safeTarget.database, "programmable_restore_cutover01"); + return { sql: { side: "restore" } }; + }, + closeHostedDatabase: async () => { + closeCount += 1; + }, + assertRestoreTargetIsEmpty: async (sql, target) => { + assert.equal(sql.side, "restore"); + assert.equal(target.host, "127.0.0.1"); + }, + captureDatabaseManifest: async () => manifest(), + now: () => new Date("2026-08-01T08:00:00.000Z"), + runCommand: async (binary, args, options) => { + calls.push({ binary, args, options }); + for (const secret of [ + SOURCE_PASSWORD, + RESTORE_PASSWORD, + fixture.input.sourceDatabaseUrl, + fixture.input.restoreDatabaseUrl, + CA, + RESTORE_CA, + ]) { + assert.equal(args.join("\n").includes(secret), false); + } + assert.equal(options.env.PGSSLMODE, "verify-full"); + assert.equal(options.env.PGCONNECT_TIMEOUT, "8"); + assert.equal( + Object.keys(options.env).every((key) => + [ + "LANG", + "LC_ALL", + "PATH", + "PGAPPNAME", + "PGCONNECT_TIMEOUT", + "PGPASSWORD", + "PGSSLMODE", + "PGSSLROOTCERT", + "SYSTEMROOT", + ].includes(key), + ), + true, + ); + caPaths.add(options.env.PGSSLROOTCERT); + const caMetadata = await lstat(options.env.PGSSLROOTCERT); + assert.equal(caMetadata.mode & 0o777, 0o600); + const ca = await readFile(options.env.PGSSLROOTCERT, "utf8"); + assert.equal([CA, RESTORE_CA].includes(ca), true); + if (args.includes("--version")) { + return { stdout: Buffer.from("pg_dump (PostgreSQL) 17.6\n"), stderr: Buffer.alloc(0) }; + } + if (binary === "pg_dump") { + const fileIndex = args.indexOf("--file"); + assert.notEqual(fileIndex, -1); + await writeFile(args[fileIndex + 1], Buffer.from("valid-custom-archive")); + } + if (binary === "pg_restore" && args[0] === "--list") { + return { stdout: Buffer.from("; Archive created at 2026-08-01\nTABLE DATA\n"), stderr: Buffer.alloc(0) }; + } + return { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) }; + }, + ...overrides, + }; + return { + dependencies, + calls, + caPaths, + get closeCount() { + return closeCount; + }, + }; +} + +test("createBackupAndRestoreEvidence keeps secrets in child env and proves an isolated restore", async (t) => { + const fixture = await backupFixture(); + t.after(() => rm(fixture.directory, { recursive: true, force: true })); + const harness = backupDependencies(fixture); + const result = await createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: harness.dependencies, + }); + assert.equal(result.status, "created"); + assert.equal(result.changed, true); + assert.equal(result.evidence.source.port, 5432); + assert.equal(result.evidence.restore.database, "programmable_restore_cutover01"); + assert.equal(result.evidence.sourceManifestSha256, manifest().manifestSha256); + assert.equal(result.evidence.restoredManifestSha256, manifest().manifestSha256); + assert.equal(result.evidence.tableCount, 27); + assert.equal(result.evidence.rowCount, 265); + assert.equal(result.evidence.backup.format, "pg-custom-v1"); + assert.equal(result.evidence.postgresVersion, "PostgreSQL 17.6"); + assert.equal(harness.closeCount, 2); + const dump = harness.calls.find( + ({ binary, args }) => binary === "pg_dump" && !args.includes("--version"), + ); + assert.ok(dump); + assert.deepEqual( + dump.args.filter((value) => value === "--schema").length, + 3, + ); + assert.equal(dump.args.includes("--serializable-deferrable"), true); + assert.equal(dump.args.includes("--no-owner"), true); + assert.equal(dump.args.includes("--no-privileges"), true); + const restore = harness.calls.find( + ({ binary, args }) => binary === "pg_restore" && args.includes("--single-transaction"), + ); + assert.ok(restore); + assert.equal(restore.args.includes("--exit-on-error"), true); + const psql = harness.calls.find(({ binary }) => binary === "psql"); + assert.ok(psql); + const roleSql = psql.args.at(-1); + for (const { loginRole, capabilityRole } of ROLE_SPECS) { + assert.match(roleSql, new RegExp(loginRole, "u")); + assert.match(roleSql, new RegExp(capabilityRole, "u")); + } + const [backupMode, evidenceMode] = await Promise.all([ + lstat(fixture.input.backupPath), + lstat(fixture.input.evidencePath), + ]); + assert.equal(backupMode.mode & 0o777, 0o600); + assert.equal(evidenceMode.mode & 0o777, 0o600); + const serialized = JSON.stringify(result); + for (const secret of [SOURCE_PASSWORD, RESTORE_PASSWORD, CA, RESTORE_CA]) { + assert.equal(serialized.includes(secret), false); + } + for (const caPath of harness.caPaths) { + await assert.rejects(lstat(caPath), { code: "ENOENT" }); + } +}); + +test("createBackupAndRestoreEvidence records an exact empty target-schema baseline", async (t) => { + const fixture = await backupFixture(); + t.after(() => rm(fixture.directory, { recursive: true, force: true })); + const emptyManifest = { + manifestSha256: `0x${"e".repeat(64)}`, + tableCount: 0, + rowCount: 0, + }; + const harness = backupDependencies(fixture, { + captureDatabaseManifest: async () => emptyManifest, + }); + const result = await createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: harness.dependencies, + }); + + assert.equal(result.status, "created"); + assert.equal(result.evidence.tableCount, 0); + assert.equal(result.evidence.rowCount, 0); + assert.equal(result.evidence.backup.format, "empty-target-schemas-v1"); + assert.equal( + harness.calls.some( + ({ binary, args }) => binary === "pg_dump" && !args.includes("--version"), + ), + false, + ); + assert.equal( + harness.calls.some(({ binary }) => binary === "pg_restore" || binary === "psql"), + false, + ); +}); + +test("createBackupAndRestoreEvidence is idempotent only for matching private evidence", async (t) => { + const fixture = await backupFixture(); + t.after(() => rm(fixture.directory, { recursive: true, force: true })); + const firstHarness = backupDependencies(fixture); + const first = await createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: firstHarness.dependencies, + }); + let externalCalls = 0; + const second = await createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: { + runCommand: async () => { + externalCalls += 1; + throw new Error("must not execute"); + }, + openHostedDatabase: async () => { + externalCalls += 1; + throw new Error("must not connect"); + }, + openRestoreDatabase: async () => { + externalCalls += 1; + throw new Error("must not connect"); + }, + }, + }); + assert.equal(externalCalls, 0); + assert.equal(second.status, "current"); + assert.equal(second.changed, false); + assert.deepEqual(second.evidence, first.evidence); + + const stored = JSON.parse(await readFile(fixture.input.evidencePath, "utf8")); + stored.repositoryCommit = "b".repeat(40); + await writeFile(fixture.input.evidencePath, `${JSON.stringify(stored)}\n`, { + mode: 0o600, + }); + await assert.rejects( + createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: { + runCommand: async () => { + throw new Error("must not execute"); + }, + }, + }), + /database backup and isolated restore failed/u, + ); + assert.equal((await lstat(fixture.input.backupPath)).isFile(), true); +}); + +test("createBackupAndRestoreEvidence rejects non-direct source and non-isolated restore targets", async (t) => { + const fixture = await backupFixture(); + t.after(() => rm(fixture.directory, { recursive: true, force: true })); + await assert.rejects( + createBackupAndRestoreEvidence({ + ...fixture.input, + sourceDatabaseUrl: `postgresql://postgres:${SOURCE_PASSWORD}@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full`, + }), + /direct Supabase endpoint/u, + ); + await assert.rejects( + createBackupAndRestoreEvidence({ + ...fixture.input, + restoreDatabaseUrl: `postgresql://postgres:${RESTORE_PASSWORD}@db.other.supabase.co:5432/postgres?sslmode=verify-full`, + }), + /isolated loopback/u, + ); +}); + +test("createBackupAndRestoreEvidence removes partial backup and redacts tool failures", async (t) => { + const fixture = await backupFixture(); + t.after(() => rm(fixture.directory, { recursive: true, force: true })); + const harness = backupDependencies(fixture, { + runCommand: async (binary, args) => { + if (args.includes("--version")) { + return { stdout: Buffer.from("pg_dump (PostgreSQL) 17.6\n") }; + } + if (binary === "pg_dump") { + const fileIndex = args.indexOf("--file"); + await writeFile(args[fileIndex + 1], "partial"); + const error = new Error(`failure ${SOURCE_PASSWORD}`); + error.code = "EFAIL"; + throw error; + } + return { stdout: Buffer.alloc(0) }; + }, + }); + await assert.rejects( + createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: harness.dependencies, + }), + (error) => { + assert.equal( + error.message, + "database backup and isolated restore failed", + ); + assert.equal(error.message.includes(SOURCE_PASSWORD), false); + return true; + }, + ); + await assert.rejects(lstat(fixture.input.backupPath), { code: "ENOENT" }); + await assert.rejects(lstat(fixture.input.evidencePath), { code: "ENOENT" }); + assert.equal(harness.closeCount, 2); +}); + +test("createBackupAndRestoreEvidence fails closed on source drift or restore mismatch", async (t) => { + const fixture = await backupFixture(); + t.after(() => rm(fixture.directory, { recursive: true, force: true })); + let captureCount = 0; + const harness = backupDependencies(fixture, { + captureDatabaseManifest: async () => { + captureCount += 1; + return captureCount === 1 ? manifest("c") : manifest("d"); + }, + }); + await assert.rejects( + createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: harness.dependencies, + }), + /database backup and isolated restore failed/u, + ); + await assert.rejects(lstat(fixture.input.backupPath), { code: "ENOENT" }); + assert.equal( + harness.calls.some(({ binary }) => binary === "psql"), + false, + ); +}); + +test("createBackupAndRestoreEvidence requires Postgres 17 tools and a clean target", async (t) => { + const fixture = await backupFixture(); + t.after(() => rm(fixture.directory, { recursive: true, force: true })); + const oldTools = backupDependencies(fixture, { + runCommand: async (_binary, args) => { + if (args.includes("--version")) { + return { stdout: Buffer.from("pg_dump (PostgreSQL) 16.9\n") }; + } + return { stdout: Buffer.alloc(0) }; + }, + }); + await assert.rejects( + createBackupAndRestoreEvidence({ + ...fixture.input, + dependencies: oldTools.dependencies, + }), + /database backup and isolated restore failed/u, + ); + await assert.rejects(lstat(fixture.input.backupPath), { code: "ENOENT" }); + + const second = await backupFixture(); + t.after(() => rm(second.directory, { recursive: true, force: true })); + let ran = false; + const dirty = backupDependencies(second, { + assertRestoreTargetIsEmpty: async () => { + throw new Error("target contains data"); + }, + runCommand: async () => { + ran = true; + return { stdout: Buffer.alloc(0) }; + }, + }); + await assert.rejects( + createBackupAndRestoreEvidence({ + ...second.input, + dependencies: dirty.dependencies, + }), + /database backup and isolated restore failed/u, + ); + assert.equal(ran, false); +}); diff --git a/scripts/data-pipeline/cutover-database.mjs b/scripts/data-pipeline/cutover-database.mjs new file mode 100644 index 00000000..6950ab20 --- /dev/null +++ b/scripts/data-pipeline/cutover-database.mjs @@ -0,0 +1,360 @@ +import { readFile } from "node:fs/promises"; + +import { + canonicalJson, + sha256, +} from "./hosted-db-operator-core.mjs"; +import { + closeHostedDatabase, + openHostedDatabase, +} from "./hosted-db-postgres.mjs"; + +const BYTES32 = /^0x(?!0{64}$)[0-9a-f]{64}$/u; +const COMMIT = /^[0-9a-f]{40}$/u; +const DEPLOYMENT_ID = /^dpl_[A-Za-z0-9]{20,80}$/u; + +function integer(value, label) { + const normalized = typeof value === "bigint" ? value.toString() : String(value); + if (!/^(?:0|[1-9][0-9]*)$/u.test(normalized)) { + throw new Error(`${label} is invalid`); + } + return normalized; +} + +function bytes(value, label) { + const normalized = typeof value === "string" ? value.toLowerCase() : ""; + if (!BYTES32.test(normalized)) throw new Error(`${label} is invalid`); + return Buffer.from(normalized.slice(2), "hex"); +} + +function rowBytes(value, label) { + if (!Buffer.isBuffer(value) || value.length !== 32) { + throw new Error(`${label} is invalid`); + } + return `0x${value.toString("hex")}`; +} + +function timestamp(value, label) { + const date = value instanceof Date ? value : new Date(String(value)); + if (!Number.isFinite(date.valueOf())) throw new Error(`${label} is invalid`); + return date.toISOString(); +} + +export function buildDatabasePromotionInput(input) { + if ( + typeof input.envioProviderDeploymentId !== "string" || + !/^[0-9a-f-]{36}$/u.test(input.envioProviderDeploymentId) || + !COMMIT.test(input.productCommit ?? "") || + !DEPLOYMENT_ID.test(input.stagedDeploymentId ?? "") + ) { + throw new Error("database promotion identity is invalid"); + } + const promotedAt = timestamp(input.promotedAt, "database promotion timestamp"); + const payload = Object.freeze({ + schemaVersion: 1, + candidateEndpointIdentity: input.candidateEndpointIdentity, + envioProviderDeploymentId: input.envioProviderDeploymentId, + baselineCommitment: input.baselineCommitment, + candidateInventoryParityCommitment: input.candidateInventoryParityCommitment, + envioPromotionAttestationCommitment: input.envioPromotionAttestationCommitment, + productCommit: input.productCommit, + stagedDeploymentId: input.stagedDeploymentId, + promotedAt, + }); + const normalized = Object.freeze({ + ...payload, + baselineCommitment: `0x${bytes(payload.baselineCommitment, "baseline commitment").toString("hex")}`, + candidateInventoryParityCommitment: + `0x${bytes(payload.candidateInventoryParityCommitment, "candidate inventory parity commitment").toString("hex")}`, + envioPromotionAttestationCommitment: + `0x${bytes(payload.envioPromotionAttestationCommitment, "Envio promotion attestation").toString("hex")}`, + }); + return Object.freeze({ + ...normalized, + inputCommitment: sha256( + `programmable:candidate-database-promotion:v1\0${canonicalJson(normalized)}`, + ), + }); +} + +export async function inspectCandidateDatabase(sql) { + const rows = await sql.unsafe(` + select control.database_mode::text as database_mode, + control.envio_provider_deployment_id::text as envio_provider_deployment_id, + control.promotion_attestation_commitment, + control.product_commit, + control.staged_deployment_id, + control.promoted_at, + (select pg_catalog.count(*)::text + from programmable_private.projection_publications) as publication_count + from programmable_private.candidate_database_control as control + where control.singleton + `); + if (rows.length !== 1) throw new Error("candidate database control is unavailable"); + const row = rows[0]; + return Object.freeze({ + databaseMode: row.database_mode, + envioProviderDeploymentId: row.envio_provider_deployment_id, + promoted: row.promoted_at !== null, + promotedAt: row.promoted_at === null ? null : timestamp(row.promoted_at, "database promotion timestamp"), + publicationCount: Number(row.publication_count), + promotionAttestationCommitment: + row.promotion_attestation_commitment === null + ? null + : rowBytes(row.promotion_attestation_commitment, "database promotion attestation"), + productCommit: row.product_commit, + stagedDeploymentId: row.staged_deployment_id, + }); +} + +export async function attestCandidateDatabasePromotion({ sql, promotion }) { + return sql.begin(async (transaction) => { + await transaction.unsafe( + "set local lock_timeout = '4s'; set local statement_timeout = '30s'", + ).simple(); + const [lock] = await transaction.unsafe(` + select pg_catalog.pg_try_advisory_xact_lock( + pg_catalog.hashtextextended('programmable:candidate-cutover:v1', 0) + ) as acquired + `); + if (lock?.acquired !== true) { + throw new Error("another candidate cutover operator holds the database lock"); + } + const [sourceLease] = await transaction.unsafe(` + select lease_generation::text as lease_generation, + expires_at, + released_at, + pg_catalog.clock_timestamp() as observed_at + from programmable_private.projector_runtime_lease_current + where singleton_key = 'canonical-projector-runtime-v1' + for update + `); + const [marketLease] = await transaction.unsafe(` + select lease_generation::text as lease_generation, + expires_at, + released_at, + pg_catalog.clock_timestamp() as observed_at + from programmable_private.market_projector_runtime_lease_current + where singleton_key = 'canonical-market-projector-runtime-v1' + for update + `); + assertPromotionLeaseRowsDrained([ + { projector: "source", ...sourceLease }, + { projector: "market", ...marketLease }, + ]); + await transaction.unsafe("set local role programmable_operator").simple(); + const [result] = await transaction` + select programmable_private.attest_candidate_database_promotion( + ${promotion.envioProviderDeploymentId}::uuid, + ${bytes(promotion.baselineCommitment, "baseline commitment")}::bytea, + ${bytes(promotion.candidateInventoryParityCommitment, "candidate inventory parity commitment")}::bytea, + ${bytes(promotion.envioPromotionAttestationCommitment, "Envio promotion attestation")}::bytea, + ${bytes(promotion.inputCommitment, "database promotion input commitment")}::bytea, + ${promotion.productCommit}::text, + ${promotion.stagedDeploymentId}::text, + ${promotion.promotedAt}::timestamptz + ) as changed + `; + if (typeof result?.changed !== "boolean") { + throw new Error("database promotion attestation returned an invalid result"); + } + return Object.freeze({ changed: result.changed }); + }); +} + +export function assertPromotionLeaseRowsDrained(rows) { + if (!Array.isArray(rows) || rows.length !== 2) { + throw new Error("promotion lease rows are incomplete"); + } + const seen = new Set(); + for (const row of rows) { + if ( + !row || + !["source", "market"].includes(row.projector) || + seen.has(row.projector) + ) { + throw new Error("promotion lease rows are invalid"); + } + seen.add(row.projector); + const generation = integer(row.lease_generation, "lease generation"); + const observedAt = new Date(timestamp(row.observed_at, "lease observation timestamp")); + const released = row.released_at !== null && row.released_at !== undefined; + const expired = + row.expires_at !== null && + row.expires_at !== undefined && + new Date(timestamp(row.expires_at, "lease expiry timestamp")) <= observedAt; + if (generation !== "0" && !released && !expired) { + throw new Error(`active ${row.projector} projector lease blocks promotion`); + } + } + return true; +} + +export async function readCheckpointInventory(sql) { + const rows = await sql.begin(async (transaction) => { + await transaction.unsafe( + "set local role programmable_reconciler; set local statement_timeout = '15s'; set local transaction read only", + ).simple(); + return transaction.unsafe(` + select chain_id::text, release_id::text, model_id::text, + source_group::text, epoch_id::text, + pointer_generation::text, checkpoint_id::text, + block_number::text, + '0x' || pg_catalog.encode(block_hash, 'hex') as block_hash + from programmable_private.checkpoint_summary_v1 + order by release_id + `); + }); + return Object.freeze([...rows]); +} + +export async function inspectProjectorLeaseDrain(sql) { + const rows = await sql.begin(async (transaction) => { + await transaction.unsafe( + "set local transaction read only; set local statement_timeout = '10s'", + ).simple(); + return transaction.unsafe(` + with observed as ( + select pg_catalog.clock_timestamp() as observed_at + ) + select 'source'::text as projector, + lease.lease_generation::text as lease_generation, + lease.expires_at, + lease.released_at, + observed.observed_at, + ( + lease.lease_generation = 0 + or lease.released_at is not null + or lease.expires_at <= observed.observed_at + ) as drained + from programmable_private.projector_runtime_lease_current as lease + cross join observed + union all + select 'market'::text as projector, + lease.lease_generation::text as lease_generation, + lease.expires_at, + lease.released_at, + observed.observed_at, + ( + lease.lease_generation = 0 + or lease.released_at is not null + or lease.expires_at <= observed.observed_at + ) as drained + from programmable_private.market_projector_runtime_lease_current as lease + cross join observed + order by projector + `); + }); + if ( + rows.length !== 2 || + rows[0]?.projector !== "market" || + rows[1]?.projector !== "source" || + rows.some((row) => typeof row.drained !== "boolean") + ) { + throw new Error("projector lease state is unavailable"); + } + const observedAt = timestamp(rows[0].observed_at, "lease observation timestamp"); + if (timestamp(rows[1].observed_at, "lease observation timestamp") !== observedAt) { + throw new Error("projector leases were not observed atomically"); + } + const leases = rows.map((row) => Object.freeze({ + projector: row.projector, + leaseGeneration: integer(row.lease_generation, "lease generation"), + expiresAt: + row.expires_at === null + ? null + : timestamp(row.expires_at, "lease expiry timestamp"), + releasedAt: + row.released_at === null + ? null + : timestamp(row.released_at, "lease release timestamp"), + drained: row.drained, + })); + return Object.freeze({ + observedAt, + drained: leases.every((lease) => lease.drained), + leases: Object.freeze(leases), + }); +} + +export async function waitForProjectorLeaseDrain(input) { + const maximumWaitMs = input.maximumWaitMs ?? 120_000; + const intervalMs = input.intervalMs ?? 1_000; + const stabilityWindowMs = input.stabilityWindowMs ?? 0; + if ( + !Number.isSafeInteger(maximumWaitMs) || + maximumWaitMs < 1_000 || + maximumWaitMs > 180_000 || + !Number.isSafeInteger(intervalMs) || + intervalMs < 100 || + intervalMs > 5_000 || + !Number.isSafeInteger(stabilityWindowMs) || + stabilityWindowMs < 0 || + stabilityWindowMs > maximumWaitMs - intervalMs + ) { + throw new Error("projector lease drain bound is invalid"); + } + const now = input.now ?? (() => Date.now()); + const sleep = input.sleep ?? ((milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds))); + const startedAt = now(); + let attempts = 0; + let stableSince = null; + let stableFingerprint = null; + while (now() - startedAt <= maximumWaitMs) { + attempts += 1; + const state = await input.inspect(); + if (state?.drained === true) { + const fingerprint = JSON.stringify( + Array.isArray(state.leases) + ? state.leases.map((lease) => ({ + projector: lease.projector, + leaseGeneration: lease.leaseGeneration, + expiresAt: lease.expiresAt, + releasedAt: lease.releasedAt, + })) + : [], + ); + if (stableFingerprint !== fingerprint) { + stableFingerprint = fingerprint; + stableSince = now(); + } + const stableForMs = now() - stableSince; + if (stableForMs >= stabilityWindowMs) { + return Object.freeze({ + ...state, + attempts, + waitedMs: now() - startedAt, + stabilityWindowMs, + stableForMs, + }); + } + } else { + stableSince = null; + stableFingerprint = null; + } + await sleep(intervalMs); + } + throw new Error("projector leases did not drain before the cutover deadline"); +} + +export async function withDirectOperatorDatabase(input, operation) { + const connection = await openHostedDatabase({ + databaseUrl: input.databaseUrl, + expectedProjectRef: input.expectedProjectRef, + sslCaPem: input.sslCaPem, + }); + try { + return await operation(connection.sql); + } finally { + await closeHostedDatabase(connection.sql); + } +} + +export async function readJsonEvidence(path) { + const parsed = JSON.parse(await readFile(path, "utf8")); + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("evidence file is invalid"); + } + return parsed; +} diff --git a/scripts/data-pipeline/cutover-database.test.mjs b/scripts/data-pipeline/cutover-database.test.mjs new file mode 100644 index 00000000..6f6e797d --- /dev/null +++ b/scripts/data-pipeline/cutover-database.test.mjs @@ -0,0 +1,228 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + assertPromotionLeaseRowsDrained, + attestCandidateDatabasePromotion, + buildDatabasePromotionInput, + waitForProjectorLeaseDrain, +} from "./cutover-database.mjs"; + +const HASH = `0x${"1".repeat(64)}`; + +test("database promotion binds Envio, inventory, product and staged deployment", () => { + const input = buildDatabasePromotionInput({ + candidateEndpointIdentity: "envio:d7a39a2", + envioProviderDeploymentId: "123e4567-e89b-42d3-a456-426614174000", + baselineCommitment: HASH, + candidateInventoryParityCommitment: `0x${"2".repeat(64)}`, + envioPromotionAttestationCommitment: `0x${"3".repeat(64)}`, + productCommit: "a".repeat(40), + stagedDeploymentId: "dpl_12345678901234567890", + promotedAt: "2026-08-01T08:00:00.000Z", + }); + assert.match(input.inputCommitment, /^0x[0-9a-f]{64}$/u); + const changed = buildDatabasePromotionInput({ + ...input, + productCommit: "b".repeat(40), + }); + assert.notEqual(changed.inputCommitment, input.inputCommitment); +}); + +test("database promotion rejects zero evidence and mutable deployment aliases", () => { + assert.throws( + () => buildDatabasePromotionInput({ + candidateEndpointIdentity: "envio:d7a39a2", + envioProviderDeploymentId: "123e4567-e89b-42d3-a456-426614174000", + baselineCommitment: `0x${"0".repeat(64)}`, + candidateInventoryParityCommitment: HASH, + envioPromotionAttestationCommitment: HASH, + productCommit: "a".repeat(40), + stagedDeploymentId: "production", + promotedAt: "2026-08-01T08:00:00.000Z", + }), + /identity|baseline/u, + ); +}); + +test("lease drain waits for both projectors and returns the exact drained observation", async () => { + let clock = 0; + let calls = 0; + const result = await waitForProjectorLeaseDrain({ + maximumWaitMs: 2_000, + intervalMs: 100, + now: () => clock, + sleep: async (milliseconds) => { + clock += milliseconds; + }, + inspect: async () => { + calls += 1; + return { + observedAt: "2026-08-01T08:00:00.000Z", + drained: calls === 2, + leases: [], + }; + }, + }); + assert.equal(result.drained, true); + assert.equal(result.attempts, 2); + assert.equal(result.waitedMs, 100); +}); + +test("lease drain fails closed when an active lease survives the deadline", async () => { + let clock = 0; + await assert.rejects( + waitForProjectorLeaseDrain({ + maximumWaitMs: 1_000, + intervalMs: 500, + now: () => clock, + sleep: async (milliseconds) => { + clock += milliseconds; + }, + inspect: async () => ({ drained: false }), + }), + /did not drain/u, + ); +}); + +test("lease drain proves scheduler isolation across a full stability window", async () => { + let clock = 0; + let generation = "0"; + const result = await waitForProjectorLeaseDrain({ + maximumWaitMs: 2_000, + intervalMs: 100, + stabilityWindowMs: 500, + now: () => clock, + sleep: async (milliseconds) => { + clock += milliseconds; + if (clock === 300) generation = "1"; + }, + inspect: async () => ({ + observedAt: "2026-08-01T08:00:00.000Z", + drained: true, + leases: [ + { + projector: "source", + leaseGeneration: generation, + expiresAt: null, + releasedAt: generation === "0" ? null : "2026-08-01T08:00:00.000Z", + }, + ], + }), + }); + assert.equal(result.stabilityWindowMs, 500); + assert.equal(result.stableForMs, 500); + assert.equal(result.waitedMs, 800); +}); + +test("database promotion rechecks current leases under its transaction before mutation", async () => { + const observedAt = "2026-08-01T08:00:00.000Z"; + assert.throws( + () => assertPromotionLeaseRowsDrained([ + { + projector: "source", + lease_generation: "3", + expires_at: "2026-08-01T08:01:00.000Z", + released_at: null, + observed_at: observedAt, + }, + { + projector: "market", + lease_generation: "0", + expires_at: null, + released_at: null, + observed_at: observedAt, + }, + ]), + /active source projector lease/u, + ); + + let promotionMutationCalls = 0; + const transaction = async () => { + promotionMutationCalls += 1; + return [{ changed: true }]; + }; + transaction.unsafe = (query) => { + if (query.startsWith("set local")) { + return { simple: async () => undefined }; + } + if (query.includes("pg_try_advisory_xact_lock")) { + return Promise.resolve([{ acquired: true }]); + } + if (query.includes("projector_runtime_lease_current")) { + return Promise.resolve([{ + lease_generation: "4", + expires_at: "2026-08-01T08:01:00.000Z", + released_at: null, + observed_at: observedAt, + }]); + } + return Promise.resolve([{ + lease_generation: "0", + expires_at: null, + released_at: null, + observed_at: observedAt, + }]); + }; + const sql = { begin: async (operation) => operation(transaction) }; + await assert.rejects( + attestCandidateDatabasePromotion({ + sql, + promotion: buildDatabasePromotionInput({ + candidateEndpointIdentity: "envio:d7a39a2", + envioProviderDeploymentId: "123e4567-e89b-42d3-a456-426614174000", + baselineCommitment: HASH, + candidateInventoryParityCommitment: `0x${"2".repeat(64)}`, + envioPromotionAttestationCommitment: `0x${"3".repeat(64)}`, + productCommit: "a".repeat(40), + stagedDeploymentId: "dpl_12345678901234567890", + promotedAt: observedAt, + }), + }), + /active source projector lease/u, + ); + assert.equal(promotionMutationCalls, 0); +}); + +test("database promotion atomically persists the reviewed commit and deployment", async () => { + const observedAt = "2026-08-01T08:00:00.000Z"; + const productCommit = "a".repeat(40); + const stagedDeploymentId = "dpl_12345678901234567890"; + let mutationValues; + const transaction = async (_strings, ...values) => { + mutationValues = values; + return [{ changed: true }]; + }; + transaction.unsafe = (query) => { + if (query.startsWith("set local")) { + return { simple: async () => undefined }; + } + if (query.includes("pg_try_advisory_xact_lock")) { + return Promise.resolve([{ acquired: true }]); + } + return Promise.resolve([{ + lease_generation: "0", + expires_at: null, + released_at: null, + observed_at: observedAt, + }]); + }; + const sql = { begin: async (operation) => operation(transaction) }; + const result = await attestCandidateDatabasePromotion({ + sql, + promotion: buildDatabasePromotionInput({ + candidateEndpointIdentity: "envio:d7a39a2", + envioProviderDeploymentId: "123e4567-e89b-42d3-a456-426614174000", + baselineCommitment: HASH, + candidateInventoryParityCommitment: `0x${"2".repeat(64)}`, + envioPromotionAttestationCommitment: `0x${"3".repeat(64)}`, + productCommit, + stagedDeploymentId, + promotedAt: observedAt, + }), + }); + assert.equal(result.changed, true); + assert.equal(mutationValues[5], productCommit); + assert.equal(mutationValues[6], stagedDeploymentId); + assert.equal(mutationValues.length, 8); +}); diff --git a/scripts/data-pipeline/cutover-envio.mjs b/scripts/data-pipeline/cutover-envio.mjs new file mode 100644 index 00000000..b55e115e --- /dev/null +++ b/scripts/data-pipeline/cutover-envio.mjs @@ -0,0 +1,1239 @@ +import { lstat, readFile, realpath } from "node:fs/promises"; +import path from "node:path"; + +import { canonicalJson, sha256 } from "./hosted-db-operator-core.mjs"; + +const CANDIDATE_MANIFEST_PATH = + "config/data-pipeline-envio-candidate.v1.json"; +const RELEASE_BINDING_PATH = "config/data-pipeline-release.v1.json"; +const DEPLOYMENT_EVIDENCE_PATH = + "docs/data-pipeline/envio-candidate-7f24e63-deployment-7ffd15c.json"; + +const COMMIT = /^(?!0{40}$)[0-9a-f]{40}$/u; +const SHA256 = /^0x[0-9a-f]{64}$/u; +const ENDPOINT_ID = /^[a-z0-9]{7,64}$/u; +const DEPLOYMENT = /^[a-z0-9][a-z0-9._-]{0,127}$/u; +const EVIDENCE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,255}$/u; +const ENVIO_HOST = "indexer.hyperindex.xyz"; +const PROMOTION_KIND = "programmable-envio-promotion-attestation"; +const ROLLBACK_PLAN_KIND = "programmable-envio-rollback-plan"; +const ROLLBACK_EVIDENCE_KIND = "programmable-envio-rollback-evidence"; + +const IDENTITY_KEYS = Object.freeze([ + "deployment", + "sourceCommit", + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "eventCount", +]); + +const ROLLBACK_STEPS = Object.freeze([ + "freeze-publication-and-stop-projectors", + "promote-exact-rollback-envio", + "restore-or-discard-pre-attestation-database", + "verify-exact-rollback-runtime-and-inventory", + "verify-vercel-production-unchanged", +]); + +function isPlainObject(value) { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function exactObject(value, label, keys) { + if (!isPlainObject(value)) throw new Error(`${label} must be an object`); + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + if ( + actual.length !== expected.length || + actual.some((key, index) => key !== expected[index]) + ) { + throw new Error(`${label} must contain exactly: ${expected.join(", ")}`); + } + return value; +} + +function exactString(value, label, pattern, maximum = 512) { + if ( + typeof value !== "string" || + value.length === 0 || + value.length > maximum || + !pattern.test(value) + ) { + throw new Error(`${label} is invalid`); + } + return value; +} + +function exactCommit(value, label) { + return exactString(value, label, COMMIT, 40); +} + +function exactSha(value, label) { + return exactString(value, label, SHA256, 66); +} + +function exactTimestamp(value, label) { + if (typeof value !== "string") throw new Error(`${label} is invalid`); + const parsed = new Date(value); + if (!Number.isFinite(parsed.valueOf()) || parsed.toISOString() !== value) { + throw new Error(`${label} is invalid`); + } + return value; +} + +function exactSafeInteger(value, label, minimum = 0) { + if (!Number.isSafeInteger(value) || value < minimum) { + throw new Error(`${label} must be a safe integer`); + } + return value; +} + +function exactEndpoint(value, expectedId, label) { + if (typeof value !== "string" || value.length > 256) { + throw new Error(`${label} is invalid`); + } + let parsed; + try { + parsed = new URL(value); + } catch { + throw new Error(`${label} is invalid`); + } + const match = /^\/([a-z0-9]{7,64})\/v1\/graphql$/u.exec(parsed.pathname); + if ( + parsed.protocol !== "https:" || + parsed.hostname !== ENVIO_HOST || + parsed.port !== "" || + parsed.username !== "" || + parsed.password !== "" || + parsed.search !== "" || + parsed.hash !== "" || + parsed.toString() !== value || + match === null || + (expectedId !== undefined && match[1] !== expectedId) + ) { + throw new Error(`${label} is invalid`); + } + return { endpoint: value, endpointId: match[1] }; +} + +function parseRuntimeIdentity(value, label) { + const object = exactObject(value, label, IDENTITY_KEYS); + return { + deployment: exactString( + object.deployment, + `${label}.deployment`, + DEPLOYMENT, + 128, + ), + sourceCommit: exactCommit(object.sourceCommit, `${label}.sourceCommit`), + configSha256: exactSha(object.configSha256, `${label}.configSha256`), + schemaSha256: exactSha(object.schemaSha256, `${label}.schemaSha256`), + handlerSha256: exactSha(object.handlerSha256, `${label}.handlerSha256`), + sourceRegistrySha256: exactSha( + object.sourceRegistrySha256, + `${label}.sourceRegistrySha256`, + ), + eventSetSha256: exactSha( + object.eventSetSha256, + `${label}.eventSetSha256`, + ), + eventCount: exactSafeInteger(object.eventCount, `${label}.eventCount`, 1), + }; +} + +function parsePerRelease(value, label) { + if (!isPlainObject(value) || Object.keys(value).length === 0) { + throw new Error(`${label} must be a non-empty object`); + } + const result = {}; + for (const key of Object.keys(value).sort()) { + exactString(key, `${label} key`, /^[a-z0-9][a-z0-9-]{0,63}$/u, 64); + result[key] = exactSafeInteger(value[key], `${label}.${key}`); + } + return result; +} + +function parseInventory(value, label) { + const object = exactObject(value, label, ["count", "perRelease", "sha256"]); + const inventory = { + count: exactSafeInteger(object.count, `${label}.count`, 1), + perRelease: parsePerRelease(object.perRelease, `${label}.perRelease`), + sha256: exactSha(object.sha256, `${label}.sha256`), + }; + const total = Object.values(inventory.perRelease).reduce( + (sum, count) => sum + count, + 0, + ); + if (total !== inventory.count) { + throw new Error(`${label}.perRelease does not sum to count`); + } + return inventory; +} + +function same(left, right, label) { + if (canonicalJson(left) !== canonicalJson(right)) { + throw new Error(`${label} mismatch`); + } +} + +function deepFreeze(value) { + if (value && typeof value === "object" && !Object.isFrozen(value)) { + Object.freeze(value); + for (const child of Object.values(value)) deepFreeze(child); + } + return value; +} + +async function readRepositoryJson(workspace, relativePath) { + const workspacePath = await realpath(workspace); + const absolutePath = path.resolve(workspacePath, relativePath); + if (!absolutePath.startsWith(`${workspacePath}${path.sep}`)) { + throw new Error(`${relativePath} escapes the repository`); + } + const unresolvedMetadata = await lstat(absolutePath); + if (!unresolvedMetadata.isFile() || unresolvedMetadata.isSymbolicLink()) { + throw new Error(`${relativePath} must be a regular file`); + } + const resolved = await realpath(absolutePath); + if (!resolved.startsWith(`${workspacePath}${path.sep}`)) { + throw new Error(`${relativePath} escapes the repository`); + } + const bytes = await readFile(resolved); + let value; + try { + value = JSON.parse(bytes.toString("utf8")); + } catch { + throw new Error(`${relativePath} is not valid JSON`); + } + if (!isPlainObject(value)) throw new Error(`${relativePath} must be an object`); + return { path: relativePath, bytes, fileSha256: sha256(bytes), value }; +} + +function candidateIdentityFromManifest(manifest) { + return parseRuntimeIdentity( + { + deployment: manifest.deploymentLabel, + sourceCommit: manifest.sourceCommit, + configSha256: manifest.configSha256, + schemaSha256: manifest.schemaSha256, + handlerSha256: manifest.handlerSha256, + sourceRegistrySha256: manifest.sourceRegistrySha256, + eventSetSha256: manifest.eventSetSha256, + eventCount: manifest.eventCount, + }, + "candidate manifest identity", + ); +} + +function rollbackIdentityFromEvidence(rollback) { + return parseRuntimeIdentity( + { + deployment: rollback.deployment, + sourceCommit: rollback.sourceCommit, + configSha256: rollback.configSha256, + schemaSha256: rollback.schemaSha256, + handlerSha256: rollback.handlerSha256, + sourceRegistrySha256: rollback.sourceRegistrySha256, + eventSetSha256: rollback.eventSetSha256, + eventCount: rollback.eventCount, + }, + "rollback identity", + ); +} + +/** + * Loads the one reviewed Envio candidate and rollback identity from committed + * release evidence. Provider input is deliberately not accepted here. + */ +export async function loadEnvioCutoverIdentity({ workspace }) { + if (typeof workspace !== "string" || workspace.length === 0) { + throw new Error("workspace is required"); + } + const [manifestFile, releaseFile, deploymentFile] = await Promise.all([ + readRepositoryJson(workspace, CANDIDATE_MANIFEST_PATH), + readRepositoryJson(workspace, RELEASE_BINDING_PATH), + readRepositoryJson(workspace, DEPLOYMENT_EVIDENCE_PATH), + ]); + const manifest = manifestFile.value; + const release = releaseFile.value; + const deployment = deploymentFile.value; + + if ( + manifest.schemaVersion !== 1 || + manifest.status !== "deployed-synced-audited-not-promoted" || + manifest.policy?.databaseMode !== "candidate-only" || + manifest.policy?.legacyProductionDeploymentRegistered !== false || + manifest.policy?.publicationAllowedBeforePromotion !== false || + manifest.policy?.promotion !== "atomic-attestation-required" + ) { + throw new Error("candidate manifest is not an isolated audited candidate"); + } + if ( + deployment.schemaVersion !== 1 || + deployment.kind !== "envio-candidate-deployment-evidence" || + deployment.status !== "deployed-synced-audited-not-promoted" || + deployment.candidate?.promoted !== false || + deployment.promotion?.state !== "not-promoted" || + deployment.promotion?.productionBindingMayChange !== false + ) { + throw new Error("deployment evidence is not pre-promotion evidence"); + } + + const controlPlane = exactObject(deployment.deploymentMirror, "deployment mirror", [ + "repository", + "branch", + "branchProtected", + "candidateCommit", + ]); + const repository = exactString( + controlPlane.repository, + "deployment mirror repository", + /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u, + 256, + ); + const repositoryMatch = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/u.exec( + repository, + ); + const owner = repositoryMatch?.[1]; + const project = repositoryMatch?.[2]; + if (!owner || !project || controlPlane.branch !== "production") { + throw new Error("deployment mirror control-plane identity is invalid"); + } + + const candidateEndpoint = exactEndpoint( + deployment.candidate.endpoint, + exactString( + deployment.candidate.endpointId, + "candidate endpoint id", + ENDPOINT_ID, + 64, + ), + "candidate endpoint", + ); + const manifestEndpoint = exactEndpoint( + manifest.graphqlEndpoint, + candidateEndpoint.endpointId, + "candidate manifest endpoint", + ); + const candidateRuntimeIdentity = parseRuntimeIdentity( + { + deployment: deployment.candidate.deploymentLabel, + ...deployment.candidate.identity, + }, + "candidate runtime identity", + ); + same( + candidateRuntimeIdentity, + candidateIdentityFromManifest(manifest), + "candidate manifest/runtime identity", + ); + if ( + manifestEndpoint.endpoint !== candidateEndpoint.endpoint || + manifest.redactedIdentity !== `envio:${candidateRuntimeIdentity.deployment}` || + !SHA256.test(manifest.deploymentCommitment ?? "") || + !SHA256.test(manifest.schemaCommitment ?? "") + ) { + throw new Error("candidate manifest and deployment evidence diverge"); + } + + const candidateMirrorCommit = exactCommit( + controlPlane.candidateCommit, + "candidate mirror commit", + ); + const rollbackMirrorCommit = exactCommit( + deployment.rollback?.deploymentMirrorCommit, + "rollback mirror commit", + ); + if ( + deployment.activeProduction?.mirrorCommit !== rollbackMirrorCommit || + deployment.activeProduction?.controlPlaneStatus !== "prod" + ) { + throw new Error("rollback target is not the recorded active production"); + } + + const auditReference = deployment.artifacts?.candidateAudit; + const baselineReference = deployment.artifacts?.baseline; + const identityReference = deployment.artifacts?.identity; + for (const [reference, label] of [ + [auditReference, "candidate audit reference"], + [baselineReference, "rollback baseline reference"], + [identityReference, "candidate identity reference"], + ]) { + if (!isPlainObject(reference)) throw new Error(`${label} is missing`); + exactString(reference.path, `${label}.path`, /^[a-zA-Z0-9._/-]+$/u, 512); + exactSha(reference.fileSha256, `${label}.fileSha256`); + } + const [auditFile, baselineFile, identityFile] = await Promise.all([ + readRepositoryJson(workspace, auditReference.path), + readRepositoryJson(workspace, baselineReference.path), + readRepositoryJson(workspace, identityReference.path), + ]); + if ( + auditFile.fileSha256 !== auditReference.fileSha256 || + baselineFile.fileSha256 !== baselineReference.fileSha256 || + identityFile.fileSha256 !== identityReference.fileSha256 + ) { + throw new Error("an Envio evidence artifact hash does not match its manifest"); + } + + const audit = auditFile.value; + const baseline = baselineFile.value; + const identityArtifact = parseRuntimeIdentity( + identityFile.value, + "candidate identity artifact", + ); + same(identityArtifact, candidateRuntimeIdentity, "candidate identity artifact"); + if ( + audit.kind !== "envio-release-inventory" || + audit.digest !== auditReference.internalDigest || + audit.endpoint !== candidateEndpoint.endpoint || + audit.deployment?.mirrorCommit !== candidateMirrorCommit || + audit.deployment?.endpointId !== candidateEndpoint.endpointId + ) { + throw new Error("candidate audit is not bound to the reviewed deployment"); + } + same( + parseRuntimeIdentity(audit.identity, "candidate audit identity"), + candidateRuntimeIdentity, + "candidate audit runtime identity", + ); + const candidateInventory = parseInventory( + audit.inventory, + "candidate audited inventory", + ); + if ( + candidateInventory.sha256 !== auditReference.inventorySha256 || + candidateInventory.count !== deployment.inventory?.count + ) { + throw new Error("candidate audited inventory commitment mismatch"); + } + same( + candidateInventory.perRelease, + deployment.inventory.perRelease, + "candidate inventory release counts", + ); + + const rollbackEndpoint = exactEndpoint( + deployment.rollback.graphqlEndpoint, + undefined, + "rollback endpoint", + ); + if ( + deployment.activeProduction.endpoint !== rollbackEndpoint.endpoint || + baseline.endpoint !== rollbackEndpoint.endpoint || + baseline.deployment?.endpointId !== rollbackEndpoint.endpointId || + baseline.digest !== baselineReference.internalDigest + ) { + throw new Error("rollback baseline is not bound to active production"); + } + const rollbackRuntimeIdentity = rollbackIdentityFromEvidence( + deployment.rollback, + ); + const releaseRuntimeIdentity = parseRuntimeIdentity( + { + deployment: release.envio?.deploymentLabel, + sourceCommit: release.envio?.sourceCommit, + configSha256: release.envio?.configSha256, + schemaSha256: release.envio?.schemaSha256, + handlerSha256: release.envio?.handlerSha256, + sourceRegistrySha256: release.envio?.sourceRegistrySha256, + eventSetSha256: release.envio?.eventSetSha256, + eventCount: release.envio?.eventCount, + }, + "release candidate identity", + ); + same( + releaseRuntimeIdentity, + candidateRuntimeIdentity, + "release/candidate runtime identity", + ); + if (release.envio?.graphqlEndpoint !== candidateEndpoint.endpoint) { + throw new Error("release binding is not the reviewed candidate endpoint"); + } + const rollbackInventory = parseInventory( + baseline.inventory, + "rollback audited inventory", + ); + if (rollbackInventory.sha256 !== baselineReference.inventorySha256) { + throw new Error("rollback inventory commitment mismatch"); + } + + return deepFreeze({ + schemaVersion: 1, + evidence: { + candidateManifest: { + path: manifestFile.path, + fileSha256: manifestFile.fileSha256, + }, + deployment: { + path: deploymentFile.path, + fileSha256: deploymentFile.fileSha256, + }, + releaseBinding: { + path: releaseFile.path, + fileSha256: releaseFile.fileSha256, + }, + }, + controlPlane: { owner, project, repository, branch: "production" }, + candidate: { + mirrorCommit: candidateMirrorCommit, + deploymentLabel: candidateRuntimeIdentity.deployment, + endpoint: candidateEndpoint.endpoint, + endpointId: candidateEndpoint.endpointId, + runtimeIdentity: candidateRuntimeIdentity, + inventory: { + ...candidateInventory, + artifactPath: auditFile.path, + artifactFileSha256: auditFile.fileSha256, + artifactDigest: exactSha( + audit.digest, + "candidate audit artifact digest", + ), + }, + }, + rollback: { + mirrorCommit: rollbackMirrorCommit, + deploymentLabel: rollbackRuntimeIdentity.deployment, + endpoint: rollbackEndpoint.endpoint, + endpointId: rollbackEndpoint.endpointId, + runtimeIdentity: rollbackRuntimeIdentity, + inventory: { + ...rollbackInventory, + artifactPath: baselineFile.path, + artifactFileSha256: baselineFile.fileSha256, + artifactDigest: exactSha( + baseline.digest, + "rollback baseline artifact digest", + ), + }, + }, + }); +} + +function promotionPayload(value) { + return { + kind: value.kind, + schemaVersion: value.schemaVersion, + observedAt: value.observedAt, + productGitCommit: value.productGitCommit, + releaseGateEvidenceSha256: value.releaseGateEvidenceSha256, + controlPlane: value.controlPlane, + runtime: value.runtime, + auditedInventory: value.auditedInventory, + candidateTarget: value.candidateTarget, + rollbackTarget: value.rollbackTarget, + sourceEvidence: value.sourceEvidence, + }; +} + +function targetFromIdentity(target) { + return { + mirrorCommit: target.mirrorCommit, + deploymentLabel: target.deploymentLabel, + endpoint: target.endpoint, + endpointId: target.endpointId, + runtimeIdentity: target.runtimeIdentity, + inventorySha256: target.inventory.sha256, + }; +} + +function parseTarget(value, label) { + const object = exactObject(value, label, [ + "mirrorCommit", + "deploymentLabel", + "endpoint", + "endpointId", + "runtimeIdentity", + "inventorySha256", + ]); + const endpointId = exactString( + object.endpointId, + `${label}.endpointId`, + ENDPOINT_ID, + 64, + ); + const runtimeIdentity = parseRuntimeIdentity( + object.runtimeIdentity, + `${label}.runtimeIdentity`, + ); + const target = { + mirrorCommit: exactCommit(object.mirrorCommit, `${label}.mirrorCommit`), + deploymentLabel: exactString( + object.deploymentLabel, + `${label}.deploymentLabel`, + DEPLOYMENT, + 128, + ), + endpoint: exactEndpoint(object.endpoint, endpointId, `${label}.endpoint`).endpoint, + endpointId, + runtimeIdentity, + inventorySha256: exactSha( + object.inventorySha256, + `${label}.inventorySha256`, + ), + }; + if (target.deploymentLabel !== runtimeIdentity.deployment) { + throw new Error(`${label} deployment identity mismatch`); + } + return target; +} + +function parseEvidenceReference(value, label) { + const object = exactObject(value, label, ["path", "fileSha256"]); + return { + path: exactString( + object.path, + `${label}.path`, + /^[a-zA-Z0-9._/-]+$/u, + 512, + ), + fileSha256: exactSha(object.fileSha256, `${label}.fileSha256`), + }; +} + +function parseSourceEvidence(value) { + const object = exactObject(value, "source evidence", [ + "candidateManifest", + "deployment", + "releaseBinding", + ]); + return { + candidateManifest: parseEvidenceReference( + object.candidateManifest, + "candidate manifest evidence", + ), + deployment: parseEvidenceReference( + object.deployment, + "deployment evidence", + ), + releaseBinding: parseEvidenceReference( + object.releaseBinding, + "release binding evidence", + ), + }; +} + +function parseAttestedInventory(value, label) { + const object = exactObject(value, label, [ + "artifactPath", + "artifactFileSha256", + "artifactDigest", + "count", + "perRelease", + "sha256", + ]); + return { + artifactPath: exactString( + object.artifactPath, + `${label}.artifactPath`, + /^[a-zA-Z0-9._/-]+$/u, + 512, + ), + artifactFileSha256: exactSha( + object.artifactFileSha256, + `${label}.artifactFileSha256`, + ), + artifactDigest: exactSha(object.artifactDigest, `${label}.artifactDigest`), + ...parseInventory( + { count: object.count, perRelease: object.perRelease, sha256: object.sha256 }, + label, + ), + }; +} + +function parseControlPlaneObservation(value, expected) { + const object = exactObject(value, "control-plane observation", [ + "owner", + "project", + "status", + "mirrorCommit", + "deploymentLabel", + ]); + const parsed = { + owner: exactString(object.owner, "control-plane owner", /^[A-Za-z0-9_.-]+$/u, 64), + project: exactString( + object.project, + "control-plane project", + /^[A-Za-z0-9_.-]+$/u, + 128, + ), + status: exactString(object.status, "control-plane status", /^prod$/u, 4), + mirrorCommit: exactCommit(object.mirrorCommit, "control-plane mirror commit"), + deploymentLabel: exactString( + object.deploymentLabel, + "control-plane deployment", + DEPLOYMENT, + 128, + ), + }; + same( + parsed, + { + owner: expected.controlPlane.owner, + project: expected.controlPlane.project, + status: "prod", + mirrorCommit: expected.candidate.mirrorCommit, + deploymentLabel: expected.candidate.deploymentLabel, + }, + "candidate control-plane observation", + ); + return parsed; +} + +function parseRuntimeObservation(value, expected, label = "runtime observation") { + const object = exactObject(value, label, [ + "endpoint", + "endpointId", + "deploymentLabel", + "identity", + ]); + const endpointId = exactString( + object.endpointId, + `${label}.endpointId`, + ENDPOINT_ID, + 64, + ); + const endpoint = exactEndpoint(object.endpoint, endpointId, `${label}.endpoint`); + const parsed = { + endpoint: endpoint.endpoint, + endpointId, + deploymentLabel: exactString( + object.deploymentLabel, + `${label}.deploymentLabel`, + DEPLOYMENT, + 128, + ), + identity: parseRuntimeIdentity(object.identity, `${label}.identity`), + }; + same( + parsed, + { + endpoint: expected.endpoint, + endpointId: expected.endpointId, + deploymentLabel: expected.deploymentLabel, + identity: expected.runtimeIdentity, + }, + label, + ); + return parsed; +} + +function parseInventoryObservation(value, expected, label) { + const parsed = parseAttestedInventory(value, label); + same(parsed, expected, label); + return parsed; +} + +function sourceEvidence(identity) { + return { + candidateManifest: identity.evidence.candidateManifest, + deployment: identity.evidence.deployment, + releaseBinding: identity.evidence.releaseBinding, + }; +} + +/** Creates a canonical promotion receipt without invoking Envio. */ +export function createEnvioPromotionAttestation(input) { + const object = exactObject(input, "promotion input", [ + "identity", + "observedAt", + "productGitCommit", + "releaseGateEvidenceSha256", + "controlPlane", + "runtime", + "auditedInventory", + "existingAttestation", + ]); + const identity = object.identity; + if (!isPlainObject(identity) || identity.schemaVersion !== 1) { + throw new Error("loaded Envio cutover identity is required"); + } + const attestation = { + kind: PROMOTION_KIND, + schemaVersion: 1, + observedAt: exactTimestamp(object.observedAt, "promotion observedAt"), + productGitCommit: exactCommit( + object.productGitCommit, + "promotion product Git commit", + ), + releaseGateEvidenceSha256: exactSha( + object.releaseGateEvidenceSha256, + "promotion release-gate evidence", + ), + controlPlane: parseControlPlaneObservation(object.controlPlane, identity), + runtime: parseRuntimeObservation( + object.runtime, + identity.candidate, + "candidate runtime observation", + ), + auditedInventory: parseInventoryObservation( + object.auditedInventory, + identity.candidate.inventory, + "candidate audited inventory", + ), + candidateTarget: targetFromIdentity(identity.candidate), + rollbackTarget: targetFromIdentity(identity.rollback), + sourceEvidence: sourceEvidence(identity), + }; + const result = deepFreeze({ + ...attestation, + attestationSha256: sha256( + `programmable:envio-promotion-attestation:v1\0${canonicalJson(attestation)}`, + ), + }); + validateEnvioPromotionAttestation(result); + if (object.existingAttestation !== null) { + const existing = validateEnvioPromotionAttestation( + object.existingAttestation, + ); + if (canonicalJson(existing) !== canonicalJson(result)) { + throw new Error("conflicting Envio promotion attestation already exists"); + } + return existing; + } + return result; +} + +export function validateEnvioPromotionAttestation(value) { + const object = exactObject(value, "promotion attestation", [ + "kind", + "schemaVersion", + "observedAt", + "productGitCommit", + "releaseGateEvidenceSha256", + "controlPlane", + "runtime", + "auditedInventory", + "candidateTarget", + "rollbackTarget", + "sourceEvidence", + "attestationSha256", + ]); + if (object.kind !== PROMOTION_KIND || object.schemaVersion !== 1) { + throw new Error("unsupported Envio promotion attestation"); + } + exactTimestamp(object.observedAt, "promotion observedAt"); + exactCommit(object.productGitCommit, "promotion product Git commit"); + exactSha(object.releaseGateEvidenceSha256, "promotion release-gate evidence"); + exactSha(object.attestationSha256, "promotion attestation digest"); + const candidateTarget = parseTarget(object.candidateTarget, "candidate target"); + const rollbackTarget = parseTarget(object.rollbackTarget, "rollback target"); + if (canonicalJson(candidateTarget) === canonicalJson(rollbackTarget)) { + throw new Error("candidate and rollback targets must differ"); + } + const controlPlane = exactObject(object.controlPlane, "control-plane observation", [ + "owner", + "project", + "status", + "mirrorCommit", + "deploymentLabel", + ]); + exactString(controlPlane.owner, "control-plane owner", /^[A-Za-z0-9_.-]+$/u, 64); + exactString( + controlPlane.project, + "control-plane project", + /^[A-Za-z0-9_.-]+$/u, + 128, + ); + if (controlPlane.status !== "prod") { + throw new Error("control-plane status must be prod"); + } + exactCommit(controlPlane.mirrorCommit, "control-plane mirror commit"); + exactString( + controlPlane.deploymentLabel, + "control-plane deployment", + DEPLOYMENT, + 128, + ); + const runtime = parseRuntimeObservation( + object.runtime, + { + endpoint: candidateTarget.endpoint, + endpointId: candidateTarget.endpointId, + deploymentLabel: candidateTarget.deploymentLabel, + runtimeIdentity: candidateTarget.runtimeIdentity, + }, + "candidate runtime observation", + ); + const auditedInventory = parseAttestedInventory( + object.auditedInventory, + "candidate audited inventory", + ); + parseSourceEvidence(object.sourceEvidence); + if ( + controlPlane.mirrorCommit !== candidateTarget.mirrorCommit || + controlPlane.deploymentLabel !== candidateTarget.deploymentLabel || + runtime.endpoint !== candidateTarget.endpoint || + auditedInventory.sha256 !== candidateTarget.inventorySha256 + ) { + throw new Error("promotion observations do not match the candidate target"); + } + const expected = sha256( + `programmable:envio-promotion-attestation:v1\0${canonicalJson( + promotionPayload(object), + )}`, + ); + if (expected !== object.attestationSha256) { + throw new Error("promotion attestation digest mismatch"); + } + return deepFreeze(object); +} + +function parseDatabaseRecovery(value) { + const object = exactObject(value, "database recovery", [ + "mode", + "evidenceId", + "evidenceSha256", + ]); + const mode = exactString( + object.mode, + "database recovery mode", + /^(?:restore-pre-attestation-snapshot|discard-post-attestation-state)$/u, + 64, + ); + return { + mode, + evidenceId: exactString( + object.evidenceId, + "database recovery evidence id", + EVIDENCE_ID, + 256, + ), + evidenceSha256: exactSha( + object.evidenceSha256, + "database recovery evidence digest", + ), + }; +} + +function parseVercelBinding(value, label) { + const object = exactObject(value, label, ["deploymentId", "productGitCommit"]); + return { + deploymentId: exactString( + object.deploymentId, + `${label}.deploymentId`, + /^[A-Za-z0-9_-]{8,128}$/u, + 128, + ), + productGitCommit: exactCommit( + object.productGitCommit, + `${label}.productGitCommit`, + ), + }; +} + +function rollbackPlanPayload(value) { + return { + kind: value.kind, + schemaVersion: value.schemaVersion, + createdAt: value.createdAt, + productGitCommit: value.productGitCommit, + promotionAttestationSha256: value.promotionAttestationSha256, + rollbackTarget: value.rollbackTarget, + rollbackInventory: value.rollbackInventory, + databaseRecovery: value.databaseRecovery, + vercelProductionMustRemain: value.vercelProductionMustRemain, + sourceEvidence: value.sourceEvidence, + steps: value.steps, + }; +} + +/** Produces an ordered, declarative rollback plan. It never runs a command. */ +export function createRollbackPlan(input) { + const object = exactObject(input, "rollback plan input", [ + "identity", + "promotionAttestation", + "createdAt", + "databaseRecovery", + "vercelProduction", + "existingPlan", + ]); + const identity = object.identity; + if (!isPlainObject(identity) || identity.schemaVersion !== 1) { + throw new Error("loaded Envio cutover identity is required"); + } + const promotion = validateEnvioPromotionAttestation( + object.promotionAttestation, + ); + same( + promotion.rollbackTarget, + targetFromIdentity(identity.rollback), + "promotion/rollback target", + ); + const databaseRecovery = parseDatabaseRecovery(object.databaseRecovery); + const vercelProduction = parseVercelBinding( + object.vercelProduction, + "Vercel production binding", + ); + const steps = [ + { + ordinal: 1, + id: ROLLBACK_STEPS[0], + requiredState: { + publicReadFlagsEnabled: false, + sourceProjectorRunning: false, + marketProjectorRunning: false, + reconcilerRunning: false, + }, + }, + { + ordinal: 2, + id: ROLLBACK_STEPS[1], + requiredState: { + owner: identity.controlPlane.owner, + project: identity.controlPlane.project, + mirrorCommit: identity.rollback.mirrorCommit, + deploymentLabel: identity.rollback.deploymentLabel, + }, + }, + { + ordinal: 3, + id: ROLLBACK_STEPS[2], + requiredState: databaseRecovery, + }, + { + ordinal: 4, + id: ROLLBACK_STEPS[3], + requiredState: { + endpoint: identity.rollback.endpoint, + endpointId: identity.rollback.endpointId, + runtimeIdentity: identity.rollback.runtimeIdentity, + inventorySha256: identity.rollback.inventory.sha256, + }, + }, + { + ordinal: 5, + id: ROLLBACK_STEPS[4], + requiredState: { ...vercelProduction, changed: false }, + }, + ]; + const plan = { + kind: ROLLBACK_PLAN_KIND, + schemaVersion: 1, + createdAt: exactTimestamp(object.createdAt, "rollback plan createdAt"), + productGitCommit: promotion.productGitCommit, + promotionAttestationSha256: promotion.attestationSha256, + rollbackTarget: targetFromIdentity(identity.rollback), + rollbackInventory: identity.rollback.inventory, + databaseRecovery, + vercelProductionMustRemain: vercelProduction, + sourceEvidence: sourceEvidence(identity), + steps, + }; + const result = deepFreeze({ + ...plan, + planSha256: sha256( + `programmable:envio-rollback-plan:v1\0${canonicalJson(plan)}`, + ), + }); + if (object.existingPlan !== null) { + const existing = parseRollbackPlan(object.existingPlan); + if (canonicalJson(existing) !== canonicalJson(result)) { + throw new Error("conflicting Envio rollback plan already exists"); + } + return existing; + } + return result; +} + +function parseRollbackPlan(value) { + const object = exactObject(value, "rollback plan", [ + "kind", + "schemaVersion", + "createdAt", + "productGitCommit", + "promotionAttestationSha256", + "rollbackTarget", + "rollbackInventory", + "databaseRecovery", + "vercelProductionMustRemain", + "sourceEvidence", + "steps", + "planSha256", + ]); + if (object.kind !== ROLLBACK_PLAN_KIND || object.schemaVersion !== 1) { + throw new Error("unsupported Envio rollback plan"); + } + exactTimestamp(object.createdAt, "rollback plan createdAt"); + exactCommit(object.productGitCommit, "rollback product Git commit"); + exactSha(object.promotionAttestationSha256, "promotion attestation digest"); + exactSha(object.planSha256, "rollback plan digest"); + if (!Array.isArray(object.steps) || object.steps.length !== ROLLBACK_STEPS.length) { + throw new Error("rollback plan steps are incomplete"); + } + object.steps.forEach((step, index) => { + if ( + !isPlainObject(step) || + step.ordinal !== index + 1 || + step.id !== ROLLBACK_STEPS[index] || + !isPlainObject(step.requiredState) + ) { + throw new Error("rollback plan step order is invalid"); + } + }); + const expected = sha256( + `programmable:envio-rollback-plan:v1\0${canonicalJson( + rollbackPlanPayload(object), + )}`, + ); + if (expected !== object.planSha256) throw new Error("rollback plan digest mismatch"); + return deepFreeze(object); +} + +/** + * Validates completed rollback observations and returns their canonical receipt. + * Every step needs an independently committed receipt; no credentials or argv + * are represented in the evidence format. + */ +export function validateRollbackEvidence(input) { + const object = exactObject(input, "rollback evidence input", [ + "identity", + "plan", + "completedAt", + "controls", + "controlPlane", + "runtime", + "inventory", + "databaseRecovery", + "vercelProduction", + "stepReceipts", + "existingEvidence", + ]); + const identity = object.identity; + if (!isPlainObject(identity) || identity.schemaVersion !== 1) { + throw new Error("loaded Envio cutover identity is required"); + } + const plan = parseRollbackPlan(object.plan); + same(plan.rollbackTarget, targetFromIdentity(identity.rollback), "rollback plan target"); + const controls = exactObject(object.controls, "rollback controls", [ + "publicReadFlagsEnabled", + "sourceProjectorRunning", + "marketProjectorRunning", + "reconcilerRunning", + ]); + if (Object.values(controls).some((state) => state !== false)) { + throw new Error("public reads and every projector must be stopped first"); + } + const controlPlane = exactObject(object.controlPlane, "rollback control plane", [ + "owner", + "project", + "status", + "mirrorCommit", + "deploymentLabel", + ]); + same( + controlPlane, + { + owner: identity.controlPlane.owner, + project: identity.controlPlane.project, + status: "prod", + mirrorCommit: identity.rollback.mirrorCommit, + deploymentLabel: identity.rollback.deploymentLabel, + }, + "rollback control plane", + ); + const runtime = parseRuntimeObservation( + object.runtime, + identity.rollback, + "rollback runtime observation", + ); + const inventory = parseInventoryObservation( + object.inventory, + identity.rollback.inventory, + "rollback audited inventory", + ); + const databaseRecovery = exactObject( + object.databaseRecovery, + "completed database recovery", + ["mode", "evidenceId", "evidenceSha256", "status"], + ); + const parsedDatabase = parseDatabaseRecovery({ + mode: databaseRecovery.mode, + evidenceId: databaseRecovery.evidenceId, + evidenceSha256: databaseRecovery.evidenceSha256, + }); + same(parsedDatabase, plan.databaseRecovery, "completed database recovery"); + const expectedDatabaseStatus = parsedDatabase.mode.startsWith("restore-") + ? "restored" + : "discarded"; + if (databaseRecovery.status !== expectedDatabaseStatus) { + throw new Error("database recovery was not completed as planned"); + } + const vercel = exactObject(object.vercelProduction, "Vercel rollback evidence", [ + "deploymentId", + "productGitCommit", + "changed", + ]); + if (vercel.changed !== false) { + throw new Error("Vercel production changed during Envio rollback"); + } + same( + { deploymentId: vercel.deploymentId, productGitCommit: vercel.productGitCommit }, + plan.vercelProductionMustRemain, + "Vercel production binding", + ); + if (!Array.isArray(object.stepReceipts) || object.stepReceipts.length !== ROLLBACK_STEPS.length) { + throw new Error("rollback step receipts are incomplete"); + } + const stepReceipts = object.stepReceipts.map((receipt, index) => { + const parsed = exactObject(receipt, `rollback step receipt ${index + 1}`, [ + "ordinal", + "stepId", + "status", + "evidenceSha256", + ]); + if ( + parsed.ordinal !== index + 1 || + parsed.stepId !== ROLLBACK_STEPS[index] || + parsed.status !== "succeeded" + ) { + throw new Error("rollback step receipt order or status is invalid"); + } + return { + ordinal: parsed.ordinal, + stepId: parsed.stepId, + status: parsed.status, + evidenceSha256: exactSha( + parsed.evidenceSha256, + `rollback step receipt ${index + 1} digest`, + ), + }; + }); + const evidence = { + kind: ROLLBACK_EVIDENCE_KIND, + schemaVersion: 1, + completedAt: exactTimestamp(object.completedAt, "rollback completedAt"), + planSha256: plan.planSha256, + productGitCommit: plan.productGitCommit, + controls: { ...controls }, + controlPlane: { ...controlPlane }, + runtime, + inventory, + databaseRecovery: { ...parsedDatabase, status: expectedDatabaseStatus }, + vercelProduction: { ...vercel }, + stepReceipts, + }; + const result = deepFreeze({ + ...evidence, + rollbackEvidenceSha256: sha256( + `programmable:envio-rollback-evidence:v1\0${canonicalJson(evidence)}`, + ), + }); + if (object.existingEvidence !== null) { + if (!isPlainObject(object.existingEvidence)) { + throw new Error("existing rollback evidence must be an object"); + } + if (canonicalJson(object.existingEvidence) !== canonicalJson(result)) { + throw new Error("conflicting Envio rollback evidence already exists"); + } + } + return result; +} diff --git a/scripts/data-pipeline/cutover-envio.test.mjs b/scripts/data-pipeline/cutover-envio.test.mjs new file mode 100644 index 00000000..5e333b3d --- /dev/null +++ b/scripts/data-pipeline/cutover-envio.test.mjs @@ -0,0 +1,454 @@ +import assert from "node:assert/strict"; +import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + createEnvioPromotionAttestation, + createRollbackPlan, + loadEnvioCutoverIdentity, + validateEnvioPromotionAttestation, + validateRollbackEvidence, +} from "./cutover-envio.mjs"; + +const WORKSPACE = path.resolve(import.meta.dirname, "../.."); +const PRODUCT_COMMIT = "a".repeat(40); +const PRE_CUTOVER_VERCEL_COMMIT = "9".repeat(40); +const GATE_SHA = `0x${"b".repeat(64)}`; +const OBSERVED_AT = "2026-08-01T08:00:00.000Z"; +const CREATED_AT = "2026-08-01T08:01:00.000Z"; +const COMPLETED_AT = "2026-08-01T08:02:00.000Z"; +const VERCEL_DEPLOYMENT_ID = "dpl_programmable_candidate_123"; + +function clone(value) { + return structuredClone(value); +} + +function candidateControlPlane(identity) { + return { + owner: identity.controlPlane.owner, + project: identity.controlPlane.project, + status: "prod", + mirrorCommit: identity.candidate.mirrorCommit, + deploymentLabel: identity.candidate.deploymentLabel, + }; +} + +function runtimeObservation(target) { + return { + endpoint: target.endpoint, + endpointId: target.endpointId, + deploymentLabel: target.deploymentLabel, + identity: target.runtimeIdentity, + }; +} + +function inventoryObservation(target) { + return { + artifactPath: target.inventory.artifactPath, + artifactFileSha256: target.inventory.artifactFileSha256, + artifactDigest: target.inventory.artifactDigest, + count: target.inventory.count, + perRelease: target.inventory.perRelease, + sha256: target.inventory.sha256, + }; +} + +function createPromotion(identity, overrides = {}) { + return createEnvioPromotionAttestation({ + identity, + observedAt: OBSERVED_AT, + productGitCommit: PRODUCT_COMMIT, + releaseGateEvidenceSha256: GATE_SHA, + controlPlane: candidateControlPlane(identity), + runtime: runtimeObservation(identity.candidate), + auditedInventory: inventoryObservation(identity.candidate), + existingAttestation: null, + ...overrides, + }); +} + +function createPlan(identity, promotion, overrides = {}) { + return createRollbackPlan({ + identity, + promotionAttestation: promotion, + createdAt: CREATED_AT, + databaseRecovery: { + mode: "restore-pre-attestation-snapshot", + evidenceId: "supabase:pre-envio-promotion", + evidenceSha256: `0x${"c".repeat(64)}`, + }, + vercelProduction: { + deploymentId: VERCEL_DEPLOYMENT_ID, + productGitCommit: PRE_CUTOVER_VERCEL_COMMIT, + }, + existingPlan: null, + ...overrides, + }); +} + +function rollbackControlPlane(identity) { + return { + owner: identity.controlPlane.owner, + project: identity.controlPlane.project, + status: "prod", + mirrorCommit: identity.rollback.mirrorCommit, + deploymentLabel: identity.rollback.deploymentLabel, + }; +} + +function successfulRollbackEvidence(identity, plan, overrides = {}) { + const input = { + identity, + plan, + completedAt: COMPLETED_AT, + controls: { + publicReadFlagsEnabled: false, + sourceProjectorRunning: false, + marketProjectorRunning: false, + reconcilerRunning: false, + }, + controlPlane: rollbackControlPlane(identity), + runtime: runtimeObservation(identity.rollback), + inventory: inventoryObservation(identity.rollback), + databaseRecovery: { + ...plan.databaseRecovery, + status: "restored", + }, + vercelProduction: { + ...plan.vercelProductionMustRemain, + changed: false, + }, + stepReceipts: plan.steps.map((step) => ({ + ordinal: step.ordinal, + stepId: step.id, + status: "succeeded", + evidenceSha256: `0x${String(step.ordinal).repeat(64)}`, + })), + existingEvidence: null, + ...overrides, + }; + return validateRollbackEvidence(input); +} + +async function fixtureWorkspace(t) { + const root = await mkdtemp(path.join(os.tmpdir(), "envio-cutover-test-")); + t.after(() => rm(root, { recursive: true, force: true })); + const files = [ + "config/data-pipeline-envio-candidate.v1.json", + "config/data-pipeline-release.v1.json", + "docs/data-pipeline/envio-candidate-7f24e63-deployment-7ffd15c.json", + "docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z.json", + "docs/data-pipeline/envio-candidate-7f24e63-baseline-20260801T042058Z.json", + "docs/data-pipeline/envio-candidate-identity-7f24e63.json", + ]; + for (const file of files) { + await mkdir(path.dirname(path.join(root, file)), { recursive: true }); + await cp(path.join(WORKSPACE, file), path.join(root, file)); + } + return root; +} + +test("loads exact candidate and rollback targets only from committed evidence", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + + assert.equal(identity.candidate.endpointId, "d7a39a2"); + assert.equal( + identity.candidate.mirrorCommit, + "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + ); + assert.equal(identity.rollback.endpointId, "f6714ef"); + assert.equal( + identity.rollback.mirrorCommit, + "2cb1c35c7738fea63e656ad11589664dc93d785d", + ); + assert.equal(identity.candidate.inventory.count, 265); + assert.equal(identity.rollback.inventory.count, 265); + assert.equal(Object.isFrozen(identity.candidate.runtimeIdentity), true); +}); + +test("fails closed when checked-in candidate evidence is altered", async (t) => { + const workspace = await fixtureWorkspace(t); + const manifestPath = path.join( + workspace, + "config/data-pipeline-envio-candidate.v1.json", + ); + const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + manifest.graphqlEndpoint = + "https://indexer.hyperindex.xyz/aaaaaaa/v1/graphql"; + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + + await assert.rejects( + loadEnvioCutoverIdentity({ workspace }), + /endpoint|diverge/u, + ); +}); + +test("canonical release must bind the candidate while rollback stays in audited evidence", async (t) => { + const workspace = await fixtureWorkspace(t); + const releasePath = path.join(workspace, "config/data-pipeline-release.v1.json"); + const release = JSON.parse(await readFile(releasePath, "utf8")); + release.envio = { + deploymentLabel: "production-1e7c381", + graphqlEndpoint: "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + schemaVersion: "1", + sourceCommit: "1e7c38125714e2f485f8be0c665b12e7d7fb1809", + configSha256: "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2", + schemaSha256: "0x3217def060af2d1053ec3bca854187ff547fb43d91b113bc87a9f3285489362d", + handlerSha256: "0x241e18c3eda104b96eec4142826459c41c39cbce0474322634b5ea161d2fdf3e", + sourceRegistrySha256: "0x552e941d2ad7fea1184bf1efb97f840bdce9835c647b76f753f1326c6afe211f", + eventSetSha256: "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243", + eventCount: 51, + }; + await writeFile(releasePath, `${JSON.stringify(release, null, 2)}\n`); + await assert.rejects( + loadEnvioCutoverIdentity({ workspace }), + /release\/candidate|reviewed candidate endpoint/u, + ); +}); + +test("fails closed when an audited inventory artifact no longer matches its file commitment", async (t) => { + const workspace = await fixtureWorkspace(t); + const auditPath = path.join( + workspace, + "docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z.json", + ); + const audit = JSON.parse(await readFile(auditPath, "utf8")); + audit.inventory.count += 1; + await writeFile(auditPath, `${JSON.stringify(audit, null, 2)}\n`); + + await assert.rejects( + loadEnvioCutoverIdentity({ workspace }), + /artifact hash/u, + ); +}); + +test("promotion attestation binds control plane, runtime, inventory, product and rollback", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const attestation = createPromotion(identity); + + assert.equal(attestation.kind, "programmable-envio-promotion-attestation"); + assert.equal(attestation.productGitCommit, PRODUCT_COMMIT); + assert.equal(attestation.controlPlane.mirrorCommit, identity.candidate.mirrorCommit); + assert.deepEqual(attestation.runtime.identity, identity.candidate.runtimeIdentity); + assert.equal( + attestation.auditedInventory.sha256, + identity.candidate.inventory.sha256, + ); + assert.equal(attestation.rollbackTarget.mirrorCommit, identity.rollback.mirrorCommit); + assert.match(attestation.attestationSha256, /^0x[0-9a-f]{64}$/u); + assert.equal(validateEnvioPromotionAttestation(attestation), attestation); +}); + +test("promotion rejects a different mirror, runtime, inventory or secret-shaped input", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const wrongControl = candidateControlPlane(identity); + wrongControl.mirrorCommit = "d".repeat(40); + assert.throws( + () => createPromotion(identity, { controlPlane: wrongControl }), + /control-plane observation mismatch/u, + ); + + const wrongRuntime = runtimeObservation(identity.candidate); + wrongRuntime.endpoint = "https://indexer.hyperindex.xyz/aaaaaaa/v1/graphql"; + wrongRuntime.endpointId = "aaaaaaa"; + assert.throws( + () => createPromotion(identity, { runtime: wrongRuntime }), + /runtime observation mismatch/u, + ); + + const wrongInventory = inventoryObservation(identity.candidate); + wrongInventory.sha256 = `0x${"e".repeat(64)}`; + assert.throws( + () => createPromotion(identity, { auditedInventory: wrongInventory }), + /audited inventory mismatch/u, + ); + + assert.throws( + () => + createEnvioPromotionAttestation({ + identity, + observedAt: OBSERVED_AT, + productGitCommit: PRODUCT_COMMIT, + releaseGateEvidenceSha256: GATE_SHA, + controlPlane: candidateControlPlane(identity), + runtime: runtimeObservation(identity.candidate), + auditedInventory: inventoryObservation(identity.candidate), + existingAttestation: null, + apiToken: "must-not-be-accepted", + }), + /must contain exactly/u, + ); +}); + +test("promotion validation rejects mutation even when the shape remains valid", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const attestation = clone(createPromotion(identity)); + attestation.productGitCommit = "f".repeat(40); + + assert.throws( + () => validateEnvioPromotionAttestation(attestation), + /digest mismatch/u, + ); +}); + +test("promotion replay is idempotent and rejects conflicting evidence", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const first = createPromotion(identity); + const replay = createPromotion(identity, { existingAttestation: first }); + assert.deepEqual(replay, first); + + const conflict = clone(first); + conflict.observedAt = "2026-08-01T08:00:01.000Z"; + assert.throws( + () => createPromotion(identity, { existingAttestation: conflict }), + /digest mismatch|conflicting/u, + ); +}); + +test("rollback plan fixes the safety order and leaves Vercel unchanged", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const promotion = createPromotion(identity); + const plan = createPlan(identity, promotion); + + assert.deepEqual( + plan.steps.map(({ id }) => id), + [ + "freeze-publication-and-stop-projectors", + "promote-exact-rollback-envio", + "restore-or-discard-pre-attestation-database", + "verify-exact-rollback-runtime-and-inventory", + "verify-vercel-production-unchanged", + ], + ); + assert.equal(plan.rollbackTarget.mirrorCommit, identity.rollback.mirrorCommit); + assert.deepEqual(plan.vercelProductionMustRemain, { + deploymentId: VERCEL_DEPLOYMENT_ID, + productGitCommit: PRE_CUTOVER_VERCEL_COMMIT, + }); + assert.notEqual( + plan.vercelProductionMustRemain.productGitCommit, + plan.productGitCommit, + ); + assert.equal(JSON.stringify(plan).includes("argv"), false); + assert.equal(JSON.stringify(plan).includes("token"), false); +}); + +test("rollback plan replay is idempotent and rejects a conflicting plan", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const promotion = createPromotion(identity); + const first = createPlan(identity, promotion); + assert.deepEqual(createPlan(identity, promotion, { existingPlan: first }), first); + + const conflict = clone(first); + conflict.createdAt = "2026-08-01T08:01:01.000Z"; + assert.throws( + () => createPlan(identity, promotion, { existingPlan: conflict }), + /digest mismatch|conflicting/u, + ); +}); + +test("accepts complete exact rollback evidence and supports exact replay", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const promotion = createPromotion(identity); + const plan = createPlan(identity, promotion); + const evidence = successfulRollbackEvidence(identity, plan); + + assert.equal(evidence.controlPlane.mirrorCommit, identity.rollback.mirrorCommit); + assert.equal(evidence.runtime.endpointId, "f6714ef"); + assert.equal(evidence.vercelProduction.changed, false); + assert.match(evidence.rollbackEvidenceSha256, /^0x[0-9a-f]{64}$/u); + + const replay = successfulRollbackEvidence(identity, plan, { + existingEvidence: evidence, + }); + assert.deepEqual(replay, evidence); +}); + +test("rollback evidence fails closed on live reads, wrong runtime, wrong inventory, DB failure or Vercel change", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const plan = createPlan(identity, createPromotion(identity)); + const baseControls = { + publicReadFlagsEnabled: false, + sourceProjectorRunning: false, + marketProjectorRunning: false, + reconcilerRunning: false, + }; + + assert.throws( + () => + successfulRollbackEvidence(identity, plan, { + controls: { ...baseControls, publicReadFlagsEnabled: true }, + }), + /must be stopped first/u, + ); + + const candidateRuntime = runtimeObservation(identity.candidate); + assert.throws( + () => successfulRollbackEvidence(identity, plan, { runtime: candidateRuntime }), + /runtime observation mismatch/u, + ); + + const wrongInventory = inventoryObservation(identity.rollback); + wrongInventory.sha256 = `0x${"f".repeat(64)}`; + assert.throws( + () => successfulRollbackEvidence(identity, plan, { inventory: wrongInventory }), + /audited inventory mismatch/u, + ); + + assert.throws( + () => + successfulRollbackEvidence(identity, plan, { + databaseRecovery: { ...plan.databaseRecovery, status: "failed" }, + }), + /not completed/u, + ); + + assert.throws( + () => + successfulRollbackEvidence(identity, plan, { + vercelProduction: { + ...plan.vercelProductionMustRemain, + changed: true, + }, + }), + /Vercel production changed/u, + ); +}); + +test("rollback evidence rejects missing, reordered, failed and conflicting receipts", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const plan = createPlan(identity, createPromotion(identity)); + const receipts = plan.steps.map((step) => ({ + ordinal: step.ordinal, + stepId: step.id, + status: "succeeded", + evidenceSha256: `0x${String(step.ordinal).repeat(64)}`, + })); + + assert.throws( + () => successfulRollbackEvidence(identity, plan, { stepReceipts: receipts.slice(1) }), + /incomplete/u, + ); + const reordered = clone(receipts); + [reordered[0], reordered[1]] = [reordered[1], reordered[0]]; + assert.throws( + () => successfulRollbackEvidence(identity, plan, { stepReceipts: reordered }), + /order or status/u, + ); + const failed = clone(receipts); + failed[2].status = "failed"; + assert.throws( + () => successfulRollbackEvidence(identity, plan, { stepReceipts: failed }), + /order or status/u, + ); + + const evidence = successfulRollbackEvidence(identity, plan); + const conflict = clone(evidence); + conflict.completedAt = "2026-08-01T08:02:01.000Z"; + assert.throws( + () => successfulRollbackEvidence(identity, plan, { existingEvidence: conflict }), + /conflicting/u, + ); +}); diff --git a/scripts/data-pipeline/cutover-http.mjs b/scripts/data-pipeline/cutover-http.mjs new file mode 100644 index 00000000..dace694a --- /dev/null +++ b/scripts/data-pipeline/cutover-http.mjs @@ -0,0 +1,335 @@ +import { execFile } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; + +import { + deploymentCommit, + fetchVercelDeployment, +} from "../perf/read-model-live-verifier.mjs"; + +const execute = promisify(execFile); +const MAXIMUM_RESPONSE_BYTES = 2 * 1024 * 1024; + +export function exactStagedTarget(value, deploymentId) { + let target; + try { + target = new URL(value); + } catch { + throw new Error("staged target is invalid"); + } + if ( + target.protocol !== "https:" || + target.username || + target.password || + target.pathname !== "/" || + target.search || + target.hash || + !target.hostname.endsWith(".vercel.app") || + !/^dpl_[A-Za-z0-9]{20,80}$/u.test(deploymentId ?? "") + ) { + throw new Error("staged target must be an exact Vercel deployment"); + } + return target; +} + +function cronSecret(value) { + if ( + typeof value !== "string" || + Buffer.byteLength(value, "utf8") < 32 || + Buffer.byteLength(value, "utf8") > 1_024 || + /[\r\n]/u.test(value) + ) { + throw new Error("CRON_SECRET is required"); + } + return value; +} + +function automationBypassSecret(value) { + if ( + typeof value !== "string" || + Buffer.byteLength(value, "utf8") < 32 || + Buffer.byteLength(value, "utf8") > 512 || + /[\r\n]/u.test(value) + ) { + throw new Error("VERCEL_AUTOMATION_BYPASS_SECRET is required"); + } + return value; +} + +async function jsonRequest({ + target, + pathName, + method = "GET", + body, + secret, + bypassSecret, + fetchImpl = fetch, +}) { + const response = await fetchImpl(new URL(pathName, target), { + method, + redirect: "error", + headers: { + Accept: "application/json", + Authorization: `Bearer ${cronSecret(secret)}`, + "x-vercel-protection-bypass": automationBypassSecret(bypassSecret), + ...(body === undefined ? {} : { "Content-Type": "application/json" }), + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + signal: AbortSignal.timeout(95_000), + }); + const text = await response.text(); + if ( + Buffer.byteLength(text, "utf8") > MAXIMUM_RESPONSE_BYTES || + response.headers.get("cache-control") !== "no-store" + ) { + throw new Error("staged worker returned unsafe response metadata"); + } + let parsed; + try { + parsed = JSON.parse(text); + } catch { + throw new Error("staged worker did not return JSON"); + } + if (!response.ok) throw new Error(`staged worker failed: ${pathName}`); + return parsed; +} + +export function createStagedWorkers(input) { + const target = exactStagedTarget(input.targetUrl, input.deploymentId); + const secret = cronSecret(input.cronSecret); + const bypassSecret = automationBypassSecret(input.automationBypassSecret); + const common = { target, secret, bypassSecret, fetchImpl: input.fetchImpl }; + return Object.freeze({ + runSourceProjector: () => jsonRequest({ ...common, pathName: "/api/ops/projector" }), + runMarketProjector: () => jsonRequest({ ...common, pathName: "/api/ops/market-projector" }), + runReconciler: (request) => jsonRequest({ + ...common, + pathName: "/api/ops/reconcile-preparity", + method: "POST", + body: request, + }), + }); +} + +function deploymentAliases(deployment) { + const values = [deployment?.alias, deployment?.aliases].flatMap((value) => + Array.isArray(value) ? value : value === undefined ? [] : [value], + ); + return values.map((value) => { + const candidate = + typeof value === "string" + ? value + : value && typeof value === "object" + ? value.alias ?? value.domain + : undefined; + return typeof candidate === "string" ? candidate.toLowerCase() : ""; + }).filter(Boolean).sort(); +} + +async function resolveVercelAlias(input) { + const endpoint = new URL( + `/v4/aliases/${encodeURIComponent(input.alias)}`, + "https://api.vercel.com", + ); + endpoint.searchParams.set("teamId", input.teamId); + const response = await (input.fetchImpl ?? fetch)(endpoint, { + redirect: "error", + headers: { Authorization: `Bearer ${input.token}` }, + signal: AbortSignal.timeout(10_000), + }); + if (response.status === 404) return undefined; + const text = await response.text(); + if (!response.ok || Buffer.byteLength(text, "utf8") > MAXIMUM_RESPONSE_BYTES) { + throw new Error("Vercel alias lookup failed"); + } + let value; + try { + value = JSON.parse(text); + } catch { + throw new Error("Vercel alias lookup returned invalid JSON"); + } + return value; +} + +async function activeDeploymentAliases(input) { + const resolveAlias = input.resolveAlias ?? resolveVercelAlias; + const active = await Promise.all( + input.aliases.map(async (alias) => { + const value = await resolveAlias({ + alias, + token: input.token, + teamId: input.teamId, + fetchImpl: input.fetchImpl, + }); + return value?.deploymentId === input.deploymentId ? alias : undefined; + }), + ); + return active.filter(Boolean).sort(); +} + +export async function inspectUnexposedStagedDeployment(input) { + const target = exactStagedTarget(input.targetUrl, input.deploymentId); + if ( + !/^[0-9a-f]{40}$/u.test(input.productCommit ?? "") || + !/^prj_[A-Za-z0-9]{8,80}$/u.test(input.projectId ?? "") || + typeof input.token !== "string" || + input.token.length < 16 || + typeof input.teamId !== "string" || + input.teamId.length < 3 + ) { + throw new Error("staged Vercel control-plane input is invalid"); + } + const productionDomain = (input.productionDomain ?? "programmable.family").toLowerCase(); + if (!/^[a-z0-9.-]+$/u.test(productionDomain)) { + throw new Error("production domain is invalid"); + } + const lookup = input.fetchDeployment ?? fetchVercelDeployment; + const [candidate, production] = await Promise.all([ + lookup({ + idOrUrl: input.deploymentId, + token: input.token, + teamId: input.teamId, + fetchImpl: input.fetchImpl, + }), + lookup({ + idOrUrl: productionDomain, + token: input.token, + teamId: input.teamId, + fetchImpl: input.fetchImpl, + }), + ]); + const candidateHost = String(candidate?.url ?? "") + .replace(/^https?:\/\//u, "") + .replace(/\/$/u, ""); + const aliases = await activeDeploymentAliases({ + aliases: deploymentAliases(candidate), + deploymentId: input.deploymentId, + token: input.token, + teamId: input.teamId, + fetchImpl: input.fetchImpl, + resolveAlias: input.resolveAlias, + }); + const projectMatches = + candidate?.projectId === input.projectId || + candidate?.project?.id === input.projectId; + const productionProjectMatches = + production?.projectId === input.projectId || + production?.project?.id === input.projectId; + const productionAliases = deploymentAliases(production); + const productionDomainAssigned = aliases.includes(productionDomain); + const schedulerExposure = candidate?.id === production?.id; + if ( + candidate?.id !== input.deploymentId || + candidateHost !== target.hostname || + candidate?.readyState !== "READY" || + candidate?.target !== "production" || + !projectMatches || + deploymentCommit(candidate) !== input.productCommit || + aliases.length !== 0 || + productionDomainAssigned || + schedulerExposure || + production?.readyState !== "READY" || + production?.target !== "production" || + !productionProjectMatches || + !productionAliases.includes(productionDomain) || + !/^[0-9a-f]{40}$/u.test(deploymentCommit(production) ?? "") + ) { + throw new Error("staged deployment is exposed, aliased or not exactly bound"); + } + return Object.freeze({ + stagedDeploymentId: input.deploymentId, + stagedTarget: target.toString(), + productCommit: input.productCommit, + projectId: input.projectId, + productionDomain, + productionDomainAssigned, + schedulerExposure, + assignedAliases: Object.freeze(aliases), + currentProduction: Object.freeze({ + deploymentId: production.id, + productCommit: deploymentCommit(production), + }), + }); +} + +function lastJsonLine(value, label) { + const lines = value.trim().split("\n"); + for (let index = lines.length - 1; index >= 0; index -= 1) { + try { + return JSON.parse(lines.slice(index).join("\n")); + } catch { + // Multi-line JSON may begin on an earlier line. + } + } + throw new Error(`${label} returned invalid JSON`); +} + +export async function captureAndGateStagedReadModel(input) { + const target = exactStagedTarget(input.targetUrl, input.deploymentId); + const outputDirectory = path.resolve(input.outputDirectory); + const capture = await (input.execute ?? execute)( + process.execPath, + [ + "scripts/perf/read-model-capture.mjs", + "--target-url", + target.toString(), + "--deployment-id", + input.deploymentId, + "--output-directory", + outputDirectory, + "--kind", + "preview", + ], + { + cwd: input.workspace, + env: input.environment ?? process.env, + maxBuffer: 4 * 1024 * 1024, + timeout: 20 * 60 * 1_000, + }, + ); + const captureResult = lastJsonLine(capture.stdout, "read-model capture"); + if ( + captureResult?.mode !== "capture" || + typeof captureResult.evidencePath !== "string" + ) { + throw new Error("read-model capture did not produce evidence"); + } + const gate = await (input.execute ?? execute)( + process.execPath, + [ + "scripts/perf/read-model-gate.mjs", + "--require-release-evidence", + "--evidence", + captureResult.evidencePath, + ], + { + cwd: input.workspace, + env: { + ...(input.environment ?? process.env), + PROGRAMMABLE_READ_MODEL_TARGET_URL: target.toString(), + PROGRAMMABLE_READ_MODEL_VERCEL_DEPLOYMENT_ID: input.deploymentId, + }, + maxBuffer: 4 * 1024 * 1024, + timeout: 10 * 60 * 1_000, + }, + ); + const gateResult = lastJsonLine(gate.stdout, "read-model gate"); + if ( + gateResult?.status !== "accepted" || + gateResult?.releaseEvidenceAccepted !== true + ) { + throw new Error("read-model gate rejected staged evidence"); + } + const evidence = JSON.parse(await readFile(captureResult.evidencePath, "utf8")); + const commitment = evidence?.evidenceSha256 ?? evidence?.releaseEvidenceSha256; + if (typeof commitment !== "string" || !/^0x[0-9a-f]{64}$/u.test(commitment)) { + throw new Error("read-model evidence commitment is missing"); + } + return Object.freeze({ + status: "accepted", + releaseEvidenceAccepted: true, + evidenceSha256: commitment, + evidencePath: captureResult.evidencePath, + }); +} diff --git a/scripts/data-pipeline/cutover-http.test.mjs b/scripts/data-pipeline/cutover-http.test.mjs new file mode 100644 index 00000000..a5a127db --- /dev/null +++ b/scripts/data-pipeline/cutover-http.test.mjs @@ -0,0 +1,156 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + createStagedWorkers, + exactStagedTarget, + inspectUnexposedStagedDeployment, +} from "./cutover-http.mjs"; + +const DEPLOYMENT = "dpl_12345678901234567890"; +const SECRET = "s".repeat(32); +const BYPASS_SECRET = "b".repeat(32); + +test("staged target accepts only a deployment-specific Vercel origin", () => { + assert.equal( + exactStagedTarget("https://launcher-abc.vercel.app/", DEPLOYMENT).hostname, + "launcher-abc.vercel.app", + ); + for (const target of [ + "https://programmable.family/", + "http://launcher-abc.vercel.app/", + "https://launcher-abc.vercel.app/path", + "https://user:secret@launcher-abc.vercel.app/", + ]) { + assert.throws(() => exactStagedTarget(target, DEPLOYMENT), /exact Vercel/u); + } +}); + +test("staged worker authorization stays in headers and responses must be no-store", async () => { + const observed = []; + const workers = createStagedWorkers({ + targetUrl: "https://launcher-abc.vercel.app/", + deploymentId: DEPLOYMENT, + cronSecret: SECRET, + automationBypassSecret: BYPASS_SECRET, + fetchImpl: async (url, options) => { + observed.push({ url: String(url), options }); + return new Response( + JSON.stringify( + String(url).endsWith("reconcile-preparity") + ? { ok: true } + : { ok: true, readiness: { status: "caught-up" } }, + ), + { status: 200, headers: { "Cache-Control": "no-store" } }, + ); + }, + }); + await workers.runSourceProjector(); + await workers.runReconciler({ checkpointId: "x" }); + assert.equal(observed.length, 2); + assert.equal(observed[0].options.headers.Authorization, `Bearer ${SECRET}`); + assert.equal( + observed[0].options.headers["x-vercel-protection-bypass"], + BYPASS_SECRET, + ); + assert.equal(observed[0].url.includes(SECRET), false); + assert.equal(observed[0].url.includes(BYPASS_SECRET), false); + assert.equal(observed[1].options.body.includes(SECRET), false); + assert.equal(observed[1].options.body.includes(BYPASS_SECRET), false); +}); + +test("staged worker rejects cacheable and failed responses", async () => { + const cacheable = createStagedWorkers({ + targetUrl: "https://launcher-abc.vercel.app/", + deploymentId: DEPLOYMENT, + cronSecret: SECRET, + automationBypassSecret: BYPASS_SECRET, + fetchImpl: async () => new Response("{}", { status: 200 }), + }); + await assert.rejects(cacheable.runSourceProjector(), /unsafe response/u); + + const failed = createStagedWorkers({ + targetUrl: "https://launcher-abc.vercel.app/", + deploymentId: DEPLOYMENT, + cronSecret: SECRET, + automationBypassSecret: BYPASS_SECRET, + fetchImpl: async () => new Response("{}", { + status: 503, + headers: { "Cache-Control": "no-store" }, + }), + }); + await assert.rejects(failed.runSourceProjector(), /worker failed/u); +}); + +test("staged exposure gate accepts only the exact unaliased deployment", async () => { + const candidateCommit = "a".repeat(40); + const productionCommit = "b".repeat(40); + const projectId = "prj_12345678"; + const candidate = { + id: DEPLOYMENT, + url: "launcher-abc.vercel.app", + readyState: "READY", + target: "production", + projectId, + alias: [], + meta: { githubCommitSha: candidateCommit }, + }; + const production = { + id: "dpl_09876543210987654321", + url: "launcher-live.vercel.app", + readyState: "READY", + target: "production", + projectId, + alias: ["programmable.family"], + meta: { githubCommitSha: productionCommit }, + }; + const fetchDeployment = async ({ idOrUrl }) => + idOrUrl === DEPLOYMENT ? candidate : production; + const resolveAlias = async ({ alias }) => + candidate.alias.includes(alias) + ? { alias, deploymentId: DEPLOYMENT } + : undefined; + const result = await inspectUnexposedStagedDeployment({ + targetUrl: "https://launcher-abc.vercel.app/", + deploymentId: DEPLOYMENT, + productCommit: candidateCommit, + projectId, + token: "v".repeat(32), + teamId: "team_123", + fetchDeployment, + resolveAlias, + }); + assert.equal(result.schedulerExposure, false); + assert.equal(result.currentProduction.deploymentId, production.id); + + candidate.alias = ["programmable.family"]; + await assert.rejects( + inspectUnexposedStagedDeployment({ + targetUrl: "https://launcher-abc.vercel.app/", + deploymentId: DEPLOYMENT, + productCommit: candidateCommit, + projectId, + token: "v".repeat(32), + teamId: "team_123", + fetchDeployment, + resolveAlias, + }), + /exposed, aliased or not exactly bound/u, + ); + + candidate.alias = []; + production.id = DEPLOYMENT; + await assert.rejects( + inspectUnexposedStagedDeployment({ + targetUrl: "https://launcher-abc.vercel.app/", + deploymentId: DEPLOYMENT, + productCommit: candidateCommit, + projectId, + token: "v".repeat(32), + teamId: "team_123", + fetchDeployment, + resolveAlias, + }), + /exposed, aliased or not exactly bound/u, + ); +}); diff --git a/scripts/data-pipeline/cutover-operator.mjs b/scripts/data-pipeline/cutover-operator.mjs new file mode 100644 index 00000000..b342fa05 --- /dev/null +++ b/scripts/data-pipeline/cutover-operator.mjs @@ -0,0 +1,707 @@ +#!/usr/bin/env node + +import { execFile } from "node:child_process"; +import { + chmod, + lstat, + open, + readFile, +} from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +import { + ROLE_SPECS, + createBackupAndRestoreEvidence, + provisionLoginRoles, + verifyPoolerLogins, +} from "./cutover-credentials.mjs"; +import { + attestCandidateDatabasePromotion, + buildDatabasePromotionInput, + inspectCandidateDatabase, + inspectProjectorLeaseDrain, + readCheckpointInventory, + waitForProjectorLeaseDrain, + withDirectOperatorDatabase, +} from "./cutover-database.mjs"; +import { + createEnvioPromotionAttestation, + createRollbackPlan, + loadEnvioCutoverIdentity, + validateEnvioPromotionAttestation, + validateRollbackEvidence, +} from "./cutover-envio.mjs"; +import { + captureAndGateStagedReadModel, + createStagedWorkers, + exactStagedTarget, + inspectUnexposedStagedDeployment, +} from "./cutover-http.mjs"; +import { + assertCandidateFence, + runPostAttestationStagedGates, +} from "./cutover-phases.mjs"; +import { runConfiguredCandidateRawBackfill } from "./cutover-runtime.mjs"; +import { + assertNoSecretOutput, + canonicalJson, + safeFailure, + sha256, +} from "./hosted-db-operator-core.mjs"; +import { validateStagedReleaseAttestation } from "../perf/read-model-deploy-policy.mjs"; + +const execute = promisify(execFile); +const workspace = fileURLToPath(new URL("../../", import.meta.url)); +const CREDENTIAL_ENVIRONMENT_NAME = + /(?:DATABASE_URL|PASSWORD|API_KEY|TOKEN|SECRET|SSL_CA(?:_PEM)?|RPC_URL)$/u; +const SHA256 = /^0x(?!0{64}$)[0-9a-f]{64}$/u; +const COMMIT = /^[0-9a-f]{40}$/u; +const PRIVATE_MODE = 0o600; + +export const HELP = `Usage: + node scripts/data-pipeline/cutover-operator.mjs roles-provision --expected-project-ref REF --output FILE + node scripts/data-pipeline/cutover-operator.mjs roles-verify --expected-project-ref REF --pooler-host HOST --output FILE + node scripts/data-pipeline/cutover-operator.mjs backup-restore --expected-project-ref REF --operation-id ID --restore-isolation-id ID --backup FILE --evidence FILE + node scripts/data-pipeline/cutover-operator.mjs raw-backfill --expected-project-ref REF --backup-evidence FILE --output FILE [--maximum-cycles N] + node scripts/data-pipeline/cutover-operator.mjs projector-drain --expected-project-ref REF --target-url URL --deployment-id ID --release-gate FILE --output FILE + node scripts/data-pipeline/cutover-operator.mjs envio-attest --observation FILE --drain-evidence FILE --output FILE + node scripts/data-pipeline/cutover-operator.mjs database-plan --expected-project-ref REF --envio-attestation FILE --drain-evidence FILE --staged-deployment-id ID --output FILE + node scripts/data-pipeline/cutover-operator.mjs database-apply --expected-project-ref REF --envio-attestation FILE --drain-evidence FILE --plan FILE --confirm-apply SHA256 --output FILE + node scripts/data-pipeline/cutover-operator.mjs staged-gates --expected-project-ref REF --target-url URL --deployment-id ID --drain-evidence FILE --output-directory DIR --output FILE [--maximum-cycles N] + node scripts/data-pipeline/cutover-operator.mjs rollback-plan --envio-attestation FILE --backup-evidence FILE --vercel-deployment-id ID --vercel-product-commit COMMIT --output FILE + node scripts/data-pipeline/cutover-operator.mjs rollback-verify --plan FILE --observation FILE --output FILE + +All credentials and certificates are environment-only. Evidence outputs must be +absolute paths outside the repository and are created as mode 0600 files. +`; + +function plainObject(value, label) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + Object.getPrototypeOf(value) !== Object.prototype + ) { + throw new Error(`${label} is invalid`); + } + return value; +} + +export function parseArguments(argv) { + const [command, ...rest] = argv; + if (!command || command === "help" || command === "--help") { + return { command: "help", flags: new Map() }; + } + if (rest.length % 2 !== 0) throw new Error("operator arguments are invalid"); + const flags = new Map(); + for (let index = 0; index < rest.length; index += 2) { + const name = rest[index]; + const value = rest[index + 1]; + if (!name?.startsWith("--") || !value || value.startsWith("--") || flags.has(name)) { + throw new Error("operator arguments are invalid"); + } + flags.set(name, value); + } + return { command, flags }; +} + +function exactFlags(flags, required, optional = []) { + const allowed = new Set([...required, ...optional]); + for (const key of flags.keys()) { + if (!allowed.has(key)) throw new Error("operator argument is not allowed"); + } + for (const key of required) { + if (!flags.has(key)) throw new Error(`${key} is required`); + } +} + +function boundedCycles(value) { + if (value === undefined) return 256; + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 4096) { + throw new Error("maximum cycles is invalid"); + } + return parsed; +} + +export function credentialsFromEnvironment(environment) { + const names = { + apiReader: "PROGRAMMABLE_API_READER_DATABASE_PASSWORD", + projector: "PROGRAMMABLE_PROJECTOR_DATABASE_PASSWORD", + projectorRuntime: "PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_PASSWORD", + reconciler: "PROGRAMMABLE_RECONCILER_DATABASE_PASSWORD", + releaseProbe: "PROGRAMMABLE_RELEASE_PROBE_DATABASE_PASSWORD", + }; + const credentials = {}; + for (const { key } of ROLE_SPECS) credentials[key] = environment[names[key]]; + return credentials; +} + +function secretValues(environment) { + return Object.entries(environment) + .filter(([name]) => CREDENTIAL_ENVIRONMENT_NAME.test(name)) + .map(([, value]) => value) + .filter((value) => typeof value === "string" && value.length > 0); +} + +function absoluteExternalPath(value, label) { + if (typeof value !== "string" || !path.isAbsolute(value) || value.includes("\0")) { + throw new Error(`${label} must be an absolute path`); + } + const normalized = path.normalize(value); + const relative = path.relative(workspace, normalized); + if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) { + throw new Error(`${label} must be outside the repository`); + } + return normalized; +} + +async function writePrivateOutput(value, outputPath, environment) { + const target = absoluteExternalPath(outputPath, "output path"); + assertNoSecretOutput(value, secretValues(environment)); + const serialized = `${JSON.stringify(value, null, 2)}\n`; + const descriptor = await open(target, "wx", PRIVATE_MODE); + try { + await descriptor.writeFile(serialized, "utf8"); + await descriptor.sync(); + } finally { + await descriptor.close(); + } + await chmod(target, PRIVATE_MODE); + return target; +} + +async function readArtifact(filePath, { privateFile = false } = {}) { + const target = path.resolve(filePath); + const metadata = await lstat(target); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error("evidence artifact must be a regular file"); + } + if (privateFile && (metadata.mode & 0o777) !== PRIVATE_MODE) { + throw new Error("operator evidence must have mode 0600"); + } + return plainObject(JSON.parse(await readFile(target, "utf8")), "evidence artifact"); +} + +export function evidenceCommitment(value, label = "evidence") { + const object = plainObject(value, label); + const candidates = [ + object.evidenceSha256, + object.releaseEvidenceSha256, + object.attestationSha256, + object.rollbackEvidenceSha256, + ].filter((candidate) => typeof candidate === "string"); + if (candidates.length > 1 || (candidates.length === 1 && !SHA256.test(candidates[0]))) { + throw new Error(`${label} commitment is invalid`); + } + return candidates[0] ?? sha256(canonicalJson(object)); +} + +async function gitCommit() { + const { stdout } = await execute("git", ["rev-parse", "HEAD"], { cwd: workspace }); + const commit = stdout.trim(); + if (!COMMIT.test(commit)) throw new Error("repository commit is invalid"); + return commit; +} + +async function assertCleanCheckout() { + const { stdout } = await execute( + "git", + ["status", "--porcelain=v1", "--untracked-files=all"], + { cwd: workspace }, + ); + if (stdout.trim() !== "") throw new Error("cutover checkout must be clean"); + return gitCommit(); +} + +function directDatabase(environment, expectedProjectRef) { + return { + databaseUrl: environment.PROGRAMMABLE_MIGRATOR_DATABASE_URL, + expectedProjectRef, + sslCaPem: environment.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + }; +} + +function identityTarget(target) { + return { + mirrorCommit: target.mirrorCommit, + deploymentLabel: target.deploymentLabel, + endpoint: target.endpoint, + endpointId: target.endpointId, + runtimeIdentity: target.runtimeIdentity, + inventorySha256: target.inventory.sha256, + }; +} + +export function assertAttestationMatchesIdentity(attestation, identity) { + const validated = validateEnvioPromotionAttestation(attestation); + if ( + canonicalJson(validated.candidateTarget) !== canonicalJson(identityTarget(identity.candidate)) || + canonicalJson(validated.rollbackTarget) !== canonicalJson(identityTarget(identity.rollback)) + ) { + throw new Error("Envio attestation does not match checked-in cutover identity"); + } + return validated; +} + +function assertBackupEvidence(value, commit) { + const evidence = plainObject(value, "backup evidence"); + if ( + evidence.kind !== "programmable-database-backup-restore-evidence" || + evidence.schemaVersion !== 1 || + evidence.repositoryCommit !== commit || + evidence.sourceManifestSha256 !== evidence.restoredManifestSha256 || + !SHA256.test(evidence.sourceManifestSha256 ?? "") || + !SHA256.test(evidence.backup?.sha256 ?? "") + ) { + throw new Error("backup and restore evidence is not valid for this commit"); + } + return evidence; +} + +export function assertProjectorDrainEvidence(value, commit, stagedDeploymentId) { + const evidence = plainObject(value, "projector drain evidence"); + if ( + evidence.kind !== "programmable-projector-drain-evidence" || + evidence.schemaVersion !== 1 || + evidence.productCommit !== commit || + (stagedDeploymentId !== undefined && + evidence.stagedDeploymentId !== stagedDeploymentId) || + evidence.publicationFence !== "closed" || + evidence.stageExposure?.stagedDeploymentId !== evidence.stagedDeploymentId || + evidence.stageExposure?.stagedTarget !== evidence.stagedTarget || + evidence.stageExposure?.productCommit !== commit || + evidence.stageExposure?.productionDomainAssigned !== false || + evidence.stageExposure?.schedulerExposure !== false || + !Array.isArray(evidence.stageExposure?.assignedAliases) || + evidence.stageExposure.assignedAliases.length !== 0 || + evidence.leaseDrain?.drained !== true || + !Number.isSafeInteger(evidence.leaseDrain?.stabilityWindowMs) || + evidence.leaseDrain.stabilityWindowMs < 65_000 || + !Number.isSafeInteger(evidence.leaseDrain?.stableForMs) || + evidence.leaseDrain.stableForMs < evidence.leaseDrain.stabilityWindowMs || + !SHA256.test(evidence.releaseGateEvidenceSha256 ?? "") || + !SHA256.test(evidence.evidenceSha256 ?? "") + ) { + throw new Error("projector drain evidence is invalid for this cutover"); + } + const { evidenceSha256, ...payload } = evidence; + if (sha256(canonicalJson(payload)) !== evidenceSha256) { + throw new Error("projector drain evidence commitment is invalid"); + } + return evidence; +} + +export function assertStagedGateMatchesDrain( + value, + commit, + stagedDeploymentId, + targetUrl, +) { + const evidence = assertProjectorDrainEvidence(value, commit, stagedDeploymentId); + const target = exactStagedTarget(targetUrl, stagedDeploymentId); + if (evidence.stagedTarget !== target.toString()) { + throw new Error("staged gate target differs from the drained deployment"); + } + return evidence; +} + +async function reverifyUnexposedStage(evidence, environment) { + const observed = await inspectUnexposedStagedDeployment({ + targetUrl: evidence.stagedTarget, + deploymentId: evidence.stagedDeploymentId, + productCommit: evidence.productCommit, + projectId: environment.VERCEL_PROJECT_ID, + token: environment.VERCEL_TOKEN, + teamId: environment.VERCEL_ORG_ID, + productionDomain: evidence.stageExposure.productionDomain, + }); + if (canonicalJson(observed) !== canonicalJson(evidence.stageExposure)) { + throw new Error("staged deployment exposure changed after the drain gate"); + } + return observed; +} + +async function createDatabasePlan({ + environment, + expectedProjectRef, + attestationPath, + drainEvidencePath, + stagedDeploymentId, +}) { + const commit = await assertCleanCheckout(); + const identity = await loadEnvioCutoverIdentity({ workspace }); + const attestation = assertAttestationMatchesIdentity( + await readArtifact(attestationPath, { privateFile: true }), + identity, + ); + if (attestation.productGitCommit !== commit) { + throw new Error("Envio attestation is for a different product commit"); + } + const drain = assertProjectorDrainEvidence( + await readArtifact(drainEvidencePath, { privateFile: true }), + commit, + stagedDeploymentId, + ); + if (attestation.releaseGateEvidenceSha256 !== drain.evidenceSha256) { + throw new Error("Envio attestation is not bound to the projector drain gate"); + } + await reverifyUnexposedStage(drain, environment); + return withDirectOperatorDatabase( + directDatabase(environment, expectedProjectRef), + async (sql) => { + const state = assertCandidateFence(await inspectCandidateDatabase(sql), "fenced"); + return buildDatabasePromotionInput({ + candidateEndpointIdentity: `envio:${identity.candidate.endpointId}`, + envioProviderDeploymentId: state.envioProviderDeploymentId, + baselineCommitment: identity.rollback.inventory.artifactDigest, + candidateInventoryParityCommitment: identity.candidate.inventory.sha256, + envioPromotionAttestationCommitment: attestation.attestationSha256, + productCommit: commit, + stagedDeploymentId, + promotedAt: attestation.observedAt, + }); + }, + ); +} + +async function runCommand(command, flags, environment) { + if (command === "roles-provision") { + exactFlags(flags, ["--expected-project-ref", "--output"]); + const result = await provisionLoginRoles({ + ...directDatabase(environment, flags.get("--expected-project-ref")), + credentials: credentialsFromEnvironment(environment), + }); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "roles-verify") { + exactFlags(flags, ["--expected-project-ref", "--pooler-host", "--output"]); + const result = await verifyPoolerLogins({ + expectedProjectRef: flags.get("--expected-project-ref"), + poolerHost: flags.get("--pooler-host"), + sslCaPem: environment.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + credentials: credentialsFromEnvironment(environment), + }); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "backup-restore") { + exactFlags(flags, [ + "--expected-project-ref", + "--operation-id", + "--restore-isolation-id", + "--backup", + "--evidence", + ]); + return createBackupAndRestoreEvidence({ + operationId: flags.get("--operation-id"), + repositoryCommit: await assertCleanCheckout(), + sourceDatabaseUrl: environment.PROGRAMMABLE_MIGRATOR_DATABASE_URL, + expectedProjectRef: flags.get("--expected-project-ref"), + sslCaPem: environment.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + restoreDatabaseUrl: environment.PROGRAMMABLE_CUTOVER_RESTORE_DATABASE_URL, + restoreIsolationId: flags.get("--restore-isolation-id"), + restoreSslCaPem: environment.PROGRAMMABLE_CUTOVER_RESTORE_SSL_CA_PEM, + backupPath: absoluteExternalPath(flags.get("--backup"), "backup path"), + evidencePath: absoluteExternalPath(flags.get("--evidence"), "evidence path"), + }); + } + if (command === "raw-backfill") { + exactFlags( + flags, + ["--expected-project-ref", "--backup-evidence", "--output"], + ["--maximum-cycles"], + ); + const commit = await assertCleanCheckout(); + assertBackupEvidence(await readArtifact(flags.get("--backup-evidence"), { privateFile: true }), commit); + const startedAt = new Date().toISOString(); + const result = await withDirectOperatorDatabase( + directDatabase(environment, flags.get("--expected-project-ref")), + (sql) => runConfiguredCandidateRawBackfill({ + environment, + maximumCycles: boundedCycles(flags.get("--maximum-cycles")), + inspectFence: () => inspectCandidateDatabase(sql), + startedAt, + completedAt: () => new Date().toISOString(), + }), + ); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "projector-drain") { + exactFlags(flags, [ + "--expected-project-ref", + "--target-url", + "--deployment-id", + "--release-gate", + "--output", + ]); + const commit = await assertCleanCheckout(); + const target = exactStagedTarget(flags.get("--target-url"), flags.get("--deployment-id")); + const gate = validateStagedReleaseAttestation( + await readArtifact(flags.get("--release-gate"), { privateFile: true }), + { + verifiedSha: commit, + vercelProjectId: environment.VERCEL_PROJECT_ID, + stagedDeploymentId: flags.get("--deployment-id"), + stagedDeploymentUrl: target.origin, + productionOrigin: "https://programmable.family", + requireWorkersActive: true, + requireIndexedRoutesActive: true, + }, + ); + const stageExposure = await inspectUnexposedStagedDeployment({ + targetUrl: target.toString(), + deploymentId: flags.get("--deployment-id"), + productCommit: commit, + projectId: environment.VERCEL_PROJECT_ID, + token: environment.VERCEL_TOKEN, + teamId: environment.VERCEL_ORG_ID, + }); + const result = await withDirectOperatorDatabase( + directDatabase(environment, flags.get("--expected-project-ref")), + async (sql) => { + const fence = assertCandidateFence(await inspectCandidateDatabase(sql), "fenced"); + const leaseDrain = await waitForProjectorLeaseDrain({ + inspect: () => inspectProjectorLeaseDrain(sql), + stabilityWindowMs: 65_000, + }); + const payload = { + kind: "programmable-projector-drain-evidence", + schemaVersion: 1, + productCommit: commit, + stagedDeploymentId: flags.get("--deployment-id"), + stagedTarget: target.toString(), + candidateEndpointIdentity: "envio:d7a39a2", + releaseGateEvidenceSha256: evidenceCommitment(gate, "release gate evidence"), + publicationFence: "closed", + envioProviderDeploymentId: fence.envioProviderDeploymentId, + stageExposure, + leaseDrain, + completedAt: new Date().toISOString(), + }; + return Object.freeze({ + ...payload, + evidenceSha256: sha256(canonicalJson(payload)), + }); + }, + ); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "envio-attest") { + exactFlags(flags, ["--observation", "--drain-evidence", "--output"]); + const commit = await assertCleanCheckout(); + const identity = await loadEnvioCutoverIdentity({ workspace }); + const observation = await readArtifact(flags.get("--observation"), { privateFile: true }); + const drain = assertProjectorDrainEvidence( + await readArtifact(flags.get("--drain-evidence"), { privateFile: true }), + commit, + ); + const result = createEnvioPromotionAttestation({ + identity, + observedAt: observation.observedAt, + productGitCommit: commit, + releaseGateEvidenceSha256: drain.evidenceSha256, + controlPlane: observation.controlPlane, + runtime: observation.runtime, + auditedInventory: identity.candidate.inventory, + existingAttestation: null, + }); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "database-plan") { + exactFlags(flags, [ + "--expected-project-ref", + "--envio-attestation", + "--drain-evidence", + "--staged-deployment-id", + "--output", + ]); + exactStagedTarget("https://candidate.vercel.app/", flags.get("--staged-deployment-id")); + const result = await createDatabasePlan({ + environment, + expectedProjectRef: flags.get("--expected-project-ref"), + attestationPath: flags.get("--envio-attestation"), + drainEvidencePath: flags.get("--drain-evidence"), + stagedDeploymentId: flags.get("--staged-deployment-id"), + }); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "database-apply") { + exactFlags(flags, [ + "--expected-project-ref", + "--envio-attestation", + "--drain-evidence", + "--plan", + "--confirm-apply", + "--output", + ]); + const reviewed = await readArtifact(flags.get("--plan"), { privateFile: true }); + const rebuilt = await createDatabasePlan({ + environment, + expectedProjectRef: flags.get("--expected-project-ref"), + attestationPath: flags.get("--envio-attestation"), + drainEvidencePath: flags.get("--drain-evidence"), + stagedDeploymentId: reviewed.stagedDeploymentId, + }); + if ( + canonicalJson(reviewed) !== canonicalJson(rebuilt) || + flags.get("--confirm-apply") !== rebuilt.inputCommitment + ) { + throw new Error("database promotion confirmation does not match the reviewed plan"); + } + const result = await withDirectOperatorDatabase( + directDatabase(environment, flags.get("--expected-project-ref")), + async (sql) => { + const changed = await attestCandidateDatabasePromotion({ sql, promotion: rebuilt }); + const state = assertCandidateFence(await inspectCandidateDatabase(sql), "attested"); + if (state.promotionAttestationCommitment !== rebuilt.envioPromotionAttestationCommitment) { + throw new Error("database promotion attestation did not persist exactly"); + } + if ( + state.productCommit !== rebuilt.productCommit || + state.stagedDeploymentId !== rebuilt.stagedDeploymentId + ) { + throw new Error("database promotion deployment binding did not persist exactly"); + } + return { + kind: "programmable-database-promotion-result", + schemaVersion: 1, + changed: changed.changed, + promotionInputCommitment: rebuilt.inputCommitment, + state, + }; + }, + ); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "staged-gates") { + exactFlags( + flags, + [ + "--expected-project-ref", + "--target-url", + "--deployment-id", + "--drain-evidence", + "--output-directory", + "--output", + ], + ["--maximum-cycles"], + ); + const commit = await assertCleanCheckout(); + const drain = assertStagedGateMatchesDrain( + await readArtifact(flags.get("--drain-evidence"), { privateFile: true }), + commit, + flags.get("--deployment-id"), + flags.get("--target-url"), + ); + await reverifyUnexposedStage(drain, environment); + const target = exactStagedTarget(flags.get("--target-url"), flags.get("--deployment-id")); + const workers = createStagedWorkers({ + targetUrl: target.toString(), + deploymentId: flags.get("--deployment-id"), + cronSecret: environment.CRON_SECRET, + automationBypassSecret: environment.VERCEL_AUTOMATION_BYPASS_SECRET, + }); + const result = await withDirectOperatorDatabase( + directDatabase(environment, flags.get("--expected-project-ref")), + (sql) => runPostAttestationStagedGates({ + candidateEndpointIdentity: "envio:d7a39a2", + stagedDeploymentId: flags.get("--deployment-id"), + productCommit: commit, + maximumWorkerCycles: boundedCycles(flags.get("--maximum-cycles")), + inspectFence: () => inspectCandidateDatabase(sql), + readCheckpoints: () => readCheckpointInventory(sql), + ...workers, + runLoadGate: () => captureAndGateStagedReadModel({ + targetUrl: target.toString(), + deploymentId: flags.get("--deployment-id"), + outputDirectory: absoluteExternalPath(flags.get("--output-directory"), "output directory"), + workspace, + environment, + }), + completedAt: () => new Date().toISOString(), + }), + ); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "rollback-plan") { + exactFlags(flags, [ + "--envio-attestation", + "--backup-evidence", + "--vercel-deployment-id", + "--vercel-product-commit", + "--output", + ]); + const commit = await assertCleanCheckout(); + const identity = await loadEnvioCutoverIdentity({ workspace }); + const attestation = assertAttestationMatchesIdentity( + await readArtifact(flags.get("--envio-attestation"), { privateFile: true }), + identity, + ); + if (attestation.productGitCommit !== commit) { + throw new Error("Envio attestation is for a different product commit"); + } + const backup = assertBackupEvidence( + await readArtifact(flags.get("--backup-evidence"), { privateFile: true }), + commit, + ); + const result = createRollbackPlan({ + identity, + promotionAttestation: attestation, + createdAt: new Date().toISOString(), + databaseRecovery: { + mode: "restore-pre-attestation-snapshot", + evidenceId: `supabase:${backup.operationId}`, + evidenceSha256: evidenceCommitment(backup, "backup evidence"), + }, + vercelProduction: { + deploymentId: flags.get("--vercel-deployment-id"), + productGitCommit: flags.get("--vercel-product-commit"), + }, + existingPlan: null, + }); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + if (command === "rollback-verify") { + exactFlags(flags, ["--plan", "--observation", "--output"]); + await assertCleanCheckout(); + const identity = await loadEnvioCutoverIdentity({ workspace }); + const plan = await readArtifact(flags.get("--plan"), { privateFile: true }); + const observation = await readArtifact(flags.get("--observation"), { privateFile: true }); + const result = validateRollbackEvidence({ + identity, + plan, + ...observation, + existingEvidence: null, + }); + await writePrivateOutput(result, flags.get("--output"), environment); + return result; + } + throw new Error("unknown cutover operator command"); +} + +export async function main(argv = process.argv.slice(2), environment = process.env) { + const { command, flags } = parseArguments(argv); + if (command === "help") { + process.stdout.write(HELP); + return null; + } + return runCommand(command, flags, environment); +} + +if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) { + main().catch((error) => { + process.stderr.write(`${safeFailure(error)}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/data-pipeline/cutover-operator.test.mjs b/scripts/data-pipeline/cutover-operator.test.mjs new file mode 100644 index 00000000..1a207c01 --- /dev/null +++ b/scripts/data-pipeline/cutover-operator.test.mjs @@ -0,0 +1,174 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import test from "node:test"; + +import { + assertAttestationMatchesIdentity, + assertProjectorDrainEvidence, + assertStagedGateMatchesDrain, + credentialsFromEnvironment, + evidenceCommitment, + parseArguments, +} from "./cutover-operator.mjs"; +import { + createEnvioPromotionAttestation, + loadEnvioCutoverIdentity, +} from "./cutover-envio.mjs"; +import { canonicalJson, sha256 } from "./hosted-db-operator-core.mjs"; + +const WORKSPACE = path.resolve(import.meta.dirname, "../.."); + +test("operator parser rejects positional, duplicate and value-less arguments", () => { + assert.deepEqual(parseArguments(["roles-provision", "--output", "/tmp/result"]), { + command: "roles-provision", + flags: new Map([["--output", "/tmp/result"]]), + }); + assert.throws( + () => parseArguments(["roles-provision", "output", "/tmp/result"]), + /arguments/u, + ); + assert.throws( + () => parseArguments(["roles-provision", "--output"]), + /arguments/u, + ); + assert.throws( + () => parseArguments(["roles-provision", "--output", "a", "--output", "b"]), + /arguments/u, + ); +}); + +test("credentials are read from five environment-only names", () => { + const environment = { + PROGRAMMABLE_API_READER_DATABASE_PASSWORD: "a".repeat(32), + PROGRAMMABLE_PROJECTOR_DATABASE_PASSWORD: "b".repeat(32), + PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_PASSWORD: "c".repeat(32), + PROGRAMMABLE_RECONCILER_DATABASE_PASSWORD: "d".repeat(32), + PROGRAMMABLE_RELEASE_PROBE_DATABASE_PASSWORD: "e".repeat(32), + }; + assert.deepEqual(credentialsFromEnvironment(environment), { + apiReader: "a".repeat(32), + projector: "b".repeat(32), + projectorRuntime: "c".repeat(32), + reconciler: "d".repeat(32), + releaseProbe: "e".repeat(32), + }); +}); + +test("evidence commitment prefers one canonical embedded commitment", () => { + const commitment = `0x${"a".repeat(64)}`; + assert.equal(evidenceCommitment({ evidenceSha256: commitment }), commitment); + assert.match(evidenceCommitment({ kind: "plain", value: 1 }), /^0x[0-9a-f]{64}$/u); + assert.throws( + () => evidenceCommitment({ evidenceSha256: commitment, attestationSha256: commitment }), + /commitment/u, + ); + assert.throws( + () => evidenceCommitment({ evidenceSha256: `0x${"0".repeat(64)}` }), + /commitment/u, + ); +}); + +test("promotion attestation must match the checked-in candidate and rollback", async () => { + const identity = await loadEnvioCutoverIdentity({ workspace: WORKSPACE }); + const attestation = createEnvioPromotionAttestation({ + identity, + observedAt: "2026-08-01T08:00:00.000Z", + productGitCommit: "a".repeat(40), + releaseGateEvidenceSha256: `0x${"b".repeat(64)}`, + controlPlane: { + owner: identity.controlPlane.owner, + project: identity.controlPlane.project, + status: "prod", + mirrorCommit: identity.candidate.mirrorCommit, + deploymentLabel: identity.candidate.deploymentLabel, + }, + runtime: { + endpoint: identity.candidate.endpoint, + endpointId: identity.candidate.endpointId, + deploymentLabel: identity.candidate.deploymentLabel, + identity: identity.candidate.runtimeIdentity, + }, + auditedInventory: identity.candidate.inventory, + existingAttestation: null, + }); + assert.equal(assertAttestationMatchesIdentity(attestation, identity), attestation); + + const changedIdentity = structuredClone(identity); + changedIdentity.rollback.inventory.sha256 = `0x${"c".repeat(64)}`; + assert.throws( + () => assertAttestationMatchesIdentity(attestation, changedIdentity), + /checked-in cutover identity/u, + ); +}); + +test("projector drain evidence binds stopped schedulers, drained leases and stage", () => { + const commit = "a".repeat(40); + const payload = { + kind: "programmable-projector-drain-evidence", + schemaVersion: 1, + productCommit: commit, + stagedDeploymentId: "dpl_12345678901234567890", + stagedTarget: "https://launcher-abc.vercel.app/", + publicationFence: "closed", + stageExposure: { + stagedDeploymentId: "dpl_12345678901234567890", + stagedTarget: "https://launcher-abc.vercel.app/", + productCommit: commit, + productionDomainAssigned: false, + schedulerExposure: false, + assignedAliases: [], + }, + leaseDrain: { + drained: true, + stabilityWindowMs: 65_000, + stableForMs: 65_000, + }, + releaseGateEvidenceSha256: `0x${"b".repeat(64)}`, + }; + const evidence = { + ...payload, + evidenceSha256: sha256(canonicalJson(payload)), + }; + assert.equal( + assertProjectorDrainEvidence( + evidence, + commit, + "dpl_12345678901234567890", + ), + evidence, + ); + assert.equal( + assertStagedGateMatchesDrain( + evidence, + commit, + "dpl_12345678901234567890", + "https://launcher-abc.vercel.app/", + ), + evidence, + ); + assert.throws( + () => assertProjectorDrainEvidence( + { ...evidence, leaseDrain: { drained: false } }, + commit, + "dpl_12345678901234567890", + ), + /drain evidence/u, + ); + assert.throws( + () => assertProjectorDrainEvidence( + evidence, + commit, + "dpl_09876543210987654321", + ), + /drain evidence/u, + ); + assert.throws( + () => assertStagedGateMatchesDrain( + evidence, + commit, + "dpl_12345678901234567890", + "https://other.vercel.app/", + ), + /differs from the drained deployment/u, + ); +}); diff --git a/scripts/data-pipeline/cutover-phases.mjs b/scripts/data-pipeline/cutover-phases.mjs new file mode 100644 index 00000000..82132928 --- /dev/null +++ b/scripts/data-pipeline/cutover-phases.mjs @@ -0,0 +1,331 @@ +import { canonicalJson, sha256 } from "./hosted-db-operator-core.mjs"; + +const BYTES32 = /^0x(?!0{64}$)[0-9a-f]{64}$/u; +const COMMIT = /^[0-9a-f]{40}$/u; +const DEPLOYMENT_ID = /^dpl_[A-Za-z0-9]{20,80}$/u; +const UUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u; +const RELEASES = Object.freeze([ + Object.freeze({ releaseId: "classic-v2", modelId: "classic" }), + Object.freeze({ releaseId: "classic-v3", modelId: "classic" }), + Object.freeze({ releaseId: "stock-paired-v1", modelId: "stock-paired" }), + Object.freeze({ releaseId: "stock-paired-v2", modelId: "stock-paired" }), + Object.freeze({ releaseId: "stock-paired-v3", modelId: "stock-paired" }), +]); + +function object(value, label) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${label} is invalid`); + } + return value; +} + +function integer(value, label, { positive = false } = {}) { + const text = typeof value === "bigint" ? value.toString() : String(value); + if (!/^(?:0|[1-9][0-9]*)$/u.test(text) || (positive && text === "0")) { + throw new Error(`${label} is invalid`); + } + return text; +} + +function bytes32(value, label) { + const result = typeof value === "string" ? value.toLowerCase() : ""; + if (!BYTES32.test(result)) throw new Error(`${label} is invalid`); + return result; +} + +function isoTimestamp(value, label) { + if ( + typeof value !== "string" || + Number.isNaN(Date.parse(value)) || + new Date(value).toISOString() !== value + ) { + throw new Error(`${label} is invalid`); + } + return value; +} + +function commitEvidence(payload) { + return Object.freeze({ + ...payload, + evidenceSha256: sha256(canonicalJson(payload)), + }); +} + +export function assertCandidateFence(value, expectedState = "fenced") { + const input = object(value, "candidate database fence"); + const publicationCount = Number(integer(input.publicationCount, "publication count")); + const promoted = input.promoted === true; + if ( + input.databaseMode !== "candidate-only" || + typeof input.envioProviderDeploymentId !== "string" || + !UUID.test(input.envioProviderDeploymentId) || + !Number.isSafeInteger(publicationCount) || + publicationCount < 0 + ) { + throw new Error("candidate database fence is invalid"); + } + if (expectedState === "fenced" && (promoted || publicationCount !== 0)) { + throw new Error("candidate database publication fence is not closed"); + } + if (expectedState === "attested" && !promoted) { + throw new Error("candidate database promotion is not attested"); + } + if ( + promoted && + (!COMMIT.test(input.productCommit ?? "") || + !DEPLOYMENT_ID.test(input.stagedDeploymentId ?? "")) + ) { + throw new Error("candidate database deployment binding is invalid"); + } + if ( + !promoted && + (input.productCommit !== null || input.stagedDeploymentId !== null) + ) { + throw new Error("fenced candidate database already has a deployment binding"); + } + return Object.freeze({ + databaseMode: "candidate-only", + envioProviderDeploymentId: input.envioProviderDeploymentId, + promoted, + publicationCount, + promotionAttestationCommitment: + input.promotionAttestationCommitment === null || + input.promotionAttestationCommitment === undefined + ? null + : bytes32( + input.promotionAttestationCommitment, + "database promotion attestation", + ), + productCommit: input.productCommit, + stagedDeploymentId: input.stagedDeploymentId, + }); +} + +export async function runFencedRawBackfill(input) { + const maximumCycles = input.maximumCycles ?? 256; + if (!Number.isSafeInteger(maximumCycles) || maximumCycles < 1 || maximumCycles > 4096) { + throw new Error("raw backfill cycle bound is invalid"); + } + const before = assertCandidateFence(await input.inspectFence(), "fenced"); + const cycles = []; + let terminal = false; + for (let index = 0; index < maximumCycles; index += 1) { + const raw = object(await input.runRawCycle(), "raw backfill result"); + const candidateCount = Number(integer(raw.candidateCount ?? 0, "candidate count")); + if (!Number.isSafeInteger(candidateCount) || candidateCount < 0) { + throw new Error("raw backfill result is invalid"); + } + if ( + ![ + "committed", + "committed-empty", + "recovered-reorg", + "staged-dynamic-parent", + "idle", + ].includes(raw.status) + ) { + throw new Error("raw backfill failed"); + } + cycles.push(Object.freeze({ + ordinal: index + 1, + status: raw.status, + candidateCount, + snapshotBlock: integer(raw.snapshotBlock, "raw snapshot block"), + ...(raw.generation === undefined + ? {} + : { generation: integer(raw.generation, "raw cursor generation") }), + })); + if (raw.status === "idle") { + terminal = true; + break; + } + } + if (!terminal) throw new Error("raw backfill did not reach an idle boundary"); + const after = assertCandidateFence(await input.inspectFence(), "fenced"); + if ( + before.envioProviderDeploymentId !== after.envioProviderDeploymentId || + before.publicationCount !== after.publicationCount + ) { + throw new Error("candidate database fence changed during raw backfill"); + } + const payload = { + kind: "programmable-candidate-raw-backfill-evidence", + schemaVersion: 1, + candidateEndpointIdentity: input.candidateEndpointIdentity, + envioProviderDeploymentId: before.envioProviderDeploymentId, + startedAt: isoTimestamp(input.startedAt, "raw backfill start"), + completedAt: isoTimestamp(input.completedAt(), "raw backfill completion"), + cycleCount: cycles.length, + candidateCount: cycles.reduce((sum, cycle) => sum + cycle.candidateCount, 0), + terminalStatus: "idle", + publicationFence: "closed", + cycles: Object.freeze(cycles), + }; + return commitEvidence(payload); +} + +export function checkpointRequestsFromRows(rows, maximumEntityCount = 10_000) { + if ( + !Array.isArray(rows) || + !Number.isSafeInteger(maximumEntityCount) || + maximumEntityCount < 1 || + maximumEntityCount > 10_000 + ) { + throw new Error("checkpoint inventory is invalid"); + } + const requests = rows.map((rowValue) => { + const row = object(rowValue, "checkpoint row"); + const releaseId = String(row.release_id ?? row.releaseId ?? ""); + const modelId = String(row.model_id ?? row.modelId ?? ""); + const expected = RELEASES.find( + (release) => release.releaseId === releaseId && release.modelId === modelId, + ); + const epochId = String(row.epoch_id ?? row.epochId ?? ""); + const checkpointId = String(row.checkpoint_id ?? row.checkpointId ?? ""); + const blockHash = String( + row.checkpoint_block_hash ?? row.checkpointBlockHash ?? row.block_hash ?? row.blockHash ?? "", + ).toLowerCase(); + if ( + !expected || + !UUID.test(epochId) || + !UUID.test(checkpointId) || + !BYTES32.test(blockHash) + ) { + throw new Error("checkpoint row is invalid"); + } + return Object.freeze({ + chainId: integer(row.chain_id ?? row.chainId, "checkpoint chain"), + releaseId, + modelId, + sourceGroup: String(row.source_group ?? row.sourceGroup ?? ""), + epochId, + pointerGeneration: integer( + row.pointer_generation ?? row.pointerGeneration, + "checkpoint pointer generation", + { positive: true }, + ), + checkpointId, + checkpointBlockNumber: integer( + row.block_number ?? row.checkpointBlockNumber, + "checkpoint block", + ), + checkpointBlockHash: blockHash, + maximumEntityCount, + }); + }); + if ( + requests.length !== RELEASES.length || + requests.some((request) => request.chainId !== "1" || request.sourceGroup !== "core") || + RELEASES.some( + (release) => + requests.filter( + (request) => + request.releaseId === release.releaseId && request.modelId === release.modelId, + ).length !== 1, + ) + ) { + throw new Error("checkpoint inventory is incomplete or duplicated"); + } + return Object.freeze( + [...requests].sort( + (left, right) => + RELEASES.findIndex(({ releaseId }) => releaseId === left.releaseId) - + RELEASES.findIndex(({ releaseId }) => releaseId === right.releaseId), + ), + ); +} + +function sourceCaughtUp(result) { + return result?.ok === true && result?.readiness?.status === "caught-up" && + result.readiness.activationReady === true && result.readiness.lagging === false; +} + +function marketCaughtUp(result) { + return result?.caughtUp === true && result?.lagBlocks === "0"; +} + +export async function runPostAttestationStagedGates(input) { + const fence = assertCandidateFence(await input.inspectFence(), "attested"); + if ( + fence.productCommit !== input.productCommit || + fence.stagedDeploymentId !== input.stagedDeploymentId + ) { + throw new Error("staged runtime does not match the database deployment binding"); + } + const maximumWorkerCycles = input.maximumWorkerCycles ?? 256; + if ( + !Number.isSafeInteger(maximumWorkerCycles) || + maximumWorkerCycles < 1 || + maximumWorkerCycles > 4096 + ) { + throw new Error("worker cycle bound is invalid"); + } + let source; + let sourceCycles = 0; + for (; sourceCycles < maximumWorkerCycles; sourceCycles += 1) { + source = await input.runSourceProjector(); + if (sourceCaughtUp(source)) break; + if (source?.ok !== true || source?.status === "disabled") { + throw new Error("source projector failed before catch-up"); + } + } + if (!sourceCaughtUp(source)) throw new Error("source projector did not catch up"); + + let market; + let marketCycles = 0; + for (; marketCycles < maximumWorkerCycles; marketCycles += 1) { + market = await input.runMarketProjector(); + if (marketCaughtUp(market)) break; + if (market?.status === "disabled") { + throw new Error("market projector is disabled"); + } + } + if (!marketCaughtUp(market)) throw new Error("market projector did not catch up"); + + const checkpoints = checkpointRequestsFromRows(await input.readCheckpoints()); + const reconciliations = []; + for (const request of checkpoints) { + const result = object(await input.runReconciler(request), "reconciler result"); + if ( + result.ok !== true || + result.status !== "succeeded" || + result.mismatchCount !== 0 || + result.checkpointId !== request.checkpointId || + result.checkpointBlockNumber !== request.checkpointBlockNumber || + String(result.checkpointBlockHash).toLowerCase() !== request.checkpointBlockHash + ) { + throw new Error(`reconciler parity failed: ${request.releaseId}`); + } + reconciliations.push(Object.freeze({ + releaseId: request.releaseId, + checkpointId: request.checkpointId, + checkpointBlockNumber: request.checkpointBlockNumber, + checkpointBlockHash: request.checkpointBlockHash, + routeCount: Number(integer(result.routeCount, "reconciler route count", { positive: true })), + mismatchCount: 0, + })); + } + const load = object(await input.runLoadGate(), "load gate result"); + if (load.status !== "accepted" || load.releaseEvidenceAccepted !== true) { + throw new Error("staged load and parity evidence was rejected"); + } + const payload = { + kind: "programmable-post-attestation-staged-gate-evidence", + schemaVersion: 1, + candidateEndpointIdentity: input.candidateEndpointIdentity, + stagedDeploymentId: input.stagedDeploymentId, + productCommit: input.productCommit, + envioProviderDeploymentId: fence.envioProviderDeploymentId, + databasePromotionAttestationCommitment: + fence.promotionAttestationCommitment, + sourceProjectorCycles: sourceCycles + 1, + marketProjectorCycles: marketCycles + 1, + reconciliations: Object.freeze(reconciliations), + loadEvidenceCommitment: bytes32(load.evidenceSha256, "load evidence commitment"), + completedAt: isoTimestamp(input.completedAt(), "staged gate completion"), + }; + return commitEvidence(payload); +} + +export const CUTOVER_RELEASES = RELEASES; diff --git a/scripts/data-pipeline/cutover-phases.test.mjs b/scripts/data-pipeline/cutover-phases.test.mjs new file mode 100644 index 00000000..54882eac --- /dev/null +++ b/scripts/data-pipeline/cutover-phases.test.mjs @@ -0,0 +1,203 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + assertCandidateFence, + checkpointRequestsFromRows, + runFencedRawBackfill, + runPostAttestationStagedGates, +} from "./cutover-phases.mjs"; + +const PROVIDER = "123e4567-e89b-42d3-a456-426614174000"; +const HASH = `0x${"1".repeat(64)}`; +const ATTESTATION = `0x${"2".repeat(64)}`; +const RELEASES = [ + ["classic-v2", "classic"], + ["classic-v3", "classic"], + ["stock-paired-v1", "stock-paired"], + ["stock-paired-v2", "stock-paired"], + ["stock-paired-v3", "stock-paired"], +]; + +function fence(promoted = false) { + return { + databaseMode: "candidate-only", + envioProviderDeploymentId: PROVIDER, + promoted, + publicationCount: promoted ? 5 : 0, + promotionAttestationCommitment: promoted ? ATTESTATION : null, + productCommit: promoted ? "a".repeat(40) : null, + stagedDeploymentId: promoted ? "dpl_12345678901234567890" : null, + }; +} + +function checkpointRows() { + return RELEASES.map(([releaseId, modelId], index) => ({ + chain_id: 1, + release_id: releaseId, + model_id: modelId, + source_group: "core", + epoch_id: `123e4567-e89b-42d3-a456-42661417400${index}`, + pointer_generation: 1, + checkpoint_id: `223e4567-e89b-42d3-a456-42661417400${index}`, + block_number: 25_657_000 + index, + block_hash: HASH, + })); +} + +test("candidate fence rejects any pre-attestation publication", () => { + assert.equal(assertCandidateFence(fence()).promoted, false); + assert.throws( + () => assertCandidateFence({ ...fence(), publicationCount: 1 }), + /fence is not closed/u, + ); + assert.throws( + () => assertCandidateFence(fence(), "attested"), + /not attested/u, + ); +}); + +test("raw backfill reaches idle without opening the publication fence", async () => { + const results = [ + { status: "committed", candidateCount: 200, snapshotBlock: "25657000", generation: "1" }, + { status: "committed", candidateCount: 65, snapshotBlock: "25657100", generation: "2" }, + { status: "idle", candidateCount: 0, snapshotBlock: "25657100" }, + ]; + const output = await runFencedRawBackfill({ + candidateEndpointIdentity: "envio:d7a39a2", + inspectFence: async () => fence(), + runRawCycle: async () => results.shift(), + startedAt: "2026-08-01T08:00:00.000Z", + completedAt: () => "2026-08-01T08:01:00.000Z", + }); + assert.equal(output.candidateCount, 265); + assert.equal(output.cycleCount, 3); + assert.match(output.evidenceSha256, /^0x[0-9a-f]{64}$/u); +}); + +test("raw backfill fails on an unbounded run or a fence transition", async () => { + await assert.rejects( + runFencedRawBackfill({ + candidateEndpointIdentity: "envio:d7a39a2", + inspectFence: async () => fence(), + runRawCycle: async () => ({ + status: "committed", + candidateCount: 1, + snapshotBlock: "1", + generation: "1", + }), + maximumCycles: 2, + startedAt: "2026-08-01T08:00:00.000Z", + completedAt: () => "2026-08-01T08:01:00.000Z", + }), + /did not reach an idle/u, + ); + let inspections = 0; + await assert.rejects( + runFencedRawBackfill({ + candidateEndpointIdentity: "envio:d7a39a2", + inspectFence: async () => (++inspections === 1 ? fence() : fence(true)), + runRawCycle: async () => ({ status: "idle", candidateCount: 0, snapshotBlock: "1" }), + startedAt: "2026-08-01T08:00:00.000Z", + completedAt: () => "2026-08-01T08:01:00.000Z", + }), + /publication fence is not closed/u, + ); +}); + +test("checkpoint payloads cover each exact supported release once", () => { + const requests = checkpointRequestsFromRows(checkpointRows()); + assert.deepEqual(requests.map(({ releaseId }) => releaseId), RELEASES.map(([id]) => id)); + assert.equal(requests[0].maximumEntityCount, 10_000); + assert.throws( + () => checkpointRequestsFromRows([...checkpointRows(), checkpointRows()[0]]), + /incomplete or duplicated/u, + ); + assert.throws( + () => checkpointRequestsFromRows(checkpointRows().slice(1)), + /incomplete or duplicated/u, + ); +}); + +test("post-attestation gates require source, market, every parity and load evidence", async () => { + let sourceCalls = 0; + let marketCalls = 0; + const output = await runPostAttestationStagedGates({ + candidateEndpointIdentity: "envio:d7a39a2", + stagedDeploymentId: "dpl_12345678901234567890", + productCommit: "a".repeat(40), + inspectFence: async () => fence(true), + runSourceProjector: async () => { + sourceCalls += 1; + return sourceCalls === 1 + ? { ok: true, readiness: { status: "progressed", activationReady: false, lagging: true } } + : { ok: true, readiness: { status: "caught-up", activationReady: true, lagging: false } }; + }, + runMarketProjector: async () => { + marketCalls += 1; + return marketCalls === 1 + ? { status: "committed", caughtUp: false, lagBlocks: "1" } + : { status: "idle", caughtUp: true, lagBlocks: "0" }; + }, + readCheckpoints: async () => checkpointRows(), + runReconciler: async (request) => ({ + ok: true, + status: "succeeded", + mismatchCount: 0, + routeCount: request.releaseId === "classic-v2" ? 4 : request.releaseId === "classic-v3" ? 6 : 5, + checkpointId: request.checkpointId, + checkpointBlockNumber: request.checkpointBlockNumber, + checkpointBlockHash: request.checkpointBlockHash, + }), + runLoadGate: async () => ({ + status: "accepted", + releaseEvidenceAccepted: true, + evidenceSha256: HASH, + }), + completedAt: () => "2026-08-01T08:10:00.000Z", + }); + assert.equal(output.reconciliations.length, 5); + assert.equal(output.sourceProjectorCycles, 2); + assert.equal(output.marketProjectorCycles, 2); +}); + +test("post-attestation gates reject a runtime that is not the database-bound deployment", async () => { + await assert.rejects( + runPostAttestationStagedGates({ + candidateEndpointIdentity: "envio:d7a39a2", + stagedDeploymentId: "dpl_09876543210987654321", + productCommit: "a".repeat(40), + inspectFence: async () => fence(true), + }), + /does not match the database deployment binding/u, + ); +}); + +test("post-attestation gates reject one mismatched release", async () => { + await assert.rejects( + runPostAttestationStagedGates({ + candidateEndpointIdentity: "envio:d7a39a2", + stagedDeploymentId: "dpl_12345678901234567890", + productCommit: "a".repeat(40), + inspectFence: async () => fence(true), + runSourceProjector: async () => ({ + ok: true, + readiness: { status: "caught-up", activationReady: true, lagging: false }, + }), + runMarketProjector: async () => ({ status: "idle", caughtUp: true, lagBlocks: "0" }), + readCheckpoints: async () => checkpointRows(), + runReconciler: async (request) => ({ + ok: true, + status: "succeeded", + mismatchCount: request.releaseId === "stock-paired-v2" ? 1 : 0, + routeCount: 5, + checkpointId: request.checkpointId, + checkpointBlockNumber: request.checkpointBlockNumber, + checkpointBlockHash: request.checkpointBlockHash, + }), + runLoadGate: async () => ({ status: "accepted", releaseEvidenceAccepted: true, evidenceSha256: HASH }), + completedAt: () => "2026-08-01T08:10:00.000Z", + }), + /stock-paired-v2/u, + ); +}); diff --git a/scripts/data-pipeline/cutover-runtime.mjs b/scripts/data-pipeline/cutover-runtime.mjs new file mode 100644 index 00000000..dc1c929e --- /dev/null +++ b/scripts/data-pipeline/cutover-runtime.mjs @@ -0,0 +1,276 @@ +import { execFile } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +import { createServer } from "vite"; + +import { createBootstrapPlan } from "./hosted-db-bootstrap-runtime.mjs"; +import { loadEnvioCutoverIdentity } from "./cutover-envio.mjs"; +import { runFencedRawBackfill } from "./cutover-phases.mjs"; + +const run = promisify(execFile); +const workspace = fileURLToPath(new URL("../../", import.meta.url)); +const CUTOVER_CANDIDATES_PER_COMMIT = 4_096; +const CANDIDATE_ENDPOINT = + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql"; + +async function gitCommit() { + const { stdout } = await run("git", ["rev-parse", "HEAD"], { cwd: workspace }); + const commit = stdout.trim(); + if (!/^[0-9a-f]{40}$/u.test(commit)) throw new Error("repository commit is invalid"); + return commit; +} + +export async function loadCandidateRuntimeIdentity() { + const identity = await loadEnvioCutoverIdentity({ workspace }); + const candidate = identity.candidate; + if ( + candidate.endpoint !== CANDIDATE_ENDPOINT || + candidate.endpointId !== "d7a39a2" || + candidate.deploymentLabel !== "production-7f24e63" || + candidate.mirrorCommit !== "7ffd15c2a28c481a2d3632e30b315262c2471b2e" + ) { + throw new Error("reviewed Envio candidate identity is invalid"); + } + return Object.freeze({ + endpoint: candidate.endpoint, + endpointId: candidate.endpointId, + mirrorCommit: candidate.mirrorCommit, + redactedIdentity: `envio:${candidate.deploymentLabel}`, + }); +} + +async function withRuntimeModules(operation) { + const vite = await createServer({ + root: workspace, + configFile: false, + appType: "custom", + logLevel: "silent", + server: { middlewareMode: true }, + ssr: { noExternal: ["server-only"] }, + plugins: [ + { + name: "candidate-raw-backfill-server-only-boundary", + enforce: "pre", + resolveId(id) { + return id === "server-only" ? "\0cutover-server-only" : null; + }, + load(id) { + return id === "\0cutover-server-only" ? "export {};" : null; + }, + }, + ], + }); + try { + const [ + postgresModule, + projectorStoreModule, + leaseModule, + envioModule, + projectorModule, + dualRpcModule, + rpcModule, + connectionModule, + codecsModule, + ] = await Promise.all([ + vite.ssrLoadModule("/lib/data-pipeline/postgres.ts"), + vite.ssrLoadModule("/lib/data-pipeline/postgres-projector.ts"), + vite.ssrLoadModule("/lib/data-pipeline/projector-runtime-lease.server.ts"), + vite.ssrLoadModule("/lib/data-pipeline/envio.ts"), + vite.ssrLoadModule("/lib/data-pipeline/projector.ts"), + vite.ssrLoadModule("/lib/data-pipeline/dual-rpc.ts"), + vite.ssrLoadModule("/lib/data-pipeline/rpc-providers.server.ts"), + vite.ssrLoadModule("/lib/data-pipeline/postgres-connection.server.ts"), + vite.ssrLoadModule("/lib/data-pipeline/codecs.ts"), + ]); + return await operation({ + postgresModule, + projectorStoreModule, + leaseModule, + envioModule, + projectorModule, + dualRpcModule, + rpcModule, + connectionModule, + codecsModule, + }); + } finally { + await vite.close(); + } +} + +export function candidateGenesisAnchorBlock(bootstrap) { + const starts = bootstrap?.releases?.flatMap((release) => + release?.sourceBindings?.map(({ inclusiveStartBlock }) => { + const value = String(inclusiveStartBlock ?? ""); + if (!/^[1-9][0-9]*$/u.test(value)) { + throw new Error("candidate release start block is invalid"); + } + return BigInt(value); + }) ?? [] + ); + if (!Array.isArray(starts) || starts.length < 1) { + throw new Error("candidate release start blocks are unavailable"); + } + const first = starts.reduce( + (minimum, current) => current < minimum ? current : minimum, + ); + if (first < 1n) throw new Error("candidate genesis anchor is invalid"); + return (first - 1n).toString(); +} + +export async function withCandidateRuntimeLease(input) { + const acquired = await input.lease.tryAcquire(); + if (acquired.status !== "acquired" || !acquired.fence) { + throw new Error("candidate raw backfill lease is busy"); + } + let operationFailed = true; + try { + const result = await input.operation(acquired.fence); + operationFailed = false; + return result; + } finally { + const released = await input.lease + .release(acquired.fence) + .catch(() => false); + if (!released && !operationFailed) { + throw new Error("candidate raw backfill lease release failed"); + } + } +} + +export async function runConfiguredCandidateRawBackfill(input) { + const environment = input.environment ?? process.env; + const identity = await loadCandidateRuntimeIdentity(); + const commit = await gitCommit(); + const bootstrap = await createBootstrapPlan({ + repositoryCommit: commit, + environment, + }); + if ( + bootstrap.execution?.ready !== true || + bootstrap.execution?.targetDatabaseMode !== "candidate-only" || + bootstrap.candidateIsolation?.candidateEnvioIdentity !== identity.redactedIdentity + ) { + throw new Error("candidate bootstrap is not executable"); + } + return withRuntimeModules(async (modules) => { + const sslCaPem = modules.connectionModule.validatedPostgresSslCa( + environment.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + ); + const writerConnection = + modules.connectionModule.validatedPostgresConnectionString( + environment.PROGRAMMABLE_PROJECTOR_DATABASE_URL, + ); + const runtimeConnection = + modules.connectionModule.validatedPostgresConnectionString( + environment.PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_URL, + ); + const providers = modules.rpcModule.createProductionDualRpcProviders(environment); + modules.rpcModule.assertProductionDualRpcProviders(providers); + const reviewedRpcBindings = bootstrap.providerBindings.filter( + ({ providerType }) => providerType === "rpc_provider", + ); + const providerBindings = Object.freeze( + bootstrap.providerBindings + .filter(({ providerType }) => providerType !== "uniswap_subgraph") + .map((provider) => Object.freeze({ + type: provider.providerType, + redactedIdentity: provider.redactedIdentity, + deploymentCommitment: provider.deploymentCommitment, + schemaCommitment: provider.schemaCommitment, + })), + ); + if ( + providerBindings.length !== 3 || + providerBindings[0]?.redactedIdentity !== identity.redactedIdentity || + reviewedRpcBindings[0]?.endpointUrlCommitment !== providers[0].endpointCommitment || + reviewedRpcBindings[1]?.endpointUrlCommitment !== providers[1].endpointCommitment + ) { + throw new Error("candidate provider bindings do not match runtime providers"); + } + const releaseScopes = Object.freeze( + bootstrap.releases.map(({ scope }) => Object.freeze({ + releaseId: scope.releaseId, + modelId: scope.modelId, + sourceGroup: scope.sourceGroup, + })), + ); + const anchorBlockNumber = candidateGenesisAnchorBlock(bootstrap); + const runtimeExecutor = modules.postgresModule.createPostgresExecutor({ + connectionString: runtimeConnection, + sslCaPem, + maxConnections: 1, + connectTimeoutMs: 2_000, + idleTimeoutMs: 60_000, + }); + const writerExecutor = modules.postgresModule.createPostgresExecutor({ + connectionString: writerConnection, + sslCaPem, + maxConnections: 1, + connectTimeoutMs: 2_000, + idleTimeoutMs: 60_000, + }); + const lease = modules.leaseModule.createProjectorRuntimeLeaseController({ + executor: runtimeExecutor, + }); + try { + await withCandidateRuntimeLease({ + lease, + operation: async (runtimeFence) => { + const anchorBlock = await providers[0].client.getBlock({ + blockNumber: BigInt(anchorBlockNumber), + }); + const anchorBlockHash = modules.codecsModule.canonicalBytes32( + anchorBlock?.hash, + ); + const safeHead = await modules.dualRpcModule.readDualRpcSafeHead({ + providers, + cursor: { blockNumber: anchorBlockNumber, blockHash: anchorBlockHash }, + }); + await modules.projectorStoreModule.initializePostgresProjectorGenesis({ + executor: writerExecutor, + providers: providerBindings, + releaseScopes, + runtimeFence, + evidence: { anchorBlockNumber, anchorBlockHash, safeHead }, + }); + }, + }); + const envio = modules.envioModule.createEnvioClient({ + endpoint: identity.endpoint, + token: environment.PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN || undefined, + }); + return await runFencedRawBackfill({ + candidateEndpointIdentity: "envio:d7a39a2", + inspectFence: input.inspectFence, + runRawCycle: () => withCandidateRuntimeLease({ + lease, + operation: (runtimeFence) => { + const store = modules.projectorStoreModule.createPostgresProjectorStore({ + executor: writerExecutor, + providers: providerBindings, + releaseScopes, + runtimeFence, + }); + return modules.projectorModule.runProjectorCycle({ + store, + envio, + providers, + deadlineMs: 75_000, + preferredCandidatesPerCommit: + CUTOVER_CANDIDATES_PER_COMMIT, + }); + }, + }), + maximumCycles: input.maximumCycles, + startedAt: input.startedAt, + completedAt: input.completedAt, + }); + } finally { + await Promise.allSettled([writerExecutor.close(), runtimeExecutor.close()]); + } + }); +} + +export const CANDIDATE_RUNTIME_ENDPOINT = CANDIDATE_ENDPOINT; diff --git a/scripts/data-pipeline/cutover-runtime.test.mjs b/scripts/data-pipeline/cutover-runtime.test.mjs new file mode 100644 index 00000000..67eb1957 --- /dev/null +++ b/scripts/data-pipeline/cutover-runtime.test.mjs @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + CANDIDATE_RUNTIME_ENDPOINT, + candidateGenesisAnchorBlock, + loadCandidateRuntimeIdentity, + withCandidateRuntimeLease, +} from "./cutover-runtime.mjs"; + +test("raw runtime is pinned to the reviewed candidate evidence", async () => { + const identity = await loadCandidateRuntimeIdentity(); + assert.deepEqual(identity, { + endpoint: "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + endpointId: "d7a39a2", + mirrorCommit: "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + redactedIdentity: "envio:production-7f24e63", + }); + assert.equal(CANDIDATE_RUNTIME_ENDPOINT, identity.endpoint); + assert.equal(Object.isFrozen(identity), true); +}); + +test("candidate genesis is the predecessor of the earliest reviewed source", () => { + assert.equal(candidateGenesisAnchorBlock({ + releases: [ + { sourceBindings: [{ inclusiveStartBlock: "25639538" }] }, + { + sourceBindings: [ + { inclusiveStartBlock: "25624131" }, + { inclusiveStartBlock: "25624130" }, + ], + }, + ], + }), "25624129"); + assert.throws( + () => candidateGenesisAnchorBlock({ releases: [] }), + /start blocks are unavailable/u, + ); +}); + +test("raw backfill leases one bounded projector cycle at a time", async () => { + let generation = 0; + const operations = []; + const releases = []; + const lease = { + async tryAcquire() { + generation += 1; + return { + status: "acquired", + fence: { + holderId: `projector-runtime-00000000-0000-4000-8000-${String(generation).padStart(12, "0")}`, + generation: String(generation), + tokenHash: `0x${String(generation).padStart(64, "0")}`, + }, + }; + }, + async release(fence) { + releases.push(fence.generation); + return true; + }, + }; + + for (let index = 0; index < 3; index += 1) { + await withCandidateRuntimeLease({ + lease, + operation: async (fence) => { + operations.push(fence.generation); + }, + }); + } + + assert.deepEqual(operations, ["1", "2", "3"]); + assert.deepEqual(releases, ["1", "2", "3"]); +}); + +test("raw backfill releases a lease when a cycle fails", async () => { + let released = false; + const lease = { + async tryAcquire() { + return { + status: "acquired", + fence: { + holderId: "projector-runtime-00000000-0000-4000-8000-000000000001", + generation: "1", + tokenHash: `0x${"11".repeat(32)}`, + }, + }; + }, + async release() { + released = true; + return true; + }, + }; + + await assert.rejects( + withCandidateRuntimeLease({ + lease, + operation: async () => { + throw new Error("cycle failed"); + }, + }), + /cycle failed/u, + ); + assert.equal(released, true); +}); diff --git a/scripts/data-pipeline/hosted-db-bootstrap-runtime.mjs b/scripts/data-pipeline/hosted-db-bootstrap-runtime.mjs new file mode 100644 index 00000000..f126f705 --- /dev/null +++ b/scripts/data-pipeline/hosted-db-bootstrap-runtime.mjs @@ -0,0 +1,288 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +import { createServer } from "vite"; + +import { buildReviewedBootstrapPlan } from "./bootstrap-evidence.mjs"; +import { canonicalJson, sha256 } from "./hosted-db-operator-core.mjs"; + +const workspace = fileURLToPath(new URL("../../", import.meta.url)); +const RELEASE_BINDING_PATH = "config/data-pipeline-release.v1.json"; +const BOOTSTRAP_CATALOG_PATH = "config/data-pipeline-bootstrap.v1.json"; +const CANDIDATE_ENVIO_PATH = + "config/data-pipeline-envio-candidate.v1.json"; +const NONZERO_BYTES32 = /^0x(?!0{64}$)[0-9a-f]{64}$/u; + +function exactKeys(value, expected, label) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + canonicalJson(Object.keys(value).sort()) !== + canonicalJson(expected.slice().sort()) + ) { + throw new Error(`${label} shape is invalid`); + } +} + +function parseObject(bytes, label) { + let value; + try { + value = JSON.parse(bytes.toString("utf8")); + } catch { + throw new Error(`${label} is not valid JSON`); + } + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${label} must be an object`); + } + return value; +} + +async function readRepositoryFile(relativePath) { + return readFile(new URL(`../../${relativePath}`, import.meta.url)); +} + +function candidateEnvioEvidence(value, bytes) { + exactKeys( + value, + [ + "schemaVersion", + "status", + "deploymentLabel", + "graphqlEndpoint", + "sourceCommit", + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "eventCount", + "redactedIdentity", + "deploymentCommitment", + "schemaCommitment", + "audit", + "policy", + ], + "candidate Envio evidence", + ); + exactKeys( + value.audit, + [ + "entityCount", + "entityCounts", + "coordinatorRepairCount", + "baselineEvidenceSha256", + "candidateAuditEvidenceSha256", + ], + "candidate Envio audit evidence", + ); + exactKeys( + value.audit.entityCounts, + ["ClassicLaunch", "ClassicPool", "StockLaunch", "StockPool", "Token"], + "candidate Envio entity counts", + ); + exactKeys( + value.policy, + [ + "databaseMode", + "legacyProductionDeploymentRegistered", + "publicationAllowedBeforePromotion", + "promotion", + ], + "candidate Envio policy evidence", + ); + const commitments = [ + "configSha256", + "schemaSha256", + "handlerSha256", + "sourceRegistrySha256", + "eventSetSha256", + "deploymentCommitment", + "schemaCommitment", + ]; + if ( + value.schemaVersion !== 1 || + value.status !== "deployed-synced-audited-not-promoted" || + !/^[a-z0-9][a-z0-9-]{0,127}$/u.test(value.deploymentLabel ?? "") || + value.redactedIdentity !== `envio:${value.deploymentLabel}` || + !/^https:\/\/indexer\.hyperindex\.xyz\/[a-z0-9]{7,64}\/v1\/graphql$/u.test( + value.graphqlEndpoint ?? "", + ) || + !/^[0-9a-f]{40}$/u.test(value.sourceCommit ?? "") || + commitments.some((field) => !NONZERO_BYTES32.test(value[field] ?? "")) || + !Number.isSafeInteger(value.eventCount) || + value.eventCount < 1 || + !Number.isSafeInteger(value.audit.entityCount) || + value.audit.entityCount < 1 || + !Number.isSafeInteger(value.audit.coordinatorRepairCount) || + value.audit.coordinatorRepairCount < 0 || + Object.values(value.audit.entityCounts).some( + (count) => !Number.isSafeInteger(count) || count < 0, + ) || + Object.values(value.audit.entityCounts).reduce( + (total, count) => total + count, + 0, + ) !== value.audit.entityCount || + !NONZERO_BYTES32.test(value.audit.baselineEvidenceSha256 ?? "") || + !NONZERO_BYTES32.test(value.audit.candidateAuditEvidenceSha256 ?? "") || + value.policy.databaseMode !== "candidate-only" || + value.policy.legacyProductionDeploymentRegistered !== false || + value.policy.publicationAllowedBeforePromotion !== false || + value.policy.promotion !== "atomic-attestation-required" + ) { + throw new Error("candidate Envio evidence is incomplete"); + } + return Object.freeze({ + path: CANDIDATE_ENVIO_PATH, + fileSha256: sha256(bytes), + status: value.status, + deploymentLabel: value.deploymentLabel, + graphqlEndpoint: value.graphqlEndpoint, + schemaVersion: String(value.schemaVersion), + sourceCommit: value.sourceCommit, + configSha256: value.configSha256, + schemaSha256: value.schemaSha256, + handlerSha256: value.handlerSha256, + sourceRegistrySha256: value.sourceRegistrySha256, + eventSetSha256: value.eventSetSha256, + eventCount: value.eventCount, + redactedIdentity: value.redactedIdentity, + deploymentCommitment: value.deploymentCommitment, + schemaCommitment: value.schemaCommitment, + auditEvidenceCommitment: sha256( + `programmable:data-pipeline:envio-candidate-audit:v1\0${canonicalJson( + value.audit, + )}`, + ), + policyCommitment: sha256( + `programmable:data-pipeline:envio-candidate-policy:v1\0${canonicalJson( + value.policy, + )}`, + ), + }); +} + +async function withRuntimeModules(run) { + const vite = await createServer({ + root: workspace, + configFile: false, + appType: "custom", + logLevel: "silent", + server: { middlewareMode: true }, + ssr: { noExternal: ["server-only"] }, + plugins: [ + { + name: "hosted-db-bootstrap-server-only-boundary", + enforce: "pre", + resolveId(id) { + return id === "server-only" ? "\0operator-server-only" : null; + }, + load(id) { + return id === "\0operator-server-only" ? "export {};" : null; + }, + }, + ], + }); + try { + const [ + releaseModule, + rpcModule, + marketModule, + eventModule, + foldModule, + ] = await Promise.all([ + vite.ssrLoadModule("/lib/data-pipeline/release-binding.server.ts"), + vite.ssrLoadModule("/lib/data-pipeline/rpc-providers.server.ts"), + vite.ssrLoadModule( + "/lib/data-pipeline/market-projector-runtime.server.ts", + ), + vite.ssrLoadModule("/lib/data-pipeline/event-manifest.ts"), + vite.ssrLoadModule("/lib/data-pipeline/projector-fold.ts"), + ]); + return await run({ + releaseModule, + rpcModule, + marketModule, + eventModule, + foldModule, + }); + } finally { + await vite.close(); + } +} + +export async function createBootstrapPlan({ + repositoryCommit, + environment = process.env, + createdAt, +}) { + const [bindingBytes, catalogBytes, candidateBytes] = await Promise.all([ + readRepositoryFile(RELEASE_BINDING_PATH), + readRepositoryFile(BOOTSTRAP_CATALOG_PATH), + readRepositoryFile(CANDIDATE_ENVIO_PATH), + ]); + const catalog = parseObject(catalogBytes, "bootstrap semantic catalog"); + const candidate = candidateEnvioEvidence( + parseObject(candidateBytes, "candidate Envio evidence"), + candidateBytes, + ); + const planCreatedAt = createdAt ?? catalog.createdAt; + + return withRuntimeModules(async ({ + releaseModule, + rpcModule, + marketModule, + eventModule, + foldModule, + }) => { + const binding = releaseModule.getDataPipelineReleaseBinding(); + const rpcProviders = rpcModule.createProductionDualRpcProviders(environment); + const rpcCommitments = + rpcModule.productionRpcProjectorCommitments(environment); + const providers = [ + Object.freeze({ + providerType: "envio_deployment", + redactedIdentity: candidate.redactedIdentity, + deploymentCommitment: candidate.deploymentCommitment, + schemaCommitment: candidate.schemaCommitment, + }), + ...rpcProviders.map((provider) => + Object.freeze({ + providerType: "rpc_provider", + redactedIdentity: `rpc:1:${provider.vendorGroup}`, + vendor: provider.vendorGroup, + chainId: 1, + constructorVersion: "rpc-provider-v1", + endpointUrlCommitment: provider.endpointCommitment, + endpointOriginCommitment: provider.endpointOriginCommitment, + endpointEvidenceDomain: "rpc-endpoint-commitments-v1", + deploymentCommitment: + rpcCommitments[provider.vendorGroup].deploymentCommitment, + schemaCommitment: + rpcCommitments[provider.vendorGroup].schemaCommitment, + }), + ), + Object.freeze({ + providerType: "uniswap_subgraph", + redactedIdentity: + `uniswap-v4:ethereum:${binding.uniswapV4Subgraph.deployment}`, + deploymentCommitment: + marketModule.MARKET_GRAPH_DEPLOYMENT_COMMITMENT, + schemaCommitment: marketModule.MARKET_GRAPH_SCHEMA_COMMITMENT, + subgraphId: binding.uniswapV4Subgraph.subgraphId, + deployment: binding.uniswapV4Subgraph.deployment, + }), + ]; + return buildReviewedBootstrapPlan({ + workspace, + repositoryCommit, + binding, + bindingSha256: sha256(bindingBytes), + providers, + eventSignatures: eventModule.PROGRAMMABLE_EVENT_SIGNATURES, + projectionRules: foldModule.projectorFoldProjectionRules(), + createdAt: planCreatedAt, + candidateEnvioEvidence: candidate, + }); + }); +} diff --git a/scripts/data-pipeline/hosted-db-bootstrap.test.mjs b/scripts/data-pipeline/hosted-db-bootstrap.test.mjs new file mode 100644 index 00000000..86375c6d --- /dev/null +++ b/scripts/data-pipeline/hosted-db-bootstrap.test.mjs @@ -0,0 +1,206 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createBootstrapPlan } from "./hosted-db-bootstrap-runtime.mjs"; +import { validateReviewedBootstrapPlan } from "./bootstrap-evidence.mjs"; +import { canonicalJson, sha256 } from "./hosted-db-operator-core.mjs"; + +const repositoryCommit = "561abe6a36caa0e9b5bc4ea20d10edca0f5401bc"; +const createdAt = "2026-08-01T09:00:00.000Z"; +const environment = Object.freeze({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: + "https://eth-mainnet.g.alchemy.com/v2/abcdefgh", + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: + "https://example.quiknode.pro/abcdefgh", +}); + +test("builds a complete deterministic candidate-only bootstrap plan", async () => { + const left = await createBootstrapPlan({ + repositoryCommit, + environment, + createdAt, + }); + const right = await createBootstrapPlan({ + repositoryCommit, + environment, + createdAt, + }); + assert.deepEqual(left, right); + assert.equal(validateReviewedBootstrapPlan(left), left); + assert.equal(left.execution.ready, true); + assert.equal(left.execution.targetDatabaseMode, "candidate-only"); + assert.equal( + left.providerBindings[0].redactedIdentity, + "envio:production-7f24e63", + ); + assert.equal( + left.candidateIsolation.canonicalReleaseEnvioIdentity, + left.providerBindings[0].redactedIdentity, + ); + assert.equal( + left.candidateIsolation.canonicalReleaseEnvioEndpoint, + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + ); + assert.equal(left.candidateIsolation.legacyProductionDeploymentRegistered, false); + assert.equal(left.releases.length, 5); + assert.equal( + JSON.stringify(left).includes("unresolved"), + false, + ); + const classic = left.releases.find( + ({ scope }) => scope.releaseId === "classic-v3", + ); + assert.ok(classic); + assert.equal( + classic.sourceBindings.find( + ({ sourceName }) => sourceName === "ClassicV3RewardVaultFactory", + )?.sourceRole, + "vault_factory", + ); + const stock = left.releases.find( + ({ scope }) => scope.releaseId === "stock-paired-v3", + ); + assert.ok(stock); + const stockTemplate = stock.dynamicSourceTemplates[0]; + assert.equal(stockTemplate.immutableBindingSpec.factoryConfigurationField, null); + assert.deepEqual( + [...new Set( + stockTemplate.immutableBindingSpec.bindings + .filter(({ source }) => source === "deferred_allocation_evidence") + .map(({ evidenceRole }) => evidenceRole), + )].sort(), + ["beneficiary_count", "configuration_hash"], + ); + for (const release of left.releases) { + assert.equal( + new Set(release.projectionEventRules.map( + ({ sourceRole, eventType }) => `${sourceRole}\0${eventType}`, + )).size, + release.projectionEventRules.length, + ); + } +}); + +test("rejects a bootstrap plan whose canonical release is not the candidate", async () => { + const plan = await createBootstrapPlan({ + repositoryCommit, + environment, + createdAt, + }); + const identity = structuredClone(plan); + identity.candidateIsolation.canonicalReleaseEnvioIdentity = + "envio:production-legacy"; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(identity)), + /candidate database isolation is invalid/u, + ); + + const endpoint = structuredClone(plan); + endpoint.candidateIsolation.canonicalReleaseEnvioEndpoint = + "https://indexer.hyperindex.xyz/legacy1/v1/graphql"; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(endpoint)), + /candidate initialization input drifted/u, + ); +}); + +function recommit(plan) { + const payload = structuredClone(plan); + delete payload.planSha256; + plan.planSha256 = sha256(canonicalJson(payload)); + return plan; +} + +test("rejects semantic role drift even after the outer plan is recommitted", async () => { + const plan = await createBootstrapPlan({ + repositoryCommit, + environment, + createdAt, + }); + const changed = structuredClone(plan); + changed.releases[0].sourceBindings[0].sourceRole = "wrong"; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(changed)), + /source commitment drifted/u, + ); +}); + +test("rejects recovery selector, ABI and creation-code evidence drift", async () => { + const plan = await createBootstrapPlan({ + repositoryCommit, + environment, + createdAt, + }); + const selector = structuredClone(plan); + selector.releases[0].sourceBindings[1].recoverySelector = "0x00000000"; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(selector)), + /source commitment drifted/u, + ); + + const abi = structuredClone(plan); + abi.releases[0].sourceBindings[0].abiEventSetCommitment = + `0x${"11".repeat(32)}`; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(abi)), + /source commitment drifted/u, + ); + + const creation = structuredClone(plan); + creation.releases[1].dynamicSourceTemplates[0] + .deployedArtifactCreationCodeHash = `0x${"22".repeat(32)}`; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(creation)), + /creation-code evidence drifted/u, + ); +}); + +test("rejects dynamic lineage, RPC endpoint and activation input drift", async () => { + const plan = await createBootstrapPlan({ + repositoryCommit, + environment, + createdAt, + }); + const dynamic = structuredClone(plan); + dynamic.releases[1].dynamicSourceTemplates[0] + .parentFactoryReleaseBindingId = + dynamic.releases[1].sourceBindings[1].bindingId; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(dynamic)), + /dynamic source identity is invalid/u, + ); + + const rpc = structuredClone(plan); + rpc.providerBindings[1].endpointUrlCommitment = + `0x${"33".repeat(32)}`; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(rpc)), + /provider deterministic identity drifted/u, + ); + + const activation = structuredClone(plan); + activation.releases[0].activation.nextGeneration = "2"; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(activation)), + /activation input drifted/u, + ); +}); + +test("rejects release replay across canonical scopes", async () => { + const plan = await createBootstrapPlan({ + repositoryCommit, + environment, + createdAt, + }); + const replay = structuredClone(plan); + [replay.releases[0], replay.releases[1]] = [ + replay.releases[1], + replay.releases[0], + ]; + replay.releases[0].ordinal = 1; + replay.releases[1].ordinal = 2; + assert.throws( + () => validateReviewedBootstrapPlan(recommit(replay)), + /release identity is invalid/u, + ); +}); diff --git a/scripts/data-pipeline/hosted-db-operator-core.mjs b/scripts/data-pipeline/hosted-db-operator-core.mjs new file mode 100644 index 00000000..eeedcb70 --- /dev/null +++ b/scripts/data-pipeline/hosted-db-operator-core.mjs @@ -0,0 +1,462 @@ +import { createHash } from "node:crypto"; +import { lstat, readFile, readdir, realpath } from "node:fs/promises"; +import path from "node:path"; + +const MIGRATION_FILE = /^(\d{14})_([a-z][a-z0-9_]*)\.sql$/u; +const HEX_SHA256 = /^0x[0-9a-f]{64}$/u; +const GIT_COMMIT = /^[0-9a-f]{40}$/u; +const PROJECT_REF = /^[a-z0-9]{20}$/u; + +export const MIGRATION_PLAN_KIND = + "programmable-hosted-db-migration-plan"; +export const BOOTSTRAP_PLAN_KIND = + "programmable-data-pipeline-bootstrap-plan"; + +function isPlainObject(value) { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +export function canonicalJson(value) { + if (Array.isArray(value)) { + return `[${value.map((item) => canonicalJson(item)).join(",")}]`; + } + if (isPlainObject(value)) { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`) + .join(",")}}`; + } + if ( + value === null || + typeof value === "string" || + typeof value === "boolean" || + (typeof value === "number" && Number.isFinite(value)) + ) { + return JSON.stringify(value); + } + throw new Error("value is not canonical JSON"); +} + +export function sha256(value) { + return `0x${createHash("sha256").update(value).digest("hex")}`; +} + +function migrationPlanPayload(plan) { + return { + kind: plan.kind, + schemaVersion: plan.schemaVersion, + repositoryCommit: plan.repositoryCommit, + migrationRoot: plan.migrationRoot, + migrationCount: plan.migrationCount, + orderSha256: plan.orderSha256, + migrations: plan.migrations, + }; +} + +function migrationOrderCommitment(migrations) { + return sha256( + migrations + .map( + ({ ordinal, version, name, file, fileSha256, bytes }) => + `${ordinal}\0${version}\0${name}\0${file}\0${fileSha256}\0${bytes}`, + ) + .join("\n"), + ); +} + +export async function discoverMigrationPlan({ + workspace, + repositoryCommit, + migrationRoot = "supabase/migrations", +}) { + if (!GIT_COMMIT.test(repositoryCommit)) { + throw new Error("repository commit must be an exact full commit hash"); + } + const root = path.resolve(workspace, migrationRoot); + const workspacePath = await realpath(workspace); + const rootPath = await realpath(root); + if ( + rootPath !== workspacePath && + !rootPath.startsWith(`${workspacePath}${path.sep}`) + ) { + throw new Error("migration root escapes the repository"); + } + + const entries = await readdir(rootPath, { withFileTypes: true }); + const migrationNames = entries + .filter(({ name }) => name.endsWith(".sql")) + .map(({ name }) => name) + .sort(); + if (migrationNames.length === 0) { + throw new Error("no migration files were found"); + } + + const migrations = []; + const versions = new Set(); + for (const [index, fileName] of migrationNames.entries()) { + const match = MIGRATION_FILE.exec(fileName); + if (!match) { + throw new Error(`noncanonical migration file: ${fileName}`); + } + const [, version, name] = match; + if (versions.has(version)) { + throw new Error(`duplicate migration version: ${version}`); + } + versions.add(version); + const absolutePath = path.join(rootPath, fileName); + const metadata = await lstat(absolutePath); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error(`migration must be a regular file: ${fileName}`); + } + const contents = await readFile(absolutePath); + if (contents.byteLength === 0) { + throw new Error(`migration must not be empty: ${fileName}`); + } + migrations.push({ + ordinal: index + 1, + version, + name, + file: path.posix.join(migrationRoot, fileName), + fileSha256: sha256(contents), + bytes: contents.byteLength, + }); + } + + const plan = { + kind: MIGRATION_PLAN_KIND, + schemaVersion: 1, + repositoryCommit, + migrationRoot, + migrationCount: migrations.length, + orderSha256: migrationOrderCommitment(migrations), + migrations, + }; + return { + ...plan, + planSha256: sha256(canonicalJson(migrationPlanPayload(plan))), + }; +} + +export function validateMigrationPlan(value) { + if ( + !isPlainObject(value) || + value.kind !== MIGRATION_PLAN_KIND || + value.schemaVersion !== 1 || + !GIT_COMMIT.test(value.repositoryCommit ?? "") || + value.migrationRoot !== "supabase/migrations" || + !Number.isSafeInteger(value.migrationCount) || + value.migrationCount <= 0 || + !HEX_SHA256.test(value.orderSha256 ?? "") || + !HEX_SHA256.test(value.planSha256 ?? "") || + !Array.isArray(value.migrations) || + value.migrations.length !== value.migrationCount + ) { + throw new Error("migration plan is invalid"); + } + const versions = new Set(); + for (const [index, migration] of value.migrations.entries()) { + if (!isPlainObject(migration)) throw new Error("migration plan is invalid"); + const fileName = path.posix.basename(migration.file ?? ""); + const match = MIGRATION_FILE.exec(fileName); + if ( + !match || + migration.file !== path.posix.join(value.migrationRoot, fileName) || + migration.ordinal !== index + 1 || + migration.version !== match[1] || + migration.name !== match[2] || + versions.has(migration.version) || + !HEX_SHA256.test(migration.fileSha256 ?? "") || + !Number.isSafeInteger(migration.bytes) || + migration.bytes <= 0 + ) { + throw new Error("migration plan is invalid"); + } + versions.add(migration.version); + } + if (migrationOrderCommitment(value.migrations) !== value.orderSha256) { + throw new Error("migration order commitment does not match"); + } + if ( + sha256(canonicalJson(migrationPlanPayload(value))) !== value.planSha256 + ) { + throw new Error("migration plan commitment does not match"); + } + return value; +} + +export function validateDirectSupabaseTarget(rawUrl, expectedProjectRef) { + if (!PROJECT_REF.test(expectedProjectRef ?? "")) { + throw new Error("expected Supabase project ref is invalid"); + } + if (typeof rawUrl !== "string" || rawUrl.length < 1 || rawUrl.length > 2048) { + throw new Error("migrator database URL is required"); + } + let parsed; + try { + parsed = new URL(rawUrl); + } catch { + throw new Error("migrator database URL is invalid"); + } + const parameters = [...parsed.searchParams.entries()]; + if ( + !["postgres:", "postgresql:"].includes(parsed.protocol) || + parsed.hostname !== `db.${expectedProjectRef}.supabase.co` || + parsed.port !== "5432" || + parsed.pathname !== "/postgres" || + parsed.username !== "postgres" || + parsed.password.length < 1 || + parsed.hash !== "" || + parameters.length !== 1 || + parameters[0][0] !== "sslmode" || + parameters[0][1] !== "verify-full" + ) { + throw new Error( + "migrator target must be the expected direct Supabase endpoint on port 5432 with sslmode=verify-full", + ); + } + return Object.freeze({ + projectRef: expectedProjectRef, + host: parsed.hostname, + port: 5432, + database: "postgres", + sslMode: "verify-full", + }); +} + +export function compareMigrationHistory({ + plan, + historyRows, + evidenceRows, + evidenceTablePresent, +}) { + validateMigrationPlan(plan); + if (!Array.isArray(historyRows) || !Array.isArray(evidenceRows)) { + throw new Error("migration history response is invalid"); + } + const localByVersion = new Map( + plan.migrations.map((migration) => [migration.version, migration]), + ); + const evidenceByVersion = new Map(); + for (const evidence of evidenceRows) { + if ( + !isPlainObject(evidence) || + typeof evidence.version !== "string" || + evidenceByVersion.has(evidence.version) + ) { + throw new Error("migration evidence is invalid"); + } + evidenceByVersion.set(evidence.version, evidence); + } + if (evidenceRows.length > 0 && !evidenceTablePresent) { + throw new Error("migration evidence table state is invalid"); + } + + const applied = []; + for (const [index, row] of historyRows.entries()) { + if (!isPlainObject(row) || typeof row.version !== "string") { + throw new Error("migration history is invalid"); + } + const expected = plan.migrations[index]; + if (!expected || row.version !== expected.version) { + throw new Error("remote migration history is not an exact local prefix"); + } + if (row.name !== expected.name) { + throw new Error(`remote migration name mismatch: ${row.version}`); + } + if (!Array.isArray(row.statements) || row.statements.length === 0) { + throw new Error(`remote migration statements are missing: ${row.version}`); + } + const evidence = evidenceByVersion.get(row.version); + if (!evidence) { + throw new Error(`file evidence is missing for migration: ${row.version}`); + } + if ( + evidence.name !== expected.name || + evidence.file_name !== path.posix.basename(expected.file) || + Number(evidence.ordinal) !== expected.ordinal || + evidence.file_sha256 !== expected.fileSha256 || + !HEX_SHA256.test(evidence.plan_sha256 ?? "") || + !GIT_COMMIT.test(evidence.repository_commit ?? "") + ) { + throw new Error(`file evidence mismatch for migration: ${row.version}`); + } + applied.push(expected.version); + } + for (const version of evidenceByVersion.keys()) { + if (!localByVersion.has(version) || !applied.includes(version)) { + throw new Error(`orphan migration evidence: ${version}`); + } + } + return Object.freeze({ + status: historyRows.length === plan.migrations.length ? "current" : "pending", + appliedCount: applied.length, + pending: plan.migrations.slice(applied.length).map(({ version, file }) => ({ + version, + file, + })), + }); +} + +function assertProvider(provider) { + if ( + !isPlainObject(provider) || + typeof provider.providerType !== "string" || + typeof provider.redactedIdentity !== "string" || + !HEX_SHA256.test(provider.deploymentCommitment ?? "") || + !HEX_SHA256.test(provider.schemaCommitment ?? "") + ) { + throw new Error("bootstrap provider commitment is invalid"); + } +} + +export function buildBootstrapPlan({ + binding, + bindingSha256, + repositoryCommit, + providers, +}) { + if ( + !isPlainObject(binding) || + binding.schemaVersion !== 1 || + binding.chainId !== 1 || + !Array.isArray(binding.sources) || + binding.sources.length === 0 || + !Array.isArray(binding.releases) || + binding.releases.length === 0 || + !HEX_SHA256.test(bindingSha256 ?? "") || + !GIT_COMMIT.test(repositoryCommit ?? "") || + !Array.isArray(providers) || + providers.length !== 4 + ) { + throw new Error("bootstrap input is invalid"); + } + providers.forEach(assertProvider); + const sourceByName = new Map( + binding.sources.map((source) => [source.contractName, source]), + ); + if (sourceByName.size !== binding.sources.length) { + throw new Error("bootstrap sources are not unique"); + } + + const releases = binding.releases.map((release, index) => { + const sources = release.sourceContracts.map((contractName) => { + const source = sourceByName.get(contractName); + if (!source) throw new Error("bootstrap release references an unknown source"); + return { + contractName: source.contractName, + address: source.address, + inclusiveStartBlock: source.startBlock, + runtimeCodeHash: source.runtimeCodeHash, + unresolved: [ + "sourceRole", + "sourceType", + "recoverySelector", + "abiEventSetCommitment", + "artifactCreationCodeCommitment", + "bindingCommitment", + ], + }; + }); + return { + ordinal: index + 1, + scope: { + chainId: binding.chainId, + releaseId: release.releaseVersion, + modelId: release.model, + sourceGroup: "core", + }, + activationBlock: release.activationBlock, + sourceBindings: sources, + dynamicSourceTemplates: release.dynamicContracts.map((contractName) => ({ + contractName, + unresolved: [ + "parentFactoryReleaseBindingId", + "parentSourceRole", + "factoryEventType", + "deployedAddressField", + "deployedSourceRole", + "deployedArtifactCreationCodeCommitment", + "normalizedRuntimeCodeHash", + "immutableReferencesCommitment", + "immutableBindingSpec", + "immutableBindingCommitment", + "runtimeCodeLength", + "abiEventSetCommitment", + "templateCommitment", + ], + })), + unresolved: [ + "epochId", + "epochNumber", + "epochCommitment", + "artifactCreationCodeCommitment", + "createInputCommitment", + "activationGeneration", + "activationInputCommitment", + ], + }; + }); + + const providerBindings = providers.map((provider) => ({ + ...provider, + unresolved: + provider.providerType === "rpc_provider" + ? [ + "providerDeploymentId", + "endpointEvidenceCommitment", + "inputCommitment", + "createdAt", + ] + : ["providerDeploymentId", "inputCommitment", "createdAt"], + })); + const payload = { + kind: BOOTSTRAP_PLAN_KIND, + schemaVersion: 1, + repositoryCommit, + releaseBinding: { + path: "config/data-pipeline-release.v1.json", + sha256: bindingSha256, + chainId: binding.chainId, + startBlock: binding.startBlock, + confirmations: binding.confirmations, + }, + providerBindings, + releases, + execution: { + mode: "plan-only", + ready: false, + reason: + "release bootstrap requires reviewed semantic, ABI, creation-code, endpoint-evidence and activation inputs that are not present in the release binding", + }, + }; + return { + ...payload, + planSha256: sha256(canonicalJson(payload)), + }; +} + +export function assertNoSecretOutput(value, secrets) { + const serialized = canonicalJson(value); + for (const secret of secrets) { + if (typeof secret === "string" && secret.length > 0 && serialized.includes(secret)) { + throw new Error("operator output contains a credential"); + } + } + return serialized; +} + +export function safeFailure(error) { + const code = + typeof error === "object" && + error !== null && + typeof error.code === "string" && + /^[A-Z0-9]{5}$/u.test(error.code) + ? ` (${error.code})` + : ""; + return `operator failed${code}`; +} diff --git a/scripts/data-pipeline/hosted-db-operator.mjs b/scripts/data-pipeline/hosted-db-operator.mjs new file mode 100644 index 00000000..ead227f1 --- /dev/null +++ b/scripts/data-pipeline/hosted-db-operator.mjs @@ -0,0 +1,289 @@ +#!/usr/bin/env node + +import { execFile } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +import { createBootstrapPlan } from "./hosted-db-bootstrap-runtime.mjs"; +import { + assertNoSecretOutput, + discoverMigrationPlan, + safeFailure, + validateMigrationPlan, +} from "./hosted-db-operator-core.mjs"; +import { validateReviewedBootstrapPlan } from "./bootstrap-evidence.mjs"; +import { + applyReviewedBootstrap, + applyPendingMigrations, + closeHostedDatabase, + inspectBootstrapState, + inspectMigrationState, + openHostedDatabase, +} from "./hosted-db-postgres.mjs"; + +const run = promisify(execFile); +const workspace = fileURLToPath(new URL("../../", import.meta.url)); +const CREDENTIAL_ENVIRONMENT_NAME = + /(?:DATABASE_URL|PASSWORD|API_KEY|TOKEN|SECRET|SSL_CA(?:_PEM)?|RPC_URL)$/u; + +const HELP = `Usage: + node scripts/data-pipeline/hosted-db-operator.mjs plan [--output FILE] + node scripts/data-pipeline/hosted-db-operator.mjs dry-run --plan FILE --expected-project-ref REF + node scripts/data-pipeline/hosted-db-operator.mjs verify --plan FILE --expected-project-ref REF + node scripts/data-pipeline/hosted-db-operator.mjs apply --plan FILE --expected-project-ref REF --confirm-apply PLAN_SHA256 + node scripts/data-pipeline/hosted-db-operator.mjs bootstrap-plan [--output FILE] + node scripts/data-pipeline/hosted-db-operator.mjs bootstrap-dry-run --plan FILE --expected-project-ref REF + node scripts/data-pipeline/hosted-db-operator.mjs bootstrap-verify --plan FILE --expected-project-ref REF + node scripts/data-pipeline/hosted-db-operator.mjs bootstrap-apply --plan FILE --expected-project-ref REF --confirm-apply PLAN_SHA256 + +Database credentials are accepted only through PROGRAMMABLE_MIGRATOR_DATABASE_URL. +The CA certificate is accepted only through PROGRAMMABLE_POSTGRES_SSL_CA_PEM. +`; + +function parseArguments(argv) { + const [command, ...rest] = argv; + if (!command || command === "help" || command === "--help") { + return { command: "help", flags: new Map() }; + } + const flags = new Map(); + for (let index = 0; index < rest.length; index += 2) { + const flag = rest[index]; + const value = rest[index + 1]; + if (!flag?.startsWith("--") || value === undefined || value.startsWith("--")) { + throw new Error("operator arguments are invalid"); + } + if (flags.has(flag)) throw new Error("operator argument is duplicated"); + flags.set(flag, value); + } + return { command, flags }; +} + +function exactFlags(flags, allowed) { + for (const flag of flags.keys()) { + if (!allowed.includes(flag)) throw new Error("operator argument is not allowed"); + } +} + +async function gitCommit() { + const { stdout } = await run("git", ["rev-parse", "HEAD"], { + cwd: workspace, + }); + return stdout.trim(); +} + +async function assertMigrationCheckoutIsTrackedAndClean() { + const [{ stdout: status }, { stdout: tracked }] = await Promise.all([ + run( + "git", + [ + "status", + "--porcelain=v1", + "--untracked-files=all", + "--", + "supabase/migrations", + ], + { cwd: workspace }, + ), + run("git", ["ls-files", "--", "supabase/migrations"], { + cwd: workspace, + }), + ]); + if (status.trim() !== "") { + throw new Error("migration directory must match the exact commit"); + } + const trackedSql = tracked + .split("\n") + .filter((file) => file.endsWith(".sql")) + .sort(); + const plan = await discoverMigrationPlan({ + workspace, + repositoryCommit: await gitCommit(), + }); + if ( + trackedSql.length !== plan.migrationCount || + trackedSql.some((file, index) => file !== plan.migrations[index].file) + ) { + throw new Error("every migration must be tracked by the exact commit"); + } + return plan; +} + +async function readReviewedPlan(planPath) { + if (!planPath) throw new Error("--plan is required"); + const value = JSON.parse(await readFile(planPath, "utf8")); + const plan = validateMigrationPlan(value); + const checkout = await assertMigrationCheckoutIsTrackedAndClean(); + if ( + checkout.repositoryCommit !== plan.repositoryCommit || + checkout.planSha256 !== plan.planSha256 + ) { + throw new Error("reviewed migration plan does not match this checkout"); + } + return plan; +} + +async function readReviewedBootstrapPlan(planPath) { + if (!planPath) throw new Error("--plan is required"); + const plan = validateReviewedBootstrapPlan( + JSON.parse(await readFile(planPath, "utf8")), + ); + if ((await gitCommit()) !== plan.repositoryCommit) { + throw new Error("reviewed bootstrap plan does not match this checkout"); + } + const rebuilt = await createBootstrapPlan({ + repositoryCommit: plan.repositoryCommit, + environment: process.env, + createdAt: plan.createdAt, + }); + if (rebuilt.planSha256 !== plan.planSha256) { + throw new Error("reviewed bootstrap evidence changed after planning"); + } + return plan; +} + +function outputSecrets(environment) { + return Object.entries(environment) + .filter(([name]) => CREDENTIAL_ENVIRONMENT_NAME.test(name)) + .map(([, value]) => value) + .filter(Boolean); +} + +async function writeOutput(value, outputPath) { + assertNoSecretOutput(value, outputSecrets(process.env)); + const serialized = `${JSON.stringify(value, null, 2)}\n`; + if (outputPath) { + await writeFile(outputPath, serialized, { encoding: "utf8", flag: "wx" }); + } else { + process.stdout.write(serialized); + } +} + +async function runDatabaseCommand(command, flags) { + const allowed = ["--plan", "--expected-project-ref"]; + if (command === "apply") allowed.push("--confirm-apply"); + exactFlags(flags, allowed); + const plan = await readReviewedPlan(flags.get("--plan")); + if ( + command === "apply" && + flags.get("--confirm-apply") !== plan.planSha256 + ) { + throw new Error("--confirm-apply must equal the reviewed plan commitment"); + } + const connection = await openHostedDatabase({ + databaseUrl: process.env.PROGRAMMABLE_MIGRATOR_DATABASE_URL, + expectedProjectRef: flags.get("--expected-project-ref"), + sslCaPem: process.env.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + }); + try { + const state = + command === "apply" + ? await applyPendingMigrations({ + sql: connection.sql, + workspace, + plan, + }) + : await inspectMigrationState({ sql: connection.sql, plan }); + const output = { + kind: "programmable-hosted-db-operator-result", + schemaVersion: 1, + operation: command, + planSha256: plan.planSha256, + target: connection.target, + state, + changed: + command === "apply" && + Array.isArray(state.appliedThisRun) && + state.appliedThisRun.length > 0, + }; + await writeOutput(output); + if (command === "verify" && state.status !== "current") { + process.exitCode = 2; + } + } finally { + await closeHostedDatabase(connection.sql); + } +} + +async function runBootstrapDatabaseCommand(command, flags) { + const allowed = ["--plan", "--expected-project-ref"]; + if (command === "bootstrap-apply") allowed.push("--confirm-apply"); + exactFlags(flags, allowed); + const plan = await readReviewedBootstrapPlan(flags.get("--plan")); + if ( + command === "bootstrap-apply" && + flags.get("--confirm-apply") !== plan.planSha256 + ) { + throw new Error("--confirm-apply must equal the reviewed plan commitment"); + } + const connection = await openHostedDatabase({ + databaseUrl: process.env.PROGRAMMABLE_MIGRATOR_DATABASE_URL, + expectedProjectRef: flags.get("--expected-project-ref"), + sslCaPem: process.env.PROGRAMMABLE_POSTGRES_SSL_CA_PEM, + }); + try { + const migrationPlan = await assertMigrationCheckoutIsTrackedAndClean(); + const migrationState = await inspectMigrationState({ + sql: connection.sql, + plan: migrationPlan, + }); + if (migrationState.status !== "current") { + throw new Error("all reviewed migrations must be current before bootstrap"); + } + const state = command === "bootstrap-apply" + ? await applyReviewedBootstrap({ sql: connection.sql, plan }) + : await inspectBootstrapState({ sql: connection.sql, plan }); + await writeOutput({ + kind: "programmable-hosted-db-bootstrap-result", + schemaVersion: 1, + operation: command, + planSha256: plan.planSha256, + target: connection.target, + migrationPlanSha256: migrationPlan.planSha256, + state, + changed: state.changed === true, + }); + if (command === "bootstrap-verify" && state.status !== "current") { + process.exitCode = 2; + } + } finally { + await closeHostedDatabase(connection.sql); + } +} + +async function main() { + const { command, flags } = parseArguments(process.argv.slice(2)); + if (command === "help") { + process.stdout.write(HELP); + return; + } + if (command === "plan") { + exactFlags(flags, ["--output"]); + const plan = await assertMigrationCheckoutIsTrackedAndClean(); + await writeOutput(plan, flags.get("--output")); + return; + } + if (command === "bootstrap-plan") { + exactFlags(flags, ["--output"]); + const plan = await createBootstrapPlan({ + repositoryCommit: await gitCommit(), + environment: process.env, + }); + await writeOutput(plan, flags.get("--output")); + return; + } + if (["dry-run", "verify", "apply"].includes(command)) { + await runDatabaseCommand(command, flags); + return; + } + if (["bootstrap-dry-run", "bootstrap-verify", "bootstrap-apply"].includes(command)) { + await runBootstrapDatabaseCommand(command, flags); + return; + } + throw new Error("unknown operator command"); +} + +main().catch((error) => { + process.stderr.write(`${safeFailure(error)}\n`); + process.exitCode = 1; +}); diff --git a/scripts/data-pipeline/hosted-db-operator.test.mjs b/scripts/data-pipeline/hosted-db-operator.test.mjs new file mode 100644 index 00000000..5e64d411 --- /dev/null +++ b/scripts/data-pipeline/hosted-db-operator.test.mjs @@ -0,0 +1,234 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + assertNoSecretOutput, + buildBootstrapPlan, + compareMigrationHistory, + discoverMigrationPlan, + safeFailure, + validateDirectSupabaseTarget, + validateMigrationPlan, +} from "./hosted-db-operator-core.mjs"; + +const COMMIT = "a".repeat(40); +const HASH = `0x${"1".repeat(64)}`; + +async function fixture(files) { + const workspace = await mkdtemp(path.join(os.tmpdir(), "db-operator-test-")); + const root = path.join(workspace, "supabase", "migrations"); + await mkdir(root, { recursive: true }); + for (const [name, contents] of Object.entries(files)) { + await writeFile(path.join(root, name), contents); + } + return { workspace, root }; +} + +test("migration discovery includes every ordered current and later file", async (t) => { + const { workspace } = await fixture({ + "20260731000200_second.sql": "select 2;\n", + "20260731000100_first.sql": "select 1;\n", + "20270101000000_later.sql": "select 3;\n", + }); + t.after(() => rm(workspace, { recursive: true, force: true })); + const plan = await discoverMigrationPlan({ workspace, repositoryCommit: COMMIT }); + assert.equal(plan.migrationCount, 3); + assert.deepEqual( + plan.migrations.map(({ version, ordinal }) => [version, ordinal]), + [ + ["20260731000100", 1], + ["20260731000200", 2], + ["20270101000000", 3], + ], + ); + assert.equal(validateMigrationPlan(plan), plan); +}); + +test("migration discovery rejects noncanonical files and symlinks", async (t) => { + const noncanonical = await fixture({ "1_bad.sql": "select 1;\n" }); + t.after(() => rm(noncanonical.workspace, { recursive: true, force: true })); + await assert.rejects( + discoverMigrationPlan({ + workspace: noncanonical.workspace, + repositoryCommit: COMMIT, + }), + /noncanonical migration/u, + ); + + const linked = await fixture({ + "20260731000100_first.sql": "select 1;\n", + }); + t.after(() => rm(linked.workspace, { recursive: true, force: true })); + await symlink( + path.join(linked.root, "20260731000100_first.sql"), + path.join(linked.root, "20260731000200_link.sql"), + ); + await assert.rejects( + discoverMigrationPlan({ workspace: linked.workspace, repositoryCommit: COMMIT }), + /regular file/u, + ); +}); + +test("reviewed plan commitment detects file and order tampering", async (t) => { + const { workspace } = await fixture({ + "20260731000100_first.sql": "select 1;\n", + }); + t.after(() => rm(workspace, { recursive: true, force: true })); + const plan = await discoverMigrationPlan({ workspace, repositoryCommit: COMMIT }); + const tampered = structuredClone(plan); + tampered.migrations[0].bytes += 1; + assert.throws(() => validateMigrationPlan(tampered), /commitment/u); +}); + +test("direct target validation accepts only the expected 5432 endpoint", () => { + const projectRef = "abcdefghijklmnopqrst"; + const target = validateDirectSupabaseTarget( + `postgresql://postgres:private@db.${projectRef}.supabase.co:5432/postgres?sslmode=verify-full`, + projectRef, + ); + assert.deepEqual(target, { + projectRef, + host: `db.${projectRef}.supabase.co`, + port: 5432, + database: "postgres", + sslMode: "verify-full", + }); + assert.doesNotMatch(JSON.stringify(target), /private/u); + assert.throws( + () => + validateDirectSupabaseTarget( + `postgresql://postgres:private@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full`, + projectRef, + ), + /direct Supabase endpoint/u, + ); + assert.throws( + () => + validateDirectSupabaseTarget( + `postgresql://postgres:private@db.${projectRef}.supabase.co:5432/postgres?sslmode=require`, + projectRef, + ), + /direct Supabase endpoint/u, + ); +}); + +test("history verification is prefix-only and requires file evidence", async (t) => { + const { workspace } = await fixture({ + "20260731000100_first.sql": "select 1;\n", + "20260731000200_second.sql": "select 2;\n", + }); + t.after(() => rm(workspace, { recursive: true, force: true })); + const plan = await discoverMigrationPlan({ workspace, repositoryCommit: COMMIT }); + const first = plan.migrations[0]; + const state = compareMigrationHistory({ + plan, + historyRows: [{ version: first.version, name: first.name, statements: ["select 1"] }], + evidenceTablePresent: true, + evidenceRows: [ + { + version: first.version, + name: first.name, + file_name: path.posix.basename(first.file), + ordinal: first.ordinal, + file_sha256: first.fileSha256, + plan_sha256: plan.planSha256, + repository_commit: plan.repositoryCommit, + }, + ], + }); + assert.equal(state.status, "pending"); + assert.deepEqual(state.pending.map(({ version }) => version), [ + "20260731000200", + ]); + assert.throws( + () => + compareMigrationHistory({ + plan, + historyRows: [ + { version: first.version, name: first.name, statements: ["select 1"] }, + ], + evidenceTablePresent: false, + evidenceRows: [], + }), + /evidence is missing/u, + ); + assert.throws( + () => + compareMigrationHistory({ + plan, + historyRows: [ + { version: plan.migrations[1].version, name: "second", statements: ["select 2"] }, + ], + evidenceTablePresent: true, + evidenceRows: [], + }), + /exact local prefix/u, + ); +}); + +test("bootstrap plan exposes exact binding facts and remains fail closed", () => { + const binding = { + schemaVersion: 1, + chainId: 1, + startBlock: 100, + confirmations: 12, + sources: [ + { + contractName: "Launcher", + address: `0x${"2".repeat(40)}`, + startBlock: 101, + runtimeCodeHash: HASH, + }, + ], + releases: [ + { + model: "classic", + releaseVersion: "classic-v1", + activationBlock: 101, + sourceContracts: ["Launcher"], + dynamicContracts: [], + }, + ], + }; + const providers = ["envio_deployment", "rpc_provider", "rpc_provider", "uniswap_subgraph"].map( + (providerType, index) => ({ + providerType, + redactedIdentity: `provider-${index}`, + deploymentCommitment: `0x${String(index + 2).repeat(64)}`, + schemaCommitment: `0x${String(index + 6).repeat(64)}`, + }), + ); + const plan = buildBootstrapPlan({ + binding, + bindingSha256: HASH, + repositoryCommit: COMMIT, + providers, + }); + assert.equal(plan.execution.mode, "plan-only"); + assert.equal(plan.execution.ready, false); + assert.deepEqual(plan.releases[0].scope, { + chainId: 1, + releaseId: "classic-v1", + modelId: "classic", + sourceGroup: "core", + }); + assert.ok( + plan.releases[0].sourceBindings[0].unresolved.includes( + "abiEventSetCommitment", + ), + ); +}); + +test("secret guard and safe failures do not echo credentials", () => { + assert.throws( + () => assertNoSecretOutput({ value: "top-secret" }, ["top-secret"]), + /credential/u, + ); + assert.equal(safeFailure(new Error("postgres://user:secret@example")), "operator failed"); + const postgresError = new Error("postgres://user:secret@example"); + postgresError.code = "42P01"; + assert.equal(safeFailure(postgresError), "operator failed (42P01)"); +}); diff --git a/scripts/data-pipeline/hosted-db-postgres.mjs b/scripts/data-pipeline/hosted-db-postgres.mjs new file mode 100644 index 00000000..68a5c603 --- /dev/null +++ b/scripts/data-pipeline/hosted-db-postgres.mjs @@ -0,0 +1,584 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import postgres from "postgres"; + +import { + compareMigrationHistory, + sha256, + validateDirectSupabaseTarget, +} from "./hosted-db-operator-core.mjs"; +import { validateReviewedBootstrapPlan } from "./bootstrap-evidence.mjs"; + +const HISTORY_DDL = ` +set lock_timeout = '4s'; +create schema if not exists supabase_migrations; +create table if not exists supabase_migrations.schema_migrations ( + version text not null primary key +); +alter table supabase_migrations.schema_migrations + add column if not exists statements text[]; +alter table supabase_migrations.schema_migrations + add column if not exists name text; +create table if not exists supabase_migrations.programmable_migration_evidence ( + version text primary key + references supabase_migrations.schema_migrations(version) on delete restrict, + name text not null, + file_name text not null unique, + ordinal integer not null unique check (ordinal > 0), + file_sha256 text not null + check (file_sha256 ~ '^0x[0-9a-f]{64}$'), + plan_sha256 text not null + check (plan_sha256 ~ '^0x[0-9a-f]{64}$'), + repository_commit text not null + check (repository_commit ~ '^[0-9a-f]{40}$'), + applied_at timestamptz not null default pg_catalog.clock_timestamp() +); +revoke all on schema supabase_migrations from public; +revoke all on table supabase_migrations.schema_migrations from public; +revoke all on table supabase_migrations.programmable_migration_evidence from public; +reset lock_timeout; +`; + +function sslConfiguration(caPem) { + if ( + typeof caPem !== "string" || + caPem.length < 64 || + caPem.length > 32_768 || + !caPem.includes("-----BEGIN CERTIFICATE-----") || + !caPem.includes("-----END CERTIFICATE-----") + ) { + throw new Error("a valid server-only Postgres CA certificate is required"); + } + return { rejectUnauthorized: true, ca: caPem }; +} + +export async function openHostedDatabase({ + databaseUrl, + expectedProjectRef, + sslCaPem, +}) { + const target = validateDirectSupabaseTarget(databaseUrl, expectedProjectRef); + const connectionUrl = new URL(databaseUrl); + const sql = postgres({ + host: connectionUrl.hostname, + port: Number(connectionUrl.port), + database: connectionUrl.pathname.slice(1), + username: decodeURIComponent(connectionUrl.username), + password: decodeURIComponent(connectionUrl.password), + ssl: sslConfiguration(sslCaPem), + max: 1, + prepare: false, + connect_timeout: 8, + idle_timeout: 5, + max_lifetime: 60, + onnotice: () => {}, + connection: { + application_name: "programmable-hosted-db-operator", + }, + }); + try { + const [identity] = await sql.unsafe(` + select + pg_catalog.current_database() as database_name, + pg_catalog.inet_server_port() as server_port, + pg_catalog.current_setting('server_version_num') as server_version_num + `); + if ( + identity?.database_name !== "postgres" || + Number(identity?.server_port) !== 5432 || + Number(identity?.server_version_num) < 150000 + ) { + throw new Error("connected database identity is not an approved target"); + } + } catch (error) { + await sql.end({ timeout: 1 }).catch(() => {}); + throw error; + } + return Object.freeze({ sql, target }); +} + +export async function readRemoteMigrationState(sql) { + const [tables] = await sql.unsafe(` + select + pg_catalog.to_regclass('supabase_migrations.schema_migrations')::text + as history_table, + pg_catalog.to_regclass( + 'supabase_migrations.programmable_migration_evidence' + )::text as evidence_table + `); + const historyPresent = + tables?.history_table === "supabase_migrations.schema_migrations"; + const evidenceTablePresent = + tables?.evidence_table === + "supabase_migrations.programmable_migration_evidence"; + const historyRows = historyPresent + ? await sql.unsafe(` + select version, coalesce(name, '') as name, statements + from supabase_migrations.schema_migrations + order by version + `) + : []; + const evidenceRows = evidenceTablePresent + ? await sql.unsafe(` + select version, name, file_name, ordinal, file_sha256, + plan_sha256, repository_commit + from supabase_migrations.programmable_migration_evidence + order by ordinal + `) + : []; + return { historyRows, evidenceRows, evidenceTablePresent }; +} + +export async function inspectMigrationState({ sql, plan }) { + const remote = await readRemoteMigrationState(sql); + return compareMigrationHistory({ plan, ...remote }); +} + +async function ensureMigrationHistory(sql) { + await sql.unsafe(HISTORY_DDL).simple(); +} + +async function applyMigration({ sql, workspace, plan, migration }) { + const absolutePath = path.resolve(workspace, migration.file); + const contents = await readFile(absolutePath); + if ( + contents.byteLength !== migration.bytes || + sha256(contents) !== migration.fileSha256 + ) { + throw new Error(`migration file changed after planning: ${migration.version}`); + } + const migrationSql = contents.toString("utf8"); + await sql.unsafe("reset all").simple(); + await sql.begin(async (transaction) => { + await transaction + .unsafe("set local lock_timeout = '4s'; set local statement_timeout = '15min'") + .simple(); + await transaction.unsafe(migrationSql).simple(); + await transaction` + insert into supabase_migrations.schema_migrations ( + version, name, statements + ) values ( + ${migration.version}, + ${migration.name}, + ${transaction.array([migrationSql])} + ) + `; + await transaction` + insert into supabase_migrations.programmable_migration_evidence ( + version, name, file_name, ordinal, file_sha256, + plan_sha256, repository_commit + ) values ( + ${migration.version}, + ${migration.name}, + ${path.posix.basename(migration.file)}, + ${migration.ordinal}, + ${migration.fileSha256}, + ${plan.planSha256}, + ${plan.repositoryCommit} + ) + `; + }); +} + +export async function applyPendingMigrations({ sql, workspace, plan }) { + const [lock] = await sql.unsafe(` + select pg_catalog.pg_try_advisory_lock( + pg_catalog.hashtextextended( + 'programmable:hosted-db-migrations:v1', 0 + ) + ) as acquired + `); + if (lock?.acquired !== true) { + throw new Error("another migration operator holds the database lock"); + } + try { + const before = await readRemoteMigrationState(sql); + const initial = compareMigrationHistory({ plan, ...before }); + if (initial.pending.length === 0) { + return { ...initial, appliedThisRun: [] }; + } + const appliedThisRun = initial.pending.map(({ version }) => version); + await ensureMigrationHistory(sql); + for (const pending of initial.pending) { + const migration = plan.migrations.find( + ({ version }) => version === pending.version, + ); + if (!migration) { + throw new Error("pending migration is absent from the plan"); + } + await applyMigration({ sql, workspace, plan, migration }); + } + return { + ...(await inspectMigrationState({ sql, plan })), + appliedThisRun, + }; + } finally { + await sql + .unsafe(` + select pg_catalog.pg_advisory_unlock( + pg_catalog.hashtextextended( + 'programmable:hosted-db-migrations:v1', 0 + ) + ) + `) + .catch(() => {}); + } +} + +export async function closeHostedDatabase(sql) { + await sql.end({ timeout: 5 }); +} + +function databaseBytes(value) { + if (typeof value !== "string" || !/^0x[0-9a-f]+$/u.test(value)) { + throw new Error("bootstrap hexadecimal input is invalid"); + } + return Buffer.from(value.slice(2), "hex"); +} + +function rowHex(value) { + if (!Buffer.isBuffer(value)) throw new Error("bootstrap database bytes are invalid"); + return `0x${value.toString("hex")}`; +} + +async function bootstrapFootprint(transaction) { + const [row] = await transaction.unsafe(` + select + (select pg_catalog.count(*)::text + from programmable_private.provider_deployments) as provider_count, + (select pg_catalog.count(*)::text + from programmable_private.release_epochs + where release_id <> 'envio-control') as epoch_count, + (select pg_catalog.count(*)::text + from programmable_private.release_epoch_current + where release_id <> 'envio-control') as pointer_count, + (select pg_catalog.count(*)::text + from programmable_private.candidate_database_control) as control_count + `); + return Object.freeze({ + providers: Number(row?.provider_count), + epochs: Number(row?.epoch_count), + pointers: Number(row?.pointer_count), + controls: Number(row?.control_count), + }); +} + +async function assertBootstrapMatches(transaction, plan) { + const providerRows = await transaction.unsafe(` + select provider_deployment_id::text, provider_type::text, + redacted_identity::text, deployment_commitment, + schema_commitment, created_at::text + from programmable_private.provider_deployments + order by provider_deployment_id + `); + const expectedProviders = [...plan.providerBindings] + .map((provider) => ({ + provider_deployment_id: provider.providerDeploymentId, + provider_type: provider.providerType, + redacted_identity: provider.redactedIdentity, + deployment_commitment: provider.deploymentCommitment, + schema_commitment: provider.schemaCommitment, + created_at: provider.createdAt.replace("T", " ").replace("Z", "+00"), + })) + .sort((left, right) => + left.provider_deployment_id.localeCompare(right.provider_deployment_id), + ); + if ( + providerRows.length !== expectedProviders.length || + providerRows.some((row, index) => { + const expected = expectedProviders[index]; + return !expected || + row.provider_deployment_id !== expected.provider_deployment_id || + row.provider_type !== expected.provider_type || + row.redacted_identity !== expected.redacted_identity || + rowHex(row.deployment_commitment) !== expected.deployment_commitment || + rowHex(row.schema_commitment) !== expected.schema_commitment || + Date.parse(row.created_at) !== Date.parse(expected.created_at); + }) + ) { + throw new Error("bootstrap provider state does not match the reviewed plan"); + } + const [control] = await transaction.unsafe(` + select database_mode::text, envio_provider_deployment_id::text, + envio_deployment_commitment, envio_schema_commitment, + initialization_input_commitment, initialized_at::text, + promoted_at + from programmable_private.candidate_database_control + where singleton + `); + const candidate = plan.providerBindings.find( + ({ providerType }) => providerType === "envio_deployment", + ); + if ( + !candidate || !control || control.database_mode !== "candidate-only" || + control.envio_provider_deployment_id !== candidate.providerDeploymentId || + rowHex(control.envio_deployment_commitment) !== candidate.deploymentCommitment || + rowHex(control.envio_schema_commitment) !== candidate.schemaCommitment || + rowHex(control.initialization_input_commitment) !== + plan.candidateIsolation.candidateInitializationInputCommitment || + Date.parse(control.initialized_at) !== Date.parse(plan.createdAt) || + control.promoted_at !== null + ) { + throw new Error("candidate database control does not match the reviewed plan"); + } + const epochRows = await transaction.unsafe(` + select epoch.epoch_id::text, epoch.chain_id::text, + epoch.release_id::text, epoch.model_id::text, + epoch.source_group::text, epoch.epoch_number::text, + epoch.epoch_commitment, epoch.artifact_creation_code_commitment, + current_epoch.generation::text + from programmable_private.release_epochs as epoch + join programmable_private.release_epoch_current as current_epoch + on current_epoch.epoch_id = epoch.epoch_id + where epoch.release_id <> 'envio-control' + order by epoch.release_id + `); + const expectedEpochs = [...plan.releases].sort((left, right) => + left.scope.releaseId.localeCompare(right.scope.releaseId), + ); + if ( + epochRows.length !== expectedEpochs.length || + epochRows.some((row, index) => { + const expected = expectedEpochs[index]; + return !expected || row.epoch_id !== expected.epochId || + row.chain_id !== String(expected.scope.chainId) || + row.release_id !== expected.scope.releaseId || + row.model_id !== expected.scope.modelId || + row.source_group !== expected.scope.sourceGroup || + row.epoch_number !== expected.epochNumber || row.generation !== "1" || + rowHex(row.epoch_commitment) !== expected.epochCommitment || + rowHex(row.artifact_creation_code_commitment) !== + expected.artifactCreationCodeCommitment; + }) + ) { + throw new Error("bootstrap release epochs do not match the reviewed plan"); + } + for (const release of plan.releases) { + const [counts] = await transaction` + select + (select pg_catalog.count(*)::text + from programmable_private.release_source_bindings + where epoch_id = ${release.epochId}::uuid) as source_count, + (select pg_catalog.count(*)::text + from programmable_private.release_dynamic_source_templates + where epoch_id = ${release.epochId}::uuid) as template_count, + (select pg_catalog.count(*)::text + from programmable_private.release_projection_event_rules + where epoch_id = ${release.epochId}::uuid) as rule_count, + (select pg_catalog.count(*)::text + from programmable_private.release_launch_completeness_requirements + where epoch_id = ${release.epochId}::uuid) as requirement_count + `; + if ( + Number(counts?.source_count) !== release.sourceBindings.length || + Number(counts?.template_count) !== release.dynamicSourceTemplates.length || + Number(counts?.rule_count) !== release.projectionEventRules.length || + Number(counts?.requirement_count) !== + release.launchCompletenessRequirements.length + ) { + throw new Error("bootstrap release child counts do not match the reviewed plan"); + } + } +} + +async function registerBootstrapProvider(transaction, provider) { + if (provider.providerType === "rpc_provider") { + await transaction` + select programmable_private.register_rpc_provider_deployment( + ${provider.providerDeploymentId}::uuid, + ${provider.chainId}::bigint, + ${provider.vendor}::text, + ${provider.constructorVersion}::text, + ${databaseBytes(provider.endpointUrlCommitment)}::bytea, + ${databaseBytes(provider.endpointOriginCommitment)}::bytea, + ${provider.endpointEvidenceDomain}::text, + ${databaseBytes(provider.endpointEvidenceCommitment)}::bytea, + ${databaseBytes(provider.deploymentCommitment)}::bytea, + ${databaseBytes(provider.schemaCommitment)}::bytea, + ${databaseBytes(provider.inputCommitment)}::bytea, + ${provider.createdAt}::timestamptz + ) + `; + return; + } + await transaction` + select programmable_private.register_provider_deployment( + ${provider.providerDeploymentId}::uuid, + ${provider.providerType}::text, + ${provider.redactedIdentity}::text, + ${databaseBytes(provider.deploymentCommitment)}::bytea, + ${databaseBytes(provider.schemaCommitment)}::bytea, + ${databaseBytes(provider.inputCommitment)}::bytea, + ${provider.createdAt}::timestamptz + ) + `; +} + +async function applyReleaseBootstrap(transaction, release, createdAt) { + await transaction` + select programmable_private.create_release_epoch( + ${release.epochId}::uuid, + ${release.scope.chainId}::bigint, + ${release.scope.releaseId}::text, + ${release.scope.modelId}::text, + ${release.scope.sourceGroup}::text, + ${release.epochNumber}::bigint, + ${databaseBytes(release.epochCommitment)}::bytea, + ${databaseBytes(release.artifactCreationCodeCommitment)}::bytea, + ${databaseBytes(release.createInputCommitment)}::bytea, + ${createdAt}::timestamptz + ) + `; + for (const source of release.sourceBindings) { + await transaction` + select programmable_private.append_release_source_binding( + ${source.bindingId}::uuid, + ${release.epochId}::uuid, + ${source.sourceName}::text, + ${source.sourceRole}::text, + ${source.sourceType}::text, + ${databaseBytes(source.sourceAddress)}::bytea, + ${source.recoverySelector === null + ? null + : databaseBytes(source.recoverySelector)}::bytea, + ${source.inclusiveStartBlock}::numeric, + ${databaseBytes(source.abiEventSetCommitment)}::bytea, + ${databaseBytes(source.artifactCreationCodeCommitment)}::bytea, + ${databaseBytes(source.bindingCommitment)}::bytea, + ${databaseBytes(source.inputCommitment)}::bytea, + ${createdAt}::timestamptz + ) + `; + } + for (const template of release.dynamicSourceTemplates) { + await transaction` + select programmable_private.append_release_dynamic_source_template( + ${template.dynamicSourceTemplateId}::uuid, + ${release.epochId}::uuid, + ${template.parentFactoryReleaseBindingId}::uuid, + ${template.parentSourceRole}::text, + ${template.factoryEventType}::text, + ${template.deployedAddressField}::text, + ${template.deployedSourceRole}::text, + ${databaseBytes(template.deployedArtifactCreationCodeCommitment)}::bytea, + ${databaseBytes(template.normalizedRuntimeCodeHash)}::bytea, + ${databaseBytes(template.immutableReferencesCommitment)}::bytea, + ${transaction.json(template.immutableBindingSpec)}::jsonb, + ${databaseBytes(template.immutableBindingCommitment)}::bytea, + ${template.runtimeCodeLength}::numeric, + ${databaseBytes(template.abiEventSetCommitment)}::bytea, + ${databaseBytes(template.templateCommitment)}::bytea, + ${createdAt}::timestamptz + ) + `; + } + for (const rule of release.projectionEventRules) { + await transaction` + select programmable_private.append_release_projection_event_rule( + ${rule.projectionEventRuleId}::uuid, + ${release.epochId}::uuid, + ${rule.projectionKind}::text, + ${rule.sourceRole}::text, + ${rule.eventType}::text, + ${databaseBytes(rule.ruleCommitment)}::bytea, + ${createdAt}::timestamptz + ) + `; + } + for (const requirement of release.launchCompletenessRequirements) { + await transaction` + select programmable_private.append_release_launch_requirement( + ${requirement.launchRequirementId}::uuid, + ${release.epochId}::uuid, + ${requirement.requirementOrdinal}::integer, + ${requirement.occurrenceRole}::text, + ${requirement.eventType}::text, + ${requirement.requiredWhen}::text, + ${databaseBytes(requirement.requirementCommitment)}::bytea, + ${createdAt}::timestamptz + ) + `; + } + await transaction` + select programmable_private.activate_release_epoch( + ${release.scope.chainId}::bigint, + ${release.scope.releaseId}::text, + ${release.scope.modelId}::text, + ${release.scope.sourceGroup}::text, + ${release.epochId}::uuid, + ${release.activation.expectedGeneration}::bigint, + ${release.activation.nextGeneration}::bigint, + ${databaseBytes(release.activation.inputCommitment)}::bytea, + ${release.activation.changedAt}::timestamptz + ) + `; +} + +export async function inspectBootstrapState({ sql, plan }) { + validateReviewedBootstrapPlan(plan); + const footprint = await bootstrapFootprint(sql); + if ( + footprint.providers === 0 && footprint.epochs === 0 && + footprint.pointers === 0 && footprint.controls === 0 + ) { + return Object.freeze({ status: "empty", footprint }); + } + await assertBootstrapMatches(sql, plan); + return Object.freeze({ status: "current", footprint }); +} + +export async function applyReviewedBootstrap({ sql, plan }) { + validateReviewedBootstrapPlan(plan); + return sql.begin(async (transaction) => { + await transaction.unsafe( + "set local lock_timeout = '4s'; set local statement_timeout = '15min'", + ).simple(); + const [lock] = await transaction.unsafe(` + select pg_catalog.pg_try_advisory_xact_lock( + pg_catalog.hashtextextended( + 'programmable:candidate-db-bootstrap:v1', 0 + ) + ) as acquired + `); + if (lock?.acquired !== true) { + throw new Error("another bootstrap operator holds the database lock"); + } + const footprint = await bootstrapFootprint(transaction); + const empty = footprint.providers === 0 && footprint.epochs === 0 && + footprint.pointers === 0 && footprint.controls === 0; + if (!empty) { + await assertBootstrapMatches(transaction, plan); + return Object.freeze({ status: "current", changed: false, footprint }); + } + await transaction.unsafe("set local role programmable_projector").simple(); + for (const provider of plan.providerBindings) { + await registerBootstrapProvider(transaction, provider); + } + const candidate = plan.providerBindings.find( + ({ providerType }) => providerType === "envio_deployment", + ); + if (!candidate) throw new Error("candidate Envio provider is absent"); + await transaction` + select programmable_private.initialize_candidate_database( + ${candidate.providerDeploymentId}::uuid, + ${databaseBytes(candidate.deploymentCommitment)}::bytea, + ${databaseBytes(candidate.schemaCommitment)}::bytea, + ${databaseBytes( + plan.candidateIsolation.candidateInitializationInputCommitment, + )}::bytea, + ${plan.createdAt}::timestamptz + ) + `; + for (const release of plan.releases) { + await applyReleaseBootstrap(transaction, release, plan.createdAt); + } + await transaction.unsafe("reset role").simple(); + await assertBootstrapMatches(transaction, plan); + return Object.freeze({ + status: "current", + changed: true, + footprint: await bootstrapFootprint(transaction), + }); + }); +} diff --git a/scripts/perf/read-model-capture.mjs b/scripts/perf/read-model-capture.mjs new file mode 100644 index 00000000..ad720071 --- /dev/null +++ b/scripts/perf/read-model-capture.mjs @@ -0,0 +1,1453 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { createHmac, randomBytes } from "node:crypto"; +import { + appendFileSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { basename, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { + parseReadModelLoadProfile, + ROUTE_NAMES, + sha256Bytes, +} from "./read-model-gate-core.mjs"; +import { buildReadModelReleaseProbe } from "./read-model-release-probe.mjs"; + +const RUNTIME_CAPTURE_PATH = "/api/ops/read-model-performance-capture"; +const MAX_RUNTIME_EVIDENCE_BYTES = 8 * 1024 * 1024; +const ADDRESS = /^0x[0-9a-fA-F]{40}$/u; +const HASH = /^0x[0-9a-fA-F]{64}$/u; +const BYTES32 = /^0x[0-9a-f]{64}$/u; +const HEX_DIGEST = /^[0-9a-f]{64}$/u; +const SHADOW_PROBE_ROUTES = new Set([ + "exploreList", + "tokenDetail", + "tokenChart", + "creatorProfile", + "classicProfile", + "stockProfile", + "classicLaunchLookup", + "stockLaunchLookup", +]); +export const EXPLORE_MATRIX_SORTS = Object.freeze([ + "newest", + "oldest", + "market-cap", + "market-cap-asc", +]); +export const EXPLORE_MATRIX_PAGE_SIZE = 6; +export const EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND = 8; +export const EXPLORE_MATRIX_CLAMP_PAGE = Number.MAX_SAFE_INTEGER; +export const EXPLORE_MATRIX_MANIFEST_FILE = + "explore-matrix-evidence.v1.json"; +export const EXPLORE_MATRIX_PAGES_FILE = "explore-matrix-pages.v1.jsonl"; +const EXPLORE_MATRIX_MAXIMUM_PAGES_PER_CASE = 100; +const REQUIRED_RELEASE_VERSIONS = Object.freeze([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +]); + +function canonicalJson(value) { + if (Array.isArray(value)) { + return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`; + } + if (value !== null && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map( + (key) => + `${JSON.stringify(key)}:${canonicalJson(value[key])}`, + ) + .join(",")}}`; + } + const encoded = JSON.stringify(value); + if (encoded === undefined) { + throw new Error("explore matrix contains a non-JSON value"); + } + return encoded; +} + +export function sha256Canonical(value) { + return sha256Bytes(Buffer.from(canonicalJson(value), "utf8")); +} + +export function normalizeExploreMatrixQuery(value) { + if (typeof value !== "string") { + throw new Error("explore matrix query must be a string"); + } + return value.trim().toLowerCase().replace(/^\$/u, ""); +} + +function alternatingAsciiCase(value) { + let upper = true; + return [...value] + .map((character) => { + if (!/[a-z]/iu.test(character)) return character; + const result = upper + ? character.toUpperCase() + : character.toLowerCase(); + upper = !upper; + return result; + }) + .join(""); +} + +function caseIdentity(value) { + return { + caseId: value.caseId, + kind: value.kind, + query: value.query, + normalizedQuery: value.normalizedQuery, + sourceTokenAddress: value.sourceTokenAddress, + sourceReleaseVersion: value.sourceReleaseVersion, + }; +} + +export function commitExploreMatrixCase(value) { + return sha256Canonical(caseIdentity(value)); +} + +function committedCase(value) { + const identity = caseIdentity(value); + return Object.freeze({ + ...identity, + commitment: commitExploreMatrixCase(identity), + }); +} + +function emptyExploreMatrixCase() { + return committedCase({ + caseId: "empty", + kind: "empty", + query: " ", + normalizedQuery: "", + sourceTokenAddress: null, + sourceReleaseVersion: null, + }); +} + +function argumentsFrom(argv) { + const values = {}; + for (let index = 0; index < argv.length; index += 2) { + const name = argv[index]; + const value = argv[index + 1]; + if (!name?.startsWith("--") || !value || value.startsWith("--")) { + throw new Error("capture arguments must be --name value pairs"); + } + const key = name.slice(2); + if (values[key]) throw new Error(`duplicate argument: ${name}`); + values[key] = value; + } + for (const required of [ + "target-url", + "deployment-id", + "output-directory", + "kind", + ]) { + if (!values[required]) throw new Error(`--${required} is required`); + } + if (!new Set(["preview", "production-canary"]).has(values.kind)) { + throw new Error("--kind must be preview or production-canary"); + } + return values; +} + +function gitHead(rootDirectory) { + return execFileSync("git", ["rev-parse", "HEAD"], { + cwd: rootDirectory, + encoding: "utf8", + }).trim(); +} + +function requiredSecret(value, name) { + if ( + typeof value !== "string" || + value.length < 32 || + value.length > 512 || + /[\r\n]/u.test(value) + ) { + throw new Error(`${name} is required`); + } + return value; +} + +function secret(environment, name) { + return requiredSecret(environment[name], name); +} + +function deterministicSchedule(profile) { + const schedule = []; + for (const route of ROUTE_NAMES) { + const count = profile.load.routeMixBps[route] / 10; + if (!Number.isInteger(count)) { + throw new Error("route mix must resolve exactly across 1000 samples"); + } + schedule.push(...Array.from({ length: count }, () => route)); + } + let state = 0x4f1bbcdc; + for (let index = schedule.length - 1; index > 0; index -= 1) { + state = (Math.imul(state, 1_664_525) + 1_013_904_223) >>> 0; + const swapIndex = state % (index + 1); + [schedule[index], schedule[swapIndex]] = [ + schedule[swapIndex], + schedule[index], + ]; + } + return schedule; +} + +function datasetAddress(values, sequence, name) { + if (!Array.isArray(values) || values.length < 1) { + throw new Error(`runtime dataset has no ${name}`); + } + const coverageIndex = sequence % values.length; + const value = values[coverageIndex]; + if (typeof value !== "string" || !ADDRESS.test(value)) { + throw new Error(`runtime dataset contains an invalid ${name} address`); + } + return value; +} + +function datasetLaunch(values, sequence, name) { + if (!Array.isArray(values) || values.length < 1) { + throw new Error(`runtime dataset has no ${name} launches`); + } + const coverageIndex = sequence % values.length; + const value = values[coverageIndex]; + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + !ADDRESS.test(value.account) || + !HASH.test(value.transactionHash) + ) { + throw new Error(`runtime dataset contains an invalid ${name} launch`); + } + return value; +} + +function requestPath( + route, + sequence, + keyIndex, + keys, + captureNonce, + probeIssuedAtMs, + shadowProbeToken, +) { + const token = datasetAddress(keys.tokenAddresses, keyIndex, "token"); + const account = datasetAddress(keys.accountAddresses, keyIndex, "account"); + const classicLaunch = datasetLaunch( + keys.classicLaunches, + keyIndex, + "Classic", + ); + const stockLaunch = datasetLaunch(keys.stockLaunches, keyIndex, "Stock"); + const releaseProbe = SHADOW_PROBE_ROUTES.has(route) + ? buildReadModelReleaseProbe({ + route, + issuedAtMs: probeIssuedAtMs, + captureNonce, + sequence, + secret: shadowProbeToken, + }) + : null; + const cacheBuster = encodeURIComponent( + releaseProbe?.nonce ?? + `perf-${probeIssuedAtMs}-${captureNonce.slice(2)}-${sequence}`, + ); + const encodedToken = encodeURIComponent(token); + const encodedAccount = encodeURIComponent(account); + const result = (datasetKey, path) => ({ + datasetKey, + key: `${route}:${sequence}:${datasetKey.toLowerCase()}`, + path: `${path}${path.includes("?") ? "&" : "?"}${ + releaseProbe ? "__read_model_probe" : "__performance_probe" + }=${cacheBuster}`, + releaseProbe, + }); + if (route === "exploreList") { + return result( + token, + `/api/explore?limit=6&page=1&q=${encodedToken}&sort=market-cap`, + ); + } + if (route === "tokenDetail") { + return result(token, `/api/explore/token?address=${encodedToken}`); + } + if (route === "tokenChart") { + const range = sequence % 2 === 1 ? "1h" : "all"; + return result( + token, + `/api/explore/token/chart?address=${encodedToken}&range=${range}`, + ); + } + if (route === "creatorProfile") { + return result(account, `/api/explore/profile?account=${encodedAccount}`); + } + if (route === "classicProfile") { + return result(account, `/api/profile/classic-v3?account=${encodedAccount}`); + } + if (route === "stockProfile") { + return result(account, `/api/profile/stock-paired?account=${encodedAccount}`); + } + if (route === "classicLaunchLookup") { + return result( + classicLaunch.transactionHash, + `/api/profile/classic-v3?account=${encodeURIComponent(classicLaunch.account)}&launch=${encodeURIComponent(classicLaunch.transactionHash)}`, + ); + } + if (route === "stockLaunchLookup") { + return result( + stockLaunch.transactionHash, + `/api/explore/launch/stock-paired?account=${encodeURIComponent(stockLaunch.account)}&transaction=${encodeURIComponent(stockLaunch.transactionHash)}`, + ); + } + if (route === "publicIndexer") { + return result( + token, + `/api/indexers/v1/tokens?address=${encodedToken}`, + ); + } + return result("health", "/api/ops/health"); +} + +function optionalIntegerHeader(response, name) { + const value = response.headers.get(name); + if (value === null || !/^(0|[1-9]\d*)$/u.test(value)) return null; + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : null; +} + +function optionalBooleanHeader(response, name) { + const value = response.headers.get(name); + if (value === "true") return true; + if (value === "false") return false; + return null; +} + +function sameAddress(left, right) { + return ( + typeof left === "string" && + typeof right === "string" && + left.toLowerCase() === right.toLowerCase() + ); +} + +function responseMatchesDatasetKey(route, body, datasetKey, expectedRange) { + if (body === null || typeof body !== "object" || Array.isArray(body)) { + return false; + } + if (route === "exploreList") { + return ( + sameAddress(body.query, datasetKey) && + Array.isArray(body.tokens) && + body.tokens.some((token) => sameAddress(token?.tokenAddress, datasetKey)) + ); + } + if (route === "tokenDetail") { + return sameAddress(body.token?.tokenAddress, datasetKey); + } + if (route === "tokenChart") { + return ( + sameAddress(body.address, datasetKey) && + Array.isArray(body.points) && + body.range === expectedRange + ); + } + if (route === "creatorProfile") { + return sameAddress(body.account, datasetKey); + } + if (route === "classicProfile" || route === "stockProfile") { + return sameAddress(body.account, datasetKey) && Array.isArray(body.rewards); + } + if (route === "classicLaunchLookup") { + return ( + typeof body.launch === "object" && + body.launch !== null && + body.launch.launchTransactionHash?.toLowerCase() === + datasetKey.toLowerCase() + ); + } + if (route === "stockLaunchLookup") { + return ( + typeof body.launch === "object" && + body.launch !== null && + body.launch.transactionHash?.toLowerCase() === datasetKey.toLowerCase() + ); + } + if (route === "publicIndexer") { + return sameAddress(body.address, datasetKey); + } + return body.status === "healthy"; +} + +function validSafeInteger(value, minimum = 0) { + return Number.isSafeInteger(value) && value >= minimum; +} + +function eligibleLaunchIndex(eligibleLaunches) { + if (!Array.isArray(eligibleLaunches) || eligibleLaunches.length < 1) { + throw new Error("explore matrix requires the complete eligible launch inventory"); + } + const result = new Map(); + for (const launch of eligibleLaunches) { + if ( + launch === null || + typeof launch !== "object" || + Array.isArray(launch) || + !ADDRESS.test(launch.tokenAddress) || + !REQUIRED_RELEASE_VERSIONS.includes(launch.releaseVersion) + ) { + throw new Error("explore matrix eligible launch is invalid"); + } + const tokenAddress = launch.tokenAddress.toLowerCase(); + if (result.has(tokenAddress)) { + throw new Error("explore matrix eligible token inventory is duplicated"); + } + result.set(tokenAddress, launch.releaseVersion); + } + for (const releaseVersion of REQUIRED_RELEASE_VERSIONS) { + if (![...result.values()].includes(releaseVersion)) { + throw new Error(`explore matrix has no ${releaseVersion} inventory`); + } + } + return result; +} + +function frozenInventoryCommitment(eligibleLaunches) { + return sha256Canonical( + eligibleLaunches + .map((launch) => ({ + tokenAddress: launch.tokenAddress.toLowerCase(), + transactionHash: launch.transactionHash.toLowerCase(), + releaseVersion: launch.releaseVersion, + })) + .sort((left, right) => + left.tokenAddress.localeCompare(right.tokenAddress), + ), + ); +} + +function minimalExploreToken(token, releases) { + if ( + token === null || + typeof token !== "object" || + Array.isArray(token) || + !ADDRESS.test(token.tokenAddress) || + typeof token.name !== "string" || + token.name.length < 1 || + token.name.length > 256 || + typeof token.symbol !== "string" || + token.symbol.length < 1 || + token.symbol.length > 128 + ) { + throw new Error("explore matrix response contains an invalid token identity"); + } + const tokenAddress = token.tokenAddress.toLowerCase(); + const releaseVersion = releases.get(tokenAddress); + if (!releaseVersion) { + throw new Error("explore matrix response contains a non-inventory token"); + } + return Object.freeze({ + tokenAddress, + name: token.name, + symbol: token.symbol, + releaseVersion, + }); +} + +function parseExploreMatrixBody(body, request, releases) { + if (body === null || typeof body !== "object" || Array.isArray(body)) { + throw new Error("explore matrix response is not an object"); + } + if ( + body.status !== "ready" || + !Array.isArray(body.tokens) || + !validSafeInteger(body.page, 1) || + !validSafeInteger(body.pageSize, 1) || + !validSafeInteger(body.total) || + !validSafeInteger(body.totalPages) || + body.sort !== request.sort || + typeof body.query !== "string" || + normalizeExploreMatrixQuery(body.query) !== request.normalizedQuery + ) { + throw new Error("explore matrix response has invalid pagination metadata"); + } + const snapshot = body.snapshot; + if ( + snapshot === null || + typeof snapshot !== "object" || + Array.isArray(snapshot) || + snapshot.chainId !== 1 || + typeof snapshot.blockNumber !== "string" || + !/^(0|[1-9]\d*)$/u.test(snapshot.blockNumber) || + !HASH.test(snapshot.blockHash) || + !validSafeInteger(snapshot.confirmations) + ) { + throw new Error("explore matrix response has no canonical checkpoint"); + } + return { + page: body.page, + pageSize: body.pageSize, + total: body.total, + totalPages: body.totalPages, + sort: body.sort, + tokens: body.tokens.map((token) => minimalExploreToken(token, releases)), + snapshot, + }; +} + +function matchesExploreMatrixQuery(token, normalizedQuery) { + if (normalizedQuery === "") return true; + return ( + token.name.toLowerCase().includes(normalizedQuery) || + token.symbol.toLowerCase().includes(normalizedQuery) || + token.tokenAddress.includes(normalizedQuery) + ); +} + +function boundedQueryCandidates(inventory, kind) { + const field = kind === "name" ? "name" : "symbol"; + const ordered = [...inventory].sort( + (left, right) => + REQUIRED_RELEASE_VERSIONS.indexOf(left.releaseVersion) - + REQUIRED_RELEASE_VERSIONS.indexOf(right.releaseVersion) || + left.tokenAddress.localeCompare(right.tokenAddress), + ); + const candidates = []; + const seenQueries = new Set(); + for (const token of ordered) { + const normalizedQuery = normalizeExploreMatrixQuery(token[field]); + if ( + normalizedQuery.length < 1 || + normalizedQuery.length > 128 || + seenQueries.has(normalizedQuery) + ) { + continue; + } + const matchCount = inventory.filter((candidate) => + matchesExploreMatrixQuery(candidate, normalizedQuery), + ).length; + if (matchCount < 1 || matchCount > EXPLORE_MATRIX_PAGE_SIZE) continue; + seenQueries.add(normalizedQuery); + candidates.push({ token, normalizedQuery }); + } + if (candidates.length < 1) { + throw new Error(`explore matrix has no bounded real ${kind} query case`); + } + return candidates; +} + +function selectedQueryCandidates(candidates) { + const selected = []; + const usedQueries = new Set(); + for (const releaseVersion of REQUIRED_RELEASE_VERSIONS) { + const candidate = candidates.find( + (entry) => + entry.token.releaseVersion === releaseVersion && + !usedQueries.has(entry.normalizedQuery), + ); + if (candidate) { + selected.push(candidate); + usedQueries.add(candidate.normalizedQuery); + } + } + for (const candidate of candidates) { + if (selected.length >= EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND) break; + if (usedQueries.has(candidate.normalizedQuery)) continue; + selected.push(candidate); + usedQueries.add(candidate.normalizedQuery); + } + return selected; +} + +function queryCase(kind, candidate) { + const rawValue = alternatingAsciiCase( + kind === "address" + ? candidate.token.tokenAddress + : candidate.normalizedQuery, + ); + const query = ` ${kind === "symbol" ? "$" : ""}${rawValue} `; + const identityDigest = sha256Canonical({ + kind, + query, + sourceTokenAddress: candidate.token.tokenAddress, + }); + return committedCase({ + caseId: `${kind}-${identityDigest.slice(0, 20)}`, + kind, + query, + normalizedQuery: normalizeExploreMatrixQuery(query), + sourceTokenAddress: candidate.token.tokenAddress, + sourceReleaseVersion: candidate.token.releaseVersion, + }); +} + +export function buildExploreMatrixCases(inventory) { + if (!Array.isArray(inventory) || inventory.length < 1) { + throw new Error("explore matrix inventory is empty"); + } + const nameCases = selectedQueryCandidates( + boundedQueryCandidates(inventory, "name"), + ).map((candidate) => queryCase("name", candidate)); + const symbolCases = selectedQueryCandidates( + boundedQueryCandidates(inventory, "symbol"), + ).map((candidate) => queryCase("symbol", candidate)); + const addressCandidates = selectedQueryCandidates( + inventory.map((token) => ({ + token, + normalizedQuery: token.tokenAddress, + })), + ); + const addressCases = addressCandidates.map((candidate) => + queryCase("address", candidate), + ); + if ( + nameCases.length !== EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND || + symbolCases.length !== EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND || + addressCases.length !== EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND + ) { + throw new Error( + "explore matrix requires eight unique bounded real cases per query kind", + ); + } + return Object.freeze([ + emptyExploreMatrixCase(), + ...nameCases, + ...symbolCases, + ...addressCases, + ]); +} + +function exploreMatrixRequestPath(input) { + const search = new URLSearchParams(); + search.set("limit", String(EXPLORE_MATRIX_PAGE_SIZE)); + search.set("page", String(input.requestedPage)); + search.set("q", input.queryCase.query); + search.set("sort", input.sort); + search.set("__read_model_probe", input.probeNonce); + return `/api/explore?${search.toString()}`; +} + +function pageIdentity(value) { + const identity = { ...value }; + delete identity.pageCommitment; + return identity; +} + +export function commitExploreMatrixPage(value) { + return sha256Canonical(pageIdentity(value)); +} + +export function serializeExploreMatrixPages(pages) { + if (!Array.isArray(pages) || pages.length < 1) { + throw new Error("explore matrix pages are empty"); + } + return `${pages.map((page) => JSON.stringify(page)).join("\n")}\n`; +} + +export function exploreMatrixCorpusCommitment(input) { + return sha256Canonical({ + captureNonce: input.captureNonce, + target: input.target, + datasetManifestSha256: input.datasetManifestSha256, + inventorySha256: input.inventorySha256, + casesSha256: input.casesSha256, + pagesSha256: input.pagesSha256, + checkpointSha256: input.checkpointSha256, + eligibleLaunchCount: input.eligibleLaunchCount, + caseCount: input.caseCount, + pageCount: input.pageCount, + tokenObservationCount: input.tokenObservationCount, + }); +} + +async function captureExploreMatrixPage(input) { + const issuedAtMs = input.now(); + const releaseProbe = buildReadModelReleaseProbe({ + route: "exploreList", + issuedAtMs, + captureNonce: input.captureNonce, + sequence: input.sequence, + secret: input.shadowProbeToken, + }); + const requestPath = exploreMatrixRequestPath({ + queryCase: input.queryCase, + sort: input.sort, + requestedPage: input.requestedPage, + probeNonce: releaseProbe.nonce, + }); + const startedAtMs = input.now(); + const response = await input.fetchImpl(new URL(requestPath, input.targetUrl), { + headers: { + Accept: "application/json", + "x-vercel-protection-bypass": input.automationBypassSecret, + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-signature": releaseProbe.signature, + }, + redirect: "error", + signal: AbortSignal.timeout(input.probeTimeoutMs), + }); + const bodyBytes = Buffer.from(await response.arrayBuffer()); + const completedAtMs = input.now(); + let body; + try { + body = JSON.parse(bodyBytes.toString("utf8")); + } catch { + throw new Error("explore matrix response is not JSON"); + } + const parsed = parseExploreMatrixBody( + body, + { ...input.queryCase, sort: input.sort }, + input.releases, + ); + const parity = response.headers.get("x-programmable-shadow-parity") ?? "missing"; + const readSource = response.headers.get("x-programmable-read-source") ?? "missing"; + const fallback = optionalBooleanHeader( + response, + "x-programmable-live-fallback", + ); + const shadowOverheadMs = optionalIntegerHeader( + response, + "x-programmable-shadow-overhead-ms", + ); + const record = { + schemaVersion: 1, + sequence: input.sequence, + probeIssuedAtMs: issuedAtMs, + probeNonce: releaseProbe.nonce, + probeSignatureSha256: sha256Bytes( + Buffer.from(releaseProbe.signature, "hex"), + ), + caseId: input.queryCase.caseId, + caseCommitment: input.queryCase.commitment, + sort: input.sort, + requestedPage: input.requestedPage, + resolvedPage: parsed.page, + pageSize: parsed.pageSize, + total: parsed.total, + totalPages: parsed.totalPages, + isClamp: input.isClamp, + requestPath, + startedAtMs, + completedAtMs, + durationMs: completedAtMs - startedAtMs, + status: response.status, + cacheControl: response.headers.get("cache-control") ?? "missing", + vercelCache: response.headers.get("x-vercel-cache") ?? "NONE", + shadowOverheadMs, + parity, + readSource, + fallback, + checkpointSha256: sha256Canonical(parsed.snapshot), + bodySha256: sha256Bytes(bodyBytes), + bodyBytes: bodyBytes.byteLength, + tokenRowsSha256: sha256Canonical(parsed.tokens), + tokens: parsed.tokens, + }; + return { + page: Object.freeze({ + ...record, + pageCommitment: commitExploreMatrixPage(record), + }), + snapshot: parsed.snapshot, + }; +} + +async function captureExploreMatrixPlans(input, plans, sequenceState) { + const results = []; + for ( + let offset = 0; + offset < plans.length; + offset += input.concurrency + ) { + const batchPlans = plans.slice(offset, offset + input.concurrency); + const batchInputs = batchPlans.map((plan) => ({ + ...plan, + sequence: sequenceState.value++, + })); + results.push( + ...(await Promise.all( + batchInputs.map((plan) => + captureExploreMatrixPage({ ...input, ...plan }), + ), + )), + ); + } + return results; +} + +function continuationPlans(firstPages, caseById) { + return firstPages.flatMap(({ page }) => { + if ( + !validSafeInteger(page.totalPages) || + page.totalPages > EXPLORE_MATRIX_MAXIMUM_PAGES_PER_CASE + ) { + throw new Error("explore matrix page count exceeds the bounded corpus"); + } + const queryCase = caseById.get(page.caseId); + if (!queryCase) throw new Error("explore matrix case binding is missing"); + return [ + ...Array.from( + { length: Math.max(0, page.totalPages - 1) }, + (_, index) => ({ + queryCase, + sort: page.sort, + requestedPage: index + 2, + isClamp: false, + }), + ), + { + queryCase, + sort: page.sort, + requestedPage: EXPLORE_MATRIX_CLAMP_PAGE, + isClamp: true, + }, + ]; + }); +} + +function inventoryFromEmptyPages(pages, releases) { + const newestPages = pages + .filter( + (page) => + page.caseId === "empty" && + page.sort === "newest" && + page.isClamp === false, + ) + .sort((left, right) => left.requestedPage - right.requestedPage); + const inventory = newestPages.flatMap((page) => page.tokens); + const observed = inventory.map((token) => token.tokenAddress); + const expected = [...releases.keys()].sort(); + if ( + new Set(observed).size !== observed.length || + observed.length !== expected.length || + [...observed].sort().some((value, index) => value !== expected[index]) + ) { + throw new Error( + "explore matrix empty traversal does not equal the frozen launch inventory", + ); + } + return inventory; +} + +export async function captureExploreMatrix(input) { + const releases = eligibleLaunchIndex(input.datasetManifest.eligibleLaunches); + const automationBypassSecret = requiredSecret( + input.automationBypassSecret, + "VERCEL_AUTOMATION_BYPASS_SECRET", + ); + const common = { + targetUrl: input.targetUrl, + captureNonce: input.captureNonce, + shadowProbeToken: input.shadowProbeToken, + automationBypassSecret, + probeTimeoutMs: input.probeTimeoutMs, + releases, + fetchImpl: input.fetchImpl ?? fetch, + now: input.now ?? Date.now, + concurrency: Math.max(1, Math.min(20, input.concurrency ?? 8)), + }; + const sequenceState = { value: 0 }; + const emptyCase = emptyExploreMatrixCase(); + const emptyFirst = await captureExploreMatrixPlans( + common, + EXPLORE_MATRIX_SORTS.map((sort) => ({ + queryCase: emptyCase, + sort, + requestedPage: 1, + isClamp: false, + })), + sequenceState, + ); + const emptyCaseById = new Map([[emptyCase.caseId, emptyCase]]); + const emptyRest = await captureExploreMatrixPlans( + common, + continuationPlans(emptyFirst, emptyCaseById), + sequenceState, + ); + const emptyPages = [...emptyFirst, ...emptyRest].map((entry) => entry.page); + const inventory = inventoryFromEmptyPages(emptyPages, releases); + const cases = buildExploreMatrixCases(inventory); + const queryCases = cases.filter((queryCase) => queryCase.kind !== "empty"); + const caseById = new Map(cases.map((queryCase) => [queryCase.caseId, queryCase])); + const queryFirst = await captureExploreMatrixPlans( + common, + queryCases.flatMap((queryCase) => + EXPLORE_MATRIX_SORTS.map((sort) => ({ + queryCase, + sort, + requestedPage: 1, + isClamp: false, + })), + ), + sequenceState, + ); + const queryRest = await captureExploreMatrixPlans( + common, + continuationPlans(queryFirst, caseById), + sequenceState, + ); + const captured = [...emptyFirst, ...emptyRest, ...queryFirst, ...queryRest]; + const pages = captured.map((entry) => entry.page); + const pagesBytes = Buffer.from(serializeExploreMatrixPages(pages), "utf8"); + const pagesSha256 = sha256Bytes(pagesBytes); + const casesSha256 = sha256Canonical(cases); + const checkpoint = captured[0]?.snapshot; + if (!checkpoint) throw new Error("explore matrix checkpoint is missing"); + const checkpointSha256 = sha256Canonical(checkpoint); + const inventorySha256 = frozenInventoryCommitment( + input.datasetManifest.eligibleLaunches, + ); + const target = { + url: input.targetUrl.toString(), + vercelDeploymentId: input.deploymentId, + gitHead: input.gitHead, + }; + const caseCounts = Object.fromEntries( + ["empty", "name", "symbol", "address"].map((kind) => [ + kind, + cases.filter((queryCase) => queryCase.kind === kind).length, + ]), + ); + const tokenObservationCount = pages.reduce( + (total, page) => total + page.tokens.length, + 0, + ); + const corpusInput = { + captureNonce: input.captureNonce, + target, + datasetManifestSha256: input.datasetManifestSha256, + inventorySha256, + casesSha256, + pagesSha256, + checkpointSha256, + eligibleLaunchCount: input.datasetManifest.eligibleLaunches.length, + caseCount: cases.length, + pageCount: pages.length, + tokenObservationCount, + }; + const manifest = Object.freeze({ + schemaVersion: 1, + profileId: input.profileId, + captureNonce: input.captureNonce, + capturedAt: new Date((input.now ?? Date.now)()).toISOString(), + target, + dataset: { + manifestFile: "dataset-manifest.v1.json", + manifestSha256: input.datasetManifestSha256, + generatedAt: input.datasetManifest.generatedAt, + eligibleLaunchCount: input.datasetManifest.eligibleLaunches.length, + releaseCounts: input.datasetManifest.releaseCounts, + inventorySha256, + }, + checkpoint: { + snapshot: checkpoint, + snapshotSha256: checkpointSha256, + }, + matrix: { + sorts: EXPLORE_MATRIX_SORTS, + pageSize: EXPLORE_MATRIX_PAGE_SIZE, + maxQueryCasesPerKind: EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND, + cases, + casesSha256, + caseCounts, + caseCount: cases.length, + pageCount: pages.length, + tokenObservationCount, + pagesFile: EXPLORE_MATRIX_PAGES_FILE, + pagesSha256, + corpusSha256: exploreMatrixCorpusCommitment(corpusInput), + }, + }); + if (!HEX_DIGEST.test(manifest.matrix.corpusSha256)) { + throw new Error("explore matrix corpus commitment failed"); + } + return { manifest, pages, pagesBytes }; +} + +async function captureSample(input) { + const request = requestPath( + input.route, + input.sequence, + input.keyIndex, + input.keys, + input.captureNonce, + input.probeIssuedAtMs, + input.shadowProbeToken, + ); + const startedAtMs = Date.now(); + const headers = { Accept: "application/json" }; + headers["x-vercel-protection-bypass"] = input.automationBypassSecret; + const shadowProbe = SHADOW_PROBE_ROUTES.has(input.route); + if (shadowProbe) { + headers["x-programmable-shadow-probe"] = "1"; + headers["x-programmable-shadow-probe-signature"] = + request.releaseProbe.signature; + } + try { + const response = await fetch(new URL(request.path, input.targetUrl), { + headers, + redirect: "error", + signal: AbortSignal.timeout(input.probeTimeoutMs), + }); + const body = Buffer.from(await response.arrayBuffer()); + const completedAtMs = Date.now(); + const parity = response.headers.get("x-programmable-shadow-parity"); + const readSource = response.headers.get("x-programmable-read-source"); + let parsedBody; + try { + parsedBody = JSON.parse(body.toString("utf8")); + } catch { + parsedBody = null; + } + return { + route: input.route, + requestKey: request.key, + datasetKey: request.datasetKey, + keyMatched: responseMatchesDatasetKey( + input.route, + parsedBody, + request.datasetKey, + input.route === "tokenChart" + ? input.sequence % 2 === 1 + ? "1h" + : "all" + : undefined, + ), + startedAtMs, + completedAtMs, + durationMs: completedAtMs - startedAtMs, + status: response.status, + cacheControl: response.headers.get("cache-control") ?? "missing", + vercelCache: response.headers.get("x-vercel-cache") ?? "NONE", + bodySha256: sha256Bytes(body), + bodyBytes: body.byteLength, + shadowOverheadMs: optionalIntegerHeader( + response, + "x-programmable-shadow-overhead-ms", + ), + parity: + parity === "match" || + parity === "mismatch" || + parity === "incomparable" + ? parity + : shadowProbe + ? "missing" + : "not-observed", + readSource: + readSource === "rpc" || readSource === "blob" || readSource === "indexed" + ? readSource + : shadowProbe + ? "missing" + : "not-observed", + fallback: + shadowProbe + ? optionalBooleanHeader( + response, + "x-programmable-live-fallback", + ) + : null, + }; + } catch (error) { + const completedAtMs = Date.now(); + const body = Buffer.from( + error instanceof Error ? error.name : "RequestError", + ); + return { + route: input.route, + requestKey: request.key, + datasetKey: request.datasetKey, + keyMatched: false, + startedAtMs, + completedAtMs, + durationMs: completedAtMs - startedAtMs, + status: 599, + cacheControl: "missing", + vercelCache: "NONE", + bodySha256: sha256Bytes(body), + bodyBytes: body.byteLength, + shadowOverheadMs: null, + parity: shadowProbe ? "missing" : "not-observed", + readSource: shadowProbe ? "missing" : "not-observed", + fallback: null, + }; + } +} + +function exactKeys(value, expected, subject) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${subject} must be an object`); + } + const actual = Object.keys(value).sort(); + const sortedExpected = [...expected].sort(); + if ( + actual.length !== sortedExpected.length || + actual.some((key, index) => key !== sortedExpected[index]) + ) { + throw new Error(`${subject} has an unexpected shape`); + } + return value; +} + +async function runtimeEvidence(input) { + const startedAtMs = Date.now(); + const requestBody = JSON.stringify({ + schemaVersion: 2, + profileId: input.profile.profileId, + gitHead: input.gitHead, + targetUrl: input.targetUrl.toString(), + vercelDeploymentId: input.deploymentId, + captureNonce: input.captureNonce, + issuedAtMs: startedAtMs, + }); + const releaseSignature = createHmac( + "sha256", + input.performanceProbeToken, + ) + .update(requestBody, "utf8") + .digest("hex"); + const response = await fetch( + new URL(RUNTIME_CAPTURE_PATH, input.targetUrl), + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-programmable-performance-probe": "1", + "x-programmable-performance-probe-token": input.performanceProbeToken, + "x-programmable-release-capture-signature": `v1=${releaseSignature}`, + "x-vercel-protection-bypass": input.automationBypassSecret, + }, + body: requestBody, + redirect: "error", + signal: AbortSignal.timeout(input.profile.projector.hostingDeadlineMs), + }, + ); + const declaredLength = Number(response.headers.get("content-length")); + if ( + !response.ok || + (Number.isFinite(declaredLength) && + declaredLength > MAX_RUNTIME_EVIDENCE_BYTES) || + response.headers.get("cache-control") !== "private, no-store" + ) { + throw new Error("staged runtime evidence endpoint rejected the capture"); + } + const bytes = Buffer.from(await response.arrayBuffer()); + const completedAtMs = Date.now(); + if (bytes.byteLength < 2 || bytes.byteLength > MAX_RUNTIME_EVIDENCE_BYTES) { + throw new Error("staged runtime evidence has an invalid size"); + } + let payload; + try { + payload = JSON.parse(bytes.toString("utf8")); + } catch { + throw new Error("staged runtime evidence is not JSON"); + } + const envelope = exactKeys( + payload, + ["schemaVersion", "captureNonce", "datasetManifest", "rpcTrace"], + "runtime evidence", + ); + const rpcTrace = exactKeys( + envelope.rpcTrace, + [ + "schemaVersion", + "profileId", + "gitHead", + "targetUrl", + "vercelDeploymentId", + "captureNonce", + "startedAtMs", + "completedAtMs", + "candidateBatchSize", + "hardDeadlineMs", + "maxCallsPerProvider", + "elapsedMs", + "providerCallCounts", + "candidateEvidence", + "calls", + ], + "runtime RPC trace", + ); + const datasetManifest = exactKeys( + envelope.datasetManifest, + [ + "schemaVersion", + "profileId", + "generatedAt", + "counts", + "releaseCounts", + "eligibleLaunches", + "accountEvidence", + "accessEvidence", + "keys", + ], + "runtime dataset manifest", + ); + const datasetGeneratedAtMs = Date.parse(datasetManifest.generatedAt); + if ( + envelope.schemaVersion !== 1 || + envelope.captureNonce !== input.captureNonce || + rpcTrace.schemaVersion !== 1 || + rpcTrace.profileId !== input.profile.profileId || + rpcTrace.gitHead !== input.gitHead || + new URL(rpcTrace.targetUrl).toString() !== input.targetUrl.toString() || + rpcTrace.vercelDeploymentId !== input.deploymentId || + rpcTrace.captureNonce !== input.captureNonce || + !Number.isSafeInteger(rpcTrace.startedAtMs) || + !Number.isSafeInteger(rpcTrace.completedAtMs) || + rpcTrace.startedAtMs < startedAtMs - 5_000 || + rpcTrace.completedAtMs > completedAtMs + 5_000 || + datasetManifest.schemaVersion !== 1 || + datasetManifest.profileId !== input.profile.profileId || + !Number.isFinite(datasetGeneratedAtMs) || + datasetGeneratedAtMs < startedAtMs - 5_000 || + datasetGeneratedAtMs > completedAtMs + 5_000 + ) { + throw new Error("staged runtime evidence is not bound to this capture"); + } + return { datasetManifest, rpcTrace }; +} + +function exclusiveWrite(path, contents) { + writeFileSync(path, contents, { flag: "wx", mode: 0o600 }); +} + +export async function main(argv = process.argv.slice(2)) { + const rootDirectory = process.cwd(); + const args = argumentsFrom(argv); + const profile = parseReadModelLoadProfile( + JSON.parse( + readFileSync( + resolve(rootDirectory, "config/read-model-release-profile.v1.json"), + "utf8", + ), + ), + ); + const targetUrl = new URL(args["target-url"]); + if ( + targetUrl.protocol !== "https:" || + targetUrl.username !== "" || + targetUrl.password !== "" || + targetUrl.pathname !== "/" || + targetUrl.search !== "" || + targetUrl.hash !== "" || + !targetUrl.hostname.endsWith(".vercel.app") + ) { + throw new Error("--target-url must be a deployment-specific Vercel URL"); + } + if (!/^dpl_[A-Za-z0-9]{20,80}$/u.test(args["deployment-id"])) { + throw new Error("--deployment-id must be a Vercel deployment id"); + } + const outputDirectory = resolve(args["output-directory"]); + mkdirSync(outputDirectory, { recursive: true, mode: 0o700 }); + const currentGitHead = gitHead(rootDirectory); + const captureNonce = `0x${randomBytes(32).toString("hex")}`; + if (!BYTES32.test(captureNonce)) throw new Error("capture nonce failed"); + const performanceProbeToken = secret( + process.env, + "PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN", + ); + const shadowProbeToken = secret( + process.env, + "PROGRAMMABLE_SHADOW_PROBE_TOKEN", + ); + const automationBypassSecret = secret( + process.env, + "VERCEL_AUTOMATION_BYPASS_SECRET", + ); + const capturedRuntime = await runtimeEvidence({ + targetUrl, + deploymentId: args["deployment-id"], + profile, + gitHead: currentGitHead, + captureNonce, + performanceProbeToken, + automationBypassSecret, + }); + if (!Array.isArray(capturedRuntime.datasetManifest.eligibleLaunches)) { + throw new Error("runtime dataset has no eligible launch corpus"); + } + const loadKeys = { + tokenAddresses: capturedRuntime.datasetManifest.keys.tokenAddresses, + accountAddresses: capturedRuntime.datasetManifest.keys.accountAddresses, + classicLaunches: capturedRuntime.datasetManifest.keys.classicLaunches, + stockLaunches: capturedRuntime.datasetManifest.keys.stockLaunches, + }; + const schedule = deterministicSchedule(profile); + const probeIssuedAtMs = Date.now(); + const samples = []; + const routeKeyClass = (route) => + ["exploreList", "tokenDetail", "tokenChart", "publicIndexer"].includes(route) + ? "token" + : ["creatorProfile", "classicProfile", "stockProfile"].includes(route) + ? "account" + : route === "classicLaunchLookup" + ? "classic" + : route === "stockLaunchLookup" + ? "stock" + : "health"; + const keyClassIndexes = new Map( + ["token", "account", "classic", "stock", "health"].map((key) => [key, 0]), + ); + if ( + schedule.length !== profile.load.minimumCompletedRequests || + schedule.length % profile.load.concurrency !== 0 + ) { + throw new Error("load schedule must be one exact concurrency-aligned cycle"); + } + const batchCount = schedule.length / profile.load.concurrency; + const captureDurationMs = profile.load.durationSeconds * 1_000; + let loadAnchorMs; + for (let batchIndex = 0; batchIndex < batchCount; batchIndex += 1) { + if (batchIndex > 0 && batchCount > 1) { + const scheduledStart = + loadAnchorMs + + Math.floor((batchIndex * captureDurationMs) / (batchCount - 1)); + const delayMs = scheduledStart - Date.now(); + if (delayMs > 0) { + await new Promise((resolveDelay) => setTimeout(resolveDelay, delayMs)); + } + } + const batchStart = samples.length; + const routes = Array.from( + { length: profile.load.concurrency }, + (_, offset) => { + const route = schedule[(batchStart + offset) % schedule.length]; + const keyClass = routeKeyClass(route); + const keyIndex = keyClassIndexes.get(keyClass); + keyClassIndexes.set(keyClass, keyIndex + 1); + return { route, keyIndex }; + }, + ); + const batch = await Promise.all( + routes.map(({ route, keyIndex }, offset) => + captureSample({ + targetUrl, + route, + sequence: batchStart + offset, + keyIndex, + keys: loadKeys, + captureNonce, + probeIssuedAtMs, + shadowProbeToken, + automationBypassSecret, + probeTimeoutMs: profile.load.probeTimeoutMs, + }), + ), + ); + samples.push(...batch); + if (batchIndex === 0) { + loadAnchorMs = Math.min(...batch.map((sample) => sample.startedAtMs)); + } + } + + const datasetFile = "dataset-manifest.v1.json"; + const samplesFile = "http-samples.v1.jsonl"; + const rpcTraceFile = "rpc-trace.v1.json"; + const datasetContents = `${JSON.stringify(capturedRuntime.datasetManifest, null, 2)}\n`; + const datasetManifestSha256 = sha256Bytes( + Buffer.from(datasetContents, "utf8"), + ); + const exploreMatrix = await captureExploreMatrix({ + targetUrl, + deploymentId: args["deployment-id"], + profileId: profile.profileId, + gitHead: currentGitHead, + captureNonce, + shadowProbeToken, + automationBypassSecret, + probeTimeoutMs: profile.load.probeTimeoutMs, + concurrency: profile.load.concurrency, + datasetManifest: capturedRuntime.datasetManifest, + datasetManifestSha256, + }); + + exclusiveWrite(resolve(outputDirectory, datasetFile), datasetContents); + exclusiveWrite( + resolve(outputDirectory, rpcTraceFile), + `${JSON.stringify(capturedRuntime.rpcTrace, null, 2)}\n`, + ); + exclusiveWrite( + resolve(outputDirectory, samplesFile), + `${samples.map((sample) => JSON.stringify(sample)).join("\n")}\n`, + ); + exclusiveWrite( + resolve(outputDirectory, EXPLORE_MATRIX_PAGES_FILE), + exploreMatrix.pagesBytes, + ); + exclusiveWrite( + resolve(outputDirectory, EXPLORE_MATRIX_MANIFEST_FILE), + `${JSON.stringify(exploreMatrix.manifest, null, 2)}\n`, + ); + const artifactDescriptor = (file) => ({ + file, + sha256: sha256Bytes(readFileSync(resolve(outputDirectory, file))), + }); + const evidence = { + schemaVersion: 1, + profileId: profile.profileId, + evidenceKind: args.kind, + capturedAt: new Date().toISOString(), + captureNonce, + target: { + url: targetUrl.toString(), + vercelDeploymentId: args["deployment-id"], + gitHead: currentGitHead, + }, + artifacts: { + datasetManifest: artifactDescriptor(datasetFile), + httpSamples: artifactDescriptor(samplesFile), + rpcTrace: artifactDescriptor(rpcTraceFile), + }, + }; + const evidencePath = resolve( + outputDirectory, + "read-model-release-evidence.v1.json", + ); + exclusiveWrite(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`); + if (args["github-output"]) { + appendFileSync( + resolve(args["github-output"]), + `evidence_path=${evidencePath}\nevidence_directory=${outputDirectory}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + } + const outputArtifacts = { + ...evidence.artifacts, + exploreMatrixManifest: artifactDescriptor(EXPLORE_MATRIX_MANIFEST_FILE), + exploreMatrixPages: artifactDescriptor(EXPLORE_MATRIX_PAGES_FILE), + }; + process.stdout.write( + `${JSON.stringify({ + mode: "capture", + releaseEvidenceAccepted: false, + evidencePath, + sampleCount: samples.length, + exploreMatrixPageCount: exploreMatrix.pages.length, + artifacts: Object.fromEntries( + Object.entries(outputArtifacts).map(([key, value]) => [ + key, + { file: basename(value.file), sha256: value.sha256 }, + ]), + ), + })}\n`, + ); +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + await main(); +} diff --git a/scripts/perf/read-model-deploy-policy.mjs b/scripts/perf/read-model-deploy-policy.mjs new file mode 100644 index 00000000..da20ee71 --- /dev/null +++ b/scripts/perf/read-model-deploy-policy.mjs @@ -0,0 +1,632 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { runtimeProductionProviderBindingsFromUrls } from "./read-model-provider-binding.mjs"; + +export const RELEASE_GATED_FLAG_NAMES = Object.freeze([ + "INDEXED_EXPLORE_LIST_READS_ENABLED", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED", + "INDEXED_EXPLORE_CHART_READS_ENABLED", + "INDEXED_CREATOR_PROFILE_READS_ENABLED", + "INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED", + "INDEXED_LAUNCH_LOOKUP_ENABLED", + "INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", + "INDEXED_READ_SHADOW_COMPARE_ENABLED", +]); +const PUBLIC_INDEXED_ROUTE_FLAG_NAMES = Object.freeze( + RELEASE_GATED_FLAG_NAMES.filter( + (name) => name !== "INDEXED_READ_SHADOW_COMPARE_ENABLED", + ), +); + +export const WORKER_ACTIVATION_FLAG_NAMES = Object.freeze([ + "PROGRAMMABLE_PROJECTOR_ACTIVE", + "PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE", +]); + +export const REQUIRED_NON_SECRET_RUNTIME_ENV_NAMES = Object.freeze([ + "PROGRAMMABLE_ENVIO_GRAPHQL_URL", + "PROGRAMMABLE_PROJECTOR_BINDING_MODE", + "PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY", + "PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT", + "PROGRAMMABLE_SOURCE_PROJECTOR_VERSION", + "PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL", + "PROGRAMMABLE_UNISWAP_GRAPH_REDACTED_IDENTITY", + "PROGRAMMABLE_UNISWAP_GRAPH_DEPLOYMENT_COMMITMENT", + "PROGRAMMABLE_UNISWAP_GRAPH_SCHEMA_COMMITMENT", +]); + +const CANONICAL_PRODUCTION_ORIGIN = "https://programmable.family"; +const EXPECTED_SOURCE_PROJECTOR_VERSION = "projector-v1"; +const EXPECTED_PROJECTOR_BINDING_MODE = "release"; +const EXPECTED_PROJECTOR_ENVIO_MIRROR_COMMIT = + "7ffd15c2a28c481a2d3632e30b315262c2471b2e"; +const EXPECTED_UNISWAP_GRAPH_BASE_URL = "https://gateway.thegraph.com"; +const EXPECTED_UNISWAP_GRAPH_REDACTED_IDENTITY = "uniswap-v4-official"; +const EXPECTED_UNISWAP_GRAPH_DEPLOYMENT_COMMITMENT = + "0x44c8d7127503563653f7f53ea339caa383453e00224a6c33cf95fc29f5c3e35c"; +const EXPECTED_UNISWAP_GRAPH_SCHEMA_COMMITMENT = + "0xd0d2087059ca0a7c1e7c633999ff75ea34fcc00d42cee8985a79d0ef76e6813c"; + +const COMMITMENT_NAMES = Object.freeze([ + "PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT", + "PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT", +]); +const HEX_BYTES32 = /^0x[0-9a-f]{64}$/u; +const RUNTIME_RPC_URL_NAMES = Object.freeze([ + "PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL", + "PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL", + "ETHEREUM_RPC_URL", + "ETHEREUM_RPC_URL_B", +]); +const VERCEL_SENSITIVE_PLACEHOLDER = /^\[[A-Za-z]{1,32}\]$/u; + +function decodeDotenvValue(value, name) { + const trimmed = value.trim(); + if ( + trimmed.length >= 2 && + ((trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'"))) + ) { + return trimmed.slice(1, -1); + } + if (/^[^\s#]*$/u.test(trimmed)) return trimmed; + throw new Error(`${name} has an unsupported dotenv encoding`); +} + +function readSelectedDotenvValues(contents, selectedNames) { + const values = new Map(); + for (const [index, line] of contents.split(/\r?\n/u).entries()) { + const trimmed = line.trim(); + if (trimmed === "" || trimmed.startsWith("#")) continue; + const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(line); + if (!match) continue; + const [, name, rawValue] = match; + if (!selectedNames.includes(name)) continue; + if (values.has(name)) { + throw new Error(`${name} is duplicated at line ${index + 1}`); + } + values.set(name, decodeDotenvValue(rawValue, name)); + } + return Object.fromEntries(selectedNames.map((name) => [name, values.get(name)])); +} + +export function readReleaseGatedFlags(contents) { + return readSelectedDotenvValues(contents, RELEASE_GATED_FLAG_NAMES); +} + +function normalizedFlags(raw, names, missingIsFalse) { + const values = {}; + const invalidNames = []; + for (const name of names) { + const value = raw[name]; + if ((value === undefined || value === "") && missingIsFalse) { + values[name] = false; + } else if (value === "true" || value === "false") { + values[name] = value === "true"; + } else { + values[name] = value !== "false"; + invalidNames.push(name); + } + } + return Object.freeze({ + values: Object.freeze(values), + invalidNames: Object.freeze(invalidNames), + }); +} + +function parseReleaseExpectations(rootDirectory) { + const manifestPath = resolve( + rootDirectory, + "config/data-pipeline-release.v1.json", + ); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + const deploymentLabel = manifest?.envio?.deploymentLabel; + const graphqlEndpoint = manifest?.envio?.graphqlEndpoint; + if ( + typeof deploymentLabel !== "string" || + !/^[a-z0-9][a-z0-9._-]{0,95}$/u.test(deploymentLabel) || + typeof graphqlEndpoint !== "string" || + !/^https:\/\/indexer\.hyperindex\.xyz\/[a-z0-9]{7}\/(?:v1)\/graphql$/u.test( + graphqlEndpoint, + ) + ) { + throw new Error("data-pipeline release has an invalid Envio binding"); + } + return Object.freeze({ + PROGRAMMABLE_ENVIO_GRAPHQL_URL: graphqlEndpoint, + PROGRAMMABLE_PROJECTOR_BINDING_MODE: EXPECTED_PROJECTOR_BINDING_MODE, + PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY: `envio:${deploymentLabel}`, + PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT: + EXPECTED_PROJECTOR_ENVIO_MIRROR_COMMIT, + PROGRAMMABLE_SOURCE_PROJECTOR_VERSION: + EXPECTED_SOURCE_PROJECTOR_VERSION, + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: + EXPECTED_UNISWAP_GRAPH_BASE_URL, + PROGRAMMABLE_UNISWAP_GRAPH_REDACTED_IDENTITY: + EXPECTED_UNISWAP_GRAPH_REDACTED_IDENTITY, + PROGRAMMABLE_UNISWAP_GRAPH_DEPLOYMENT_COMMITMENT: + EXPECTED_UNISWAP_GRAPH_DEPLOYMENT_COMMITMENT, + PROGRAMMABLE_UNISWAP_GRAPH_SCHEMA_COMMITMENT: + EXPECTED_UNISWAP_GRAPH_SCHEMA_COMMITMENT, + }); +} + +export function readReleasePolicyExpectations( + rootDirectory = process.cwd(), +) { + return parseReleaseExpectations(rootDirectory); +} + +function validateNonSecretRuntimeEnvironment(contents, expectations) { + if (!expectations) { + return Object.freeze({ ready: true, invalidNames: Object.freeze([]) }); + } + const configured = readSelectedDotenvValues( + contents, + REQUIRED_NON_SECRET_RUNTIME_ENV_NAMES, + ); + const invalidNames = REQUIRED_NON_SECRET_RUNTIME_ENV_NAMES.filter( + (name) => + typeof configured[name] !== "string" || + configured[name] === "" || + configured[name] !== expectations[name], + ); + return Object.freeze({ + ready: invalidNames.length === 0, + invalidNames: Object.freeze(invalidNames), + }); +} + +export function evaluateReadModelDeployPolicy( + contents, + environment = {}, + expectations, +) { + const indexedFlags = normalizedFlags( + readReleaseGatedFlags(contents), + RELEASE_GATED_FLAG_NAMES, + false, + ); + const workerFlags = normalizedFlags( + readSelectedDotenvValues(contents, WORKER_ACTIVATION_FLAG_NAMES), + WORKER_ACTIVATION_FLAG_NAMES, + true, + ); + const nonLegacyFlags = [ + ...RELEASE_GATED_FLAG_NAMES.filter((name) => indexedFlags.values[name]), + ...WORKER_ACTIVATION_FLAG_NAMES.filter((name) => workerFlags.values[name]), + ]; + const invalidFlagNames = Object.freeze([ + ...indexedFlags.invalidNames, + ...workerFlags.invalidNames, + ]); + const environmentPreflight = validateNonSecretRuntimeEnvironment( + contents, + expectations, + ); + const evidenceRequired = nonLegacyFlags.length > 0; + const invalidCommitments = evidenceRequired + ? COMMITMENT_NAMES.filter( + (name) => !HEX_BYTES32.test(environment[name] ?? ""), + ) + : []; + let runtimeCommitmentsMatch = !evidenceRequired; + let runtimeProviderBinding = evidenceRequired ? "unverified" : "not-required"; + if (evidenceRequired && invalidCommitments.length === 0) { + try { + const runtimeEnvironment = readSelectedDotenvValues( + contents, + RUNTIME_RPC_URL_NAMES, + ); + const configuredRuntimeValues = Object.values(runtimeEnvironment).filter( + (value) => value !== undefined && value !== "", + ); + const sensitiveValuesDeferred = + configuredRuntimeValues.length >= 2 && + configuredRuntimeValues.every((value) => + VERCEL_SENSITIVE_PLACEHOLDER.test(value), + ); + if (sensitiveValuesDeferred) { + // Vercel deliberately replaces Sensitive environment values during + // `vercel pull`. The staged runtime capture recomputes both endpoint + // commitments from the real URLs and the release gate compares them + // with the pinned GitHub environment variables before promotion. + runtimeCommitmentsMatch = true; + runtimeProviderBinding = "deferred-stage"; + } else { + const runtimeBindings = runtimeProductionProviderBindingsFromUrls( + runtimeEnvironment, + ); + runtimeCommitmentsMatch = runtimeBindings.every( + (binding) => + binding.endpointCommitment === + environment[ + binding.vendorGroup === "alchemy" + ? COMMITMENT_NAMES[0] + : COMMITMENT_NAMES[1] + ], + ); + runtimeProviderBinding = runtimeCommitmentsMatch + ? "verified" + : "unverified"; + } + } catch { + runtimeCommitmentsMatch = false; + runtimeProviderBinding = "unverified"; + } + } + return Object.freeze({ + mode: evidenceRequired ? "indexed-or-shadow" : "legacy-only", + evidenceRequired, + nonLegacyFlags, + indexedFlags: indexedFlags.values, + workerActivationFlags: workerFlags.values, + policyReady: + invalidFlagNames.length === 0 && environmentPreflight.ready, + invalidFlagNames, + invalidNonSecretEnvironmentNames: environmentPreflight.invalidNames, + commitmentsReady: + invalidCommitments.length === 0 && runtimeCommitmentsMatch, + runtimeProviderBinding, + invalidCommitmentNames: + invalidCommitments.length > 0 + ? invalidCommitments + : runtimeCommitmentsMatch + ? [] + : ["runtime-provider-commitment-mismatch"], + }); +} + +function exactHttpsOrigin(value, subject) { + let target; + try { + target = new URL(value); + } catch { + throw new Error(`${subject} must be an exact HTTPS origin`); + } + if ( + target.protocol !== "https:" || + target.username !== "" || + target.password !== "" || + target.pathname !== "/" || + target.search !== "" || + target.hash !== "" + ) { + throw new Error(`${subject} must be an exact HTTPS origin`); + } + return target; +} + +export function canonicalJson(value) { + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + return JSON.stringify(value); + } + if (typeof value === "number" && Number.isSafeInteger(value)) { + return JSON.stringify(value); + } + if (Array.isArray(value)) { + return `[${value.map(canonicalJson).join(",")}]`; + } + if (typeof value === "object") { + return `{${Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`) + .join(",")}}`; + } + throw new Error("release attestation contains a non-canonical value"); +} + +function exactObjectKeys(value, keys, label) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + Object.getPrototypeOf(value) !== Object.prototype || + Object.keys(value).sort().join("\0") !== [...keys].sort().join("\0") + ) { + throw new Error(`${label} shape is invalid`); + } + return value; +} + +function exactBooleanRecord(value, keys, label) { + const record = exactObjectKeys(value, keys, label); + if (keys.some((key) => typeof record[key] !== "boolean")) { + throw new Error(`${label} values are invalid`); + } + return Object.freeze(Object.fromEntries(keys.map((key) => [key, record[key]]))); +} + +export function validateStagedReleaseAttestation(value, expectations = {}) { + const attestation = exactObjectKeys( + value, + [ + "schemaVersion", + "verifiedSha", + "vercelProjectId", + "stagedDeploymentId", + "stagedDeploymentUrl", + "productionOrigin", + "policyMode", + "indexedFlags", + "workerActivationFlags", + "timestamp", + ], + "staged release attestation", + ); + if ( + attestation.schemaVersion !== 1 || + !/^[0-9a-f]{40}$/u.test(attestation.verifiedSha ?? "") || + !/^prj_[A-Za-z0-9]{8,80}$/u.test(attestation.vercelProjectId ?? "") || + !/^dpl_[A-Za-z0-9]{20,80}$/u.test(attestation.stagedDeploymentId ?? "") + ) { + throw new Error("staged release attestation identity is invalid"); + } + const stagedTarget = exactHttpsOrigin( + attestation.stagedDeploymentUrl, + "staged release attestation URL", + ); + if (!stagedTarget.hostname.endsWith(".vercel.app")) { + throw new Error("staged release attestation URL is not deployment-specific"); + } + if ( + attestation.productionOrigin !== CANONICAL_PRODUCTION_ORIGIN || + exactHttpsOrigin(attestation.productionOrigin, "production origin").origin !== + CANONICAL_PRODUCTION_ORIGIN + ) { + throw new Error("staged release attestation production origin is invalid"); + } + const indexedFlags = exactBooleanRecord( + attestation.indexedFlags, + RELEASE_GATED_FLAG_NAMES, + "staged release indexed flags", + ); + const workerActivationFlags = exactBooleanRecord( + attestation.workerActivationFlags, + WORKER_ACTIVATION_FLAG_NAMES, + "staged release worker flags", + ); + const nonLegacy = + Object.values(indexedFlags).some(Boolean) || + Object.values(workerActivationFlags).some(Boolean); + const expectedMode = nonLegacy ? "indexed-or-shadow" : "legacy-only"; + if (attestation.policyMode !== expectedMode) { + throw new Error("staged release attestation mode is invalid"); + } + const timestampMs = Date.parse(attestation.timestamp ?? ""); + const nowMs = expectations.nowMs ?? Date.now(); + const maximumAgeMs = expectations.maximumAgeMs ?? 2 * 60 * 60 * 1_000; + if ( + !Number.isFinite(timestampMs) || + !Number.isSafeInteger(nowMs) || + !Number.isSafeInteger(maximumAgeMs) || + maximumAgeMs < 1_000 || + maximumAgeMs > 24 * 60 * 60 * 1_000 || + timestampMs > nowMs + 60_000 || + nowMs - timestampMs > maximumAgeMs + ) { + throw new Error("staged release attestation timestamp is invalid"); + } + const exactExpectations = [ + ["verifiedSha", attestation.verifiedSha], + ["vercelProjectId", attestation.vercelProjectId], + ["stagedDeploymentId", attestation.stagedDeploymentId], + ["stagedDeploymentUrl", stagedTarget.origin], + ["productionOrigin", attestation.productionOrigin], + ]; + for (const [key, observed] of exactExpectations) { + if (expectations[key] !== undefined && expectations[key] !== observed) { + throw new Error(`staged release attestation ${key} does not match`); + } + } + if ( + expectations.requireWorkersActive === true && + Object.values(workerActivationFlags).some((active) => active !== true) + ) { + throw new Error("staged release attestation workers are not active"); + } + if ( + expectations.requireIndexedFlagsFalse === true && + Object.values(indexedFlags).some(Boolean) + ) { + throw new Error("staged release attestation exposes indexed reads"); + } + if ( + expectations.requireIndexedRoutesActive === true && + (PUBLIC_INDEXED_ROUTE_FLAG_NAMES.some((name) => indexedFlags[name] !== true) || + indexedFlags.INDEXED_READ_SHADOW_COMPARE_ENABLED !== false) + ) { + throw new Error("staged release attestation does not activate exact indexed routes"); + } + return Object.freeze({ + ...attestation, + stagedDeploymentUrl: stagedTarget.origin, + indexedFlags, + workerActivationFlags, + }); +} + +export function createStagedReleaseAttestation(input) { + if (!input.policy?.policyReady || !input.policy?.commitmentsReady) { + throw new Error("release policy must pass before attestation"); + } + if (!/^[0-9a-f]{40}$/u.test(input.verifiedSha ?? "")) { + throw new Error("verified SHA must be an exact Git commit"); + } + if (!/^prj_[A-Za-z0-9]{8,80}$/u.test(input.vercelProjectId ?? "")) { + throw new Error("Vercel project ID is invalid"); + } + if (!/^dpl_[A-Za-z0-9]{20,80}$/u.test(input.stagedDeploymentId ?? "")) { + throw new Error("staged deployment ID is invalid"); + } + const stagedTarget = exactHttpsOrigin( + input.stagedDeploymentUrl, + "staged deployment URL", + ); + if (!stagedTarget.hostname.endsWith(".vercel.app")) { + throw new Error("staged deployment URL must use a deployment-specific Vercel host"); + } + const productionTarget = exactHttpsOrigin( + input.productionOrigin, + "production origin", + ); + if ( + productionTarget.origin !== CANONICAL_PRODUCTION_ORIGIN || + input.productionOrigin !== CANONICAL_PRODUCTION_ORIGIN + ) { + throw new Error("production origin is not the canonical Programmable domain"); + } + if ( + input.expectedMode !== undefined && + input.expectedMode !== input.policy.mode + ) { + throw new Error("staged runtime mode differs from the preflight policy"); + } + const timestamp = new Date(input.timestamp ?? Date.now()); + if (!Number.isFinite(timestamp.getTime())) { + throw new Error("release attestation timestamp is invalid"); + } + const attestation = Object.freeze({ + schemaVersion: 1, + verifiedSha: input.verifiedSha, + vercelProjectId: input.vercelProjectId, + stagedDeploymentId: input.stagedDeploymentId, + stagedDeploymentUrl: stagedTarget.origin, + productionOrigin: CANONICAL_PRODUCTION_ORIGIN, + policyMode: input.policy.mode, + indexedFlags: input.policy.indexedFlags, + workerActivationFlags: input.policy.workerActivationFlags, + timestamp: timestamp.toISOString(), + }); + const json = canonicalJson(attestation); + return Object.freeze({ + attestation, + json, + sha256: createHash("sha256").update(json, "utf8").digest("hex"), + }); +} + +function argumentsFrom(argv) { + const result = {}; + for (let index = 0; index < argv.length; index += 2) { + const name = argv[index]; + const value = argv[index + 1]; + if (!name?.startsWith("--") || !value || value.startsWith("--")) { + throw new Error("arguments must be --name value pairs"); + } + result[name.slice(2)] = value; + } + if (!result["env-file"]) throw new Error("--env-file is required"); + return result; +} + +function main() { + const args = argumentsFrom(process.argv.slice(2)); + const expectations = readReleasePolicyExpectations(process.cwd()); + const result = evaluateReadModelDeployPolicy( + readFileSync(resolve(args["env-file"]), "utf8"), + process.env, + expectations, + ); + if (!result.policyReady) { + throw new Error( + [ + ...result.invalidFlagNames, + ...result.invalidNonSecretEnvironmentNames, + ].length > 0 + ? `release environment preflight failed: ${[ + ...result.invalidFlagNames, + ...result.invalidNonSecretEnvironmentNames, + ].join(", ")}` + : "release environment preflight failed", + ); + } + if (!result.commitmentsReady) { + throw new Error( + `indexed/shadow release requires pinned commitments: ${result.invalidCommitmentNames.join(", ")}`, + ); + } + let attestation; + if (args["attestation-output"]) { + const requiredAttestationArguments = [ + "verified-sha", + "vercel-project-id", + "staged-deployment-id", + "staged-target-url", + "production-origin", + "expected-mode", + ]; + const missing = requiredAttestationArguments.filter((name) => !args[name]); + if (missing.length > 0) { + throw new Error(`release attestation arguments missing: ${missing.join(", ")}`); + } + attestation = createStagedReleaseAttestation({ + policy: result, + verifiedSha: args["verified-sha"], + vercelProjectId: args["vercel-project-id"], + stagedDeploymentId: args["staged-deployment-id"], + stagedDeploymentUrl: args["staged-target-url"], + productionOrigin: args["production-origin"], + expectedMode: args["expected-mode"], + }); + writeFileSync(resolve(args["attestation-output"]), attestation.json, { + encoding: "utf8", + mode: 0o600, + flag: "wx", + }); + } + if (args["github-output"]) { + appendFileSync( + resolve(args["github-output"]), + [ + `mode=${result.mode}`, + `evidence_required=${result.evidenceRequired}`, + ...(attestation + ? [ + `attestation_path=${resolve(args["attestation-output"])}`, + `attestation_sha256=${attestation.sha256}`, + ] + : []), + "", + ].join("\n"), + { encoding: "utf8", mode: 0o600 }, + ); + } + process.stdout.write( + `${JSON.stringify({ + mode: result.mode, + evidenceRequired: result.evidenceRequired, + exactFalseFlags: + RELEASE_GATED_FLAG_NAMES.filter( + (name) => result.indexedFlags[name] === false, + ).length, + gatedFlags: result.nonLegacyFlags, + workerActivationFlags: result.workerActivationFlags, + policyReady: result.policyReady, + commitmentsReady: result.commitmentsReady, + runtimeProviderBinding: result.runtimeProviderBinding, + ...(attestation + ? { attestationSha256: attestation.sha256 } + : {}), + })}\n`, + ); +} + +if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file:").href) { + try { + main(); + } catch (error) { + process.stderr.write( + `${error instanceof Error ? error.message : "deploy policy failed"}\n`, + ); + process.exitCode = 1; + } +} diff --git a/scripts/perf/read-model-gate-core.mjs b/scripts/perf/read-model-gate-core.mjs new file mode 100644 index 00000000..7e0687ef --- /dev/null +++ b/scripts/perf/read-model-gate-core.mjs @@ -0,0 +1,1800 @@ +import { createHash } from "node:crypto"; +import { lstatSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +export const ROUTE_NAMES = Object.freeze([ + "exploreList", + "tokenDetail", + "tokenChart", + "creatorProfile", + "classicProfile", + "stockProfile", + "classicLaunchLookup", + "stockLaunchLookup", + "publicIndexer", + "health", +]); + +const DATASET_KEYS = Object.freeze([ + "launches", + "chainEvents", + "marketSnapshots", + "marketCandles", + "accounts", + "rewardRows", +]); + +const CACHE_KEYS = Object.freeze([ + "exploreList", + "tokenDetail", + "tokenChart", + "creatorProfile", + "classicProfile", + "stockProfile", + "classicLaunchLookup", + "stockLaunchLookup", + "publicIndexer", + "tokenList", + "health", + "accountMutation", + "transactionPreparation", +]); + +const SHADOW_ROUTES = Object.freeze([ + "exploreList", + "tokenDetail", + "tokenChart", + "creatorProfile", + "classicProfile", + "stockProfile", + "classicLaunchLookup", + "stockLaunchLookup", +]); +const ARTIFACT_KEYS = Object.freeze([ + "datasetManifest", + "httpSamples", + "rpcTrace", +]); +const RPC_OPERATIONS = new Set([ + "getChainId", + "getBlockNumber", + "getBlock", + "getTransactionReceipt", + "getBytecode", +]); +const HEX_ADDRESS = /^0x[0-9a-fA-F]{40}$/u; +const HEX_HASH = /^0x[0-9a-fA-F]{64}$/u; +const CANDIDATE_ID = /^1:(0x[0-9a-fA-F]{64}):(0x[0-9a-fA-F]{64}):([0-9]+)$/u; +const HEX_DIGEST = /^[0-9a-f]{64}$/u; +const HEX_BYTES32 = /^0x[0-9a-f]{64}$/u; +const GIT_SHA = /^[0-9a-f]{40}$/u; +const DEPLOYMENT_ID = /^dpl_[A-Za-z0-9]{20,80}$/u; +const ARTIFACT_FILE = /^[a-z0-9][a-z0-9._-]{0,127}$/u; +const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024; +const SMOKE_PROFILE_ID = "read-model-smoke-v1"; +const RELEASE_PROFILE_ID = "read-model-release-v1"; +const PROFILE_CONTRACTS = Object.freeze({ + [SMOKE_PROFILE_ID]: Object.freeze({ + tokenKeyCount: 100, + accountKeyCount: 100, + candidateCount: 8, + minimumEligibleLaunches: 200, + expectedDataset: Object.freeze({ + launches: 200, + chainEvents: 600, + marketSnapshots: 200, + marketCandles: 200, + accounts: 100, + rewardRows: 200, + }), + }), + [RELEASE_PROFILE_ID]: Object.freeze({ + tokenKeyCount: 264, + accountKeyCount: 100, + candidateCount: 32, + minimumEligibleLaunches: 264, + expectedDataset: Object.freeze({ + launches: 264, + chainEvents: 792, + marketSnapshots: 264, + marketCandles: 264, + accounts: 100, + rewardRows: 264, + }), + }), +}); +const REQUIRED_RELEASE_VERSIONS = Object.freeze([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +]); + +const EXPECTED_CACHE_CONTRACTS = Object.freeze({ + exploreList: + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + tokenDetail: + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + tokenChart: + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + creatorProfile: "private, max-age=0, s-maxage=15", + classicProfile: "no-store", + stockProfile: "no-store", + classicLaunchLookup: "no-store", + stockLaunchLookup: "no-store", + publicIndexer: + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + tokenList: + "public, max-age=0, s-maxage=60, stale-while-revalidate=300", + health: "public, max-age=0, s-maxage=30", + accountMutation: "private, no-store", + transactionPreparation: "private, no-store", +}); + +function fail(path, message) { + throw new Error(`${path}: ${message}`); +} + +function object(value, path) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) + ) { + fail(path, "expected an object"); + } + return value; +} + +function exactKeys(value, keys, path) { + const input = object(value, path); + const actual = Object.keys(input).sort(); + const expected = [...keys].sort(); + if ( + actual.length !== expected.length || + actual.some((key, index) => key !== expected[index]) + ) { + fail(path, `expected exactly: ${expected.join(", ")}`); + } + return input; +} + +function integer(value, path, minimum = 0) { + if (!Number.isSafeInteger(value) || value < minimum) { + fail(path, `expected an integer greater than or equal to ${minimum}`); + } + return value; +} + +function string(value, path, maximumLength = 256) { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > maximumLength + ) { + fail(path, `expected a non-empty string of at most ${maximumLength} characters`); + } + return value; +} + +function stringArray(value, path) { + if (!Array.isArray(value) || value.length < 1) { + fail(path, "expected a non-empty string array"); + } + const parsed = value.map((entry, index) => + string(entry, `${path}[${index}]`), + ); + if (new Set(parsed).size !== parsed.length) { + fail(path, "entries must be unique"); + } + return parsed; +} + +function timestamp(value, path) { + const parsed = Date.parse(string(value, path)); + if (!Number.isFinite(parsed)) fail(path, "expected an ISO timestamp"); + return parsed; +} + +function sameRecord(left, right, keys) { + return keys.every((key) => left[key] === right[key]); +} + +function atLeastRecord(observed, minimums, keys) { + return keys.every((key) => observed[key] >= minimums[key]); +} + +function parseDataset(value, path) { + const input = exactKeys(value, DATASET_KEYS, path); + for (const key of DATASET_KEYS) integer(input[key], `${path}.${key}`, 1); + return input; +} + +function parseCacheContracts(value, path) { + const input = exactKeys(value, CACHE_KEYS, path); + for (const key of CACHE_KEYS) string(input[key], `${path}.${key}`); + return input; +} + +export function projectorCallsPerProviderPerAttempt( + profile, + candidateBatchSize, +) { + integer(candidateBatchSize, "candidateBatchSize", 1); + return ( + profile.projector.rpc.fixedCallsPerProviderPerAttempt + + candidateBatchSize * + profile.projector.rpc.callsPerCandidatePerProviderPerAttempt + ); +} + +export function projectorWorstCaseRetryContract(profile, candidateBatchSize) { + integer(candidateBatchSize, "candidateBatchSize", 1); + const rpc = profile.projector.rpc; + const operationWorstCaseMs = + rpc.maxAttemptsPerCall * rpc.perCallTimeoutMs + + Array.from( + { length: rpc.maxAttemptsPerCall - 1 }, + (_, index) => rpc.baseBackoffMs * 2 ** index, + ).reduce((total, value) => total + value, 0); + const stateWaves = 1; + const blockWaves = Math.ceil( + (candidateBatchSize + 1) / rpc.maxConcurrencyPerProvider, + ); + const receiptWaves = Math.ceil( + candidateBatchSize / rpc.maxConcurrencyPerProvider, + ); + const bytecodeWaves = receiptWaves; + return { + callsPerProvider: + projectorCallsPerProviderPerAttempt(profile, candidateBatchSize) * + rpc.maxAttemptsPerCall, + durationMs: + (stateWaves + blockWaves + receiptWaves + bytecodeWaves) * + operationWorstCaseMs, + }; +} + +export function parseReadModelLoadProfile(value) { + const input = exactKeys( + value, + [ + "schemaVersion", + "profileId", + "scope", + "evidence", + "dataset", + "datasetCoverage", + "load", + "projector", + "providerLimits", + "shadow", + "cacheContracts", + ], + "profile", + ); + if (input.schemaVersion !== 1) fail("profile.schemaVersion", "expected 1"); + const contract = PROFILE_CONTRACTS[input.profileId]; + if (!contract) { + fail( + "profile.profileId", + "expected read-model-smoke-v1 or read-model-release-v1", + ); + } + const releaseProfile = input.profileId === RELEASE_PROFILE_ID; + + const scope = exactKeys( + input.scope, + ["models", "excludedModels"], + "profile.scope", + ); + const models = stringArray(scope.models, "profile.scope.models"); + const excludedModels = stringArray( + scope.excludedModels, + "profile.scope.excludedModels", + ); + if ( + models.join(",") !== "classic,stock-paired" || + excludedModels.join(",") !== "adaptive,deep" + ) { + fail("profile.scope", "must cover Classic and Stock-Paired only"); + } + + const evidence = exactKeys( + input.evidence, + [ + "maximumAgeSeconds", + "requiredKinds", + "requireExactGitHead", + "requireLiveVercelBinding", + "requiredArtifactDigests", + ], + "profile.evidence", + ); + integer(evidence.maximumAgeSeconds, "profile.evidence.maximumAgeSeconds", 60); + if ( + stringArray(evidence.requiredKinds, "profile.evidence.requiredKinds").join(",") !== + "preview,production-canary" || + evidence.requireExactGitHead !== true || + evidence.requireLiveVercelBinding !== true || + stringArray( + evidence.requiredArtifactDigests, + "profile.evidence.requiredArtifactDigests", + ).join(",") !== ARTIFACT_KEYS.join(",") + ) { + fail("profile.evidence", "release binding requirements are incomplete"); + } + + const dataset = parseDataset(input.dataset, "profile.dataset"); + if (!sameRecord(dataset, contract.expectedDataset, DATASET_KEYS)) { + fail("profile.dataset", "does not match the profile dataset floor"); + } + + const datasetCoverage = exactKeys( + input.datasetCoverage, + [ + "minimumEligibleLaunches", + "maximumEligibleLaunches", + "maximumClassicLookupLaunches", + "maximumStockLookupLaunches", + "minimumRowsPerLaunch", + "requiredReleaseVersions", + "minimumClassicLookupLaunches", + "minimumStockLookupLaunches", + "tokenSampleCount", + "accountSampleCount", + "classicLaunchSampleCount", + "stockLaunchSampleCount", + "candidateSampleCount", + ], + "profile.datasetCoverage", + ); + for (const key of [ + "minimumEligibleLaunches", + "maximumEligibleLaunches", + "maximumClassicLookupLaunches", + "maximumStockLookupLaunches", + "minimumClassicLookupLaunches", + "minimumStockLookupLaunches", + "tokenSampleCount", + "accountSampleCount", + "classicLaunchSampleCount", + "stockLaunchSampleCount", + "candidateSampleCount", + ]) { + integer(datasetCoverage[key], `profile.datasetCoverage.${key}`, 1); + } + const minimumRowsPerLaunch = exactKeys( + datasetCoverage.minimumRowsPerLaunch, + ["chainEvents", "marketSnapshots", "marketCandles", "rewardRows"], + "profile.datasetCoverage.minimumRowsPerLaunch", + ); + for (const key of Object.keys(minimumRowsPerLaunch)) { + integer( + minimumRowsPerLaunch[key], + `profile.datasetCoverage.minimumRowsPerLaunch.${key}`, + 1, + ); + } + if ( + stringArray( + datasetCoverage.requiredReleaseVersions, + "profile.datasetCoverage.requiredReleaseVersions", + ).join(",") !== REQUIRED_RELEASE_VERSIONS.join(",") || + datasetCoverage.minimumEligibleLaunches !== + contract.minimumEligibleLaunches || + datasetCoverage.maximumEligibleLaunches !== 400 || + datasetCoverage.maximumClassicLookupLaunches !== 300 || + datasetCoverage.maximumStockLookupLaunches !== 100 || + datasetCoverage.minimumClassicLookupLaunches !== 32 || + datasetCoverage.minimumStockLookupLaunches !== 32 || + datasetCoverage.tokenSampleCount !== contract.tokenKeyCount || + datasetCoverage.accountSampleCount !== contract.accountKeyCount || + datasetCoverage.classicLaunchSampleCount !== 32 || + datasetCoverage.stockLaunchSampleCount !== 32 || + datasetCoverage.candidateSampleCount !== contract.candidateCount || + !sameRecord( + minimumRowsPerLaunch, + { + chainEvents: 3, + marketSnapshots: 1, + marketCandles: 1, + rewardRows: 1, + }, + ["chainEvents", "marketSnapshots", "marketCandles", "rewardRows"], + ) + ) { + fail("profile.datasetCoverage", "does not match the v1 real-corpus contract"); + } + + const load = exactKeys( + input.load, + [ + "concurrency", + "durationSeconds", + "minimumCompletedRequests", + "probeTimeoutMs", + "maximumErrorRateBps", + "maximumCacheHitRateBps", + "minimumDistinctTokenKeys", + "minimumDistinctAccountKeys", + "minimumDistinctClassicLaunchKeys", + "minimumDistinctStockLaunchKeys", + "probeCacheControl", + "requiredVercelCacheStatuses", + "routeMixBps", + "maximumRouteP95Ms", + "maximumRouteP99Ms", + ], + "profile.load", + ); + integer(load.concurrency, "profile.load.concurrency", 1); + integer(load.durationSeconds, "profile.load.durationSeconds", 1); + integer( + load.minimumCompletedRequests, + "profile.load.minimumCompletedRequests", + 1, + ); + integer(load.probeTimeoutMs, "profile.load.probeTimeoutMs", 1); + integer(load.maximumErrorRateBps, "profile.load.maximumErrorRateBps"); + integer(load.maximumCacheHitRateBps, "profile.load.maximumCacheHitRateBps"); + integer(load.minimumDistinctTokenKeys, "profile.load.minimumDistinctTokenKeys", 2); + integer( + load.minimumDistinctAccountKeys, + "profile.load.minimumDistinctAccountKeys", + 2, + ); + integer( + load.minimumDistinctClassicLaunchKeys, + "profile.load.minimumDistinctClassicLaunchKeys", + 2, + ); + integer( + load.minimumDistinctStockLaunchKeys, + "profile.load.minimumDistinctStockLaunchKeys", + 2, + ); + if (load.probeCacheControl !== "private, no-store") { + fail("profile.load.probeCacheControl", "expected private, no-store"); + } + if ( + stringArray( + load.requiredVercelCacheStatuses, + "profile.load.requiredVercelCacheStatuses", + ).join(",") !== "MISS,BYPASS" + ) { + fail("profile.load.requiredVercelCacheStatuses", "expected MISS and BYPASS"); + } + const routeMix = exactKeys( + load.routeMixBps, + ROUTE_NAMES, + "profile.load.routeMixBps", + ); + const latencyBudgets = exactKeys( + load.maximumRouteP95Ms, + ROUTE_NAMES, + "profile.load.maximumRouteP95Ms", + ); + const p99LatencyBudgets = exactKeys( + load.maximumRouteP99Ms, + ROUTE_NAMES, + "profile.load.maximumRouteP99Ms", + ); + const routeMixTotal = ROUTE_NAMES.reduce( + (total, route) => + total + integer(routeMix[route], `profile.load.routeMixBps.${route}`), + 0, + ); + for (const route of ROUTE_NAMES) { + integer(latencyBudgets[route], `profile.load.maximumRouteP95Ms.${route}`, 1); + integer( + p99LatencyBudgets[route], + `profile.load.maximumRouteP99Ms.${route}`, + 1, + ); + if (p99LatencyBudgets[route] < latencyBudgets[route]) { + fail(`profile.load.maximumRouteP99Ms.${route}`, "must be at least p95"); + } + } + if ( + routeMixTotal !== 10_000 || + load.concurrency !== 20 || + load.durationSeconds !== 60 || + load.minimumCompletedRequests !== 1_000 || + load.probeTimeoutMs !== 30_000 || + load.maximumErrorRateBps !== 0 || + load.maximumCacheHitRateBps !== 0 || + load.minimumDistinctTokenKeys !== contract.tokenKeyCount || + load.minimumDistinctAccountKeys !== contract.accountKeyCount || + load.minimumDistinctClassicLaunchKeys !== 32 || + load.minimumDistinctStockLaunchKeys !== 32 + ) { + fail("profile.load", "does not match the v1 load contract"); + } + + const projector = exactKeys( + input.projector, + [ + "hostingDeadlineMs", + "hardDeadlineMs", + "minimumReserveMs", + "smokeCandidateBatchSize", + "maximumCandidateBatchSize", + "rpc", + ], + "profile.projector", + ); + for (const key of [ + "hostingDeadlineMs", + "hardDeadlineMs", + "minimumReserveMs", + "smokeCandidateBatchSize", + "maximumCandidateBatchSize", + ]) { + integer(projector[key], `profile.projector.${key}`, 1); + } + if ( + projector.hostingDeadlineMs !== 90_000 || + projector.hardDeadlineMs !== 75_000 || + projector.minimumReserveMs !== 15_000 || + projector.smokeCandidateBatchSize !== 8 || + projector.maximumCandidateBatchSize !== + (releaseProfile ? 32 : 8) || + projector.hostingDeadlineMs - projector.hardDeadlineMs < + projector.minimumReserveMs + ) { + fail("profile.projector", "deadline or batch contract is invalid"); + } + + const rpc = exactKeys( + projector.rpc, + [ + "providerCount", + "perCallTimeoutMs", + "maxAttemptsPerCall", + "baseBackoffMs", + "maxConcurrencyPerProvider", + "fixedCallsPerProviderPerAttempt", + "callsPerCandidatePerProviderPerAttempt", + "smokeFirstAttemptCallsPerProvider", + "theoreticalWorstCaseCallsPerProvider", + "theoreticalWorstCaseDurationMs", + "globalRetryAllowancePerProvider", + "maxCallsPerProviderPerRun", + "maxAggregateCallsPerRun", + ], + "profile.projector.rpc", + ); + for (const key of Object.keys(rpc)) { + integer(rpc[key], `profile.projector.rpc.${key}`, key === "baseBackoffMs" ? 0 : 1); + } + if ( + rpc.providerCount !== 2 || + rpc.perCallTimeoutMs !== 5_000 || + rpc.maxAttemptsPerCall !== 3 || + rpc.baseBackoffMs !== 50 || + rpc.maxConcurrencyPerProvider !== 4 + ) { + fail("profile.projector.rpc", "does not match the runtime RPC policy"); + } + const firstAttempt = projectorCallsPerProviderPerAttempt( + input, + projector.smokeCandidateBatchSize, + ); + const theoretical = projectorWorstCaseRetryContract( + input, + projector.maximumCandidateBatchSize, + ); + const releaseFirstAttempt = projectorCallsPerProviderPerAttempt( + input, + projector.maximumCandidateBatchSize, + ); + if ( + rpc.smokeFirstAttemptCallsPerProvider !== firstAttempt || + rpc.theoreticalWorstCaseCallsPerProvider !== theoretical.callsPerProvider || + rpc.theoreticalWorstCaseDurationMs !== theoretical.durationMs || + rpc.maxCallsPerProviderPerRun !== + releaseFirstAttempt + rpc.globalRetryAllowancePerProvider || + rpc.maxAggregateCallsPerRun !== + rpc.maxCallsPerProviderPerRun * rpc.providerCount || + theoretical.durationMs <= projector.hardDeadlineMs + ) { + fail("profile.projector.rpc", "call or retry math is inconsistent"); + } + + const providerLimits = exactKeys( + input.providerLimits, + ["envio"], + "profile.providerLimits", + ); + const envio = exactKeys( + providerLimits.envio, + ["planQueriesPerMinute", "steadyQueriesPerMinute", "burstQueriesPerMinute"], + "profile.providerLimits.envio", + ); + for (const key of Object.keys(envio)) { + integer(envio[key], `profile.providerLimits.envio.${key}`, 1); + } + if ( + envio.steadyQueriesPerMinute * 2 > envio.planQueriesPerMinute || + envio.burstQueriesPerMinute * 4 > envio.planQueriesPerMinute * 3 || + envio.steadyQueriesPerMinute > envio.burstQueriesPerMinute + ) { + fail("profile.providerLimits.envio", "provider headroom is insufficient"); + } + + const shadow = exactKeys( + input.shadow, + [ + "requiredRoutes", + "minimumSamples", + "maximumP50Ms", + "maximumP95Ms", + "maximumP99Ms", + "maximumLiveComparisonP50Ms", + "maximumLiveComparisonP95Ms", + "maximumLiveComparisonP99Ms", + "maximumParityMismatches", + "maximumFallbacks", + ], + "profile.shadow", + ); + if ( + stringArray(shadow.requiredRoutes, "profile.shadow.requiredRoutes").join(",") !== + SHADOW_ROUTES.join(",") + ) { + fail("profile.shadow.requiredRoutes", "must cover every indexed read route"); + } + for (const key of [ + "minimumSamples", + "maximumP50Ms", + "maximumP95Ms", + "maximumP99Ms", + "maximumLiveComparisonP50Ms", + "maximumLiveComparisonP95Ms", + "maximumLiveComparisonP99Ms", + "maximumParityMismatches", + "maximumFallbacks", + ]) { + integer(shadow[key], `profile.shadow.${key}`, key === "minimumSamples" ? 1 : 0); + } + if ( + shadow.maximumP50Ms > shadow.maximumP95Ms || + shadow.maximumP95Ms > shadow.maximumP99Ms || + shadow.maximumLiveComparisonP50Ms > + shadow.maximumLiveComparisonP95Ms || + shadow.maximumLiveComparisonP95Ms > + shadow.maximumLiveComparisonP99Ms + ) { + fail("profile.shadow", "latency percentiles are not monotonic"); + } + + const cacheContracts = parseCacheContracts( + input.cacheContracts, + "profile.cacheContracts", + ); + if (!sameRecord(cacheContracts, EXPECTED_CACHE_CONTRACTS, CACHE_KEYS)) { + fail("profile.cacheContracts", "does not match the route cache contract"); + } + return input; +} + +export function sha256Bytes(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function readArtifact(directory, descriptor, path) { + const input = exactKeys(descriptor, ["file", "sha256"], path); + const file = string(input.file, `${path}.file`); + const digest = string(input.sha256, `${path}.sha256`); + if (!ARTIFACT_FILE.test(file) || !HEX_DIGEST.test(digest)) { + fail(path, "invalid artifact filename or digest"); + } + const artifactPath = resolve(directory, file); + if (dirname(artifactPath) !== directory) fail(path, "artifact escaped its bundle"); + const stats = lstatSync(artifactPath); + if (!stats.isFile() || stats.size < 1 || stats.size > MAX_ARTIFACT_BYTES) { + fail(path, "artifact size is outside the accepted range"); + } + const bytes = readFileSync(artifactPath); + const actualDigest = sha256Bytes(bytes); + if (actualDigest !== digest) fail(path, "artifact digest mismatch"); + return { path: artifactPath, bytes, sha256: actualDigest }; +} + +function parseReleaseEvidence(value) { + const input = exactKeys( + value, + [ + "schemaVersion", + "profileId", + "evidenceKind", + "capturedAt", + "captureNonce", + "target", + "artifacts", + ], + "evidence", + ); + if (input.schemaVersion !== 1) fail("evidence.schemaVersion", "expected 1"); + string(input.profileId, "evidence.profileId"); + string(input.evidenceKind, "evidence.evidenceKind"); + timestamp(input.capturedAt, "evidence.capturedAt"); + if (!HEX_BYTES32.test(string(input.captureNonce, "evidence.captureNonce"))) { + fail("evidence.captureNonce", "expected a bytes32 nonce"); + } + const target = exactKeys( + input.target, + ["url", "vercelDeploymentId", "gitHead"], + "evidence.target", + ); + const targetUrl = new URL(string(target.url, "evidence.target.url", 1_024)); + if ( + targetUrl.protocol !== "https:" || + targetUrl.username !== "" || + targetUrl.password !== "" || + targetUrl.search !== "" || + targetUrl.hash !== "" || + !targetUrl.hostname.endsWith(".vercel.app") + ) { + fail("evidence.target.url", "expected a deployment-specific Vercel URL"); + } + if (!DEPLOYMENT_ID.test(string(target.vercelDeploymentId, "evidence.target.vercelDeploymentId"))) { + fail("evidence.target.vercelDeploymentId", "invalid deployment id"); + } + if (!GIT_SHA.test(string(target.gitHead, "evidence.target.gitHead"))) { + fail("evidence.target.gitHead", "invalid Git commit"); + } + exactKeys(input.artifacts, ARTIFACT_KEYS, "evidence.artifacts"); + return input; +} + +function parseAddressList(value, path, expectedLength) { + if (!Array.isArray(value) || value.length !== expectedLength) { + fail(path, `expected exactly ${expectedLength} addresses`); + } + const canonical = value.map((entry, index) => { + const address = string(entry, `${path}[${index}]`); + if (!HEX_ADDRESS.test(address)) { + fail(`${path}[${index}]`, "expected an Ethereum address"); + } + return address.toLowerCase(); + }); + if (new Set(canonical).size !== expectedLength) { + fail(path, "addresses must be unique"); + } + return canonical; +} + +function parseLaunchSample(value, path) { + const launch = exactKeys(value, ["account", "transactionHash"], path); + const account = string(launch.account, `${path}.account`); + const transactionHash = string( + launch.transactionHash, + `${path}.transactionHash`, + ); + if (!HEX_ADDRESS.test(account) || !HEX_HASH.test(transactionHash)) { + fail(path, "invalid launch key"); + } + return `${account.toLowerCase()}:${transactionHash.toLowerCase()}`; +} + +function parseDatasetManifest(value, profile) { + const contract = PROFILE_CONTRACTS[profile.profileId]; + if (!contract) fail("datasetManifest.profileId", "unknown profile"); + const input = exactKeys( + value, + [ + "schemaVersion", + "profileId", + "generatedAt", + "counts", + "releaseCounts", + "eligibleLaunches", + "accountEvidence", + "accessEvidence", + "keys", + ], + "datasetManifest", + ); + if (input.schemaVersion !== 1) fail("datasetManifest.schemaVersion", "expected 1"); + string(input.profileId, "datasetManifest.profileId"); + timestamp(input.generatedAt, "datasetManifest.generatedAt"); + const counts = parseDataset(input.counts, "datasetManifest.counts"); + const releaseCounts = exactKeys( + input.releaseCounts, + REQUIRED_RELEASE_VERSIONS, + "datasetManifest.releaseCounts", + ); + for (const releaseVersion of REQUIRED_RELEASE_VERSIONS) { + integer( + releaseCounts[releaseVersion], + `datasetManifest.releaseCounts.${releaseVersion}`, + 1, + ); + } + if ( + REQUIRED_RELEASE_VERSIONS.reduce( + (total, releaseVersion) => total + releaseCounts[releaseVersion], + 0, + ) !== counts.launches + ) { + fail("datasetManifest.releaseCounts", "must sum to the eligible launch count"); + } + if (!Array.isArray(input.eligibleLaunches)) { + fail("datasetManifest.eligibleLaunches", "expected every eligible launch"); + } + const eligibleLaunches = input.eligibleLaunches.map((value, index) => { + const path = `datasetManifest.eligibleLaunches[${index}]`; + const launch = exactKeys( + value, + ["account", "transactionHash", "tokenAddress", "releaseVersion"], + path, + ); + const account = string(launch.account, `${path}.account`); + const transactionHash = string( + launch.transactionHash, + `${path}.transactionHash`, + ); + const tokenAddress = string(launch.tokenAddress, `${path}.tokenAddress`); + const releaseVersion = string( + launch.releaseVersion, + `${path}.releaseVersion`, + ); + if ( + !HEX_ADDRESS.test(account) || + !HEX_HASH.test(transactionHash) || + !HEX_ADDRESS.test(tokenAddress) || + !REQUIRED_RELEASE_VERSIONS.includes(releaseVersion) + ) { + fail(path, "invalid eligible launch"); + } + return { + account: account.toLowerCase(), + transactionHash: transactionHash.toLowerCase(), + tokenAddress: tokenAddress.toLowerCase(), + releaseVersion, + }; + }); + if (eligibleLaunches.length !== counts.launches) { + fail( + "datasetManifest.eligibleLaunches", + "must contain every counted eligible launch", + ); + } + if ( + new Set(eligibleLaunches.map((launch) => launch.transactionHash)).size !== + eligibleLaunches.length || + new Set(eligibleLaunches.map((launch) => launch.tokenAddress)).size !== + eligibleLaunches.length + ) { + fail("datasetManifest.eligibleLaunches", "token and transaction keys must be unique"); + } + for (const releaseVersion of REQUIRED_RELEASE_VERSIONS) { + if ( + eligibleLaunches.filter( + (launch) => launch.releaseVersion === releaseVersion, + ).length !== releaseCounts[releaseVersion] + ) { + fail( + `datasetManifest.releaseCounts.${releaseVersion}`, + "does not match the eligible launch records", + ); + } + } + const keys = exactKeys( + input.keys, + [ + "tokenAddresses", + "accountAddresses", + "classicLaunches", + "stockLaunches", + "candidateIds", + ], + "datasetManifest.keys", + ); + const tokenSamples = parseAddressList( + keys.tokenAddresses, + "datasetManifest.keys.tokenAddresses", + contract.tokenKeyCount, + ); + const accountSamples = parseAddressList( + keys.accountAddresses, + "datasetManifest.keys.accountAddresses", + contract.accountKeyCount, + ); + if ( + !Array.isArray(input.accountEvidence) || + input.accountEvidence.length !== contract.accountKeyCount + ) { + fail( + "datasetManifest.accountEvidence", + `expected exactly ${contract.accountKeyCount} attested accounts`, + ); + } + const accountEvidence = input.accountEvidence.map((value, index) => { + const path = `datasetManifest.accountEvidence[${index}]`; + const evidence = exactKeys( + value, + ["account", "profileRows", "rewardRows"], + path, + ); + const account = string(evidence.account, `${path}.account`).toLowerCase(); + if (!HEX_ADDRESS.test(account)) fail(`${path}.account`, "invalid address"); + const profileRows = integer(evidence.profileRows, `${path}.profileRows`); + const rewardRows = integer(evidence.rewardRows, `${path}.rewardRows`); + if (profileRows + rewardRows < 1) { + fail(path, "account must have real profile or reward evidence"); + } + return { account, profileRows, rewardRows }; + }); + const evidenceAccounts = accountEvidence.map((entry) => entry.account); + if ( + new Set(evidenceAccounts).size !== contract.accountKeyCount || + accountSamples.some((account) => !evidenceAccounts.includes(account)) || + accountEvidence.reduce((total, entry) => total + entry.profileRows, 0) > + counts.accounts || + accountEvidence.reduce((total, entry) => total + entry.rewardRows, 0) > + counts.rewardRows + ) { + fail( + "datasetManifest.accountEvidence", + "must exactly attest the sampled real account corpus", + ); + } + const accessEvidence = exactKeys( + input.accessEvidence, + [ + "projectorSessionUser", + "projectorCurrentRole", + "projectorCurrentSettingRole", + "apiReaderSessionUser", + "apiReaderCurrentRole", + "apiReaderCurrentSettingRole", + "apiReaderDeniedSqlstate", + "apiReaderFunctionExecute", + "apiReaderViewSelect", + ], + "datasetManifest.accessEvidence", + ); + for (const key of [ + "projectorSessionUser", + "projectorCurrentRole", + "projectorCurrentSettingRole", + "apiReaderSessionUser", + "apiReaderCurrentRole", + "apiReaderCurrentSettingRole", + "apiReaderDeniedSqlstate", + ]) { + string(accessEvidence[key], `datasetManifest.accessEvidence.${key}`); + } + if ( + accessEvidence.projectorSessionUser !== "programmable_projector_login" || + accessEvidence.projectorCurrentRole !== "programmable_projector" || + accessEvidence.projectorCurrentSettingRole !== "programmable_projector" || + accessEvidence.apiReaderSessionUser !== "programmable_api_reader_login" || + accessEvidence.apiReaderCurrentRole !== "programmable_api_reader" || + accessEvidence.apiReaderCurrentSettingRole !== "programmable_api_reader" || + accessEvidence.apiReaderDeniedSqlstate !== "42501" || + accessEvidence.apiReaderFunctionExecute !== false || + accessEvidence.apiReaderViewSelect !== false + ) { + fail( + "datasetManifest.accessEvidence", + "must prove projector-only corpus access and the API reader denial", + ); + } + const eligibleTokens = new Set( + eligibleLaunches.map((launch) => launch.tokenAddress), + ); + if (tokenSamples.some((token) => !eligibleTokens.has(token))) { + fail("datasetManifest.keys.tokenAddresses", "contains a non-eligible token"); + } + for (const key of ["classicLaunches", "stockLaunches"]) { + if (!Array.isArray(keys[key]) || keys[key].length !== 32) { + fail(`datasetManifest.keys.${key}`, "expected exactly 32 launch keys"); + } + const canonical = keys[key].map((entry, index) => + parseLaunchSample(entry, `datasetManifest.keys.${key}[${index}]`), + ); + if (new Set(canonical).size !== 32) { + fail(`datasetManifest.keys.${key}`, "launch keys must be unique"); + } + const allowedRelease = (releaseVersion) => + key === "classicLaunches" + ? releaseVersion === "classic-v3" + : releaseVersion.startsWith("stock-paired-"); + const eligibleIdentities = new Set( + eligibleLaunches + .filter((launch) => allowedRelease(launch.releaseVersion)) + .map( + (launch) => `${launch.account}:${launch.transactionHash}`, + ), + ); + if (canonical.some((identity) => !eligibleIdentities.has(identity))) { + fail(`datasetManifest.keys.${key}`, "contains a non-eligible launch"); + } + } + if ( + !Array.isArray(keys.candidateIds) || + keys.candidateIds.length !== contract.candidateCount + ) { + fail( + "datasetManifest.keys.candidateIds", + `expected exactly ${contract.candidateCount} candidate keys`, + ); + } + const candidateIds = keys.candidateIds.map((candidateId, index) => + string(candidateId, `datasetManifest.keys.candidateIds[${index}]`), + ); + if ( + candidateIds.some((candidateId) => !CANDIDATE_ID.test(candidateId)) || + new Set(candidateIds).size !== contract.candidateCount + ) { + fail( + "datasetManifest.keys.candidateIds", + "candidate keys must be unique canonical mainnet candidate ids", + ); + } + return input; +} + +function parseHttpSample(value, index) { + const path = `httpSamples[${index}]`; + const input = exactKeys( + value, + [ + "route", + "requestKey", + "datasetKey", + "keyMatched", + "startedAtMs", + "completedAtMs", + "durationMs", + "status", + "cacheControl", + "vercelCache", + "bodySha256", + "bodyBytes", + "shadowOverheadMs", + "parity", + "readSource", + "fallback", + ], + path, + ); + if (!ROUTE_NAMES.includes(input.route)) fail(`${path}.route`, "unknown route"); + string(input.requestKey, `${path}.requestKey`); + const datasetKey = string(input.datasetKey, `${path}.datasetKey`); + const launchRoute = + input.route === "classicLaunchLookup" || + input.route === "stockLaunchLookup"; + if ( + (input.route === "health" && datasetKey !== "health") || + (launchRoute && !HEX_HASH.test(datasetKey)) || + (input.route !== "health" && !launchRoute && !HEX_ADDRESS.test(datasetKey)) + ) { + fail(`${path}.datasetKey`, "does not match the route key contract"); + } + if (typeof input.keyMatched !== "boolean") { + fail(`${path}.keyMatched`, "expected a measured boolean"); + } + integer(input.startedAtMs, `${path}.startedAtMs`, 1); + integer(input.completedAtMs, `${path}.completedAtMs`, 1); + integer(input.durationMs, `${path}.durationMs`); + integer(input.status, `${path}.status`, 100); + string(input.cacheControl, `${path}.cacheControl`); + string(input.vercelCache, `${path}.vercelCache`); + if (!HEX_DIGEST.test(string(input.bodySha256, `${path}.bodySha256`))) { + fail(`${path}.bodySha256`, "invalid response digest"); + } + integer(input.bodyBytes, `${path}.bodyBytes`); + if (input.completedAtMs - input.startedAtMs !== input.durationMs) { + fail(`${path}.durationMs`, "does not match the measured interval"); + } + const shadowRequired = SHADOW_ROUTES.includes(input.route); + if (shadowRequired) { + integer(input.shadowOverheadMs, `${path}.shadowOverheadMs`); + if (input.shadowOverheadMs > input.durationMs) { + fail(`${path}.shadowOverheadMs`, "cannot exceed total request duration"); + } + if ( + input.parity !== "match" && + input.parity !== "mismatch" && + input.parity !== "incomparable" + ) { + fail(`${path}.parity`, "missing raw parity result"); + } + if ( + input.readSource !== "rpc" && + input.readSource !== "blob" && + input.readSource !== "indexed" + ) { + fail(`${path}.readSource`, "missing selected read source"); + } + if (typeof input.fallback !== "boolean") { + fail(`${path}.fallback`, "missing raw fallback result"); + } + } else if ( + input.shadowOverheadMs !== null || + input.parity !== "not-observed" || + input.readSource !== "not-observed" || + input.fallback !== null + ) { + fail(path, "non-shadow samples must not fabricate shadow measurements"); + } + return input; +} + +function parseJsonLines(bytes, path) { + const text = bytes.toString("utf8"); + if (text.endsWith("\n") === false) fail(path, "must end with a newline"); + const lines = text.slice(0, -1).split("\n"); + if (lines.some((line) => line.length < 2)) fail(path, "contains an empty line"); + return lines.map((line, index) => { + try { + return JSON.parse(line); + } catch { + fail(`${path}[${index}]`, "invalid JSON line"); + } + }); +} + +function parseRpcTrace(value, profile) { + const contract = PROFILE_CONTRACTS[profile.profileId]; + if (!contract) fail("rpcTrace.profileId", "unknown profile"); + const input = exactKeys( + value, + [ + "schemaVersion", + "profileId", + "gitHead", + "targetUrl", + "vercelDeploymentId", + "captureNonce", + "startedAtMs", + "completedAtMs", + "candidateBatchSize", + "hardDeadlineMs", + "maxCallsPerProvider", + "elapsedMs", + "providerCallCounts", + "candidateEvidence", + "calls", + ], + "rpcTrace", + ); + if (input.schemaVersion !== 1) fail("rpcTrace.schemaVersion", "expected 1"); + string(input.profileId, "rpcTrace.profileId"); + if (!GIT_SHA.test(string(input.gitHead, "rpcTrace.gitHead"))) { + fail("rpcTrace.gitHead", "invalid Git commit"); + } + string(input.targetUrl, "rpcTrace.targetUrl", 1_024); + string(input.vercelDeploymentId, "rpcTrace.vercelDeploymentId"); + if (!HEX_BYTES32.test(string(input.captureNonce, "rpcTrace.captureNonce"))) { + fail("rpcTrace.captureNonce", "expected a bytes32 nonce"); + } + integer(input.startedAtMs, "rpcTrace.startedAtMs", 1); + integer(input.completedAtMs, "rpcTrace.completedAtMs", 1); + integer(input.candidateBatchSize, "rpcTrace.candidateBatchSize", 1); + integer(input.hardDeadlineMs, "rpcTrace.hardDeadlineMs", 1); + integer(input.maxCallsPerProvider, "rpcTrace.maxCallsPerProvider", 1); + integer(input.elapsedMs, "rpcTrace.elapsedMs"); + if ( + input.completedAtMs < input.startedAtMs || + input.completedAtMs - input.startedAtMs !== input.elapsedMs + ) { + fail("rpcTrace.elapsedMs", "does not match the measured runtime interval"); + } + if (!Array.isArray(input.providerCallCounts) || input.providerCallCounts.length !== 2) { + fail("rpcTrace.providerCallCounts", "expected exactly two provider counts"); + } + input.providerCallCounts.forEach((count, index) => + integer(count, `rpcTrace.providerCallCounts[${index}]`, 1), + ); + if ( + !Array.isArray(input.candidateEvidence) || + input.candidateEvidence.length !== contract.candidateCount + ) { + fail( + "rpcTrace.candidateEvidence", + `expected exactly ${contract.candidateCount} verified candidates`, + ); + } + const candidateEvidence = input.candidateEvidence.map((value, index) => { + const path = `rpcTrace.candidateEvidence[${index}]`; + const candidate = exactKeys( + value, + [ + "candidateId", + "candidateBlockNumber", + "candidateBlockHash", + "transactionHash", + "sourceAddress", + ], + path, + ); + const candidateId = string(candidate.candidateId, `${path}.candidateId`); + const idMatch = CANDIDATE_ID.exec(candidateId); + const candidateBlockNumber = string( + candidate.candidateBlockNumber, + `${path}.candidateBlockNumber`, + ); + const candidateBlockHash = string( + candidate.candidateBlockHash, + `${path}.candidateBlockHash`, + ).toLowerCase(); + const transactionHash = string( + candidate.transactionHash, + `${path}.transactionHash`, + ).toLowerCase(); + const sourceAddress = string( + candidate.sourceAddress, + `${path}.sourceAddress`, + ).toLowerCase(); + if ( + idMatch === null || + !/^(0|[1-9][0-9]*)$/u.test(candidateBlockNumber) || + !HEX_HASH.test(candidateBlockHash) || + !HEX_HASH.test(transactionHash) || + !HEX_ADDRESS.test(sourceAddress) || + idMatch[1].toLowerCase() !== candidateBlockHash || + idMatch[2].toLowerCase() !== transactionHash + ) { + fail(path, "invalid or internally inconsistent candidate evidence"); + } + return { + candidateId, + candidateBlockNumber, + candidateBlockHash, + transactionHash, + sourceAddress, + }; + }); + const candidateBlockNumbers = candidateEvidence.map((candidate) => + BigInt(candidate.candidateBlockNumber), + ); + if ( + new Set(candidateEvidence.map((candidate) => candidate.candidateId)).size !== + contract.candidateCount || + new Set(candidateEvidence.map((candidate) => candidate.candidateBlockNumber)) + .size !== contract.candidateCount || + new Set(candidateEvidence.map((candidate) => candidate.transactionHash)).size !== + contract.candidateCount || + new Set( + candidateEvidence.map( + (candidate) => + `${candidate.candidateBlockNumber}:${candidate.sourceAddress}`, + ), + ).size !== contract.candidateCount || + candidateBlockNumbers.some( + (blockNumber, index) => + index > 0 && blockNumber <= candidateBlockNumbers[index - 1], + ) + ) { + fail( + "rpcTrace.candidateEvidence", + "candidates must cover strictly ordered unique blocks, transactions and block/source pairs", + ); + } + if (!Array.isArray(input.calls) || input.calls.length < 1) { + fail("rpcTrace.calls", "expected raw call traces"); + } + for (const [index, value] of input.calls.entries()) { + const call = exactKeys( + value, + [ + "providerIdentity", + "providerVendorGroup", + "providerEndpointCommitment", + "providerOriginCommitment", + "operation", + "attempt", + "startedOffsetMs", + "durationMs", + "outcome", + ], + `rpcTrace.calls[${index}]`, + ); + string(call.providerIdentity, `rpcTrace.calls[${index}].providerIdentity`); + string(call.providerVendorGroup, `rpcTrace.calls[${index}].providerVendorGroup`); + if ( + !HEX_BYTES32.test(call.providerEndpointCommitment) || + !HEX_BYTES32.test(call.providerOriginCommitment) + ) { + fail(`rpcTrace.calls[${index}]`, "invalid provider commitment"); + } + if (!RPC_OPERATIONS.has(call.operation)) { + fail(`rpcTrace.calls[${index}].operation`, "unknown RPC operation"); + } + integer(call.attempt, `rpcTrace.calls[${index}].attempt`, 1); + integer(call.startedOffsetMs, `rpcTrace.calls[${index}].startedOffsetMs`); + integer(call.durationMs, `rpcTrace.calls[${index}].durationMs`); + if (call.outcome !== "success" && call.outcome !== "error") { + fail(`rpcTrace.calls[${index}].outcome`, "invalid call outcome"); + } + } + return input; +} + +export function loadReadModelReleaseEvidence(input) { + const profile = parseReadModelLoadProfile(input.profile); + const evidencePath = resolve(input.evidencePath); + const evidence = parseReleaseEvidence( + JSON.parse(readFileSync(evidencePath, "utf8")), + ); + const directory = dirname(evidencePath); + const artifactFilenames = ARTIFACT_KEYS.map( + (key) => evidence.artifacts[key].file, + ); + if (new Set(artifactFilenames).size !== ARTIFACT_KEYS.length) { + fail("evidence.artifacts", "artifact filenames must be unique"); + } + const artifacts = {}; + for (const key of ARTIFACT_KEYS) { + artifacts[key] = readArtifact( + directory, + evidence.artifacts[key], + `evidence.artifacts.${key}`, + ); + } + const datasetManifest = parseDatasetManifest( + JSON.parse(artifacts.datasetManifest.bytes.toString("utf8")), + profile, + ); + const httpSamples = parseJsonLines( + artifacts.httpSamples.bytes, + "httpSamples", + ).map(parseHttpSample); + const rpcTrace = parseRpcTrace( + JSON.parse(artifacts.rpcTrace.bytes.toString("utf8")), + profile, + ); + return { + profile, + evidence, + artifacts, + datasetManifest, + httpSamples, + rpcTrace, + }; +} + +export function percentile(values, percentileValue) { + if (!Array.isArray(values) || values.length < 1) { + fail("percentile", "requires at least one sample"); + } + const sorted = [...values].sort((left, right) => left - right); + const index = Math.max( + 0, + Math.ceil((percentileValue / 100) * sorted.length) - 1, + ); + return sorted[index]; +} + +function observedMaximumConcurrency(samples) { + const events = samples.flatMap((sample) => [ + [sample.startedAtMs, 1], + [sample.completedAtMs, -1], + ]); + events.sort((left, right) => left[0] - right[0] || right[1] - left[1]); + let active = 0; + let maximum = 0; + for (const [, delta] of events) { + active += delta; + maximum = Math.max(maximum, active); + } + return maximum; +} + +export function evaluateReadModelReleaseEvidence(bundle, input) { + const { profile, evidence, datasetManifest, httpSamples, rpcTrace } = bundle; + const releaseProfile = profile.profileId === RELEASE_PROFILE_ID; + const requiredCandidateBatchSize = releaseProfile + ? profile.projector.maximumCandidateBatchSize + : profile.projector.smokeCandidateBatchSize; + const checks = []; + const failures = []; + const check = (id, condition, detail) => { + checks.push({ id, status: condition ? "pass" : "fail", detail }); + if (!condition) failures.push({ id, detail }); + }; + const nowMs = input.nowMs ?? Date.now(); + const capturedAtMs = Date.parse(evidence.capturedAt); + const evidenceAgeMs = nowMs - capturedAtMs; + const datasetGeneratedAtMs = Date.parse(datasetManifest.generatedAt); + const eligibleClassicLookupLaunches = datasetManifest.eligibleLaunches.filter( + (launch) => launch.releaseVersion === "classic-v3", + ); + const eligibleStockLaunches = datasetManifest.eligibleLaunches.filter( + (launch) => launch.releaseVersion.startsWith("stock-paired-"), + ); + const eligibleAccounts = new Set( + datasetManifest.eligibleLaunches.map((launch) => launch.account), + ); + const coverage = profile.datasetCoverage; + const ratioCountsValid = + datasetManifest.counts.chainEvents >= + datasetManifest.counts.launches * + coverage.minimumRowsPerLaunch.chainEvents && + datasetManifest.counts.marketSnapshots >= + datasetManifest.counts.launches * + coverage.minimumRowsPerLaunch.marketSnapshots && + datasetManifest.counts.marketCandles >= + datasetManifest.counts.launches * + coverage.minimumRowsPerLaunch.marketCandles && + datasetManifest.counts.rewardRows >= + datasetManifest.counts.launches * + coverage.minimumRowsPerLaunch.rewardRows; + const exactBinding = + evidence.profileId === profile.profileId && + datasetManifest.profileId === profile.profileId && + rpcTrace.profileId === profile.profileId && + evidence.target.gitHead === input.gitHead && + rpcTrace.gitHead === input.gitHead && + rpcTrace.captureNonce === evidence.captureNonce && + rpcTrace.targetUrl === evidence.target.url && + rpcTrace.vercelDeploymentId === evidence.target.vercelDeploymentId; + check("exact-binding", exactBinding, "profile, commit, target and trace binding are exact"); + check( + "evidence-kind", + profile.evidence.requiredKinds.includes(evidence.evidenceKind), + "only preview or production-canary evidence is accepted", + ); + check( + "freshness", + evidenceAgeMs >= 0 && + evidenceAgeMs <= profile.evidence.maximumAgeSeconds * 1_000, + `evidence age is ${Math.max(0, Math.floor(evidenceAgeMs / 1_000))} seconds`, + ); + check( + "dataset-cardinality", + datasetManifest.counts.launches >= coverage.minimumEligibleLaunches && + datasetManifest.counts.launches <= coverage.maximumEligibleLaunches && + datasetManifest.eligibleLaunches.length === + datasetManifest.counts.launches && + coverage.requiredReleaseVersions.every( + (releaseVersion) => datasetManifest.releaseCounts[releaseVersion] > 0, + ) && + eligibleClassicLookupLaunches.length >= + coverage.minimumClassicLookupLaunches && + eligibleClassicLookupLaunches.length <= + coverage.maximumClassicLookupLaunches && + eligibleStockLaunches.length >= + coverage.minimumStockLookupLaunches && + eligibleStockLaunches.length <= coverage.maximumStockLookupLaunches, + `${datasetManifest.eligibleLaunches.length} unique eligible launches across exactly five releases`, + ); + check( + "dataset-row-coverage", + atLeastRecord(datasetManifest.counts, profile.dataset, DATASET_KEYS) && + ratioCountsValid && + datasetManifest.counts.accounts >= eligibleAccounts.size, + "projection rows and real accounts meet the ratio-bound corpus floor", + ); + check( + "deterministic-real-samples", + datasetManifest.keys.tokenAddresses.length === coverage.tokenSampleCount && + datasetManifest.keys.accountAddresses.length === + coverage.accountSampleCount && + datasetManifest.keys.classicLaunches.length === + coverage.classicLaunchSampleCount && + datasetManifest.keys.stockLaunches.length === + coverage.stockLaunchSampleCount && + datasetManifest.keys.candidateIds.length === + coverage.candidateSampleCount, + `${coverage.tokenSampleCount} token, ${coverage.accountSampleCount} account, ${coverage.classicLaunchSampleCount} Classic, ${coverage.stockLaunchSampleCount} Stock and ${coverage.candidateSampleCount} candidate keys`, + ); + check( + "release-corpus-cycles", + !releaseProfile || + (datasetManifest.eligibleLaunches.length >= 264 && + Math.ceil( + datasetManifest.keys.tokenAddresses.length / + profile.projector.maximumCandidateBatchSize, + ) >= 9), + releaseProfile + ? `${datasetManifest.keys.tokenAddresses.length} real launch tokens require at least nine 32-row cycles` + : "smoke profile keeps corpus-cycle enforcement disabled", + ); + check( + "projector-only-corpus", + datasetManifest.accessEvidence.projectorSessionUser === + "programmable_projector_login" && + datasetManifest.accessEvidence.projectorCurrentRole === + "programmable_projector" && + datasetManifest.accessEvidence.projectorCurrentSettingRole === + "programmable_projector" && + datasetManifest.accessEvidence.apiReaderSessionUser === + "programmable_api_reader_login" && + datasetManifest.accessEvidence.apiReaderCurrentRole === + "programmable_api_reader" && + datasetManifest.accessEvidence.apiReaderCurrentSettingRole === + "programmable_api_reader" && + datasetManifest.accessEvidence.apiReaderDeniedSqlstate === "42501" && + datasetManifest.accessEvidence.apiReaderFunctionExecute === false && + datasetManifest.accessEvidence.apiReaderViewSelect === false, + "the projector login can capture the corpus and the public API reader cannot execute or select it", + ); + check( + "dataset-freshness", + datasetGeneratedAtMs <= capturedAtMs && + capturedAtMs - datasetGeneratedAtMs <= + profile.evidence.maximumAgeSeconds * 1_000, + "dataset counts were captured within the release evidence window", + ); + check( + "rpc-trace-freshness", + rpcTrace.startedAtMs <= rpcTrace.completedAtMs && + rpcTrace.completedAtMs <= capturedAtMs && + capturedAtMs - rpcTrace.completedAtMs <= + profile.evidence.maximumAgeSeconds * 1_000, + "the raw projector run completed within the release evidence window", + ); + + const completed = httpSamples.length; + const errors = httpSamples.filter( + (sample) => sample.status < 200 || sample.status >= 300, + ).length; + const errorRateBps = completed > 0 ? Math.ceil((errors * 10_000) / completed) : 10_000; + const firstStart = Math.min(...httpSamples.map((sample) => sample.startedAtMs)); + const lastCompletion = Math.max( + ...httpSamples.map((sample) => sample.completedAtMs), + ); + const maximumConcurrency = observedMaximumConcurrency(httpSamples); + const cacheHits = httpSamples.filter((sample) => + ["HIT", "STALE"].includes(sample.vercelCache), + ).length; + const cacheHitRateBps = completed + ? Math.ceil((cacheHits * 10_000) / completed) + : 10_000; + const distinctTokenKeys = new Set( + httpSamples + .filter((sample) => + ["exploreList", "tokenDetail", "tokenChart", "publicIndexer"].includes( + sample.route, + ), + ) + .map((sample) => sample.datasetKey.toLowerCase()), + ).size; + const distinctAccountKeys = new Set( + httpSamples + .filter((sample) => + ["creatorProfile", "classicProfile", "stockProfile"].includes( + sample.route, + ), + ) + .map((sample) => sample.datasetKey.toLowerCase()), + ).size; + const distinctClassicLaunchKeys = new Set( + httpSamples + .filter((sample) => sample.route === "classicLaunchLookup") + .map((sample) => sample.datasetKey.toLowerCase()), + ).size; + const distinctStockLaunchKeys = new Set( + httpSamples + .filter((sample) => sample.route === "stockLaunchLookup") + .map((sample) => sample.datasetKey.toLowerCase()), + ).size; + check( + "throughput-shape", + completed >= profile.load.minimumCompletedRequests && + lastCompletion - firstStart >= profile.load.durationSeconds * 1_000 && + maximumConcurrency >= profile.load.concurrency, + `${completed} requests over ${lastCompletion - firstStart}ms at observed concurrency ${maximumConcurrency}`, + ); + check( + "throughput-errors", + errorRateBps <= profile.load.maximumErrorRateBps, + `${errors} failed requests, ${errorRateBps} error basis points`, + ); + check( + "throughput-cache-and-identity", + new Set(httpSamples.map((sample) => sample.requestKey)).size === completed && + httpSamples.every((sample) => + profile.load.requiredVercelCacheStatuses.includes(sample.vercelCache), + ) && + cacheHitRateBps <= profile.load.maximumCacheHitRateBps && + httpSamples.every((sample) => sample.keyMatched === true), + `${completed} unique origin requests with ${cacheHitRateBps} cache-hit basis points and exact response identities`, + ); + check( + "throughput-key-distribution", + distinctTokenKeys === profile.load.minimumDistinctTokenKeys && + distinctTokenKeys === datasetManifest.keys.tokenAddresses.length && + distinctAccountKeys === profile.load.minimumDistinctAccountKeys && + distinctAccountKeys === datasetManifest.keys.accountAddresses.length && + distinctClassicLaunchKeys === + profile.load.minimumDistinctClassicLaunchKeys && + distinctClassicLaunchKeys === + datasetManifest.keys.classicLaunches.length && + distinctStockLaunchKeys === + profile.load.minimumDistinctStockLaunchKeys && + distinctStockLaunchKeys === datasetManifest.keys.stockLaunches.length, + `${distinctTokenKeys} token, ${distinctAccountKeys} account, ${distinctClassicLaunchKeys} Classic and ${distinctStockLaunchKeys} Stock keys were repeatedly exercised`, + ); + check( + "http-capture-freshness", + firstStart >= rpcTrace.completedAtMs && lastCompletion <= capturedAtMs, + "HTTP load ran after the runtime trace and before the evidence manifest", + ); + + for (const route of ROUTE_NAMES) { + const routeSamples = httpSamples.filter((sample) => sample.route === route); + const exactMix = + routeSamples.length * 10_000 === + completed * profile.load.routeMixBps[route]; + check( + `route-mix-${route}`, + exactMix, + `${routeSamples.length}/${completed} samples`, + ); + const selectedPathDurations = routeSamples.map((sample) => + profile.shadow.requiredRoutes.includes(route) + ? sample.durationMs - sample.shadowOverheadMs + : sample.durationMs, + ); + const p95 = routeSamples.length + ? percentile(selectedPathDurations, 95) + : Number.POSITIVE_INFINITY; + const p99 = routeSamples.length + ? percentile(selectedPathDurations, 99) + : Number.POSITIVE_INFINITY; + check( + `route-latency-p95-${route}`, + p95 <= profile.load.maximumRouteP95Ms[route], + `${route} p95 is ${p95}ms`, + ); + check( + `route-latency-p99-${route}`, + p99 <= profile.load.maximumRouteP99Ms[route], + `${route} p99 is ${p99}ms`, + ); + check( + `route-cache-${route}`, + routeSamples.length > 0 && + routeSamples.every( + (sample) => + sample.cacheControl === + (profile.shadow.requiredRoutes.includes(route) + ? profile.load.probeCacheControl + : profile.cacheContracts[route]), + ), + `${route} raw cache headers match the probe contract`, + ); + } + + const shadowSamples = httpSamples.filter((sample) => + profile.shadow.requiredRoutes.includes(sample.route), + ); + const indexedComparisonDurations = shadowSamples + .filter( + (sample) => sample.readSource === "rpc" || sample.readSource === "blob", + ) + .map((sample) => sample.shadowOverheadMs); + const liveComparisonDurations = shadowSamples + .filter((sample) => sample.readSource === "indexed") + .map((sample) => sample.shadowOverheadMs); + const parityMismatches = shadowSamples.filter( + (sample) => sample.parity !== "match", + ).length; + const fallbacks = shadowSamples.filter((sample) => sample.fallback === true).length; + const measuredPercentiles = (values) => + values.length > 0 + ? { + p50: percentile(values, 50), + p95: percentile(values, 95), + p99: percentile(values, 99), + } + : { p50: 0, p95: 0, p99: 0 }; + const indexedComparison = measuredPercentiles(indexedComparisonDurations); + const liveComparison = measuredPercentiles(liveComparisonDurations); + check( + "shadow-overhead", + shadowSamples.length >= profile.shadow.minimumSamples && + indexedComparison.p50 <= profile.shadow.maximumP50Ms && + indexedComparison.p95 <= profile.shadow.maximumP95Ms && + indexedComparison.p99 <= profile.shadow.maximumP99Ms && + liveComparison.p50 <= + profile.shadow.maximumLiveComparisonP50Ms && + liveComparison.p95 <= + profile.shadow.maximumLiveComparisonP95Ms && + liveComparison.p99 <= profile.shadow.maximumLiveComparisonP99Ms, + `indexed-comparison p50/p95/p99 ${indexedComparison.p50}/${indexedComparison.p95}/${indexedComparison.p99}ms; live-comparison ${liveComparison.p50}/${liveComparison.p95}/${liveComparison.p99}ms across ${shadowSamples.length} samples`, + ); + check( + "shadow-parity", + parityMismatches <= profile.shadow.maximumParityMismatches && + shadowSamples.every((sample) => sample.parity === "match"), + `${parityMismatches} parity mismatches`, + ); + check( + "live-fallbacks", + fallbacks <= profile.shadow.maximumFallbacks && + shadowSamples.every((sample) => sample.fallback === false), + `${fallbacks} live fallbacks`, + ); + + check( + "projector-runtime-policy", + rpcTrace.candidateBatchSize === requiredCandidateBatchSize && + rpcTrace.candidateEvidence.length === rpcTrace.candidateBatchSize && + rpcTrace.candidateEvidence.every( + (candidate, index) => + candidate.candidateId === datasetManifest.keys.candidateIds[index], + ) && + rpcTrace.hardDeadlineMs === profile.projector.hardDeadlineMs && + rpcTrace.maxCallsPerProvider === + profile.projector.rpc.maxCallsPerProviderPerRun && + rpcTrace.elapsedMs <= profile.projector.hardDeadlineMs, + "raw projector trace uses the enforced batch, deadline and call budget", + ); + const expectedProviders = input.expectedProviders; + const expectedProviderCommitments = new Set( + expectedProviders.map((provider) => provider.endpointCommitment), + ); + const observedProviderOrigins = []; + const providerTraceChecks = expectedProviders.map((expected, providerIndex) => { + const calls = rpcTrace.calls.filter( + (call) => call.providerVendorGroup === expected.vendorGroup, + ); + const successfulOperationCounts = Object.fromEntries( + [...RPC_OPERATIONS].map((operation) => [ + operation, + calls.filter( + (call) => call.operation === operation && call.outcome === "success", + ).length, + ]), + ); + const origins = new Set( + calls.map((call) => call.providerOriginCommitment), + ); + observedProviderOrigins.push(...origins); + return ( + calls.length === rpcTrace.providerCallCounts[providerIndex] && + calls.length <= profile.projector.rpc.maxCallsPerProviderPerRun && + calls.every( + (call) => + call.providerIdentity === expected.identity && + call.providerEndpointCommitment === expected.endpointCommitment && + call.attempt <= profile.projector.rpc.maxAttemptsPerCall && + call.startedOffsetMs + call.durationMs <= + rpcTrace.elapsedMs, + ) && + successfulOperationCounts.getChainId === 1 && + successfulOperationCounts.getBlockNumber === 1 && + successfulOperationCounts.getBlock === requiredCandidateBatchSize + 1 && + successfulOperationCounts.getTransactionReceipt === + requiredCandidateBatchSize && + successfulOperationCounts.getBytecode === requiredCandidateBatchSize + ); + }); + const aggregateCalls = rpcTrace.providerCallCounts.reduce( + (total, count) => total + count, + 0, + ); + check( + "rpc-provider-trace", + expectedProviders.length === 2 && + expectedProviderCommitments.size === 2 && + observedProviderOrigins.length === 2 && + new Set(observedProviderOrigins).size === 2 && + providerTraceChecks.every(Boolean) && + rpcTrace.calls.length === aggregateCalls && + aggregateCalls <= profile.projector.rpc.maxAggregateCallsPerRun, + `${aggregateCalls} raw calls bound to exact Alchemy and QuickNode commitments`, + ); + + return { + schemaVersion: 1, + profileId: profile.profileId, + mode: "release", + releaseEvidenceAccepted: failures.length === 0, + checks, + failures, + artifactDigests: Object.fromEntries( + ARTIFACT_KEYS.map((key) => [key, bundle.artifacts[key].sha256]), + ), + }; +} diff --git a/scripts/perf/read-model-gate.mjs b/scripts/perf/read-model-gate.mjs new file mode 100644 index 00000000..fa6def30 --- /dev/null +++ b/scripts/perf/read-model-gate.mjs @@ -0,0 +1,1105 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { lstatSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { + evaluateReadModelReleaseEvidence, + loadReadModelReleaseEvidence, + parseReadModelLoadProfile, + sha256Bytes, +} from "./read-model-gate-core.mjs"; +import { + commitExploreMatrixCase, + commitExploreMatrixPage, + EXPLORE_MATRIX_CLAMP_PAGE, + EXPLORE_MATRIX_MANIFEST_FILE, + EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND, + EXPLORE_MATRIX_PAGE_SIZE, + EXPLORE_MATRIX_PAGES_FILE, + EXPLORE_MATRIX_SORTS, + exploreMatrixCorpusCommitment, + normalizeExploreMatrixQuery, + sha256Canonical, +} from "./read-model-capture.mjs"; +import { + verifyLiveCacheAndKeyContracts, + verifyLiveRollbackTarget, + verifyLiveVercelBinding, +} from "./read-model-live-verifier.mjs"; +import { expectedProductionProviderBindings } from "./read-model-provider-binding.mjs"; +import { evaluateReadModelSourceContracts } from "./read-model-source-contracts.mjs"; + +function parseArguments(argv) { + let evidencePath; + let requireReleaseEvidence = false; + let ifPresent = false; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--evidence") { + evidencePath = argv[index + 1]; + if (!evidencePath || evidencePath.startsWith("--")) { + throw new Error("--evidence requires a bundle path"); + } + index += 1; + continue; + } + if (argument === "--require-release-evidence") { + requireReleaseEvidence = true; + continue; + } + if (argument === "--if-present") { + ifPresent = true; + continue; + } + throw new Error(`unsupported argument: ${argument}`); + } + if (!requireReleaseEvidence) { + throw new Error("the gate only accepts --require-release-evidence mode"); + } + return { evidencePath, ifPresent }; +} + +function output(value, exitCode) { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); + process.exitCode = exitCode; +} + +const HEX_ADDRESS = /^0x[0-9a-f]{40}$/u; +const HEX_HASH = /^0x[0-9a-f]{64}$/u; +const HEX_DIGEST = /^[0-9a-f]{64}$/u; +const PROBE_NONCE = /^[1-9]\d{12}-[0-9a-f]{64}-(?:0|[1-9]\d{0,9})$/u; +const MATRIX_MAXIMUM_BYTES = 32 * 1024 * 1024; +const RELEASE_VERSIONS = Object.freeze([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +]); + +function matrixObject(value, path) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${path}: expected an object`); + } + return value; +} + +function matrixExact(value, keys, path) { + const input = matrixObject(value, path); + const actual = Object.keys(input).sort(); + const expected = [...keys].sort(); + if ( + actual.length !== expected.length || + actual.some((key, index) => key !== expected[index]) + ) { + throw new Error(`${path}: unexpected shape`); + } + return input; +} + +function matrixString(value, path, maximum = 512) { + if ( + typeof value !== "string" || + value.length < 1 || + value.length > maximum + ) { + throw new Error(`${path}: expected a bounded non-empty string`); + } + return value; +} + +function matrixInteger(value, path, minimum = 0) { + if (!Number.isSafeInteger(value) || value < minimum) { + throw new Error(`${path}: expected a safe integer >= ${minimum}`); + } + return value; +} + +function matrixDigest(value, path) { + if (!HEX_DIGEST.test(matrixString(value, path, 64))) { + throw new Error(`${path}: expected a sha256 digest`); + } + return value; +} + +function parseMatrixCase(value, index) { + const path = `exploreMatrix.cases[${index}]`; + const input = matrixExact( + value, + [ + "caseId", + "kind", + "query", + "normalizedQuery", + "sourceTokenAddress", + "sourceReleaseVersion", + "commitment", + ], + path, + ); + matrixString(input.caseId, `${path}.caseId`, 128); + if (!["empty", "name", "symbol", "address"].includes(input.kind)) { + throw new Error(`${path}.kind: unsupported case kind`); + } + if (typeof input.query !== "string" || input.query.length > 256) { + throw new Error(`${path}.query: expected a bounded string`); + } + if ( + typeof input.normalizedQuery !== "string" || + input.normalizedQuery.length > 128 + ) { + throw new Error(`${path}.normalizedQuery: expected a bounded string`); + } + if (input.kind === "empty") { + if ( + input.sourceTokenAddress !== null || + input.sourceReleaseVersion !== null + ) { + throw new Error(`${path}: empty case must not claim a source token`); + } + } else if ( + !HEX_ADDRESS.test(input.sourceTokenAddress) || + !RELEASE_VERSIONS.includes(input.sourceReleaseVersion) + ) { + throw new Error(`${path}: query case has an invalid source token`); + } + matrixDigest(input.commitment, `${path}.commitment`); + return input; +} + +function parseMatrixToken(value, pageIndex, tokenIndex) { + const path = `exploreMatrix.pages[${pageIndex}].tokens[${tokenIndex}]`; + const input = matrixExact( + value, + ["tokenAddress", "name", "symbol", "releaseVersion"], + path, + ); + if (!HEX_ADDRESS.test(input.tokenAddress)) { + throw new Error(`${path}.tokenAddress: expected a canonical address`); + } + matrixString(input.name, `${path}.name`, 256); + matrixString(input.symbol, `${path}.symbol`, 128); + if (!RELEASE_VERSIONS.includes(input.releaseVersion)) { + throw new Error(`${path}.releaseVersion: unsupported release`); + } + return input; +} + +function parseMatrixPage(value, index) { + const path = `exploreMatrix.pages[${index}]`; + const input = matrixExact( + value, + [ + "schemaVersion", + "sequence", + "probeIssuedAtMs", + "probeNonce", + "probeSignatureSha256", + "caseId", + "caseCommitment", + "sort", + "requestedPage", + "resolvedPage", + "pageSize", + "total", + "totalPages", + "isClamp", + "requestPath", + "startedAtMs", + "completedAtMs", + "durationMs", + "status", + "cacheControl", + "vercelCache", + "shadowOverheadMs", + "parity", + "readSource", + "fallback", + "checkpointSha256", + "bodySha256", + "bodyBytes", + "tokenRowsSha256", + "tokens", + "pageCommitment", + ], + path, + ); + if (input.schemaVersion !== 1) { + throw new Error(`${path}.schemaVersion: expected 1`); + } + for (const [field, minimum] of [ + ["sequence", 0], + ["probeIssuedAtMs", 1], + ["requestedPage", 1], + ["resolvedPage", 1], + ["pageSize", 1], + ["total", 0], + ["totalPages", 0], + ["startedAtMs", 1], + ["completedAtMs", 1], + ["durationMs", 0], + ["status", 100], + ["bodyBytes", 0], + ]) { + matrixInteger(input[field], `${path}.${field}`, minimum); + } + if (input.shadowOverheadMs !== null) { + matrixInteger(input.shadowOverheadMs, `${path}.shadowOverheadMs`); + } + if (typeof input.isClamp !== "boolean") { + throw new Error(`${path}.isClamp: expected a boolean`); + } + for (const field of [ + "probeNonce", + "caseId", + "sort", + "requestPath", + "cacheControl", + "vercelCache", + "parity", + "readSource", + ]) { + matrixString(input[field], `${path}.${field}`, 4_096); + } + for (const field of [ + "probeSignatureSha256", + "caseCommitment", + "checkpointSha256", + "bodySha256", + "tokenRowsSha256", + "pageCommitment", + ]) { + matrixDigest(input[field], `${path}.${field}`); + } + if (input.fallback !== null && typeof input.fallback !== "boolean") { + throw new Error(`${path}.fallback: expected a measured boolean or null`); + } + if (!Array.isArray(input.tokens)) { + throw new Error(`${path}.tokens: expected an array`); + } + input.tokens = input.tokens.map((token, tokenIndex) => + parseMatrixToken(token, index, tokenIndex), + ); + return input; +} + +export function parseExploreMatrixManifest(value) { + const input = matrixExact( + value, + [ + "schemaVersion", + "profileId", + "captureNonce", + "capturedAt", + "target", + "dataset", + "checkpoint", + "matrix", + ], + "exploreMatrix", + ); + if (input.schemaVersion !== 1) { + throw new Error("exploreMatrix.schemaVersion: expected 1"); + } + matrixString(input.profileId, "exploreMatrix.profileId"); + if (!/^0x[0-9a-f]{64}$/u.test(input.captureNonce)) { + throw new Error("exploreMatrix.captureNonce: expected bytes32"); + } + if (!Number.isFinite(Date.parse(matrixString(input.capturedAt, "exploreMatrix.capturedAt")))) { + throw new Error("exploreMatrix.capturedAt: expected an ISO timestamp"); + } + const target = matrixExact( + input.target, + ["url", "vercelDeploymentId", "gitHead"], + "exploreMatrix.target", + ); + const targetUrl = new URL(matrixString(target.url, "exploreMatrix.target.url", 1_024)); + if ( + targetUrl.protocol !== "https:" || + targetUrl.pathname !== "/" || + targetUrl.search !== "" || + targetUrl.hash !== "" || + !targetUrl.hostname.endsWith(".vercel.app") + ) { + throw new Error("exploreMatrix.target.url: expected a staged Vercel URL"); + } + matrixString(target.vercelDeploymentId, "exploreMatrix.target.vercelDeploymentId"); + if (!/^[0-9a-f]{40}$/u.test(target.gitHead)) { + throw new Error("exploreMatrix.target.gitHead: expected a Git SHA"); + } + const dataset = matrixExact( + input.dataset, + [ + "manifestFile", + "manifestSha256", + "generatedAt", + "eligibleLaunchCount", + "releaseCounts", + "inventorySha256", + ], + "exploreMatrix.dataset", + ); + matrixString(dataset.manifestFile, "exploreMatrix.dataset.manifestFile"); + matrixDigest(dataset.manifestSha256, "exploreMatrix.dataset.manifestSha256"); + if (!Number.isFinite(Date.parse(matrixString(dataset.generatedAt, "exploreMatrix.dataset.generatedAt")))) { + throw new Error("exploreMatrix.dataset.generatedAt: expected an ISO timestamp"); + } + matrixInteger( + dataset.eligibleLaunchCount, + "exploreMatrix.dataset.eligibleLaunchCount", + 1, + ); + matrixExact( + dataset.releaseCounts, + RELEASE_VERSIONS, + "exploreMatrix.dataset.releaseCounts", + ); + for (const releaseVersion of RELEASE_VERSIONS) { + matrixInteger( + dataset.releaseCounts[releaseVersion], + `exploreMatrix.dataset.releaseCounts.${releaseVersion}`, + 1, + ); + } + matrixDigest(dataset.inventorySha256, "exploreMatrix.dataset.inventorySha256"); + const checkpoint = matrixExact( + input.checkpoint, + ["snapshot", "snapshotSha256"], + "exploreMatrix.checkpoint", + ); + matrixObject(checkpoint.snapshot, "exploreMatrix.checkpoint.snapshot"); + matrixDigest(checkpoint.snapshotSha256, "exploreMatrix.checkpoint.snapshotSha256"); + const matrix = matrixExact( + input.matrix, + [ + "sorts", + "pageSize", + "maxQueryCasesPerKind", + "cases", + "casesSha256", + "caseCounts", + "caseCount", + "pageCount", + "tokenObservationCount", + "pagesFile", + "pagesSha256", + "corpusSha256", + ], + "exploreMatrix.matrix", + ); + if (!Array.isArray(matrix.sorts)) { + throw new Error("exploreMatrix.matrix.sorts: expected an array"); + } + for (const [field, minimum] of [ + ["pageSize", 1], + ["maxQueryCasesPerKind", 1], + ["caseCount", 1], + ["pageCount", 1], + ["tokenObservationCount", 1], + ]) { + matrixInteger(matrix[field], `exploreMatrix.matrix.${field}`, minimum); + } + if (!Array.isArray(matrix.cases) || matrix.cases.length < 1) { + throw new Error("exploreMatrix.matrix.cases: expected cases"); + } + matrix.cases = matrix.cases.map(parseMatrixCase); + matrixExact( + matrix.caseCounts, + ["empty", "name", "symbol", "address"], + "exploreMatrix.matrix.caseCounts", + ); + for (const kind of ["empty", "name", "symbol", "address"]) { + matrixInteger( + matrix.caseCounts[kind], + `exploreMatrix.matrix.caseCounts.${kind}`, + 1, + ); + } + matrixString(matrix.pagesFile, "exploreMatrix.matrix.pagesFile"); + for (const field of ["casesSha256", "pagesSha256", "corpusSha256"]) { + matrixDigest(matrix[field], `exploreMatrix.matrix.${field}`); + } + return input; +} + +function readMatrixArtifact(directory, filename) { + const path = resolve(directory, filename); + const stat = lstatSync(path); + if (!stat.isFile() || stat.isSymbolicLink()) { + throw new Error(`${filename}: expected a regular file`); + } + if (stat.size < 2 || stat.size > MATRIX_MAXIMUM_BYTES) { + throw new Error(`${filename}: invalid artifact size`); + } + const bytes = readFileSync(path); + return { path, bytes, sha256: sha256Bytes(bytes) }; +} + +function parseMatrixPages(bytes) { + const text = bytes.toString("utf8"); + if (!text.endsWith("\n")) { + throw new Error("explore matrix pages must end with a newline"); + } + const lines = text.slice(0, -1).split("\n"); + if (lines.length < 1 || lines.length > 20_000 || lines.some((line) => line.length < 2)) { + throw new Error("explore matrix pages have an invalid cardinality"); + } + return lines.map((line, index) => { + let value; + try { + value = JSON.parse(line); + } catch { + throw new Error(`explore matrix page ${index} is not JSON`); + } + return parseMatrixPage(value, index); + }); +} + +export function loadExploreMatrixReleaseEvidence(input) { + const directory = dirname(resolve(input.evidencePath)); + const manifestArtifact = readMatrixArtifact( + directory, + EXPLORE_MATRIX_MANIFEST_FILE, + ); + const pagesArtifact = readMatrixArtifact(directory, EXPLORE_MATRIX_PAGES_FILE); + const manifest = parseExploreMatrixManifest( + JSON.parse(manifestArtifact.bytes.toString("utf8")), + ); + const pages = parseMatrixPages(pagesArtifact.bytes); + return { + manifest, + pages, + artifacts: { + exploreMatrixManifest: manifestArtifact, + exploreMatrixPages: pagesArtifact, + }, + }; +} + +function expectedMatrixRequestPath(queryCase, page) { + const search = new URLSearchParams(); + search.set("limit", String(EXPLORE_MATRIX_PAGE_SIZE)); + search.set("page", String(page.requestedPage)); + search.set("q", queryCase.query); + search.set("sort", page.sort); + search.set("__read_model_probe", page.probeNonce); + return `/api/explore?${search.toString()}`; +} + +function frozenInventorySha256(eligibleLaunches) { + return sha256Canonical( + eligibleLaunches + .map((launch) => ({ + tokenAddress: launch.tokenAddress.toLowerCase(), + transactionHash: launch.transactionHash.toLowerCase(), + releaseVersion: launch.releaseVersion, + })) + .sort((left, right) => + left.tokenAddress.localeCompare(right.tokenAddress), + ), + ); +} + +function queryMatchesInventoryToken(token, query) { + if (query === "") return true; + return ( + token.name.toLowerCase().includes(query) || + token.symbol.toLowerCase().includes(query) || + token.tokenAddress.includes(query) + ); +} + +function sameArray(left, right) { + return ( + left.length === right.length && + left.every((value, index) => value === right[index]) + ); +} + +export function evaluateExploreMatrixReleaseEvidence(matrixBundle, releaseBundle) { + const { manifest, pages } = matrixBundle; + const checks = []; + const failures = []; + const check = (id, accepted, detail) => { + checks.push({ id, status: accepted ? "pass" : "fail", detail }); + if (!accepted) failures.push({ id, detail }); + }; + const evidence = releaseBundle.evidence; + const dataset = releaseBundle.datasetManifest; + const eligibleLaunches = dataset.eligibleLaunches.map((launch) => ({ + tokenAddress: launch.tokenAddress.toLowerCase(), + transactionHash: launch.transactionHash.toLowerCase(), + releaseVersion: launch.releaseVersion, + })); + const releaseByToken = new Map( + eligibleLaunches.map((launch) => [ + launch.tokenAddress, + launch.releaseVersion, + ]), + ); + const matrixTargetMatches = + new URL(manifest.target.url).toString() === + new URL(evidence.target.url).toString() && + manifest.target.vercelDeploymentId === evidence.target.vercelDeploymentId && + manifest.target.gitHead === evidence.target.gitHead && + manifest.profileId === evidence.profileId && + manifest.captureNonce === evidence.captureNonce; + check( + "explore-matrix-deployment-binding", + matrixTargetMatches, + "matrix nonce, profile, Git SHA, URL and deployment id match the release bundle", + ); + + const expectedInventorySha256 = frozenInventorySha256(eligibleLaunches); + const datasetBinding = + manifest.dataset.manifestFile === "dataset-manifest.v1.json" && + manifest.dataset.manifestSha256 === + releaseBundle.artifacts.datasetManifest.sha256 && + manifest.dataset.generatedAt === dataset.generatedAt && + manifest.dataset.eligibleLaunchCount === eligibleLaunches.length && + sha256Canonical(manifest.dataset.releaseCounts) === + sha256Canonical(dataset.releaseCounts) && + manifest.dataset.inventorySha256 === expectedInventorySha256; + check( + "explore-matrix-inventory-binding", + datasetBinding, + `${manifest.dataset.eligibleLaunchCount} launches bind to the exact dataset digest and release counts`, + ); + + const snapshot = manifest.checkpoint.snapshot; + const checkpointValid = + snapshot.chainId === 1 && + typeof snapshot.blockNumber === "string" && + /^(0|[1-9]\d*)$/u.test(snapshot.blockNumber) && + typeof snapshot.blockHash === "string" && + HEX_HASH.test(snapshot.blockHash.toLowerCase()) && + Number.isSafeInteger(snapshot.confirmations) && + snapshot.confirmations >= 0 && + manifest.checkpoint.snapshotSha256 === sha256Canonical(snapshot) && + pages.every( + (page) => + page.checkpointSha256 === manifest.checkpoint.snapshotSha256, + ); + check( + "explore-matrix-checkpoint-binding", + checkpointValid, + `every page binds to checkpoint ${String(snapshot.blockNumber)}:${String(snapshot.blockHash)}`, + ); + + const cases = manifest.matrix.cases; + const caseById = new Map(cases.map((queryCase) => [queryCase.caseId, queryCase])); + const actualCaseCounts = Object.fromEntries( + ["empty", "name", "symbol", "address"].map((kind) => [ + kind, + cases.filter((queryCase) => queryCase.kind === kind).length, + ]), + ); + const casesBound = + manifest.matrix.sorts.length === EXPLORE_MATRIX_SORTS.length && + manifest.matrix.sorts.every( + (sort, index) => sort === EXPLORE_MATRIX_SORTS[index], + ) && + manifest.matrix.pageSize === EXPLORE_MATRIX_PAGE_SIZE && + manifest.matrix.maxQueryCasesPerKind === + EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND && + manifest.matrix.caseCount === cases.length && + new Set(cases.map((queryCase) => queryCase.caseId)).size === cases.length && + cases.every( + (queryCase) => + queryCase.normalizedQuery === + normalizeExploreMatrixQuery(queryCase.query) && + queryCase.commitment === commitExploreMatrixCase(queryCase), + ) && + actualCaseCounts.empty === 1 && + ["name", "symbol", "address"].every( + (kind) => + actualCaseCounts[kind] === + EXPLORE_MATRIX_MAX_QUERY_CASES_PER_KIND, + ) && + ["empty", "name", "symbol", "address"].every( + (kind) => manifest.matrix.caseCounts[kind] === actualCaseCounts[kind], + ) && + cases.filter((queryCase) => queryCase.kind === "empty")[0] + ?.normalizedQuery === "" && + manifest.matrix.casesSha256 === sha256Canonical(cases); + check( + "explore-matrix-case-commitments", + casesBound, + `${cases.length} deterministic empty/name/symbol/address cases are committed`, + ); + + const requestPaths = new Set(); + const pageEvidenceValid = pages.every((page, index) => { + const queryCase = caseById.get(page.caseId); + const expectedNonce = `${page.probeIssuedAtMs}-${manifest.captureNonce.slice(2)}-${page.sequence}`; + const requestPath = queryCase + ? expectedMatrixRequestPath(queryCase, page) + : "missing"; + requestPaths.add(page.requestPath); + return ( + page.sequence === index && + page.probeNonce === expectedNonce && + PROBE_NONCE.test(page.probeNonce) && + page.probeIssuedAtMs <= page.startedAtMs && + page.startedAtMs - page.probeIssuedAtMs <= 5_000 && + page.completedAtMs - page.startedAtMs === page.durationMs && + HEX_DIGEST.test(page.probeSignatureSha256) && + queryCase !== undefined && + page.caseCommitment === queryCase.commitment && + EXPLORE_MATRIX_SORTS.includes(page.sort) && + page.pageSize === EXPLORE_MATRIX_PAGE_SIZE && + page.requestPath === requestPath && + page.tokenRowsSha256 === sha256Canonical(page.tokens) && + page.pageCommitment === commitExploreMatrixPage(page) && + page.tokens.every( + (token) => + releaseByToken.get(token.tokenAddress) === token.releaseVersion, + ) + ); + }); + check( + "explore-matrix-signed-page-bindings", + pageEvidenceValid && requestPaths.size === pages.length, + `${pages.length} unique route-bound probe nonces and page commitments were captured`, + ); + + const cacheValid = pages.every( + (page) => + page.status === 200 && + page.cacheControl === "private, no-store" && + ["MISS", "BYPASS"].includes(page.vercelCache), + ); + check( + "explore-matrix-cache", + cacheValid, + "every matrix page reached the staged origin under the private no-store probe contract", + ); + const parityValid = pages.every( + (page) => + page.parity === "match" && + ["rpc", "blob", "indexed"].includes(page.readSource) && + Number.isSafeInteger(page.shadowOverheadMs) && + page.shadowOverheadMs >= 0 && + page.shadowOverheadMs <= page.durationMs, + ); + check( + "explore-matrix-parity", + parityValid, + "every signed Explore matrix response reports exact shadow parity", + ); + const fallbackValid = pages.every((page) => page.fallback === false); + check( + "explore-matrix-fallback", + fallbackValid, + "no matrix request used a live fallback", + ); + + const emptyNewestRows = pages + .filter( + (page) => + page.caseId === "empty" && + page.sort === "newest" && + page.isClamp === false, + ) + .sort((left, right) => left.requestedPage - right.requestedPage) + .flatMap((page) => page.tokens); + const inventoryByToken = new Map( + emptyNewestRows.map((token) => [token.tokenAddress, token]), + ); + const expectedAddresses = [...releaseByToken.keys()].sort(); + const observedAddresses = [...inventoryByToken.keys()].sort(); + const metadataStable = pages.every((page) => + page.tokens.every((token) => { + const frozen = inventoryByToken.get(token.tokenAddress); + return ( + frozen !== undefined && + frozen.name === token.name && + frozen.symbol === token.symbol && + frozen.releaseVersion === token.releaseVersion + ); + }), + ); + check( + "explore-matrix-frozen-inventory", + sameArray(observedAddresses, expectedAddresses) && metadataStable, + `${observedAddresses.length}/${expectedAddresses.length} frozen token identities were observed with stable metadata`, + ); + + const queryCasesValid = cases.every((queryCase) => { + if (queryCase.kind === "empty") { + return ( + queryCase.sourceTokenAddress === null && + queryCase.sourceReleaseVersion === null && + queryCase.normalizedQuery === "" + ); + } + const source = inventoryByToken.get(queryCase.sourceTokenAddress); + if (!source || source.releaseVersion !== queryCase.sourceReleaseVersion) { + return false; + } + if (queryCase.kind === "name") { + return normalizeExploreMatrixQuery(source.name) === queryCase.normalizedQuery; + } + if (queryCase.kind === "symbol") { + return normalizeExploreMatrixQuery(source.symbol) === queryCase.normalizedQuery; + } + return source.tokenAddress === queryCase.normalizedQuery; + }); + check( + "explore-matrix-real-query-sources", + queryCasesValid, + "every bounded query is derived from a committed token name, symbol or address", + ); + + let traversalValid = true; + let clampValid = true; + let missingCases = 0; + const releaseSortCoverage = new Map(); + for (const queryCase of cases) { + const expectedMatches = [...inventoryByToken.values()].filter((token) => + queryMatchesInventoryToken(token, queryCase.normalizedQuery), + ); + if ( + queryCase.kind !== "empty" && + (expectedMatches.length < 1 || + expectedMatches.length > EXPLORE_MATRIX_PAGE_SIZE) + ) { + traversalValid = false; + } + const expectedMatchAddresses = expectedMatches + .map((token) => token.tokenAddress) + .sort(); + const expectedTotalPages = Math.ceil( + expectedMatches.length / EXPLORE_MATRIX_PAGE_SIZE, + ); + const expectedRealPageCount = Math.max(1, expectedTotalPages); + for (const sort of EXPLORE_MATRIX_SORTS) { + const group = pages.filter( + (page) => page.caseId === queryCase.caseId && page.sort === sort, + ); + const realPages = group + .filter((page) => page.isClamp === false) + .sort((left, right) => left.requestedPage - right.requestedPage); + const clampPages = group.filter((page) => page.isClamp === true); + if (group.length < 1) missingCases += 1; + const realRequestedPages = realPages.map((page) => page.requestedPage); + const expectedRequestedPages = Array.from( + { length: expectedRealPageCount }, + (_, index) => index + 1, + ); + const realAddresses = realPages + .flatMap((page) => page.tokens.map((token) => token.tokenAddress)) + .sort(); + const pageMetadataValid = realPages.every((page, index) => { + const expectedResolvedPage = expectedTotalPages === 0 ? 1 : index + 1; + const expectedTokenCount = Math.min( + EXPLORE_MATRIX_PAGE_SIZE, + Math.max( + 0, + expectedMatches.length - index * EXPLORE_MATRIX_PAGE_SIZE, + ), + ); + return ( + page.resolvedPage === expectedResolvedPage && + page.total === expectedMatches.length && + page.totalPages === expectedTotalPages && + page.tokens.length === expectedTokenCount + ); + }); + traversalValid = + traversalValid && + sameArray(realRequestedPages, expectedRequestedPages) && + new Set(realAddresses).size === realAddresses.length && + sameArray(realAddresses, expectedMatchAddresses) && + pageMetadataValid; + const lastRealPage = realPages.at(-1); + const clampPage = clampPages[0]; + clampValid = + clampValid && + clampPages.length === 1 && + clampPage?.requestedPage === EXPLORE_MATRIX_CLAMP_PAGE && + clampPage?.resolvedPage === Math.max(1, expectedTotalPages) && + clampPage?.total === expectedMatches.length && + clampPage?.totalPages === expectedTotalPages && + lastRealPage !== undefined && + sameArray( + clampPage.tokens.map((token) => token.tokenAddress), + lastRealPage.tokens.map((token) => token.tokenAddress), + ); + if (queryCase.kind === "empty") { + for (const releaseVersion of RELEASE_VERSIONS) { + const expectedReleaseAddresses = eligibleLaunches + .filter((launch) => launch.releaseVersion === releaseVersion) + .map((launch) => launch.tokenAddress) + .sort(); + const observedReleaseAddresses = realPages + .flatMap((page) => page.tokens) + .filter((token) => token.releaseVersion === releaseVersion) + .map((token) => token.tokenAddress) + .sort(); + releaseSortCoverage.set( + `${releaseVersion}:${sort}`, + sameArray(observedReleaseAddresses, expectedReleaseAddresses), + ); + } + } + } + } + check( + "explore-matrix-case-coverage", + missingCases === 0 && + pages.every((page) => caseById.has(page.caseId)) && + pages.length === manifest.matrix.pageCount, + `${missingCases} case/sort traversals are missing`, + ); + check( + "explore-matrix-page-and-cursor-coverage", + traversalValid, + "every real page is present once with no cursor gap, duplicate or inventory omission", + ); + check( + "explore-matrix-page-clamping", + clampValid, + "every case and sort includes one maximum-page clamp bound to the final real page", + ); + for (const releaseVersion of RELEASE_VERSIONS) { + for (const sort of EXPLORE_MATRIX_SORTS) { + check( + `explore-matrix-release-${releaseVersion}-${sort}`, + releaseSortCoverage.get(`${releaseVersion}:${sort}`) === true, + `${releaseVersion} is completely represented in the ${sort} traversal`, + ); + } + } + + const tokenObservationCount = pages.reduce( + (total, page) => total + page.tokens.length, + 0, + ); + const pagesDigestBound = + manifest.matrix.pagesFile === EXPLORE_MATRIX_PAGES_FILE && + manifest.matrix.pagesSha256 === + matrixBundle.artifacts.exploreMatrixPages.sha256 && + manifest.matrix.pageCount === pages.length && + manifest.matrix.tokenObservationCount === tokenObservationCount; + check( + "explore-matrix-page-digest", + pagesDigestBound, + `${pages.length} pages and ${tokenObservationCount} token observations match the committed artifact`, + ); + const corpusSha256 = exploreMatrixCorpusCommitment({ + captureNonce: manifest.captureNonce, + target: manifest.target, + datasetManifestSha256: manifest.dataset.manifestSha256, + inventorySha256: manifest.dataset.inventorySha256, + casesSha256: manifest.matrix.casesSha256, + pagesSha256: manifest.matrix.pagesSha256, + checkpointSha256: manifest.checkpoint.snapshotSha256, + eligibleLaunchCount: manifest.dataset.eligibleLaunchCount, + caseCount: manifest.matrix.caseCount, + pageCount: manifest.matrix.pageCount, + tokenObservationCount: manifest.matrix.tokenObservationCount, + }); + check( + "explore-matrix-corpus-digest", + corpusSha256 === manifest.matrix.corpusSha256, + "deployment, dataset, checkpoint, cases, pages and counts share one corpus commitment", + ); + + const matrixCapturedAtMs = Date.parse(manifest.capturedAt); + const releaseCapturedAtMs = Date.parse(evidence.capturedAt); + const firstMatrixStart = Math.min(...pages.map((page) => page.startedAtMs)); + const lastMatrixCompletion = Math.max( + ...pages.map((page) => page.completedAtMs), + ); + const lastLoadCompletion = Math.max( + ...releaseBundle.httpSamples.map((sample) => sample.completedAtMs), + ); + const captureWindowValid = + firstMatrixStart >= lastLoadCompletion && + lastMatrixCompletion <= matrixCapturedAtMs && + matrixCapturedAtMs <= releaseCapturedAtMs && + releaseCapturedAtMs - matrixCapturedAtMs <= 5_000; + check( + "explore-matrix-capture-window", + captureWindowValid, + "the separate matrix ran after the load sample and before the signed release manifest", + ); + + return { + schemaVersion: 1, + status: failures.length === 0 ? "accepted" : "rejected", + releaseEvidenceAccepted: failures.length === 0, + checks, + failures, + artifactDigests: { + exploreMatrixManifest: + matrixBundle.artifacts.exploreMatrixManifest.sha256, + exploreMatrixPages: matrixBundle.artifacts.exploreMatrixPages.sha256, + }, + }; +} + +export async function main(argv = process.argv.slice(2)) { + const rootDirectory = process.cwd(); + const args = parseArguments(argv); + const evidencePath = + args.evidencePath ?? + process.env.PROGRAMMABLE_READ_MODEL_PERF_EVIDENCE_PATH; + if (!evidencePath && args.ifPresent) { + output( + { + schemaVersion: 1, + mode: "release", + status: "skipped", + releaseEvidenceAccepted: false, + reason: "no exact release evidence was explicitly provided", + }, + 0, + ); + return; + } + if (!evidencePath) { + throw new Error( + "PROGRAMMABLE_READ_MODEL_PERF_EVIDENCE_PATH or --evidence is required", + ); + } + const profile = parseReadModelLoadProfile( + JSON.parse( + readFileSync( + resolve(rootDirectory, "config/read-model-release-profile.v1.json"), + "utf8", + ), + ), + ); + const gitHead = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: rootDirectory, + encoding: "utf8", + }).trim(); + const expectedProviders = expectedProductionProviderBindings(); + const resolvedEvidencePath = resolve(rootDirectory, evidencePath); + const bundle = loadReadModelReleaseEvidence({ + profile, + evidencePath: resolvedEvidencePath, + }); + const matrixBundle = loadExploreMatrixReleaseEvidence({ + evidencePath: resolvedEvidencePath, + }); + const expectedTargetUrl = process.env.PROGRAMMABLE_READ_MODEL_TARGET_URL; + const expectedDeploymentId = + process.env.PROGRAMMABLE_READ_MODEL_VERCEL_DEPLOYMENT_ID; + if (!expectedTargetUrl || !expectedDeploymentId) { + throw new Error( + "PROGRAMMABLE_READ_MODEL_TARGET_URL and PROGRAMMABLE_READ_MODEL_VERCEL_DEPLOYMENT_ID are required", + ); + } + const exactWorkflowTarget = + new URL(bundle.evidence.target.url).toString() === + new URL(expectedTargetUrl).toString() && + bundle.evidence.target.vercelDeploymentId === expectedDeploymentId; + const evidenceResult = evaluateReadModelReleaseEvidence(bundle, { + gitHead, + expectedProviders, + }); + const matrixResult = evaluateExploreMatrixReleaseEvidence( + matrixBundle, + bundle, + ); + const sourceResult = evaluateReadModelSourceContracts( + rootDirectory, + profile, + ); + const [vercelResult, rollbackResult, cacheResult] = await Promise.all([ + verifyLiveVercelBinding({ + evidence: bundle.evidence, + gitHead, + token: process.env.VERCEL_TOKEN, + teamId: process.env.VERCEL_ORG_ID, + projectId: process.env.VERCEL_PROJECT_ID, + }), + verifyLiveRollbackTarget({ + stagedDeploymentId: bundle.evidence.target.vercelDeploymentId, + token: process.env.VERCEL_TOKEN, + teamId: process.env.VERCEL_ORG_ID, + projectId: process.env.VERCEL_PROJECT_ID, + productionDomain: + process.env.PROGRAMMABLE_PRODUCTION_DOMAIN ?? "programmable.family", + }), + verifyLiveCacheAndKeyContracts({ + profile, + evidence: bundle.evidence, + datasetManifest: bundle.datasetManifest, + }), + ]); + const failures = [ + ...(exactWorkflowTarget + ? [] + : [ + { + id: "workflow-target-binding", + detail: "evidence does not target the staged deployment", + }, + ]), + ...evidenceResult.failures, + ...matrixResult.failures, + ...sourceResult.failures, + ...vercelResult.failures, + ...rollbackResult.failures, + ...cacheResult.failures, + ]; + output( + { + schemaVersion: 1, + profileId: profile.profileId, + mode: "release", + status: failures.length === 0 ? "accepted" : "rejected", + releaseEvidenceAccepted: failures.length === 0, + checks: [ + { + id: "workflow-target-binding", + status: exactWorkflowTarget ? "pass" : "fail", + detail: "evidence targets the staged deployment", + }, + ...evidenceResult.checks, + ...matrixResult.checks, + ...sourceResult.checks, + ...vercelResult.checks, + ...rollbackResult.checks, + ...cacheResult.checks, + ], + failures, + artifactDigests: { + ...evidenceResult.artifactDigests, + ...matrixResult.artifactDigests, + }, + }, + failures.length === 0 ? 0 : 1, + ); +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + main().catch((error) => { + output( + { + schemaVersion: 1, + mode: "release", + status: "rejected", + releaseEvidenceAccepted: false, + checks: [], + failures: [ + { + id: "gate-input", + detail: error instanceof Error ? error.message : "invalid input", + }, + ], + }, + 1, + ); + }); +} diff --git a/scripts/perf/read-model-live-verifier.mjs b/scripts/perf/read-model-live-verifier.mjs new file mode 100644 index 00000000..969d4176 --- /dev/null +++ b/scripts/perf/read-model-live-verifier.mjs @@ -0,0 +1,475 @@ +import { sha256Bytes } from "./read-model-gate-core.mjs"; + +function safeJson(text, subject) { + if (text.length < 2 || text.length > 2 * 1024 * 1024) { + throw new Error(`${subject} returned an invalid response size`); + } + try { + return JSON.parse(text); + } catch { + throw new Error(`${subject} did not return JSON`); + } +} + +async function boundedFetch(fetchImpl, url, init, timeoutMs = 10_000) { + return fetchImpl(url, { + ...init, + redirect: "error", + signal: AbortSignal.timeout(timeoutMs), + }); +} + +function deploymentCommit(deployment) { + const candidates = [ + deployment?.meta?.githubCommitSha, + deployment?.gitSource?.sha, + deployment?.github?.commitSha, + ]; + return candidates.find( + (value) => typeof value === "string" && /^[0-9a-f]{40}$/u.test(value), + ); +} + +export async function fetchVercelDeployment(input) { + if (!input.token || !input.teamId) { + throw new Error("VERCEL_TOKEN and VERCEL_ORG_ID are required"); + } + const endpoint = new URL( + `/v13/deployments/${encodeURIComponent(input.idOrUrl)}`, + "https://api.vercel.com", + ); + endpoint.searchParams.set("teamId", input.teamId); + const response = await boundedFetch( + input.fetchImpl ?? fetch, + endpoint, + { headers: { Authorization: `Bearer ${input.token}` } }, + ); + if (!response.ok) { + throw new Error(`Vercel deployment lookup failed with HTTP ${response.status}`); + } + return safeJson(await response.text(), "Vercel"); +} + +export { deploymentCommit }; + +export async function verifyLiveVercelBinding(input) { + if (!input.token || !input.teamId || !input.projectId) { + throw new Error( + "VERCEL_TOKEN, VERCEL_ORG_ID and VERCEL_PROJECT_ID are required", + ); + } + const deployment = await fetchVercelDeployment({ + idOrUrl: input.evidence.target.vercelDeploymentId, + token: input.token, + teamId: input.teamId, + fetchImpl: input.fetchImpl, + }); + const target = new URL(input.evidence.target.url); + const deploymentHost = + typeof deployment.url === "string" + ? deployment.url.replace(/^https?:\/\//u, "").replace(/\/$/u, "") + : ""; + const commit = deploymentCommit(deployment); + const checks = [ + { + id: "vercel-deployment-id", + condition: deployment.id === input.evidence.target.vercelDeploymentId, + detail: "Vercel returned the exact deployment id", + }, + { + id: "vercel-target-url", + condition: deploymentHost === target.host, + detail: "Vercel returned the exact deployment hostname", + }, + { + id: "vercel-project", + condition: + deployment.projectId === input.projectId || + deployment.project?.id === input.projectId, + detail: "deployment belongs to the configured Vercel project", + }, + { + id: "vercel-ready", + condition: deployment.readyState === "READY", + detail: "deployment is READY", + }, + { + id: "vercel-git-head", + condition: + commit === input.gitHead && + input.evidence.target.gitHead === input.gitHead, + detail: "Vercel metadata is bound to the exact local Git HEAD", + }, + ]; + return { + ok: checks.every((check) => check.condition), + checks: checks.map(({ id, condition, detail }) => ({ + id, + status: condition ? "pass" : "fail", + detail, + })), + failures: checks + .filter((check) => !check.condition) + .map(({ id, detail }) => ({ id, detail })), + }; +} + +function deploymentAliases(deployment) { + const values = [deployment?.alias, deployment?.aliases].flatMap((value) => + Array.isArray(value) ? value : value === undefined ? [] : [value], + ); + return new Set( + values.flatMap((value) => { + if (typeof value === "string") return [value.toLowerCase()]; + if (value && typeof value === "object") { + const candidate = value.alias ?? value.domain; + return typeof candidate === "string" + ? [candidate.toLowerCase()] + : []; + } + return []; + }), + ); +} + +export async function verifyLiveRollbackTarget(input) { + if (!input.token || !input.teamId || !input.projectId) { + throw new Error( + "VERCEL_TOKEN, VERCEL_ORG_ID and VERCEL_PROJECT_ID are required", + ); + } + const productionDomain = ( + input.productionDomain ?? "programmable.family" + ).toLowerCase(); + if (!/^[a-z0-9.-]+$/u.test(productionDomain)) { + throw new Error("production rollback domain is invalid"); + } + const endpoint = new URL("/v6/deployments", "https://api.vercel.com"); + endpoint.searchParams.set("teamId", input.teamId); + endpoint.searchParams.set("projectId", input.projectId); + endpoint.searchParams.set("target", "production"); + endpoint.searchParams.set("state", "READY"); + endpoint.searchParams.set("limit", "20"); + const response = await boundedFetch( + input.fetchImpl ?? fetch, + endpoint, + { headers: { Authorization: `Bearer ${input.token}` } }, + ); + if (!response.ok) { + throw new Error( + `Vercel rollback lookup failed with HTTP ${response.status}`, + ); + } + const payload = safeJson(await response.text(), "Vercel rollback lookup"); + const deployments = Array.isArray(payload?.deployments) + ? payload.deployments + : []; + const rollbackTarget = deployments.find( + (deployment) => + deployment?.id !== input.stagedDeploymentId && + deployment?.readyState === "READY" && + deployment?.target === "production" && + (deployment?.projectId === input.projectId || + deployment?.project?.id === input.projectId) && + deploymentAliases(deployment).has(productionDomain), + ); + const checks = [ + { + id: "vercel-rollback-target", + condition: Boolean(rollbackTarget), + detail: + "the current production domain has a distinct READY deployment available for rollback", + }, + { + id: "vercel-rollback-project", + condition: + rollbackTarget?.projectId === input.projectId || + rollbackTarget?.project?.id === input.projectId, + detail: "the rollback deployment belongs to the configured Vercel project", + }, + ]; + return { + ok: checks.every((check) => check.condition), + rollbackDeploymentId: + typeof rollbackTarget?.id === "string" ? rollbackTarget.id : null, + checks: checks.map(({ id, condition, detail }) => ({ + id, + status: condition ? "pass" : "fail", + detail, + })), + failures: checks + .filter((check) => !check.condition) + .map(({ id, detail }) => ({ id, detail })), + }; +} + +async function requestJson(fetchImpl, targetUrl, path, expectedCacheControl) { + const url = new URL(path, targetUrl); + const response = await boundedFetch( + fetchImpl, + url, + { headers: { Accept: "application/json" } }, + ); + const text = await response.text(); + const body = safeJson(text, url.pathname); + return { + ok: + response.status >= 200 && + response.status < 300 && + response.headers.get("cache-control") === expectedCacheControl, + status: response.status, + cacheControl: response.headers.get("cache-control"), + body, + bodySha256: sha256Bytes(Buffer.from(text)), + }; +} + +function sameAddress(left, right) { + return ( + typeof left === "string" && + typeof right === "string" && + left.toLowerCase() === right.toLowerCase() + ); +} + +export async function verifyLiveCacheAndKeyContracts(input) { + const fetchImpl = input.fetchImpl ?? fetch; + const { profile, evidence, datasetManifest } = input; + const keys = datasetManifest.keys; + const primaryTokenAddress = keys.tokenAddresses[0]; + const secondaryTokenAddress = keys.tokenAddresses[1]; + const primaryAccountAddress = keys.accountAddresses[0]; + const secondaryAccountAddress = keys.accountAddresses[1]; + const classicLaunch = keys.classicLaunches[0]; + const stockLaunch = keys.stockLaunches[0]; + const encodedPrimaryToken = encodeURIComponent(primaryTokenAddress); + const encodedSecondaryToken = encodeURIComponent(secondaryTokenAddress); + const encodedPrimaryAccount = encodeURIComponent(primaryAccountAddress); + const encodedSecondaryAccount = encodeURIComponent(secondaryAccountAddress); + const probes = await Promise.all([ + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore?limit=6&page=1&q=${encodedPrimaryToken}&sort=market-cap`, + profile.cacheContracts.exploreList, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore?limit=6&page=1&q=${encodedSecondaryToken}&sort=market-cap`, + profile.cacheContracts.exploreList, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/token?address=${encodedPrimaryToken}`, + profile.cacheContracts.tokenDetail, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/token?address=${encodedSecondaryToken}`, + profile.cacheContracts.tokenDetail, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/token/chart?address=${encodedPrimaryToken}&range=all`, + profile.cacheContracts.tokenChart, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/token/chart?address=${encodedSecondaryToken}&range=all`, + profile.cacheContracts.tokenChart, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/token/chart?address=${encodedPrimaryToken}&range=1h`, + profile.cacheContracts.tokenChart, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/profile?account=${encodedPrimaryAccount}`, + profile.cacheContracts.creatorProfile, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/profile?account=${encodedSecondaryAccount}`, + profile.cacheContracts.creatorProfile, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/profile/classic-v3?account=${encodedPrimaryAccount}`, + profile.cacheContracts.classicProfile, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/profile/stock-paired?account=${encodedPrimaryAccount}`, + profile.cacheContracts.stockProfile, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/profile/classic-v3?account=${encodeURIComponent(classicLaunch.account)}&launch=${encodeURIComponent(classicLaunch.transactionHash)}`, + profile.cacheContracts.classicLaunchLookup, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/explore/launch/stock-paired?account=${encodeURIComponent(stockLaunch.account)}&transaction=${encodeURIComponent(stockLaunch.transactionHash)}`, + profile.cacheContracts.stockLaunchLookup, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/indexers/v1/tokens?address=${encodedPrimaryToken}`, + profile.cacheContracts.publicIndexer, + ), + requestJson( + fetchImpl, + evidence.target.url, + `/api/indexers/v1/tokens?address=${encodedSecondaryToken}`, + profile.cacheContracts.publicIndexer, + ), + requestJson( + fetchImpl, + evidence.target.url, + "/api/indexers/v1/token-list", + profile.cacheContracts.tokenList, + ), + requestJson( + fetchImpl, + evidence.target.url, + "/api/ops/health", + profile.cacheContracts.health, + ), + ]); + const [ + explorePrimary, + exploreSecondary, + tokenPrimary, + tokenSecondary, + chartPrimary, + chartSecondary, + chartHour, + profilePrimary, + profileSecondary, + classicProfile, + stockProfile, + classicLaunchLookup, + stockLaunchLookup, + indexerPrimary, + indexerSecondary, + tokenList, + health, + ] = probes; + const checks = [ + { + id: "live-cache-headers", + condition: probes.every((probe) => probe.ok), + detail: "live HTTP responses expose the exact route cache policies", + }, + { + id: "cache-key-explore-query", + condition: + explorePrimary.body?.query === primaryTokenAddress && + exploreSecondary.body?.query === secondaryTokenAddress, + detail: "Explore cache keys preserve distinct search queries", + }, + { + id: "cache-key-token-address", + condition: + sameAddress( + tokenPrimary.body?.token?.tokenAddress, + primaryTokenAddress, + ) && + sameAddress( + tokenSecondary.body?.token?.tokenAddress, + secondaryTokenAddress, + ), + detail: "token detail cache keys preserve distinct token addresses", + }, + { + id: "cache-key-chart-address", + condition: + sameAddress(chartPrimary.body?.address, primaryTokenAddress) && + sameAddress(chartSecondary.body?.address, secondaryTokenAddress), + detail: "chart cache keys preserve distinct token addresses", + }, + { + id: "cache-key-chart-range", + condition: + chartPrimary.body?.range === "all" && + chartSecondary.body?.range === "all" && + chartHour.body?.range === "1h" && + sameAddress(chartHour.body?.address, primaryTokenAddress), + detail: "chart cache keys preserve distinct ranges", + }, + { + id: "cache-key-profile-account", + condition: + sameAddress(profilePrimary.body?.account, primaryAccountAddress) && + sameAddress(profileSecondary.body?.account, secondaryAccountAddress), + detail: "profile cache keys preserve distinct accounts", + }, + { + id: "cache-key-classic-profile-account", + condition: sameAddress(classicProfile.body?.account, primaryAccountAddress), + detail: "Classic profile cache keys preserve the account", + }, + { + id: "cache-key-stock-profile-account", + condition: sameAddress(stockProfile.body?.account, primaryAccountAddress), + detail: "Stock-Paired profile cache keys preserve the account", + }, + { + id: "cache-key-classic-launch", + condition: + classicLaunchLookup.body?.launch?.launchTransactionHash?.toLowerCase() === + classicLaunch.transactionHash.toLowerCase(), + detail: "Classic launch lookup preserves the transaction key", + }, + { + id: "cache-key-stock-launch", + condition: + stockLaunchLookup.body?.launch?.transactionHash?.toLowerCase() === + stockLaunch.transactionHash.toLowerCase(), + detail: "Stock launch lookup preserves the transaction key", + }, + { + id: "cache-key-indexer-address", + condition: + sameAddress(indexerPrimary.body?.address, primaryTokenAddress) && + sameAddress(indexerSecondary.body?.address, secondaryTokenAddress), + detail: "indexer cache keys preserve distinct token addresses", + }, + { + id: "live-token-list", + condition: + Array.isArray(tokenList.body?.tokens) && tokenList.body.tokens.length > 0, + detail: "live token list is populated", + }, + { + id: "live-health", + condition: health.body?.status === "healthy", + detail: "live read-model health is healthy", + }, + ]; + return { + ok: checks.every((check) => check.condition), + checks: checks.map(({ id, condition, detail }) => ({ + id, + status: condition ? "pass" : "fail", + detail, + })), + failures: checks + .filter((check) => !check.condition) + .map(({ id, detail }) => ({ id, detail })), + }; +} diff --git a/scripts/perf/read-model-ops-source-contracts.mjs b/scripts/perf/read-model-ops-source-contracts.mjs new file mode 100644 index 00000000..af1bf5fa --- /dev/null +++ b/scripts/perf/read-model-ops-source-contracts.mjs @@ -0,0 +1,676 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const APPROVED_OPERATIONS = Object.freeze({ + legacyIndexer: Object.freeze({ + path: "/api/ops/index-v2", + schedule: "*/5 * * * *", + retainedUntil: "indexed-read-cutover", + route: "app/api/ops/index-v2/route.ts", + sha256: "9638ec482ff66c5f3b1377c60b946e6348fb895769b6f8596fca2dc8cfbac535", + closedAlias: Object.freeze({ + path: "/api/ops/index", + route: "app/api/ops/index/route.ts", + status: 410, + sha256: "bb498b00334df908029a588bec552516f281fdc0dfc3185bc5cd820984a9ee1f", + }), + }), + workers: Object.freeze([ + Object.freeze({ + id: "source-projector", + path: "/api/ops/projector", + schedule: "* * * * *", + activationEnvironment: "PROGRAMMABLE_PROJECTOR_ACTIVE", + route: Object.freeze({ + path: "app/api/ops/projector/route.ts", + sha256: "9b12168cbbadf0addac351c45f71931f3c04370bcd6cabe6174d21daeb00a94d", + }), + runtime: Object.freeze({ + path: "lib/data-pipeline/projector-runtime-config.server.ts", + sha256: "f54859e55f35b99784eebd6cef58a40a5848904be21417249f3bff5bf1c88637", + }), + dependencies: Object.freeze([ + Object.freeze({ + path: "lib/data-pipeline/candidate-projector-runtime-binding.server.ts", + sha256: "32efa13d740614f7e66fd20a0158edf3383f4f6643a7fe34268fabda6261931c", + }), + ]), + migrations: Object.freeze([ + Object.freeze({ + path: "supabase/migrations/20260731203900_projector_runtime_singleton_lease.sql", + sha256: "068f27a70ec6df57b84bf336fc2c46b316a7d10d40b9d489fc47e95acb6f74b0", + }), + Object.freeze({ + path: "supabase/migrations/20260731224000_projector_provider_evidence_binding.sql", + sha256: "0404f7c610a34af23fe536f021927efec4e0aede235068b70be04331c58f03af", + }), + Object.freeze({ + path: "supabase/migrations/20260801090000_bootstrap_dynamic_evidence_and_launch_requirements.sql", + sha256: "e095d128feb12c8962c81be003e693dd67417cfed209144c998ab57d5e8786aa", + }), + Object.freeze({ + path: "supabase/migrations/20260801091000_candidate_projector_unpromoted_gate.sql", + sha256: "cd8b5a4aa4801ca773cb84047edbf05349288cada47d671bd47e7d997902c91f", + }), + Object.freeze({ + path: "supabase/migrations/20260801092000_verify_candidate_database_promoted.sql", + sha256: "ed5f54a374ad8178393e88a3948281ad9acba10aebbbd5209ea6793691b8c677", + }), + Object.freeze({ + path: "supabase/migrations/20260801093000_bind_candidate_promotion_to_product.sql", + sha256: "c6a032ef371b2211004c8d72c0a8c4eec4ba630776210aed48d2d054e642dbbe", + }), + Object.freeze({ + path: "supabase/migrations/20260801125441_reuse_safe_head_observations.sql", + sha256: "afbeea7bcf60e492e51bfd0c56517613f32a6f87a0182af00c48bdaef6569e74", + }), + Object.freeze({ + path: "supabase/migrations/20260801144403_accept_uuid_v8_dynamic_source_lineage.sql", + sha256: "85e0509d2a4fa49062a18d891e51cd0c64c1015926c3c3ef47a83ce16edb4170", + }), + Object.freeze({ + path: "supabase/migrations/20260801155212_reuse_dual_rpc_block_evidence.sql", + sha256: "51142370cf7fdf2bd60c2812978fe2cbbacf99f42b87c72f0ad1ac61b303cf51", + }), + Object.freeze({ + path: "supabase/migrations/20260801204500_reuse_dual_rpc_block_evidence_constraint.sql", + sha256: "92cc63189b41eda613ba9da21b7ef21bee650a93f1825f5ee063727ee6c06b11", + }), + ]), + }), + Object.freeze({ + id: "market-projector", + path: "/api/ops/market-projector", + schedule: "* * * * *", + activationEnvironment: "PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE", + route: Object.freeze({ + path: "app/api/ops/market-projector/route.ts", + sha256: "73bf9299095cfdf75d5452513ee818e161297a83c6355760ab2f79a22a13edbd", + }), + runtime: Object.freeze({ + path: "lib/data-pipeline/market-projector-runtime.server.ts", + sha256: "ed1c55148d05a47d747616a4bc8250996780be65d053b989d51db21b4519109b", + }), + migrations: Object.freeze([ + Object.freeze({ + path: "supabase/migrations/20260731223000_market_projector_contract.sql", + sha256: "ea73f4112a53b25e72aa697d3fc0679bf9c6e7f93a496edd167803d6a7f81a24", + }), + ]), + }), + ]), + eventTriggers: Object.freeze([ + Object.freeze({ + id: "quicknode-stream-projector-wake", + path: "/api/ops/projector-wake", + provider: "quicknode-streams", + mode: "wake-only", + secretEnvironment: "PROGRAMMABLE_QUICKNODE_STREAM_SECRET", + route: Object.freeze({ + path: "app/api/ops/projector-wake/route.ts", + sha256: "cdea2e18ebdb545e0f4de7bdd54da181c5cf6fa715e5abe1ac32c0bae66d138b", + }), + verifier: Object.freeze({ + path: "lib/data-pipeline/quicknode-stream-wake.server.ts", + sha256: "9c452c2ae94b62d31ed2ffdaaf974b1acd4c7a856493ac02013e43f89eb4bc65", + }), + }), + ]), +}); + +function readSource(rootDirectory, path, overrides) { + if (Object.hasOwn(overrides, path)) return overrides[path]; + try { + return readFileSync(resolve(rootDirectory, path), "utf8"); + } catch { + return null; + } +} + +function parseJson(source) { + if (typeof source !== "string") return null; + try { + return JSON.parse(source); + } catch { + return null; + } +} + +function sha256(source) { + return typeof source === "string" + ? createHash("sha256").update(source, "utf8").digest("hex") + : null; +} + +function exactCronMap(vercel) { + if (!vercel || !Array.isArray(vercel.crons)) return null; + const entries = new Map(); + for (const cron of vercel.crons) { + if ( + !cron || + typeof cron.path !== "string" || + typeof cron.schedule !== "string" || + entries.has(cron.path) + ) { + return null; + } + entries.set(cron.path, cron.schedule); + } + return entries; +} + +function exactJson(left, right) { + return JSON.stringify(left) === JSON.stringify(right); +} + +function expectedDigest(path, approved, overrides) { + return overrides?.[path] ?? approved; +} + +function sourceBindingMatches(source, binding, expectedSha256Overrides) { + return ( + binding && + typeof binding.path === "string" && + typeof binding.sha256 === "string" && + sha256(source(binding.path)) === + expectedDigest(binding.path, binding.sha256, expectedSha256Overrides) + ); +} + +function routeIsAuthenticatedAndFailClosed(source, requireCutover = false) { + const directSecretBounds = + /Buffer\.byteLength\(secret,\s*["']utf8["']\)\s*<\s*32/u.test(source) && + /Buffer\.byteLength\(secret,\s*["']utf8["']\)\s*>\s*1_024/u.test(source); + const namedSecretBounds = + /const\s+secretLength\s*=\s*secret\s*\?\s*Buffer\.byteLength\(secret,\s*["']utf8["']\)\s*:\s*0/u.test(source) && + /secretLength\s*<\s*32/u.test(source) && + /secretLength\s*>\s*1_024/u.test(source); + const standardAuthorization = + /matchesBearer\(request,\s*process\.env\.CRON_SECRET\)/u.test(source) || + /const\s+secret\s*=\s*process\.env\.CRON_SECRET/u.test(source); + const cutoverAuthorization = + /PROGRAMMABLE_CUTOVER_BACKFILL_ACTIVE\s*===\s*["']true["']/u.test(source) && + /process\.env\.PROGRAMMABLE_CUTOVER_OPERATOR_SECRET/u.test(source) && + /x-programmable-cutover-mode/u.test(source) && + /raw-backfill-v1/u.test(source); + return ( + typeof source === "string" && + /request\.headers\.get\(["']authorization["']\)/u.test(source) && + (directSecretBounds || namedSecretBounds) && + /authorization(?:\?\.|\.)startsWith\(["']Bearer ["']\)/u.test(source) && + /provided\.length\s*===\s*expected\.length/u.test(source) && + /timingSafeEqual\(provided,\s*expected\)/u.test(source) && + standardAuthorization && + (!requireCutover || + (cutoverAuthorization && + /mode\s*===\s*["']cutover["']/u.test(source))) && + (/if\s*\(\s*!isAuthorized\(request\)\s*\)/u.test(source) || + /if\s*\(\s*mode\s*===\s*null\s*\)/u.test(source)) && + /status\s*:\s*401\b/u.test(source) && + /status\s*:\s*503\b/u.test(source) && + /["']Cache-Control["']\s*:\s*["']no-store["']/u.test(source) + ); +} + +function routeIsPermanentlyClosed(source, binding) { + return ( + typeof source === "string" && + binding?.status === 410 && + source.includes('code: "legacy_index_route_closed"') && + /status\s*:\s*410\b/u.test(source) && + /["']Cache-Control["']\s*:\s*["']no-store["']/u.test(source) + ); +} + +function activationIsExplicitAndSafe(source, environmentName) { + return ( + typeof source === "string" && + source.includes(`env.${environmentName}`) && + /===\s*["']false["']/u.test(source) && + /(?:===|!==)\s*["']true["']/u.test(source) && + /===\s*undefined/u.test(source) && + /status\s*:\s*["']disabled["']|return\s+["']disabled["']/u.test(source) && + /invalidRuntimeConfig|invalidInput|throw\s+/u.test(source) + ); +} + +function eventTriggerIsAuthenticatedAndBound(route, verifier, trigger) { + return ( + typeof route === "string" && + typeof verifier === "string" && + trigger?.provider === "quicknode-streams" && + trigger?.mode === "wake-only" && + trigger?.secretEnvironment === "PROGRAMMABLE_QUICKNODE_STREAM_SECRET" && + route.includes("verifyQuickNodeStreamWake(request)") && + route.includes("after(runWakeCycle)") && + route.includes("runConfiguredProjectorCycle()") && + route.includes("runConfiguredMarketProjectorCycle()") && + /export\s+async\s+function\s+POST\s*\(/u.test(route) && + /status\s*:\s*202\b/u.test(route) && + /["']Cache-Control["']\s*:\s*["']no-store["']/u.test(route) && + verifier.includes("PROGRAMMABLE_QUICKNODE_STREAM_SECRET") && + verifier.includes('exactHeader(request, "x-qn-nonce"') && + verifier.includes('exactHeader(request, "x-qn-timestamp"') && + verifier.includes('exactHeader(request, "x-qn-signature"') && + verifier.includes('createHmac("sha256", secret)') && + verifier.includes("timingSafeEqual(provided, expected)") && + verifier.includes("MAXIMUM_TIMESTAMP_AGE_SECONDS") && + verifier.includes("MAXIMUM_ENCODED_BODY_BYTES") && + verifier.includes("maxOutputLength: MAXIMUM_DECODED_BODY_BYTES") + ); +} + +function migrationContract(id, source) { + if (typeof source !== "string") return false; + if (id === "source-projector-lease") { + return ( + source.includes("try_acquire_projector_runtime_lease_v1") && + source.includes("release_projector_runtime_lease_v1") && + /force row level security/iu.test(source) + ); + } + if (id === "source-projector-provider-evidence") { + return ( + source.includes("projection_provider_execution_evidence") && + source.includes("reward_snapshot_provider_evidence") && + source.includes("projection_publication_provider_bindings") && + /force row level security/iu.test(source) + ); + } + if (id === "source-projector-safe-head-reuse") { + return ( + source.includes("append_or_reuse_safe_head_observation_v1") && + source.includes( + "safe_head_observations_epoch_id_content_fingerprint_key", + ) && + source.includes("for key share") && + source.includes("safe-head fingerprint replay conflicts with stored evidence") && + /security definer/iu.test(source) + ); + } + if (id === "source-projector-dynamic-lineage") { + return ( + source.includes("accept_uuid_v8_dynamic_source_lineage") || + (source.includes("dynamic_source") && + source.includes("uuid") && + /security definer/iu.test(source)) + ); + } + if (id === "source-projector-block-evidence-reuse") { + return ( + source.includes("append_or_reuse_dual_rpc_block_evidence_v1") && + source.includes( + "dual_rpc_block_evidence_epoch_id_content_fingerprint_key", + ) && + source.includes("block-evidence fingerprint replay conflicts with stored evidence") && + /security definer/iu.test(source) + ); + } + if (id === "source-projector-block-evidence-conflict-fence") { + return ( + source.includes("append_or_reuse_dual_rpc_block_evidence_v1") && + source.includes( + "dual_rpc_block_evidence_epoch_id_content_fingerprint_key", + ) && + source.includes( + "dual_rpc_block_evidence_observation_id_block_number_key", + ) && + source.includes("for key share") && + /security definer/iu.test(source) + ); + } + if (id === "candidate-control-bootstrap") { + return ( + source.includes("candidate_database_control") && + source.includes("initialize_candidate_database") && + source.includes("attest_candidate_database_promotion") && + source.includes("enforce_candidate_database_promotion") && + /force row level security/iu.test(source) + ); + } + if (id === "candidate-unpromoted-gate") { + return ( + source.includes("verify_candidate_database_unpromoted_v1") && + source.includes("envio:production-7f24e63") && + source.includes("programmable_private.assert_caller('programmable_projector')") && + /grant execute[\s\S]*to programmable_projector/iu.test(source) + ); + } + if (id === "candidate-promoted-gate") { + return ( + source.includes("verify_candidate_database_promoted_v1") && + source.includes("envio:production-7f24e63") && + source.includes("programmable_private.assert_caller('programmable_projector')") && + /grant execute[\s\S]*to programmable_projector/iu.test(source) + ); + } + if (id === "candidate-product-binding") { + return ( + source.includes("candidate_database_control_product_binding") && + source.includes("product_commit") && + source.includes("staged_deployment_id") && + source.includes("verify_candidate_database_promoted_v2") && + source.includes("candidate product-bound promotion CAS lost") && + /validate constraint candidate_database_control_product_binding/iu.test(source) + ); + } + if (id === "market-projector") { + return ( + source.includes("market_projector_cursor_history") && + source.includes("market_snapshot_lineage_memberships") && + source.includes("market_candle_lineage_memberships") && + source.includes("try_acquire_market_projector_runtime_lease_v1") && + source.includes("assert_market_projector_runtime_lease_v1") && + source.includes("release_market_projector_runtime_lease_v1") && + source.includes("projector_checkpoint_current") && + /cursor_block_global_log_index\s*<>\s*4294967295/iu.test(source) && + /cursor_candidate_id\s*<>\s*'empty-page'/iu.test(source) && + /force row level security/iu.test(source) + ); + } + return false; +} + +export function evaluateReadModelOperationsSourceContracts( + rootDirectory, + options = {}, +) { + const overrides = options.sourceOverrides ?? {}; + const expectedSha256Overrides = options.expectedSha256Overrides ?? {}; + const source = (path) => readSource(rootDirectory, path, overrides); + const checks = []; + const failures = []; + const check = (id, condition, detail) => { + const status = condition ? "pass" : "fail"; + checks.push({ id, status, detail }); + if (!condition) failures.push({ id, detail }); + }; + + const operations = parseJson(source("config/read-model-operations.v1.json")); + const vercel = parseJson(source("vercel.json")); + const crons = exactCronMap(vercel); + const workers = Array.isArray(operations?.workers) ? operations.workers : []; + const eventTriggers = Array.isArray(operations?.eventTriggers) + ? operations.eventTriggers + : []; + const unscheduled = Array.isArray(operations?.unscheduled) + ? operations.unscheduled + : []; + const approvedCrons = new Map([ + [APPROVED_OPERATIONS.legacyIndexer.path, APPROVED_OPERATIONS.legacyIndexer.schedule], + ...APPROVED_OPERATIONS.workers.map((worker) => [worker.path, worker.schedule]), + ]); + + check( + "ops-config-schema", + operations?.schemaVersion === 1 && + exactJson(operations?.legacyIndexer, APPROVED_OPERATIONS.legacyIndexer) && + exactJson(workers, APPROVED_OPERATIONS.workers) && + exactJson(eventTriggers, APPROVED_OPERATIONS.eventTriggers), + "the manifest exactly binds the reviewed legacy indexer, workers and event trigger", + ); + check( + "ops-cron-exact-set", + crons !== null && + crons.size === approvedCrons.size && + [...approvedCrons].every( + ([path, schedule]) => crons.get(path) === schedule, + ), + "Vercel has only the independently approved schedules", + ); + check( + "ops-legacy-cron-preserved", + crons?.get(APPROVED_OPERATIONS.legacyIndexer.path) === + APPROVED_OPERATIONS.legacyIndexer.schedule && + sha256(source(APPROVED_OPERATIONS.legacyIndexer.route)) === + expectedDigest( + APPROVED_OPERATIONS.legacyIndexer.route, + APPROVED_OPERATIONS.legacyIndexer.sha256, + expectedSha256Overrides, + ), + "the five-minute legacy route remains byte-bound until indexed-read cutover", + ); + const closedLegacyAlias = APPROVED_OPERATIONS.legacyIndexer.closedAlias; + check( + "ops-legacy-alias-closed", + !crons?.has(closedLegacyAlias.path) && + exactJson( + operations?.legacyIndexer?.closedAlias, + closedLegacyAlias, + ) && + sha256(source(closedLegacyAlias.route)) === closedLegacyAlias.sha256 && + routeIsPermanentlyClosed( + source(closedLegacyAlias.route), + closedLegacyAlias, + ), + "the former legacy writer alias is byte-bound to a permanent 410 response", + ); + + for (const approvedWorker of APPROVED_OPERATIONS.workers) { + const worker = workers.find(({ id }) => id === approvedWorker.id); + const route = source(approvedWorker.route.path); + const runtime = source(approvedWorker.runtime.path); + check( + `ops-${approvedWorker.id}-schedule`, + worker?.path === approvedWorker.path && + worker?.schedule === approvedWorker.schedule && + crons?.get(approvedWorker.path) === approvedWorker.schedule, + `${approvedWorker.id} has its independently fixed production schedule`, + ); + check( + `ops-${approvedWorker.id}-source-digests`, + sourceBindingMatches(source, worker?.route, expectedSha256Overrides) && + sourceBindingMatches(source, worker?.runtime, expectedSha256Overrides) && + (approvedWorker.dependencies ?? []).every((binding, index) => + sourceBindingMatches( + source, + worker?.dependencies?.[index], + expectedSha256Overrides, + ), + ) && + approvedWorker.migrations.every((binding, index) => + sourceBindingMatches( + source, + worker?.migrations?.[index], + expectedSha256Overrides, + ), + ), + `${approvedWorker.id} route, runtime and migrations match reviewed bytes`, + ); + check( + `ops-${approvedWorker.id}-route-auth`, + routeIsAuthenticatedAndFailClosed( + route, + approvedWorker.id === "source-projector", + ), + `${approvedWorker.id} reads Authorization, compares CRON_SECRET safely and fails closed`, + ); + check( + `ops-${approvedWorker.id}-activation`, + worker?.activationEnvironment === approvedWorker.activationEnvironment && + activationIsExplicitAndSafe(runtime, approvedWorker.activationEnvironment), + `${approvedWorker.id} is false by default and only exact true activates work`, + ); + const runtimeBinding = approvedWorker.id === "source-projector" + ? typeof runtime === "string" && + runtime.includes("createProjectorRuntimeLeaseController") && + /leaseController\.tryAcquire\(\)/u.test(runtime) && + /acquisition\.status\s*===\s*["']busy["']/u.test(runtime) + : typeof runtime === "string" && + /store\.tryAcquireLease\(\)/u.test(runtime) && + /store\.releaseLease\(lease\)/u.test(runtime) && + /status\s*:\s*["']busy["']/u.test(runtime) && + runtime.includes("sourceCheckpointGeneration"); + check( + `ops-${approvedWorker.id}-runtime-binding`, + runtimeBinding, + `${approvedWorker.id} executes through its singleton lease and checkpoint binding`, + ); + } + + const approvedTrigger = APPROVED_OPERATIONS.eventTriggers[0]; + const eventTrigger = eventTriggers.find(({ id }) => id === approvedTrigger.id); + check( + "ops-quicknode-stream-wake-binding", + eventTriggers.length === 1 && + eventTrigger?.path === approvedTrigger.path && + !crons?.has(approvedTrigger.path) && + sourceBindingMatches(source, eventTrigger?.route, expectedSha256Overrides) && + sourceBindingMatches(source, eventTrigger?.verifier, expectedSha256Overrides) && + eventTriggerIsAuthenticatedAndBound( + source(approvedTrigger.route.path), + source(approvedTrigger.verifier.path), + eventTrigger, + ), + "the unscheduled QuickNode webhook is HMAC-authenticated and only wakes the fenced projectors", + ); + + check( + "ops-reconciler-unscheduled", + unscheduled.length === 1 && + unscheduled[0]?.path === "/api/ops/reconcile-preparity" && + !crons?.has(unscheduled[0].path) && + ![...(crons?.keys() ?? [])].some((path) => /reconcil/iu.test(path)), + "the reconciler stays unscheduled until every active release family is supported", + ); + + const sourceWorker = workers.find(({ id }) => id === "source-projector"); + const marketWorker = workers.find(({ id }) => id === "market-projector"); + check( + "ops-source-projector-migrations", + sourceWorker?.dependencies?.length === 1 && + source(sourceWorker.dependencies[0]?.path)?.includes( + "verify_candidate_database_promoted_v2", + ) && + sourceWorker?.migrations?.length === 10 && + migrationContract( + "source-projector-lease", + source(sourceWorker.migrations[0]?.path), + ) && + migrationContract( + "source-projector-provider-evidence", + source(sourceWorker.migrations[1]?.path), + ) && + migrationContract( + "candidate-control-bootstrap", + source(sourceWorker.migrations[2]?.path), + ) && + migrationContract( + "candidate-unpromoted-gate", + source(sourceWorker.migrations[3]?.path), + ) && + migrationContract( + "candidate-promoted-gate", + source(sourceWorker.migrations[4]?.path), + ) && + migrationContract( + "candidate-product-binding", + source(sourceWorker.migrations[5]?.path), + ) && + migrationContract( + "source-projector-safe-head-reuse", + source(sourceWorker.migrations[6]?.path), + ) && + migrationContract( + "source-projector-dynamic-lineage", + source(sourceWorker.migrations[7]?.path), + ) && + migrationContract( + "source-projector-block-evidence-reuse", + source(sourceWorker.migrations[8]?.path), + ) && + migrationContract( + "source-projector-block-evidence-conflict-fence", + source(sourceWorker.migrations[9]?.path), + ), + "the source worker is byte-bound to its runtime selector, database fence and provider evidence", + ); + check( + "ops-market-projector-migration", + marketWorker?.migrations?.length === 1 && + migrationContract( + "market-projector", + source(marketWorker.migrations[0]?.path), + ), + "the market worker is bound to exact lineage, terminal checkpoint and lease SQL", + ); + + const deployWorkflow = source(".github/workflows/deploy-production.yml") ?? ""; + const verifyWorkflow = source(".github/workflows/verify.yml") ?? ""; + const packageJson = parseJson(source("package.json")); + const postPromotion = source("scripts/perf/read-model-post-promotion.mjs") ?? ""; + const productionBinding = source( + "scripts/perf/read-model-production-binding.mjs", + ) ?? ""; + const operationsRunbook = source( + "docs/operations/read-model-scheduler-cutover.md", + ) ?? ""; + check( + "ops-package-verify-binding", + packageJson?.scripts?.verify?.includes("npm run perf:read-model:ops-gate") === true, + "the canonical local verification command runs the operations source contract", + ); + check( + "ops-exact-release-dependency", + deployWorkflow.includes("needs: release-gate") && + deployWorkflow.includes("needs.release-gate.outputs.verified_sha") && + deployWorkflow.includes("pnpm --dir indexer audit --prod --audit-level high") && + deployWorkflow.includes("npm run contracts:verify:ci") && + deployWorkflow.includes("npm run contracts:official-deployments") && + deployWorkflow.includes("npm run contracts:slither") && + deployWorkflow.includes("npm run audit:prod") && + deployWorkflow.indexOf("npm run perf:read-model:ops-gate") < + deployWorkflow.indexOf("vercel build --prod"), + "production staging reproduces the complete exact-commit release gate", + ); + check( + "ops-verify-workflow-binding", + verifyWorkflow.includes("npm run perf:read-model:ops-gate"), + "pull requests and production pushes run the same operations contract", + ); + check( + "ops-post-promotion-binding", + deployWorkflow.includes('id: production-before') && + deployWorkflow.includes('--reject-git-head "$GITHUB_SHA"') && + deployWorkflow.indexOf("id: production-before") < + deployWorkflow.indexOf("id: deploy") && + deployWorkflow.includes( + "DEPLOYMENT_ID: ${{ steps.staged-deployment.outputs.deployment_id }}", + ) && + deployWorkflow.includes('vercel promote "$DEPLOYMENT_ID"') && + deployWorkflow.includes('--deployment-id "$DEPLOYMENT_ID"') && + deployWorkflow.includes("npm run perf:read-model:post-promotion") && + deployWorkflow.includes("vercel rollback") && + deployWorkflow.indexOf("vercel promote") < + deployWorkflow.indexOf("npm run perf:read-model:post-promotion") && + postPromotion.includes("verifyProductionDeploymentBinding") && + productionBinding.includes("resolveProductionBinding") && + postPromotion.includes('"/api/ops/health"') && + postPromotion.includes('"/api/explore?limit=6&page=1&sort=market-cap"') && + postPromotion.includes("verifyLiveCacheAndKeyContracts"), + "promotion, production alias verification and rollback bind to the staged deployment", + ); + check( + "ops-vercel-project-prerequisite", + /Auto-assign Custom Production\s+Domains/u.test(operationsRunbook) && + operationsRunbook.includes("only the reviewed workflow") && + productionBinding.includes("rejectGitHead") && + productionBinding.includes("automatic production-domain assignment"), + "the external Vercel auto-assignment prerequisite is documented and detected fail-closed", + ); + + return { ok: failures.length === 0, checks, failures }; +} + +function main() { + const result = evaluateReadModelOperationsSourceContracts(process.cwd()); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + if (!result.ok) process.exitCode = 1; +} + +if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file:").href) { + main(); +} diff --git a/scripts/perf/read-model-post-promotion.mjs b/scripts/perf/read-model-post-promotion.mjs new file mode 100644 index 00000000..057889cc --- /dev/null +++ b/scripts/perf/read-model-post-promotion.mjs @@ -0,0 +1,277 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { + loadReadModelReleaseEvidence, + parseReadModelLoadProfile, +} from "./read-model-gate-core.mjs"; +import { + deploymentCommit, + fetchVercelDeployment, + verifyLiveCacheAndKeyContracts, +} from "./read-model-live-verifier.mjs"; + +const HEALTH_PATH = "/api/ops/health"; +const EXPLORE_PATH = "/api/explore?limit=6&page=1&sort=market-cap"; +const TOKEN_LIST_PATH = "/api/indexers/v1/token-list"; +const MAXIMUM_RESPONSE_BYTES = 2 * 1024 * 1024; + +function argumentsFrom(argv) { + const result = {}; + for (let index = 0; index < argv.length; index += 2) { + const name = argv[index]; + const value = argv[index + 1]; + if (!name?.startsWith("--") || !value || value.startsWith("--")) { + throw new Error("arguments must be --name value pairs"); + } + result[name.slice(2)] = value; + } + if ( + !result["target-url"] || + !result["deployment-id"] || + !result["git-head"] + ) { + throw new Error("--target-url, --deployment-id and --git-head are required"); + } + return result; +} + +function safeJson(text, subject) { + if (text.length < 2 || Buffer.byteLength(text, "utf8") > MAXIMUM_RESPONSE_BYTES) { + throw new Error(`${subject} returned an invalid response size`); + } + try { + return JSON.parse(text); + } catch { + throw new Error(`${subject} did not return JSON`); + } +} + +async function request(fetchImpl, targetUrl, path, json = true) { + const url = new URL(path, targetUrl); + const response = await fetchImpl(url, { + redirect: "error", + headers: { Accept: json ? "application/json" : "text/html" }, + signal: AbortSignal.timeout(30_000), + }); + const text = await response.text(); + if (Buffer.byteLength(text, "utf8") > MAXIMUM_RESPONSE_BYTES) { + throw new Error(`${url.pathname} returned an oversized response`); + } + return { + ok: response.ok, + status: response.status, + body: json ? safeJson(text, url.pathname) : text, + }; +} + +async function retry(operation, attempts = 12, delayMs = 5_000) { + let lastError; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + const value = await operation(); + if (value.ok) return value; + lastError = new Error(`verification attempt ${attempt} failed`); + } catch (error) { + lastError = error; + } + if (attempt < attempts) { + await new Promise((resolveDelay) => setTimeout(resolveDelay, delayMs)); + } + } + throw lastError ?? new Error("post-promotion verification failed"); +} + +function publicChecks(responses) { + return [ + { + id: "production-root", + condition: + responses.root.ok && + typeof responses.root.body === "string" && + responses.root.body.length > 0, + detail: "the production application serves its root document", + }, + { + id: "production-health", + condition: + responses.health.ok && responses.health.body?.status === "healthy", + detail: "the production operational health route is healthy", + }, + { + id: "production-explore", + condition: + responses.explore.ok && + responses.explore.body?.status === "ready" && + Array.isArray(responses.explore.body?.tokens) && + responses.explore.body.tokens.length > 0, + detail: "the production Explore route returns a populated ready token page", + }, + { + id: "production-token-list", + condition: + responses.tokenList.ok && + Array.isArray(responses.tokenList.body?.tokens) && + responses.tokenList.body.tokens.length > 0, + detail: "the production indexed token list remains populated", + }, + ]; +} + +export async function verifyProductionDeploymentBinding(input) { + const target = new URL(input.targetUrl); + const deployment = await fetchVercelDeployment({ + idOrUrl: target.hostname, + token: input.token, + teamId: input.teamId, + fetchImpl: input.fetchImpl, + }); + const checks = [ + { + id: "production-deployment-id", + condition: deployment.id === input.expectedDeploymentId, + detail: "the production domain resolves to the staged deployment id", + }, + { + id: "production-deployment-project", + condition: + deployment.projectId === input.projectId || + deployment.project?.id === input.projectId, + detail: "the promoted deployment belongs to the configured project", + }, + { + id: "production-deployment-ready", + condition: deployment.readyState === "READY", + detail: "the promoted deployment is READY", + }, + { + id: "production-deployment-commit", + condition: deploymentCommit(deployment) === input.expectedGitHead, + detail: "the production domain resolves to the exact reviewed Git commit", + }, + ]; + return checks.map(({ id, condition, detail }) => ({ + id, + status: condition ? "pass" : "fail", + detail, + })); +} + +export async function verifyPostPromotion(input) { + const target = new URL(input.targetUrl); + if ( + target.protocol !== "https:" || + target.username !== "" || + target.password !== "" || + target.pathname !== "/" || + target.search !== "" || + target.hash !== "" + ) { + throw new Error("post-promotion target must be an HTTPS origin"); + } + if ( + !/^dpl_[A-Za-z0-9]{20,80}$/u.test(input.expectedDeploymentId ?? "") || + !/^[0-9a-f]{40}$/u.test(input.expectedGitHead ?? "") || + !input.token || + !input.teamId || + !input.projectId + ) { + throw new Error("exact production deployment binding is required"); + } + const targetUrl = target.toString(); + const fetchImpl = input.fetchImpl ?? fetch; + const [deploymentChecks, ...responses] = await Promise.all([ + verifyProductionDeploymentBinding({ + targetUrl, + expectedDeploymentId: input.expectedDeploymentId, + expectedGitHead: input.expectedGitHead, + token: input.token, + teamId: input.teamId, + projectId: input.projectId, + fetchImpl, + }), + request(fetchImpl, targetUrl, "/", false), + request(fetchImpl, targetUrl, HEALTH_PATH), + request(fetchImpl, targetUrl, EXPLORE_PATH), + request(fetchImpl, targetUrl, TOKEN_LIST_PATH), + ]); + const checks = [...deploymentChecks, ...publicChecks({ + root: responses[0], + health: responses[1], + explore: responses[2], + tokenList: responses[3], + })]; + + if (input.evidencePath) { + const profile = parseReadModelLoadProfile( + JSON.parse( + readFileSync( + resolve(input.rootDirectory, "config/read-model-release-profile.v1.json"), + "utf8", + ), + ), + ); + const bundle = loadReadModelReleaseEvidence({ + profile, + evidencePath: resolve(input.rootDirectory, input.evidencePath), + }); + const indexed = await verifyLiveCacheAndKeyContracts({ + profile, + evidence: { + ...bundle.evidence, + target: { ...bundle.evidence.target, url: targetUrl }, + }, + datasetManifest: bundle.datasetManifest, + fetchImpl, + }); + checks.push(...indexed.checks.map((check) => ({ + ...check, + id: `production-${check.id}`, + condition: check.status === "pass", + }))); + } + + const normalizedChecks = checks.map(({ id, condition, status, detail }) => ({ + id, + status: status ?? (condition ? "pass" : "fail"), + detail, + })); + const failures = normalizedChecks + .filter(({ status }) => status !== "pass") + .map(({ id, detail }) => ({ id, detail })); + return { + ok: failures.length === 0, + targetUrl, + checks: normalizedChecks, + failures, + }; +} + +async function main() { + const args = argumentsFrom(process.argv.slice(2)); + const result = await retry(() => + verifyPostPromotion({ + rootDirectory: process.cwd(), + targetUrl: args["target-url"], + evidencePath: args.evidence, + expectedDeploymentId: args["deployment-id"], + expectedGitHead: args["git-head"], + token: process.env.VERCEL_TOKEN, + teamId: process.env.VERCEL_ORG_ID, + projectId: process.env.VERCEL_PROJECT_ID, + }), + ); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + if (!result.ok) process.exitCode = 1; +} + +if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file:").href) { + main().catch((error) => { + process.stderr.write( + `${error instanceof Error ? error.message : "post-promotion verification failed"}\n`, + ); + process.exitCode = 1; + }); +} diff --git a/scripts/perf/read-model-production-binding.mjs b/scripts/perf/read-model-production-binding.mjs new file mode 100644 index 00000000..cd3d109a --- /dev/null +++ b/scripts/perf/read-model-production-binding.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +import { appendFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { + deploymentCommit, + fetchVercelDeployment, +} from "./read-model-live-verifier.mjs"; + +function argumentsFrom(argv) { + const result = {}; + for (let index = 0; index < argv.length; index += 2) { + const name = argv[index]; + const value = argv[index + 1]; + if (!name?.startsWith("--") || !value || value.startsWith("--")) { + throw new Error("arguments must be --name value pairs"); + } + result[name.slice(2)] = value; + } + if (!result["target-url"] || !result["github-output"]) { + throw new Error("--target-url and --github-output are required"); + } + return result; +} + +function exactHttpsOrigin(value) { + const target = new URL(value); + if ( + target.protocol !== "https:" || + target.username || + target.password || + target.pathname !== "/" || + target.search || + target.hash + ) { + throw new Error("production target must be an exact HTTPS origin"); + } + return target; +} + +export async function resolveProductionBinding(input) { + const target = exactHttpsOrigin(input.targetUrl); + const deployment = await fetchVercelDeployment({ + idOrUrl: target.hostname, + token: input.token, + teamId: input.teamId, + fetchImpl: input.fetchImpl, + }); + const gitHead = deploymentCommit(deployment); + const deploymentHost = String(deployment.url ?? "") + .replace(/^https?:\/\//u, "") + .replace(/\/$/u, ""); + if ( + !/^dpl_[A-Za-z0-9]{20,80}$/u.test(deployment.id ?? "") || + !deploymentHost.endsWith(".vercel.app") || + deployment.readyState !== "READY" || + (deployment.projectId !== input.projectId && + deployment.project?.id !== input.projectId) || + !/^[0-9a-f]{40}$/u.test(gitHead ?? "") + ) { + throw new Error("production domain is not bound to a READY project deployment"); + } + if (input.expectedDeploymentId && deployment.id !== input.expectedDeploymentId) { + throw new Error("production domain is not bound to the expected deployment"); + } + if (input.expectedGitHead && gitHead !== input.expectedGitHead) { + throw new Error("production domain is not bound to the expected Git commit"); + } + if (input.rejectGitHead && gitHead === input.rejectGitHead) { + throw new Error( + "production already points at the candidate commit; disable automatic production-domain assignment", + ); + } + return Object.freeze({ + deploymentId: deployment.id, + deploymentUrl: `https://${deploymentHost}`, + gitHead, + targetUrl: target.toString(), + }); +} + +async function main() { + const args = argumentsFrom(process.argv.slice(2)); + const result = await resolveProductionBinding({ + targetUrl: args["target-url"], + expectedDeploymentId: args["expected-deployment-id"], + expectedGitHead: args["expected-git-head"], + rejectGitHead: args["reject-git-head"], + token: process.env.VERCEL_TOKEN, + teamId: process.env.VERCEL_ORG_ID, + projectId: process.env.VERCEL_PROJECT_ID, + }); + appendFileSync( + resolve(args["github-output"]), + [ + `deployment_id=${result.deploymentId}`, + `deployment_url=${result.deploymentUrl}`, + `git_head=${result.gitHead}`, + "", + ].join("\n"), + { encoding: "utf8", mode: 0o600 }, + ); + process.stdout.write(`${JSON.stringify({ status: "verified", ...result })}\n`); +} + +if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file:").href) { + main().catch((error) => { + process.stderr.write( + `${error instanceof Error ? error.message : "production binding failed"}\n`, + ); + process.exitCode = 1; + }); +} diff --git a/scripts/perf/read-model-provider-binding.mjs b/scripts/perf/read-model-provider-binding.mjs new file mode 100644 index 00000000..39392ada --- /dev/null +++ b/scripts/perf/read-model-provider-binding.mjs @@ -0,0 +1,151 @@ +import { keccak256, toBytes } from "viem"; + +const ALCHEMY_HOST = "eth-mainnet.g.alchemy.com"; +const ALCHEMY_API_PATH = /^\/v2\/[A-Za-z0-9_-]{8,256}$/u; +const QUICKNODE_API_PATH = /^\/[A-Za-z0-9_-]{8,256}\/?$/u; +const QUICKNODE_HOST = + /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+quiknode\.pro$/u; +const DOMAINS = Object.freeze({ + endpoint: "programmable:data-pipeline:rpc-endpoint:v1\0", + origin: "programmable:data-pipeline:rpc-origin:v1\0", +}); +const HEX_BYTES32 = /^0x[0-9a-f]{64}$/u; +const PINNED_COMMITMENT_NAMES = Object.freeze({ + alchemy: "PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT", + quicknode: "PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT", +}); + +function endpoint(value, provider) { + if (typeof value !== "string" || value.length < 1 || value.length > 1_024) { + throw new Error(`${provider} RPC URL is required`); + } + const parsed = new URL(value); + if ( + parsed.protocol !== "https:" || + parsed.username !== "" || + parsed.password !== "" || + parsed.port !== "" || + parsed.search !== "" || + parsed.hash !== "" || + (provider === "alchemy" && + (parsed.hostname !== ALCHEMY_HOST || + !ALCHEMY_API_PATH.test(parsed.pathname))) || + (provider === "quicknode" && + (!QUICKNODE_HOST.test(parsed.hostname) || + !QUICKNODE_API_PATH.test(parsed.pathname))) || + (provider === "alchemy" && parsed.pathname.slice("/v2/".length) === "docs-demo") || + (provider === "quicknode" && + parsed.pathname.replace(/^\//u, "").replace(/\/$/u, "") === "docs-demo") + ) { + throw new Error(`${provider} RPC URL is not an approved Mainnet endpoint`); + } + return parsed; +} + +function classifyEndpoint(value) { + for (const provider of ["alchemy", "quicknode"]) { + try { + return { provider, url: endpoint(value, provider) }; + } catch { + // Try the other approved vendor. Error details never contain the URL. + } + } + throw new Error("legacy RPC URL is not an approved Mainnet endpoint"); +} + +function optionalEndpoint(value, provider) { + return value === undefined || value === "" + ? undefined + : endpoint(value, provider); +} + +function selectedEndpoints(environment) { + const selected = { + alchemy: optionalEndpoint( + environment.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL, + "alchemy", + ), + quicknode: optionalEndpoint( + environment.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL, + "quicknode", + ), + }; + const fallback = { alchemy: new Map(), quicknode: new Map() }; + for (const value of [ + environment.ETHEREUM_RPC_URL, + environment.ETHEREUM_RPC_URL_B, + ]) { + if (value === undefined || value === "") continue; + const classified = classifyEndpoint(value); + fallback[classified.provider].set( + classified.url.toString(), + classified.url, + ); + } + for (const provider of ["alchemy", "quicknode"]) { + if (selected[provider]) continue; + const candidates = [...fallback[provider].values()]; + if (candidates.length !== 1) { + throw new Error(`one ${provider} Mainnet RPC endpoint is required`); + } + selected[provider] = candidates[0]; + } + return selected; +} + +function commitment(kind, value) { + return keccak256(toBytes(`${DOMAINS[kind]}${value}`)); +} + +function bindingsFromCommitments(commitments) { + if (commitments.alchemy === commitments.quicknode) { + throw new Error("Alchemy and QuickNode commitments must differ"); + } + return ["alchemy", "quicknode"].map((vendorGroup) => { + const endpointCommitment = commitments[vendorGroup]; + return Object.freeze({ + vendorGroup, + identity: `${vendorGroup}-mainnet-${endpointCommitment.slice(2, 34)}`, + endpointCommitment, + }); + }); +} + +export function runtimeProductionProviderBindingsFromUrls(environment) { + const endpoints = selectedEndpoints(environment); + if (endpoints.alchemy.origin === endpoints.quicknode.origin) { + throw new Error("Alchemy and QuickNode must be independent endpoints"); + } + return bindingsFromCommitments( + Object.fromEntries( + Object.entries(endpoints).map(([provider, url]) => [ + provider, + commitment("endpoint", url.toString()), + ]), + ), + ); +} + +export function expectedProductionProviderBindings(environment = process.env) { + const pinned = Object.fromEntries( + Object.entries(PINNED_COMMITMENT_NAMES).map(([provider, name]) => [ + provider, + environment[name], + ]), + ); + const pinnedCount = Object.values(pinned).filter( + (value) => value !== undefined && value !== "", + ).length; + if (pinnedCount !== 0 && pinnedCount !== 2) { + throw new Error("both pinned provider commitments are required"); + } + if (pinnedCount === 2) { + for (const [provider, value] of Object.entries(pinned)) { + if (!HEX_BYTES32.test(value)) { + throw new Error(`${provider} provider commitment is invalid`); + } + } + return bindingsFromCommitments(pinned); + } + return runtimeProductionProviderBindingsFromUrls(environment); +} diff --git a/scripts/perf/read-model-release-probe.mjs b/scripts/perf/read-model-release-probe.mjs new file mode 100644 index 00000000..4f8b1b06 --- /dev/null +++ b/scripts/perf/read-model-release-probe.mjs @@ -0,0 +1,65 @@ +import { createHmac } from "node:crypto"; + +const LOGICAL_TO_INDEXED_ROUTE = Object.freeze({ + exploreList: "explore-list", + tokenDetail: "explore-token", + tokenChart: "explore-chart", + creatorProfile: "creator-profile", + classicProfile: "classic-v3-profile", + stockProfile: "creator-profile", + classicLaunchLookup: "launch-lookup", + stockLaunchLookup: "launch-lookup", +}); + +const RELEASE_PROBE_SIGNATURE_VERSION = "programmable-release-probe-v1"; +const RELEASE_PROBE_NONCE = + /^(?[1-9]\d{12})-(?[0-9a-f]{64})-(?0|[1-9]\d{0,9})$/u; +const RELEASE_PROBE_SECRET = /^[A-Za-z0-9._~+/=-]{32,512}$/u; + +export function indexedRouteForPerformanceRoute(route) { + const indexedRoute = LOGICAL_TO_INDEXED_ROUTE[route]; + if (!indexedRoute) { + throw new Error(`route ${String(route)} has no indexed release-probe binding`); + } + return indexedRoute; +} + +export function signReadModelReleaseProbe(input) { + const route = indexedRouteForPerformanceRoute(input.route); + if (!RELEASE_PROBE_NONCE.test(input.nonce)) { + throw new Error("release probe nonce is invalid"); + } + if ( + typeof input.secret !== "string" || + !RELEASE_PROBE_SECRET.test(input.secret) + ) { + throw new Error("release probe secret is invalid"); + } + return createHmac("sha256", input.secret) + .update(`${RELEASE_PROBE_SIGNATURE_VERSION}\n${route}\n${input.nonce}`, "utf8") + .digest("hex"); +} + +export function buildReadModelReleaseProbe(input) { + if ( + !Number.isSafeInteger(input.issuedAtMs) || + input.issuedAtMs < 1_000_000_000_000 || + input.issuedAtMs > 9_999_999_999_999 || + !Number.isSafeInteger(input.sequence) || + input.sequence < 0 || + input.sequence > 9_999_999_999 || + typeof input.captureNonce !== "string" || + !/^0x[0-9a-f]{64}$/u.test(input.captureNonce) + ) { + throw new Error("release probe identity is invalid"); + } + const nonce = `${input.issuedAtMs}-${input.captureNonce.slice(2)}-${input.sequence}`; + return Object.freeze({ + nonce, + signature: signReadModelReleaseProbe({ + route: input.route, + nonce, + secret: input.secret, + }), + }); +} diff --git a/scripts/perf/read-model-smoke.mjs b/scripts/perf/read-model-smoke.mjs new file mode 100644 index 00000000..ae32b0b5 --- /dev/null +++ b/scripts/perf/read-model-smoke.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { parseReadModelLoadProfile } from "./read-model-gate-core.mjs"; +import { evaluateReadModelSourceContracts } from "./read-model-source-contracts.mjs"; + +function output(value, exitCode) { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); + process.exitCode = exitCode; +} + +try { + const rootDirectory = process.cwd(); + const profile = parseReadModelLoadProfile( + JSON.parse( + readFileSync( + resolve(rootDirectory, "config/read-model-load-profile.v1.json"), + "utf8", + ), + ), + ); + const source = evaluateReadModelSourceContracts(rootDirectory, profile); + output( + { + schemaVersion: 1, + profileId: profile.profileId, + mode: "contract-smoke", + contractValid: source.ok, + releaseEvidenceAccepted: false, + checks: source.checks, + failures: source.failures, + }, + source.ok ? 0 : 1, + ); +} catch (error) { + output( + { + schemaVersion: 1, + mode: "contract-smoke", + contractValid: false, + releaseEvidenceAccepted: false, + checks: [], + failures: [ + { + id: "smoke-input", + detail: error instanceof Error ? error.message : "invalid input", + }, + ], + }, + 1, + ); +} diff --git a/scripts/perf/read-model-source-contracts.mjs b/scripts/perf/read-model-source-contracts.mjs new file mode 100644 index 00000000..3dc32dbb --- /dev/null +++ b/scripts/perf/read-model-source-contracts.mjs @@ -0,0 +1,720 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import typescript from "typescript"; + +const ts = typescript; + +function readSource(rootDirectory, path, sourceOverrides) { + if (Object.hasOwn(sourceOverrides, path)) return sourceOverrides[path]; + return readFileSync(resolve(rootDirectory, path), "utf8"); +} + +function parseTypeScript(path, source) { + return ts.createSourceFile( + path, + source, + ts.ScriptTarget.Latest, + true, + path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); +} + +function visitTree(node, visitor) { + if (!node) return true; + if (visitor(node) === false) return false; + let keepWalking = true; + node.forEachChild((child) => { + if (keepWalking && visitTree(child, visitor) === false) { + keepWalking = false; + } + }); + return keepWalking; +} + +function findNode(node, predicate) { + let match; + visitTree(node, (candidate) => { + if (!predicate(candidate)) return true; + match = candidate; + return false; + }); + return match; +} + +function propertyName(node) { + if (!node) return undefined; + if (ts.isIdentifier(node) || ts.isStringLiteral(node)) return node.text; + return undefined; +} + +function findVariableInitializer(sourceFile, name) { + const declaration = findNode( + sourceFile, + (node) => + ts.isVariableDeclaration(node) && + ts.isIdentifier(node.name) && + node.name.text === name, + ); + return declaration?.initializer; +} + +function staticNumber(sourceFile, expression, seen = new Set()) { + if (!expression) return undefined; + if (ts.isParenthesizedExpression(expression)) { + return staticNumber(sourceFile, expression.expression, seen); + } + if (ts.isNumericLiteral(expression)) return Number(expression.text); + if (ts.isPrefixUnaryExpression(expression)) { + const operand = staticNumber(sourceFile, expression.operand, seen); + if (operand === undefined) return undefined; + if (expression.operator === ts.SyntaxKind.MinusToken) return -operand; + if (expression.operator === ts.SyntaxKind.PlusToken) return operand; + return undefined; + } + if (ts.isIdentifier(expression)) { + if (seen.has(expression.text)) return undefined; + const nextSeen = new Set(seen).add(expression.text); + return staticNumber( + sourceFile, + findVariableInitializer(sourceFile, expression.text), + nextSeen, + ); + } + if (!ts.isBinaryExpression(expression)) return undefined; + const left = staticNumber(sourceFile, expression.left, seen); + const right = staticNumber(sourceFile, expression.right, seen); + if (left === undefined || right === undefined) return undefined; + switch (expression.operatorToken.kind) { + case ts.SyntaxKind.PlusToken: + return left + right; + case ts.SyntaxKind.MinusToken: + return left - right; + case ts.SyntaxKind.AsteriskToken: + return left * right; + case ts.SyntaxKind.SlashToken: + return right === 0 ? undefined : left / right; + default: + return undefined; + } +} + +function constantNumber(sourceFile, name) { + return staticNumber(sourceFile, findVariableInitializer(sourceFile, name)); +} + +function unwrapExpression(expression) { + let current = expression; + while ( + current && + (ts.isParenthesizedExpression(current) || + ts.isAsExpression(current) || + ts.isTypeAssertionExpression(current) || + ts.isNonNullExpression(current) || + ts.isSatisfiesExpression(current)) + ) { + current = current.expression; + } + return current; +} + +function expressionPath(expression) { + const current = unwrapExpression(expression); + if (!current) return undefined; + if (ts.isIdentifier(current)) return current.text; + if (ts.isPropertyAccessExpression(current)) { + const owner = expressionPath(current.expression); + return owner ? `${owner}.${current.name.text}` : undefined; + } + if ( + ts.isElementAccessExpression(current) && + ts.isStringLiteral(current.argumentExpression) + ) { + const owner = expressionPath(current.expression); + return owner ? `${owner}.${current.argumentExpression.text}` : undefined; + } + return undefined; +} + +function findFunctionLike(sourceFile, name) { + const declaration = findNode(sourceFile, (node) => { + if (ts.isFunctionDeclaration(node)) return node.name?.text === name; + return ( + ts.isVariableDeclaration(node) && + ts.isIdentifier(node.name) && + node.name.text === name && + Boolean(node.initializer) && + (ts.isArrowFunction(node.initializer) || + ts.isFunctionExpression(node.initializer)) + ); + }); + if (!declaration) return undefined; + return ts.isVariableDeclaration(declaration) + ? declaration.initializer + : declaration; +} + +function containsIdentifier(node, name) { + return Boolean( + findNode(node, (candidate) => + ts.isIdentifier(candidate) && candidate.text === name, + ), + ); +} + +function identifierCount(node, name) { + let count = 0; + visitTree(node, (candidate) => { + if (ts.isIdentifier(candidate) && candidate.text === name) count += 1; + return true; + }); + return count; +} + +function containsExpressionPath(node, path) { + return Boolean( + findNode( + node, + (candidate) => expressionPath(candidate) === path, + ), + ); +} + +function containsCall(node, path) { + return Boolean( + findNode( + node, + (candidate) => + ts.isCallExpression(candidate) && + expressionPath(candidate.expression) === path, + ), + ); +} + +function containsAwaitedCall(node, path) { + return Boolean( + findNode(node, (candidate) => { + if (!ts.isAwaitExpression(candidate)) return false; + const expression = unwrapExpression(candidate.expression); + return ( + ts.isCallExpression(expression) && + expressionPath(expression.expression) === path + ); + }), + ); +} + +function containsComparison(node, leftPath, operator, rightPath) { + return Boolean( + findNode(node, (candidate) => { + if (!ts.isBinaryExpression(candidate)) return false; + return ( + expressionPath(candidate.left) === leftPath && + candidate.operatorToken.kind === operator && + expressionPath(candidate.right) === rightPath + ); + }), + ); +} + +function objectLiteral(expression) { + const current = unwrapExpression(expression); + if (!current) return undefined; + if (ts.isObjectLiteralExpression(current)) return current; + if ( + ts.isCallExpression(current) && + expressionPath(current.expression) === "Object.freeze" + ) { + return objectLiteral(current.arguments[0]); + } + return undefined; +} + +function objectPropertyNames(object) { + if (!object) return new Set(); + return new Set( + object.properties + .map((property) => propertyName(property.name)) + .filter(Boolean), + ); +} + +function hasObjectPropertyMapping(node, property, valuePath) { + return Boolean( + findNode(node, (candidate) => { + if (!ts.isObjectLiteralExpression(candidate)) return false; + return candidate.properties.some((member) => { + if ( + !ts.isPropertyAssignment(member) && + !ts.isShorthandPropertyAssignment(member) + ) { + return false; + } + if (propertyName(member.name) !== property) return false; + return ts.isShorthandPropertyAssignment(member) + ? member.name.text === valuePath + : expressionPath(member.initializer) === valuePath; + }); + }), + ); +} + +function typeLiteralPropertyNames(sourceFile, typeName) { + const declaration = findNode( + sourceFile, + (node) => ts.isTypeAliasDeclaration(node) && node.name.text === typeName, + ); + if (!declaration) return new Set(); + let type = declaration.type; + if ( + ts.isTypeReferenceNode(type) && + expressionPath(type.typeName) === "Readonly" && + type.typeArguments?.length === 1 + ) { + type = type.typeArguments[0]; + } + if (!ts.isTypeLiteralNode(type)) return new Set(); + return new Set(type.members.map((member) => propertyName(member.name)).filter(Boolean)); +} + +function containsConditionalDelete(functionNode, parameterName, guardName) { + return Boolean( + findNode(functionNode, (node) => { + if (!ts.isIfStatement(node) || !containsIdentifier(node.expression, guardName)) { + return false; + } + return Boolean( + findNode( + node.thenStatement, + (candidate) => + ts.isCallExpression(candidate) && + expressionPath(candidate.expression) === "canonical.delete" && + candidate.arguments.length === 1 && + expressionPath(candidate.arguments[0]) === parameterName, + ), + ); + }), + ); +} + +export function evaluateReadModelSourceContracts( + rootDirectory, + profile, + options = {}, +) { + const sourceOverrides = options.sourceOverrides ?? {}; + const checks = []; + const failures = []; + const check = (id, condition, detail) => { + const status = condition ? "pass" : "fail"; + checks.push({ id, status, detail }); + if (!condition) failures.push({ id, detail }); + }; + + const source = (path) => readSource(rootDirectory, path, sourceOverrides); + const dualRpc = source("lib/data-pipeline/dual-rpc.ts"); + const dualRpcAst = parseTypeScript("dual-rpc.ts", dualRpc); + const rpcPolicyFunction = findFunctionLike(dualRpcAst, "rpcExecutionPolicy"); + const retryTracedRpcFunction = findFunctionLike(dualRpcAst, "retryTracedRpc"); + const withinRpcDeadlineFunction = findFunctionLike( + dualRpcAst, + "withinRpcDeadline", + ); + const performanceCapture = source( + "lib/data-pipeline/read-model-performance-capture.server.ts", + ); + const performanceCaptureAst = parseTypeScript( + "read-model-performance-capture.server.ts", + performanceCapture, + ); + const releaseProfile = profile.profileId === "read-model-release-v1"; + const candidateConstant = releaseProfile + ? "RELEASE_REQUIRED_CANDIDATE_COUNT" + : "SMOKE_REQUIRED_CANDIDATE_COUNT"; + const callBudgetConstant = releaseProfile + ? "RELEASE_MAX_CALLS_PER_PROVIDER" + : "SMOKE_MAX_CALLS_PER_PROVIDER"; + check( + "source-rpc-concurrency", + constantNumber(dualRpcAst, "DEFAULT_RPC_CONCURRENCY") === + profile.projector.rpc.maxConcurrencyPerProvider && + containsIdentifier(rpcPolicyFunction, "DEFAULT_RPC_CONCURRENCY") && + containsIdentifier(dualRpcAst, "maxConcurrency") && + containsExpressionPath(dualRpcAst, "policy.maxConcurrency"), + "dual-RPC concurrency matches the load profile", + ); + check( + "source-rpc-attempts", + constantNumber(dualRpcAst, "DEFAULT_RPC_ATTEMPTS") === + profile.projector.rpc.maxAttemptsPerCall && + containsIdentifier(rpcPolicyFunction, "DEFAULT_RPC_ATTEMPTS") && + containsExpressionPath(retryTracedRpcFunction, "policy.maxAttempts"), + "dual-RPC retry attempts match the load profile", + ); + check( + "source-rpc-candidate-cap", + constantNumber(performanceCaptureAst, candidateConstant) === + profile.projector.maximumCandidateBatchSize && + identifierCount(performanceCaptureAst, candidateConstant) > 1, + "the measured candidate batch matches the load profile", + ); + check( + "source-rpc-hard-deadline", + constantNumber(dualRpcAst, "DEFAULT_RPC_DEADLINE_MS") === + profile.projector.hardDeadlineMs && + constantNumber(performanceCaptureAst, "HARD_DEADLINE_MS") === + profile.projector.hardDeadlineMs && + containsIdentifier(rpcPolicyFunction, "DEFAULT_RPC_DEADLINE_MS") && + identifierCount(performanceCaptureAst, "HARD_DEADLINE_MS") > 1 && + containsCall(withinRpcDeadlineFunction, "Promise.race") && + containsCall(withinRpcDeadlineFunction, "setTimeout") && + containsIdentifier(withinRpcDeadlineFunction, "deadlineAt"), + "dual-RPC runtime enforces the hard deadline", + ); + check( + "source-rpc-global-call-budget", + constantNumber(performanceCaptureAst, callBudgetConstant) === + profile.projector.rpc.maxCallsPerProviderPerRun && + constantNumber(dualRpcAst, "DEFAULT_MAXIMUM_PROVIDER_CALLS") >= + profile.projector.rpc.smokeFirstAttemptCallsPerProvider && + containsIdentifier( + rpcPolicyFunction, + "DEFAULT_MAXIMUM_PROVIDER_CALLS", + ) && + containsComparison( + retryTracedRpcFunction, + "context.callCount", + ts.SyntaxKind.GreaterThanEqualsToken, + "policy.maxCallsPerProvider", + ) && + identifierCount(performanceCaptureAst, callBudgetConstant) > 1, + "the runtime and measured trace enforce a per-provider call budget", + ); + const executionTraceProperty = findNode( + dualRpcAst, + (node) => + ts.isPropertyAssignment(node) && + propertyName(node.name) === "executionTrace" && + Boolean(objectLiteral(node.initializer)), + ); + const executionTraceProperties = objectPropertyNames( + executionTraceProperty + ? objectLiteral(executionTraceProperty.initializer) + : undefined, + ); + const traceTypeProperties = typeLiteralPropertyNames( + dualRpcAst, + "DualRpcCallTrace", + ); + check( + "source-rpc-raw-trace", + [ + "startedAtMs", + "completedAtMs", + "candidateBatchSize", + "hardDeadlineMs", + "maxCallsPerProvider", + "elapsedMs", + "providerCallCounts", + "calls", + ].every((name) => executionTraceProperties.has(name)) && + [ + "providerIdentity", + "providerVendorGroup", + "providerEndpointCommitment", + "providerOriginCommitment", + "operation", + "attempt", + "startedOffsetMs", + "durationMs", + "outcome", + ].every((name) => traceTypeProperties.has(name)) && + hasObjectPropertyMapping( + retryTracedRpcFunction, + "providerEndpointCommitment", + "context.providerEndpointCommitment", + ) && + hasObjectPropertyMapping( + retryTracedRpcFunction, + "providerOriginCommitment", + "context.providerOriginCommitment", + ) && + containsExpressionPath(performanceCaptureAst, "result.executionTrace"), + "dual-RPC output includes raw commitment-bound call traces", + ); + + const rpcProviders = source("lib/data-pipeline/rpc-providers.server.ts"); + check( + "source-rpc-timeout", + rpcProviders.includes( + `timeout: ${profile.projector.rpc.perCallTimeoutMs.toLocaleString("en-US").replace(",", "_")}`, + ), + "RPC timeout matches the load profile", + ); + + const projectorRoute = source("app/api/ops/projector/route.ts"); + check( + "source-hosting-deadline", + projectorRoute.includes( + `export const maxDuration = ${profile.projector.hostingDeadlineMs / 1_000};`, + ), + "hosting deadline leaves the required projector reserve", + ); + + const dataPipelineConfig = source("lib/data-pipeline/config.ts"); + check( + "source-dependency-timeouts", + dataPipelineConfig.includes("timeoutMs: 2_000;") && + dataPipelineConfig.includes("statementTimeoutMs: 1_000;"), + "Envio and Postgres calls retain bounded timeouts", + ); + + const publicCacheSources = [ + ["exploreList", "app/api/explore/route.ts"], + ["tokenDetail", "app/api/explore/token/route.ts"], + ["tokenChart", "app/api/explore/token/chart/route.ts"], + ["creatorProfile", "app/api/explore/profile/route.ts"], + ["classicProfile", "app/api/profile/classic-v3/route.ts"], + ["stockProfile", "app/api/profile/stock-paired/route.ts"], + ["classicLaunchLookup", "app/api/profile/classic-v3/route.ts"], + [ + "stockLaunchLookup", + "app/api/explore/launch/stock-paired/route.ts", + ], + ["tokenList", "app/api/indexers/v1/token-list/route.ts"], + ["health", "app/api/ops/health/route.ts"], + ]; + for (const [contractName, path] of publicCacheSources) { + const routeSource = source(path); + check( + `source-cache-${contractName}`, + routeSource.includes(profile.cacheContracts[contractName]), + `${contractName} cache policy matches the load profile`, + ); + } + + const publicIndexerRoute = source("app/api/indexers/v1/tokens/route.ts"); + const publicIndexerResponse = source("app/api/indexers/v1/response.ts"); + check( + "source-cache-publicIndexer", + publicIndexerRoute.includes("indexedFeedHeaders(snapshot)") && + publicIndexerResponse.includes( + `export const INDEXER_READY_CACHE_CONTROL =\n "${profile.cacheContracts.publicIndexer}";`, + ) && + publicIndexerResponse.includes( + "cacheControl = INDEXER_READY_CACHE_CONTROL", + ), + "publicIndexer cache policy matches the response helper used by the route", + ); + const deployPolicy = source("scripts/perf/read-model-deploy-policy.mjs"); + check( + "source-public-indexer-activation-gate", + deployPolicy.includes('"INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED"') && + deployPolicy.includes("RELEASE_GATED_FLAG_NAMES") && + deployPolicy.includes("evidenceRequired"), + "public indexer feed activation remains behind signed release evidence", + ); + + const readModelMigration = source( + "supabase/migrations/20260731175501_atomic_empty_envio_coverage_pages.sql", + ); + check( + "source-reorg-exact-current", + /create view programmable_private\.route_eligibility_current_exact_v1/iu.test( + readModelMigration, + ) && + /current_checkpoint\.checkpoint_generation\s*=\s*checkpoint\.checkpoint_generation/iu.test( + readModelMigration, + ) && + /current_checkpoint\.reorg_generation\s*=\s*checkpoint\.reorg_generation/iu.test( + readModelMigration, + ) && + /current_epoch\.generation\s*=\s*route\.pointer_generation/iu.test( + readModelMigration, + ), + "indexed routes reject stale checkpoint, epoch and reorg generations", + ); + + const accountMutation = source("app/api/explore/profile/claim/route.ts"); + check( + "source-cache-account-mutation", + accountMutation.includes('"Cache-Control": "no-store"'), + "account mutations are not cached", + ); + const transactionPreparation = source("app/api/trade/prepare/route.ts"); + check( + "source-cache-transaction-preparation", + transactionPreparation.includes('"Cache-Control": "no-store"'), + "transaction preparation is not cached", + ); + + const capture = source("scripts/perf/read-model-capture.mjs"); + const captureRoute = source( + "app/api/ops/read-model-performance-capture/route.ts", + ); + check( + "source-release-capture-auth", + capture.includes('"x-programmable-release-capture-signature"') && + capture.includes("const releaseSignature = createHmac(") && + capture.includes('.update(requestBody, "utf8")') && + captureRoute.includes("RELEASE_RATE_LIMIT_MS = 30_000") && + captureRoute.includes("RELEASE_REPLAY_TTL_MS = 60_000") && + captureRoute.includes('createHmac("sha256", secret)') && + captureRoute.includes("timingSafeEqual(expected, provided)"), + "the 32-candidate release capture is HMAC-bound, replay-limited and rate-limited", + ); + check( + "source-release-probe-transport", + capture.includes('headers["x-programmable-shadow-probe-signature"]') && + capture.includes('headers["x-programmable-shadow-probe"] = "1"') && + !capture.includes("x-programmable-shadow-probe-token"), + "release probes send a signed capability and never transmit the secret", + ); + check( + "source-real-corpus-selection", + capture.includes( + "capturedRuntime.datasetManifest.keys.tokenAddresses", + ) && + capture.includes( + "capturedRuntime.datasetManifest.keys.accountAddresses", + ) && + capture.includes("capturedRuntime.datasetManifest.keys.classicLaunches") && + capture.includes("capturedRuntime.datasetManifest.keys.stockLaunches") && + capture.includes('"accessEvidence"') && + !capture.includes("eligibleLaunches.map("), + "the throughput run repeats attested deterministic samples instead of padding to cardinality", + ); + const releaseProbe = source("scripts/perf/read-model-release-probe.mjs"); + check( + "source-release-probe-payload", + releaseProbe.includes( + 'const RELEASE_PROBE_SIGNATURE_VERSION = "programmable-release-probe-v1";', + ) && + releaseProbe.includes("`${RELEASE_PROBE_SIGNATURE_VERSION}\\n${route}\\n${input.nonce}`") && + releaseProbe.includes('tokenDetail: "explore-token"') && + releaseProbe.includes('classicLaunchLookup: "launch-lookup"'), + "release-probe HMACs are versioned and bound to the exact indexed route and nonce", + ); + + const routeCoordinator = source( + "lib/data-pipeline/route-coordinator.server.ts", + ); + const routeCoordinatorAst = parseTypeScript( + "route-coordinator.server.ts", + routeCoordinator, + ); + const authorizeReleaseProbe = findFunctionLike( + routeCoordinatorAst, + "authorizeRouteReleaseProbe", + ); + check( + "source-release-probe-freshness", + constantNumber(routeCoordinatorAst, "RELEASE_PROBE_MAX_AGE_MS") === + 5 * 60 * 1_000 && + constantNumber( + routeCoordinatorAst, + "RELEASE_PROBE_MAX_FUTURE_SKEW_MS", + ) === 30 * 1_000 && + containsIdentifier(authorizeReleaseProbe, "RELEASE_PROBE_MAX_AGE_MS") && + containsIdentifier( + authorizeReleaseProbe, + "RELEASE_PROBE_MAX_FUTURE_SKEW_MS", + ) && + containsAwaitedCall(authorizeReleaseProbe, "consumeReleaseProbeNonce"), + "release probes have a five-minute TTL, 30-second future skew, and await the distributed consume", + ); + + const nonceConsumer = source( + "lib/data-pipeline/release-probe-nonce.server.ts", + ); + const nonceConsumerAst = parseTypeScript( + "release-probe-nonce.server.ts", + nonceConsumer, + ); + const nonceMigration = source( + "supabase/migrations/20260731202904_release_probe_nonce_consumption.sql", + ); + check( + "source-release-probe-distributed-replay", + findVariableInitializer(nonceConsumerAst, "RELEASE_PROBE_ROLE")?.text === + "programmable_release_probe_nonce" && + findVariableInitializer(nonceConsumerAst, "RELEASE_PROBE_LOGIN")?.text === + "programmable_release_probe_nonce_login" && + containsIdentifier(nonceConsumerAst, "releaseProbeConnectionString") && + /maxConnections\s*:\s*1\b/u.test(nonceConsumer) && + /primary\s+key\s*\(\s*route_key\s*,\s*nonce_digest\s*\)/iu.test( + nonceMigration, + ) && + /expires_at\s*<=\s*issued_at\s*\+\s*interval\s*'5 minutes'/iu.test( + nonceMigration, + ) && + /session_user::text\s*<>\s*'programmable_release_probe_nonce_login'/iu.test( + nonceMigration, + ) && + /active_role\s+is\s+distinct\s+from\s+'programmable_release_probe_nonce'/iu.test( + nonceMigration, + ), + "one dedicated database identity atomically consumes each route-bound nonce", + ); + + const publicRouteReadiness = source( + "lib/data-pipeline/public-route-readiness.server.ts", + ); + const publicRouteReadinessAst = parseTypeScript( + "public-route-readiness.server.ts", + publicRouteReadiness, + ); + const preparePublicRouteRequest = findFunctionLike( + publicRouteReadinessAst, + "preparePublicRouteRequest", + ); + check( + "source-release-probe-private-failure", + containsAwaitedCall( + preparePublicRouteRequest, + "authorizeRouteReleaseProbe", + ) && + /status\s*:\s*503\b/u.test(publicRouteReadiness) && + /["']Cache-Control["']\s*:\s*["']private, no-store["']/u.test( + publicRouteReadiness, + ) && + /["']Retry-After["']\s*:\s*["']1["']/u.test(publicRouteReadiness), + "nonce-store failures stay private and fail closed with a retryable 503", + ); + check( + "source-release-probe-replay-validation", + containsConditionalDelete( + preparePublicRouteRequest, + "SHADOW_PROBE_QUERY_PARAMETER", + "releaseProbe", + ), + "only an authorized probe removes the reserved query parameter; replays reach ordinary validation", + ); + + const releaseProbeResponseFunction = findFunctionLike( + routeCoordinatorAst, + "releaseProbeResponse", + ); + const fallbackResponseFunction = findFunctionLike( + routeCoordinatorAst, + "fallbackResponse", + ); + check( + "source-release-probe-selected-provenance", + containsIdentifier(releaseProbeResponseFunction, "response") && + containsCall(fallbackResponseFunction, "provenanceHeaders") && + routeCoordinator + .toLowerCase() + .includes("x-programmable-read-source") && + !containsIdentifier(releaseProbeResponseFunction, "PROJECTION_HEADERS") && + !/headers\.delete\(\s*["']x-programmable-read-source["']\s*\)/iu.test( + routeCoordinator, + ), + "release probes retain the selected indexed, RPC, or blob provenance header", + ); + + return { + ok: failures.length === 0, + checks, + failures, + }; +} diff --git a/scripts/perf/read-model-staged-deployment.mjs b/scripts/perf/read-model-staged-deployment.mjs new file mode 100644 index 00000000..374bd464 --- /dev/null +++ b/scripts/perf/read-model-staged-deployment.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { appendFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { + deploymentCommit, + fetchVercelDeployment, +} from "./read-model-live-verifier.mjs"; + +function argument(name) { + const index = process.argv.indexOf(name); + const value = index >= 0 ? process.argv[index + 1] : undefined; + if (!value || value.startsWith("--")) { + throw new Error(`${name} is required`); + } + return value; +} + +async function main() { + const target = new URL(argument("--target-url")); + if ( + target.protocol !== "https:" || + target.pathname !== "/" || + !target.hostname.endsWith(".vercel.app") + ) { + throw new Error("target must be a deployment-specific Vercel URL"); + } + const gitHead = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: process.cwd(), + encoding: "utf8", + }).trim(); + const deployment = await fetchVercelDeployment({ + idOrUrl: target.hostname, + token: process.env.VERCEL_TOKEN, + teamId: process.env.VERCEL_ORG_ID, + }); + const deploymentHost = String(deployment.url ?? "") + .replace(/^https?:\/\//u, "") + .replace(/\/$/u, ""); + if ( + !/^dpl_[A-Za-z0-9]{20,80}$/u.test(deployment.id ?? "") || + deploymentHost !== target.hostname || + deployment.readyState !== "READY" || + (deployment.projectId !== process.env.VERCEL_PROJECT_ID && + deployment.project?.id !== process.env.VERCEL_PROJECT_ID) || + deploymentCommit(deployment) !== gitHead + ) { + throw new Error("staged deployment is not bound to this project and Git HEAD"); + } + const outputPath = resolve(argument("--github-output")); + appendFileSync( + outputPath, + `deployment_id=${deployment.id}\ntarget_url=${target.toString()}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + process.stdout.write( + `${JSON.stringify({ + status: "verified-staged", + deploymentId: deployment.id, + targetUrl: target.toString(), + gitHead, + })}\n`, + ); +} + +main().catch((error) => { + process.stderr.write( + `${error instanceof Error ? error.message : "deployment binding failed"}\n`, + ); + process.exitCode = 1; +}); diff --git a/scripts/run-pglite-db-tests.mjs b/scripts/run-pglite-db-tests.mjs new file mode 100644 index 00000000..9c85edcf --- /dev/null +++ b/scripts/run-pglite-db-tests.mjs @@ -0,0 +1,59 @@ +import { readFile, readdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; +import { PGlite } from "@electric-sql/pglite"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const migrationsDirectory = join(root, "supabase", "migrations"); +const testsDirectory = join(root, "supabase", "tests", "database"); +const pgTapCompatibilityFile = join( + root, + "supabase", + "tests", + "pglite", + "pgtap-compatibility.sql", +); + +const migrationFiles = (await readdir(migrationsDirectory)) + .filter((file) => file.endsWith(".sql")) + .sort(); +const testFiles = (await readdir(testsDirectory)) + .filter((file) => file.endsWith(".test.sql")) + .sort(); +const pgTapCompatibility = await readFile(pgTapCompatibilityFile, "utf8"); + +if (migrationFiles.length === 0 || testFiles.length === 0) { + throw new Error("database migrations and pgTAP tests must both be present"); +} + +let failed = false; +for (const testFile of testFiles) { + const database = new PGlite(); + try { + await database.exec(` + create role anon nologin; + create role authenticated nologin; + create role service_role nologin; + `); + for (const migrationFile of migrationFiles) { + await database.exec( + await readFile(join(migrationsDirectory, migrationFile), "utf8"), + ); + } + await database.exec(pgTapCompatibility); + await database.exec(await readFile(join(testsDirectory, testFile), "utf8")); + console.log(`PASS ${testFile}`); + } catch (error) { + failed = true; + console.error(`FAIL ${testFile}`); + console.error(error instanceof Error ? error.message : error); + if (error && typeof error === "object" && "detail" in error && error.detail) { + console.error(error.detail); + } + } finally { + await database.close(); + } + if (failed) break; +} + +if (failed) process.exitCode = 1; diff --git a/scripts/security/run-gitleaks-ci.sh b/scripts/security/run-gitleaks-ci.sh new file mode 100644 index 00000000..b9e861b3 --- /dev/null +++ b/scripts/security/run-gitleaks-ci.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly gitleaks_version="8.30.1" +readonly gitleaks_archive="gitleaks_${gitleaks_version}_linux_x64.tar.gz" +readonly gitleaks_sha256="551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" +readonly gitleaks_url="https://github.com/gitleaks/gitleaks/releases/download/v${gitleaks_version}/${gitleaks_archive}" +readonly workspace="${GITHUB_WORKSPACE:-$(git rev-parse --show-toplevel)}" +readonly temp_root="${RUNNER_TEMP:-/tmp}" +scan_dir="$(mktemp -d "${temp_root%/}/programmable-gitleaks.XXXXXX")" + +cleanup() { + rm -rf -- "$scan_dir" +} +trap cleanup EXIT + +curl --fail --silent --show-error --location \ + --proto '=https' --tlsv1.2 \ + "$gitleaks_url" \ + --output "$scan_dir/$gitleaks_archive" +printf '%s %s\n' "$gitleaks_sha256" "$scan_dir/$gitleaks_archive" \ + | sha256sum --check --status +tar --extract --gzip --file "$scan_dir/$gitleaks_archive" \ + --directory "$scan_dir" gitleaks + +head_sha="$(git -C "$workspace" rev-parse HEAD)" +base_sha="${PROGRAMMABLE_GITLEAKS_BASE_SHA:-}" +if [[ ! "$base_sha" =~ ^[0-9a-f]{40}$ ]] \ + || [[ "$base_sha" =~ ^0{40}$ ]] \ + || ! git -C "$workspace" cat-file -e "${base_sha}^{commit}" 2>/dev/null; then + if git -C "$workspace" rev-parse HEAD^ >/dev/null 2>&1; then + base_sha="$(git -C "$workspace" rev-parse HEAD^)" + else + base_sha="" + fi +fi + +log_opts="$head_sha" +if [[ -n "$base_sha" ]]; then + log_opts="${base_sha}..${head_sha}" +fi + +"$scan_dir/gitleaks" git "$workspace" \ + --log-opts "$log_opts" \ + --redact=100 \ + --no-banner diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 00000000..4de2e4c6 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,50 @@ +project_id = "programmable-read-model-local" + +[api] +enabled = false +port = 54321 +schemas = ["public", "graphql_public"] +extra_search_path = ["public", "extensions"] +max_rows = 1000 + +[db] +port = 54322 +shadow_port = 54320 +health_timeout = "2m" +major_version = 17 + +[db.pooler] +enabled = true +port = 54329 +pool_mode = "transaction" +default_pool_size = 20 +max_client_conn = 100 + +[db.migrations] +enabled = true +schema_paths = [] + +[db.seed] +enabled = true +sql_paths = ["./seed.sql"] + +[realtime] +enabled = false + +[studio] +enabled = false + +[inbucket] +enabled = false + +[storage] +enabled = false + +[auth] +enabled = false + +[analytics] +enabled = false + +[edge_runtime] +enabled = false diff --git a/supabase/migrations/20260731000100_private_schema_roles_domains.sql b/supabase/migrations/20260731000100_private_schema_roles_domains.sql new file mode 100644 index 00000000..f0cc5155 --- /dev/null +++ b/supabase/migrations/20260731000100_private_schema_roles_domains.sql @@ -0,0 +1,810 @@ +-- Programmable private read model: roles, private schema, domains and shared validators. +-- Transaction/log ordinals use an explicit unsigned 32-bit ceiling. Ethereum JSON-RPC +-- quantities are still stored in bigint, so no signed int4 truncation can occur. + +do $bootstrap$ +begin + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_migrator') then + create role programmable_migrator + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_projector') then + create role programmable_projector + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_reconciler') then + create role programmable_reconciler + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_api_reader') then + create role programmable_api_reader + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_profile_binder') then + create role programmable_profile_binder + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_profile_recovery') then + create role programmable_profile_recovery + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_profile_writer') then + create role programmable_profile_writer + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_maintenance') then + create role programmable_maintenance + nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls; + end if; + -- Login identities are deliberately separate from capability roles. They + -- start with a null password and cannot inherit capability privileges; the + -- deployment operator supplies credentials out of band after migration and + -- the service must explicitly SET ROLE for each session/transaction. + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_api_reader_login') then + create role programmable_api_reader_login + login password null nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_projector_login') then + create role programmable_projector_login + login password null nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; + if not exists (select 1 from pg_catalog.pg_roles where rolname = 'programmable_reconciler_login') then + create role programmable_reconciler_login + login password null nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; +end +$bootstrap$; + +alter role programmable_migrator + nologin nocreatedb nocreaterole noinherit; +alter role programmable_projector + nologin nocreatedb nocreaterole noinherit; +alter role programmable_reconciler + nologin nocreatedb nocreaterole noinherit; +alter role programmable_api_reader + nologin nocreatedb nocreaterole noinherit; +alter role programmable_profile_binder + nologin nocreatedb nocreaterole noinherit; +alter role programmable_profile_recovery + nologin nocreatedb nocreaterole noinherit; +alter role programmable_profile_writer + nologin nocreatedb nocreaterole noinherit; +alter role programmable_maintenance + nologin nocreatedb nocreaterole noinherit; +alter role programmable_api_reader_login + login nocreatedb nocreaterole noinherit; +alter role programmable_projector_login + login nocreatedb nocreaterole noinherit; +alter role programmable_reconciler_login + login nocreatedb nocreaterole noinherit; + +do $posture$ +begin + if exists ( + select 1 + from pg_catalog.pg_roles + where rolname = any (array[ + 'programmable_migrator', + 'programmable_projector', + 'programmable_reconciler', + 'programmable_api_reader', + 'programmable_profile_binder', + 'programmable_profile_recovery', + 'programmable_profile_writer', + 'programmable_maintenance', + 'programmable_api_reader_login', + 'programmable_projector_login', + 'programmable_reconciler_login' + ]::name[]) + and (rolsuper or rolreplication or rolbypassrls) + ) then + raise exception 'programmable role posture is privileged'; + end if; +end +$posture$; + +grant programmable_api_reader to programmable_api_reader_login + with inherit false, set true; +grant programmable_projector to programmable_projector_login + with inherit false, set true; +grant programmable_reconciler to programmable_reconciler_login + with inherit false, set true; + +-- PostgreSQL 17 records the CREATEROLE creator as grantor, so granting ADMIN +-- back to that same role is rejected. Grant only the SET capability needed by +-- the migration connection and then verify it explicitly. +grant programmable_migrator to postgres with inherit false, set true; + +do $migrator_membership$ +begin + if not pg_catalog.pg_has_role( + current_user, + 'programmable_migrator', + 'set' + ) then + raise exception 'migration connection cannot set programmable_migrator'; + end if; +end +$migrator_membership$; + +create schema if not exists programmable_private authorization programmable_migrator; +alter schema programmable_private owner to programmable_migrator; + +revoke all on schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +set role programmable_migrator; + +create domain programmable_private.eth_address as bytea + check (value is null or pg_catalog.octet_length(value) = 20); + +create domain programmable_private.bytes32_value as bytea + check (value is null or pg_catalog.octet_length(value) = 32); + +create domain programmable_private.hex_selector as bytea + check (value is null or pg_catalog.octet_length(value) = 4); + +-- Deliberately based on unconstrained numeric. A numeric(78,0) typmod would +-- round fractional input before this check and is therefore forbidden. +create domain programmable_private.uint256_value as numeric + check ( + value is null + or ( + value = pg_catalog.trunc(value) + and value >= 0 + and value <= + 115792089237316195423570985008687907853269984665640564039457584007913129639935 + ) + ); + +create domain programmable_private.basis_points as integer + check (value is not null and value between 0 and 10000); + +create domain programmable_private.chain_id_value as bigint + check (value is not null and value > 0); + +create domain programmable_private.block_number_value as bigint + check (value is not null and value between 0 and 9223372036854775807); + +create domain programmable_private.transaction_index_value as bigint + check (value is not null and value between 0 and 4294967295); + +create domain programmable_private.block_log_index_value as bigint + check (value is not null and value between 0 and 4294967295); + +create domain programmable_private.receipt_log_ordinal_value as bigint + check (value is not null and value between 0 and 4294967295); + +create domain programmable_private.release_identifier as text + check ( + value is not null + and pg_catalog.octet_length(value) between 1 and 64 + and value operator(pg_catalog.~) '^[a-z0-9][a-z0-9._-]*$' + ); + +create domain programmable_private.model_identifier as text + check ( + value is not null + and pg_catalog.octet_length(value) between 1 and 64 + and value operator(pg_catalog.~) '^[a-z0-9][a-z0-9._-]*$' + ); + +create domain programmable_private.source_identifier as text + check ( + value is not null + and pg_catalog.octet_length(value) between 1 and 128 + and value operator(pg_catalog.~) '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + ); + +-- Envio's stable event identifier is materially longer than a human-facing +-- source name. Keep it in a dedicated 192-byte envelope so widening a stream +-- identity cannot silently widen roles, release names, audit actions, or other +-- bounded identifiers. Ingestion writers separately enforce the exact +-- provider grammar and its unsigned-32-bit suffix against the row fields. +create domain programmable_private.envio_candidate_identifier as text + check ( + value is null + or ( + pg_catalog.octet_length(value) between 1 and 192 + and value operator(pg_catalog.~) '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + ) + ); + +create domain programmable_private.projector_identifier as text + check ( + value is not null + and pg_catalog.octet_length(value) between 1 and 128 + and value operator(pg_catalog.~) '^[A-Za-z0-9][A-Za-z0-9._+:/-]*$' + ); + +create type programmable_private.source_type as enum ( + 'ethereum_contract', + 'envio_deployment', + 'rpc_provider', + 'uniswap_subgraph' +); + +create type programmable_private.run_kind as enum ( + 'ingestion', + 'projection', + 'reconciliation', + 'rewind', + 'profile_recovery', + 'maintenance' +); + +create type programmable_private.run_status as enum ( + 'succeeded', + 'failed', + 'cancelled' +); + +create type programmable_private.occurrence_status as enum ( + 'observed', + 'canonical', + 'orphaned', + 'superseded', + 'conflicted' +); + +create type programmable_private.envio_candidate_status as enum ( + 'pending', + 'deferred', + 'resolved', + 'ignored', + 'quarantined' +); + +create type programmable_private.reward_seed_status as enum ( + 'observed', + 'verified', + 'quarantined', + 'orphaned', + 'conflicted', + 'revoked' +); + +create type programmable_private.reconciliation_severity as enum ( + 'info', + 'warning', + 'high', + 'critical' +); + +create type programmable_private.recovery_method as enum ( + 'historical_getters', + 'launcher_calldata', + 'coordinator_calldata', + 'factory_calldata' +); + +create type programmable_private.route_mode as enum ( + 'indexed', + 'blob', + 'rpc', + 'disabled' +); + +create type programmable_private.route_eligibility_status as enum ( + 'eligible', + 'ineligible', + 'quarantined' +); + +create type programmable_private.historical_enrichment_status as enum ( + 'matched', + 'unavailable' +); + +create type programmable_private.dependency_health_status as enum ( + 'closed', + 'open', + 'half_open', + 'frozen' +); + +create type programmable_private.profile_hash_version_state as enum ( + 'current', + 'verify_only', + 'retired' +); + +create type programmable_private.profile_alias_state as enum ( + 'current', + 'verify_only', + 'tombstoned' +); + +create type programmable_private.profile_binding_state as enum ( + 'active', + 'recovered', + 'tombstoned' +); + +create type programmable_private.profile_recovery_method as enum ( + 'linked_wallet', + 'wallet_signature', + 'verified_subject_recovery' +); + +create type programmable_private.market_interval as enum ( + 'snapshot', + 'hour', + 'day' +); + +create function programmable_private.derive_envio_candidate_id( + p_chain_id bigint, + p_block_hash bytea, + p_transaction_hash bytea, + p_block_global_log_index numeric +) +returns programmable_private.envio_candidate_identifier +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +begin + if p_chain_id <> 1 + or pg_catalog.octet_length(p_block_hash) <> 32 + or pg_catalog.octet_length(p_transaction_hash) <> 32 + or p_block_global_log_index <> pg_catalog.trunc(p_block_global_log_index) + or p_block_global_log_index < 0 + or p_block_global_log_index > 4294967295 + then + raise exception using + errcode = '22023', + message = 'invalid canonical Envio candidate identity components'; + end if; + return pg_catalog.format( + '1:0x%s:0x%s:%s', + pg_catalog.encode(p_block_hash, 'hex'), + pg_catalog.encode(p_transaction_hash, 'hex'), + p_block_global_log_index::bigint + )::programmable_private.envio_candidate_identifier; +end +$function$; + +create function programmable_private.validate_uint256(p_value numeric) +returns programmable_private.uint256_value +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +begin + if p_value <> pg_catalog.trunc(p_value) + or p_value < 0 + or p_value > + 115792089237316195423570985008687907853269984665640564039457584007913129639935 + then + raise exception using + errcode = '22003', + message = 'uint256 value must be an integer in [0, 2^256-1]'; + end if; + return p_value::programmable_private.uint256_value; +end +$function$; + +create function programmable_private.parse_uint256_decimal(p_value text) +returns programmable_private.uint256_value +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +declare + parsed numeric; +begin + if p_value !~ '^(0|[1-9][0-9]*)$' then + raise exception using + errcode = '22P02', + message = 'uint256 decimal must use canonical unsigned integer grammar'; + end if; + parsed := p_value::numeric; + return programmable_private.validate_uint256(parsed); +end +$function$; + +create function programmable_private.valid_topics(p_topics bytea[]) +returns boolean +language sql +immutable +strict +security invoker +set search_path = '' +as $function$ + select coalesce( + pg_catalog.bool_and(pg_catalog.octet_length(topic) = 32), + true + ) + from pg_catalog.unnest(p_topics) as topic +$function$; + +create function programmable_private.valid_immutable_values(p_values bytea[]) +returns boolean +language sql +immutable +strict +security invoker +set search_path = '' +as $function$ + select pg_catalog.cardinality(p_values) between 1 and 64 + and coalesce( + ( + select pg_catalog.bool_and( + value is not null + and pg_catalog.octet_length(value) between 1 and 32 + ) + from pg_catalog.unnest(p_values) as value + ), + false + ) +$function$; + +create function programmable_private.valid_immutable_binding_spec(p_spec jsonb) +returns boolean +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +declare + binding jsonb; + binding_count integer; + ordinal integer := 0; + binding_offset integer; + binding_length integer; + previous_end integer := 0; + source_kind text; + encoding_kind text; + field_name text; + constant_value text; +begin + if pg_catalog.jsonb_typeof(p_spec) <> 'object' + or pg_catalog.octet_length(p_spec::text) > 65536 + or pg_catalog.jsonb_typeof(p_spec -> 'bindings') <> 'array' + or pg_catalog.jsonb_typeof(p_spec -> 'factoryConfigurationField') + <> 'string' + or (p_spec ->> 'factoryConfigurationField') !~ + '^[A-Za-z][A-Za-z0-9_]{0,63}$' + then + return false; + end if; + binding_count := pg_catalog.jsonb_array_length(p_spec -> 'bindings'); + if binding_count < 1 or binding_count > 64 then + return false; + end if; + for binding in + select value from pg_catalog.jsonb_array_elements(p_spec -> 'bindings') + loop + if pg_catalog.jsonb_typeof(binding) <> 'object' + or coalesce(binding ->> 'ordinal', '') !~ '^(0|[1-9][0-9]*)$' + or coalesce(binding ->> 'offset', '') !~ '^(0|[1-9][0-9]*)$' + or coalesce(binding ->> 'length', '') !~ '^[1-9][0-9]*$' + then + return false; + end if; + if (binding ->> 'ordinal')::integer <> ordinal then + return false; + end if; + binding_offset := (binding ->> 'offset')::integer; + binding_length := (binding ->> 'length')::integer; + source_kind := binding ->> 'source'; + encoding_kind := binding ->> 'encoding'; + field_name := binding ->> 'field'; + constant_value := binding ->> 'value'; + if binding_length > 32 + or binding_offset < previous_end + or source_kind not in ('factory_event', 'constant', 'deployed_address') + or encoding_kind not in ('address', 'bytes') + or (encoding_kind = 'address' and binding_length not in (20, 32)) + or ( + source_kind = 'factory_event' + and ( + field_name is null + or field_name !~ '^[A-Za-z][A-Za-z0-9_]{0,63}$' + or constant_value is not null + ) + ) + or ( + source_kind = 'constant' + and ( + field_name is not null + or constant_value is null + or constant_value !~ '^0x([0-9a-f][0-9a-f])+$' + or pg_catalog.length(constant_value) <> 2 + (2 * binding_length) + ) + ) + or ( + source_kind = 'deployed_address' + and (field_name is not null or constant_value is not null + or encoding_kind <> 'address') + ) + then + return false; + end if; + previous_end := binding_offset + binding_length; + ordinal := ordinal + 1; + end loop; + return true; +exception when others then + return false; +end +$function$; + +create function programmable_private.immutable_values_match_binding_spec( + p_spec jsonb, + p_factory_payload jsonb, + p_deployed_address bytea, + p_values bytea[] +) +returns boolean +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +declare + binding jsonb; + ordinal integer := 1; + binding_length integer; + source_kind text; + encoding_kind text; + source_value text; + expected_value bytea; +begin + if not programmable_private.valid_immutable_binding_spec(p_spec) + or pg_catalog.octet_length(p_deployed_address) <> 20 + or pg_catalog.cardinality(p_values) + <> pg_catalog.jsonb_array_length(p_spec -> 'bindings') + or exists ( + select 1 from pg_catalog.unnest(p_values) as value + where value is null + ) + then + return false; + end if; + for binding in + select value from pg_catalog.jsonb_array_elements(p_spec -> 'bindings') + loop + binding_length := (binding ->> 'length')::integer; + source_kind := binding ->> 'source'; + encoding_kind := binding ->> 'encoding'; + if pg_catalog.octet_length(p_values[ordinal]) <> binding_length then + return false; + end if; + if source_kind = 'constant' then + expected_value := pg_catalog.decode( + pg_catalog.substring(binding ->> 'value', 3), 'hex' + ); + elsif source_kind = 'deployed_address' then + expected_value := case + when binding_length = 20 then p_deployed_address + else pg_catalog.decode(pg_catalog.repeat('00', 12), 'hex') + || p_deployed_address + end; + else + source_value := p_factory_payload ->> (binding ->> 'field'); + if encoding_kind = 'address' then + if source_value is null + or source_value !~ '^0x[0-9a-f]{40}$' + then + return false; + end if; + expected_value := case + when binding_length = 20 then pg_catalog.decode( + pg_catalog.substring(source_value, 3), 'hex' + ) + else pg_catalog.decode(pg_catalog.repeat('00', 12), 'hex') + || pg_catalog.decode( + pg_catalog.substring(source_value, 3), 'hex' + ) + end; + else + if source_value is null + or source_value !~ '^0x([0-9a-f][0-9a-f])+$' + or pg_catalog.length(source_value) <> 2 + (2 * binding_length) + then + return false; + end if; + expected_value := pg_catalog.decode( + pg_catalog.substring(source_value, 3), 'hex' + ); + end if; + end if; + if p_values[ordinal] <> expected_value then + return false; + end if; + ordinal := ordinal + 1; + end loop; + return true; +exception when others then + return false; +end +$function$; + +create function programmable_private.immutable_binding_spec_fits_runtime( + p_spec jsonb, + p_runtime_code_length bigint +) +returns boolean +language sql +immutable +strict +security invoker +set search_path = '' +as $function$ + select programmable_private.valid_immutable_binding_spec(p_spec) + and p_runtime_code_length > 0 + and not exists ( + select 1 + from pg_catalog.jsonb_array_elements(p_spec -> 'bindings') as binding + where (binding ->> 'offset')::bigint + + (binding ->> 'length')::bigint > p_runtime_code_length + ) +$function$; + +create function programmable_private.valid_beneficiary_set( + p_beneficiaries bytea[], + p_shares integer[], + p_max_entries integer +) +returns boolean +language sql +immutable +strict +security invoker +set search_path = '' +as $function$ + select + coalesce(pg_catalog.array_length(p_beneficiaries, 1), 0) + between 1 and p_max_entries + and coalesce(pg_catalog.array_length(p_beneficiaries, 1), 0) + = coalesce(pg_catalog.array_length(p_shares, 1), 0) + and ( + select pg_catalog.bool_and( + pg_catalog.octet_length(beneficiary) = 20 + and beneficiary <> pg_catalog.decode('0000000000000000000000000000000000000000', 'hex') + ) + from pg_catalog.unnest(p_beneficiaries) as beneficiary + ) + and ( + select pg_catalog.count(*) = pg_catalog.count(distinct beneficiary) + from pg_catalog.unnest(p_beneficiaries) as beneficiary + ) + and ( + select pg_catalog.bool_and(share > 0 and share <= 10000) + from pg_catalog.unnest(p_shares) as share + ) + and ( + select pg_catalog.sum(share)::bigint = 10000 + from pg_catalog.unnest(p_shares) as share + ) +$function$; + +create function programmable_private.valid_avatar_reference(p_value text) +returns boolean +language sql +immutable +security invoker +set search_path = '' +as $function$ + select p_value is null or ( + pg_catalog.octet_length(p_value) between 1 and 512 + and p_value !~ '[[:cntrl:]]' + and ( + p_value ~ '^https://[A-Za-z0-9.-]+(?::[0-9]+)?(?:/[A-Za-z0-9._~:/?#@!$&''()*+,;=%-]*)?$' + or p_value ~ '^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$' + ) + ) +$function$; + +create function programmable_private.valid_profile_username(p_value text) +returns boolean +language sql +immutable +security invoker +set search_path = '' +as $function$ + select p_value is null or p_value ~ '^[A-Za-z0-9]{3,12}$' +$function$; + +create function programmable_private.assert_caller(p_expected name) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + active_role text := pg_catalog.current_setting('role', true); +begin + if session_user::text <> p_expected::text + and coalesce(active_role, 'none') <> p_expected::text + then + raise exception using + errcode = '42501', + message = pg_catalog.format('function requires capability role %I', p_expected); + end if; +end +$function$; + +create function programmable_private.caller_role_name() +returns name +language sql +stable +security invoker +set search_path = '' +as $function$ + select case + when pg_catalog.current_setting('role', true) is null + or pg_catalog.current_setting('role', true) = 'none' + then session_user::name + else pg_catalog.current_setting('role', true)::name + end +$function$; + +revoke all on all functions in schema programmable_private from public; +do $revoke_public_type_usage$ +declare + private_type record; +begin + for private_type in + select type_row.typname + from pg_catalog.pg_type as type_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = type_row.typnamespace + where namespace_row.nspname = 'programmable_private' + and type_row.typtype in ('d', 'e') + loop + execute pg_catalog.format( + 'revoke all on type programmable_private.%I from public', + private_type.typname + ); + end loop; +end +$revoke_public_type_usage$; + +alter default privileges for role programmable_migrator in schema programmable_private + revoke all on tables from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; +alter default privileges for role programmable_migrator in schema programmable_private + revoke all on sequences from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; +alter default privileges for role programmable_migrator in schema programmable_private + revoke execute on functions from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; +alter default privileges for role programmable_migrator in schema programmable_private + revoke usage on types from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on all tables in schema programmable_private + from public, anon, authenticated, service_role; +revoke all on all sequences in schema programmable_private + from public, anon, authenticated, service_role; +revoke all on all functions in schema programmable_private + from public, anon, authenticated, service_role; + +reset role; diff --git a/supabase/migrations/20260731000200_provider_ingestion_control.sql b/supabase/migrations/20260731000200_provider_ingestion_control.sql new file mode 100644 index 00000000..4060b7a7 --- /dev/null +++ b/supabase/migrations/20260731000200_provider_ingestion_control.sql @@ -0,0 +1,1916 @@ +-- Provider commitments, immutable run provenance, dual-RPC gates, release +-- epochs, lease generations and atomic checkpoint identities. + +set role programmable_migrator; + +create table programmable_private.fingerprint_encoding_versions ( + fingerprint_domain text not null + check (fingerprint_domain in ('occurrence', 'allocation', 'evidence')), + encoding_version smallint not null check (encoding_version > 0), + domain_prefix bytea not null, + write_enabled boolean not null, + definition_commitment programmable_private.bytes32_value not null, + allowlisted_at timestamptz not null, + primary key (fingerprint_domain, encoding_version), + unique (domain_prefix), + check ( + pg_catalog.octet_length(domain_prefix) >= 25 + and pg_catalog.get_byte( + domain_prefix, + pg_catalog.octet_length(domain_prefix) - 1 + ) = 0 + ) +); + +insert into programmable_private.fingerprint_encoding_versions ( + fingerprint_domain, encoding_version, domain_prefix, write_enabled, + definition_commitment, allowlisted_at +) +values + ( + 'occurrence', 1, + pg_catalog.decode( + '70726f6772616d6d61626c653a6f6363757272656e63653a763100', + 'hex' + ), + true, pg_catalog.decode(pg_catalog.repeat('01', 32), 'hex'), + '2026-07-31T00:00:00Z' + ), + ( + 'allocation', 1, + pg_catalog.decode( + '70726f6772616d6d61626c653a616c6c6f636174696f6e3a763100', + 'hex' + ), + true, pg_catalog.decode(pg_catalog.repeat('02', 32), 'hex'), + '2026-07-31T00:00:00Z' + ), + ( + 'evidence', 1, + pg_catalog.decode( + '70726f6772616d6d61626c653a65766964656e63653a763100', + 'hex' + ), + true, pg_catalog.decode(pg_catalog.repeat('03', 32), 'hex'), + '2026-07-31T00:00:00Z' + ), + ( + 'evidence', 2, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200', + 'hex' + ), + true, + pg_catalog.decode( + '45b8e9d1bf3ffc2e70b7fd612ec2346aef5e74ae08348b699eb68ce0afbc9483', + 'hex' + ), + '2026-07-31T00:00:00Z' + ); + +create table programmable_private.provider_evidence_encoding_subtypes ( + evidence_subtype programmable_private.source_identifier primary key, + fingerprint_domain text not null default 'evidence' + check (fingerprint_domain = 'evidence'), + encoding_version smallint not null, + subtype_tag smallint not null check (subtype_tag between 1 and 255), + frame_prefix bytea not null unique, + definition_commitment programmable_private.bytes32_value not null, + foreign key (fingerprint_domain, encoding_version) + references programmable_private.fingerprint_encoding_versions( + fingerprint_domain, encoding_version + ) on delete restrict, + unique (encoding_version, subtype_tag), + check (pg_catalog.octet_length(frame_prefix) = 35) +); + +insert into programmable_private.provider_evidence_encoding_subtypes ( + evidence_subtype, encoding_version, subtype_tag, frame_prefix, + definition_commitment +) values + ( + 'safe_head', 2, 1, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320001', + 'hex' + ), + pg_catalog.decode( + '3a26ae9c9220347568e33b5850ac6f605d120e6443f64e9e8b8742ea8a016f52', + 'hex' + ) + ), + ( + 'block', 2, 2, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320002', + 'hex' + ), + pg_catalog.decode( + '83948b75a3c05b9d257749f754f09a1b02e658496ba562f36e07bc15be3d7bec', + 'hex' + ) + ), + ( + 'runtime_code', 2, 3, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320003', + 'hex' + ), + pg_catalog.decode( + '4c191e91130097832a91025e85c2ff3be2705af0e3ea9abc396f09e7cd9dbbc5', + 'hex' + ) + ), + ( + 'dynamic_attestation', 2, 4, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320004', + 'hex' + ), + pg_catalog.decode( + '206e1f89ad459e55e0591de13eb40856dd94ff62923d76034eba5776706e6de9', + 'hex' + ) + ); + +create function programmable_private.assert_fingerprint_encoding( + p_fingerprint_domain text, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + expected_prefix bytea; +begin + select version_row.domain_prefix into expected_prefix + from programmable_private.fingerprint_encoding_versions as version_row + where version_row.fingerprint_domain = p_fingerprint_domain + and version_row.encoding_version = p_encoding_version + and version_row.write_enabled; + if not found + or pg_catalog.octet_length(p_content_fingerprint) <> 32 + or pg_catalog.octet_length(p_canonical_preimage) + < pg_catalog.octet_length(expected_prefix) + or pg_catalog.substring( + p_canonical_preimage, + 1, + pg_catalog.octet_length(expected_prefix) + ) <> expected_prefix + then + raise exception using + errcode = '22023', + message = 'fingerprint encoding version or domain prefix is not allowlisted'; + end if; +end +$function$; + +create function programmable_private.assert_provider_evidence_encoding( + p_evidence_subtype text, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + expected_prefix bytea; +begin + perform programmable_private.assert_fingerprint_encoding( + 'evidence', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select subtype.frame_prefix into expected_prefix + from programmable_private.provider_evidence_encoding_subtypes as subtype + where subtype.evidence_subtype = p_evidence_subtype + and subtype.encoding_version = p_encoding_version; + if not found + or pg_catalog.octet_length(p_canonical_preimage) + < pg_catalog.octet_length(expected_prefix) + or pg_catalog.substring( + p_canonical_preimage, 1, pg_catalog.octet_length(expected_prefix) + ) <> expected_prefix + then + raise exception using + errcode = '22023', + message = 'provider evidence subtype or frame tag is not allowlisted'; + end if; +end +$function$; + +create table programmable_private.mutation_audits ( + audit_id uuid primary key, + action programmable_private.source_identifier not null, + caller_role name not null, + input_commitment programmable_private.bytes32_value not null, + run_id uuid, + occurred_at timestamptz not null, + check (occurred_at <= pg_catalog.clock_timestamp() + interval '5 minutes') +); + +create table programmable_private.release_epochs ( + epoch_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_number bigint not null check (epoch_number > 0), + epoch_commitment programmable_private.bytes32_value not null, + artifact_creation_code_commitment programmable_private.bytes32_value not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (chain_id, release_id, model_id, source_group, epoch_number), + unique (epoch_id, chain_id, release_id, model_id, source_group) +); + +create table programmable_private.release_source_bindings ( + binding_id uuid primary key, + epoch_id uuid not null + references programmable_private.release_epochs(epoch_id) + on delete restrict, + source_name programmable_private.source_identifier not null, + source_role programmable_private.source_identifier not null, + source_type programmable_private.source_type not null, + source_address programmable_private.eth_address, + recovery_selector programmable_private.hex_selector, + inclusive_start_block programmable_private.block_number_value not null, + abi_event_set_commitment programmable_private.bytes32_value not null, + artifact_creation_code_commitment programmable_private.bytes32_value not null, + binding_commitment programmable_private.bytes32_value not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + (source_type = 'ethereum_contract' and source_address is not null) + or (source_type <> 'ethereum_contract' and source_address is null) + ), + check (recovery_selector is null or source_type = 'ethereum_contract'), + unique (epoch_id, source_name), + unique (epoch_id, source_role, source_address), + unique (epoch_id, source_address, abi_event_set_commitment), + unique (epoch_id, binding_commitment) +); + +create table programmable_private.provider_deployments ( + provider_deployment_id uuid primary key, + provider_type programmable_private.source_type not null, + redacted_identity programmable_private.source_identifier not null, + deployment_commitment programmable_private.bytes32_value not null, + schema_commitment programmable_private.bytes32_value not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check (provider_type in ('rpc_provider', 'envio_deployment', 'uniswap_subgraph')), + unique (redacted_identity), + unique (provider_type, deployment_commitment, schema_commitment) +); + +-- RPC endpoints are production secrets. Only domain-separated commitments are +-- retained, and the two independent mainnet vendors have one canonical order. +create table programmable_private.rpc_endpoint_evidence_domains ( + evidence_domain programmable_private.source_identifier primary key, + definition_commitment programmable_private.bytes32_value not null, + enabled boolean not null, + allowlisted_at timestamptz not null, + check ( + definition_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ) +); + +insert into programmable_private.rpc_endpoint_evidence_domains ( + evidence_domain, definition_commitment, enabled, allowlisted_at +) +values ( + 'rpc-endpoint-commitments-v1', + pg_catalog.decode(pg_catalog.repeat('31', 32), 'hex'), + true, + '2026-07-31T00:00:00Z' +); + +create table programmable_private.rpc_provider_deployment_metadata ( + provider_deployment_id uuid primary key + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + vendor programmable_private.source_identifier not null, + vendor_order smallint not null, + constructor_version programmable_private.projector_identifier not null, + endpoint_url_commitment programmable_private.bytes32_value not null, + endpoint_origin_commitment programmable_private.bytes32_value not null, + endpoint_evidence_domain programmable_private.source_identifier not null + references programmable_private.rpc_endpoint_evidence_domains(evidence_domain) + on delete restrict, + endpoint_evidence_commitment programmable_private.bytes32_value not null, + check (chain_id = 1), + check ( + (vendor = 'alchemy' and vendor_order = 1) + or (vendor = 'quicknode' and vendor_order = 2) + ), + check ( + endpoint_url_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + and endpoint_origin_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + and endpoint_evidence_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ), + unique (chain_id, vendor), + unique (chain_id, vendor_order) +); + +create table programmable_private.release_epoch_current ( + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + generation bigint not null check (generation > 0), + changed_at timestamptz not null, + changed_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + primary key (chain_id, release_id, model_id, source_group), + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) + on delete restrict +); + +create table programmable_private.release_epoch_pointer_history ( + history_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + previous_epoch_id uuid, + next_epoch_id uuid not null, + previous_generation bigint not null check (previous_generation >= 0), + next_generation bigint not null check (next_generation = previous_generation + 1), + changed_at timestamptz not null, + changed_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (next_epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) + on delete restrict, + check (previous_epoch_id is not null or previous_generation = 0), + unique (chain_id, release_id, model_id, source_group, next_generation) +); + +-- Release-neutral Envio ingestion uses one migration-owned control scope. +-- Product release epochs never own the global stream cursor or raw inbox. +insert into programmable_private.mutation_audits ( + audit_id, action, caller_role, input_commitment, run_id, occurred_at +) values ( + '70000000-0000-0000-0000-000000000001', + 'envio_control.bootstrap', 'programmable_migrator'::name, + pg_catalog.decode(pg_catalog.repeat('e0', 32), 'hex'), null, + '2026-07-31T00:00:00Z' +); + +insert into programmable_private.release_epochs ( + epoch_id, chain_id, release_id, model_id, source_group, epoch_number, + epoch_commitment, artifact_creation_code_commitment, created_at, + created_by_audit_id +) values ( + '70000000-0000-0000-0000-000000000002', 1, + 'envio-control', 'envio-control', 'canonical-events', 1, + pg_catalog.decode(pg_catalog.repeat('e1', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('e2', 32), 'hex'), + '2026-07-31T00:00:00Z', + '70000000-0000-0000-0000-000000000001' +); + +insert into programmable_private.release_epoch_current ( + chain_id, release_id, model_id, source_group, epoch_id, generation, + changed_at, changed_by_audit_id +) values ( + 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + '2026-07-31T00:00:00Z', + '70000000-0000-0000-0000-000000000001' +); + +insert into programmable_private.release_epoch_pointer_history ( + history_id, chain_id, release_id, model_id, source_group, + previous_epoch_id, next_epoch_id, previous_generation, next_generation, + changed_at, changed_by_audit_id +) values ( + '70000000-0000-0000-0000-000000000003', 1, + 'envio-control', 'envio-control', 'canonical-events', null, + '70000000-0000-0000-0000-000000000002', 0, 1, + '2026-07-31T00:00:00Z', + '70000000-0000-0000-0000-000000000001' +); + +create table programmable_private.run_headers ( + run_id uuid primary key, + run_kind programmable_private.run_kind not null, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + captured_pointer_generation bigint not null check (captured_pointer_generation > 0), + worker_version programmable_private.projector_identifier not null, + request_commitment programmable_private.bytes32_value not null, + caller_role name not null, + started_at timestamptz not null, + opened_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) + on delete restrict, + unique (run_id, epoch_id, captured_pointer_generation) +); + +alter table programmable_private.mutation_audits + add constraint mutation_audits_run_id_fkey + foreign key (run_id) + references programmable_private.run_headers(run_id) + on delete restrict; + +create table programmable_private.run_lifecycle_outcomes ( + outcome_id uuid primary key, + run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + status programmable_private.run_status not null, + result_commitment programmable_private.bytes32_value not null, + caller_role name not null, + finished_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (run_id), + unique (outcome_id, run_id) +); + +create table programmable_private.run_telemetry ( + telemetry_id uuid primary key, + run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + sample_kind programmable_private.source_identifier not null, + sampled_at timestamptz not null, + duration_ms bigint check (duration_ms is null or duration_ms >= 0), + item_count bigint check (item_count is null or item_count >= 0), + diagnostic_sample jsonb, + failed_or_reorg boolean not null default false +); + +create index run_telemetry_retention_idx + on programmable_private.run_telemetry (failed_or_reorg, sampled_at, telemetry_id); + +create table programmable_private.safe_head_observations ( + observation_id uuid primary key, + epoch_id uuid not null, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + pointer_generation bigint not null check (pointer_generation > 0), + provider_a_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + reported_chain_id_a programmable_private.chain_id_value not null, + reported_chain_id_b programmable_private.chain_id_value not null, + head_a programmable_private.block_number_value not null, + head_b programmable_private.block_number_value not null, + finality_depth bigint not null check (finality_depth = 12), + safe_block_number programmable_private.block_number_value not null, + safe_block_hash_a programmable_private.bytes32_value not null, + safe_block_hash_b programmable_private.bytes32_value not null, + agreed_safe_block_hash programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version = 2), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verification_run_id uuid not null, + observed_at timestamptz not null, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) + on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) + on delete restrict, + check (provider_a_id <> provider_b_id), + check (reported_chain_id_a = chain_id and reported_chain_id_b = chain_id), + check (head_a >= 12 and head_b >= 12), + check (safe_block_number = least(head_a, head_b) - 12), + check ( + safe_block_hash_a = safe_block_hash_b + and safe_block_hash_a = agreed_safe_block_hash + ), + check ( + pg_catalog.octet_length(canonical_preimage) >= 35 + and pg_catalog.substring(canonical_preimage, 1, 35) + = pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320001', + 'hex' + ) + ), + unique (epoch_id, content_fingerprint), + unique (observation_id, epoch_id, chain_id, pointer_generation), + unique (observation_id, agreed_safe_block_hash) +); + +create table programmable_private.dual_rpc_block_evidence ( + block_evidence_id uuid primary key, + observation_id uuid not null, + epoch_id uuid not null, + chain_id programmable_private.chain_id_value not null, + pointer_generation bigint not null check (pointer_generation > 0), + block_number programmable_private.block_number_value not null, + provider_a_block_hash programmable_private.bytes32_value not null, + provider_b_block_hash programmable_private.bytes32_value not null, + agreed_block_hash programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version = 2), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + foreign key (observation_id, epoch_id, chain_id, pointer_generation) + references programmable_private.safe_head_observations( + observation_id, epoch_id, chain_id, pointer_generation + ) + on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) + on delete restrict, + check ( + provider_a_block_hash = provider_b_block_hash + and provider_a_block_hash = agreed_block_hash + ), + check ( + pg_catalog.octet_length(canonical_preimage) >= 35 + and pg_catalog.substring(canonical_preimage, 1, 35) + = pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320002', + 'hex' + ) + ), + unique (observation_id, block_number), + unique (epoch_id, content_fingerprint), + unique (block_evidence_id, epoch_id, chain_id, pointer_generation), + unique (block_evidence_id, observation_id, epoch_id, chain_id, pointer_generation), + unique (block_evidence_id, agreed_block_hash) +); + +create table programmable_private.projector_lease_current ( + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + lease_generation bigint not null check (lease_generation > 0), + lease_token_hash programmable_private.bytes32_value not null, + holder_id programmable_private.source_identifier not null, + acquired_at timestamptz not null, + expires_at timestamptz not null, + changed_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + primary key ( + chain_id, release_id, model_id, source_group, projector_version + ), + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) + on delete restrict, + check (expires_at > acquired_at and expires_at <= acquired_at + interval '10 minutes') +); + +create table programmable_private.projector_lease_history ( + lease_history_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + previous_generation bigint not null check (previous_generation >= 0), + lease_generation bigint not null check (lease_generation = previous_generation + 1), + lease_token_hash programmable_private.bytes32_value not null, + holder_id programmable_private.source_identifier not null, + acquired_at timestamptz not null, + expires_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique ( + chain_id, release_id, model_id, source_group, + projector_version, lease_generation + ) +); + +create table programmable_private.projector_checkpoints ( + checkpoint_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + lease_generation bigint not null check (lease_generation > 0), + checkpoint_generation bigint not null check (checkpoint_generation > 0), + reorg_generation bigint not null check (reorg_generation >= 0), + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + cursor_block_global_log_index + programmable_private.block_log_index_value not null, + cursor_candidate_id + programmable_private.envio_candidate_identifier not null, + safe_head_observation_id uuid not null, + target_block_evidence_id uuid not null, + run_id uuid not null, + terminal_outcome_id uuid not null, + created_at timestamptz not null, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) + on delete restrict, + foreign key ( + target_block_evidence_id, safe_head_observation_id, epoch_id, + chain_id, pointer_generation + ) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, observation_id, epoch_id, chain_id, pointer_generation + ) + on delete restrict, + foreign key (target_block_evidence_id, block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) + on delete restrict, + foreign key (terminal_outcome_id, run_id) + references programmable_private.run_lifecycle_outcomes(outcome_id, run_id) + on delete restrict, + unique ( + chain_id, release_id, model_id, source_group, + projector_version, checkpoint_generation + ), + unique (checkpoint_id, epoch_id, pointer_generation) +); + +create table programmable_private.projector_checkpoint_current ( + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + checkpoint_generation bigint not null check (checkpoint_generation > 0), + reorg_generation bigint not null check (reorg_generation >= 0), + changed_at timestamptz not null, + primary key ( + chain_id, release_id, model_id, source_group, projector_version + ) +); + +create table programmable_private.dependency_health_history ( + health_event_id uuid primary key, + dependency programmable_private.source_identifier not null, + circuit_status programmable_private.dependency_health_status not null, + failure_count integer not null check (failure_count >= 0), + observed_at timestamptz not null, + retry_after timestamptz, + detail_commitment programmable_private.bytes32_value not null, + run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + (circuit_status in ('open', 'half_open') and retry_after is not null) + or (circuit_status in ('closed', 'frozen')) + ) +); + +create table programmable_private.dependency_health_current ( + dependency programmable_private.source_identifier primary key, + health_event_id uuid not null + references programmable_private.dependency_health_history(health_event_id) + on delete restrict, + circuit_status programmable_private.dependency_health_status not null, + observed_at timestamptz not null +); + +create function programmable_private.append_mutation_audit( + p_action text, + p_input_commitment bytea, + p_run_id uuid default null, + p_occurred_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security invoker +set search_path = '' +as $function$ +declare + audit_id uuid := pg_catalog.gen_random_uuid(); +begin + if p_action is null + or pg_catalog.octet_length(p_action) not between 1 and 128 + or p_action !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_occurred_at is null + then + raise exception using errcode = '22023', message = 'invalid audit input'; + end if; + + insert into programmable_private.mutation_audits ( + audit_id, action, caller_role, input_commitment, run_id, occurred_at + ) + values ( + audit_id, + p_action::programmable_private.source_identifier, + programmable_private.caller_role_name(), + p_input_commitment::programmable_private.bytes32_value, + p_run_id, + p_occurred_at + ); + return audit_id; +end +$function$; + +create function programmable_private.assert_current_epoch( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_generation bigint +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +begin + if not exists ( + select 1 + from programmable_private.release_epoch_current as current_epoch + where current_epoch.chain_id = p_chain_id + and current_epoch.release_id = p_release_id + and current_epoch.model_id = p_model_id + and current_epoch.source_group = p_source_group + and current_epoch.epoch_id = p_epoch_id + and current_epoch.generation = p_generation + ) then + raise exception using + errcode = '40001', + message = 'stale release epoch or pointer generation'; + end if; +end +$function$; + +create function programmable_private.create_release_epoch( + p_epoch_id uuid, + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_number bigint, + p_epoch_commitment bytea, + p_artifact_creation_code_commitment bytea, + p_input_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_epoch_id is null + or p_chain_id <= 0 + or p_epoch_number <= 0 + or pg_catalog.octet_length(p_epoch_commitment) <> 32 + or pg_catalog.octet_length(p_artifact_creation_code_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid release epoch input'; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'release_epoch.create', p_input_commitment, null, p_created_at + ); + insert into programmable_private.release_epochs ( + epoch_id, chain_id, release_id, model_id, source_group, epoch_number, + epoch_commitment, artifact_creation_code_commitment, created_at, + created_by_audit_id + ) + values ( + p_epoch_id, + p_chain_id::programmable_private.chain_id_value, + p_release_id::programmable_private.release_identifier, + p_model_id::programmable_private.model_identifier, + p_source_group::programmable_private.source_identifier, + p_epoch_number, + p_epoch_commitment::programmable_private.bytes32_value, + p_artifact_creation_code_commitment::programmable_private.bytes32_value, + p_created_at, + audit_id + ); + return p_epoch_id; +end +$function$; + +create function programmable_private.append_release_source_binding( + p_binding_id uuid, + p_epoch_id uuid, + p_source_name text, + p_source_role text, + p_source_type text, + p_source_address bytea, + p_recovery_selector bytea, + p_inclusive_start_block numeric, + p_abi_event_set_commitment bytea, + p_artifact_creation_code_commitment bytea, + p_binding_commitment bytea, + p_input_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; + normalized_block bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_binding_id is null + or p_inclusive_start_block <> pg_catalog.trunc(p_inclusive_start_block) + or p_inclusive_start_block < 0 + or p_inclusive_start_block > 9223372036854775807 + or pg_catalog.octet_length(p_abi_event_set_commitment) <> 32 + or pg_catalog.octet_length(p_artifact_creation_code_commitment) <> 32 + or pg_catalog.octet_length(p_binding_commitment) <> 32 + or (p_recovery_selector is not null + and pg_catalog.octet_length(p_recovery_selector) <> 4) + then + raise exception using errcode = '22023', message = 'invalid release binding input'; + end if; + if not exists ( + select 1 + from programmable_private.release_epochs as epoch + where epoch.epoch_id = p_epoch_id + and epoch.artifact_creation_code_commitment + = p_artifact_creation_code_commitment + ) then + raise exception using + errcode = '23514', + message = 'release binding artifact commitment mismatch'; + end if; + if exists ( + select 1 + from programmable_private.release_epoch_current as current_epoch + where current_epoch.epoch_id = p_epoch_id + ) then + raise exception using + errcode = '55000', + message = 'active release epoch bindings are immutable'; + end if; + normalized_block := p_inclusive_start_block::bigint; + audit_id := programmable_private.append_mutation_audit( + 'release_binding.append', p_input_commitment, null, p_created_at + ); + insert into programmable_private.release_source_bindings ( + binding_id, epoch_id, source_name, source_role, source_type, source_address, + recovery_selector, + inclusive_start_block, abi_event_set_commitment, + artifact_creation_code_commitment, binding_commitment, created_at, + created_by_audit_id + ) + values ( + p_binding_id, + p_epoch_id, + p_source_name::programmable_private.source_identifier, + p_source_role::programmable_private.source_identifier, + p_source_type::programmable_private.source_type, + case when p_source_address is null then null + else p_source_address::programmable_private.eth_address end, + case when p_recovery_selector is null then null + else p_recovery_selector::programmable_private.hex_selector end, + normalized_block::programmable_private.block_number_value, + p_abi_event_set_commitment::programmable_private.bytes32_value, + p_artifact_creation_code_commitment::programmable_private.bytes32_value, + p_binding_commitment::programmable_private.bytes32_value, + p_created_at, + audit_id + ); + return p_binding_id; +end +$function$; + +create function programmable_private.activate_release_epoch( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_expected_generation bigint, + p_next_generation bigint, + p_input_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; + old_epoch_id uuid; + actual_generation bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_chain_id <= 0 + or p_expected_generation < 0 + or p_next_generation <> p_expected_generation + 1 + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid release pointer CAS'; + end if; + if not exists ( + select 1 + from programmable_private.release_epochs as epoch + where epoch.epoch_id = p_epoch_id + and epoch.chain_id = p_chain_id + and epoch.release_id = p_release_id + and epoch.model_id = p_model_id + and epoch.source_group = p_source_group + ) then + raise exception using errcode = '23503', message = 'epoch does not match release scope'; + end if; + + if p_expected_generation = 0 then + audit_id := programmable_private.append_mutation_audit( + 'release_epoch.activate', p_input_commitment, null, p_changed_at + ); + insert into programmable_private.release_epoch_current ( + chain_id, release_id, model_id, source_group, epoch_id, generation, + changed_at, changed_by_audit_id + ) + values ( + p_chain_id::programmable_private.chain_id_value, + p_release_id::programmable_private.release_identifier, + p_model_id::programmable_private.model_identifier, + p_source_group::programmable_private.source_identifier, + p_epoch_id, p_next_generation, p_changed_at, audit_id + ) + on conflict (chain_id, release_id, model_id, source_group) do nothing; + if not found then + raise exception using errcode = '40001', message = 'release pointer CAS lost'; + end if; + old_epoch_id := null; + else + select current_epoch.epoch_id, current_epoch.generation + into old_epoch_id, actual_generation + from programmable_private.release_epoch_current as current_epoch + where current_epoch.chain_id = p_chain_id + and current_epoch.release_id = p_release_id + and current_epoch.model_id = p_model_id + and current_epoch.source_group = p_source_group + for update; + if not found or actual_generation <> p_expected_generation then + raise exception using errcode = '40001', message = 'release pointer CAS lost'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'release_epoch.activate', p_input_commitment, null, p_changed_at + ); + update programmable_private.release_epoch_current + set epoch_id = p_epoch_id, + generation = p_next_generation, + changed_at = p_changed_at, + changed_by_audit_id = audit_id + where chain_id = p_chain_id + and release_id = p_release_id + and model_id = p_model_id + and source_group = p_source_group + and generation = p_expected_generation; + if not found then + raise exception using errcode = '40001', message = 'release pointer CAS lost'; + end if; + end if; + + insert into programmable_private.release_epoch_pointer_history ( + history_id, chain_id, release_id, model_id, source_group, + previous_epoch_id, next_epoch_id, previous_generation, next_generation, + changed_at, changed_by_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), + p_chain_id::programmable_private.chain_id_value, + p_release_id::programmable_private.release_identifier, + p_model_id::programmable_private.model_identifier, + p_source_group::programmable_private.source_identifier, + old_epoch_id, p_epoch_id, p_expected_generation, p_next_generation, + p_changed_at, audit_id + ); + return true; +end +$function$; + +create function programmable_private.register_provider_deployment( + p_provider_deployment_id uuid, + p_provider_type text, + p_redacted_identity text, + p_deployment_commitment bytea, + p_schema_commitment bytea, + p_input_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_provider_type = 'rpc_provider' then + raise exception using + errcode = '42501', + message = 'RPC providers require specialized deployment registration'; + end if; + if p_provider_deployment_id is null + or p_redacted_identity is null + or pg_catalog.octet_length(p_deployment_commitment) <> 32 + or pg_catalog.octet_length(p_schema_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid provider commitment'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'provider_deployment.register', p_input_commitment, null, p_created_at + ); + insert into programmable_private.provider_deployments ( + provider_deployment_id, provider_type, redacted_identity, + deployment_commitment, schema_commitment, created_at, created_by_audit_id + ) + values ( + p_provider_deployment_id, + p_provider_type::programmable_private.source_type, + p_redacted_identity::programmable_private.source_identifier, + p_deployment_commitment::programmable_private.bytes32_value, + p_schema_commitment::programmable_private.bytes32_value, + p_created_at, + audit_id + ); + return p_provider_deployment_id; +end +$function$; + +create function programmable_private.register_rpc_provider_deployment( + p_provider_deployment_id uuid, + p_chain_id bigint, + p_vendor text, + p_constructor_version text, + p_endpoint_url_commitment bytea, + p_endpoint_origin_commitment bytea, + p_endpoint_evidence_domain text, + p_endpoint_evidence_commitment bytea, + p_deployment_commitment bytea, + p_schema_commitment bytea, + p_input_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; + resolved_vendor_order smallint; + resolved_identity text; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_provider_deployment_id is null + or p_chain_id <> 1 + or p_vendor not in ('alchemy', 'quicknode') + or p_constructor_version is null + or p_endpoint_evidence_domain <> 'rpc-endpoint-commitments-v1' + or pg_catalog.octet_length(p_endpoint_url_commitment) <> 32 + or pg_catalog.octet_length(p_endpoint_origin_commitment) <> 32 + or pg_catalog.octet_length(p_endpoint_evidence_commitment) <> 32 + or pg_catalog.octet_length(p_deployment_commitment) <> 32 + or pg_catalog.octet_length(p_schema_commitment) <> 32 + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_endpoint_url_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_endpoint_origin_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_endpoint_evidence_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_deployment_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_schema_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_input_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + then + raise exception using + errcode = '22023', + message = 'invalid RPC provider deployment metadata'; + end if; + if not exists ( + select 1 + from programmable_private.rpc_endpoint_evidence_domains as domain_row + where domain_row.evidence_domain = p_endpoint_evidence_domain + and domain_row.enabled + ) then + raise exception using + errcode = '22023', + message = 'RPC endpoint evidence domain is not enabled'; + end if; + + resolved_vendor_order := case p_vendor + when 'alchemy' then 1::smallint + else 2::smallint + end; + resolved_identity := 'rpc:1:' || p_vendor; + audit_id := programmable_private.append_mutation_audit( + 'rpc_provider_deployment.register', p_input_commitment, null, p_created_at + ); + insert into programmable_private.provider_deployments ( + provider_deployment_id, provider_type, redacted_identity, + deployment_commitment, schema_commitment, created_at, created_by_audit_id + ) values ( + p_provider_deployment_id, 'rpc_provider', + resolved_identity::programmable_private.source_identifier, + p_deployment_commitment::programmable_private.bytes32_value, + p_schema_commitment::programmable_private.bytes32_value, + p_created_at, audit_id + ); + insert into programmable_private.rpc_provider_deployment_metadata ( + provider_deployment_id, chain_id, vendor, vendor_order, + constructor_version, endpoint_url_commitment, + endpoint_origin_commitment, endpoint_evidence_domain, + endpoint_evidence_commitment + ) values ( + p_provider_deployment_id, + p_chain_id::programmable_private.chain_id_value, + p_vendor::programmable_private.source_identifier, + resolved_vendor_order, + p_constructor_version::programmable_private.projector_identifier, + p_endpoint_url_commitment::programmable_private.bytes32_value, + p_endpoint_origin_commitment::programmable_private.bytes32_value, + p_endpoint_evidence_domain::programmable_private.source_identifier, + p_endpoint_evidence_commitment::programmable_private.bytes32_value + ); + return p_provider_deployment_id; +end +$function$; + +create function programmable_private.open_run( + p_run_id uuid, + p_run_kind text, + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_worker_version text, + p_request_commitment bytea, + p_started_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + kind programmable_private.run_kind; + expected_role name; + audit_id uuid; +begin + kind := p_run_kind::programmable_private.run_kind; + expected_role := case + when kind in ('ingestion', 'projection', 'rewind') then 'programmable_projector'::name + when kind = 'reconciliation' then 'programmable_reconciler'::name + when kind = 'maintenance' then 'programmable_maintenance'::name + else 'programmable_profile_recovery'::name + end; + perform programmable_private.assert_caller(expected_role); + perform programmable_private.assert_current_epoch( + p_chain_id, p_release_id, p_model_id, p_source_group, + p_epoch_id, p_pointer_generation + ); + if p_run_id is null or pg_catalog.octet_length(p_request_commitment) <> 32 then + raise exception using errcode = '22023', message = 'invalid run header'; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'run.open', p_request_commitment, null, p_started_at + ); + insert into programmable_private.run_headers ( + run_id, run_kind, chain_id, release_id, model_id, source_group, epoch_id, + captured_pointer_generation, worker_version, request_commitment, + caller_role, started_at, opened_by_audit_id + ) + values ( + p_run_id, kind, + p_chain_id::programmable_private.chain_id_value, + p_release_id::programmable_private.release_identifier, + p_model_id::programmable_private.model_identifier, + p_source_group::programmable_private.source_identifier, + p_epoch_id, p_pointer_generation, + p_worker_version::programmable_private.projector_identifier, + p_request_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), + p_started_at, + audit_id + ); + return p_run_id; +end +$function$; + +create function programmable_private.append_run_outcome( + p_outcome_id uuid, + p_run_id uuid, + p_status text, + p_result_commitment bytea, + p_finished_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + audit_id uuid; +begin + select * into header + from programmable_private.run_headers + where run_id = p_run_id + for update; + if not found then + raise exception using errcode = '23503', message = 'unknown run'; + end if; + perform programmable_private.assert_caller(header.caller_role); + if p_status = 'succeeded' then + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + end if; + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '23505', message = 'run is already terminal'; + end if; + if p_outcome_id is null or pg_catalog.octet_length(p_result_commitment) <> 32 then + raise exception using errcode = '22023', message = 'invalid run outcome'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'run.outcome.append', p_result_commitment, p_run_id, p_finished_at + ); + insert into programmable_private.run_lifecycle_outcomes ( + outcome_id, run_id, status, result_commitment, caller_role, + finished_at, audit_id + ) + values ( + p_outcome_id, p_run_id, p_status::programmable_private.run_status, + p_result_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_finished_at, audit_id + ); + return p_outcome_id; +end +$function$; + +create function programmable_private.append_run_telemetry( + p_telemetry_id uuid, + p_run_id uuid, + p_sample_kind text, + p_sampled_at timestamptz, + p_duration_ms bigint, + p_item_count bigint, + p_diagnostic_sample jsonb, + p_failed_or_reorg boolean +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + select * into header + from programmable_private.run_headers + where run_id = p_run_id; + if not found then + raise exception using errcode = '23503', message = 'unknown run'; + end if; + perform programmable_private.assert_caller(header.caller_role); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if p_duration_ms < 0 or p_item_count < 0 + or pg_catalog.octet_length(p_diagnostic_sample::text) > 8192 + then + raise exception using errcode = '22023', message = 'invalid telemetry sample'; + end if; + insert into programmable_private.run_telemetry ( + telemetry_id, run_id, sample_kind, sampled_at, duration_ms, item_count, + diagnostic_sample, failed_or_reorg + ) + values ( + p_telemetry_id, p_run_id, + p_sample_kind::programmable_private.source_identifier, + p_sampled_at, p_duration_ms, p_item_count, p_diagnostic_sample, + p_failed_or_reorg + ); + return p_telemetry_id; +end +$function$; + +create function programmable_private.append_safe_head_observation( + p_observation_id uuid, + p_run_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_reported_chain_id_a bigint, + p_reported_chain_id_b bigint, + p_head_a numeric, + p_head_b numeric, + p_finality_depth bigint, + p_safe_block_number numeric, + p_safe_block_hash_a bytea, + p_safe_block_hash_b bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_observed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + identity_a text; + identity_b text; + rpc_chain_id_a bigint; + rpc_chain_id_b bigint; + vendor_a text; + vendor_b text; + vendor_order_a smallint; + vendor_order_b smallint; + normalized_head_a bigint; + normalized_head_b bigint; + normalized_safe bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'safe_head', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection', 'rewind') + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid projector run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if p_provider_a_id = p_provider_b_id then + raise exception using errcode = '22023', message = 'RPC providers must differ'; + end if; + select deployment.redacted_identity, metadata.chain_id, + metadata.vendor, metadata.vendor_order + into identity_a, rpc_chain_id_a, vendor_a, vendor_order_a + from programmable_private.provider_deployments as deployment + join programmable_private.rpc_provider_deployment_metadata as metadata + on metadata.provider_deployment_id = deployment.provider_deployment_id + where deployment.provider_deployment_id = p_provider_a_id + and deployment.provider_type = 'rpc_provider'; + if not found then + raise exception using errcode = '22023', message = 'invalid first RPC deployment'; + end if; + select deployment.redacted_identity, metadata.chain_id, + metadata.vendor, metadata.vendor_order + into identity_b, rpc_chain_id_b, vendor_b, vendor_order_b + from programmable_private.provider_deployments as deployment + join programmable_private.rpc_provider_deployment_metadata as metadata + on metadata.provider_deployment_id = deployment.provider_deployment_id + where deployment.provider_deployment_id = p_provider_b_id + and deployment.provider_type = 'rpc_provider'; + if not found then + raise exception using errcode = '22023', message = 'invalid second RPC deployment'; + end if; + if identity_a = identity_b + or rpc_chain_id_a <> header.chain_id + or rpc_chain_id_b <> header.chain_id + or vendor_a <> 'alchemy' + or vendor_order_a <> 1 + or vendor_b <> 'quicknode' + or vendor_order_b <> 2 + then + raise exception using + errcode = '22023', + message = 'RPC deployments violate the canonical mainnet vendor order'; + end if; + if p_reported_chain_id_a <> header.chain_id + or p_reported_chain_id_b <> header.chain_id + or p_head_a <> pg_catalog.trunc(p_head_a) + or p_head_b <> pg_catalog.trunc(p_head_b) + or p_safe_block_number <> pg_catalog.trunc(p_safe_block_number) + or p_head_a < 12 or p_head_b < 12 + or p_head_a > 9223372036854775807 + or p_head_b > 9223372036854775807 + or p_safe_block_number < 0 + or p_safe_block_number > 9223372036854775807 + then + raise exception using errcode = '22023', message = 'invalid RPC chain/head observation'; + end if; + normalized_head_a := p_head_a::bigint; + normalized_head_b := p_head_b::bigint; + normalized_safe := p_safe_block_number::bigint; + if p_finality_depth <> 12 + or normalized_safe <> least(normalized_head_a, normalized_head_b) - 12 + or pg_catalog.octet_length(p_safe_block_hash_a) <> 32 + or p_safe_block_hash_a <> p_safe_block_hash_b + then + raise exception using errcode = '22023', message = 'dual-RPC safe-head gate failed'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'safe_head.append', p_content_fingerprint, p_run_id, p_observed_at + ); + insert into programmable_private.safe_head_observations ( + observation_id, epoch_id, chain_id, release_id, model_id, source_group, + pointer_generation, provider_a_id, provider_b_id, reported_chain_id_a, + reported_chain_id_b, head_a, head_b, finality_depth, safe_block_number, + safe_block_hash_a, safe_block_hash_b, agreed_safe_block_hash, + encoding_version, canonical_preimage, content_fingerprint, + verification_run_id, observed_at + ) + values ( + p_observation_id, header.epoch_id, header.chain_id, header.release_id, + header.model_id, header.source_group, header.captured_pointer_generation, + p_provider_a_id, p_provider_b_id, + p_reported_chain_id_a::programmable_private.chain_id_value, + p_reported_chain_id_b::programmable_private.chain_id_value, + normalized_head_a::programmable_private.block_number_value, + normalized_head_b::programmable_private.block_number_value, + p_finality_depth, + normalized_safe::programmable_private.block_number_value, + p_safe_block_hash_a::programmable_private.bytes32_value, + p_safe_block_hash_b::programmable_private.bytes32_value, + p_safe_block_hash_a::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_run_id, p_observed_at + ); + perform audit_id; + return p_observation_id; +end +$function$; + +create function programmable_private.append_dual_rpc_block_evidence( + p_block_evidence_id uuid, + p_observation_id uuid, + p_run_id uuid, + p_block_number numeric, + p_provider_a_block_hash bytea, + p_provider_b_block_hash bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + observation programmable_private.safe_head_observations%rowtype; + header programmable_private.run_headers%rowtype; + normalized_block bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'block', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_observation_id; + if not found then + raise exception using errcode = '23503', message = 'unknown safe-head observation'; + end if; + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection', 'rewind') + for share; + if not found + or header.epoch_id <> observation.epoch_id + or header.captured_pointer_generation <> observation.pointer_generation + then + raise exception using errcode = '23503', message = 'run and observation scope differ'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 + or p_block_number > observation.safe_block_number + or p_block_number > 9223372036854775807 + then + raise exception using errcode = '22023', message = 'block exceeds accepted safe head'; + end if; + normalized_block := p_block_number::bigint; + if pg_catalog.octet_length(p_provider_a_block_hash) <> 32 + or p_provider_a_block_hash <> p_provider_b_block_hash + then + raise exception using errcode = '22023', message = 'per-block RPC evidence disagrees'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'block_evidence.append', p_content_fingerprint, p_run_id, p_verified_at + ); + insert into programmable_private.dual_rpc_block_evidence ( + block_evidence_id, observation_id, epoch_id, chain_id, + pointer_generation, block_number, provider_a_block_hash, + provider_b_block_hash, agreed_block_hash, encoding_version, + canonical_preimage, content_fingerprint, verification_run_id, verified_at + ) + values ( + p_block_evidence_id, p_observation_id, observation.epoch_id, + observation.chain_id, observation.pointer_generation, + normalized_block::programmable_private.block_number_value, + p_provider_a_block_hash::programmable_private.bytes32_value, + p_provider_b_block_hash::programmable_private.bytes32_value, + p_provider_a_block_hash::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_run_id, p_verified_at + ); + perform audit_id; + return p_block_evidence_id; +end +$function$; + +create function programmable_private.acquire_projector_lease( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_projector_version text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_expected_lease_generation bigint, + p_next_lease_generation bigint, + p_lease_token_hash bytea, + p_holder_id text, + p_acquired_at timestamptz, + p_expires_at timestamptz, + p_input_commitment bytea +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + current_generation bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_current_epoch( + p_chain_id, p_release_id, p_model_id, p_source_group, + p_epoch_id, p_pointer_generation + ); + if p_expected_lease_generation < 0 + or p_next_lease_generation <> p_expected_lease_generation + 1 + or pg_catalog.octet_length(p_lease_token_hash) <> 32 + or p_expires_at <= p_acquired_at + or p_expires_at > p_acquired_at + interval '10 minutes' + then + raise exception using errcode = '22023', message = 'invalid projector lease CAS'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'projector_lease.acquire', p_input_commitment, null, p_acquired_at + ); + if p_expected_lease_generation = 0 then + insert into programmable_private.projector_lease_current ( + chain_id, release_id, model_id, source_group, projector_version, + epoch_id, pointer_generation, lease_generation, lease_token_hash, + holder_id, acquired_at, expires_at, changed_by_audit_id + ) + values ( + p_chain_id::programmable_private.chain_id_value, + p_release_id::programmable_private.release_identifier, + p_model_id::programmable_private.model_identifier, + p_source_group::programmable_private.source_identifier, + p_projector_version::programmable_private.projector_identifier, + p_epoch_id, p_pointer_generation, p_next_lease_generation, + p_lease_token_hash::programmable_private.bytes32_value, + p_holder_id::programmable_private.source_identifier, + p_acquired_at, p_expires_at, audit_id + ) + on conflict ( + chain_id, release_id, model_id, source_group, projector_version + ) do nothing; + if not found then + raise exception using errcode = '40001', message = 'projector lease CAS lost'; + end if; + else + select lease_generation into current_generation + from programmable_private.projector_lease_current + where chain_id = p_chain_id + and release_id = p_release_id + and model_id = p_model_id + and source_group = p_source_group + and projector_version = p_projector_version + for update; + if not found or current_generation <> p_expected_lease_generation then + raise exception using errcode = '40001', message = 'projector lease CAS lost'; + end if; + update programmable_private.projector_lease_current + set epoch_id = p_epoch_id, + pointer_generation = p_pointer_generation, + lease_generation = p_next_lease_generation, + lease_token_hash = p_lease_token_hash::programmable_private.bytes32_value, + holder_id = p_holder_id::programmable_private.source_identifier, + acquired_at = p_acquired_at, + expires_at = p_expires_at, + changed_by_audit_id = audit_id + where chain_id = p_chain_id + and release_id = p_release_id + and model_id = p_model_id + and source_group = p_source_group + and projector_version = p_projector_version + and lease_generation = p_expected_lease_generation; + if not found then + raise exception using errcode = '40001', message = 'projector lease CAS lost'; + end if; + end if; + insert into programmable_private.projector_lease_history ( + lease_history_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, previous_generation, + lease_generation, lease_token_hash, holder_id, acquired_at, expires_at, + audit_id + ) + values ( + pg_catalog.gen_random_uuid(), + p_chain_id::programmable_private.chain_id_value, + p_release_id::programmable_private.release_identifier, + p_model_id::programmable_private.model_identifier, + p_source_group::programmable_private.source_identifier, + p_projector_version::programmable_private.projector_identifier, + p_epoch_id, p_pointer_generation, p_expected_lease_generation, + p_next_lease_generation, + p_lease_token_hash::programmable_private.bytes32_value, + p_holder_id::programmable_private.source_identifier, + p_acquired_at, p_expires_at, audit_id + ); + return true; +end +$function$; + +create function programmable_private.append_dependency_health( + p_health_event_id uuid, + p_run_id uuid, + p_dependency text, + p_circuit_status text, + p_failure_count integer, + p_observed_at timestamptz, + p_retry_after timestamptz, + p_detail_commitment bytea +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if not exists ( + select 1 from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'reconciliation' + ) then + raise exception using errcode = '23503', message = 'invalid reconciliation run'; + end if; + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if p_failure_count < 0 or pg_catalog.octet_length(p_detail_commitment) <> 32 then + raise exception using errcode = '22023', message = 'invalid health evidence'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'dependency_health.append', p_detail_commitment, p_run_id, p_observed_at + ); + insert into programmable_private.dependency_health_history ( + health_event_id, dependency, circuit_status, failure_count, observed_at, + retry_after, detail_commitment, run_id, audit_id + ) + values ( + p_health_event_id, + p_dependency::programmable_private.source_identifier, + p_circuit_status::programmable_private.dependency_health_status, + p_failure_count, p_observed_at, p_retry_after, + p_detail_commitment::programmable_private.bytes32_value, + p_run_id, audit_id + ); + insert into programmable_private.dependency_health_current ( + dependency, health_event_id, circuit_status, observed_at + ) + values ( + p_dependency::programmable_private.source_identifier, + p_health_event_id, + p_circuit_status::programmable_private.dependency_health_status, + p_observed_at + ) + on conflict (dependency) do update + set health_event_id = excluded.health_event_id, + circuit_status = excluded.circuit_status, + observed_at = excluded.observed_at + where programmable_private.dependency_health_current.observed_at + < excluded.observed_at; + return p_health_event_id; +end +$function$; + +do $lockdown$ +declare + table_record record; +begin + for table_record in + select c.relname + from pg_catalog.pg_class as c + join pg_catalog.pg_namespace as n on n.oid = c.relnamespace + where n.nspname = 'programmable_private' + and c.relkind in ('r', 'p') + loop + execute pg_catalog.format( + 'alter table programmable_private.%I enable row level security', + table_record.relname + ); + execute pg_catalog.format( + 'alter table programmable_private.%I force row level security', + table_record.relname + ); + execute pg_catalog.format( + 'create policy migrator_owner_all on programmable_private.%I ' || + 'for all to programmable_migrator using (true) with check (true)', + table_record.relname + ); + end loop; +end +$lockdown$; + +revoke all on all tables in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all sequences in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all functions in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +grant usage on schema programmable_private + to programmable_projector, programmable_reconciler; + +grant execute on function programmable_private.create_release_epoch( + uuid, bigint, text, text, text, bigint, bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_release_source_binding( + uuid, uuid, text, text, text, bytea, bytea, numeric, + bytea, bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.activate_release_epoch( + bigint, text, text, text, uuid, bigint, bigint, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.register_provider_deployment( + uuid, text, text, bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.register_rpc_provider_deployment( + uuid, bigint, text, text, bytea, bytea, text, bytea, + bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.open_run( + uuid, text, bigint, text, text, text, uuid, bigint, text, bytea, timestamptz +) to programmable_projector, programmable_reconciler; +grant execute on function programmable_private.append_run_outcome( + uuid, uuid, text, bytea, timestamptz +) to programmable_projector, programmable_reconciler; +grant execute on function programmable_private.append_run_telemetry( + uuid, uuid, text, timestamptz, bigint, bigint, jsonb, boolean +) to programmable_projector, programmable_reconciler; +grant execute on function programmable_private.append_safe_head_observation( + uuid, uuid, uuid, uuid, bigint, bigint, numeric, numeric, bigint, numeric, + bytea, bytea, smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_dual_rpc_block_evidence( + uuid, uuid, uuid, numeric, bytea, bytea, smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.acquire_projector_lease( + bigint, text, text, text, text, uuid, bigint, bigint, bigint, bytea, + text, timestamptz, timestamptz, bytea +) to programmable_projector; +grant execute on function programmable_private.append_dependency_health( + uuid, uuid, text, text, integer, timestamptz, timestamptz, bytea +) to programmable_reconciler; + +reset role; diff --git a/supabase/migrations/20260731000300_event_occurrences_and_reward_seeds.sql b/supabase/migrations/20260731000300_event_occurrences_and_reward_seeds.sql new file mode 100644 index 00000000..92a5a387 --- /dev/null +++ b/supabase/migrations/20260731000300_event_occurrences_and_reward_seeds.sql @@ -0,0 +1,2135 @@ +-- Fork-aware logical events, immutable occurrences and reward-allocation seeds. + +set role programmable_migrator; + +create table programmable_private.envio_candidates ( + candidate_id programmable_private.envio_candidate_identifier primary key, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + transaction_hash programmable_private.bytes32_value not null, + transaction_index programmable_private.transaction_index_value not null, + block_global_log_index programmable_private.block_log_index_value not null, + source_address programmable_private.eth_address not null, + event_signature programmable_private.bytes32_value not null, + event_type programmable_private.source_identifier not null, + ordered_topics bytea[] not null, + raw_data bytea not null, + decoded_payload jsonb not null, + payload_hash programmable_private.bytes32_value not null, + provider_cursor programmable_private.envio_candidate_identifier not null, + provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + first_seen_run_id uuid not null, + first_seen_at timestamptz not null, + content_commitment programmable_private.bytes32_value not null, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) + on delete restrict, + foreign key (first_seen_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) + on delete restrict, + check (programmable_private.valid_topics(ordered_topics)), + check (pg_catalog.octet_length(decoded_payload::text) <= 65536), + check ( + candidate_id = programmable_private.derive_envio_candidate_id( + chain_id, block_hash, transaction_hash, block_global_log_index + ) + ), + check (provider_cursor = candidate_id), + unique (chain_id, block_hash, transaction_hash, block_global_log_index) +); + +create table programmable_private.chain_event_identities ( + logical_event_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + transaction_hash programmable_private.bytes32_value not null, + receipt_log_ordinal programmable_private.receipt_log_ordinal_value not null, + first_verification_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + created_at timestamptz not null, + unique (chain_id, transaction_hash, receipt_log_ordinal), + unique (logical_event_id, chain_id) +); + +create table programmable_private.chain_event_occurrences ( + occurrence_id uuid primary key, + logical_event_id uuid not null, + chain_id programmable_private.chain_id_value not null, + transaction_hash programmable_private.bytes32_value not null, + receipt_log_ordinal programmable_private.receipt_log_ordinal_value not null, + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + block_timestamp timestamptz not null, + transaction_index programmable_private.transaction_index_value not null, + source_address programmable_private.eth_address not null, + block_global_log_index programmable_private.block_log_index_value not null, + event_signature programmable_private.bytes32_value not null, + event_type programmable_private.source_identifier not null, + ordered_topics bytea[] not null, + raw_data bytea not null, + decoded_payload jsonb not null, + payload_hash programmable_private.bytes32_value not null, + decoder_version programmable_private.projector_identifier not null, + abi_event_set_commitment programmable_private.bytes32_value not null, + release_binding_id uuid not null + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + first_seen_envio_candidate_id + programmable_private.envio_candidate_identifier not null + references programmable_private.envio_candidates(candidate_id) + on delete restrict, + first_seen_provider_cursor + programmable_private.envio_candidate_identifier not null, + verification_run_id uuid not null, + block_evidence_id uuid not null, + encoding_version smallint not null check (encoding_version > 0), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + logical_event_id, chain_id + ) references programmable_private.chain_event_identities( + logical_event_id, chain_id + ) on delete restrict, + foreign key ( + block_evidence_id, block_hash + ) references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) + on delete restrict, + check (programmable_private.valid_topics(ordered_topics)), + check (pg_catalog.octet_length(decoded_payload::text) <= 65536), + check ( + pg_catalog.octet_length(canonical_preimage) >= 24 + and pg_catalog.substring(canonical_preimage, 1, 24) + = pg_catalog.decode( + '70726f6772616d6d61626c653a6f6363757272656e63653a', + 'hex' + ) + ), + unique (chain_id, transaction_hash, receipt_log_ordinal, block_hash), + unique (occurrence_id, logical_event_id, block_hash), + unique (occurrence_id, epoch_id, pointer_generation), + unique (epoch_id, content_fingerprint) +); + +create index chain_event_occurrences_order_idx + on programmable_private.chain_event_occurrences ( + chain_id, release_id, model_id, block_number, + transaction_index, receipt_log_ordinal + ); +create index chain_event_occurrences_source_idx + on programmable_private.chain_event_occurrences ( + epoch_id, source_address, event_signature, block_number + ); + +-- Chain placement and logical identity are global. Decoding, release binding, +-- ABI selection and verification evidence are exact release-epoch +-- materializations. This ledger is the authorization boundary for every +-- scoped projection writer; the release columns retained on the occurrence +-- row are only the immutable first-materialization snapshot. +create table programmable_private.chain_event_occurrence_materializations ( + materialization_id uuid primary key, + occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + release_binding_id uuid + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + dynamic_source_attestation_id uuid, + first_seen_envio_candidate_id programmable_private.envio_candidate_identifier, + first_seen_neutral_candidate_id + programmable_private.envio_candidate_identifier, + candidate_resolution_id uuid, + decoder_version programmable_private.projector_identifier not null, + event_type programmable_private.source_identifier not null, + abi_event_set_commitment programmable_private.bytes32_value not null, + decoded_payload jsonb not null, + payload_hash programmable_private.bytes32_value not null, + first_seen_provider_cursor + programmable_private.envio_candidate_identifier not null, + verification_run_id uuid not null, + block_evidence_id uuid not null, + encoding_version smallint not null check (encoding_version > 0), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + foreign key ( + block_evidence_id, epoch_id, chain_id, pointer_generation + ) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, epoch_id, chain_id, pointer_generation + ) on delete restrict, + check (pg_catalog.octet_length(decoded_payload::text) <= 65536), + check ( + (release_binding_id is null) <> (dynamic_source_attestation_id is null) + ), + check ( + ( + first_seen_envio_candidate_id is not null + and first_seen_neutral_candidate_id is null + and candidate_resolution_id is null + and dynamic_source_attestation_id is null + ) + or ( + first_seen_envio_candidate_id is null + and first_seen_neutral_candidate_id is not null + and candidate_resolution_id is not null + ) + ), + unique (occurrence_id, epoch_id, pointer_generation), + unique (materialization_id, occurrence_id, epoch_id, pointer_generation), + unique (candidate_resolution_id), + unique (epoch_id, content_fingerprint) +); + +create index chain_event_materializations_scope_idx + on programmable_private.chain_event_occurrence_materializations ( + epoch_id, pointer_generation, occurrence_id + ); + +create table programmable_private.chain_event_occurrence_status_history ( + status_history_id uuid primary key, + occurrence_id uuid not null, + logical_event_id uuid not null, + block_hash programmable_private.bytes32_value not null, + status programmable_private.occurrence_status not null, + safe_head_observation_id uuid not null + references programmable_private.safe_head_observations(observation_id) + on delete restrict, + block_evidence_id uuid not null + references programmable_private.dual_rpc_block_evidence(block_evidence_id) + on delete restrict, + decision_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + decision_commitment programmable_private.bytes32_value not null, + decided_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (occurrence_id, logical_event_id, block_hash) + references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) + on delete restrict, + unique (occurrence_id, status, decision_commitment) +); + +create table programmable_private.chain_event_current_canonical ( + logical_event_id uuid primary key + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + occurrence_id uuid not null unique, + block_hash programmable_private.bytes32_value not null, + status_history_id uuid not null unique + references programmable_private.chain_event_occurrence_status_history(status_history_id) + on delete restrict, + selected_by_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + selected_at timestamptz not null, + foreign key (occurrence_id, logical_event_id, block_hash) + references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) + on delete restrict +); + +create table programmable_private.reward_allocation_facts ( + allocation_fact_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + vault programmable_private.eth_address not null, + factory_occurrence_id uuid not null, + factory_release_binding_id uuid not null + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + factory_release_binding_commitment programmable_private.bytes32_value not null, + factory_logical_event_id uuid not null, + factory_occurrence_block_hash programmable_private.bytes32_value not null, + creation_block_number programmable_private.block_number_value not null, + creation_transaction_index programmable_private.transaction_index_value not null, + ordered_beneficiaries bytea[] not null, + ordered_shares_bps integer[] not null, + allocation_hash programmable_private.bytes32_value not null, + configuration_hash programmable_private.bytes32_value not null, + active_configuration_hash programmable_private.bytes32_value, + manifest_artifact_creation_code_commitment programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version > 0), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verification_run_id uuid not null, + created_at timestamptz not null, + foreign key ( + factory_occurrence_id, factory_logical_event_id, factory_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) + on delete restrict, + check ( + programmable_private.valid_beneficiary_set( + ordered_beneficiaries, + ordered_shares_bps, + case when model_id like 'classic%' then 5 else 8 end + ) + ), + check ( + (model_id like 'classic%' and active_configuration_hash is not null) + or (model_id not like 'classic%' and active_configuration_hash is null) + ), + check ( + pg_catalog.octet_length(canonical_preimage) >= 24 + and pg_catalog.substring(canonical_preimage, 1, 24) + = pg_catalog.decode( + '70726f6772616d6d61626c653a616c6c6f636174696f6e3a', + 'hex' + ) + ), + unique ( + chain_id, release_id, vault, factory_occurrence_id, + allocation_hash, configuration_hash + ), + unique (epoch_id, content_fingerprint), + unique (allocation_fact_id, factory_occurrence_id, vault) +); + +create table programmable_private.reward_allocation_required_occurrences ( + allocation_fact_id uuid not null + references programmable_private.reward_allocation_facts(allocation_fact_id) + on delete restrict, + occurrence_ordinal integer not null check (occurrence_ordinal >= 0), + occurrence_role programmable_private.source_identifier not null, + occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + release_binding_id uuid not null + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + release_binding_commitment programmable_private.bytes32_value not null, + primary key (allocation_fact_id, occurrence_ordinal), + unique (allocation_fact_id, occurrence_id) +); + +create table programmable_private.reward_allocation_evidence ( + allocation_evidence_id uuid primary key, + allocation_fact_id uuid not null, + factory_occurrence_id uuid not null, + vault programmable_private.eth_address not null, + recovery_method programmable_private.recovery_method not null, + evidence_version programmable_private.projector_identifier not null, + recovery_release_binding_id uuid not null + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + recovery_release_binding_commitment programmable_private.bytes32_value not null, + top_level_destination programmable_private.eth_address, + method_selector programmable_private.hex_selector, + transaction_input_hash programmable_private.bytes32_value, + recomputed_allocation_hash programmable_private.bytes32_value not null, + recomputed_configuration_hash programmable_private.bytes32_value not null, + recomputed_active_configuration_hash programmable_private.bytes32_value, + is_recomputation_attested boolean not null, + constructor_arguments_commitment programmable_private.bytes32_value not null, + local_init_code_hash programmable_private.bytes32_value not null, + create2_salt programmable_private.bytes32_value not null, + local_create2_address programmable_private.eth_address not null, + historical_enrichment_status programmable_private.historical_enrichment_status not null, + getter_block_hash programmable_private.bytes32_value, + getter_result_hash_a programmable_private.bytes32_value, + getter_result_hash_b programmable_private.bytes32_value, + predict_result_hash_a programmable_private.bytes32_value, + predict_result_hash_b programmable_private.bytes32_value, + predicted_vault_a programmable_private.eth_address, + predicted_vault_b programmable_private.eth_address, + selected_rpc_result_hash_a programmable_private.bytes32_value not null, + selected_rpc_result_hash_b programmable_private.bytes32_value not null, + selected_rpc_transaction_receipt_hash_a programmable_private.bytes32_value, + selected_rpc_transaction_receipt_hash_b programmable_private.bytes32_value, + encoding_version smallint not null check (encoding_version > 0), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verification_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + verified_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (allocation_fact_id, factory_occurrence_id, vault) + references programmable_private.reward_allocation_facts( + allocation_fact_id, factory_occurrence_id, vault + ) + on delete restrict, + check (local_create2_address = vault), + check (selected_rpc_result_hash_a = selected_rpc_result_hash_b), + check ( + ( + recovery_method = 'historical_getters' + and top_level_destination is null + and method_selector is null + and transaction_input_hash is null + and historical_enrichment_status = 'matched' + and getter_block_hash is not null + and getter_result_hash_a is not null + and getter_result_hash_a = getter_result_hash_b + and predict_result_hash_a is not null + and predict_result_hash_a = predict_result_hash_b + and predicted_vault_a is not null + and predicted_vault_b is not null + and predicted_vault_a = vault + and predicted_vault_b = vault + and selected_rpc_transaction_receipt_hash_a is null + and selected_rpc_transaction_receipt_hash_b is null + ) + or ( + recovery_method <> 'historical_getters' + and top_level_destination is not null + and method_selector is not null + and transaction_input_hash is not null + and selected_rpc_transaction_receipt_hash_a is not null + and selected_rpc_transaction_receipt_hash_a + = selected_rpc_transaction_receipt_hash_b + and ( + ( + historical_enrichment_status = 'matched' + and getter_block_hash is not null + and getter_result_hash_a is not null + and getter_result_hash_a = getter_result_hash_b + and predict_result_hash_a is not null + and predict_result_hash_a = predict_result_hash_b + and predicted_vault_a is not null + and predicted_vault_b is not null + and predicted_vault_a = vault + and predicted_vault_b = vault + ) + or ( + historical_enrichment_status = 'unavailable' + and getter_block_hash is null + and getter_result_hash_a is null + and getter_result_hash_b is null + and predict_result_hash_a is null + and predict_result_hash_b is null + and predicted_vault_a is null + and predicted_vault_b is null + ) + ) + ) + ), + check ( + pg_catalog.octet_length(canonical_preimage) >= 22 + and pg_catalog.substring(canonical_preimage, 1, 22) + = pg_catalog.decode( + '70726f6772616d6d61626c653a65766964656e63653a', + 'hex' + ) + ), + unique (allocation_fact_id, recovery_method, evidence_version, content_fingerprint), + unique (allocation_evidence_id, allocation_fact_id) +); + +create table programmable_private.reward_allocation_status_history ( + seed_status_history_id uuid primary key, + allocation_fact_id uuid not null + references programmable_private.reward_allocation_facts(allocation_fact_id) + on delete restrict, + allocation_evidence_id uuid, + status programmable_private.reward_seed_status not null, + reason_commitment programmable_private.bytes32_value not null, + decision_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + decided_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (allocation_evidence_id, allocation_fact_id) + references programmable_private.reward_allocation_evidence( + allocation_evidence_id, allocation_fact_id + ) + on delete restrict, + unique (allocation_evidence_id, status, reason_commitment) +); + +create table programmable_private.reward_allocation_mismatch_evidence ( + mismatch_evidence_id uuid primary key, + allocation_fact_id uuid not null + references programmable_private.reward_allocation_facts(allocation_fact_id) + on delete restrict, + recovery_method text not null, + observed_destination bytea, + observed_selector bytea, + observed_transaction_input_hash bytea, + observed_constructor_arguments_commitment bytea, + observed_local_init_code_hash bytea, + observed_create2_salt bytea, + observed_local_create2_address bytea, + observed_allocation_hash bytea, + observed_configuration_hash bytea, + observed_active_configuration_hash bytea, + mismatch_commitment programmable_private.bytes32_value not null, + verification_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + recorded_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (allocation_fact_id, mismatch_commitment) +); + +create table programmable_private.reward_allocation_current_verified ( + factory_occurrence_id uuid not null, + vault programmable_private.eth_address not null, + allocation_fact_id uuid not null, + allocation_evidence_id uuid not null, + seed_status_history_id uuid not null unique + references programmable_private.reward_allocation_status_history(seed_status_history_id) + on delete restrict, + selected_by_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + selected_at timestamptz not null, + primary key (factory_occurrence_id, vault), + unique (allocation_fact_id), + foreign key (allocation_fact_id, factory_occurrence_id, vault) + references programmable_private.reward_allocation_facts( + allocation_fact_id, factory_occurrence_id, vault + ) + on delete restrict, + foreign key (allocation_evidence_id, allocation_fact_id) + references programmable_private.reward_allocation_evidence( + allocation_evidence_id, allocation_fact_id + ) + on delete restrict +); + +create index reward_allocation_facts_vault_idx + on programmable_private.reward_allocation_facts ( + chain_id, release_id, vault, creation_block_number + ); + +create function programmable_private.append_envio_candidate( + p_candidate_id text, + p_run_id uuid, + p_block_number numeric, + p_block_hash bytea, + p_transaction_hash bytea, + p_transaction_index numeric, + p_block_global_log_index numeric, + p_source_address bytea, + p_event_signature bytea, + p_event_type text, + p_ordered_topics bytea[], + p_raw_data bytea, + p_decoded_payload jsonb, + p_payload_hash bytea, + p_provider_cursor text, + p_provider_deployment_id uuid, + p_content_commitment bytea, + p_first_seen_at timestamptz default pg_catalog.clock_timestamp() +) +returns text +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + existing programmable_private.envio_candidates%rowtype; + normalized_block bigint; + normalized_tx_index bigint; + normalized_log_index bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion' + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid ingestion run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if p_block_number <> pg_catalog.trunc(p_block_number) + or p_transaction_index <> pg_catalog.trunc(p_transaction_index) + or p_block_global_log_index <> pg_catalog.trunc(p_block_global_log_index) + or p_block_number < 0 or p_block_number > 9223372036854775807 + or p_transaction_index < 0 or p_transaction_index > 4294967295 + or p_block_global_log_index < 0 or p_block_global_log_index > 4294967295 + or pg_catalog.octet_length(p_block_hash) <> 32 + or pg_catalog.octet_length(p_transaction_hash) <> 32 + or pg_catalog.octet_length(p_source_address) <> 20 + or pg_catalog.octet_length(p_event_signature) <> 32 + or not programmable_private.valid_topics(p_ordered_topics) + or p_raw_data is null + or pg_catalog.octet_length(p_payload_hash) <> 32 + or pg_catalog.octet_length(p_content_commitment) <> 32 + or pg_catalog.octet_length(p_decoded_payload::text) > 65536 + or p_candidate_id is distinct from + programmable_private.derive_envio_candidate_id( + header.chain_id, p_block_hash, p_transaction_hash, + p_block_global_log_index + )::text + or p_provider_cursor is distinct from p_candidate_id + then + raise exception using errcode = '22023', message = 'invalid Envio candidate'; + end if; + normalized_block := p_block_number::bigint; + normalized_tx_index := p_transaction_index::bigint; + normalized_log_index := p_block_global_log_index::bigint; + select * into existing + from programmable_private.envio_candidates + where candidate_id = p_candidate_id; + if found then + if existing.epoch_id <> header.epoch_id + or existing.pointer_generation <> header.captured_pointer_generation + or existing.block_number <> normalized_block + or existing.block_hash <> p_block_hash + or existing.transaction_hash <> p_transaction_hash + or existing.transaction_index <> normalized_tx_index + or existing.block_global_log_index <> normalized_log_index + or existing.source_address <> p_source_address + or existing.event_signature <> p_event_signature + or existing.event_type <> p_event_type + or existing.ordered_topics <> p_ordered_topics + or existing.raw_data <> p_raw_data + or existing.decoded_payload <> p_decoded_payload + or existing.payload_hash <> p_payload_hash + or existing.provider_cursor <> p_provider_cursor + or existing.provider_deployment_id <> p_provider_deployment_id + or existing.content_commitment <> p_content_commitment + then + raise exception using errcode = '23505', message = 'candidate replay changed immutable content'; + end if; + return existing.candidate_id; + end if; + insert into programmable_private.envio_candidates ( + candidate_id, epoch_id, pointer_generation, chain_id, release_id, model_id, + source_group, block_number, block_hash, transaction_hash, + transaction_index, block_global_log_index, source_address, + event_signature, event_type, ordered_topics, raw_data, decoded_payload, + payload_hash, provider_cursor, provider_deployment_id, first_seen_run_id, + first_seen_at, content_commitment + ) + values ( + p_candidate_id::programmable_private.envio_candidate_identifier, + header.epoch_id, header.captured_pointer_generation, header.chain_id, + header.release_id, header.model_id, header.source_group, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + p_transaction_hash::programmable_private.bytes32_value, + normalized_tx_index::programmable_private.transaction_index_value, + normalized_log_index::programmable_private.block_log_index_value, + p_source_address::programmable_private.eth_address, + p_event_signature::programmable_private.bytes32_value, + p_event_type::programmable_private.source_identifier, + p_ordered_topics, p_raw_data, p_decoded_payload, + p_payload_hash::programmable_private.bytes32_value, + p_provider_cursor::programmable_private.envio_candidate_identifier, + p_provider_deployment_id, p_run_id, p_first_seen_at, + p_content_commitment::programmable_private.bytes32_value + ); + perform programmable_private.append_mutation_audit( + 'candidate.append', p_content_commitment, p_run_id, p_first_seen_at + ); + return p_candidate_id; +end +$function$; + +create function programmable_private.append_chain_event_occurrence( + p_logical_event_id uuid, + p_occurrence_id uuid, + p_run_id uuid, + p_candidate_id text, + p_receipt_log_ordinal numeric, + p_block_timestamp timestamptz, + p_decoder_version text, + p_abi_event_set_commitment bytea, + p_block_evidence_id uuid, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + candidate programmable_private.envio_candidates%rowtype; + evidence programmable_private.dual_rpc_block_evidence%rowtype; + identity programmable_private.chain_event_identities%rowtype; + existing programmable_private.chain_event_occurrences%rowtype; + materialization programmable_private.chain_event_occurrence_materializations%rowtype; + source_binding programmable_private.release_source_bindings%rowtype; + ordinal bigint; + audit_id uuid; + status_id uuid; + occurrence_inserted boolean := false; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_fingerprint_encoding( + 'occurrence', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection') + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid verification run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into candidate + from programmable_private.envio_candidates + where candidate_id = p_candidate_id; + if not found + or candidate.epoch_id <> header.epoch_id + or candidate.pointer_generation <> header.captured_pointer_generation + then + raise exception using errcode = '23503', message = 'candidate scope mismatch'; + end if; + select * into evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id; + if not found + or evidence.epoch_id <> header.epoch_id + or evidence.pointer_generation <> header.captured_pointer_generation + or evidence.block_number <> candidate.block_number + or evidence.agreed_block_hash <> candidate.block_hash + then + raise exception using errcode = '23503', message = 'candidate lacks matching dual-RPC block evidence'; + end if; + select * into source_binding + from programmable_private.release_source_bindings as binding + join programmable_private.release_epochs as epoch + on epoch.epoch_id = binding.epoch_id + where binding.epoch_id = header.epoch_id + and binding.source_type = 'ethereum_contract' + and binding.source_address = candidate.source_address + and binding.inclusive_start_block <= candidate.block_number + and binding.abi_event_set_commitment = p_abi_event_set_commitment + and binding.artifact_creation_code_commitment + = epoch.artifact_creation_code_commitment; + if not found then + raise exception using + errcode = '23514', + message = 'candidate is not covered by the active release source manifest'; + end if; + if p_receipt_log_ordinal <> pg_catalog.trunc(p_receipt_log_ordinal) + or p_receipt_log_ordinal < 0 + or p_receipt_log_ordinal > 4294967295 + or pg_catalog.octet_length(p_abi_event_set_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid occurrence encoding or ordinal'; + end if; + ordinal := p_receipt_log_ordinal::bigint; + select * into identity + from programmable_private.chain_event_identities + where chain_id = header.chain_id + and transaction_hash = candidate.transaction_hash + and receipt_log_ordinal = ordinal + for share; + if found and identity.logical_event_id <> p_logical_event_id then + raise exception using errcode = '23505', message = 'logical identity UUID changed'; + elsif not found then + insert into programmable_private.chain_event_identities ( + logical_event_id, chain_id, transaction_hash, receipt_log_ordinal, + first_verification_run_id, created_at + ) + values ( + p_logical_event_id, header.chain_id, candidate.transaction_hash, + ordinal::programmable_private.receipt_log_ordinal_value, + p_run_id, p_verified_at + ); + end if; + select * into existing + from programmable_private.chain_event_occurrences + where chain_id = header.chain_id + and transaction_hash = candidate.transaction_hash + and receipt_log_ordinal = ordinal + and block_hash = candidate.block_hash; + if found then + if existing.occurrence_id <> p_occurrence_id + or existing.logical_event_id <> p_logical_event_id + or existing.block_number <> candidate.block_number + or existing.block_timestamp <> p_block_timestamp + or existing.transaction_index <> candidate.transaction_index + or existing.source_address <> candidate.source_address + or existing.block_global_log_index <> candidate.block_global_log_index + or existing.event_signature <> candidate.event_signature + or existing.ordered_topics <> candidate.ordered_topics + or existing.raw_data <> candidate.raw_data + then + raise exception using errcode = '23505', message = 'raw occurrence replay changed immutable chain data'; + end if; + else + insert into programmable_private.chain_event_occurrences ( + occurrence_id, logical_event_id, chain_id, transaction_hash, + receipt_log_ordinal, block_number, block_hash, block_timestamp, + transaction_index, source_address, block_global_log_index, + event_signature, event_type, ordered_topics, raw_data, decoded_payload, + payload_hash, decoder_version, abi_event_set_commitment, + release_binding_id, release_id, + model_id, epoch_id, pointer_generation, first_seen_envio_candidate_id, + first_seen_provider_cursor, verification_run_id, block_evidence_id, + encoding_version, canonical_preimage, content_fingerprint, verified_at + ) + values ( + p_occurrence_id, p_logical_event_id, header.chain_id, + candidate.transaction_hash, + ordinal::programmable_private.receipt_log_ordinal_value, + candidate.block_number, candidate.block_hash, p_block_timestamp, + candidate.transaction_index, candidate.source_address, + candidate.block_global_log_index, candidate.event_signature, + candidate.event_type, candidate.ordered_topics, candidate.raw_data, + candidate.decoded_payload, candidate.payload_hash, + p_decoder_version::programmable_private.projector_identifier, + p_abi_event_set_commitment::programmable_private.bytes32_value, + source_binding.binding_id, + header.release_id, header.model_id, header.epoch_id, + header.captured_pointer_generation, candidate.candidate_id, + candidate.provider_cursor, p_run_id, p_block_evidence_id, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, p_verified_at + ); + occurrence_inserted := true; + end if; + + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if found then + if materialization.chain_id <> header.chain_id + or materialization.release_id <> header.release_id + or materialization.model_id <> header.model_id + or materialization.source_group <> header.source_group + or materialization.release_binding_id <> source_binding.binding_id + or materialization.dynamic_source_attestation_id is not null + or materialization.first_seen_envio_candidate_id <> p_candidate_id + or materialization.first_seen_neutral_candidate_id is not null + or materialization.candidate_resolution_id is not null + or materialization.decoder_version <> p_decoder_version + or materialization.event_type <> candidate.event_type + or materialization.abi_event_set_commitment <> p_abi_event_set_commitment + or materialization.decoded_payload <> candidate.decoded_payload + or materialization.payload_hash <> candidate.payload_hash + or materialization.first_seen_provider_cursor <> candidate.provider_cursor + or materialization.verification_run_id <> p_run_id + or materialization.block_evidence_id <> p_block_evidence_id + or materialization.encoding_version <> p_encoding_version + or materialization.canonical_preimage <> p_canonical_preimage + or materialization.content_fingerprint <> p_content_fingerprint + then + raise exception using errcode = '23505', message = 'occurrence materialization replay changed exact scope'; + end if; + return p_occurrence_id; + end if; + insert into programmable_private.chain_event_occurrence_materializations ( + materialization_id, occurrence_id, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, release_binding_id, + dynamic_source_attestation_id, first_seen_envio_candidate_id, + first_seen_neutral_candidate_id, candidate_resolution_id, + decoder_version, event_type, abi_event_set_commitment, + decoded_payload, payload_hash, + first_seen_provider_cursor, verification_run_id, block_evidence_id, + encoding_version, canonical_preimage, content_fingerprint, verified_at + ) values ( + case when occurrence_inserted then p_occurrence_id + else pg_catalog.gen_random_uuid() end, + p_occurrence_id, header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, header.captured_pointer_generation, + source_binding.binding_id, null, candidate.candidate_id, null, null, + p_decoder_version::programmable_private.projector_identifier, + candidate.event_type, + p_abi_event_set_commitment::programmable_private.bytes32_value, + candidate.decoded_payload, candidate.payload_hash, candidate.provider_cursor, + p_run_id, p_block_evidence_id, p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, p_verified_at + ); + audit_id := programmable_private.append_mutation_audit( + case when occurrence_inserted then 'occurrence.append' + else 'occurrence.materialize' end, + p_content_fingerprint, p_run_id, p_verified_at + ); + if not occurrence_inserted then + return p_occurrence_id; + end if; + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.chain_event_occurrence_status_history ( + status_history_id, occurrence_id, logical_event_id, block_hash, status, + safe_head_observation_id, block_evidence_id, decision_run_id, + decision_commitment, decided_at, audit_id + ) + values ( + status_id, p_occurrence_id, p_logical_event_id, candidate.block_hash, + 'observed', evidence.observation_id, p_block_evidence_id, p_run_id, + p_content_fingerprint::programmable_private.bytes32_value, + p_verified_at, audit_id + ); + return p_occurrence_id; +end +$function$; + +create function programmable_private.append_reward_allocation_fact( + p_allocation_fact_id uuid, + p_run_id uuid, + p_vault bytea, + p_factory_occurrence_id uuid, + p_ordered_beneficiaries bytea[], + p_ordered_shares_bps numeric[], + p_allocation_hash bytea, + p_configuration_hash bytea, + p_active_configuration_hash bytea, + p_manifest_artifact_creation_code_commitment bytea, + p_required_occurrence_ids uuid[], + p_required_occurrence_roles text[], + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + factory_occurrence programmable_private.chain_event_occurrences%rowtype; + factory_materialization programmable_private.chain_event_occurrence_materializations%rowtype; + factory_binding programmable_private.release_source_bindings%rowtype; + existing programmable_private.reward_allocation_facts%rowtype; + shares integer[]; + required_id uuid; + required_role text; + ordinal integer := 0; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_fingerprint_encoding( + 'allocation', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection') + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid allocation run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into factory_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = p_factory_occurrence_id; + select * into factory_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_factory_occurrence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found + or factory_occurrence.occurrence_id is null + or factory_occurrence.chain_id <> header.chain_id + or not exists ( + select 1 + from programmable_private.chain_event_current_canonical as selected + where selected.occurrence_id = p_factory_occurrence_id + ) + then + raise exception using errcode = '23503', message = 'factory occurrence is not current canonical'; + end if; + select * into factory_binding + from programmable_private.release_source_bindings + where binding_id = factory_materialization.release_binding_id; + if not found + or factory_binding.epoch_id <> header.epoch_id + or factory_binding.source_role <> 'vault_factory' + or factory_binding.source_address <> factory_occurrence.source_address + or factory_binding.abi_event_set_commitment + <> factory_materialization.abi_event_set_commitment + or factory_binding.inclusive_start_block > factory_occurrence.block_number + then + raise exception using + errcode = '23514', + message = 'vault factory occurrence lacks its exact release binding'; + end if; + if coalesce(pg_catalog.array_length(p_ordered_shares_bps, 1), 0) + <> coalesce(pg_catalog.array_length(p_ordered_beneficiaries, 1), 0) + or exists ( + select 1 from pg_catalog.unnest(p_ordered_shares_bps) as share + where share <> pg_catalog.trunc(share) + ) + or coalesce(pg_catalog.array_length(p_required_occurrence_ids, 1), 0) = 0 + or pg_catalog.array_length(p_required_occurrence_ids, 1) + <> pg_catalog.array_length(p_required_occurrence_roles, 1) + or pg_catalog.octet_length(p_vault) <> 20 + or pg_catalog.octet_length(p_allocation_hash) <> 32 + or pg_catalog.octet_length(p_configuration_hash) <> 32 + or pg_catalog.octet_length(p_manifest_artifact_creation_code_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid allocation fact'; + end if; + select pg_catalog.array_agg(share::integer order by ord) + into shares + from pg_catalog.unnest(p_ordered_shares_bps) with ordinality as item(share, ord); + if not programmable_private.valid_beneficiary_set( + p_ordered_beneficiaries, shares, + case when header.model_id like 'classic%' then 5 else 8 end + ) then + raise exception using errcode = '22023', message = 'invalid beneficiary allocation'; + end if; + if ( + header.model_id like 'classic%' + and ( + p_active_configuration_hash is null + or pg_catalog.octet_length(p_active_configuration_hash) <> 32 + ) + ) + or (header.model_id not like 'classic%' and p_active_configuration_hash is not null) + then + raise exception using errcode = '22023', message = 'invalid active configuration commitment'; + end if; + if not exists ( + select 1 + from programmable_private.release_epochs + where epoch_id = header.epoch_id + and artifact_creation_code_commitment = p_manifest_artifact_creation_code_commitment + ) then + raise exception using errcode = '23514', message = 'manifest artifact/init-code commitment mismatch'; + end if; + if p_required_occurrence_roles + is distinct from array['launcher', 'vault_factory', 'hook']::text[] + then + raise exception using + errcode = '23514', + message = 'complete ordered launcher/vault-factory/hook occurrences are required'; + end if; + select * into existing + from programmable_private.reward_allocation_facts + where chain_id = header.chain_id + and release_id = header.release_id + and vault = p_vault + and factory_occurrence_id = p_factory_occurrence_id + and allocation_hash = p_allocation_hash + and configuration_hash = p_configuration_hash; + if found then + if existing.allocation_fact_id <> p_allocation_fact_id + or existing.ordered_beneficiaries <> p_ordered_beneficiaries + or existing.ordered_shares_bps <> shares + or existing.active_configuration_hash is distinct from p_active_configuration_hash + or existing.manifest_artifact_creation_code_commitment + <> p_manifest_artifact_creation_code_commitment + or existing.factory_release_binding_id <> factory_binding.binding_id + or existing.factory_release_binding_commitment + <> factory_binding.binding_commitment + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + or ( + select pg_catalog.array_agg( + required.occurrence_id order by required.occurrence_ordinal + ) + from programmable_private.reward_allocation_required_occurrences + as required + where required.allocation_fact_id = existing.allocation_fact_id + ) is distinct from p_required_occurrence_ids + or ( + select pg_catalog.array_agg( + required.occurrence_role::text order by required.occurrence_ordinal + ) + from programmable_private.reward_allocation_required_occurrences + as required + where required.allocation_fact_id = existing.allocation_fact_id + ) is distinct from p_required_occurrence_roles + then + raise exception using errcode = '23505', message = 'allocation replay changed immutable content'; + end if; + return existing.allocation_fact_id; + end if; + insert into programmable_private.reward_allocation_facts ( + allocation_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, vault, factory_occurrence_id, + factory_release_binding_id, factory_release_binding_commitment, + factory_logical_event_id, factory_occurrence_block_hash, + creation_block_number, creation_transaction_index, ordered_beneficiaries, + ordered_shares_bps, allocation_hash, configuration_hash, + active_configuration_hash, manifest_artifact_creation_code_commitment, + encoding_version, canonical_preimage, content_fingerprint, + verification_run_id, created_at + ) + values ( + p_allocation_fact_id, header.chain_id, header.release_id, header.model_id, + header.epoch_id, header.captured_pointer_generation, + p_vault::programmable_private.eth_address, p_factory_occurrence_id, + factory_binding.binding_id, factory_binding.binding_commitment, + factory_occurrence.logical_event_id, factory_occurrence.block_hash, + factory_occurrence.block_number, factory_occurrence.transaction_index, + p_ordered_beneficiaries, shares, + p_allocation_hash::programmable_private.bytes32_value, + p_configuration_hash::programmable_private.bytes32_value, + case when p_active_configuration_hash is null then null + else p_active_configuration_hash::programmable_private.bytes32_value end, + p_manifest_artifact_creation_code_commitment::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_run_id, p_created_at + ); + for required_id, required_role in + select ids.id, roles.role + from pg_catalog.unnest(p_required_occurrence_ids) with ordinality as ids(id, ord) + join pg_catalog.unnest(p_required_occurrence_roles) with ordinality as roles(role, ord) + using (ord) + order by ids.ord + loop + if not exists ( + select 1 + from programmable_private.chain_event_occurrences as required_occurrence + join programmable_private.chain_event_occurrence_materializations + as required_materialization + on required_materialization.occurrence_id = + required_occurrence.occurrence_id + join programmable_private.chain_event_current_canonical as selected + on selected.occurrence_id = required_occurrence.occurrence_id + join programmable_private.release_source_bindings as required_binding + on required_binding.binding_id = + required_materialization.release_binding_id + where required_occurrence.occurrence_id = required_id + and required_occurrence.chain_id = header.chain_id + and required_materialization.chain_id = header.chain_id + and required_materialization.release_id = header.release_id + and required_materialization.model_id = header.model_id + and required_materialization.source_group = header.source_group + and required_materialization.epoch_id = header.epoch_id + and required_materialization.pointer_generation + = header.captured_pointer_generation + and required_binding.epoch_id = header.epoch_id + and required_binding.source_role = required_role + and required_binding.source_address = required_occurrence.source_address + and required_binding.abi_event_set_commitment + = required_materialization.abi_event_set_commitment + and required_binding.inclusive_start_block + <= required_occurrence.block_number + ) then + raise exception using + errcode = '23514', + message = 'required occurrence is not current canonical in the release epoch'; + end if; + insert into programmable_private.reward_allocation_required_occurrences ( + allocation_fact_id, occurrence_ordinal, occurrence_role, occurrence_id, + release_binding_id, release_binding_commitment + ) + values ( + p_allocation_fact_id, ordinal, + required_role::programmable_private.source_identifier, required_id, + (select release_binding_id + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = required_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation), + (select binding.binding_commitment + from programmable_private.chain_event_occurrence_materializations + as materialization + join programmable_private.release_source_bindings as binding + on binding.binding_id = materialization.release_binding_id + where materialization.occurrence_id = required_id + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation) + ); + ordinal := ordinal + 1; + end loop; + perform programmable_private.append_mutation_audit( + 'reward_allocation_fact.append', p_content_fingerprint, p_run_id, p_created_at + ); + return p_allocation_fact_id; +end +$function$; + +create function programmable_private.append_reward_allocation_evidence( + p_allocation_evidence_id uuid, + p_allocation_fact_id uuid, + p_run_id uuid, + p_recovery_method text, + p_evidence_version text, + p_top_level_destination bytea, + p_method_selector bytea, + p_transaction_input_hash bytea, + p_constructor_arguments_commitment bytea, + p_local_init_code_hash bytea, + p_create2_salt bytea, + p_local_create2_address bytea, + p_historical_enrichment_status text, + p_getter_block_hash bytea, + p_getter_result_hash_a bytea, + p_getter_result_hash_b bytea, + p_predict_result_hash_a bytea, + p_predict_result_hash_b bytea, + p_predicted_vault_a bytea, + p_predicted_vault_b bytea, + p_selected_rpc_result_hash_a bytea, + p_selected_rpc_result_hash_b bytea, + p_selected_rpc_transaction_receipt_hash_a bytea, + p_selected_rpc_transaction_receipt_hash_b bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp(), + p_recomputed_allocation_hash bytea default null, + p_recomputed_configuration_hash bytea default null, + p_recomputed_active_configuration_hash bytea default null +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + fact programmable_private.reward_allocation_facts%rowtype; + existing programmable_private.reward_allocation_evidence%rowtype; + header programmable_private.run_headers%rowtype; + factory_occurrence programmable_private.chain_event_occurrences%rowtype; + factory_materialization programmable_private.chain_event_occurrence_materializations%rowtype; + recovery_binding programmable_private.release_source_bindings%rowtype; + recovery_role text; + mismatch boolean; + recomputation_supplied boolean; + recomputation_attested boolean; + audit_id uuid; + status_id uuid; + route_status_id uuid; + route_record record; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_fingerprint_encoding( + 'evidence', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into fact + from programmable_private.reward_allocation_facts + where allocation_fact_id = p_allocation_fact_id; + if not found then + raise exception using errcode = '23503', message = 'unknown allocation fact'; + end if; + recomputation_supplied := p_recomputed_allocation_hash is not null + or p_recomputed_configuration_hash is not null + or p_recomputed_active_configuration_hash is not null; + recomputation_attested := p_recomputed_allocation_hash is not null + and p_recomputed_configuration_hash is not null + and p_recomputed_active_configuration_hash + is not distinct from fact.active_configuration_hash; + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection') + and chain_id = fact.chain_id + and release_id = fact.release_id + and model_id = fact.model_id + and epoch_id = fact.epoch_id + and captured_pointer_generation = fact.pointer_generation; + if not found then + raise exception using errcode = '23503', message = 'allocation evidence run scope mismatch'; + end if; + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into factory_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = fact.factory_occurrence_id; + select * into factory_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = fact.factory_occurrence_id + and chain_id = fact.chain_id + and release_id = fact.release_id + and model_id = fact.model_id + and source_group = header.source_group + and epoch_id = fact.epoch_id + and pointer_generation = fact.pointer_generation; + perform programmable_private.assert_current_epoch( + fact.chain_id, fact.release_id, fact.model_id, + (select source_group from programmable_private.run_headers where run_id = p_run_id), + fact.epoch_id, fact.pointer_generation + ); + if pg_catalog.octet_length(p_constructor_arguments_commitment) <> 32 + or pg_catalog.octet_length(p_local_init_code_hash) <> 32 + or pg_catalog.octet_length(p_create2_salt) <> 32 + or pg_catalog.octet_length(p_local_create2_address) <> 20 + or pg_catalog.octet_length(p_selected_rpc_result_hash_a) <> 32 + or pg_catalog.octet_length(p_selected_rpc_result_hash_b) <> 32 + then + raise exception using + errcode = '22023', + message = 'invalid per-instance CREATE2 or selected-RPC evidence shape'; + end if; + if p_historical_enrichment_status not in ('matched', 'unavailable') + or ( + p_recovery_method = 'historical_getters' + and ( + p_top_level_destination is not null + or p_method_selector is not null + or p_transaction_input_hash is not null + or p_historical_enrichment_status <> 'matched' + or p_getter_block_hash is null + or p_getter_result_hash_a is null + or p_getter_result_hash_b is null + or p_predict_result_hash_a is null + or p_predict_result_hash_b is null + or p_predicted_vault_a is null + or p_predicted_vault_b is null + or p_selected_rpc_transaction_receipt_hash_a is not null + or p_selected_rpc_transaction_receipt_hash_b is not null + ) + ) + or ( + p_recovery_method <> 'historical_getters' + and ( + p_top_level_destination is null + or p_method_selector is null + or p_transaction_input_hash is null + or p_selected_rpc_transaction_receipt_hash_a is null + or p_selected_rpc_transaction_receipt_hash_b is null + or ( + p_historical_enrichment_status = 'matched' + and ( + p_getter_block_hash is null + or p_getter_result_hash_a is null + or p_getter_result_hash_b is null + or p_predict_result_hash_a is null + or p_predict_result_hash_b is null + or p_predicted_vault_a is null + or p_predicted_vault_b is null + ) + ) + or ( + p_historical_enrichment_status = 'unavailable' + and ( + p_getter_block_hash is not null + or p_getter_result_hash_a is not null + or p_getter_result_hash_b is not null + or p_predict_result_hash_a is not null + or p_predict_result_hash_b is not null + or p_predicted_vault_a is not null + or p_predicted_vault_b is not null + ) + ) + ) + ) + then + raise exception using + errcode = '23514', + message = 'historical enrichment evidence shape is incomplete'; + end if; + select * into existing + from programmable_private.reward_allocation_evidence + where allocation_evidence_id = p_allocation_evidence_id; + if found and ( + existing.allocation_fact_id <> p_allocation_fact_id + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + ) then + raise exception using + errcode = '23505', + message = 'allocation evidence replay changed immutable content'; + end if; + recovery_role := case p_recovery_method + when 'launcher_calldata' then 'launcher' + when 'coordinator_calldata' then 'coordinator' + when 'factory_calldata' then 'factory' + else 'vault_factory' + end; + if recovery_role = 'vault_factory' then + select * into recovery_binding + from programmable_private.release_source_bindings + where binding_id = fact.factory_release_binding_id; + elsif recovery_role = 'coordinator' then + select * into recovery_binding + from programmable_private.release_source_bindings + where epoch_id = fact.epoch_id + and source_role = 'coordinator' + and source_address = p_top_level_destination + and recovery_selector = p_method_selector; + else + select binding.* into recovery_binding + from programmable_private.reward_allocation_required_occurrences as required + join programmable_private.release_source_bindings as binding + on binding.binding_id = required.release_binding_id + where required.allocation_fact_id = p_allocation_fact_id + and required.occurrence_role = recovery_role; + end if; + mismatch := recovery_binding.binding_id is null + or factory_occurrence.occurrence_id is null + or factory_materialization.materialization_id is null + or factory_materialization.release_binding_id + is distinct from fact.factory_release_binding_id + or recovery_binding.epoch_id <> fact.epoch_id + or recovery_binding.source_role <> recovery_role + or recovery_binding.binding_commitment is distinct from ( + case when recovery_role = 'vault_factory' + then fact.factory_release_binding_commitment + when recovery_role = 'coordinator' + then recovery_binding.binding_commitment + else ( + select required.release_binding_commitment + from programmable_private.reward_allocation_required_occurrences as required + where required.allocation_fact_id = p_allocation_fact_id + and required.occurrence_role = recovery_role + ) + end + ) + or fact.manifest_artifact_creation_code_commitment + is distinct from recovery_binding.artifact_creation_code_commitment + or pg_catalog.octet_length(p_constructor_arguments_commitment) <> 32 + or pg_catalog.octet_length(p_local_init_code_hash) <> 32 + or p_local_init_code_hash + = fact.manifest_artifact_creation_code_commitment + or pg_catalog.octet_length(p_create2_salt) <> 32 + or p_local_create2_address is distinct from fact.vault + or ( + recomputation_supplied + and ( + not recomputation_attested + or p_recomputed_allocation_hash is distinct from fact.allocation_hash + or p_recomputed_configuration_hash + is distinct from fact.configuration_hash + or p_recomputed_active_configuration_hash + is distinct from fact.active_configuration_hash + ) + ) + or pg_catalog.octet_length(p_selected_rpc_result_hash_a) <> 32 + or p_selected_rpc_result_hash_a is distinct from p_selected_rpc_result_hash_b + or p_selected_rpc_result_hash_a is distinct from fact.configuration_hash + or exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences as required + join programmable_private.chain_event_occurrences as required_occurrence + on required_occurrence.occurrence_id = required.occurrence_id + left join programmable_private.chain_event_occurrence_materializations + as required_materialization + on required_materialization.occurrence_id = required.occurrence_id + and required_materialization.epoch_id = fact.epoch_id + and required_materialization.pointer_generation = fact.pointer_generation + join programmable_private.release_source_bindings as required_binding + on required_binding.binding_id = required.release_binding_id + left join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = required.occurrence_id + and canonical.logical_event_id = required_occurrence.logical_event_id + and canonical.block_hash = required_occurrence.block_hash + where required.allocation_fact_id = p_allocation_fact_id + and ( + canonical.occurrence_id is null + or required_materialization.materialization_id is null + or required.release_binding_commitment <> required_binding.binding_commitment + or required_binding.source_role <> required.occurrence_role + or required_materialization.release_binding_id + <> required.release_binding_id + ) + ) + or ( + p_recovery_method = 'historical_getters' + and ( + p_top_level_destination is not null + or p_method_selector is not null + or p_transaction_input_hash is not null + or p_historical_enrichment_status <> 'matched' + ) + ) + or ( + p_recovery_method <> 'historical_getters' + and ( + p_top_level_destination is distinct from recovery_binding.source_address + or p_method_selector is distinct from recovery_binding.recovery_selector + or p_transaction_input_hash is null + or p_selected_rpc_transaction_receipt_hash_a + is distinct from factory_occurrence.transaction_hash + or p_selected_rpc_transaction_receipt_hash_b + is distinct from factory_occurrence.transaction_hash + ) + ) + or ( + p_historical_enrichment_status = 'matched' + and ( + p_getter_block_hash is distinct from fact.factory_occurrence_block_hash + or p_getter_result_hash_a is distinct from p_getter_result_hash_b + or ( + fact.active_configuration_hash is not null + and p_getter_result_hash_a is distinct from fact.active_configuration_hash + ) + or p_predict_result_hash_a is distinct from p_predict_result_hash_b + or p_predicted_vault_a is distinct from fact.vault + or p_predicted_vault_b is distinct from fact.vault + ) + ); + if mismatch then + audit_id := programmable_private.append_mutation_audit( + 'reward_allocation_evidence.quarantine', + p_content_fingerprint, p_run_id, p_verified_at + ); + insert into programmable_private.reward_allocation_mismatch_evidence ( + mismatch_evidence_id, allocation_fact_id, recovery_method, + observed_destination, observed_selector, + observed_transaction_input_hash, + observed_constructor_arguments_commitment, observed_local_init_code_hash, + observed_create2_salt, observed_local_create2_address, + observed_allocation_hash, + observed_configuration_hash, observed_active_configuration_hash, + mismatch_commitment, verification_run_id, recorded_at, audit_id + ) values ( + p_allocation_evidence_id, p_allocation_fact_id, p_recovery_method, + p_top_level_destination, p_method_selector, p_transaction_input_hash, + p_constructor_arguments_commitment, p_local_init_code_hash, + p_create2_salt, p_local_create2_address, + p_recomputed_allocation_hash, p_recomputed_configuration_hash, + p_recomputed_active_configuration_hash, + p_content_fingerprint::programmable_private.bytes32_value, + p_run_id, p_verified_at, audit_id + ) on conflict (mismatch_evidence_id) do nothing; + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_allocation_status_history ( + seed_status_history_id, allocation_fact_id, allocation_evidence_id, + status, reason_commitment, decision_run_id, decided_at, audit_id + ) values ( + status_id, p_allocation_fact_id, null, 'quarantined', + p_content_fingerprint::programmable_private.bytes32_value, + p_run_id, p_verified_at, audit_id + ); + delete from programmable_private.reward_allocation_current_verified + where allocation_fact_id = p_allocation_fact_id; + for route_record in + select * from programmable_private.route_eligibility_current + where chain_id = fact.chain_id and release_id = fact.release_id + and model_id = fact.model_id and source_group = header.source_group + and epoch_id = fact.epoch_id + and pointer_generation = fact.pointer_generation + for update + loop + route_status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, + model_id, source_group, epoch_id, pointer_generation, status, + route_mode, checkpoint_id, reason_commitment, changed_by_run_id, + changed_at, audit_id + ) values ( + route_status_id, route_record.route_key, fact.chain_id, + fact.release_id, fact.model_id, header.source_group, fact.epoch_id, + fact.pointer_generation, 'quarantined', 'rpc', + route_record.checkpoint_id, + p_content_fingerprint::programmable_private.bytes32_value, + p_run_id, p_verified_at, audit_id + ); + update programmable_private.route_eligibility_current + set status = 'quarantined', route_mode = 'rpc', + history_id = route_status_id, changed_at = p_verified_at + where route_key = route_record.route_key + and chain_id = fact.chain_id and release_id = fact.release_id + and model_id = fact.model_id and source_group = header.source_group + and epoch_id = fact.epoch_id + and pointer_generation = fact.pointer_generation; + end loop; + return p_allocation_evidence_id; + end if; + select * into existing + from programmable_private.reward_allocation_evidence + where allocation_evidence_id = p_allocation_evidence_id; + if found then + if existing.allocation_fact_id <> p_allocation_fact_id + or existing.recovery_method::text <> p_recovery_method + or existing.evidence_version <> p_evidence_version + or existing.top_level_destination is distinct from p_top_level_destination + or existing.method_selector is distinct from p_method_selector + or existing.transaction_input_hash is distinct from p_transaction_input_hash + or existing.recomputed_allocation_hash + <> coalesce(p_recomputed_allocation_hash, fact.allocation_hash) + or existing.recomputed_configuration_hash + <> coalesce(p_recomputed_configuration_hash, fact.configuration_hash) + or existing.recomputed_active_configuration_hash + is distinct from (case + when p_recomputed_active_configuration_hash is not null + then p_recomputed_active_configuration_hash + else fact.active_configuration_hash + end) + or existing.is_recomputation_attested <> recomputation_attested + or existing.recovery_release_binding_id <> recovery_binding.binding_id + or existing.recovery_release_binding_commitment + <> recovery_binding.binding_commitment + or existing.constructor_arguments_commitment + <> p_constructor_arguments_commitment + or existing.local_init_code_hash <> p_local_init_code_hash + or existing.create2_salt <> p_create2_salt + or existing.local_create2_address <> p_local_create2_address + or existing.historical_enrichment_status::text + <> p_historical_enrichment_status + or existing.getter_block_hash is distinct from p_getter_block_hash + or existing.getter_result_hash_a is distinct from p_getter_result_hash_a + or existing.getter_result_hash_b is distinct from p_getter_result_hash_b + or existing.predict_result_hash_a is distinct from p_predict_result_hash_a + or existing.predict_result_hash_b is distinct from p_predict_result_hash_b + or existing.predicted_vault_a is distinct from p_predicted_vault_a + or existing.predicted_vault_b is distinct from p_predicted_vault_b + or existing.selected_rpc_result_hash_a <> p_selected_rpc_result_hash_a + or existing.selected_rpc_result_hash_b <> p_selected_rpc_result_hash_b + or existing.selected_rpc_transaction_receipt_hash_a + is distinct from p_selected_rpc_transaction_receipt_hash_a + or existing.selected_rpc_transaction_receipt_hash_b + is distinct from p_selected_rpc_transaction_receipt_hash_b + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + or existing.verification_run_id <> p_run_id + then + raise exception using + errcode = '23505', + message = 'allocation evidence replay changed immutable content'; + end if; + return existing.allocation_evidence_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'reward_allocation_evidence.append', + p_content_fingerprint, p_run_id, p_verified_at + ); + insert into programmable_private.reward_allocation_evidence ( + allocation_evidence_id, allocation_fact_id, factory_occurrence_id, + vault, recovery_method, evidence_version, recovery_release_binding_id, + recovery_release_binding_commitment, top_level_destination, + method_selector, transaction_input_hash, recomputed_allocation_hash, + recomputed_configuration_hash, recomputed_active_configuration_hash, + is_recomputation_attested, + constructor_arguments_commitment, local_init_code_hash, create2_salt, + local_create2_address, historical_enrichment_status, getter_block_hash, + getter_result_hash_a, getter_result_hash_b, predict_result_hash_a, + predict_result_hash_b, predicted_vault_a, predicted_vault_b, + selected_rpc_result_hash_a, + selected_rpc_result_hash_b, selected_rpc_transaction_receipt_hash_a, + selected_rpc_transaction_receipt_hash_b, encoding_version, + canonical_preimage, content_fingerprint, verification_run_id, + verified_at, audit_id + ) + values ( + p_allocation_evidence_id, p_allocation_fact_id, + fact.factory_occurrence_id, fact.vault, + p_recovery_method::programmable_private.recovery_method, + p_evidence_version::programmable_private.projector_identifier, + recovery_binding.binding_id, recovery_binding.binding_commitment, + case when p_top_level_destination is null then null + else p_top_level_destination::programmable_private.eth_address end, + case when p_method_selector is null then null + else p_method_selector::programmable_private.hex_selector end, + case when p_transaction_input_hash is null then null + else p_transaction_input_hash::programmable_private.bytes32_value end, + coalesce( + p_recomputed_allocation_hash, + fact.allocation_hash + )::programmable_private.bytes32_value, + coalesce( + p_recomputed_configuration_hash, + fact.configuration_hash + )::programmable_private.bytes32_value, + case + when p_recomputed_active_configuration_hash is not null + then p_recomputed_active_configuration_hash::programmable_private.bytes32_value + else fact.active_configuration_hash + end, + recomputation_attested, + p_constructor_arguments_commitment::programmable_private.bytes32_value, + p_local_init_code_hash::programmable_private.bytes32_value, + p_create2_salt::programmable_private.bytes32_value, + p_local_create2_address::programmable_private.eth_address, + p_historical_enrichment_status::programmable_private.historical_enrichment_status, + case when p_getter_block_hash is null then null + else p_getter_block_hash::programmable_private.bytes32_value end, + case when p_getter_result_hash_a is null then null + else p_getter_result_hash_a::programmable_private.bytes32_value end, + case when p_getter_result_hash_b is null then null + else p_getter_result_hash_b::programmable_private.bytes32_value end, + case when p_predict_result_hash_a is null then null + else p_predict_result_hash_a::programmable_private.bytes32_value end, + case when p_predict_result_hash_b is null then null + else p_predict_result_hash_b::programmable_private.bytes32_value end, + case when p_predicted_vault_a is null then null + else p_predicted_vault_a::programmable_private.eth_address end, + case when p_predicted_vault_b is null then null + else p_predicted_vault_b::programmable_private.eth_address end, + p_selected_rpc_result_hash_a::programmable_private.bytes32_value, + p_selected_rpc_result_hash_b::programmable_private.bytes32_value, + case when p_selected_rpc_transaction_receipt_hash_a is null then null + else p_selected_rpc_transaction_receipt_hash_a::programmable_private.bytes32_value end, + case when p_selected_rpc_transaction_receipt_hash_b is null then null + else p_selected_rpc_transaction_receipt_hash_b::programmable_private.bytes32_value end, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_run_id, p_verified_at, audit_id + ); + return p_allocation_evidence_id; +end +$function$; + +create function programmable_private.append_reward_seed_status( + p_seed_status_history_id uuid, + p_allocation_fact_id uuid, + p_allocation_evidence_id uuid, + p_status text, + p_reason_commitment bytea, + p_run_id uuid, + p_decided_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + fact programmable_private.reward_allocation_facts%rowtype; + header programmable_private.run_headers%rowtype; + requested_status programmable_private.reward_seed_status; + audit_id uuid; + route_status_id uuid; + route_record record; +begin + perform programmable_private.assert_caller('programmable_projector'); + requested_status := p_status::programmable_private.reward_seed_status; + if requested_status = 'verified' then + raise exception using + errcode = '42501', + message = 'verified seed selection is promotion-only'; + end if; + select * into fact + from programmable_private.reward_allocation_facts + where allocation_fact_id = p_allocation_fact_id + for share; + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection') + for share; + if fact.allocation_fact_id is null + or header.run_id is null + or header.epoch_id <> fact.epoch_id + or header.captured_pointer_generation <> fact.pointer_generation + or header.chain_id <> fact.chain_id + or header.release_id <> fact.release_id + or header.model_id <> fact.model_id + or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + or not exists ( + select 1 + from programmable_private.reward_allocation_evidence + where allocation_evidence_id = p_allocation_evidence_id + and allocation_fact_id = p_allocation_fact_id + ) + or pg_catalog.octet_length(p_reason_commitment) <> 32 + then + raise exception using errcode = '23503', message = 'invalid seed status evidence'; + end if; + perform programmable_private.assert_current_epoch( + fact.chain_id, fact.release_id, fact.model_id, header.source_group, + fact.epoch_id, fact.pointer_generation + ); + audit_id := programmable_private.append_mutation_audit( + 'reward_seed_status.append', p_reason_commitment, p_run_id, p_decided_at + ); + insert into programmable_private.reward_allocation_status_history ( + seed_status_history_id, allocation_fact_id, allocation_evidence_id, + status, reason_commitment, decision_run_id, decided_at, audit_id + ) + values ( + p_seed_status_history_id, p_allocation_fact_id, p_allocation_evidence_id, + requested_status, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_decided_at, audit_id + ); + if requested_status in ('quarantined', 'orphaned', 'conflicted', 'revoked') then + delete from programmable_private.reward_allocation_current_verified + where allocation_fact_id = p_allocation_fact_id; + for route_record in + select * + from programmable_private.route_eligibility_current + where chain_id = fact.chain_id + and release_id = fact.release_id + and model_id = fact.model_id + and source_group = header.source_group + and epoch_id = fact.epoch_id + and pointer_generation = fact.pointer_generation + for update + loop + route_status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, + model_id, source_group, epoch_id, pointer_generation, status, + route_mode, checkpoint_id, reason_commitment, changed_by_run_id, + changed_at, audit_id + ) + values ( + route_status_id, route_record.route_key, fact.chain_id, + fact.release_id, fact.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, + 'quarantined', 'rpc', route_record.checkpoint_id, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_decided_at, audit_id + ); + update programmable_private.route_eligibility_current + set status = 'quarantined', + route_mode = 'rpc', + history_id = route_status_id, + changed_at = p_decided_at + where route_key = route_record.route_key + and chain_id = fact.chain_id + and release_id = fact.release_id + and model_id = fact.model_id + and source_group = header.source_group + and epoch_id = fact.epoch_id + and pointer_generation = fact.pointer_generation; + end loop; + end if; + return p_seed_status_history_id; +end +$function$; + +create function programmable_private.quarantine_conflicting_reward_allocations( + p_seed_status_history_id_a uuid, + p_seed_status_history_id_b uuid, + p_allocation_fact_id_a uuid, + p_allocation_evidence_id_a uuid, + p_allocation_fact_id_b uuid, + p_allocation_evidence_id_b uuid, + p_run_id uuid, + p_reason_commitment bytea, + p_decided_at timestamptz default pg_catalog.clock_timestamp() +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + fact_a programmable_private.reward_allocation_facts%rowtype; + fact_b programmable_private.reward_allocation_facts%rowtype; + header programmable_private.run_headers%rowtype; + audit_id uuid; + route_status_id uuid; + route_record record; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into fact_a + from programmable_private.reward_allocation_facts + where allocation_fact_id = p_allocation_fact_id_a + for share; + select * into fact_b + from programmable_private.reward_allocation_facts + where allocation_fact_id = p_allocation_fact_id_b + for share; + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection') + for share; + if p_seed_status_history_id_a is null + or p_seed_status_history_id_b is null + or p_seed_status_history_id_a = p_seed_status_history_id_b + or p_allocation_fact_id_a = p_allocation_fact_id_b + or p_allocation_evidence_id_a = p_allocation_evidence_id_b + or fact_a.allocation_fact_id is null + or fact_b.allocation_fact_id is null + or header.run_id is null + or pg_catalog.octet_length(p_reason_commitment) <> 32 + or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '23503', + message = 'invalid conflicting allocation decision'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if fact_a.chain_id <> header.chain_id + or fact_a.release_id <> header.release_id + or fact_a.model_id <> header.model_id + or fact_a.epoch_id <> header.epoch_id + or fact_a.pointer_generation <> header.captured_pointer_generation + or fact_b.chain_id <> fact_a.chain_id + or fact_b.release_id <> fact_a.release_id + or fact_b.model_id <> fact_a.model_id + or fact_b.epoch_id <> fact_a.epoch_id + or fact_b.pointer_generation <> fact_a.pointer_generation + or fact_b.factory_occurrence_id <> fact_a.factory_occurrence_id + or fact_b.vault <> fact_a.vault + or not ( + fact_b.ordered_beneficiaries is distinct from fact_a.ordered_beneficiaries + or fact_b.ordered_shares_bps is distinct from fact_a.ordered_shares_bps + or fact_b.allocation_hash <> fact_a.allocation_hash + or fact_b.configuration_hash <> fact_a.configuration_hash + or fact_b.active_configuration_hash + is distinct from fact_a.active_configuration_hash + ) + or not exists ( + select 1 + from programmable_private.chain_event_current_canonical + where occurrence_id = fact_a.factory_occurrence_id + ) + or exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences + as required + where required.allocation_fact_id in ( + p_allocation_fact_id_a, p_allocation_fact_id_b + ) + and not exists ( + select 1 + from programmable_private.chain_event_current_canonical as selected + join programmable_private.reward_allocation_facts as scoped_fact + on scoped_fact.allocation_fact_id = required.allocation_fact_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = selected.occurrence_id + and materialization.epoch_id = scoped_fact.epoch_id + and materialization.pointer_generation = + scoped_fact.pointer_generation + where selected.occurrence_id = required.occurrence_id + ) + ) + or not exists ( + select 1 + from programmable_private.reward_allocation_evidence + where allocation_evidence_id = p_allocation_evidence_id_a + and allocation_fact_id = p_allocation_fact_id_a + and is_recomputation_attested + ) + or not exists ( + select 1 + from programmable_private.reward_allocation_evidence + where allocation_evidence_id = p_allocation_evidence_id_b + and allocation_fact_id = p_allocation_fact_id_b + and is_recomputation_attested + ) + then + raise exception using + errcode = '23514', + message = 'allocations are not independently valid conflicting evidence'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'reward_seed_conflict.quarantine', + p_reason_commitment, p_run_id, p_decided_at + ); + insert into programmable_private.reward_allocation_status_history ( + seed_status_history_id, allocation_fact_id, allocation_evidence_id, + status, reason_commitment, decision_run_id, decided_at, audit_id + ) + values + ( + p_seed_status_history_id_a, p_allocation_fact_id_a, + p_allocation_evidence_id_a, 'conflicted', + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_decided_at, audit_id + ), + ( + p_seed_status_history_id_b, p_allocation_fact_id_b, + p_allocation_evidence_id_b, 'conflicted', + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_decided_at, audit_id + ); + delete from programmable_private.reward_allocation_current_verified + where factory_occurrence_id = fact_a.factory_occurrence_id + and vault = fact_a.vault; + for route_record in + select * + from programmable_private.route_eligibility_current + where chain_id = fact_a.chain_id + and release_id = fact_a.release_id + and model_id = fact_a.model_id + and source_group = header.source_group + and epoch_id = fact_a.epoch_id + and pointer_generation = fact_a.pointer_generation + for update + loop + route_status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, + model_id, source_group, epoch_id, pointer_generation, status, + route_mode, checkpoint_id, reason_commitment, changed_by_run_id, + changed_at, audit_id + ) + values ( + route_status_id, route_record.route_key, fact_a.chain_id, + fact_a.release_id, fact_a.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, + 'quarantined', 'rpc', route_record.checkpoint_id, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_decided_at, audit_id + ); + update programmable_private.route_eligibility_current + set status = 'quarantined', + route_mode = 'rpc', + history_id = route_status_id, + changed_at = p_decided_at + where route_key = route_record.route_key + and chain_id = fact_a.chain_id + and release_id = fact_a.release_id + and model_id = fact_a.model_id + and source_group = header.source_group + and epoch_id = fact_a.epoch_id + and pointer_generation = fact_a.pointer_generation; + end loop; + return true; +end +$function$; + +do $lockdown$ +declare + table_record record; +begin + for table_record in + select c.relname + from pg_catalog.pg_class as c + join pg_catalog.pg_namespace as n on n.oid = c.relnamespace + where n.nspname = 'programmable_private' + and c.relkind in ('r', 'p') + and not c.relrowsecurity + loop + execute pg_catalog.format( + 'alter table programmable_private.%I enable row level security', + table_record.relname + ); + execute pg_catalog.format( + 'alter table programmable_private.%I force row level security', + table_record.relname + ); + execute pg_catalog.format( + 'create policy migrator_owner_all on programmable_private.%I ' || + 'for all to programmable_migrator using (true) with check (true)', + table_record.relname + ); + end loop; +end +$lockdown$; + +revoke all on all tables in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all sequences in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all functions in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +grant execute on function programmable_private.append_envio_candidate( + text, uuid, numeric, bytea, bytea, numeric, numeric, bytea, bytea, text, + bytea[], bytea, jsonb, bytea, text, uuid, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_chain_event_occurrence( + uuid, uuid, uuid, text, numeric, timestamptz, text, bytea, uuid, + smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_reward_allocation_fact( + uuid, uuid, bytea, uuid, bytea[], numeric[], bytea, bytea, bytea, bytea, + uuid[], text[], smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_reward_allocation_evidence( + uuid, uuid, uuid, text, text, + bytea, bytea, bytea, bytea, bytea, bytea, bytea, text, + bytea, bytea, bytea, bytea, bytea, bytea, bytea, bytea, bytea, bytea, + bytea, + smallint, bytea, bytea, timestamptz, bytea, bytea, bytea +) to programmable_projector; +grant execute on function programmable_private.append_reward_seed_status( + uuid, uuid, uuid, text, bytea, uuid, timestamptz +) to programmable_projector; +grant execute on function + programmable_private.quarantine_conflicting_reward_allocations( + uuid, uuid, uuid, uuid, uuid, uuid, uuid, bytea, timestamptz + ) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260731000400_core_launch_reward_projections.sql b/supabase/migrations/20260731000400_core_launch_reward_projections.sql new file mode 100644 index 00000000..3c34ae5c --- /dev/null +++ b/supabase/migrations/20260731000400_core_launch_reward_projections.sql @@ -0,0 +1,3283 @@ +-- Rebuildable projections remain invisible until an atomic, fenced +-- publication binds them to a terminal run outcome and checkpoint. + +set role programmable_migrator; + +create table programmable_private.launch_projections ( + launch_projection_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + token programmable_private.eth_address not null, + creator programmable_private.eth_address not null, + launch_transaction_hash programmable_private.bytes32_value not null, + pool_id programmable_private.bytes32_value not null, + reward_vault programmable_private.eth_address, + launch_hash programmable_private.bytes32_value not null, + token_name text not null check (pg_catalog.octet_length(token_name) between 1 and 128), + token_symbol text not null check (pg_catalog.octet_length(token_symbol) between 1 and 32), + total_supply programmable_private.uint256_value not null, + last_source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + last_source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + last_source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + is_complete boolean not null, + foreign key (projection_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) + on delete restrict, + foreign key ( + last_source_occurrence_id, last_source_logical_event_id, + last_source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + unique (chain_id, release_id, token, projection_run_id) +); + +create index launch_projection_recent_idx + on programmable_private.launch_projections ( + chain_id, promoted_block_number desc, launch_transaction_hash desc, token + ); +create index launch_projection_creator_recent_idx + on programmable_private.launch_projections ( + chain_id, creator, promoted_block_number desc, token + ); +create index launch_projection_lookup_idx + on programmable_private.launch_projections ( + chain_id, launch_transaction_hash, creator + ); + +create table programmable_private.pool_projections ( + pool_projection_id uuid primary key, + launch_projection_id uuid not null + references programmable_private.launch_projections(launch_projection_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + pool_id programmable_private.bytes32_value not null, + currency0 programmable_private.eth_address not null, + currency1 programmable_private.eth_address not null, + pool_key_fee bigint not null check (pool_key_fee between 0 and 16777215), + tick_spacing integer not null, + hook programmable_private.eth_address not null, + last_source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + last_source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + last_source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + unique (chain_id, pool_id, projection_run_id), + foreign key ( + last_source_occurrence_id, last_source_logical_event_id, + last_source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict +); + +create index pool_projection_token_pair_idx + on programmable_private.pool_projections (chain_id, currency0, currency1, pool_id); + +create table programmable_private.pool_fee_configurations ( + pool_fee_configuration_id uuid primary key, + pool_projection_id uuid not null + references programmable_private.pool_projections(pool_projection_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + buy_swap_fee_bps programmable_private.basis_points not null, + sell_swap_fee_bps programmable_private.basis_points not null, + buy_creator_fee_bps programmable_private.basis_points not null, + sell_creator_fee_bps programmable_private.basis_points not null, + creator_fee_bps programmable_private.basis_points, + launcher_fee_bps programmable_private.basis_points not null, + transfer_tax_bps programmable_private.basis_points not null, + lp_fee_pips bigint not null check (lp_fee_pips between 0 and 1000000), + disclosure_source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + disclosure_source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + disclosure_source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + disclosure_source_occurrence_id, disclosure_source_logical_event_id, + disclosure_source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + check ( + (buy_creator_fee_bps = sell_creator_fee_bps + and creator_fee_bps = buy_creator_fee_bps) + or (buy_creator_fee_bps <> sell_creator_fee_bps + and creator_fee_bps is null) + ), + unique (pool_projection_id, projection_run_id) +); + +create table programmable_private.fee_accrual_facts ( + fee_accrual_fact_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + pool_id programmable_private.bytes32_value not null, + quote_asset programmable_private.eth_address, + gross_amount programmable_private.uint256_value not null, + creator_fee programmable_private.uint256_value not null, + launcher_fee programmable_private.uint256_value not null, + source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + check (creator_fee + launcher_fee <= gross_amount), + unique (source_occurrence_id, projection_run_id) +); + +create index fee_accrual_pool_block_idx + on programmable_private.fee_accrual_facts ( + chain_id, pool_id, promoted_block_number, source_occurrence_id + ); + +create table programmable_private.pool_fee_totals ( + pool_fee_total_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + pool_id programmable_private.bytes32_value not null, + quote_asset programmable_private.eth_address, + gross_total programmable_private.uint256_value not null, + creator_fee_total programmable_private.uint256_value not null, + launcher_fee_total programmable_private.uint256_value not null, + last_source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + last_source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + last_source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + last_source_occurrence_id, last_source_logical_event_id, + last_source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + check (creator_fee_total + launcher_fee_total <= gross_total), + unique (chain_id, pool_id, quote_asset, projection_run_id) +); + +create table programmable_private.reward_vault_projections ( + reward_vault_projection_id uuid primary key, + launch_projection_id uuid not null + references programmable_private.launch_projections(launch_projection_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + vault programmable_private.eth_address not null, + pool_id programmable_private.bytes32_value not null, + quote_asset programmable_private.eth_address, + configuration_hash programmable_private.bytes32_value not null, + current_allocation_fact_id uuid not null + references programmable_private.reward_allocation_facts(allocation_fact_id) + on delete restrict, + last_source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + last_source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + last_source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + last_source_occurrence_id, last_source_logical_event_id, + last_source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + unique (chain_id, vault, projection_run_id) +); + +create index reward_vault_projection_pool_idx + on programmable_private.reward_vault_projections ( + chain_id, pool_id, vault, promoted_block_number + ); + +create table programmable_private.reward_allocation_projections ( + reward_allocation_projection_id uuid primary key, + reward_vault_projection_id uuid not null + references programmable_private.reward_vault_projections(reward_vault_projection_id) + on delete restrict, + allocation_fact_id uuid not null + references programmable_private.reward_allocation_facts(allocation_fact_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + configuration_epoch bigint not null check (configuration_epoch > 0), + allocation_index integer not null check (allocation_index >= 0), + beneficiary programmable_private.eth_address not null, + payout_address programmable_private.eth_address not null, + share_bps programmable_private.basis_points not null check (share_bps > 0), + effective_from_block programmable_private.block_number_value not null, + effective_to_block bigint check ( + effective_to_block is null or effective_to_block >= 0 + ), + last_source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + last_source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + last_source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + last_source_occurrence_id, last_source_logical_event_id, + last_source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + check (effective_to_block is null or effective_to_block >= effective_from_block), + unique ( + reward_vault_projection_id, configuration_epoch, + allocation_index, projection_run_id + ) +); + +create index reward_allocation_beneficiary_idx + on programmable_private.reward_allocation_projections ( + beneficiary, effective_from_block desc, reward_vault_projection_id + ); + +create table programmable_private.claim_projections ( + claim_projection_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + vault programmable_private.eth_address not null, + claimant_kind programmable_private.source_identifier not null + check (claimant_kind in ('beneficiary', 'creator', 'launcher')), + beneficiary programmable_private.eth_address not null, + recipient programmable_private.eth_address not null, + amount programmable_private.uint256_value not null, + beneficiary_total_claimed programmable_private.uint256_value not null, + vault_total_received programmable_private.uint256_value not null, + source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + unique (source_occurrence_id, projection_run_id) +); + +create index claim_projection_beneficiary_idx + on programmable_private.claim_projections ( + chain_id, beneficiary, promoted_block_number desc + ); + +create table programmable_private.payout_change_projections ( + payout_change_projection_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + vault programmable_private.eth_address not null, + beneficiary programmable_private.eth_address not null, + previous_payout_address programmable_private.eth_address not null, + new_payout_address programmable_private.eth_address not null, + configuration_epoch bigint, + source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + unique (source_occurrence_id, projection_run_id) +); + +create table programmable_private.account_reward_balances ( + account_reward_balance_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + account programmable_private.eth_address not null, + vault programmable_private.eth_address not null, + claimable_accrued programmable_private.uint256_value not null, + claimed_total programmable_private.uint256_value not null, + last_source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + last_source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + last_source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + last_source_occurrence_id, last_source_logical_event_id, + last_source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + unique (chain_id, account, vault, projection_run_id) +); + +create index account_reward_balance_account_idx + on programmable_private.account_reward_balances ( + chain_id, account, release_id, model_id, vault + ); + +create table programmable_private.initial_buy_custody_projections ( + custody_projection_id uuid primary key, + launch_projection_id uuid not null + references programmable_private.launch_projections(launch_projection_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + custody_address programmable_private.eth_address not null, + custody_mode smallint not null check (custody_mode between 0 and 255), + duration_days integer not null check (duration_days between 0 and 65535), + cliff_days integer not null check (cliff_days between 0 and 65535), + configuration_hash programmable_private.bytes32_value not null, + source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + unique (launch_projection_id, projection_run_id) +); + +create table programmable_private.initial_buy_vesting_projections ( + vesting_projection_id uuid primary key, + custody_projection_id uuid not null + references programmable_private.initial_buy_custody_projections(custody_projection_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + beneficiary programmable_private.eth_address not null, + token programmable_private.eth_address not null, + amount programmable_private.uint256_value not null, + vesting_start timestamptz not null, + vesting_end timestamptz not null, + source_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + source_logical_event_id uuid not null + references programmable_private.chain_event_identities(logical_event_id) + on delete restrict, + source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + check (vesting_end >= vesting_start), + unique (custody_projection_id, beneficiary, projection_run_id) +); + +create table programmable_private.projection_publications ( + publication_id uuid primary key, + run_id uuid not null unique, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + terminal_outcome_id uuid not null, + target_block_number programmable_private.block_number_value not null, + target_block_hash programmable_private.bytes32_value not null, + published_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (terminal_outcome_id, run_id) + references programmable_private.run_lifecycle_outcomes(outcome_id, run_id) + on delete restrict, + foreign key (run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) + on delete restrict +); + +-- A publication advances only the entities present in its immutable delta. +-- These pointers keep unrelated prior versions visible without restaging the +-- entire release state. +create table programmable_private.projection_entity_current ( + entity_kind programmable_private.source_identifier not null, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + entity_key text not null check (pg_catalog.octet_length(entity_key) between 1 and 512), + projection_row_id uuid not null, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + publication_id uuid not null + references programmable_private.projection_publications(publication_id) + on delete restrict, + checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + promoted_block_number programmable_private.block_number_value not null, + promoted_block_hash programmable_private.bytes32_value not null, + selected_at timestamptz not null, + primary key ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key + ) +); + +create view programmable_private.current_launch_projections_v1 +with (security_invoker = false, security_barrier = true) +as +select launch.* +from programmable_private.projection_entity_current as current_entity +join programmable_private.launch_projections as launch + on launch.launch_projection_id = current_entity.projection_row_id + and launch.projection_run_id = current_entity.projection_run_id +where current_entity.entity_kind = 'launch'; + +create view programmable_private.current_account_reward_balances_v1 +with (security_invoker = false, security_barrier = true) +as +select balance.* +from programmable_private.projection_entity_current as current_entity +join programmable_private.account_reward_balances as balance + on balance.account_reward_balance_id = current_entity.projection_row_id + and balance.projection_run_id = current_entity.projection_run_id +where current_entity.entity_kind = 'account_reward_balance'; + +create view programmable_private.current_reward_vault_projections_v1 +with (security_invoker = false, security_barrier = true) +as +select vault.* +from programmable_private.projection_entity_current as current_entity +join programmable_private.reward_vault_projections as vault + on vault.reward_vault_projection_id = current_entity.projection_row_id + and vault.projection_run_id = current_entity.projection_run_id +where current_entity.entity_kind = 'reward_vault'; + +create view programmable_private.current_pool_fee_totals_v1 +with (security_invoker = false, security_barrier = true) +as +select fee_total.* +from programmable_private.projection_entity_current as current_entity +join programmable_private.pool_fee_totals as fee_total + on fee_total.pool_fee_total_id = current_entity.projection_row_id + and fee_total.projection_run_id = current_entity.projection_run_id +where current_entity.entity_kind = 'pool_fee_total'; + +create table programmable_private.projection_fold_manifests ( + run_id uuid primary key + references programmable_private.run_headers(run_id) + on delete restrict, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + target_block_number programmable_private.block_number_value not null, + target_block_hash programmable_private.bytes32_value not null, + ordered_occurrence_ids uuid[] not null, + ordered_allocation_fact_ids uuid[] not null, + ordered_allocation_evidence_ids uuid[] not null, + ordered_candidate_disposition_ids uuid[] not null, + ordered_route_keys text[] not null check (cardinality(ordered_route_keys) > 0), + cursor_block_global_log_index + programmable_private.block_log_index_value not null, + cursor_candidate_id + programmable_private.envio_candidate_identifier not null, + ordered_projection_rows text[] not null + check (cardinality(ordered_projection_rows) > 0), + projection_row_count bigint not null check (projection_row_count > 0), + result_commitment programmable_private.bytes32_value not null, + created_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check ( + cardinality(ordered_allocation_fact_ids) + = cardinality(ordered_allocation_evidence_ids) + ), + check ( + cardinality(ordered_occurrence_ids) > 0 + or cardinality(ordered_candidate_disposition_ids) > 0 + ), + check ( + cardinality(ordered_projection_rows)::bigint = projection_row_count + ) +); + +create table programmable_private.route_eligibility_history ( + route_eligibility_history_id uuid primary key, + route_key programmable_private.source_identifier not null, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + status programmable_private.route_eligibility_status not null, + route_mode programmable_private.route_mode not null, + checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + reason_commitment programmable_private.bytes32_value not null, + changed_by_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + changed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique ( + route_key, chain_id, release_id, model_id, source_group, + pointer_generation, changed_at + ) +); + +create table programmable_private.route_eligibility_current ( + route_key programmable_private.source_identifier not null, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + status programmable_private.route_eligibility_status not null, + route_mode programmable_private.route_mode not null, + checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + history_id uuid not null unique + references programmable_private.route_eligibility_history(route_eligibility_history_id) + on delete restrict, + changed_at timestamptz not null, + primary key (route_key, chain_id, release_id, model_id, source_group) +); + +-- One scope check is shared by every dedicated projection writer. It binds +-- the staged row to an open projection run and to an occurrence whose own +-- verification run belongs to the exact same source group. +create function programmable_private.projection_stage_context( + p_run_id uuid, + p_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea +) +returns table ( + chain_id programmable_private.chain_id_value, + release_id programmable_private.release_identifier, + model_id programmable_private.model_identifier, + source_group programmable_private.source_identifier, + epoch_id uuid, + pointer_generation bigint, + source_logical_event_id uuid, + source_occurrence_block_hash programmable_private.bytes32_value, + promoted_block_number programmable_private.block_number_value, + promoted_block_hash programmable_private.bytes32_value +) +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + source programmable_private.chain_event_occurrences%rowtype; + materialization programmable_private.chain_event_occurrence_materializations%rowtype; + target_block bigint; +begin + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection' + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if p_promoted_block_number <> pg_catalog.trunc(p_promoted_block_number) + or p_promoted_block_number < 0 + or p_promoted_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_promoted_block_hash) <> 32 + then + raise exception using errcode = '22023', message = 'invalid projection target'; + end if; + target_block := p_promoted_block_number::bigint; + select * into source + from programmable_private.chain_event_occurrences + where occurrence_id = p_source_occurrence_id; + select * into materialization + from programmable_private.chain_event_occurrence_materializations + as scoped_materialization + where scoped_materialization.occurrence_id = p_source_occurrence_id + and scoped_materialization.chain_id = header.chain_id + and scoped_materialization.release_id = header.release_id + and scoped_materialization.model_id = header.model_id + and scoped_materialization.source_group = header.source_group + and scoped_materialization.epoch_id = header.epoch_id + and scoped_materialization.pointer_generation = + header.captured_pointer_generation; + if source.occurrence_id is null + or source.chain_id <> header.chain_id + or materialization.materialization_id is null + or source.block_number > target_block + then + raise exception using errcode = '23503', message = 'projection source scope mismatch'; + end if; + return query + select header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, source.logical_event_id, + source.block_hash, target_block::programmable_private.block_number_value, + p_promoted_block_hash::programmable_private.bytes32_value; +end +$function$; + +create function programmable_private.stage_launch_projection( + p_launch_projection_id uuid, + p_run_id uuid, + p_token bytea, + p_creator bytea, + p_launch_transaction_hash bytea, + p_pool_id bytea, + p_reward_vault bytea, + p_launch_hash bytea, + p_token_name text, + p_token_symbol text, + p_total_supply numeric, + p_last_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + source programmable_private.chain_event_occurrences%rowtype; + scope record; + supply numeric; + block_number bigint; + existing programmable_private.launch_projections%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_last_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection' + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + supply := programmable_private.validate_uint256(p_total_supply); + if p_promoted_block_number <> pg_catalog.trunc(p_promoted_block_number) + or p_promoted_block_number < 0 + or p_promoted_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_token) <> 20 + or pg_catalog.octet_length(p_creator) <> 20 + or pg_catalog.octet_length(p_launch_transaction_hash) <> 32 + or pg_catalog.octet_length(p_pool_id) <> 32 + or (p_reward_vault is not null and pg_catalog.octet_length(p_reward_vault) <> 20) + or pg_catalog.octet_length(p_launch_hash) <> 32 + or pg_catalog.octet_length(p_promoted_block_hash) <> 32 + then + raise exception using errcode = '22023', message = 'invalid launch projection'; + end if; + block_number := p_promoted_block_number::bigint; + select * into source + from programmable_private.chain_event_occurrences + where occurrence_id = p_last_source_occurrence_id; + if not found or source.block_number > block_number + then + raise exception using errcode = '23503', message = 'projection source scope mismatch'; + end if; + select * into existing + from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id; + if found then + if existing.projection_run_id <> p_run_id + or existing.token <> p_token + or existing.creator <> p_creator + or existing.launch_transaction_hash <> p_launch_transaction_hash + or existing.pool_id <> p_pool_id + or existing.reward_vault is distinct from p_reward_vault + or existing.launch_hash <> p_launch_hash + or existing.token_name <> p_token_name + or existing.token_symbol <> p_token_symbol + or existing.total_supply <> supply + or existing.last_source_occurrence_id <> p_last_source_occurrence_id + or existing.promoted_block_number <> block_number + or existing.promoted_block_hash <> p_promoted_block_hash + then + raise exception using errcode = '23505', message = 'launch projection replay changed content'; + end if; + return existing.launch_projection_id; + end if; + insert into programmable_private.launch_projections ( + launch_projection_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, token, creator, launch_transaction_hash, pool_id, + reward_vault, launch_hash, token_name, token_symbol, total_supply, + last_source_logical_event_id, last_source_occurrence_id, + last_source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at, is_complete + ) + values ( + p_launch_projection_id, header.chain_id, header.release_id, header.model_id, + header.epoch_id, header.captured_pointer_generation, + p_token::programmable_private.eth_address, + p_creator::programmable_private.eth_address, + p_launch_transaction_hash::programmable_private.bytes32_value, + p_pool_id::programmable_private.bytes32_value, + case when p_reward_vault is null then null + else p_reward_vault::programmable_private.eth_address end, + p_launch_hash::programmable_private.bytes32_value, + p_token_name, p_token_symbol, supply, + source.logical_event_id, source.occurrence_id, source.block_hash, + p_run_id, block_number::programmable_private.block_number_value, + p_promoted_block_hash::programmable_private.bytes32_value, + p_verified_at, true + ); + perform programmable_private.append_mutation_audit( + 'launch_projection.stage', p_launch_hash, p_run_id, p_verified_at + ); + return p_launch_projection_id; +end +$function$; + +create function programmable_private.stage_pool_projection( + p_pool_projection_id uuid, + p_launch_projection_id uuid, + p_run_id uuid, + p_currency0 bytea, + p_currency1 bytea, + p_pool_key_fee numeric, + p_tick_spacing integer, + p_hook bytea, + p_last_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + launch programmable_private.launch_projections%rowtype; + source programmable_private.chain_event_occurrences%rowtype; + scope record; + fee bigint; + block_number bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_last_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into launch + from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id + and projection_run_id = p_run_id; + if not found + or launch.chain_id <> scope.chain_id + or launch.release_id <> scope.release_id + or launch.model_id <> scope.model_id + or launch.epoch_id <> scope.epoch_id + or launch.pointer_generation <> scope.pointer_generation + or launch.promoted_block_number <> scope.promoted_block_number + or launch.promoted_block_hash <> scope.promoted_block_hash + then + raise exception using errcode = '23503', message = 'missing staged launch'; + end if; + if p_pool_key_fee <> pg_catalog.trunc(p_pool_key_fee) + or p_pool_key_fee < 0 or p_pool_key_fee > 16777215 + or p_promoted_block_number <> pg_catalog.trunc(p_promoted_block_number) + or p_promoted_block_number < 0 + or p_promoted_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_currency0) <> 20 + or pg_catalog.octet_length(p_currency1) <> 20 + or p_currency0 >= p_currency1 + or (launch.token <> p_currency0 and launch.token <> p_currency1) + or pg_catalog.octet_length(p_hook) <> 20 + or pg_catalog.octet_length(p_promoted_block_hash) <> 32 + then + raise exception using errcode = '22023', message = 'invalid PoolKey projection'; + end if; + fee := p_pool_key_fee::bigint; + block_number := p_promoted_block_number::bigint; + select * into source + from programmable_private.chain_event_occurrences + where occurrence_id = p_last_source_occurrence_id; + if not found or source.block_number > block_number then + raise exception using errcode = '23503', message = 'pool source scope mismatch'; + end if; + insert into programmable_private.pool_projections ( + pool_projection_id, launch_projection_id, chain_id, release_id, model_id, + epoch_id, pointer_generation, pool_id, currency0, currency1, pool_key_fee, + tick_spacing, hook, last_source_logical_event_id, + last_source_occurrence_id, last_source_occurrence_block_hash, + projection_run_id, promoted_block_number, promoted_block_hash, verified_at + ) + values ( + p_pool_projection_id, p_launch_projection_id, launch.chain_id, + launch.release_id, launch.model_id, launch.epoch_id, + launch.pointer_generation, launch.pool_id, + p_currency0::programmable_private.eth_address, + p_currency1::programmable_private.eth_address, + fee, p_tick_spacing, p_hook::programmable_private.eth_address, + source.logical_event_id, source.occurrence_id, source.block_hash, + p_run_id, block_number::programmable_private.block_number_value, + p_promoted_block_hash::programmable_private.bytes32_value, p_verified_at + ) + on conflict (pool_projection_id) do nothing; + if not found and not exists ( + select 1 from programmable_private.pool_projections + where pool_projection_id = p_pool_projection_id + and launch_projection_id = p_launch_projection_id + and projection_run_id = p_run_id + and currency0 = p_currency0 + and currency1 = p_currency1 + and pool_key_fee = fee + and tick_spacing = p_tick_spacing + and hook = p_hook + and last_source_occurrence_id = p_last_source_occurrence_id + and promoted_block_number = block_number + and promoted_block_hash = p_promoted_block_hash + ) then + raise exception using errcode = '23505', message = 'pool projection replay changed content'; + end if; + perform programmable_private.append_mutation_audit( + 'pool_projection.stage', launch.launch_hash, p_run_id, p_verified_at + ); + return p_pool_projection_id; +end +$function$; + +create function programmable_private.stage_pool_fee_configuration( + p_pool_fee_configuration_id uuid, + p_pool_projection_id uuid, + p_run_id uuid, + p_buy_swap_fee_bps numeric, + p_sell_swap_fee_bps numeric, + p_creator_fee_bps numeric, + p_launcher_fee_bps numeric, + p_transfer_tax_bps numeric, + p_lp_fee_pips numeric, + p_disclosure_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + pool programmable_private.pool_projections%rowtype; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_disclosure_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into pool from programmable_private.pool_projections + where pool_projection_id = p_pool_projection_id + and projection_run_id = p_run_id; + if pool.pool_projection_id is null + or pool.chain_id <> scope.chain_id + or pool.release_id <> scope.release_id + or pool.model_id <> scope.model_id + or pool.epoch_id <> scope.epoch_id + or pool.pointer_generation <> scope.pointer_generation + or pool.promoted_block_number <> scope.promoted_block_number + or pool.promoted_block_hash <> scope.promoted_block_hash + or p_buy_swap_fee_bps is null + or p_buy_swap_fee_bps <> pg_catalog.trunc(p_buy_swap_fee_bps) + or p_buy_swap_fee_bps < 0 or p_buy_swap_fee_bps > 10000 + or p_sell_swap_fee_bps is null + or p_sell_swap_fee_bps <> pg_catalog.trunc(p_sell_swap_fee_bps) + or p_sell_swap_fee_bps < 0 or p_sell_swap_fee_bps > 10000 + or p_creator_fee_bps is null + or p_creator_fee_bps <> pg_catalog.trunc(p_creator_fee_bps) + or p_creator_fee_bps < 0 or p_creator_fee_bps > 10000 + or p_launcher_fee_bps is null + or p_launcher_fee_bps <> pg_catalog.trunc(p_launcher_fee_bps) + or p_launcher_fee_bps < 0 or p_launcher_fee_bps > 10000 + or p_transfer_tax_bps is null + or p_transfer_tax_bps <> pg_catalog.trunc(p_transfer_tax_bps) + or p_transfer_tax_bps < 0 or p_transfer_tax_bps > 10000 + or p_lp_fee_pips is null + or p_lp_fee_pips <> pg_catalog.trunc(p_lp_fee_pips) + or p_lp_fee_pips < 0 or p_lp_fee_pips > 1000000 + then + raise exception using errcode = '23514', message = 'pool fee scope or values mismatch'; + end if; + insert into programmable_private.pool_fee_configurations as target ( + pool_fee_configuration_id, pool_projection_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, buy_swap_fee_bps, + sell_swap_fee_bps, buy_creator_fee_bps, sell_creator_fee_bps, + creator_fee_bps, launcher_fee_bps, transfer_tax_bps, + lp_fee_pips, disclosure_source_occurrence_id, + disclosure_source_logical_event_id, + disclosure_source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_pool_fee_configuration_id, p_pool_projection_id, scope.chain_id, + scope.release_id, scope.model_id, scope.epoch_id, scope.pointer_generation, + p_buy_swap_fee_bps::programmable_private.basis_points, + p_sell_swap_fee_bps::programmable_private.basis_points, + p_creator_fee_bps::programmable_private.basis_points, + p_creator_fee_bps::programmable_private.basis_points, + p_creator_fee_bps::programmable_private.basis_points, + p_launcher_fee_bps::programmable_private.basis_points, + p_transfer_tax_bps::programmable_private.basis_points, + p_lp_fee_pips::bigint, p_disclosure_source_occurrence_id, + scope.source_logical_event_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ) + on conflict (pool_fee_configuration_id) do update + set pool_fee_configuration_id = excluded.pool_fee_configuration_id + where target is not distinct from excluded + returning pool_fee_configuration_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'pool fee replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'pool_fee_configuration.stage', p_promoted_block_hash, p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_fee_accrual_fact( + p_fee_accrual_fact_id uuid, + p_run_id uuid, + p_pool_id bytea, + p_quote_asset bytea, + p_gross_amount numeric, + p_creator_fee numeric, + p_launcher_fee numeric, + p_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + gross numeric; + creator_amount numeric; + launcher_amount numeric; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + gross := programmable_private.validate_uint256(p_gross_amount); + creator_amount := programmable_private.validate_uint256(p_creator_fee); + launcher_amount := programmable_private.validate_uint256(p_launcher_fee); + if pg_catalog.octet_length(p_pool_id) <> 32 + or (p_quote_asset is not null and pg_catalog.octet_length(p_quote_asset) <> 20) + or creator_amount + launcher_amount > gross + then + raise exception using errcode = '22023', message = 'invalid fee accrual'; + end if; + insert into programmable_private.fee_accrual_facts as target ( + fee_accrual_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, pool_id, quote_asset, gross_amount, creator_fee, + launcher_fee, source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash, projection_run_id, promoted_block_number, + promoted_block_hash, verified_at + ) values ( + p_fee_accrual_fact_id, scope.chain_id, scope.release_id, scope.model_id, + scope.epoch_id, scope.pointer_generation, + p_pool_id::programmable_private.bytes32_value, + case when p_quote_asset is null then null + else p_quote_asset::programmable_private.eth_address end, + gross, creator_amount, launcher_amount, p_source_occurrence_id, + scope.source_logical_event_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ) + on conflict (fee_accrual_fact_id) do update + set fee_accrual_fact_id = excluded.fee_accrual_fact_id + where target is not distinct from excluded + returning fee_accrual_fact_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'fee accrual replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'fee_accrual.stage', p_promoted_block_hash, p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_pool_fee_total( + p_pool_fee_total_id uuid, + p_run_id uuid, + p_pool_id bytea, + p_quote_asset bytea, + p_gross_total numeric, + p_creator_fee_total numeric, + p_launcher_fee_total numeric, + p_last_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + gross numeric; + creator_amount numeric; + launcher_amount numeric; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_last_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + gross := programmable_private.validate_uint256(p_gross_total); + creator_amount := programmable_private.validate_uint256(p_creator_fee_total); + launcher_amount := programmable_private.validate_uint256(p_launcher_fee_total); + if pg_catalog.octet_length(p_pool_id) <> 32 + or (p_quote_asset is not null and pg_catalog.octet_length(p_quote_asset) <> 20) + or creator_amount + launcher_amount > gross + then + raise exception using errcode = '22023', message = 'invalid pool fee total'; + end if; + insert into programmable_private.pool_fee_totals as target ( + pool_fee_total_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, pool_id, quote_asset, gross_total, creator_fee_total, + launcher_fee_total, last_source_occurrence_id, + last_source_logical_event_id, last_source_occurrence_block_hash, + projection_run_id, promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_pool_fee_total_id, scope.chain_id, scope.release_id, scope.model_id, + scope.epoch_id, scope.pointer_generation, + p_pool_id::programmable_private.bytes32_value, + case when p_quote_asset is null then null + else p_quote_asset::programmable_private.eth_address end, + gross, creator_amount, launcher_amount, p_last_source_occurrence_id, + scope.source_logical_event_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ) + on conflict (pool_fee_total_id) do update + set pool_fee_total_id = excluded.pool_fee_total_id + where target is not distinct from excluded + returning pool_fee_total_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'pool fee total replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'pool_fee_total.stage', p_promoted_block_hash, p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_reward_vault_projection( + p_reward_vault_projection_id uuid, + p_launch_projection_id uuid, + p_run_id uuid, + p_vault bytea, + p_pool_id bytea, + p_quote_asset bytea, + p_configuration_hash bytea, + p_current_allocation_fact_id uuid, + p_last_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + launch programmable_private.launch_projections%rowtype; + fact programmable_private.reward_allocation_facts%rowtype; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_last_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into launch from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id + and projection_run_id = p_run_id; + select * into fact from programmable_private.reward_allocation_facts + where allocation_fact_id = p_current_allocation_fact_id; + if launch.launch_projection_id is null + or fact.allocation_fact_id is null + or launch.chain_id <> scope.chain_id + or launch.release_id <> scope.release_id + or launch.model_id <> scope.model_id + or launch.epoch_id <> scope.epoch_id + or launch.pointer_generation <> scope.pointer_generation + or launch.promoted_block_number <> scope.promoted_block_number + or launch.promoted_block_hash <> scope.promoted_block_hash + or fact.chain_id <> scope.chain_id + or fact.release_id <> scope.release_id + or fact.model_id <> scope.model_id + or fact.epoch_id <> scope.epoch_id + or fact.pointer_generation <> scope.pointer_generation + or fact.vault <> p_vault + or fact.configuration_hash <> p_configuration_hash + or launch.reward_vault is distinct from p_vault + or launch.pool_id <> p_pool_id + or pg_catalog.octet_length(p_vault) <> 20 + or pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_configuration_hash) <> 32 + or (p_quote_asset is not null and pg_catalog.octet_length(p_quote_asset) <> 20) + then + raise exception using errcode = '23514', message = 'reward vault projection mismatch'; + end if; + insert into programmable_private.reward_vault_projections as target ( + reward_vault_projection_id, launch_projection_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, vault, pool_id, quote_asset, + configuration_hash, current_allocation_fact_id, + last_source_logical_event_id, last_source_occurrence_id, + last_source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_reward_vault_projection_id, p_launch_projection_id, scope.chain_id, + scope.release_id, scope.model_id, scope.epoch_id, scope.pointer_generation, + p_vault::programmable_private.eth_address, + p_pool_id::programmable_private.bytes32_value, + case when p_quote_asset is null then null + else p_quote_asset::programmable_private.eth_address end, + p_configuration_hash::programmable_private.bytes32_value, + p_current_allocation_fact_id, scope.source_logical_event_id, + p_last_source_occurrence_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ) + on conflict (reward_vault_projection_id) do update + set reward_vault_projection_id = excluded.reward_vault_projection_id + where target is not distinct from excluded + returning reward_vault_projection_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'reward vault replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'reward_vault_projection.stage', p_configuration_hash, p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_reward_allocation_projection( + p_reward_allocation_projection_id uuid, + p_reward_vault_projection_id uuid, + p_run_id uuid, + p_allocation_fact_id uuid, + p_configuration_epoch bigint, + p_allocation_index integer, + p_beneficiary bytea, + p_payout_address bytea, + p_share_bps numeric, + p_effective_from_block numeric, + p_effective_to_block numeric, + p_last_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + vault programmable_private.reward_vault_projections%rowtype; + fact programmable_private.reward_allocation_facts%rowtype; + from_block bigint; + to_block bigint; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_last_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into vault from programmable_private.reward_vault_projections + where reward_vault_projection_id = p_reward_vault_projection_id + and projection_run_id = p_run_id; + select * into fact from programmable_private.reward_allocation_facts + where allocation_fact_id = p_allocation_fact_id; + if p_effective_from_block <> pg_catalog.trunc(p_effective_from_block) + or p_effective_from_block < 0 + or p_effective_from_block > 9223372036854775807 + or (p_effective_to_block is not null and ( + p_effective_to_block <> pg_catalog.trunc(p_effective_to_block) + or p_effective_to_block < p_effective_from_block + or p_effective_to_block > 9223372036854775807 + )) + then + raise exception using errcode = '22023', message = 'invalid allocation effective range'; + end if; + from_block := p_effective_from_block::bigint; + to_block := case when p_effective_to_block is null then null + else p_effective_to_block::bigint end; + if vault.reward_vault_projection_id is null + or fact.allocation_fact_id is null + or vault.chain_id <> scope.chain_id + or vault.release_id <> scope.release_id + or vault.model_id <> scope.model_id + or vault.epoch_id <> scope.epoch_id + or vault.pointer_generation <> scope.pointer_generation + or vault.promoted_block_number <> scope.promoted_block_number + or vault.promoted_block_hash <> scope.promoted_block_hash + or vault.current_allocation_fact_id <> p_allocation_fact_id + or fact.epoch_id <> scope.epoch_id + or fact.pointer_generation <> scope.pointer_generation + or p_configuration_epoch <= 0 + or p_allocation_index < 0 + or p_allocation_index >= pg_catalog.array_length(fact.ordered_beneficiaries, 1) + or fact.ordered_beneficiaries[p_allocation_index + 1] <> p_beneficiary + or fact.ordered_shares_bps[p_allocation_index + 1] <> p_share_bps + or pg_catalog.octet_length(p_beneficiary) <> 20 + or pg_catalog.octet_length(p_payout_address) <> 20 + or p_share_bps <> pg_catalog.trunc(p_share_bps) + or from_block > scope.promoted_block_number + then + raise exception using errcode = '23514', message = 'reward allocation projection mismatch'; + end if; + insert into programmable_private.reward_allocation_projections as target ( + reward_allocation_projection_id, reward_vault_projection_id, + allocation_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, configuration_epoch, allocation_index, beneficiary, + payout_address, share_bps, effective_from_block, effective_to_block, + last_source_logical_event_id, last_source_occurrence_id, + last_source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_reward_allocation_projection_id, p_reward_vault_projection_id, + p_allocation_fact_id, scope.chain_id, scope.release_id, scope.model_id, + scope.epoch_id, scope.pointer_generation, p_configuration_epoch, + p_allocation_index, p_beneficiary::programmable_private.eth_address, + p_payout_address::programmable_private.eth_address, + p_share_bps::programmable_private.basis_points, + from_block::programmable_private.block_number_value, + to_block, + scope.source_logical_event_id, p_last_source_occurrence_id, + scope.source_occurrence_block_hash, p_run_id, + scope.promoted_block_number, scope.promoted_block_hash, p_verified_at + ) + on conflict (reward_allocation_projection_id) do update + set reward_allocation_projection_id = excluded.reward_allocation_projection_id + where target is not distinct from excluded + returning reward_allocation_projection_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'reward allocation replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'reward_allocation_projection.stage', fact.configuration_hash, + p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_claim_projection( + p_claim_projection_id uuid, + p_run_id uuid, + p_vault bytea, + p_claimant_kind text, + p_beneficiary bytea, + p_recipient bytea, + p_amount numeric, + p_beneficiary_total_claimed numeric, + p_vault_total_received numeric, + p_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + claim_amount numeric; + claimant_total numeric; + vault_total numeric; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + claim_amount := programmable_private.validate_uint256(p_amount); + claimant_total := programmable_private.validate_uint256(p_beneficiary_total_claimed); + vault_total := programmable_private.validate_uint256(p_vault_total_received); + if p_claimant_kind not in ('beneficiary', 'creator', 'launcher') + or pg_catalog.octet_length(p_vault) <> 20 + or pg_catalog.octet_length(p_beneficiary) <> 20 + or pg_catalog.octet_length(p_recipient) <> 20 + or claim_amount > claimant_total + or claim_amount > vault_total + then + raise exception using errcode = '22023', message = 'invalid claim projection'; + end if; + insert into programmable_private.claim_projections as target ( + claim_projection_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, vault, claimant_kind, beneficiary, recipient, amount, + beneficiary_total_claimed, vault_total_received, source_occurrence_id, + source_logical_event_id, source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_claim_projection_id, scope.chain_id, scope.release_id, scope.model_id, + scope.epoch_id, scope.pointer_generation, + p_vault::programmable_private.eth_address, + p_claimant_kind::programmable_private.source_identifier, + p_beneficiary::programmable_private.eth_address, + p_recipient::programmable_private.eth_address, claim_amount, + claimant_total, vault_total, p_source_occurrence_id, + scope.source_logical_event_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ) + on conflict (claim_projection_id) do update + set claim_projection_id = excluded.claim_projection_id + where target is not distinct from excluded + returning claim_projection_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'claim replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'claim_projection.stage', p_promoted_block_hash, p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_payout_change_projection( + p_payout_change_projection_id uuid, + p_run_id uuid, + p_vault bytea, + p_beneficiary bytea, + p_previous_payout_address bytea, + p_new_payout_address bytea, + p_configuration_epoch bigint, + p_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + if pg_catalog.octet_length(p_vault) <> 20 + or pg_catalog.octet_length(p_beneficiary) <> 20 + or pg_catalog.octet_length(p_previous_payout_address) <> 20 + or pg_catalog.octet_length(p_new_payout_address) <> 20 + or p_previous_payout_address = p_new_payout_address + or (p_configuration_epoch is not null and p_configuration_epoch <= 0) + then + raise exception using errcode = '22023', message = 'invalid payout change projection'; + end if; + insert into programmable_private.payout_change_projections as target ( + payout_change_projection_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, vault, beneficiary, previous_payout_address, + new_payout_address, configuration_epoch, source_occurrence_id, + source_logical_event_id, source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_payout_change_projection_id, scope.chain_id, scope.release_id, + scope.model_id, scope.epoch_id, scope.pointer_generation, + p_vault::programmable_private.eth_address, + p_beneficiary::programmable_private.eth_address, + p_previous_payout_address::programmable_private.eth_address, + p_new_payout_address::programmable_private.eth_address, + p_configuration_epoch, p_source_occurrence_id, + scope.source_logical_event_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ) + on conflict (payout_change_projection_id) do update + set payout_change_projection_id = excluded.payout_change_projection_id + where target is not distinct from excluded + returning payout_change_projection_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'payout change replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'payout_change_projection.stage', p_promoted_block_hash, + p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_account_reward_balance( + p_account_reward_balance_id uuid, + p_run_id uuid, + p_account bytea, + p_vault bytea, + p_claimable_accrued numeric, + p_claimed_total numeric, + p_last_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + source programmable_private.chain_event_occurrences%rowtype; + scope record; + accrued numeric; + claimed numeric; + block_number bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_last_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection'; + if not found then + raise exception using errcode = '23503', message = 'invalid projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + accrued := programmable_private.validate_uint256(p_claimable_accrued); + claimed := programmable_private.validate_uint256(p_claimed_total); + if p_promoted_block_number <> pg_catalog.trunc(p_promoted_block_number) + or p_promoted_block_number < 0 + or p_promoted_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_account) <> 20 + or pg_catalog.octet_length(p_vault) <> 20 + or pg_catalog.octet_length(p_promoted_block_hash) <> 32 + then + raise exception using errcode = '22023', message = 'invalid reward balance projection'; + end if; + block_number := p_promoted_block_number::bigint; + select * into source + from programmable_private.chain_event_occurrences + where occurrence_id = p_last_source_occurrence_id; + if not found or source.block_number > block_number then + raise exception using errcode = '23503', message = 'reward balance source mismatch'; + end if; + insert into programmable_private.account_reward_balances ( + account_reward_balance_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, account, + vault, claimable_accrued, claimed_total, last_source_logical_event_id, + last_source_occurrence_id, last_source_occurrence_block_hash, + projection_run_id, promoted_block_number, promoted_block_hash, verified_at + ) + values ( + p_account_reward_balance_id, header.chain_id, header.release_id, + header.model_id, header.epoch_id, header.captured_pointer_generation, + p_account::programmable_private.eth_address, + p_vault::programmable_private.eth_address, accrued, claimed, + source.logical_event_id, source.occurrence_id, source.block_hash, + p_run_id, block_number::programmable_private.block_number_value, + p_promoted_block_hash::programmable_private.bytes32_value, p_verified_at + ) + on conflict (account_reward_balance_id) do nothing; + if not found and not exists ( + select 1 from programmable_private.account_reward_balances + where account_reward_balance_id = p_account_reward_balance_id + and projection_run_id = p_run_id + and account = p_account and vault = p_vault + and claimable_accrued = accrued and claimed_total = claimed + and last_source_occurrence_id = p_last_source_occurrence_id + and promoted_block_number = block_number + and promoted_block_hash = p_promoted_block_hash + ) then + raise exception using errcode = '23505', message = 'reward balance replay changed content'; + end if; + perform programmable_private.append_mutation_audit( + 'account_reward_balance.stage', p_promoted_block_hash, p_run_id, p_verified_at + ); + return p_account_reward_balance_id; +end +$function$; + +create function programmable_private.stage_initial_buy_custody_projection( + p_custody_projection_id uuid, + p_launch_projection_id uuid, + p_run_id uuid, + p_custody_address bytea, + p_custody_mode smallint, + p_duration_days integer, + p_cliff_days integer, + p_configuration_hash bytea, + p_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + launch programmable_private.launch_projections%rowtype; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into launch from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id + and projection_run_id = p_run_id; + if launch.launch_projection_id is null + or launch.chain_id <> scope.chain_id + or launch.release_id <> scope.release_id + or launch.model_id <> scope.model_id + or launch.epoch_id <> scope.epoch_id + or launch.pointer_generation <> scope.pointer_generation + or launch.promoted_block_number <> scope.promoted_block_number + or launch.promoted_block_hash <> scope.promoted_block_hash + or pg_catalog.octet_length(p_custody_address) <> 20 + or p_custody_mode < 0 + or p_duration_days < 0 or p_duration_days > 65535 + or p_cliff_days < 0 or p_cliff_days > p_duration_days + or pg_catalog.octet_length(p_configuration_hash) <> 32 + then + raise exception using errcode = '23514', message = 'custody projection mismatch'; + end if; + insert into programmable_private.initial_buy_custody_projections as target ( + custody_projection_id, launch_projection_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, custody_address, custody_mode, + duration_days, cliff_days, configuration_hash, source_occurrence_id, + source_logical_event_id, source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_custody_projection_id, p_launch_projection_id, scope.chain_id, + scope.release_id, scope.model_id, scope.epoch_id, scope.pointer_generation, + p_custody_address::programmable_private.eth_address, p_custody_mode, + p_duration_days, p_cliff_days, + p_configuration_hash::programmable_private.bytes32_value, + p_source_occurrence_id, scope.source_logical_event_id, + scope.source_occurrence_block_hash, p_run_id, + scope.promoted_block_number, scope.promoted_block_hash, p_verified_at + ) + on conflict (custody_projection_id) do update + set custody_projection_id = excluded.custody_projection_id + where target is not distinct from excluded + returning custody_projection_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'custody replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'initial_buy_custody.stage', p_configuration_hash, p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.stage_initial_buy_vesting_projection( + p_vesting_projection_id uuid, + p_custody_projection_id uuid, + p_run_id uuid, + p_beneficiary bytea, + p_token bytea, + p_amount numeric, + p_vesting_start timestamptz, + p_vesting_end timestamptz, + p_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + custody programmable_private.initial_buy_custody_projections%rowtype; + vesting_amount numeric; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into custody + from programmable_private.initial_buy_custody_projections + where custody_projection_id = p_custody_projection_id + and projection_run_id = p_run_id; + vesting_amount := programmable_private.validate_uint256(p_amount); + if custody.custody_projection_id is null + or custody.chain_id <> scope.chain_id + or custody.release_id <> scope.release_id + or custody.model_id <> scope.model_id + or custody.epoch_id <> scope.epoch_id + or custody.pointer_generation <> scope.pointer_generation + or custody.promoted_block_number <> scope.promoted_block_number + or custody.promoted_block_hash <> scope.promoted_block_hash + or pg_catalog.octet_length(p_beneficiary) <> 20 + or pg_catalog.octet_length(p_token) <> 20 + or p_vesting_end < p_vesting_start + then + raise exception using errcode = '23514', message = 'vesting projection mismatch'; + end if; + insert into programmable_private.initial_buy_vesting_projections as target ( + vesting_projection_id, custody_projection_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, beneficiary, token, amount, + vesting_start, vesting_end, source_occurrence_id, + source_logical_event_id, source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_vesting_projection_id, p_custody_projection_id, scope.chain_id, + scope.release_id, scope.model_id, scope.epoch_id, scope.pointer_generation, + p_beneficiary::programmable_private.eth_address, + p_token::programmable_private.eth_address, vesting_amount, + p_vesting_start, p_vesting_end, p_source_occurrence_id, + scope.source_logical_event_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ) + on conflict (vesting_projection_id) do update + set vesting_projection_id = excluded.vesting_projection_id + where target is not distinct from excluded + returning vesting_projection_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'vesting replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'initial_buy_vesting.stage', p_promoted_block_hash, p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.promote_projection_run( + p_publication_id uuid, + p_checkpoint_id uuid, + p_outcome_id uuid, + p_run_id uuid, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_expected_checkpoint_generation bigint, + p_next_checkpoint_generation bigint, + p_reorg_generation bigint, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea, + p_cursor_block_global_log_index numeric, + p_cursor_candidate_id text, + p_occurrence_ids uuid[], + p_allocation_fact_ids uuid[], + p_allocation_evidence_ids uuid[], + p_candidate_disposition_ids uuid[], + p_route_keys text[], + p_result_commitment bytea, + p_published_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + observation programmable_private.safe_head_observations%rowtype; + target_evidence programmable_private.dual_rpc_block_evidence%rowtype; + current_checkpoint programmable_private.projector_checkpoint_current%rowtype; + previous_checkpoint programmable_private.projector_checkpoints%rowtype; + occurrence programmable_private.chain_event_occurrences%rowtype; + occurrence_materialization programmable_private.chain_event_occurrence_materializations%rowtype; + fact programmable_private.reward_allocation_facts%rowtype; + selected_evidence_id uuid; + selected_occurrence_id uuid; + selected_fact_id uuid; + selected_route_key text; + target_block bigint; + cursor_log_index bigint; + audit_id uuid; + status_id uuid; + route_history_id uuid; + idx integer; + ordered_occurrence_ids uuid[]; + ordered_fact_ids uuid[]; + ordered_disposition_ids uuid[]; + required_disposition_ids uuid[]; + ordered_route_keys text[]; + ordered_projection_rows text[]; + projection_row_count bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection' + for update; + if not found then + raise exception using errcode = '23503', message = 'invalid projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is already terminal'; + end if; + if not exists ( + select 1 + from programmable_private.projector_lease_current as lease + where lease.chain_id = header.chain_id + and lease.release_id = header.release_id + and lease.model_id = header.model_id + and lease.source_group = header.source_group + and lease.projector_version = p_projector_version + and lease.epoch_id = header.epoch_id + and lease.pointer_generation = header.captured_pointer_generation + and lease.lease_generation = p_lease_generation + and lease.lease_token_hash = p_lease_token_hash + and lease.expires_at >= p_published_at + ) then + raise exception using errcode = '40001', message = 'stale projector lease'; + end if; + if p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or p_cursor_block_global_log_index + <> pg_catalog.trunc(p_cursor_block_global_log_index) + or p_cursor_block_global_log_index < 0 + or p_cursor_block_global_log_index > 4294967295 + or p_cursor_candidate_id is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + or p_next_checkpoint_generation <> p_expected_checkpoint_generation + 1 + or ( + coalesce(pg_catalog.array_length(p_occurrence_ids, 1), 0) = 0 + and coalesce( + pg_catalog.array_length(p_candidate_disposition_ids, 1), 0 + ) = 0 + ) + or coalesce(pg_catalog.array_length(p_route_keys, 1), 0) = 0 + or coalesce(pg_catalog.array_length(p_allocation_fact_ids, 1), 0) + <> coalesce(pg_catalog.array_length(p_allocation_evidence_ids, 1), 0) + then + raise exception using errcode = '22023', message = 'invalid promotion request'; + end if; + select pg_catalog.array_agg(item order by item) + into ordered_occurrence_ids + from (select distinct item from pg_catalog.unnest(p_occurrence_ids) as item) as unique_items; + select pg_catalog.array_agg(item order by item) + into ordered_fact_ids + from (select distinct item from pg_catalog.unnest(p_allocation_fact_ids) as item) as unique_items; + select pg_catalog.array_agg(item order by item) + into ordered_disposition_ids + from ( + select distinct item + from pg_catalog.unnest(p_candidate_disposition_ids) as item + ) as unique_items; + select pg_catalog.array_agg(item order by item) + into ordered_route_keys + from (select distinct item from pg_catalog.unnest(p_route_keys) as item) as unique_items; + if p_occurrence_ids is distinct from + coalesce(ordered_occurrence_ids, array[]::uuid[]) + or p_allocation_fact_ids is distinct from coalesce(ordered_fact_ids, array[]::uuid[]) + or p_candidate_disposition_ids is distinct from + coalesce(ordered_disposition_ids, array[]::uuid[]) + or p_route_keys is distinct from ordered_route_keys + or exists (select 1 from pg_catalog.unnest(p_occurrence_ids) as item where item is null) + or exists (select 1 from pg_catalog.unnest(p_allocation_fact_ids) as item where item is null) + or exists (select 1 from pg_catalog.unnest(p_allocation_evidence_ids) as item where item is null) + or exists (select 1 from pg_catalog.unnest(p_candidate_disposition_ids) as item where item is null) + or exists (select 1 from pg_catalog.unnest(p_route_keys) as item where item is null) + or coalesce(pg_catalog.array_length(p_allocation_evidence_ids, 1), 0) + <> coalesce(( + select pg_catalog.count(distinct item) + from pg_catalog.unnest(p_allocation_evidence_ids) as item + ), 0) + then + raise exception using + errcode = '22023', + message = 'promotion arrays must be non-null, unique and canonically ordered'; + end if; + target_block := p_target_block_number::bigint; + cursor_log_index := p_cursor_block_global_log_index::bigint; + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_safe_head_observation_id; + if not found + or observation.epoch_id <> header.epoch_id + or observation.pointer_generation <> header.captured_pointer_generation + or target_block > observation.safe_block_number + then + raise exception using errcode = '23514', message = 'target is outside accepted safe head'; + end if; + select * into target_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_target_block_evidence_id + and observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found + or target_evidence.block_number <> target_block + or target_evidence.agreed_block_hash <> p_target_block_hash + then + raise exception using errcode = '23514', message = 'target/checkpoint hash is not bound evidence'; + end if; + select * into current_checkpoint + from programmable_private.projector_checkpoint_current + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + for update; + if found then + if current_checkpoint.checkpoint_generation <> p_expected_checkpoint_generation + or current_checkpoint.reorg_generation <> p_reorg_generation + then + raise exception using errcode = '40001', message = 'checkpoint CAS lost'; + end if; + select * into previous_checkpoint + from programmable_private.projector_checkpoints + where checkpoint_id = current_checkpoint.checkpoint_id; + if not found then + raise exception using errcode = '23503', message = 'current checkpoint identity is missing'; + end if; + elsif p_expected_checkpoint_generation <> 0 or p_reorg_generation <> 0 then + raise exception using errcode = '40001', message = 'checkpoint CAS lost'; + end if; + if not exists ( + select 1 from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = p_cursor_candidate_id + and candidate.chain_id = header.chain_id + and candidate.block_number = target_block + and candidate.block_hash = p_target_block_hash + and candidate.block_global_log_index = cursor_log_index + ) then + raise exception using + errcode = '23514', + message = 'checkpoint cursor does not match its exact neutral inbox row'; + end if; + if exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + left join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + where candidate.chain_id = header.chain_id + and ( + previous_checkpoint.checkpoint_id is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id) + and coalesce(status.status::text, 'pending') + not in ('resolved', 'ignored', 'quarantined') + ) then + raise exception using + errcode = '23514', + message = 'checkpoint cursor cannot pass a pending or deferred candidate'; + end if; + select pg_catalog.array_agg(status.decision_id order by status.decision_id) + into required_disposition_ids + from programmable_private.envio_candidate_inbox as candidate + join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + and status.status in ('resolved', 'ignored', 'quarantined') + where candidate.chain_id = header.chain_id + and ( + previous_checkpoint.checkpoint_id is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id); + if p_candidate_disposition_ids is distinct from + coalesce(required_disposition_ids, array[]::uuid[]) + then + raise exception using + errcode = '23514', + message = 'candidate disposition manifest is incomplete or noncanonical'; + end if; + if not exists ( + select 1 + from programmable_private.launch_projections + where projection_run_id = p_run_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and promoted_block_number = target_block + and promoted_block_hash = p_target_block_hash + and is_complete + ) then + raise exception using errcode = '23514', message = 'run has no complete launch projection'; + end if; + if exists ( + select 1 + from ( + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.launch_projections + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.pool_projections + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.pool_fee_configurations + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.fee_accrual_facts + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.pool_fee_totals + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.reward_vault_projections + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.reward_allocation_projections + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.claim_projections + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.payout_change_projections + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.account_reward_balances + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.initial_buy_custody_projections + union all + select projection_run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, promoted_block_number, promoted_block_hash + from programmable_private.initial_buy_vesting_projections + ) as staged + where staged.projection_run_id = p_run_id + and ( + staged.chain_id <> header.chain_id + or staged.release_id <> header.release_id + or staged.model_id <> header.model_id + or staged.epoch_id <> header.epoch_id + or staged.pointer_generation <> header.captured_pointer_generation + or staged.promoted_block_number <> target_block + or staged.promoted_block_hash <> p_target_block_hash + ) + ) then + raise exception using + errcode = '23514', + message = 'every staged projection row must bind the exact promotion scope and target'; + end if; + if exists ( + select 1 + from programmable_private.launch_projections as launch + where launch.projection_run_id = p_run_id + and launch.is_complete + and not ( + select pg_catalog.count(*) = 1 + and pg_catalog.count(fee.pool_fee_configuration_id) = 1 + from programmable_private.pool_projections as pool + left join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = pool.projection_run_id + and fee.chain_id = pool.chain_id + and fee.release_id = pool.release_id + and fee.model_id = pool.model_id + and fee.epoch_id = pool.epoch_id + and fee.pointer_generation = pool.pointer_generation + and fee.promoted_block_number = pool.promoted_block_number + and fee.promoted_block_hash = pool.promoted_block_hash + where pool.projection_run_id = p_run_id + and pool.launch_projection_id = launch.launch_projection_id + and pool.pool_id = launch.pool_id + and (pool.currency0 = launch.token or pool.currency1 = launch.token) + ) + ) then + raise exception using + errcode = '23514', + message = 'each complete launch requires exactly one token-bound PoolKey and fee configuration'; + end if; + if exists ( + select 1 + from pg_catalog.unnest(p_allocation_fact_ids) as selected(allocation_fact_id) + join programmable_private.reward_allocation_facts as selected_fact + on selected_fact.allocation_fact_id = selected.allocation_fact_id + where not exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and vault.current_allocation_fact_id = selected.allocation_fact_id + and vault.vault = selected_fact.vault + and ( + select pg_catalog.count(*) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id + = vault.reward_vault_projection_id + and allocation.allocation_fact_id = selected.allocation_fact_id + ) = pg_catalog.array_length(selected_fact.ordered_beneficiaries, 1) + and ( + select pg_catalog.count(distinct allocation.allocation_index) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id + = vault.reward_vault_projection_id + and allocation.allocation_fact_id = selected.allocation_fact_id + ) = pg_catalog.array_length(selected_fact.ordered_beneficiaries, 1) + and ( + select pg_catalog.count(distinct allocation.configuration_epoch) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id + = vault.reward_vault_projection_id + and allocation.allocation_fact_id = selected.allocation_fact_id + ) = 1 + and ( + select coalesce(pg_catalog.sum(allocation.share_bps), 0) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id + = vault.reward_vault_projection_id + and allocation.allocation_fact_id = selected.allocation_fact_id + ) = 10000 + ) + ) or exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and not (vault.current_allocation_fact_id = any(p_allocation_fact_ids)) + ) then + raise exception using + errcode = '23514', + message = 'selected reward facts require complete vault and allocation projections'; + end if; + if exists ( + with staged_sources(source_occurrence_id) as ( + select last_source_occurrence_id from programmable_private.launch_projections + where projection_run_id = p_run_id + union select last_source_occurrence_id from programmable_private.pool_projections + where projection_run_id = p_run_id + union select disclosure_source_occurrence_id from programmable_private.pool_fee_configurations + where projection_run_id = p_run_id + union select source_occurrence_id from programmable_private.fee_accrual_facts + where projection_run_id = p_run_id + union select last_source_occurrence_id from programmable_private.pool_fee_totals + where projection_run_id = p_run_id + union select last_source_occurrence_id from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + union select last_source_occurrence_id from programmable_private.reward_allocation_projections + where projection_run_id = p_run_id + union select source_occurrence_id from programmable_private.claim_projections + where projection_run_id = p_run_id + union select source_occurrence_id from programmable_private.payout_change_projections + where projection_run_id = p_run_id + union select last_source_occurrence_id from programmable_private.account_reward_balances + where projection_run_id = p_run_id + union select source_occurrence_id from programmable_private.initial_buy_custody_projections + where projection_run_id = p_run_id + union select source_occurrence_id from programmable_private.initial_buy_vesting_projections + where projection_run_id = p_run_id + ) + select 1 from staged_sources + where not (source_occurrence_id = any(p_occurrence_ids)) + and ( + cardinality(p_occurrence_ids) > 0 + or not exists ( + select 1 + from programmable_private.chain_event_current_canonical as canonical + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = canonical.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + where canonical.occurrence_id = staged_sources.source_occurrence_id + ) + ) + ) then + raise exception using + errcode = '23514', + message = 'promotion occurrence fold omits a staged projection source'; + end if; + foreach selected_occurrence_id in array p_occurrence_ids loop + select * into occurrence + from programmable_private.chain_event_occurrences as candidate_occurrence + where candidate_occurrence.occurrence_id = selected_occurrence_id; + select * into occurrence_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = selected_occurrence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found + or occurrence.occurrence_id is null + or occurrence.chain_id <> header.chain_id + or occurrence.block_number > target_block + or not exists ( + select 1 + from programmable_private.dual_rpc_block_evidence as source_evidence + where source_evidence.block_evidence_id = + occurrence_materialization.block_evidence_id + and source_evidence.observation_id = p_safe_head_observation_id + and source_evidence.epoch_id = header.epoch_id + and source_evidence.pointer_generation = header.captured_pointer_generation + and source_evidence.block_number = occurrence.block_number + and source_evidence.agreed_block_hash = occurrence.block_hash + ) + then + raise exception using errcode = '23514', message = 'occurrence lacks promotion-bound block evidence'; + end if; + if exists ( + select 1 + from programmable_private.chain_event_current_canonical as selected + where selected.logical_event_id = occurrence.logical_event_id + and selected.occurrence_id <> occurrence.occurrence_id + ) then + raise exception using errcode = '23505', message = 'competing canonical occurrence requires rewind'; + end if; + end loop; + + audit_id := programmable_private.append_mutation_audit( + 'projection.promote', p_result_commitment, p_run_id, p_published_at + ); + select + pg_catalog.array_agg( + pg_catalog.format('%s:%s', staged_rows.row_kind, staged_rows.row_id) + order by staged_rows.row_kind, staged_rows.row_id + ), + pg_catalog.count(*) + into ordered_projection_rows, projection_row_count + from ( + select 'launch'::text as row_kind, launch_projection_id as row_id + from programmable_private.launch_projections + where projection_run_id = p_run_id + union all select 'pool', pool_projection_id + from programmable_private.pool_projections + where projection_run_id = p_run_id + union all select 'pool_fee_configuration', pool_fee_configuration_id + from programmable_private.pool_fee_configurations + where projection_run_id = p_run_id + union all select 'fee_accrual', fee_accrual_fact_id + from programmable_private.fee_accrual_facts + where projection_run_id = p_run_id + union all select 'pool_fee_total', pool_fee_total_id + from programmable_private.pool_fee_totals + where projection_run_id = p_run_id + union all select 'reward_vault', reward_vault_projection_id + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + union all select 'reward_allocation', reward_allocation_projection_id + from programmable_private.reward_allocation_projections + where projection_run_id = p_run_id + union all select 'claim', claim_projection_id + from programmable_private.claim_projections + where projection_run_id = p_run_id + union all select 'payout_change', payout_change_projection_id + from programmable_private.payout_change_projections + where projection_run_id = p_run_id + union all select 'account_reward_balance', account_reward_balance_id + from programmable_private.account_reward_balances + where projection_run_id = p_run_id + union all select 'initial_buy_custody', custody_projection_id + from programmable_private.initial_buy_custody_projections + where projection_run_id = p_run_id + union all select 'initial_buy_vesting', vesting_projection_id + from programmable_private.initial_buy_vesting_projections + where projection_run_id = p_run_id + ) as staged_rows; + insert into programmable_private.projection_fold_manifests ( + run_id, epoch_id, pointer_generation, target_block_number, + target_block_hash, ordered_occurrence_ids, ordered_allocation_fact_ids, + ordered_allocation_evidence_ids, ordered_candidate_disposition_ids, + ordered_route_keys, cursor_block_global_log_index, cursor_candidate_id, + ordered_projection_rows, projection_row_count, + result_commitment, created_at, audit_id + ) values ( + p_run_id, header.epoch_id, header.captured_pointer_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_occurrence_ids, p_allocation_fact_ids, p_allocation_evidence_ids, + p_candidate_disposition_ids, p_route_keys, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + ordered_projection_rows, projection_row_count, + p_result_commitment::programmable_private.bytes32_value, + p_published_at, audit_id + ); + foreach selected_occurrence_id in array p_occurrence_ids loop + select * into occurrence + from programmable_private.chain_event_occurrences as candidate_occurrence + where candidate_occurrence.occurrence_id = selected_occurrence_id; + select * into occurrence_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = selected_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.chain_event_occurrence_status_history ( + status_history_id, occurrence_id, logical_event_id, block_hash, status, + safe_head_observation_id, block_evidence_id, decision_run_id, + decision_commitment, decided_at, audit_id + ) + values ( + status_id, occurrence.occurrence_id, occurrence.logical_event_id, + occurrence.block_hash, 'canonical', p_safe_head_observation_id, + occurrence_materialization.block_evidence_id, + p_run_id, p_result_commitment, + p_published_at, audit_id + ); + insert into programmable_private.chain_event_current_canonical ( + logical_event_id, occurrence_id, block_hash, status_history_id, + selected_by_run_id, selected_at + ) + values ( + occurrence.logical_event_id, occurrence.occurrence_id, + occurrence.block_hash, status_id, p_run_id, p_published_at + ) + on conflict (logical_event_id) do update + set status_history_id = excluded.status_history_id, + selected_by_run_id = excluded.selected_by_run_id, + selected_at = excluded.selected_at + where programmable_private.chain_event_current_canonical.occurrence_id + = excluded.occurrence_id; + if not found then + raise exception using errcode = '23505', message = 'canonical pointer conflict'; + end if; + end loop; + + if coalesce(pg_catalog.array_length(p_allocation_fact_ids, 1), 0) > 0 then + for idx in 1..pg_catalog.array_length(p_allocation_fact_ids, 1) loop + selected_fact_id := p_allocation_fact_ids[idx]; + selected_evidence_id := p_allocation_evidence_ids[idx]; + select * into fact + from programmable_private.reward_allocation_facts + where allocation_fact_id = selected_fact_id; + if not found + or fact.epoch_id <> header.epoch_id + or fact.pointer_generation <> header.captured_pointer_generation + or not exists ( + select 1 + from programmable_private.reward_allocation_evidence as evidence + join programmable_private.run_headers as evidence_run + on evidence_run.run_id = evidence.verification_run_id + and evidence_run.chain_id = fact.chain_id + and evidence_run.release_id = fact.release_id + and evidence_run.model_id = fact.model_id + and evidence_run.epoch_id = fact.epoch_id + and evidence_run.captured_pointer_generation + = fact.pointer_generation + where evidence.allocation_evidence_id = selected_evidence_id + and evidence.allocation_fact_id = selected_fact_id + and evidence.recomputed_allocation_hash = fact.allocation_hash + and evidence.recomputed_configuration_hash = fact.configuration_hash + and evidence.is_recomputation_attested + and evidence.recomputed_active_configuration_hash + is not distinct from fact.active_configuration_hash + and recovery_release_binding_commitment = ( + select binding.binding_commitment + from programmable_private.release_source_bindings as binding + where binding.binding_id = recovery_release_binding_id + and binding.epoch_id = fact.epoch_id + ) + ) + or not exists ( + select 1 + from programmable_private.chain_event_current_canonical as canonical + join programmable_private.chain_event_occurrence_materializations + as factory_materialization + on factory_materialization.occurrence_id = canonical.occurrence_id + and factory_materialization.epoch_id = fact.epoch_id + and factory_materialization.pointer_generation = + fact.pointer_generation + where canonical.occurrence_id = fact.factory_occurrence_id + ) + or exists ( + select 1 + from programmable_private.reward_allocation_status_history + as rejected_status + where rejected_status.allocation_fact_id = selected_fact_id + and rejected_status.status in ( + 'quarantined', 'orphaned', 'conflicted', 'revoked' + ) + ) + or exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences as required + where required.allocation_fact_id = selected_fact_id + and not exists ( + select 1 + from programmable_private.chain_event_current_canonical as selected + join programmable_private.chain_event_occurrences as required_source + on required_source.occurrence_id = selected.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as required_materialization + on required_materialization.occurrence_id = + selected.occurrence_id + and required_materialization.epoch_id = fact.epoch_id + and required_materialization.pointer_generation = + fact.pointer_generation + join programmable_private.release_source_bindings as binding + on binding.binding_id = + required_materialization.release_binding_id + where selected.occurrence_id = required.occurrence_id + and binding.binding_id = required.release_binding_id + and binding.binding_commitment + = required.release_binding_commitment + and binding.source_role = required.occurrence_role + ) + ) + then + raise exception using errcode = '23514', message = 'allocation evidence is not promotion eligible'; + end if; + if exists ( + select 1 + from programmable_private.reward_allocation_current_verified as current_seed + where current_seed.factory_occurrence_id = fact.factory_occurrence_id + and current_seed.vault = fact.vault + and current_seed.allocation_fact_id <> selected_fact_id + ) then + raise exception using errcode = '23505', message = 'conflicting verified allocation'; + end if; + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_allocation_status_history ( + seed_status_history_id, allocation_fact_id, allocation_evidence_id, + status, reason_commitment, decision_run_id, decided_at, audit_id + ) + values ( + status_id, selected_fact_id, selected_evidence_id, 'verified', + p_result_commitment::programmable_private.bytes32_value, + p_run_id, p_published_at, audit_id + ); + insert into programmable_private.reward_allocation_current_verified ( + factory_occurrence_id, vault, allocation_fact_id, + allocation_evidence_id, seed_status_history_id, + selected_by_run_id, selected_at + ) + values ( + fact.factory_occurrence_id, fact.vault, + selected_fact_id, selected_evidence_id, + status_id, p_run_id, p_published_at + ) + on conflict (factory_occurrence_id, vault) do update + set allocation_evidence_id = excluded.allocation_evidence_id, + seed_status_history_id = excluded.seed_status_history_id, + selected_by_run_id = excluded.selected_by_run_id, + selected_at = excluded.selected_at + where programmable_private.reward_allocation_current_verified.allocation_fact_id + = excluded.allocation_fact_id; + if not found then + raise exception using errcode = '23505', message = 'verified seed pointer conflict'; + end if; + end loop; + end if; + + insert into programmable_private.run_lifecycle_outcomes ( + outcome_id, run_id, status, result_commitment, caller_role, + finished_at, audit_id + ) + values ( + p_outcome_id, p_run_id, 'succeeded', + p_result_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_published_at, audit_id + ); + insert into programmable_private.projector_checkpoints ( + checkpoint_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, lease_generation, + checkpoint_generation, reorg_generation, block_number, block_hash, + cursor_block_global_log_index, cursor_candidate_id, + safe_head_observation_id, target_block_evidence_id, run_id, + terminal_outcome_id, created_at + ) + values ( + p_checkpoint_id, header.chain_id, header.release_id, header.model_id, + header.source_group, + p_projector_version::programmable_private.projector_identifier, + header.epoch_id, header.captured_pointer_generation, p_lease_generation, + p_next_checkpoint_generation, p_reorg_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + p_safe_head_observation_id, p_target_block_evidence_id, + p_run_id, p_outcome_id, p_published_at + ); + if p_expected_checkpoint_generation = 0 then + insert into programmable_private.projector_checkpoint_current ( + chain_id, release_id, model_id, source_group, projector_version, + checkpoint_id, checkpoint_generation, reorg_generation, changed_at + ) + values ( + header.chain_id, header.release_id, header.model_id, header.source_group, + p_projector_version::programmable_private.projector_identifier, + p_checkpoint_id, p_next_checkpoint_generation, p_reorg_generation, + p_published_at + ) + on conflict ( + chain_id, release_id, model_id, source_group, projector_version + ) do nothing; + else + update programmable_private.projector_checkpoint_current + set checkpoint_id = p_checkpoint_id, + checkpoint_generation = p_next_checkpoint_generation, + reorg_generation = p_reorg_generation, + changed_at = p_published_at + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + and checkpoint_generation = p_expected_checkpoint_generation + and reorg_generation = p_reorg_generation; + end if; + if not found then + raise exception using errcode = '40001', message = 'checkpoint CAS lost'; + end if; + insert into programmable_private.projection_publications ( + publication_id, run_id, epoch_id, pointer_generation, checkpoint_id, + terminal_outcome_id, target_block_number, target_block_hash, + published_at, audit_id + ) + values ( + p_publication_id, p_run_id, header.epoch_id, + header.captured_pointer_generation, p_checkpoint_id, p_outcome_id, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_published_at, audit_id + ); + foreach selected_route_key in array p_route_keys loop + route_history_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, status, route_mode, + checkpoint_id, reason_commitment, changed_by_run_id, changed_at, audit_id + ) + values ( + route_history_id, + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, + 'eligible', 'indexed', p_checkpoint_id, + p_result_commitment::programmable_private.bytes32_value, + p_run_id, p_published_at, audit_id + ); + insert into programmable_private.route_eligibility_current ( + route_key, chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, status, route_mode, checkpoint_id, history_id, + changed_at + ) + values ( + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, + 'eligible', 'indexed', p_checkpoint_id, route_history_id, p_published_at + ) + on conflict (route_key, chain_id, release_id, model_id, source_group) do update + set epoch_id = excluded.epoch_id, + pointer_generation = excluded.pointer_generation, + status = excluded.status, + route_mode = excluded.route_mode, + checkpoint_id = excluded.checkpoint_id, + history_id = excluded.history_id, + changed_at = excluded.changed_at + where programmable_private.route_eligibility_current.pointer_generation + <= excluded.pointer_generation; + if not found then + raise exception using errcode = '40001', message = 'stale route eligibility generation'; + end if; + end loop; + return p_publication_id; +end +$function$; + +create function programmable_private.rewind_projection_run( + p_checkpoint_id uuid, + p_outcome_id uuid, + p_run_id uuid, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_expected_checkpoint_generation bigint, + p_next_checkpoint_generation bigint, + p_next_reorg_generation bigint, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea, + p_cursor_block_global_log_index numeric, + p_cursor_candidate_id text, + p_result_commitment bytea, + p_rewound_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + current_pointer programmable_private.projector_checkpoint_current%rowtype; + previous_checkpoint programmable_private.projector_checkpoints%rowtype; + target_evidence programmable_private.dual_rpc_block_evidence%rowtype; + target_block bigint; + cursor_log_index bigint; + audit_id uuid; + status_id uuid; + route_record record; + occurrence_record record; + seed_record record; + route_history_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'rewind' + for update; + if not found then + raise exception using errcode = '23503', message = 'invalid rewind run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + select * into current_pointer + from programmable_private.projector_checkpoint_current + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + for update; + if not found + or current_pointer.checkpoint_generation <> p_expected_checkpoint_generation + or p_next_checkpoint_generation <> p_expected_checkpoint_generation + 1 + or p_next_reorg_generation <= current_pointer.reorg_generation + then + raise exception using errcode = '40001', message = 'rewind checkpoint generation lost'; + end if; + select * into previous_checkpoint + from programmable_private.projector_checkpoints + where checkpoint_id = current_pointer.checkpoint_id; + if header.captured_pointer_generation <= previous_checkpoint.pointer_generation + or p_lease_generation <= previous_checkpoint.lease_generation + or not exists ( + select 1 + from programmable_private.projector_lease_current as lease + where lease.chain_id = header.chain_id + and lease.release_id = header.release_id + and lease.model_id = header.model_id + and lease.source_group = header.source_group + and lease.projector_version = p_projector_version + and lease.epoch_id = header.epoch_id + and lease.pointer_generation = header.captured_pointer_generation + and lease.lease_generation = p_lease_generation + and lease.lease_token_hash = p_lease_token_hash + and lease.expires_at >= p_rewound_at + ) + then + raise exception using errcode = '40001', message = 'rewind requires higher pointer and lease generations'; + end if; + if p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number >= previous_checkpoint.block_number + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or p_cursor_block_global_log_index + <> pg_catalog.trunc(p_cursor_block_global_log_index) + or p_cursor_block_global_log_index < 0 + or p_cursor_block_global_log_index > 4294967295 + or p_cursor_candidate_id is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'rewind target must move backward'; + end if; + target_block := p_target_block_number::bigint; + cursor_log_index := p_cursor_block_global_log_index::bigint; + select * into target_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_target_block_evidence_id + and observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found + or target_evidence.block_number <> target_block + or target_evidence.agreed_block_hash <> p_target_block_hash + then + raise exception using errcode = '23514', message = 'rewind target lacks bound evidence'; + end if; + if not exists ( + select 1 + from programmable_private.projector_checkpoints as ancestor + where ancestor.chain_id = header.chain_id + and ancestor.release_id = header.release_id + and ancestor.model_id = header.model_id + and ancestor.source_group = header.source_group + and ancestor.projector_version = p_projector_version + and ancestor.block_number = target_block + and ancestor.block_hash = p_target_block_hash + and ancestor.cursor_block_global_log_index = cursor_log_index + and ancestor.cursor_candidate_id = p_cursor_candidate_id + and ancestor.checkpoint_generation < p_expected_checkpoint_generation + ) or not exists ( + select 1 from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = p_cursor_candidate_id + and candidate.chain_id = header.chain_id + and candidate.block_number = target_block + and candidate.block_hash = p_target_block_hash + and candidate.block_global_log_index = cursor_log_index + ) then + raise exception using + errcode = '23514', + message = 'rewind cursor is not a persisted checkpoint ancestor'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'projection.rewind', p_result_commitment, p_run_id, p_rewound_at + ); + for route_record in + select * + from programmable_private.route_eligibility_current + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + for update + loop + route_history_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, status, route_mode, + checkpoint_id, reason_commitment, changed_by_run_id, changed_at, audit_id + ) + values ( + route_history_id, route_record.route_key, header.chain_id, + header.release_id, header.model_id, header.source_group, header.epoch_id, + header.captured_pointer_generation, 'ineligible', 'rpc', + previous_checkpoint.checkpoint_id, + p_result_commitment::programmable_private.bytes32_value, + p_run_id, p_rewound_at, audit_id + ); + update programmable_private.route_eligibility_current + set epoch_id = header.epoch_id, + pointer_generation = header.captured_pointer_generation, + status = 'ineligible', + route_mode = 'rpc', + history_id = route_history_id, + changed_at = p_rewound_at + where route_key = route_record.route_key + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group; + end loop; + for occurrence_record in + select selected.*, occurrence.block_number, + scoped_materialization.block_evidence_id + from programmable_private.chain_event_current_canonical as selected + join programmable_private.chain_event_occurrences as occurrence + on occurrence.occurrence_id = selected.occurrence_id + join lateral ( + select materialization.block_evidence_id + from programmable_private.chain_event_occurrence_materializations + as materialization + where materialization.occurrence_id = occurrence.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + order by materialization.pointer_generation desc, + materialization.verified_at desc + limit 1 + ) as scoped_materialization on true + where occurrence.chain_id = header.chain_id + and occurrence.block_number > target_block + for update of selected + loop + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.chain_event_occurrence_status_history ( + status_history_id, occurrence_id, logical_event_id, block_hash, status, + safe_head_observation_id, block_evidence_id, decision_run_id, + decision_commitment, decided_at, audit_id + ) + values ( + status_id, occurrence_record.occurrence_id, + occurrence_record.logical_event_id, occurrence_record.block_hash, + 'orphaned', p_safe_head_observation_id, + occurrence_record.block_evidence_id, p_run_id, + p_result_commitment::programmable_private.bytes32_value, + p_rewound_at, audit_id + ); + delete from programmable_private.chain_event_current_canonical + where logical_event_id = occurrence_record.logical_event_id + and occurrence_id = occurrence_record.occurrence_id; + end loop; + for seed_record in + select current_seed.*, fact.verification_run_id, + evidence.allocation_evidence_id + from programmable_private.reward_allocation_current_verified as current_seed + join programmable_private.reward_allocation_facts as fact + on fact.allocation_fact_id = current_seed.allocation_fact_id + join programmable_private.reward_allocation_evidence as evidence + on evidence.allocation_evidence_id = current_seed.allocation_evidence_id + join programmable_private.run_headers as fact_run + on fact_run.run_id = fact.verification_run_id + where fact.chain_id = header.chain_id + and fact.release_id = header.release_id + and fact.model_id = header.model_id + and fact_run.source_group = header.source_group + and fact.creation_block_number > target_block + for update of current_seed + loop + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_allocation_status_history ( + seed_status_history_id, allocation_fact_id, allocation_evidence_id, + status, reason_commitment, decision_run_id, decided_at, audit_id + ) + values ( + status_id, seed_record.allocation_fact_id, + seed_record.allocation_evidence_id, 'orphaned', + p_result_commitment::programmable_private.bytes32_value, + p_run_id, p_rewound_at, audit_id + ); + delete from programmable_private.reward_allocation_current_verified + where allocation_fact_id = seed_record.allocation_fact_id; + end loop; + + delete from programmable_private.initial_buy_vesting_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.initial_buy_custody_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.account_reward_balances + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + and scoped_run.chain_id = header.chain_id + and scoped_run.release_id = header.release_id + and scoped_run.model_id = header.model_id + ) + and promoted_block_number > target_block; + delete from programmable_private.payout_change_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.claim_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.reward_allocation_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.reward_vault_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.pool_fee_totals + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.fee_accrual_facts + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.pool_fee_configurations + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.pool_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + delete from programmable_private.launch_projection_occurrence_roles as role + using programmable_private.launch_projections as launch + where role.launch_projection_id = launch.launch_projection_id + and launch.chain_id = header.chain_id + and launch.release_id = header.release_id + and launch.model_id = header.model_id + and launch.projection_run_id in ( + select scoped_run.run_id + from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and launch.promoted_block_number > target_block; + delete from programmable_private.launch_projection_conditions as condition + using programmable_private.launch_projections as launch + where condition.launch_projection_id = launch.launch_projection_id + and launch.chain_id = header.chain_id + and launch.release_id = header.release_id + and launch.model_id = header.model_id + and launch.projection_run_id in ( + select scoped_run.run_id + from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and launch.promoted_block_number > target_block; + delete from programmable_private.launch_projections + where chain_id = header.chain_id and release_id = header.release_id + and model_id = header.model_id + and projection_run_id in ( + select scoped_run.run_id from programmable_private.run_headers as scoped_run + where scoped_run.source_group = header.source_group + ) + and promoted_block_number > target_block; + + insert into programmable_private.run_lifecycle_outcomes ( + outcome_id, run_id, status, result_commitment, caller_role, + finished_at, audit_id + ) + values ( + p_outcome_id, p_run_id, 'succeeded', + p_result_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_rewound_at, audit_id + ); + insert into programmable_private.projector_checkpoints ( + checkpoint_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, lease_generation, + checkpoint_generation, reorg_generation, block_number, block_hash, + cursor_block_global_log_index, cursor_candidate_id, + safe_head_observation_id, target_block_evidence_id, run_id, + terminal_outcome_id, created_at + ) + values ( + p_checkpoint_id, header.chain_id, header.release_id, header.model_id, + header.source_group, + p_projector_version::programmable_private.projector_identifier, + header.epoch_id, header.captured_pointer_generation, p_lease_generation, + p_next_checkpoint_generation, p_next_reorg_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + p_safe_head_observation_id, p_target_block_evidence_id, + p_run_id, p_outcome_id, p_rewound_at + ); + update programmable_private.projector_checkpoint_current + set checkpoint_id = p_checkpoint_id, + checkpoint_generation = p_next_checkpoint_generation, + reorg_generation = p_next_reorg_generation, + changed_at = p_rewound_at + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + and checkpoint_generation = p_expected_checkpoint_generation; + if not found then + raise exception using errcode = '40001', message = 'rewind checkpoint CAS lost'; + end if; + return p_checkpoint_id; +end +$function$; + +do $lockdown$ +declare + table_record record; +begin + for table_record in + select c.relname + from pg_catalog.pg_class as c + join pg_catalog.pg_namespace as n on n.oid = c.relnamespace + where n.nspname = 'programmable_private' + and c.relkind in ('r', 'p') + and not c.relrowsecurity + loop + execute pg_catalog.format( + 'alter table programmable_private.%I enable row level security', + table_record.relname + ); + execute pg_catalog.format( + 'alter table programmable_private.%I force row level security', + table_record.relname + ); + execute pg_catalog.format( + 'create policy migrator_owner_all on programmable_private.%I ' || + 'for all to programmable_migrator using (true) with check (true)', + table_record.relname + ); + end loop; +end +$lockdown$; + +revoke all on all tables in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all sequences in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all functions in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +grant execute on function programmable_private.stage_launch_projection( + uuid, uuid, bytea, bytea, bytea, bytea, bytea, bytea, text, text, + numeric, uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_pool_projection( + uuid, uuid, uuid, bytea, bytea, numeric, integer, bytea, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_account_reward_balance( + uuid, uuid, bytea, bytea, numeric, numeric, uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_pool_fee_configuration( + uuid, uuid, uuid, numeric, numeric, numeric, numeric, numeric, numeric, + uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_fee_accrual_fact( + uuid, uuid, bytea, bytea, numeric, numeric, numeric, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_pool_fee_total( + uuid, uuid, bytea, bytea, numeric, numeric, numeric, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_reward_vault_projection( + uuid, uuid, uuid, bytea, bytea, bytea, bytea, uuid, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_reward_allocation_projection( + uuid, uuid, uuid, uuid, bigint, integer, bytea, bytea, numeric, + numeric, numeric, uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_claim_projection( + uuid, uuid, bytea, text, bytea, bytea, numeric, numeric, numeric, + uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_payout_change_projection( + uuid, uuid, bytea, bytea, bytea, bytea, bigint, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_initial_buy_custody_projection( + uuid, uuid, uuid, bytea, smallint, integer, integer, bytea, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_initial_buy_vesting_projection( + uuid, uuid, uuid, bytea, bytea, numeric, timestamptz, timestamptz, + uuid, numeric, bytea, timestamptz +) to programmable_projector; + +grant execute on function programmable_private.promote_projection_run( + uuid, uuid, uuid, uuid, text, bigint, bytea, bigint, bigint, bigint, + uuid, uuid, numeric, bytea, numeric, text, uuid[], uuid[], uuid[], uuid[], + text[], bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.rewind_projection_run( + uuid, uuid, uuid, text, bigint, bytea, bigint, bigint, bigint, + uuid, uuid, numeric, bytea, numeric, text, bytea, timestamptz +) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260731000500_profiles_market_parity.sql b/supabase/migrations/20260731000500_profiles_market_parity.sql new file mode 100644 index 00000000..128c36af --- /dev/null +++ b/supabase/migrations/20260731000500_profiles_market_parity.sql @@ -0,0 +1,2455 @@ +-- Stable profile identities, append-only alias/binding history, reconciliation +-- evidence, bounded market analytics and explicit retention entry points. + +set role programmable_migrator; + +create table programmable_private.profile_hash_version_definitions ( + hash_version smallint primary key check (hash_version > 0), + algorithm programmable_private.source_identifier not null, + definition_commitment programmable_private.bytes32_value not null unique, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict +); + +create table programmable_private.profile_hash_version_status_history ( + status_history_id uuid primary key, + hash_version smallint not null + references programmable_private.profile_hash_version_definitions(hash_version) + on delete restrict, + state programmable_private.profile_hash_version_state not null, + reason_commitment programmable_private.bytes32_value not null, + changed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (hash_version, changed_at) +); + +create table programmable_private.profile_hash_version_status_current ( + hash_version smallint primary key + references programmable_private.profile_hash_version_definitions(hash_version) + on delete restrict, + state programmable_private.profile_hash_version_state not null, + status_history_id uuid not null unique + references programmable_private.profile_hash_version_status_history(status_history_id) + on delete restrict, + changed_at timestamptz not null +); + +create unique index profile_one_current_hash_version_idx + on programmable_private.profile_hash_version_status_current ((state)) + where state = 'current'; + +create table programmable_private.profile_subjects ( + subject_id uuid primary key, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict +); + +create table programmable_private.profile_subject_aliases ( + alias_id uuid primary key, + subject_id uuid not null + references programmable_private.profile_subjects(subject_id) + on delete restrict, + hash_version smallint not null + references programmable_private.profile_hash_version_definitions(hash_version) + on delete restrict, + keyed_subject_hash programmable_private.bytes32_value not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (hash_version, keyed_subject_hash), + unique (alias_id, subject_id) +); + +create table programmable_private.profile_subject_alias_status_history ( + alias_status_history_id uuid primary key, + alias_id uuid not null + references programmable_private.profile_subject_aliases(alias_id) + on delete restrict, + state programmable_private.profile_alias_state not null, + reason_commitment programmable_private.bytes32_value not null, + changed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (alias_id, changed_at) +); + +create table programmable_private.profile_subject_alias_status_current ( + alias_id uuid primary key + references programmable_private.profile_subject_aliases(alias_id) + on delete restrict, + state programmable_private.profile_alias_state not null, + alias_status_history_id uuid not null unique + references programmable_private.profile_subject_alias_status_history(alias_status_history_id) + on delete restrict, + changed_at timestamptz not null +); + +create table programmable_private.profile_subject_current_alias ( + subject_id uuid primary key + references programmable_private.profile_subjects(subject_id) + on delete restrict, + alias_id uuid not null unique, + generation bigint not null check (generation > 0), + changed_at timestamptz not null, + foreign key (alias_id, subject_id) + references programmable_private.profile_subject_aliases(alias_id, subject_id) + on delete restrict +); + +create table programmable_private.profile_owner_binding_history ( + binding_id uuid primary key, + subject_id uuid not null + references programmable_private.profile_subjects(subject_id) + on delete restrict, + wallet programmable_private.eth_address not null, + alias_id uuid not null, + generation bigint not null check (generation > 0), + state programmable_private.profile_binding_state not null, + recovery_method programmable_private.profile_recovery_method not null, + proof_commitment programmable_private.bytes32_value not null, + previous_binding_id uuid + references programmable_private.profile_owner_binding_history(binding_id) + on delete restrict, + created_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (alias_id, subject_id) + references programmable_private.profile_subject_aliases(alias_id, subject_id) + on delete restrict, + unique (wallet, generation), + unique (subject_id, generation), + unique (binding_id, subject_id, wallet, generation) +); + +create table programmable_private.profile_owner_binding_current ( + wallet programmable_private.eth_address primary key, + subject_id uuid not null unique + references programmable_private.profile_subjects(subject_id) + on delete restrict, + binding_id uuid not null unique, + generation bigint not null check (generation > 0), + state programmable_private.profile_binding_state not null, + changed_at timestamptz not null, + foreign key (binding_id, subject_id, wallet, generation) + references programmable_private.profile_owner_binding_history( + binding_id, subject_id, wallet, generation + ) + on delete restrict +); + +create table programmable_private.profiles ( + subject_id uuid primary key + references programmable_private.profile_subjects(subject_id) + on delete restrict, + username text, + username_key text, + avatar_reference text, + display_name text, + bio text, + revision bigint not null check (revision >= 0), + deleted_at timestamptz, + created_at timestamptz not null, + updated_at timestamptz not null, + last_mutation_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + (username is null and username_key is null) + or ( + username is not null + and username_key = pg_catalog.lower(username) + and programmable_private.valid_profile_username(username) + ) + ), + check (programmable_private.valid_avatar_reference(avatar_reference)), + check (display_name is null or pg_catalog.octet_length(display_name) between 1 and 64), + check (bio is null or pg_catalog.octet_length(bio) <= 280), + check (updated_at >= created_at) +); + +create unique index profiles_username_key_idx + on programmable_private.profiles (username_key) + where username_key is not null and deleted_at is null; + +create table programmable_private.profile_audit_records ( + profile_audit_id uuid primary key, + subject_id uuid not null + references programmable_private.profile_subjects(subject_id) + on delete restrict, + wallet programmable_private.eth_address not null, + action programmable_private.source_identifier not null, + expected_binding_generation bigint not null check (expected_binding_generation >= 0), + resulting_binding_generation bigint not null check (resulting_binding_generation >= 0), + expected_revision bigint, + resulting_revision bigint, + proof_commitment programmable_private.bytes32_value not null, + caller_role name not null, + occurred_at timestamptz not null, + mutation_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + (expected_revision is null and resulting_revision is null) + or ( + expected_revision is not null + and resulting_revision is not null + and resulting_revision >= expected_revision + ) + ) +); + +create table programmable_private.token_project_metadata ( + metadata_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + token programmable_private.eth_address not null, + project_name text check (project_name is null or pg_catalog.octet_length(project_name) <= 128), + description text check (description is null or pg_catalog.octet_length(description) <= 2000), + logo_reference text check (programmable_private.valid_avatar_reference(logo_reference)), + metadata_revision bigint not null check (metadata_revision > 0), + subject_id uuid not null + references programmable_private.profile_subjects(subject_id) + on delete restrict, + created_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (chain_id, token, metadata_revision) +); + +create table programmable_private.project_links ( + project_link_id uuid primary key, + metadata_id uuid not null + references programmable_private.token_project_metadata(metadata_id) + on delete restrict, + link_kind programmable_private.source_identifier not null, + https_url text not null, + display_order integer not null check (display_order between 0 and 15), + check ( + pg_catalog.octet_length(https_url) between 9 and 512 + and https_url ~ '^https://[A-Za-z0-9.-]+(?::[0-9]+)?/' + ), + created_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (metadata_id, link_kind), + unique (metadata_id, display_order) +); + +create table programmable_private.reconciliation_records ( + reconciliation_id uuid primary key, + run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + comparison_kind programmable_private.source_identifier not null, + severity programmable_private.reconciliation_severity not null, + source_from_block programmable_private.block_number_value not null, + source_to_block programmable_private.block_number_value not null, + compared_count bigint not null check (compared_count >= 0), + mismatch_count bigint not null check (mismatch_count >= 0 and mismatch_count <= compared_count), + evidence_commitment programmable_private.bytes32_value not null, + mismatch_identity_commitments bytea[] not null, + resolved_at timestamptz, + recorded_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check (source_to_block >= source_from_block), + check (resolved_at is null or resolved_at >= recorded_at), + check (programmable_private.valid_topics(mismatch_identity_commitments)), + unique (run_id, comparison_kind, evidence_commitment) +); + +create index reconciliation_unresolved_idx + on programmable_private.reconciliation_records ( + chain_id, release_id, model_id, severity, recorded_at + ) + where mismatch_count > 0 and resolved_at is null; + +create table programmable_private.parity_records ( + parity_record_id uuid primary key, + reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + route_key programmable_private.source_identifier not null, + legacy_dto_hash programmable_private.bytes32_value not null, + indexed_dto_hash programmable_private.bytes32_value not null, + is_match boolean not null, + compared_at timestamptz not null, + resolved_at timestamptz, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + is_match = (legacy_dto_hash = indexed_dto_hash) + and (is_match or resolved_at is null or resolved_at >= compared_at) + ), + unique (reconciliation_id, route_key) +); + +create index parity_retention_idx + on programmable_private.parity_records (is_match, compared_at, resolved_at); + +create table programmable_private.market_snapshots ( + market_snapshot_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + pool_id programmable_private.bytes32_value not null, + source_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + block_evidence_id uuid not null, + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + sqrt_price_x96 programmable_private.uint256_value not null, + liquidity programmable_private.uint256_value not null, + market_volume_token0 numeric not null check (market_volume_token0 >= 0), + market_volume_token1 numeric not null check (market_volume_token1 >= 0), + market_volume_usd numeric check (market_volume_usd is null or market_volume_usd >= 0), + hook_gross_volume programmable_private.uint256_value, + observed_at timestamptz not null, + reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (block_evidence_id, block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) + on delete restrict, + unique (chain_id, pool_id, source_deployment_id, block_hash) +); + +create index market_snapshot_retention_idx + on programmable_private.market_snapshots (observed_at, market_snapshot_id); + +create table programmable_private.market_candles ( + market_candle_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + pool_id programmable_private.bytes32_value not null, + source_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + source_block_evidence_id uuid not null, + source_block_number programmable_private.block_number_value not null, + interval programmable_private.market_interval not null, + period_start timestamptz not null, + period_end timestamptz not null, + open numeric not null check (open >= 0), + high numeric not null check (high >= 0), + low numeric not null check (low >= 0), + close numeric not null check (close >= 0), + volume_token0 numeric not null check (volume_token0 >= 0), + volume_token1 numeric not null check (volume_token1 >= 0), + volume_usd numeric check (volume_usd is null or volume_usd >= 0), + source_block_hash programmable_private.bytes32_value not null, + reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (source_block_evidence_id, source_block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) + on delete restrict, + check (interval in ('hour', 'day')), + check (period_end > period_start and high >= greatest(open, close, low)), + unique (chain_id, pool_id, interval, period_start, source_block_hash) +); + +create index market_candle_retention_idx + on programmable_private.market_candles (interval, period_start, market_candle_id); + +create table programmable_private.portfolio_points ( + portfolio_point_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + account programmable_private.eth_address not null, + interval_minutes integer not null check (interval_minutes in (5, 1440)), + point_time timestamptz not null, + exact_reward_total programmable_private.uint256_value not null, + source_checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (chain_id, account, interval_minutes, point_time) +); + +create index portfolio_point_retention_idx + on programmable_private.portfolio_points ( + interval_minutes, point_time, portfolio_point_id + ); + +create function programmable_private.profile_lock_key(p_value bytea, p_salt bigint) +returns bigint +language sql +immutable +strict +security invoker +set search_path = '' +as $function$ + select pg_catalog.hashtextextended(pg_catalog.encode(p_value, 'hex'), p_salt) +$function$; + +create function programmable_private.define_profile_hash_version( + p_hash_version smallint, + p_algorithm text, + p_definition_commitment bytea, + p_input_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns smallint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_profile_recovery'); + if p_hash_version <= 0 or pg_catalog.octet_length(p_definition_commitment) <> 32 then + raise exception using errcode = '22023', message = 'invalid hash-version definition'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'profile_hash_version.define', p_input_commitment, null, p_created_at + ); + insert into programmable_private.profile_hash_version_definitions ( + hash_version, algorithm, definition_commitment, created_at, + created_by_audit_id + ) + values ( + p_hash_version, + p_algorithm::programmable_private.source_identifier, + p_definition_commitment::programmable_private.bytes32_value, + p_created_at, audit_id + ); + return p_hash_version; +end +$function$; + +create function programmable_private.set_profile_hash_version_state( + p_status_history_id uuid, + p_hash_version smallint, + p_state text, + p_reason_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + requested_state programmable_private.profile_hash_version_state; + audit_id uuid; + previous_current smallint; + previous_history_id uuid; +begin + perform programmable_private.assert_caller('programmable_profile_recovery'); + requested_state := p_state::programmable_private.profile_hash_version_state; + if pg_catalog.octet_length(p_reason_commitment) <> 32 then + raise exception using errcode = '22023', message = 'invalid hash-version state'; + end if; + perform pg_catalog.pg_advisory_xact_lock(172920260731); + if requested_state = 'current' then + select hash_version into previous_current + from programmable_private.profile_hash_version_status_current + where state = 'current' + for update; + if found and previous_current <> p_hash_version then + previous_history_id := pg_catalog.gen_random_uuid(); + audit_id := programmable_private.append_mutation_audit( + 'profile_hash_version.rotate', p_reason_commitment, null, p_changed_at + ); + insert into programmable_private.profile_hash_version_status_history ( + status_history_id, hash_version, state, reason_commitment, + changed_at, audit_id + ) + values ( + previous_history_id, previous_current, 'verify_only', + p_reason_commitment::programmable_private.bytes32_value, + p_changed_at, audit_id + ); + update programmable_private.profile_hash_version_status_current + set state = 'verify_only', + status_history_id = previous_history_id, + changed_at = p_changed_at + where hash_version = previous_current; + end if; + end if; + if audit_id is null then + audit_id := programmable_private.append_mutation_audit( + 'profile_hash_version.state', p_reason_commitment, null, p_changed_at + ); + end if; + insert into programmable_private.profile_hash_version_status_history ( + status_history_id, hash_version, state, reason_commitment, + changed_at, audit_id + ) + values ( + p_status_history_id, p_hash_version, requested_state, + p_reason_commitment::programmable_private.bytes32_value, + p_changed_at, audit_id + ); + insert into programmable_private.profile_hash_version_status_current ( + hash_version, state, status_history_id, changed_at + ) + values ( + p_hash_version, requested_state, p_status_history_id, p_changed_at + ) + on conflict (hash_version) do update + set state = excluded.state, + status_history_id = excluded.status_history_id, + changed_at = excluded.changed_at; + return p_status_history_id; +end +$function$; + +create function programmable_private.bind_profile_subject( + p_wallet bytea, + p_hash_version smallint, + p_keyed_subject_hash bytea, + p_recovery_method text, + p_proof_commitment bytea, + p_bound_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + wallet_key bigint; + alias_key bigint; + new_subject_id uuid; + new_alias_id uuid; + new_binding_id uuid; + audit_id uuid; + alias_status_id uuid; + existing_alias programmable_private.profile_subject_aliases%rowtype; + existing_binding programmable_private.profile_owner_binding_current%rowtype; + method programmable_private.profile_recovery_method; +begin + perform programmable_private.assert_caller('programmable_profile_binder'); + method := p_recovery_method::programmable_private.profile_recovery_method; + if method not in ('linked_wallet', 'wallet_signature') + or pg_catalog.octet_length(p_wallet) <> 20 + or p_wallet = pg_catalog.decode('0000000000000000000000000000000000000000', 'hex') + or pg_catalog.octet_length(p_keyed_subject_hash) <> 32 + or pg_catalog.octet_length(p_proof_commitment) <> 32 + or not exists ( + select 1 + from programmable_private.profile_hash_version_status_current + where hash_version = p_hash_version and state = 'current' + ) + then + raise exception using errcode = '22023', message = 'invalid first-binding proof'; + end if; + wallet_key := programmable_private.profile_lock_key(p_wallet, 1); + alias_key := programmable_private.profile_lock_key( + pg_catalog.int2send(p_hash_version) || p_keyed_subject_hash, 2 + ); + perform pg_catalog.pg_advisory_xact_lock(least(wallet_key, alias_key)); + if wallet_key <> alias_key then + perform pg_catalog.pg_advisory_xact_lock(greatest(wallet_key, alias_key)); + end if; + select * into existing_binding + from programmable_private.profile_owner_binding_current + where wallet = p_wallet + for update; + select * into existing_alias + from programmable_private.profile_subject_aliases + where hash_version = p_hash_version + and keyed_subject_hash = p_keyed_subject_hash; + if existing_binding.wallet is not null or existing_alias.alias_id is not null then + if existing_binding.wallet is not null + and existing_alias.alias_id is not null + and existing_binding.subject_id = existing_alias.subject_id + and existing_binding.state in ('active', 'recovered') + and exists ( + select 1 + from programmable_private.profile_subject_alias_status_current + where alias_id = existing_alias.alias_id and state = 'current' + ) + then + return existing_binding.subject_id; + end if; + raise exception using errcode = '23505', message = 'wallet or alias is already bound or tombstoned'; + end if; + new_subject_id := pg_catalog.gen_random_uuid(); + new_alias_id := pg_catalog.gen_random_uuid(); + new_binding_id := pg_catalog.gen_random_uuid(); + alias_status_id := pg_catalog.gen_random_uuid(); + audit_id := programmable_private.append_mutation_audit( + 'profile.bind_first', p_proof_commitment, null, p_bound_at + ); + insert into programmable_private.profile_subjects ( + subject_id, created_at, created_by_audit_id + ) values (new_subject_id, p_bound_at, audit_id); + insert into programmable_private.profile_subject_aliases ( + alias_id, subject_id, hash_version, keyed_subject_hash, + created_at, created_by_audit_id + ) + values ( + new_alias_id, new_subject_id, p_hash_version, + p_keyed_subject_hash::programmable_private.bytes32_value, + p_bound_at, audit_id + ); + insert into programmable_private.profile_subject_alias_status_history ( + alias_status_history_id, alias_id, state, reason_commitment, + changed_at, audit_id + ) + values ( + alias_status_id, new_alias_id, 'current', + p_proof_commitment::programmable_private.bytes32_value, + p_bound_at, audit_id + ); + insert into programmable_private.profile_subject_alias_status_current ( + alias_id, state, alias_status_history_id, changed_at + ) values (new_alias_id, 'current', alias_status_id, p_bound_at); + insert into programmable_private.profile_subject_current_alias ( + subject_id, alias_id, generation, changed_at + ) values (new_subject_id, new_alias_id, 1, p_bound_at); + insert into programmable_private.profile_owner_binding_history ( + binding_id, subject_id, wallet, alias_id, generation, state, + recovery_method, proof_commitment, previous_binding_id, created_at, audit_id + ) + values ( + new_binding_id, new_subject_id, p_wallet::programmable_private.eth_address, + new_alias_id, 1, 'active', method, + p_proof_commitment::programmable_private.bytes32_value, + null, p_bound_at, audit_id + ); + insert into programmable_private.profile_owner_binding_current ( + wallet, subject_id, binding_id, generation, state, changed_at + ) + values ( + p_wallet::programmable_private.eth_address, + new_subject_id, new_binding_id, 1, 'active', p_bound_at + ); + insert into programmable_private.profiles ( + subject_id, username, username_key, avatar_reference, display_name, bio, + revision, deleted_at, created_at, updated_at, last_mutation_audit_id + ) + values ( + new_subject_id, null, null, null, null, null, 0, null, + p_bound_at, p_bound_at, audit_id + ); + insert into programmable_private.profile_audit_records ( + profile_audit_id, subject_id, wallet, action, + expected_binding_generation, resulting_binding_generation, + expected_revision, resulting_revision, proof_commitment, caller_role, + occurred_at, mutation_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), new_subject_id, + p_wallet::programmable_private.eth_address, 'profile.bind_first', + 0, 1, null, null, + p_proof_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_bound_at, audit_id + ); + return new_subject_id; +end +$function$; + +create function programmable_private.rekey_profile_subject( + p_wallet bytea, + p_old_hash_version smallint, + p_old_keyed_subject_hash bytea, + p_new_hash_version smallint, + p_new_keyed_subject_hash bytea, + p_expected_binding_generation bigint, + p_proof_commitment bytea, + p_rekeyed_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + keys bigint[]; + lock_key bigint; + binding programmable_private.profile_owner_binding_current%rowtype; + old_alias programmable_private.profile_subject_aliases%rowtype; + new_alias programmable_private.profile_subject_aliases%rowtype; + new_alias_id uuid; + new_binding_id uuid; + audit_id uuid; + status_id uuid; + next_generation bigint; +begin + perform programmable_private.assert_caller('programmable_profile_recovery'); + if pg_catalog.octet_length(p_wallet) <> 20 + or pg_catalog.octet_length(p_old_keyed_subject_hash) <> 32 + or pg_catalog.octet_length(p_new_keyed_subject_hash) <> 32 + or pg_catalog.octet_length(p_proof_commitment) <> 32 + or p_expected_binding_generation <= 0 + or not exists ( + select 1 + from programmable_private.profile_hash_version_status_current + where hash_version = p_new_hash_version and state = 'current' + ) + then + raise exception using errcode = '22023', message = 'invalid rekey proof'; + end if; + keys := array[ + programmable_private.profile_lock_key(p_wallet, 1), + programmable_private.profile_lock_key( + pg_catalog.int2send(p_old_hash_version) || p_old_keyed_subject_hash, 2 + ), + programmable_private.profile_lock_key( + pg_catalog.int2send(p_new_hash_version) || p_new_keyed_subject_hash, 2 + ) + ]; + for lock_key in + select distinct locked_key.key_value + from pg_catalog.unnest(keys) as locked_key(key_value) + order by locked_key.key_value + loop + perform pg_catalog.pg_advisory_xact_lock(lock_key); + end loop; + select * into binding + from programmable_private.profile_owner_binding_current + where wallet = p_wallet + for update; + select * into old_alias + from programmable_private.profile_subject_aliases + where hash_version = p_old_hash_version + and keyed_subject_hash = p_old_keyed_subject_hash; + select * into new_alias + from programmable_private.profile_subject_aliases + where hash_version = p_new_hash_version + and keyed_subject_hash = p_new_keyed_subject_hash; + if binding.wallet is null + or binding.state not in ('active', 'recovered') + or binding.generation <> p_expected_binding_generation + or old_alias.alias_id is null + or old_alias.subject_id <> binding.subject_id + or (new_alias.alias_id is not null and new_alias.subject_id <> binding.subject_id) + then + raise exception using errcode = '40001', message = 'rekey generation or subject proof failed'; + end if; + next_generation := p_expected_binding_generation + 1; + audit_id := programmable_private.append_mutation_audit( + 'profile.rekey', p_proof_commitment, null, p_rekeyed_at + ); + if new_alias.alias_id is null then + new_alias_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.profile_subject_aliases ( + alias_id, subject_id, hash_version, keyed_subject_hash, + created_at, created_by_audit_id + ) + values ( + new_alias_id, binding.subject_id, p_new_hash_version, + p_new_keyed_subject_hash::programmable_private.bytes32_value, + p_rekeyed_at, audit_id + ); + else + new_alias_id := new_alias.alias_id; + end if; + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.profile_subject_alias_status_history ( + alias_status_history_id, alias_id, state, reason_commitment, + changed_at, audit_id + ) + values ( + status_id, new_alias_id, 'current', + p_proof_commitment::programmable_private.bytes32_value, + p_rekeyed_at, audit_id + ); + insert into programmable_private.profile_subject_alias_status_current ( + alias_id, state, alias_status_history_id, changed_at + ) + values (new_alias_id, 'current', status_id, p_rekeyed_at) + on conflict (alias_id) do update + set state = excluded.state, + alias_status_history_id = excluded.alias_status_history_id, + changed_at = excluded.changed_at; + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.profile_subject_alias_status_history ( + alias_status_history_id, alias_id, state, reason_commitment, + changed_at, audit_id + ) + values ( + status_id, old_alias.alias_id, 'verify_only', + p_proof_commitment::programmable_private.bytes32_value, + p_rekeyed_at, audit_id + ); + update programmable_private.profile_subject_alias_status_current + set state = 'verify_only', + alias_status_history_id = status_id, + changed_at = p_rekeyed_at + where alias_id = old_alias.alias_id; + update programmable_private.profile_subject_current_alias + set alias_id = new_alias_id, + generation = next_generation, + changed_at = p_rekeyed_at + where subject_id = binding.subject_id + and generation = p_expected_binding_generation; + if not found then + raise exception using errcode = '40001', message = 'current alias generation lost'; + end if; + new_binding_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.profile_owner_binding_history ( + binding_id, subject_id, wallet, alias_id, generation, state, + recovery_method, proof_commitment, previous_binding_id, created_at, audit_id + ) + values ( + new_binding_id, binding.subject_id, + p_wallet::programmable_private.eth_address, new_alias_id, + next_generation, 'recovered', 'verified_subject_recovery', + p_proof_commitment::programmable_private.bytes32_value, + binding.binding_id, p_rekeyed_at, audit_id + ); + update programmable_private.profile_owner_binding_current + set binding_id = new_binding_id, + generation = next_generation, + state = 'recovered', + changed_at = p_rekeyed_at + where wallet = p_wallet + and generation = p_expected_binding_generation; + if not found then + raise exception using errcode = '40001', message = 'binding generation lost'; + end if; + insert into programmable_private.profile_audit_records ( + profile_audit_id, subject_id, wallet, action, + expected_binding_generation, resulting_binding_generation, + expected_revision, resulting_revision, proof_commitment, caller_role, + occurred_at, mutation_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), binding.subject_id, + p_wallet::programmable_private.eth_address, 'profile.rekey', + p_expected_binding_generation, next_generation, null, null, + p_proof_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_rekeyed_at, audit_id + ); + return next_generation; +end +$function$; + +create function programmable_private.tombstone_profile_binding( + p_wallet bytea, + p_hash_version smallint, + p_keyed_subject_hash bytea, + p_expected_binding_generation bigint, + p_proof_commitment bytea, + p_tombstoned_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + binding programmable_private.profile_owner_binding_current%rowtype; + supplied_alias programmable_private.profile_subject_aliases%rowtype; + alias_record record; + status_id uuid; + binding_id_next uuid := pg_catalog.gen_random_uuid(); + audit_id uuid; + next_generation bigint := p_expected_binding_generation + 1; + current_revision bigint; +begin + perform programmable_private.assert_caller('programmable_profile_recovery'); + if pg_catalog.octet_length(p_wallet) <> 20 + or pg_catalog.octet_length(p_keyed_subject_hash) <> 32 + or pg_catalog.octet_length(p_proof_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid tombstone proof'; + end if; + perform pg_catalog.pg_advisory_xact_lock( + programmable_private.profile_lock_key(p_wallet, 1) + ); + select * into binding + from programmable_private.profile_owner_binding_current + where wallet = p_wallet + for update; + select * into supplied_alias + from programmable_private.profile_subject_aliases + where hash_version = p_hash_version + and keyed_subject_hash = p_keyed_subject_hash; + if binding.wallet is null + or binding.state = 'tombstoned' + or binding.generation <> p_expected_binding_generation + or supplied_alias.alias_id is null + or supplied_alias.subject_id <> binding.subject_id + then + raise exception using errcode = '40001', message = 'tombstone generation or subject proof failed'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'profile.tombstone', p_proof_commitment, null, p_tombstoned_at + ); + for alias_record in + select alias_id + from programmable_private.profile_subject_aliases + where subject_id = binding.subject_id + order by alias_id + for share + loop + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.profile_subject_alias_status_history ( + alias_status_history_id, alias_id, state, reason_commitment, + changed_at, audit_id + ) + values ( + status_id, alias_record.alias_id, 'tombstoned', + p_proof_commitment::programmable_private.bytes32_value, + p_tombstoned_at, audit_id + ); + update programmable_private.profile_subject_alias_status_current + set state = 'tombstoned', + alias_status_history_id = status_id, + changed_at = p_tombstoned_at + where alias_id = alias_record.alias_id; + end loop; + insert into programmable_private.profile_owner_binding_history ( + binding_id, subject_id, wallet, alias_id, generation, state, + recovery_method, proof_commitment, previous_binding_id, created_at, audit_id + ) + values ( + binding_id_next, binding.subject_id, + p_wallet::programmable_private.eth_address, supplied_alias.alias_id, + next_generation, 'tombstoned', 'verified_subject_recovery', + p_proof_commitment::programmable_private.bytes32_value, + binding.binding_id, p_tombstoned_at, audit_id + ); + update programmable_private.profile_owner_binding_current + set binding_id = binding_id_next, + generation = next_generation, + state = 'tombstoned', + changed_at = p_tombstoned_at + where wallet = p_wallet and generation = p_expected_binding_generation; + if not found then + raise exception using errcode = '40001', message = 'tombstone binding generation lost'; + end if; + update programmable_private.profile_subject_current_alias + set generation = next_generation, + changed_at = p_tombstoned_at + where subject_id = binding.subject_id + and generation = p_expected_binding_generation; + if not found then + raise exception using errcode = '40001', message = 'tombstone alias generation lost'; + end if; + select revision into current_revision + from programmable_private.profiles + where subject_id = binding.subject_id + for update; + update programmable_private.profiles + set revision = current_revision + 1, + deleted_at = p_tombstoned_at, + updated_at = p_tombstoned_at, + last_mutation_audit_id = audit_id + where subject_id = binding.subject_id and revision = current_revision; + insert into programmable_private.profile_audit_records ( + profile_audit_id, subject_id, wallet, action, + expected_binding_generation, resulting_binding_generation, + expected_revision, resulting_revision, proof_commitment, caller_role, + occurred_at, mutation_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), binding.subject_id, + p_wallet::programmable_private.eth_address, 'profile.tombstone', + p_expected_binding_generation, next_generation, + current_revision, current_revision + 1, + p_proof_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_tombstoned_at, audit_id + ); + return next_generation; +end +$function$; + +create function programmable_private.recover_profile_binding( + p_wallet bytea, + p_hash_version smallint, + p_keyed_subject_hash bytea, + p_expected_binding_generation bigint, + p_proof_commitment bytea, + p_recovered_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + binding programmable_private.profile_owner_binding_current%rowtype; + alias programmable_private.profile_subject_aliases%rowtype; + status_id uuid := pg_catalog.gen_random_uuid(); + binding_id_next uuid := pg_catalog.gen_random_uuid(); + audit_id uuid; + next_generation bigint := p_expected_binding_generation + 1; + current_revision bigint; +begin + perform programmable_private.assert_caller('programmable_profile_recovery'); + perform pg_catalog.pg_advisory_xact_lock( + programmable_private.profile_lock_key(p_wallet, 1) + ); + select * into binding + from programmable_private.profile_owner_binding_current + where wallet = p_wallet + for update; + select * into alias + from programmable_private.profile_subject_aliases + where hash_version = p_hash_version + and keyed_subject_hash = p_keyed_subject_hash; + if binding.wallet is null + or binding.state <> 'tombstoned' + or binding.generation <> p_expected_binding_generation + or alias.alias_id is null + or alias.subject_id <> binding.subject_id + or not exists ( + select 1 + from programmable_private.profile_hash_version_status_current + where hash_version = p_hash_version and state in ('current', 'verify_only') + ) + or pg_catalog.octet_length(p_proof_commitment) <> 32 + then + raise exception using errcode = '40001', message = 'recovery generation or subject proof failed'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'profile.recover', p_proof_commitment, null, p_recovered_at + ); + insert into programmable_private.profile_subject_alias_status_history ( + alias_status_history_id, alias_id, state, reason_commitment, + changed_at, audit_id + ) + values ( + status_id, alias.alias_id, 'current', + p_proof_commitment::programmable_private.bytes32_value, + p_recovered_at, audit_id + ); + update programmable_private.profile_subject_alias_status_current + set state = 'current', + alias_status_history_id = status_id, + changed_at = p_recovered_at + where alias_id = alias.alias_id; + update programmable_private.profile_subject_current_alias + set alias_id = alias.alias_id, + generation = next_generation, + changed_at = p_recovered_at + where subject_id = binding.subject_id + and generation = p_expected_binding_generation; + if not found then + raise exception using errcode = '40001', message = 'recovery alias generation lost'; + end if; + insert into programmable_private.profile_owner_binding_history ( + binding_id, subject_id, wallet, alias_id, generation, state, + recovery_method, proof_commitment, previous_binding_id, created_at, audit_id + ) + values ( + binding_id_next, binding.subject_id, + p_wallet::programmable_private.eth_address, alias.alias_id, + next_generation, 'recovered', 'verified_subject_recovery', + p_proof_commitment::programmable_private.bytes32_value, + binding.binding_id, p_recovered_at, audit_id + ); + update programmable_private.profile_owner_binding_current + set binding_id = binding_id_next, + generation = next_generation, + state = 'recovered', + changed_at = p_recovered_at + where wallet = p_wallet and generation = p_expected_binding_generation; + select revision into current_revision + from programmable_private.profiles + where subject_id = binding.subject_id + for update; + update programmable_private.profiles + set revision = current_revision + 1, + deleted_at = null, + updated_at = p_recovered_at, + last_mutation_audit_id = audit_id + where subject_id = binding.subject_id and revision = current_revision; + insert into programmable_private.profile_audit_records ( + profile_audit_id, subject_id, wallet, action, + expected_binding_generation, resulting_binding_generation, + expected_revision, resulting_revision, proof_commitment, caller_role, + occurred_at, mutation_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), binding.subject_id, + p_wallet::programmable_private.eth_address, 'profile.recover', + p_expected_binding_generation, next_generation, + current_revision, current_revision + 1, + p_proof_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_recovered_at, audit_id + ); + return next_generation; +end +$function$; + +create function programmable_private.mutate_profile( + p_wallet bytea, + p_hash_version smallint, + p_keyed_subject_hash bytea, + p_expected_binding_generation bigint, + p_expected_revision bigint, + p_username text, + p_avatar_reference text, + p_display_name text, + p_bio text, + p_proof_commitment bytea, + p_mutated_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + binding programmable_private.profile_owner_binding_current%rowtype; + alias programmable_private.profile_subject_aliases%rowtype; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_profile_writer'); + if pg_catalog.octet_length(p_wallet) <> 20 + or pg_catalog.octet_length(p_keyed_subject_hash) <> 32 + or pg_catalog.octet_length(p_proof_commitment) <> 32 + or p_expected_binding_generation <= 0 + or p_expected_revision < 0 + or not programmable_private.valid_profile_username(p_username) + or not programmable_private.valid_avatar_reference(p_avatar_reference) + or (p_display_name is not null and pg_catalog.octet_length(p_display_name) not between 1 and 64) + or (p_bio is not null and pg_catalog.octet_length(p_bio) > 280) + then + raise exception using errcode = '22023', message = 'invalid profile mutation'; + end if; + perform pg_catalog.pg_advisory_xact_lock( + programmable_private.profile_lock_key(p_wallet, 1) + ); + select * into binding + from programmable_private.profile_owner_binding_current + where wallet = p_wallet + for update; + select * into alias + from programmable_private.profile_subject_aliases + where hash_version = p_hash_version + and keyed_subject_hash = p_keyed_subject_hash; + if binding.wallet is null + or binding.state not in ('active', 'recovered') + or binding.generation <> p_expected_binding_generation + or alias.alias_id is null + or alias.subject_id <> binding.subject_id + or not exists ( + select 1 + from programmable_private.profile_hash_version_status_current + where hash_version = p_hash_version and state = 'current' + ) + or not exists ( + select 1 + from programmable_private.profile_subject_current_alias + where subject_id = binding.subject_id + and alias_id = alias.alias_id + and generation = p_expected_binding_generation + ) + then + raise exception using errcode = '40001', message = 'profile binding generation or alias is stale'; + end if; + audit_id := programmable_private.append_mutation_audit( + 'profile.mutate', p_proof_commitment, null, p_mutated_at + ); + update programmable_private.profiles + set username = p_username, + username_key = case when p_username is null then null else pg_catalog.lower(p_username) end, + avatar_reference = p_avatar_reference, + display_name = p_display_name, + bio = p_bio, + revision = p_expected_revision + 1, + updated_at = p_mutated_at, + last_mutation_audit_id = audit_id + where subject_id = binding.subject_id + and revision = p_expected_revision + and deleted_at is null; + if not found then + raise exception using errcode = '40001', message = 'profile revision CAS lost'; + end if; + insert into programmable_private.profile_audit_records ( + profile_audit_id, subject_id, wallet, action, + expected_binding_generation, resulting_binding_generation, + expected_revision, resulting_revision, proof_commitment, caller_role, + occurred_at, mutation_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), binding.subject_id, + p_wallet::programmable_private.eth_address, 'profile.mutate', + p_expected_binding_generation, p_expected_binding_generation, + p_expected_revision, p_expected_revision + 1, + p_proof_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_mutated_at, audit_id + ); + return p_expected_revision + 1; +end +$function$; + +create function programmable_private.append_token_project_metadata_revision( + p_metadata_id uuid, + p_wallet bytea, + p_hash_version smallint, + p_keyed_subject_hash bytea, + p_expected_binding_generation bigint, + p_chain_id bigint, + p_token bytea, + p_expected_metadata_revision bigint, + p_project_name text, + p_description text, + p_logo_reference text, + p_input_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + lock_keys bigint[]; + lock_key bigint; + binding programmable_private.profile_owner_binding_current%rowtype; + owner_alias programmable_private.profile_subject_aliases%rowtype; + existing programmable_private.token_project_metadata%rowtype; + latest programmable_private.token_project_metadata%rowtype; + audit_id uuid; + next_revision bigint; +begin + perform programmable_private.assert_caller('programmable_profile_writer'); + if p_metadata_id is null + or p_wallet is null + or pg_catalog.octet_length(p_wallet) <> 20 + or p_wallet = pg_catalog.decode('0000000000000000000000000000000000000000', 'hex') + or p_hash_version is null + or p_hash_version <= 0 + or p_keyed_subject_hash is null + or pg_catalog.octet_length(p_keyed_subject_hash) <> 32 + or p_expected_binding_generation is null + or p_expected_binding_generation <= 0 + or p_chain_id is null + or p_chain_id <= 0 + or p_token is null + or pg_catalog.octet_length(p_token) <> 20 + or p_token = pg_catalog.decode('0000000000000000000000000000000000000000', 'hex') + or p_expected_metadata_revision is null + or p_expected_metadata_revision < 0 + or p_expected_metadata_revision = 9223372036854775807 + or (p_project_name is not null and pg_catalog.octet_length(p_project_name) > 128) + or (p_description is not null and pg_catalog.octet_length(p_description) > 2000) + or not programmable_private.valid_avatar_reference(p_logo_reference) + or p_input_commitment is null + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_created_at is null + then + raise exception using errcode = '22023', message = 'invalid token-project metadata revision'; + end if; + next_revision := p_expected_metadata_revision + 1; + + lock_keys := array[ + programmable_private.profile_lock_key(p_wallet, 1), + programmable_private.profile_lock_key( + pg_catalog.int8send(p_chain_id) || p_token, + 3 + ) + ]; + for lock_key in + select distinct requested_lock.key_value + from pg_catalog.unnest(lock_keys) as requested_lock(key_value) + order by requested_lock.key_value + loop + perform pg_catalog.pg_advisory_xact_lock(lock_key); + end loop; + + select * into binding + from programmable_private.profile_owner_binding_current + where wallet = p_wallet + for update; + select * into owner_alias + from programmable_private.profile_subject_aliases + where hash_version = p_hash_version + and keyed_subject_hash = p_keyed_subject_hash; + if binding.wallet is null + or binding.state not in ('active', 'recovered') + or binding.generation <> p_expected_binding_generation + or owner_alias.alias_id is null + or owner_alias.subject_id <> binding.subject_id + or not exists ( + select 1 + from programmable_private.profile_hash_version_status_current + where hash_version = p_hash_version and state = 'current' + ) + or not exists ( + select 1 + from programmable_private.profile_subject_alias_status_current + where alias_id = owner_alias.alias_id and state = 'current' + ) + or not exists ( + select 1 + from programmable_private.profile_subject_current_alias + where subject_id = binding.subject_id + and alias_id = owner_alias.alias_id + and generation = p_expected_binding_generation + ) + or not exists ( + select 1 + from programmable_private.profiles + where subject_id = binding.subject_id and deleted_at is null + ) + then + raise exception using + errcode = '40001', + message = 'metadata owner binding generation or alias is stale'; + end if; + + select * into existing + from programmable_private.token_project_metadata + where metadata_id = p_metadata_id; + if found then + if existing.chain_id <> p_chain_id + or existing.token <> p_token + or existing.project_name is distinct from p_project_name + or existing.description is distinct from p_description + or existing.logo_reference is distinct from p_logo_reference + or existing.metadata_revision <> next_revision + or existing.subject_id <> binding.subject_id + or existing.created_at <> p_created_at + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) is distinct from p_input_commitment + then + raise exception using + errcode = '23505', + message = 'token-project metadata replay changed content'; + end if; + return existing.metadata_id; + end if; + + select * into latest + from programmable_private.token_project_metadata + where chain_id = p_chain_id and token = p_token + order by metadata_revision desc + limit 1 + for share; + if found then + if latest.subject_id <> binding.subject_id then + raise exception using + errcode = '42501', + message = 'token-project metadata belongs to another profile subject'; + end if; + if latest.metadata_revision <> p_expected_metadata_revision then + raise exception using + errcode = '40001', + message = 'token-project metadata revision CAS lost'; + end if; + elsif p_expected_metadata_revision <> 0 then + raise exception using + errcode = '40001', + message = 'token-project metadata revision CAS lost'; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'project_metadata.append', p_input_commitment, null, p_created_at + ); + insert into programmable_private.token_project_metadata ( + metadata_id, chain_id, token, project_name, description, + logo_reference, metadata_revision, subject_id, created_at, audit_id + ) + values ( + p_metadata_id, + p_chain_id::programmable_private.chain_id_value, + p_token::programmable_private.eth_address, + p_project_name, + p_description, + p_logo_reference, + next_revision, + binding.subject_id, + p_created_at, + audit_id + ); + insert into programmable_private.profile_audit_records ( + profile_audit_id, subject_id, wallet, action, + expected_binding_generation, resulting_binding_generation, + expected_revision, resulting_revision, proof_commitment, caller_role, + occurred_at, mutation_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), binding.subject_id, + p_wallet::programmable_private.eth_address, 'project_metadata.append', + p_expected_binding_generation, p_expected_binding_generation, + null, null, + p_input_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_created_at, audit_id + ); + return p_metadata_id; +end +$function$; + +create function programmable_private.append_project_metadata_link( + p_project_link_id uuid, + p_metadata_id uuid, + p_wallet bytea, + p_hash_version smallint, + p_keyed_subject_hash bytea, + p_expected_binding_generation bigint, + p_expected_metadata_revision bigint, + p_link_kind text, + p_https_url text, + p_display_order integer, + p_input_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + lock_keys bigint[]; + lock_key bigint; + binding programmable_private.profile_owner_binding_current%rowtype; + owner_alias programmable_private.profile_subject_aliases%rowtype; + metadata programmable_private.token_project_metadata%rowtype; + latest programmable_private.token_project_metadata%rowtype; + existing programmable_private.project_links%rowtype; + requested_link_kind text; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_profile_writer'); + if p_project_link_id is null + or p_metadata_id is null + or p_wallet is null + or pg_catalog.octet_length(p_wallet) <> 20 + or p_wallet = pg_catalog.decode('0000000000000000000000000000000000000000', 'hex') + or p_hash_version is null + or p_hash_version <= 0 + or p_keyed_subject_hash is null + or pg_catalog.octet_length(p_keyed_subject_hash) <> 32 + or p_expected_binding_generation is null + or p_expected_binding_generation <= 0 + or p_expected_metadata_revision is null + or p_expected_metadata_revision <= 0 + or p_link_kind is null + or pg_catalog.octet_length(p_link_kind) not between 1 and 128 + or p_link_kind !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or p_https_url is null + or pg_catalog.octet_length(p_https_url) not between 9 and 512 + or p_https_url !~ '^https://[A-Za-z0-9.-]+(?::[0-9]+)?/' + or p_display_order is null + or p_display_order not between 0 and 15 + or p_input_commitment is null + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_created_at is null + then + raise exception using errcode = '22023', message = 'invalid project metadata link'; + end if; + requested_link_kind := p_link_kind; + + select * into metadata + from programmable_private.token_project_metadata + where metadata_id = p_metadata_id; + if not found then + raise exception using errcode = '23503', message = 'unknown token-project metadata revision'; + end if; + + lock_keys := array[ + programmable_private.profile_lock_key(p_wallet, 1), + programmable_private.profile_lock_key( + pg_catalog.int8send(metadata.chain_id::bigint) || metadata.token, + 3 + ) + ]; + for lock_key in + select distinct requested_lock.key_value + from pg_catalog.unnest(lock_keys) as requested_lock(key_value) + order by requested_lock.key_value + loop + perform pg_catalog.pg_advisory_xact_lock(lock_key); + end loop; + + select * into binding + from programmable_private.profile_owner_binding_current + where wallet = p_wallet + for update; + select * into owner_alias + from programmable_private.profile_subject_aliases + where hash_version = p_hash_version + and keyed_subject_hash = p_keyed_subject_hash; + select * into metadata + from programmable_private.token_project_metadata + where metadata_id = p_metadata_id + for share; + if binding.wallet is null + or binding.state not in ('active', 'recovered') + or binding.generation <> p_expected_binding_generation + or owner_alias.alias_id is null + or owner_alias.subject_id <> binding.subject_id + or metadata.metadata_id is null + or not exists ( + select 1 + from programmable_private.profile_hash_version_status_current + where hash_version = p_hash_version and state = 'current' + ) + or not exists ( + select 1 + from programmable_private.profile_subject_alias_status_current + where alias_id = owner_alias.alias_id and state = 'current' + ) + or not exists ( + select 1 + from programmable_private.profile_subject_current_alias + where subject_id = binding.subject_id + and alias_id = owner_alias.alias_id + and generation = p_expected_binding_generation + ) + or not exists ( + select 1 + from programmable_private.profiles + where subject_id = binding.subject_id and deleted_at is null + ) + then + raise exception using + errcode = '40001', + message = 'project-link owner binding generation or alias is stale'; + end if; + if metadata.subject_id <> binding.subject_id then + raise exception using + errcode = '42501', + message = 'token-project metadata belongs to another profile subject'; + end if; + if metadata.metadata_revision <> p_expected_metadata_revision then + raise exception using + errcode = '40001', + message = 'project-link metadata revision is stale'; + end if; + + select * into existing + from programmable_private.project_links + where project_link_id = p_project_link_id; + if found then + if existing.metadata_id <> p_metadata_id + or existing.link_kind <> requested_link_kind + or existing.https_url <> p_https_url + or existing.display_order <> p_display_order + or existing.created_at <> p_created_at + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) is distinct from p_input_commitment + then + raise exception using + errcode = '23505', + message = 'project metadata link replay changed content'; + end if; + return existing.project_link_id; + end if; + + select * into latest + from programmable_private.token_project_metadata + where chain_id = metadata.chain_id and token = metadata.token + order by metadata_revision desc + limit 1 + for share; + if not found + or latest.metadata_id <> p_metadata_id + or latest.metadata_revision <> p_expected_metadata_revision + then + raise exception using + errcode = '40001', + message = 'project link metadata revision CAS lost'; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'project_metadata_link.append', p_input_commitment, null, p_created_at + ); + insert into programmable_private.project_links ( + project_link_id, metadata_id, link_kind, https_url, display_order, + created_at, audit_id + ) + values ( + p_project_link_id, + p_metadata_id, + requested_link_kind::programmable_private.source_identifier, + p_https_url, + p_display_order, + p_created_at, + audit_id + ); + insert into programmable_private.profile_audit_records ( + profile_audit_id, subject_id, wallet, action, + expected_binding_generation, resulting_binding_generation, + expected_revision, resulting_revision, proof_commitment, caller_role, + occurred_at, mutation_audit_id + ) + values ( + pg_catalog.gen_random_uuid(), binding.subject_id, + p_wallet::programmable_private.eth_address, + 'project_metadata_link.append', + p_expected_binding_generation, p_expected_binding_generation, + null, null, + p_input_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_created_at, audit_id + ); + return p_project_link_id; +end +$function$; + +create function programmable_private.append_reconciliation_record( + p_reconciliation_id uuid, + p_run_id uuid, + p_comparison_kind text, + p_severity text, + p_source_from_block numeric, + p_source_to_block numeric, + p_compared_count bigint, + p_mismatch_count bigint, + p_evidence_commitment bytea, + p_mismatch_identity_commitments bytea[], + p_resolved_at timestamptz, + p_recorded_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + from_block bigint; + to_block bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'reconciliation'; + if not found then + raise exception using errcode = '23503', message = 'invalid reconciliation run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if p_source_from_block <> pg_catalog.trunc(p_source_from_block) + or p_source_to_block <> pg_catalog.trunc(p_source_to_block) + or p_source_from_block < 0 + or p_source_to_block < p_source_from_block + or p_source_to_block > 9223372036854775807 + or p_compared_count < 0 + or p_mismatch_count < 0 + or p_mismatch_count > p_compared_count + or pg_catalog.octet_length(p_evidence_commitment) <> 32 + or not programmable_private.valid_topics(p_mismatch_identity_commitments) + then + raise exception using errcode = '22023', message = 'invalid reconciliation evidence'; + end if; + from_block := p_source_from_block::bigint; + to_block := p_source_to_block::bigint; + audit_id := programmable_private.append_mutation_audit( + 'reconciliation.append', p_evidence_commitment, p_run_id, p_recorded_at + ); + insert into programmable_private.reconciliation_records ( + reconciliation_id, run_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, comparison_kind, severity, source_from_block, + source_to_block, compared_count, mismatch_count, evidence_commitment, + mismatch_identity_commitments, resolved_at, recorded_at, audit_id + ) + values ( + p_reconciliation_id, p_run_id, header.chain_id, header.release_id, + header.model_id, header.epoch_id, header.captured_pointer_generation, + p_comparison_kind::programmable_private.source_identifier, + p_severity::programmable_private.reconciliation_severity, + from_block::programmable_private.block_number_value, + to_block::programmable_private.block_number_value, + p_compared_count, p_mismatch_count, + p_evidence_commitment::programmable_private.bytes32_value, + p_mismatch_identity_commitments, p_resolved_at, p_recorded_at, audit_id + ); + return p_reconciliation_id; +end +$function$; + +create function programmable_private.append_parity_record( + p_parity_record_id uuid, + p_reconciliation_id uuid, + p_route_key text, + p_legacy_dto_hash bytea, + p_indexed_dto_hash bytea, + p_compared_at timestamptz, + p_resolved_at timestamptz default null +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + existing programmable_private.parity_records%rowtype; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into reconciliation + from programmable_private.reconciliation_records + where reconciliation_id = p_reconciliation_id; + if not found then + raise exception using errcode = '23503', message = 'unknown reconciliation'; + end if; + select * into header + from programmable_private.run_headers + where run_id = reconciliation.run_id + and run_kind = 'reconciliation'; + if not found then + raise exception using errcode = '23503', message = 'invalid reconciliation provenance'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = header.run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if pg_catalog.octet_length(p_legacy_dto_hash) <> 32 + or pg_catalog.octet_length(p_indexed_dto_hash) <> 32 + or (p_legacy_dto_hash = p_indexed_dto_hash and p_resolved_at is not null) + then + raise exception using errcode = '22023', message = 'invalid parity evidence'; + end if; + select * into existing + from programmable_private.parity_records + where parity_record_id = p_parity_record_id; + if found then + if existing.reconciliation_id <> p_reconciliation_id + or existing.route_key <> p_route_key + or existing.legacy_dto_hash <> p_legacy_dto_hash + or existing.indexed_dto_hash <> p_indexed_dto_hash + or existing.compared_at <> p_compared_at + or existing.resolved_at is distinct from p_resolved_at + then + raise exception using errcode = '23505', message = 'parity replay changed content'; + end if; + return existing.parity_record_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'parity.append', p_indexed_dto_hash, reconciliation.run_id, p_compared_at + ); + insert into programmable_private.parity_records ( + parity_record_id, reconciliation_id, route_key, legacy_dto_hash, + indexed_dto_hash, is_match, compared_at, resolved_at, audit_id + ) + values ( + p_parity_record_id, p_reconciliation_id, + p_route_key::programmable_private.source_identifier, + p_legacy_dto_hash::programmable_private.bytes32_value, + p_indexed_dto_hash::programmable_private.bytes32_value, + p_legacy_dto_hash = p_indexed_dto_hash, + p_compared_at, p_resolved_at, audit_id + ); + return p_parity_record_id; +end +$function$; + +create function programmable_private.append_market_snapshot( + p_market_snapshot_id uuid, + p_reconciliation_id uuid, + p_source_deployment_id uuid, + p_block_evidence_id uuid, + p_pool_id bytea, + p_block_number numeric, + p_block_hash bytea, + p_sqrt_price_x96 numeric, + p_liquidity numeric, + p_market_volume_token0 numeric, + p_market_volume_token1 numeric, + p_market_volume_usd numeric, + p_hook_gross_volume numeric, + p_observed_at timestamptz, + p_input_commitment bytea +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + block_evidence programmable_private.dual_rpc_block_evidence%rowtype; + observation programmable_private.safe_head_observations%rowtype; + existing programmable_private.market_snapshots%rowtype; + normalized_block bigint; + normalized_sqrt numeric; + normalized_liquidity numeric; + normalized_hook_volume numeric; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into reconciliation + from programmable_private.reconciliation_records + where reconciliation_id = p_reconciliation_id; + if not found then + raise exception using errcode = '23503', message = 'unknown reconciliation'; + end if; + select * into header + from programmable_private.run_headers + where run_id = reconciliation.run_id + and run_kind = 'reconciliation'; + if not found then + raise exception using errcode = '23503', message = 'invalid reconciliation provenance'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = header.run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if reconciliation.mismatch_count <> 0 or not exists ( + select 1 + from programmable_private.provider_deployments + where provider_deployment_id = p_source_deployment_id + and provider_type = 'uniswap_subgraph' + ) then + raise exception using errcode = '23503', message = 'invalid market source deployment'; + end if; + normalized_sqrt := programmable_private.validate_uint256(p_sqrt_price_x96); + normalized_liquidity := programmable_private.validate_uint256(p_liquidity); + if p_hook_gross_volume is not null then + normalized_hook_volume := + programmable_private.validate_uint256(p_hook_gross_volume); + end if; + if p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 + or p_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_block_hash) <> 32 + or p_market_volume_token0 < 0 + or p_market_volume_token1 < 0 + or (p_market_volume_usd is not null and p_market_volume_usd < 0) + or p_market_volume_token0::text in ('NaN', 'Infinity', '-Infinity') + or p_market_volume_token1::text in ('NaN', 'Infinity', '-Infinity') + or p_market_volume_usd::text in ('NaN', 'Infinity', '-Infinity') + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid market snapshot'; + end if; + normalized_block := p_block_number::bigint; + select * into block_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id; + if not found then + raise exception using errcode = '23503', message = 'unknown market block evidence'; + end if; + select * into observation + from programmable_private.safe_head_observations + where observation_id = block_evidence.observation_id; + if not found + or block_evidence.chain_id <> header.chain_id + or block_evidence.epoch_id <> header.epoch_id + or block_evidence.pointer_generation <> header.captured_pointer_generation + or block_evidence.block_number <> normalized_block + or block_evidence.agreed_block_hash <> p_block_hash + or observation.release_id <> header.release_id + or observation.model_id <> header.model_id + or observation.source_group <> header.source_group + or normalized_block < reconciliation.source_from_block + or normalized_block > reconciliation.source_to_block + then + raise exception using errcode = '23514', message = 'market snapshot lacks exact canonical block evidence'; + end if; + select * into existing + from programmable_private.market_snapshots + where market_snapshot_id = p_market_snapshot_id; + if found then + if existing.reconciliation_id <> p_reconciliation_id + or existing.source_deployment_id <> p_source_deployment_id + or existing.block_evidence_id <> p_block_evidence_id + or existing.pool_id <> p_pool_id + or existing.block_number <> normalized_block + or existing.block_hash <> p_block_hash + or existing.sqrt_price_x96 <> normalized_sqrt + or existing.liquidity <> normalized_liquidity + or existing.market_volume_token0 <> p_market_volume_token0 + or existing.market_volume_token1 <> p_market_volume_token1 + or existing.market_volume_usd is distinct from p_market_volume_usd + or existing.hook_gross_volume is distinct from normalized_hook_volume + or existing.observed_at <> p_observed_at + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_input_commitment + then + raise exception using errcode = '23505', message = 'market snapshot replay changed content'; + end if; + return existing.market_snapshot_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'market_snapshot.append', p_input_commitment, + reconciliation.run_id, p_observed_at + ); + insert into programmable_private.market_snapshots ( + market_snapshot_id, chain_id, pool_id, source_deployment_id, + block_evidence_id, + block_number, block_hash, sqrt_price_x96, liquidity, + market_volume_token0, market_volume_token1, market_volume_usd, + hook_gross_volume, observed_at, reconciliation_id, audit_id + ) + values ( + p_market_snapshot_id, reconciliation.chain_id, + p_pool_id::programmable_private.bytes32_value, p_source_deployment_id, + p_block_evidence_id, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + normalized_sqrt::programmable_private.uint256_value, + normalized_liquidity::programmable_private.uint256_value, + p_market_volume_token0, p_market_volume_token1, p_market_volume_usd, + normalized_hook_volume::programmable_private.uint256_value, + p_observed_at, p_reconciliation_id, audit_id + ); + return p_market_snapshot_id; +end +$function$; + +create function programmable_private.append_market_candle( + p_market_candle_id uuid, + p_reconciliation_id uuid, + p_source_deployment_id uuid, + p_source_block_evidence_id uuid, + p_pool_id bytea, + p_interval text, + p_period_start timestamptz, + p_period_end timestamptz, + p_open numeric, + p_high numeric, + p_low numeric, + p_close numeric, + p_volume_token0 numeric, + p_volume_token1 numeric, + p_volume_usd numeric, + p_source_block_hash bytea, + p_input_commitment bytea +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + block_evidence programmable_private.dual_rpc_block_evidence%rowtype; + observation programmable_private.safe_head_observations%rowtype; + requested_interval programmable_private.market_interval; + existing programmable_private.market_candles%rowtype; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into reconciliation + from programmable_private.reconciliation_records + where reconciliation_id = p_reconciliation_id; + if not found then + raise exception using errcode = '23503', message = 'unknown reconciliation'; + end if; + select * into header + from programmable_private.run_headers + where run_id = reconciliation.run_id; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = header.run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if reconciliation.mismatch_count <> 0 or not exists ( + select 1 + from programmable_private.provider_deployments + where provider_deployment_id = p_source_deployment_id + and provider_type = 'uniswap_subgraph' + ) then + raise exception using errcode = '23503', message = 'invalid candle source deployment'; + end if; + select * into block_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_source_block_evidence_id; + if not found then + raise exception using errcode = '23503', message = 'unknown candle block evidence'; + end if; + select * into observation + from programmable_private.safe_head_observations + where observation_id = block_evidence.observation_id; + if not found + or block_evidence.chain_id <> header.chain_id + or block_evidence.epoch_id <> header.epoch_id + or block_evidence.pointer_generation <> header.captured_pointer_generation + or block_evidence.agreed_block_hash <> p_source_block_hash + or observation.release_id <> header.release_id + or observation.model_id <> header.model_id + or observation.source_group <> header.source_group + or block_evidence.block_number < reconciliation.source_from_block + or block_evidence.block_number > reconciliation.source_to_block + then + raise exception using errcode = '23514', message = 'market candle lacks exact canonical block evidence'; + end if; + requested_interval := p_interval::programmable_private.market_interval; + if requested_interval not in ('hour', 'day') + or p_period_end <= p_period_start + or p_open < 0 or p_high < 0 or p_low < 0 or p_close < 0 + or p_high < greatest(p_open, p_close, p_low) + or p_volume_token0 < 0 or p_volume_token1 < 0 + or (p_volume_usd is not null and p_volume_usd < 0) + or p_open::text in ('NaN', 'Infinity', '-Infinity') + or p_high::text in ('NaN', 'Infinity', '-Infinity') + or p_low::text in ('NaN', 'Infinity', '-Infinity') + or p_close::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_token0::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_token1::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_usd::text in ('NaN', 'Infinity', '-Infinity') + or pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_source_block_hash) <> 32 + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid market candle'; + end if; + select * into existing + from programmable_private.market_candles + where market_candle_id = p_market_candle_id; + if found then + if existing.reconciliation_id <> p_reconciliation_id + or existing.source_deployment_id <> p_source_deployment_id + or existing.source_block_evidence_id <> p_source_block_evidence_id + or existing.source_block_number <> block_evidence.block_number + or existing.pool_id <> p_pool_id + or existing.interval <> requested_interval + or existing.period_start <> p_period_start + or existing.period_end <> p_period_end + or existing.open <> p_open + or existing.high <> p_high + or existing.low <> p_low + or existing.close <> p_close + or existing.volume_token0 <> p_volume_token0 + or existing.volume_token1 <> p_volume_token1 + or existing.volume_usd is distinct from p_volume_usd + or existing.source_block_hash <> p_source_block_hash + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_input_commitment + then + raise exception using errcode = '23505', message = 'market candle replay changed content'; + end if; + return existing.market_candle_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'market_candle.append', p_input_commitment, + reconciliation.run_id, p_period_end + ); + insert into programmable_private.market_candles ( + market_candle_id, chain_id, pool_id, source_deployment_id, + source_block_evidence_id, source_block_number, + interval, period_start, period_end, + open, high, low, close, volume_token0, volume_token1, volume_usd, + source_block_hash, reconciliation_id, audit_id + ) + values ( + p_market_candle_id, reconciliation.chain_id, + p_pool_id::programmable_private.bytes32_value, p_source_deployment_id, + p_source_block_evidence_id, block_evidence.block_number, requested_interval, + p_period_start, p_period_end, p_open, p_high, p_low, p_close, + p_volume_token0, p_volume_token1, p_volume_usd, + p_source_block_hash::programmable_private.bytes32_value, + p_reconciliation_id, audit_id + ); + return p_market_candle_id; +end +$function$; + +create function programmable_private.append_portfolio_point( + p_portfolio_point_id uuid, + p_source_checkpoint_id uuid, + p_account bytea, + p_interval_minutes integer, + p_point_time timestamptz, + p_exact_reward_total numeric, + p_input_commitment bytea +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + checkpoint programmable_private.projector_checkpoints%rowtype; + exact_total numeric; + existing programmable_private.portfolio_points%rowtype; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into checkpoint + from programmable_private.projector_checkpoints + where checkpoint_id = p_source_checkpoint_id; + if not found then + raise exception using errcode = '23503', message = 'unknown source checkpoint'; + end if; + exact_total := programmable_private.validate_uint256(p_exact_reward_total); + if pg_catalog.octet_length(p_account) <> 20 + or p_interval_minutes not in (5, 1440) + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid portfolio point'; + end if; + select * into existing + from programmable_private.portfolio_points + where portfolio_point_id = p_portfolio_point_id; + if found then + if existing.source_checkpoint_id <> p_source_checkpoint_id + or existing.account <> p_account + or existing.interval_minutes <> p_interval_minutes + or existing.point_time <> p_point_time + or existing.exact_reward_total <> exact_total + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_input_commitment + then + raise exception using errcode = '23505', message = 'portfolio point replay changed content'; + end if; + return existing.portfolio_point_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'portfolio_point.append', p_input_commitment, + checkpoint.run_id, p_point_time + ); + insert into programmable_private.portfolio_points ( + portfolio_point_id, chain_id, account, interval_minutes, point_time, + exact_reward_total, source_checkpoint_id, audit_id + ) + values ( + p_portfolio_point_id, checkpoint.chain_id, + p_account::programmable_private.eth_address, p_interval_minutes, + p_point_time, exact_total::programmable_private.uint256_value, + p_source_checkpoint_id, audit_id + ); + return p_portfolio_point_id; +end +$function$; + +create function programmable_private.prune_run_telemetry( + p_now timestamptz, + p_limit integer, + p_input_commitment bytea +) +returns integer +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + deleted_count integer; +begin + perform programmable_private.assert_caller('programmable_maintenance'); + if p_limit < 1 or p_limit > 10000 + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid retention request'; + end if; + with doomed as ( + select telemetry_id + from programmable_private.run_telemetry + where ( + not failed_or_reorg and sampled_at < p_now - interval '30 days' + ) or ( + failed_or_reorg and sampled_at < p_now - interval '180 days' + ) + order by sampled_at, telemetry_id + limit p_limit + for update skip locked + ) + delete from programmable_private.run_telemetry as telemetry + using doomed + where telemetry.telemetry_id = doomed.telemetry_id; + get diagnostics deleted_count = row_count; + perform programmable_private.append_mutation_audit( + 'retention.run_telemetry', p_input_commitment, null, p_now + ); + return deleted_count; +end +$function$; + +create function programmable_private.prune_market_data( + p_now timestamptz, + p_limit integer, + p_input_commitment bytea +) +returns integer +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + remaining integer := p_limit; + changed integer := 0; + step_count integer; +begin + perform programmable_private.assert_caller('programmable_maintenance'); + if p_limit < 1 or p_limit > 10000 + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid retention request'; + end if; + with doomed as ( + select market_snapshot_id + from programmable_private.market_snapshots + where observed_at < p_now - interval '7 days' + order by observed_at, market_snapshot_id + limit remaining + for update skip locked + ) + delete from programmable_private.market_snapshots as snapshot + using doomed + where snapshot.market_snapshot_id = doomed.market_snapshot_id; + get diagnostics step_count = row_count; + changed := changed + step_count; + remaining := remaining - step_count; + if remaining > 0 then + with doomed as ( + select market_candle_id + from programmable_private.market_candles + where interval = 'hour' + and period_start < p_now - interval '90 days' + order by period_start, market_candle_id + limit remaining + for update skip locked + ) + delete from programmable_private.market_candles as candle + using doomed + where candle.market_candle_id = doomed.market_candle_id; + get diagnostics step_count = row_count; + changed := changed + step_count; + remaining := remaining - step_count; + end if; + if remaining > 0 then + with doomed as ( + select portfolio_point_id + from programmable_private.portfolio_points + where interval_minutes = 5 + and point_time < p_now - interval '400 days' + order by point_time, portfolio_point_id + limit remaining + for update skip locked + ) + delete from programmable_private.portfolio_points as point + using doomed + where point.portfolio_point_id = doomed.portfolio_point_id; + get diagnostics step_count = row_count; + changed := changed + step_count; + end if; + perform programmable_private.append_mutation_audit( + 'retention.market', p_input_commitment, null, p_now + ); + return changed; +end +$function$; + +create function programmable_private.prune_parity_records( + p_now timestamptz, + p_limit integer, + p_input_commitment bytea +) +returns integer +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + deleted_count integer; +begin + perform programmable_private.assert_caller('programmable_maintenance'); + if p_limit < 1 or p_limit > 10000 + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid retention request'; + end if; + with doomed as ( + select parity_record_id + from programmable_private.parity_records + where ( + is_match and compared_at < p_now - interval '30 days' + ) or ( + not is_match and resolved_at is not null + and resolved_at < p_now - interval '180 days' + ) + order by compared_at, parity_record_id + limit p_limit + for update skip locked + ) + delete from programmable_private.parity_records as parity + using doomed + where parity.parity_record_id = doomed.parity_record_id; + get diagnostics deleted_count = row_count; + perform programmable_private.append_mutation_audit( + 'retention.parity', p_input_commitment, null, p_now + ); + return deleted_count; +end +$function$; + +do $lockdown$ +declare + table_record record; +begin + for table_record in + select c.relname + from pg_catalog.pg_class as c + join pg_catalog.pg_namespace as n on n.oid = c.relnamespace + where n.nspname = 'programmable_private' + and c.relkind in ('r', 'p') + and not c.relrowsecurity + loop + execute pg_catalog.format( + 'alter table programmable_private.%I enable row level security', + table_record.relname + ); + execute pg_catalog.format( + 'alter table programmable_private.%I force row level security', + table_record.relname + ); + execute pg_catalog.format( + 'create policy migrator_owner_all on programmable_private.%I ' || + 'for all to programmable_migrator using (true) with check (true)', + table_record.relname + ); + end loop; +end +$lockdown$; + +revoke all on all tables in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all sequences in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all functions in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +grant usage on schema programmable_private + to programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function programmable_private.define_profile_hash_version( + smallint, text, bytea, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.set_profile_hash_version_state( + uuid, smallint, text, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.bind_profile_subject( + bytea, smallint, bytea, text, bytea, timestamptz +) to programmable_profile_binder; +grant execute on function programmable_private.rekey_profile_subject( + bytea, smallint, bytea, smallint, bytea, bigint, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.tombstone_profile_binding( + bytea, smallint, bytea, bigint, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.recover_profile_binding( + bytea, smallint, bytea, bigint, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.mutate_profile( + bytea, smallint, bytea, bigint, bigint, text, text, text, text, + bytea, timestamptz +) to programmable_profile_writer; +grant execute on function programmable_private.append_reconciliation_record( + uuid, uuid, text, text, numeric, numeric, bigint, bigint, bytea, + bytea[], timestamptz, timestamptz +) to programmable_reconciler; +grant execute on function programmable_private.append_parity_record( + uuid, uuid, text, bytea, bytea, timestamptz, timestamptz +) to programmable_reconciler; +grant execute on function programmable_private.append_market_snapshot( + uuid, uuid, uuid, uuid, bytea, numeric, bytea, numeric, numeric, numeric, + numeric, numeric, numeric, timestamptz, bytea +) to programmable_reconciler; +grant execute on function programmable_private.append_market_candle( + uuid, uuid, uuid, uuid, bytea, text, timestamptz, timestamptz, numeric, numeric, + numeric, numeric, numeric, numeric, numeric, bytea, bytea +) to programmable_reconciler; +grant execute on function programmable_private.append_portfolio_point( + uuid, uuid, bytea, integer, timestamptz, numeric, bytea +) to programmable_reconciler; +grant execute on function programmable_private.prune_run_telemetry( + timestamptz, integer, bytea +) to programmable_maintenance; +grant execute on function programmable_private.prune_market_data( + timestamptz, integer, bytea +) to programmable_maintenance; +grant execute on function programmable_private.prune_parity_records( + timestamptz, integer, bytea +) to programmable_maintenance; + +reset role; diff --git a/supabase/migrations/20260731000600_read_views_functions_grants.sql b/supabase/migrations/20260731000600_read_views_functions_grants.sql new file mode 100644 index 00000000..e96e82dd --- /dev/null +++ b/supabase/migrations/20260731000600_read_views_functions_grants.sql @@ -0,0 +1,2129 @@ +-- Stable server-only read surface and final least-privilege closure. + +set role programmable_migrator; + +create function programmable_private.reject_immutable_mutation() +returns trigger +language plpgsql +volatile +security invoker +set search_path = '' +as $function$ +begin + raise exception using + errcode = '55000', + message = pg_catalog.format( + '%s is immutable; append a new fact/history row instead', + tg_table_schema || '.' || tg_table_name + ); +end +$function$; + +do $immutable_triggers$ +declare + table_name text; +begin + foreach table_name in array array[ + 'fingerprint_encoding_versions', + 'mutation_audits', + 'release_epochs', + 'release_source_bindings', + 'provider_deployments', + 'release_epoch_pointer_history', + 'run_headers', + 'run_lifecycle_outcomes', + 'safe_head_observations', + 'dual_rpc_block_evidence', + 'projector_lease_history', + 'projector_checkpoints', + 'dependency_health_history', + 'envio_candidates', + 'chain_event_identities', + 'chain_event_occurrences', + 'chain_event_occurrence_status_history', + 'reward_allocation_facts', + 'reward_allocation_required_occurrences', + 'reward_allocation_evidence', + 'reward_allocation_mismatch_evidence', + 'reward_allocation_status_history', + 'projection_fold_manifests', + 'projection_publications', + 'route_eligibility_history', + 'profile_hash_version_definitions', + 'profile_hash_version_status_history', + 'profile_subjects', + 'profile_subject_aliases', + 'profile_subject_alias_status_history', + 'profile_owner_binding_history', + 'profile_audit_records', + 'token_project_metadata', + 'project_links', + 'reconciliation_records' + ] + loop + execute pg_catalog.format( + 'create trigger reject_immutable_mutation ' || + 'before update or delete on programmable_private.%I ' || + 'for each row execute function programmable_private.reject_immutable_mutation()', + table_name + ); + end loop; +end +$immutable_triggers$; + +do $append_only_prunable_triggers$ +declare + table_name text; +begin + foreach table_name in array array[ + 'run_telemetry', + 'parity_records', + 'market_snapshots', + 'market_candles', + 'portfolio_points' + ] + loop + execute pg_catalog.format( + 'create trigger reject_immutable_update ' || + 'before update on programmable_private.%I ' || + 'for each row execute function programmable_private.reject_immutable_mutation()', + table_name + ); + end loop; +end +$append_only_prunable_triggers$; + +-- Global occurrence rows own physical chain identity. Every release-scoped +-- reader consumes this materialized view so decoding, binding and evidence +-- always come from the exact epoch generation rather than the first snapshot. +create view programmable_private.chain_event_materialized_occurrences_v1 +with (security_invoker = false, security_barrier = true) +as +select + occurrence.occurrence_id, + occurrence.logical_event_id, + materialization.chain_id, + occurrence.transaction_hash, + occurrence.receipt_log_ordinal, + occurrence.block_number, + occurrence.block_hash, + occurrence.block_timestamp, + occurrence.transaction_index, + occurrence.source_address, + occurrence.block_global_log_index, + occurrence.event_signature, + materialization.event_type, + occurrence.ordered_topics, + occurrence.raw_data, + materialization.decoded_payload, + materialization.payload_hash, + materialization.decoder_version, + materialization.abi_event_set_commitment, + materialization.release_binding_id, + materialization.dynamic_source_attestation_id, + materialization.release_id, + materialization.model_id, + materialization.source_group, + materialization.epoch_id, + materialization.pointer_generation, + materialization.first_seen_envio_candidate_id, + materialization.first_seen_neutral_candidate_id, + materialization.candidate_resolution_id, + materialization.first_seen_provider_cursor, + materialization.verification_run_id, + materialization.block_evidence_id, + materialization.encoding_version, + materialization.canonical_preimage, + materialization.content_fingerprint, + materialization.verified_at +from programmable_private.chain_event_occurrences as occurrence +join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = occurrence.occurrence_id; + +create function programmable_private.has_current_verified_reward_seed( + p_projection_run_id uuid, + p_vault bytea +) +returns boolean +language sql +stable +security invoker +set search_path = '' +as $function$ + select exists ( + select 1 + from programmable_private.run_headers as run + join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation + join programmable_private.reward_allocation_current_verified as verified_seed + on verified_seed.vault = p_vault + join programmable_private.reward_allocation_facts as seed_fact + on seed_fact.allocation_fact_id = verified_seed.allocation_fact_id + and seed_fact.factory_occurrence_id = verified_seed.factory_occurrence_id + and seed_fact.vault = verified_seed.vault + and seed_fact.chain_id = run.chain_id + and seed_fact.release_id = run.release_id + and seed_fact.model_id = run.model_id + and seed_fact.epoch_id = run.epoch_id + and seed_fact.pointer_generation = run.captured_pointer_generation + join programmable_private.release_source_bindings as factory_binding + on factory_binding.binding_id = seed_fact.factory_release_binding_id + and factory_binding.epoch_id = run.epoch_id + and factory_binding.source_role = 'vault_factory' + and factory_binding.binding_commitment + = seed_fact.factory_release_binding_commitment + join programmable_private.reward_allocation_evidence as seed_evidence + on seed_evidence.allocation_evidence_id + = verified_seed.allocation_evidence_id + and seed_evidence.allocation_fact_id = seed_fact.allocation_fact_id + and seed_evidence.is_recomputation_attested + and seed_evidence.recomputed_allocation_hash = seed_fact.allocation_hash + and seed_evidence.recomputed_configuration_hash + = seed_fact.configuration_hash + and seed_evidence.recomputed_active_configuration_hash + is not distinct from seed_fact.active_configuration_hash + join programmable_private.run_headers as evidence_run + on evidence_run.run_id = seed_evidence.verification_run_id + and evidence_run.chain_id = seed_fact.chain_id + and evidence_run.release_id = seed_fact.release_id + and evidence_run.model_id = seed_fact.model_id + and evidence_run.epoch_id = seed_fact.epoch_id + and evidence_run.captured_pointer_generation = + seed_fact.pointer_generation + join programmable_private.release_source_bindings as recovery_binding + on recovery_binding.binding_id + = seed_evidence.recovery_release_binding_id + and recovery_binding.epoch_id = run.epoch_id + and recovery_binding.binding_commitment + = seed_evidence.recovery_release_binding_commitment + and recovery_binding.source_role = case seed_evidence.recovery_method + when 'launcher_calldata' then 'launcher' + when 'coordinator_calldata' then 'coordinator' + when 'factory_calldata' then 'factory' + else 'vault_factory' + end + join programmable_private.chain_event_current_canonical as seed_canonical + on seed_canonical.logical_event_id = seed_fact.factory_logical_event_id + and seed_canonical.occurrence_id = seed_fact.factory_occurrence_id + and seed_canonical.block_hash = seed_fact.factory_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as seed_occurrence + on seed_occurrence.occurrence_id = seed_fact.factory_occurrence_id + and seed_occurrence.logical_event_id = seed_fact.factory_logical_event_id + and seed_occurrence.block_hash = seed_fact.factory_occurrence_block_hash + and seed_occurrence.chain_id = run.chain_id + and seed_occurrence.release_id = run.release_id + and seed_occurrence.model_id = run.model_id + and seed_occurrence.source_group = run.source_group + and seed_occurrence.epoch_id = run.epoch_id + and seed_occurrence.pointer_generation = + run.captured_pointer_generation + where run.run_id = p_projection_run_id + and run.run_kind = 'projection' + and not exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences + as required_source + join programmable_private.chain_event_materialized_occurrences_v1 + as required_occurrence + on required_occurrence.occurrence_id = required_source.occurrence_id + join programmable_private.release_source_bindings + as required_binding + on required_binding.binding_id = required_source.release_binding_id + left join programmable_private.chain_event_current_canonical + as required_canonical + on required_canonical.logical_event_id = required_occurrence.logical_event_id + and required_canonical.occurrence_id = required_occurrence.occurrence_id + and required_canonical.block_hash = required_occurrence.block_hash + where required_source.allocation_fact_id = seed_fact.allocation_fact_id + and ( + required_occurrence.chain_id <> run.chain_id + or required_occurrence.release_id <> run.release_id + or required_occurrence.model_id <> run.model_id + or required_occurrence.epoch_id <> run.epoch_id + or required_occurrence.pointer_generation + <> run.captured_pointer_generation + or required_occurrence.release_binding_id + <> required_source.release_binding_id + or required_binding.epoch_id <> run.epoch_id + or required_binding.source_role <> required_source.occurrence_role + or required_binding.binding_commitment + <> required_source.release_binding_commitment + or required_canonical.occurrence_id is null + ) + ) + ) +$function$; + +create view programmable_private.current_token_project_metadata_v1 +with (security_invoker = false, security_barrier = true) +as +select + metadata.chain_id, + metadata.token, + metadata.metadata_id, + metadata.project_name, + metadata.description as project_description, + metadata.logo_reference as project_logo_reference, + metadata.metadata_revision as project_metadata_revision, + metadata.created_at as project_metadata_created_at, + coalesce( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'kind', link.link_kind, + 'url', link.https_url, + 'displayOrder', link.display_order + ) + order by link.display_order, link.project_link_id + ) filter (where link.project_link_id is not null), + '[]'::jsonb + ) as project_links +from programmable_private.token_project_metadata as metadata +left join programmable_private.project_links as link + on link.metadata_id = metadata.metadata_id +where not exists ( + select 1 + from programmable_private.token_project_metadata as newer + where newer.chain_id = metadata.chain_id + and newer.token = metadata.token + and newer.metadata_revision > metadata.metadata_revision +) +group by + metadata.chain_id, metadata.token, metadata.metadata_id, + metadata.project_name, metadata.description, metadata.logo_reference, + metadata.metadata_revision, metadata.created_at; + +-- These route keys are the database identifiers for the independently gated +-- route groups in docs/data-pipeline/ARCHITECTURE.md: Explore list/detail, +-- creator profile (including Stock-Paired), Classic V3 profile, and launch +-- confirmation lookup. Eligibility for one route never implies another. +create view programmable_private.recent_launches_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.creator, + launch.launch_transaction_hash, + launch.pool_id, + launch.reward_vault, + launch.launch_hash, + launch.token_name, + launch.token_symbol, + launch.total_supply, + run.source_group, + launch.epoch_id, + launch.pointer_generation, + launch.projection_run_id, + source_occurrence.block_timestamp as launch_block_timestamp, + source_occurrence.transaction_index::bigint as launch_transaction_index, + source_occurrence.receipt_log_ordinal::bigint as launch_receipt_log_ordinal, + pool.currency0, + pool.currency1, + pool.hook, + pool.pool_key_fee, + pool.tick_spacing, + case + when pool.currency0 = launch.token then pool.currency1 + when pool.currency1 = launch.token then pool.currency0 + else null + end as quote_asset, + fee.buy_swap_fee_bps, + fee.sell_swap_fee_bps, + fee.buy_creator_fee_bps, + fee.sell_creator_fee_bps, + fee.creator_fee_bps, + fee.launcher_fee_bps, + fee.transfer_tax_bps, + fee.lp_fee_pips, + greatest(fee.buy_swap_fee_bps, fee.sell_swap_fee_bps) + as total_swap_fee_bps, + metadata.project_name, + metadata.project_description, + metadata.project_logo_reference, + metadata.project_metadata_revision, + metadata.project_metadata_created_at, + coalesce(metadata.project_links, '[]'::jsonb) as project_links, + launch.promoted_block_number, + launch.promoted_block_hash, + launch.verified_at, + profile.username as creator_username, + profile.avatar_reference as creator_avatar_reference +from programmable_private.current_launch_projections_v1 as launch +join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = launch.chain_id + and run.release_id = launch.release_id + and run.model_id = launch.model_id + and run.epoch_id = launch.epoch_id + and run.captured_pointer_generation = launch.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = launch.projection_run_id + and publication.epoch_id = launch.epoch_id + and publication.pointer_generation = launch.pointer_generation + and publication.target_block_number = launch.promoted_block_number + and publication.target_block_hash = launch.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = launch.chain_id + and current_epoch.release_id = launch.release_id + and current_epoch.model_id = launch.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = launch.epoch_id + and current_epoch.generation = launch.pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'explore-list' + and route.chain_id = launch.chain_id + and route.release_id = launch.release_id + and route.model_id = launch.model_id + and route.source_group = run.source_group + and route.epoch_id = launch.epoch_id + and route.pointer_generation = launch.pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as canonical + on canonical.logical_event_id = launch.last_source_logical_event_id + and canonical.occurrence_id = launch.last_source_occurrence_id + and canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as source_occurrence + on source_occurrence.occurrence_id = launch.last_source_occurrence_id + and source_occurrence.logical_event_id = launch.last_source_logical_event_id + and source_occurrence.block_hash = launch.last_source_occurrence_block_hash + and source_occurrence.chain_id = run.chain_id + and source_occurrence.release_id = run.release_id + and source_occurrence.model_id = run.model_id + and source_occurrence.epoch_id = run.epoch_id + and source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + and pool.chain_id = launch.chain_id + and pool.release_id = launch.release_id + and pool.model_id = launch.model_id + and pool.epoch_id = launch.epoch_id + and pool.pointer_generation = launch.pointer_generation + and pool.pool_id = launch.pool_id + and pool.promoted_block_number = launch.promoted_block_number + and pool.promoted_block_hash = launch.promoted_block_hash + and (pool.currency0 = launch.token or pool.currency1 = launch.token) +join programmable_private.chain_event_current_canonical as pool_canonical + on pool_canonical.logical_event_id = pool.last_source_logical_event_id + and pool_canonical.occurrence_id = pool.last_source_occurrence_id + and pool_canonical.block_hash = pool.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as pool_source_occurrence + on pool_source_occurrence.occurrence_id = pool.last_source_occurrence_id + and pool_source_occurrence.logical_event_id = pool.last_source_logical_event_id + and pool_source_occurrence.block_hash = pool.last_source_occurrence_block_hash + and pool_source_occurrence.chain_id = run.chain_id + and pool_source_occurrence.release_id = run.release_id + and pool_source_occurrence.model_id = run.model_id + and pool_source_occurrence.epoch_id = run.epoch_id + and pool_source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = pool.projection_run_id + and fee.chain_id = pool.chain_id + and fee.release_id = pool.release_id + and fee.model_id = pool.model_id + and fee.epoch_id = pool.epoch_id + and fee.pointer_generation = pool.pointer_generation + and fee.promoted_block_number = pool.promoted_block_number + and fee.promoted_block_hash = pool.promoted_block_hash +join programmable_private.chain_event_current_canonical as fee_canonical + on fee_canonical.logical_event_id = fee.disclosure_source_logical_event_id + and fee_canonical.occurrence_id = fee.disclosure_source_occurrence_id + and fee_canonical.block_hash = fee.disclosure_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as fee_source_occurrence + on fee_source_occurrence.occurrence_id = fee.disclosure_source_occurrence_id + and fee_source_occurrence.logical_event_id = fee.disclosure_source_logical_event_id + and fee_source_occurrence.block_hash = fee.disclosure_source_occurrence_block_hash + and fee_source_occurrence.chain_id = run.chain_id + and fee_source_occurrence.release_id = run.release_id + and fee_source_occurrence.model_id = run.model_id + and fee_source_occurrence.epoch_id = run.epoch_id + and fee_source_occurrence.pointer_generation = run.captured_pointer_generation +left join programmable_private.current_token_project_metadata_v1 as metadata + on metadata.chain_id = launch.chain_id + and metadata.token = launch.token +left join programmable_private.profile_owner_binding_current as owner_binding + on owner_binding.wallet = launch.creator + and owner_binding.state in ('active', 'recovered') +left join programmable_private.profiles as profile + on profile.subject_id = owner_binding.subject_id + and profile.deleted_at is null +where launch.is_complete + and ( + launch.reward_vault is null + or programmable_private.has_current_verified_reward_seed( + run.run_id, + launch.reward_vault + ) + ); + +create view programmable_private.launch_by_token_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.creator, + launch.launch_transaction_hash, + launch.pool_id, + launch.reward_vault, + launch.launch_hash, + launch.token_name, + launch.token_symbol, + launch.total_supply, + run.source_group, + launch.epoch_id, + launch.pointer_generation, + launch.projection_run_id, + source_occurrence.block_timestamp as launch_block_timestamp, + source_occurrence.transaction_index::bigint as launch_transaction_index, + source_occurrence.receipt_log_ordinal::bigint as launch_receipt_log_ordinal, + pool.currency0, + pool.currency1, + pool.hook, + pool.pool_key_fee, + pool.tick_spacing, + case + when pool.currency0 = launch.token then pool.currency1 + when pool.currency1 = launch.token then pool.currency0 + else null + end as quote_asset, + fee.buy_swap_fee_bps, + fee.sell_swap_fee_bps, + fee.buy_creator_fee_bps, + fee.sell_creator_fee_bps, + fee.creator_fee_bps, + fee.launcher_fee_bps, + fee.transfer_tax_bps, + fee.lp_fee_pips, + greatest(fee.buy_swap_fee_bps, fee.sell_swap_fee_bps) + as total_swap_fee_bps, + metadata.project_name, + metadata.project_description, + metadata.project_logo_reference, + metadata.project_metadata_revision, + metadata.project_metadata_created_at, + coalesce(metadata.project_links, '[]'::jsonb) as project_links, + launch.promoted_block_number, + launch.promoted_block_hash, + launch.verified_at, + profile.username as creator_username, + profile.avatar_reference as creator_avatar_reference +from programmable_private.current_launch_projections_v1 as launch +join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = launch.chain_id + and run.release_id = launch.release_id + and run.model_id = launch.model_id + and run.epoch_id = launch.epoch_id + and run.captured_pointer_generation = launch.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = launch.projection_run_id + and publication.epoch_id = launch.epoch_id + and publication.pointer_generation = launch.pointer_generation + and publication.target_block_number = launch.promoted_block_number + and publication.target_block_hash = launch.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = launch.chain_id + and current_epoch.release_id = launch.release_id + and current_epoch.model_id = launch.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = launch.epoch_id + and current_epoch.generation = launch.pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'explore-token' + and route.chain_id = launch.chain_id + and route.release_id = launch.release_id + and route.model_id = launch.model_id + and route.source_group = run.source_group + and route.epoch_id = launch.epoch_id + and route.pointer_generation = launch.pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as canonical + on canonical.logical_event_id = launch.last_source_logical_event_id + and canonical.occurrence_id = launch.last_source_occurrence_id + and canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as source_occurrence + on source_occurrence.occurrence_id = launch.last_source_occurrence_id + and source_occurrence.logical_event_id = launch.last_source_logical_event_id + and source_occurrence.block_hash = launch.last_source_occurrence_block_hash + and source_occurrence.chain_id = run.chain_id + and source_occurrence.release_id = run.release_id + and source_occurrence.model_id = run.model_id + and source_occurrence.epoch_id = run.epoch_id + and source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + and pool.chain_id = launch.chain_id + and pool.release_id = launch.release_id + and pool.model_id = launch.model_id + and pool.epoch_id = launch.epoch_id + and pool.pointer_generation = launch.pointer_generation + and pool.pool_id = launch.pool_id + and pool.promoted_block_number = launch.promoted_block_number + and pool.promoted_block_hash = launch.promoted_block_hash + and (pool.currency0 = launch.token or pool.currency1 = launch.token) +join programmable_private.chain_event_current_canonical as pool_canonical + on pool_canonical.logical_event_id = pool.last_source_logical_event_id + and pool_canonical.occurrence_id = pool.last_source_occurrence_id + and pool_canonical.block_hash = pool.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as pool_source_occurrence + on pool_source_occurrence.occurrence_id = pool.last_source_occurrence_id + and pool_source_occurrence.logical_event_id = pool.last_source_logical_event_id + and pool_source_occurrence.block_hash = pool.last_source_occurrence_block_hash + and pool_source_occurrence.chain_id = run.chain_id + and pool_source_occurrence.release_id = run.release_id + and pool_source_occurrence.model_id = run.model_id + and pool_source_occurrence.epoch_id = run.epoch_id + and pool_source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = pool.projection_run_id + and fee.chain_id = pool.chain_id + and fee.release_id = pool.release_id + and fee.model_id = pool.model_id + and fee.epoch_id = pool.epoch_id + and fee.pointer_generation = pool.pointer_generation + and fee.promoted_block_number = pool.promoted_block_number + and fee.promoted_block_hash = pool.promoted_block_hash +join programmable_private.chain_event_current_canonical as fee_canonical + on fee_canonical.logical_event_id = fee.disclosure_source_logical_event_id + and fee_canonical.occurrence_id = fee.disclosure_source_occurrence_id + and fee_canonical.block_hash = fee.disclosure_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as fee_source_occurrence + on fee_source_occurrence.occurrence_id = fee.disclosure_source_occurrence_id + and fee_source_occurrence.logical_event_id = fee.disclosure_source_logical_event_id + and fee_source_occurrence.block_hash = fee.disclosure_source_occurrence_block_hash + and fee_source_occurrence.chain_id = run.chain_id + and fee_source_occurrence.release_id = run.release_id + and fee_source_occurrence.model_id = run.model_id + and fee_source_occurrence.epoch_id = run.epoch_id + and fee_source_occurrence.pointer_generation = run.captured_pointer_generation +left join programmable_private.current_token_project_metadata_v1 as metadata + on metadata.chain_id = launch.chain_id + and metadata.token = launch.token +left join programmable_private.profile_owner_binding_current as owner_binding + on owner_binding.wallet = launch.creator + and owner_binding.state in ('active', 'recovered') +left join programmable_private.profiles as profile + on profile.subject_id = owner_binding.subject_id + and profile.deleted_at is null +where launch.is_complete + and ( + launch.reward_vault is null + or programmable_private.has_current_verified_reward_seed( + run.run_id, + launch.reward_vault + ) + ); + +create view programmable_private.launches_by_creator_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.creator, + launch.launch_transaction_hash, + launch.pool_id, + launch.reward_vault, + launch.launch_hash, + launch.token_name, + launch.token_symbol, + launch.total_supply, + run.source_group, + launch.epoch_id, + launch.pointer_generation, + launch.projection_run_id, + source_occurrence.block_timestamp as launch_block_timestamp, + source_occurrence.transaction_index::bigint as launch_transaction_index, + source_occurrence.receipt_log_ordinal::bigint as launch_receipt_log_ordinal, + pool.currency0, + pool.currency1, + pool.hook, + pool.pool_key_fee, + pool.tick_spacing, + case + when pool.currency0 = launch.token then pool.currency1 + when pool.currency1 = launch.token then pool.currency0 + else null + end as quote_asset, + fee.buy_swap_fee_bps, + fee.sell_swap_fee_bps, + fee.buy_creator_fee_bps, + fee.sell_creator_fee_bps, + fee.creator_fee_bps, + fee.launcher_fee_bps, + fee.transfer_tax_bps, + fee.lp_fee_pips, + greatest(fee.buy_swap_fee_bps, fee.sell_swap_fee_bps) + as total_swap_fee_bps, + metadata.project_name, + metadata.project_description, + metadata.project_logo_reference, + metadata.project_metadata_revision, + metadata.project_metadata_created_at, + coalesce(metadata.project_links, '[]'::jsonb) as project_links, + launch.promoted_block_number, + launch.promoted_block_hash, + launch.verified_at +from programmable_private.current_launch_projections_v1 as launch +join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = launch.chain_id + and run.release_id = launch.release_id + and run.model_id = launch.model_id + and run.epoch_id = launch.epoch_id + and run.captured_pointer_generation = launch.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = launch.projection_run_id + and publication.epoch_id = launch.epoch_id + and publication.pointer_generation = launch.pointer_generation + and publication.target_block_number = launch.promoted_block_number + and publication.target_block_hash = launch.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = launch.chain_id + and current_epoch.release_id = launch.release_id + and current_epoch.model_id = launch.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = launch.epoch_id + and current_epoch.generation = launch.pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'creator-profile' + and route.chain_id = launch.chain_id + and route.release_id = launch.release_id + and route.model_id = launch.model_id + and route.source_group = run.source_group + and route.epoch_id = launch.epoch_id + and route.pointer_generation = launch.pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as canonical + on canonical.logical_event_id = launch.last_source_logical_event_id + and canonical.occurrence_id = launch.last_source_occurrence_id + and canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as source_occurrence + on source_occurrence.occurrence_id = launch.last_source_occurrence_id + and source_occurrence.logical_event_id = launch.last_source_logical_event_id + and source_occurrence.block_hash = launch.last_source_occurrence_block_hash + and source_occurrence.chain_id = run.chain_id + and source_occurrence.release_id = run.release_id + and source_occurrence.model_id = run.model_id + and source_occurrence.epoch_id = run.epoch_id + and source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + and pool.chain_id = launch.chain_id + and pool.release_id = launch.release_id + and pool.model_id = launch.model_id + and pool.epoch_id = launch.epoch_id + and pool.pointer_generation = launch.pointer_generation + and pool.pool_id = launch.pool_id + and pool.promoted_block_number = launch.promoted_block_number + and pool.promoted_block_hash = launch.promoted_block_hash + and (pool.currency0 = launch.token or pool.currency1 = launch.token) +join programmable_private.chain_event_current_canonical as pool_canonical + on pool_canonical.logical_event_id = pool.last_source_logical_event_id + and pool_canonical.occurrence_id = pool.last_source_occurrence_id + and pool_canonical.block_hash = pool.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as pool_source_occurrence + on pool_source_occurrence.occurrence_id = pool.last_source_occurrence_id + and pool_source_occurrence.logical_event_id = pool.last_source_logical_event_id + and pool_source_occurrence.block_hash = pool.last_source_occurrence_block_hash + and pool_source_occurrence.chain_id = run.chain_id + and pool_source_occurrence.release_id = run.release_id + and pool_source_occurrence.model_id = run.model_id + and pool_source_occurrence.epoch_id = run.epoch_id + and pool_source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = pool.projection_run_id + and fee.chain_id = pool.chain_id + and fee.release_id = pool.release_id + and fee.model_id = pool.model_id + and fee.epoch_id = pool.epoch_id + and fee.pointer_generation = pool.pointer_generation + and fee.promoted_block_number = pool.promoted_block_number + and fee.promoted_block_hash = pool.promoted_block_hash +join programmable_private.chain_event_current_canonical as fee_canonical + on fee_canonical.logical_event_id = fee.disclosure_source_logical_event_id + and fee_canonical.occurrence_id = fee.disclosure_source_occurrence_id + and fee_canonical.block_hash = fee.disclosure_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as fee_source_occurrence + on fee_source_occurrence.occurrence_id = fee.disclosure_source_occurrence_id + and fee_source_occurrence.logical_event_id = fee.disclosure_source_logical_event_id + and fee_source_occurrence.block_hash = fee.disclosure_source_occurrence_block_hash + and fee_source_occurrence.chain_id = run.chain_id + and fee_source_occurrence.release_id = run.release_id + and fee_source_occurrence.model_id = run.model_id + and fee_source_occurrence.epoch_id = run.epoch_id + and fee_source_occurrence.pointer_generation = run.captured_pointer_generation +left join programmable_private.current_token_project_metadata_v1 as metadata + on metadata.chain_id = launch.chain_id + and metadata.token = launch.token +where launch.is_complete + and ( + launch.reward_vault is null + or programmable_private.has_current_verified_reward_seed( + run.run_id, + launch.reward_vault + ) + ); + +create view programmable_private.market_snapshots_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.pool_id, + snapshot.market_snapshot_id, + snapshot.source_deployment_id, + provider.deployment_commitment as source_deployment_commitment, + provider.schema_commitment as source_schema_commitment, + snapshot.block_evidence_id, + snapshot.block_number, + snapshot.block_hash, + snapshot.sqrt_price_x96, + snapshot.liquidity, + snapshot.market_volume_token0, + snapshot.market_volume_token1, + snapshot.market_volume_usd, + snapshot.hook_gross_volume, + snapshot.observed_at, + reconciliation.reconciliation_id, + reconciliation.evidence_commitment as reconciliation_evidence_commitment, + outcome.finished_at as reconciled_at +from programmable_private.market_snapshots as snapshot +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = snapshot.reconciliation_id + and reconciliation.chain_id = snapshot.chain_id + and reconciliation.mismatch_count = 0 + and snapshot.block_number between + reconciliation.source_from_block and reconciliation.source_to_block +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + and run.chain_id = reconciliation.chain_id + and run.release_id = reconciliation.release_id + and run.model_id = reconciliation.model_id + and run.epoch_id = reconciliation.epoch_id + and run.captured_pointer_generation = reconciliation.pointer_generation +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id + and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.provider_deployments as provider + on provider.provider_deployment_id = snapshot.source_deployment_id + and provider.provider_type = 'uniswap_subgraph' +join programmable_private.dual_rpc_block_evidence as block_evidence + on block_evidence.block_evidence_id = snapshot.block_evidence_id + and block_evidence.chain_id = run.chain_id + and block_evidence.epoch_id = run.epoch_id + and block_evidence.pointer_generation = run.captured_pointer_generation + and block_evidence.block_number = snapshot.block_number + and block_evidence.agreed_block_hash = snapshot.block_hash +join programmable_private.run_lifecycle_outcomes as evidence_outcome + on evidence_outcome.run_id = block_evidence.verification_run_id + and evidence_outcome.status = 'succeeded' +join programmable_private.safe_head_observations as observation + on observation.observation_id = block_evidence.observation_id + and observation.chain_id = run.chain_id + and observation.release_id = run.release_id + and observation.model_id = run.model_id + and observation.source_group = run.source_group + and observation.epoch_id = run.epoch_id + and observation.pointer_generation = run.captured_pointer_generation +join programmable_private.launch_by_token_v1 as launch + on launch.chain_id = run.chain_id + and launch.release_id = run.release_id + and launch.model_id = run.model_id + and launch.source_group = run.source_group + and launch.epoch_id = run.epoch_id + and launch.pointer_generation = run.captured_pointer_generation + and launch.pool_id = snapshot.pool_id; + +create view programmable_private.market_candles_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.pool_id, + candle.market_candle_id, + candle.source_deployment_id, + provider.deployment_commitment as source_deployment_commitment, + provider.schema_commitment as source_schema_commitment, + candle.source_block_evidence_id, + candle.source_block_number, + candle.source_block_hash, + candle.interval, + candle.period_start, + candle.period_end, + candle.open, + candle.high, + candle.low, + candle.close, + candle.volume_token0, + candle.volume_token1, + candle.volume_usd, + reconciliation.reconciliation_id, + reconciliation.evidence_commitment as reconciliation_evidence_commitment, + outcome.finished_at as reconciled_at +from programmable_private.market_candles as candle +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = candle.reconciliation_id + and reconciliation.chain_id = candle.chain_id + and reconciliation.mismatch_count = 0 + and candle.source_block_number between + reconciliation.source_from_block and reconciliation.source_to_block +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + and run.chain_id = reconciliation.chain_id + and run.release_id = reconciliation.release_id + and run.model_id = reconciliation.model_id + and run.epoch_id = reconciliation.epoch_id + and run.captured_pointer_generation = reconciliation.pointer_generation +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id + and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.provider_deployments as provider + on provider.provider_deployment_id = candle.source_deployment_id + and provider.provider_type = 'uniswap_subgraph' +join programmable_private.dual_rpc_block_evidence as block_evidence + on block_evidence.block_evidence_id = candle.source_block_evidence_id + and block_evidence.chain_id = run.chain_id + and block_evidence.epoch_id = run.epoch_id + and block_evidence.pointer_generation = run.captured_pointer_generation + and block_evidence.block_number = candle.source_block_number + and block_evidence.agreed_block_hash = candle.source_block_hash +join programmable_private.run_lifecycle_outcomes as evidence_outcome + on evidence_outcome.run_id = block_evidence.verification_run_id + and evidence_outcome.status = 'succeeded' +join programmable_private.safe_head_observations as observation + on observation.observation_id = block_evidence.observation_id + and observation.chain_id = run.chain_id + and observation.release_id = run.release_id + and observation.model_id = run.model_id + and observation.source_group = run.source_group + and observation.epoch_id = run.epoch_id + and observation.pointer_generation = run.captured_pointer_generation +join programmable_private.launch_by_token_v1 as launch + on launch.chain_id = run.chain_id + and launch.release_id = run.release_id + and launch.model_id = run.model_id + and launch.source_group = run.source_group + and launch.epoch_id = run.epoch_id + and launch.pointer_generation = run.captured_pointer_generation + and launch.pool_id = candle.pool_id; + +create view programmable_private.account_reward_summaries_v1 +with (security_invoker = false, security_barrier = true) +as +select + balance.chain_id, + balance.release_id, + balance.model_id, + balance.account, + balance.vault, + launch.pool_id, + launch.hook, + launch.quote_asset, + balance.claimable_accrued + balance.claimed_total as entitled, + balance.claimable_accrued, + balance.claimed_total, + balance.promoted_block_number, + balance.promoted_block_hash, + balance.verified_at, + launch.token, + launch.token_name, + launch.token_symbol, + launch.creator +from programmable_private.current_account_reward_balances_v1 as balance +join programmable_private.launches_by_creator_v1 as launch + on launch.chain_id = balance.chain_id + and launch.release_id = balance.release_id + and launch.model_id = balance.model_id + and launch.epoch_id = balance.epoch_id + and launch.pointer_generation = balance.pointer_generation + and launch.reward_vault = balance.vault +join programmable_private.run_headers as run + on run.run_id = balance.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = balance.chain_id + and run.release_id = balance.release_id + and run.model_id = balance.model_id + and run.epoch_id = balance.epoch_id + and run.captured_pointer_generation = balance.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = balance.projection_run_id + and publication.epoch_id = run.epoch_id + and publication.pointer_generation = run.captured_pointer_generation + and publication.target_block_number = balance.promoted_block_number + and publication.target_block_hash = balance.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = balance.chain_id + and current_epoch.release_id = balance.release_id + and current_epoch.model_id = balance.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'creator-profile' + and route.chain_id = balance.chain_id + and route.release_id = balance.release_id + and route.model_id = balance.model_id + and route.source_group = run.source_group + and route.epoch_id = run.epoch_id + and route.pointer_generation = run.captured_pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as canonical + on canonical.logical_event_id = balance.last_source_logical_event_id + and canonical.occurrence_id = balance.last_source_occurrence_id + and canonical.block_hash = balance.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as source_occurrence + on source_occurrence.occurrence_id = balance.last_source_occurrence_id + and source_occurrence.logical_event_id = balance.last_source_logical_event_id + and source_occurrence.block_hash = balance.last_source_occurrence_block_hash + and source_occurrence.chain_id = run.chain_id + and source_occurrence.release_id = run.release_id + and source_occurrence.model_id = run.model_id + and source_occurrence.epoch_id = run.epoch_id + and source_occurrence.pointer_generation = run.captured_pointer_generation +where programmable_private.has_current_verified_reward_seed( + run.run_id, + balance.vault +); + +create view programmable_private.creator_reward_summaries_v1 +with (security_invoker = false, security_barrier = true) +as +select + account.chain_id, + account.release_id, + account.model_id, + account.account as creator, + pg_catalog.sum(account.claimable_accrued) as claimable_accrued, + pg_catalog.sum(account.claimed_total) as claimed_total, + pg_catalog.max(account.promoted_block_number) as promoted_block_number, + pg_catalog.max(account.verified_at) as verified_at +from programmable_private.account_reward_summaries_v1 as account +group by account.chain_id, account.release_id, account.model_id, account.account; + +create view programmable_private.classic_v3_vault_history_v1 +with (security_invoker = false, security_barrier = true) +as +select + vault.chain_id, + vault.release_id, + vault.model_id, + vault.vault, + vault.pool_id, + vault.configuration_hash, + allocation.configuration_epoch, + allocation.allocation_index, + allocation.beneficiary, + allocation.payout_address, + allocation.share_bps, + allocation.effective_from_block, + allocation.effective_to_block, + vault.promoted_block_number, + vault.promoted_block_hash, + vault.verified_at +from programmable_private.current_reward_vault_projections_v1 as vault +join programmable_private.launch_projections as launch + on launch.launch_projection_id = vault.launch_projection_id + and launch.projection_run_id = vault.projection_run_id + and launch.chain_id = vault.chain_id + and launch.release_id = vault.release_id + and launch.model_id = vault.model_id + and launch.epoch_id = vault.epoch_id + and launch.pointer_generation = vault.pointer_generation + and launch.reward_vault = vault.vault + and launch.pool_id = vault.pool_id + and launch.promoted_block_number = vault.promoted_block_number + and launch.promoted_block_hash = vault.promoted_block_hash + and launch.is_complete +join programmable_private.run_headers as run + on run.run_id = vault.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = vault.chain_id + and run.release_id = vault.release_id + and run.model_id = vault.model_id + and run.epoch_id = vault.epoch_id + and run.captured_pointer_generation = vault.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = vault.projection_run_id + and publication.epoch_id = run.epoch_id + and publication.pointer_generation = run.captured_pointer_generation + and publication.target_block_number = vault.promoted_block_number + and publication.target_block_hash = vault.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'classic-v3-profile' + and route.chain_id = run.chain_id + and route.release_id = run.release_id + and route.model_id = run.model_id + and route.source_group = run.source_group + and route.epoch_id = run.epoch_id + and route.pointer_generation = run.captured_pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as launch_canonical + on launch_canonical.logical_event_id = launch.last_source_logical_event_id + and launch_canonical.occurrence_id = launch.last_source_occurrence_id + and launch_canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as launch_source_occurrence + on launch_source_occurrence.occurrence_id = launch.last_source_occurrence_id + and launch_source_occurrence.logical_event_id = launch.last_source_logical_event_id + and launch_source_occurrence.block_hash = launch.last_source_occurrence_block_hash + and launch_source_occurrence.chain_id = run.chain_id + and launch_source_occurrence.release_id = run.release_id + and launch_source_occurrence.model_id = run.model_id + and launch_source_occurrence.epoch_id = run.epoch_id + and launch_source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_current_verified as verified_seed + on verified_seed.allocation_fact_id = vault.current_allocation_fact_id + and verified_seed.vault = vault.vault +join programmable_private.reward_allocation_facts as seed_fact + on seed_fact.allocation_fact_id = verified_seed.allocation_fact_id + and seed_fact.factory_occurrence_id = verified_seed.factory_occurrence_id + and seed_fact.vault = verified_seed.vault + and seed_fact.chain_id = run.chain_id + and seed_fact.release_id = run.release_id + and seed_fact.model_id = run.model_id + and seed_fact.epoch_id = run.epoch_id + and seed_fact.pointer_generation = run.captured_pointer_generation +join programmable_private.chain_event_current_canonical as seed_canonical + on seed_canonical.logical_event_id = seed_fact.factory_logical_event_id + and seed_canonical.occurrence_id = seed_fact.factory_occurrence_id + and seed_canonical.block_hash = seed_fact.factory_occurrence_block_hash +join programmable_private.chain_event_current_canonical as vault_canonical + on vault_canonical.logical_event_id = vault.last_source_logical_event_id + and vault_canonical.occurrence_id = vault.last_source_occurrence_id + and vault_canonical.block_hash = vault.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as vault_source_occurrence + on vault_source_occurrence.occurrence_id = vault.last_source_occurrence_id + and vault_source_occurrence.logical_event_id = vault.last_source_logical_event_id + and vault_source_occurrence.block_hash = vault.last_source_occurrence_block_hash + and vault_source_occurrence.chain_id = run.chain_id + and vault_source_occurrence.release_id = run.release_id + and vault_source_occurrence.model_id = run.model_id + and vault_source_occurrence.epoch_id = run.epoch_id + and vault_source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_projections as allocation + on allocation.reward_vault_projection_id = vault.reward_vault_projection_id + and allocation.projection_run_id = vault.projection_run_id + and allocation.allocation_fact_id = seed_fact.allocation_fact_id + and allocation.chain_id = run.chain_id + and allocation.release_id = run.release_id + and allocation.model_id = run.model_id + and allocation.epoch_id = run.epoch_id + and allocation.pointer_generation = run.captured_pointer_generation + and allocation.promoted_block_number = publication.target_block_number + and allocation.promoted_block_hash = publication.target_block_hash +join programmable_private.chain_event_current_canonical as allocation_canonical + on allocation_canonical.logical_event_id = allocation.last_source_logical_event_id + and allocation_canonical.occurrence_id = allocation.last_source_occurrence_id + and allocation_canonical.block_hash = allocation.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as allocation_source_occurrence + on allocation_source_occurrence.occurrence_id = allocation.last_source_occurrence_id + and allocation_source_occurrence.logical_event_id = allocation.last_source_logical_event_id + and allocation_source_occurrence.block_hash = allocation.last_source_occurrence_block_hash + and allocation_source_occurrence.chain_id = run.chain_id + and allocation_source_occurrence.release_id = run.release_id + and allocation_source_occurrence.model_id = run.model_id + and allocation_source_occurrence.epoch_id = run.epoch_id + and allocation_source_occurrence.pointer_generation + = run.captured_pointer_generation +where vault.model_id like 'classic%' + and not exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences as required + join programmable_private.chain_event_materialized_occurrences_v1 as required_occurrence + on required_occurrence.occurrence_id = required.occurrence_id + left join programmable_private.chain_event_current_canonical as required_canonical + on required_canonical.logical_event_id = required_occurrence.logical_event_id + and required_canonical.occurrence_id = required_occurrence.occurrence_id + and required_canonical.block_hash = required_occurrence.block_hash + where required.allocation_fact_id = seed_fact.allocation_fact_id + and ( + required_occurrence.chain_id <> run.chain_id + or required_occurrence.release_id <> run.release_id + or required_occurrence.model_id <> run.model_id + or required_occurrence.epoch_id <> run.epoch_id + or required_occurrence.pointer_generation <> run.captured_pointer_generation + or required_canonical.occurrence_id is null + ) + ); + +create view programmable_private.stock_paired_vault_history_v1 +with (security_invoker = false, security_barrier = true) +as +select + vault.chain_id, + vault.release_id, + vault.model_id, + vault.vault, + vault.pool_id, + vault.quote_asset, + vault.configuration_hash, + allocation.configuration_epoch, + allocation.allocation_index, + allocation.beneficiary, + allocation.payout_address, + allocation.share_bps, + allocation.effective_from_block, + allocation.effective_to_block, + vault.promoted_block_number, + vault.promoted_block_hash, + vault.verified_at +from programmable_private.current_reward_vault_projections_v1 as vault +join programmable_private.launch_projections as launch + on launch.launch_projection_id = vault.launch_projection_id + and launch.projection_run_id = vault.projection_run_id + and launch.chain_id = vault.chain_id + and launch.release_id = vault.release_id + and launch.model_id = vault.model_id + and launch.epoch_id = vault.epoch_id + and launch.pointer_generation = vault.pointer_generation + and launch.reward_vault = vault.vault + and launch.pool_id = vault.pool_id + and launch.promoted_block_number = vault.promoted_block_number + and launch.promoted_block_hash = vault.promoted_block_hash + and launch.is_complete +join programmable_private.run_headers as run + on run.run_id = vault.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = vault.chain_id + and run.release_id = vault.release_id + and run.model_id = vault.model_id + and run.epoch_id = vault.epoch_id + and run.captured_pointer_generation = vault.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = vault.projection_run_id + and publication.epoch_id = run.epoch_id + and publication.pointer_generation = run.captured_pointer_generation + and publication.target_block_number = vault.promoted_block_number + and publication.target_block_hash = vault.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'creator-profile' + and route.chain_id = run.chain_id + and route.release_id = run.release_id + and route.model_id = run.model_id + and route.source_group = run.source_group + and route.epoch_id = run.epoch_id + and route.pointer_generation = run.captured_pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as launch_canonical + on launch_canonical.logical_event_id = launch.last_source_logical_event_id + and launch_canonical.occurrence_id = launch.last_source_occurrence_id + and launch_canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as launch_source_occurrence + on launch_source_occurrence.occurrence_id = launch.last_source_occurrence_id + and launch_source_occurrence.logical_event_id = launch.last_source_logical_event_id + and launch_source_occurrence.block_hash = launch.last_source_occurrence_block_hash + and launch_source_occurrence.chain_id = run.chain_id + and launch_source_occurrence.release_id = run.release_id + and launch_source_occurrence.model_id = run.model_id + and launch_source_occurrence.epoch_id = run.epoch_id + and launch_source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_current_verified as verified_seed + on verified_seed.allocation_fact_id = vault.current_allocation_fact_id + and verified_seed.vault = vault.vault +join programmable_private.reward_allocation_facts as seed_fact + on seed_fact.allocation_fact_id = verified_seed.allocation_fact_id + and seed_fact.factory_occurrence_id = verified_seed.factory_occurrence_id + and seed_fact.vault = verified_seed.vault + and seed_fact.chain_id = run.chain_id + and seed_fact.release_id = run.release_id + and seed_fact.model_id = run.model_id + and seed_fact.epoch_id = run.epoch_id + and seed_fact.pointer_generation = run.captured_pointer_generation +join programmable_private.chain_event_current_canonical as seed_canonical + on seed_canonical.logical_event_id = seed_fact.factory_logical_event_id + and seed_canonical.occurrence_id = seed_fact.factory_occurrence_id + and seed_canonical.block_hash = seed_fact.factory_occurrence_block_hash +join programmable_private.chain_event_current_canonical as vault_canonical + on vault_canonical.logical_event_id = vault.last_source_logical_event_id + and vault_canonical.occurrence_id = vault.last_source_occurrence_id + and vault_canonical.block_hash = vault.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as vault_source_occurrence + on vault_source_occurrence.occurrence_id = vault.last_source_occurrence_id + and vault_source_occurrence.logical_event_id = vault.last_source_logical_event_id + and vault_source_occurrence.block_hash = vault.last_source_occurrence_block_hash + and vault_source_occurrence.chain_id = run.chain_id + and vault_source_occurrence.release_id = run.release_id + and vault_source_occurrence.model_id = run.model_id + and vault_source_occurrence.epoch_id = run.epoch_id + and vault_source_occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_projections as allocation + on allocation.reward_vault_projection_id = vault.reward_vault_projection_id + and allocation.projection_run_id = vault.projection_run_id + and allocation.allocation_fact_id = seed_fact.allocation_fact_id + and allocation.chain_id = run.chain_id + and allocation.release_id = run.release_id + and allocation.model_id = run.model_id + and allocation.epoch_id = run.epoch_id + and allocation.pointer_generation = run.captured_pointer_generation + and allocation.promoted_block_number = publication.target_block_number + and allocation.promoted_block_hash = publication.target_block_hash +join programmable_private.chain_event_current_canonical as allocation_canonical + on allocation_canonical.logical_event_id = allocation.last_source_logical_event_id + and allocation_canonical.occurrence_id = allocation.last_source_occurrence_id + and allocation_canonical.block_hash = allocation.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as allocation_source_occurrence + on allocation_source_occurrence.occurrence_id = allocation.last_source_occurrence_id + and allocation_source_occurrence.logical_event_id = allocation.last_source_logical_event_id + and allocation_source_occurrence.block_hash = allocation.last_source_occurrence_block_hash + and allocation_source_occurrence.chain_id = run.chain_id + and allocation_source_occurrence.release_id = run.release_id + and allocation_source_occurrence.model_id = run.model_id + and allocation_source_occurrence.epoch_id = run.epoch_id + and allocation_source_occurrence.pointer_generation + = run.captured_pointer_generation +where vault.model_id like 'stock%' + and not exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences as required + join programmable_private.chain_event_materialized_occurrences_v1 as required_occurrence + on required_occurrence.occurrence_id = required.occurrence_id + left join programmable_private.chain_event_current_canonical as required_canonical + on required_canonical.logical_event_id = required_occurrence.logical_event_id + and required_canonical.occurrence_id = required_occurrence.occurrence_id + and required_canonical.block_hash = required_occurrence.block_hash + where required.allocation_fact_id = seed_fact.allocation_fact_id + and ( + required_occurrence.chain_id <> run.chain_id + or required_occurrence.release_id <> run.release_id + or required_occurrence.model_id <> run.model_id + or required_occurrence.epoch_id <> run.epoch_id + or required_occurrence.pointer_generation <> run.captured_pointer_generation + or required_canonical.occurrence_id is null + ) + ); + +create view programmable_private.launch_lookup_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.creator, + launch.launch_transaction_hash, + launch.pool_id, + launch.reward_vault, + launch.promoted_block_number, + launch.promoted_block_hash +from programmable_private.current_launch_projections_v1 as launch +join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = launch.chain_id + and run.release_id = launch.release_id + and run.model_id = launch.model_id + and run.epoch_id = launch.epoch_id + and run.captured_pointer_generation = launch.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = launch.projection_run_id + and publication.epoch_id = launch.epoch_id + and publication.pointer_generation = launch.pointer_generation + and publication.target_block_number = launch.promoted_block_number + and publication.target_block_hash = launch.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = launch.chain_id + and current_epoch.release_id = launch.release_id + and current_epoch.model_id = launch.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = launch.epoch_id + and current_epoch.generation = launch.pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'launch-lookup' + and route.chain_id = launch.chain_id + and route.release_id = launch.release_id + and route.model_id = launch.model_id + and route.source_group = run.source_group + and route.epoch_id = launch.epoch_id + and route.pointer_generation = launch.pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as canonical + on canonical.logical_event_id = launch.last_source_logical_event_id + and canonical.occurrence_id = launch.last_source_occurrence_id + and canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as source_occurrence + on source_occurrence.occurrence_id = launch.last_source_occurrence_id + and source_occurrence.logical_event_id = launch.last_source_logical_event_id + and source_occurrence.block_hash = launch.last_source_occurrence_block_hash + and source_occurrence.chain_id = run.chain_id + and source_occurrence.release_id = run.release_id + and source_occurrence.model_id = run.model_id + and source_occurrence.epoch_id = run.epoch_id + and source_occurrence.pointer_generation = run.captured_pointer_generation +where launch.is_complete + and ( + launch.reward_vault is null + or programmable_private.has_current_verified_reward_seed( + run.run_id, + launch.reward_vault + ) + ); + +create view programmable_private.checkpoint_summary_v1 +with (security_invoker = false, security_barrier = true) +as +select + checkpoint.chain_id, + checkpoint.release_id, + checkpoint.model_id, + checkpoint.source_group, + checkpoint.projector_version, + checkpoint.epoch_id, + checkpoint.pointer_generation, + checkpoint.lease_generation, + checkpoint.checkpoint_generation, + checkpoint.reorg_generation, + checkpoint.block_number, + checkpoint.block_hash, + checkpoint.cursor_block_global_log_index, + checkpoint.cursor_candidate_id, + checkpoint.created_at +from programmable_private.projector_checkpoint_current as current_checkpoint +join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_checkpoint.checkpoint_id +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = checkpoint.chain_id + and current_epoch.release_id = checkpoint.release_id + and current_epoch.model_id = checkpoint.model_id + and current_epoch.source_group = checkpoint.source_group + and current_epoch.epoch_id = checkpoint.epoch_id + and current_epoch.generation = checkpoint.pointer_generation; + +create view programmable_private.parity_summary_v1 +with (security_invoker = false, security_barrier = true) +as +select + parity.route_key, + reconciliation.chain_id, + reconciliation.release_id, + reconciliation.model_id, + pg_catalog.count(*) as comparison_count, + pg_catalog.count(*) filter (where parity.is_match) as matching_count, + pg_catalog.count(*) filter (where not parity.is_match) as mismatch_count, + pg_catalog.max(parity.compared_at) as last_compared_at, + pg_catalog.max(parity.resolved_at) as last_resolved_at +from programmable_private.parity_records as parity +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = parity.reconciliation_id +group by + parity.route_key, + reconciliation.chain_id, + reconciliation.release_id, + reconciliation.model_id; + +create view programmable_private.health_summary_v1 +with (security_invoker = false, security_barrier = true) +as +select + health.dependency, + health.circuit_status, + health.observed_at, + history.failure_count, + history.retry_after +from programmable_private.dependency_health_current as health +join programmable_private.dependency_health_history as history + on history.health_event_id = health.health_event_id; + +create view programmable_private.reconciliation_occurrence_summary_v1 +with (security_invoker = false, security_barrier = true) +as +select + occurrence.chain_id, + occurrence.release_id, + occurrence.model_id, + occurrence.epoch_id, + occurrence.block_number, + occurrence.event_type, + status.status, + pg_catalog.count(*) as occurrence_count +from programmable_private.chain_event_materialized_occurrences_v1 as occurrence +join programmable_private.chain_event_occurrence_status_history as status + on status.occurrence_id = occurrence.occurrence_id +group by + occurrence.chain_id, + occurrence.release_id, + occurrence.model_id, + occurrence.epoch_id, + occurrence.block_number, + occurrence.event_type, + status.status; + +create view programmable_private.reconciliation_projection_summary_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.epoch_id, + launch.pointer_generation, + launch.projection_run_id, + launch.promoted_block_number, + launch.promoted_block_hash, + pg_catalog.count(*) as launch_count +from programmable_private.launch_projections as launch +group by + launch.chain_id, + launch.release_id, + launch.model_id, + launch.epoch_id, + launch.pointer_generation, + launch.projection_run_id, + launch.promoted_block_number, + launch.promoted_block_hash; + +create function programmable_private.get_recent_launches_v1( + p_chain_id bigint, + p_limit integer, + p_before_block bigint default null, + p_before_transaction_hash bytea default null, + p_after_token bytea default null +) +returns table ( + chain_id bigint, + release_id text, + model_id text, + token bytea, + creator bytea, + launch_transaction_hash bytea, + pool_id bytea, + reward_vault bytea, + launch_hash bytea, + token_name text, + token_symbol text, + total_supply numeric, + launch_block_timestamp timestamptz, + launch_transaction_index bigint, + launch_receipt_log_ordinal bigint, + currency0 bytea, + currency1 bytea, + hook bytea, + quote_asset bytea, + pool_key_fee bigint, + tick_spacing integer, + buy_swap_fee_bps integer, + sell_swap_fee_bps integer, + buy_creator_fee_bps integer, + sell_creator_fee_bps integer, + creator_fee_bps integer, + launcher_fee_bps integer, + transfer_tax_bps integer, + lp_fee_pips bigint, + total_swap_fee_bps integer, + project_name text, + project_description text, + project_logo_reference text, + project_metadata_revision bigint, + project_metadata_created_at timestamptz, + project_links jsonb, + promoted_block_number bigint, + promoted_block_hash bytea, + verified_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id <= 0 + or p_limit < 1 + or p_limit > 100 + or (p_before_block is not null and p_before_block < 0) + or ( + (p_before_block is null)::integer + + (p_before_transaction_hash is null)::integer + + (p_after_token is null)::integer + ) not in (0, 3) + or ( + p_before_transaction_hash is not null + and pg_catalog.octet_length(p_before_transaction_hash) <> 32 + ) + or ( + p_after_token is not null + and pg_catalog.octet_length(p_after_token) <> 20 + ) + then + raise exception using errcode = '22023', message = 'invalid recent-launch query'; + end if; + return query + select + launch.chain_id::bigint, + launch.release_id::text, + launch.model_id::text, + launch.token::bytea, + launch.creator::bytea, + launch.launch_transaction_hash::bytea, + launch.pool_id::bytea, + launch.reward_vault::bytea, + launch.launch_hash::bytea, + launch.token_name, + launch.token_symbol, + launch.total_supply::numeric, + launch.launch_block_timestamp, + launch.launch_transaction_index::bigint, + launch.launch_receipt_log_ordinal::bigint, + launch.currency0::bytea, + launch.currency1::bytea, + launch.hook::bytea, + launch.quote_asset::bytea, + launch.pool_key_fee::bigint, + launch.tick_spacing, + launch.buy_swap_fee_bps::integer, + launch.sell_swap_fee_bps::integer, + launch.buy_creator_fee_bps::integer, + launch.sell_creator_fee_bps::integer, + launch.creator_fee_bps::integer, + launch.launcher_fee_bps::integer, + launch.transfer_tax_bps::integer, + launch.lp_fee_pips, + launch.total_swap_fee_bps::integer, + launch.project_name, + launch.project_description, + launch.project_logo_reference, + launch.project_metadata_revision, + launch.project_metadata_created_at, + launch.project_links, + launch.promoted_block_number::bigint, + launch.promoted_block_hash::bytea, + launch.verified_at + from programmable_private.recent_launches_v1 as launch + where launch.chain_id = p_chain_id + and ( + p_before_block is null + or launch.promoted_block_number < p_before_block + or ( + launch.promoted_block_number = p_before_block + and launch.launch_transaction_hash < p_before_transaction_hash + ) + or ( + launch.promoted_block_number = p_before_block + and launch.launch_transaction_hash = p_before_transaction_hash + and launch.token > p_after_token + ) + ) + order by + launch.promoted_block_number desc, + launch.launch_transaction_hash desc, + launch.token + limit p_limit; +end +$function$; + +create function programmable_private.get_launch_by_token_v1( + p_chain_id bigint, + p_token bytea +) +returns table ( + chain_id bigint, + release_id text, + model_id text, + token bytea, + creator bytea, + launch_transaction_hash bytea, + pool_id bytea, + reward_vault bytea, + launch_hash bytea, + token_name text, + token_symbol text, + total_supply numeric, + launch_block_timestamp timestamptz, + launch_transaction_index bigint, + launch_receipt_log_ordinal bigint, + currency0 bytea, + currency1 bytea, + hook bytea, + quote_asset bytea, + pool_key_fee bigint, + tick_spacing integer, + buy_swap_fee_bps integer, + sell_swap_fee_bps integer, + buy_creator_fee_bps integer, + sell_creator_fee_bps integer, + creator_fee_bps integer, + launcher_fee_bps integer, + transfer_tax_bps integer, + lp_fee_pips bigint, + total_swap_fee_bps integer, + project_name text, + project_description text, + project_logo_reference text, + project_metadata_revision bigint, + project_metadata_created_at timestamptz, + project_links jsonb, + promoted_block_number bigint, + promoted_block_hash bytea, + verified_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id <= 0 or pg_catalog.octet_length(p_token) <> 20 then + raise exception using errcode = '22023', message = 'invalid token lookup'; + end if; + return query + select + launch.chain_id::bigint, + launch.release_id::text, + launch.model_id::text, + launch.token::bytea, + launch.creator::bytea, + launch.launch_transaction_hash::bytea, + launch.pool_id::bytea, + launch.reward_vault::bytea, + launch.launch_hash::bytea, + launch.token_name, + launch.token_symbol, + launch.total_supply::numeric, + launch.launch_block_timestamp, + launch.launch_transaction_index::bigint, + launch.launch_receipt_log_ordinal::bigint, + launch.currency0::bytea, + launch.currency1::bytea, + launch.hook::bytea, + launch.quote_asset::bytea, + launch.pool_key_fee::bigint, + launch.tick_spacing, + launch.buy_swap_fee_bps::integer, + launch.sell_swap_fee_bps::integer, + launch.buy_creator_fee_bps::integer, + launch.sell_creator_fee_bps::integer, + launch.creator_fee_bps::integer, + launch.launcher_fee_bps::integer, + launch.transfer_tax_bps::integer, + launch.lp_fee_pips, + launch.total_swap_fee_bps::integer, + launch.project_name, + launch.project_description, + launch.project_logo_reference, + launch.project_metadata_revision, + launch.project_metadata_created_at, + launch.project_links, + launch.promoted_block_number::bigint, + launch.promoted_block_hash::bytea, + launch.verified_at + from programmable_private.launch_by_token_v1 as launch + where launch.chain_id = p_chain_id and launch.token = p_token + order by launch.promoted_block_number desc + limit 1; +end +$function$; + +create function programmable_private.get_account_reward_summary_v1( + p_chain_id bigint, + p_account bytea +) +returns table ( + chain_id bigint, + account bytea, + release_id text, + model_id text, + vault bytea, + pool_id bytea, + hook bytea, + quote_asset bytea, + entitled numeric, + claimable_accrued numeric, + claimed_total numeric, + promoted_block_number bigint, + promoted_block_hash bytea, + verified_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id <= 0 or pg_catalog.octet_length(p_account) <> 20 then + raise exception using errcode = '22023', message = 'invalid account reward query'; + end if; + return query + select + reward.chain_id::bigint, + reward.account::bytea, + reward.release_id::text, + reward.model_id::text, + reward.vault::bytea, + reward.pool_id::bytea, + reward.hook::bytea, + reward.quote_asset::bytea, + reward.entitled::numeric, + reward.claimable_accrued::numeric, + reward.claimed_total::numeric, + reward.promoted_block_number::bigint, + reward.promoted_block_hash::bytea, + reward.verified_at + from programmable_private.account_reward_summaries_v1 as reward + where reward.chain_id = p_chain_id and reward.account = p_account + order by reward.release_id, reward.model_id, reward.vault; +end +$function$; + +-- Re-close the complete schema before applying exact final grants. This also +-- prevents an earlier migration's temporary grants from widening the surface. +revoke all on schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all tables in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all sequences in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +revoke all on all functions in schema programmable_private + from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +do $revoke_private_type_usage$ +declare + private_type record; +begin + for private_type in + select type_row.typname + from pg_catalog.pg_type as type_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = type_row.typnamespace + where namespace_row.nspname = 'programmable_private' + and type_row.typtype in ('d', 'e') + loop + execute pg_catalog.format( + 'revoke all on type programmable_private.%I from public, anon, authenticated, service_role, programmable_projector, programmable_reconciler, programmable_api_reader, programmable_profile_binder, programmable_profile_recovery, programmable_profile_writer, programmable_maintenance', + private_type.typname + ); + end loop; +end +$revoke_private_type_usage$; + +grant usage on schema programmable_private + to programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +grant execute on function programmable_private.create_release_epoch( + uuid, bigint, text, text, text, bigint, bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_release_source_binding( + uuid, uuid, text, text, text, bytea, bytea, numeric, + bytea, bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.activate_release_epoch( + bigint, text, text, text, uuid, bigint, bigint, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.register_provider_deployment( + uuid, text, text, bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.register_rpc_provider_deployment( + uuid, bigint, text, text, bytea, bytea, text, bytea, + bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.open_run( + uuid, text, bigint, text, text, text, uuid, bigint, text, bytea, timestamptz +) to programmable_projector, programmable_reconciler, + programmable_profile_recovery, programmable_maintenance; +grant execute on function programmable_private.append_run_outcome( + uuid, uuid, text, bytea, timestamptz +) to programmable_projector, programmable_reconciler, + programmable_profile_recovery, programmable_maintenance; +grant execute on function programmable_private.append_run_telemetry( + uuid, uuid, text, timestamptz, bigint, bigint, jsonb, boolean +) to programmable_projector, programmable_reconciler, + programmable_profile_recovery, programmable_maintenance; +grant execute on function programmable_private.append_safe_head_observation( + uuid, uuid, uuid, uuid, bigint, bigint, numeric, numeric, bigint, numeric, + bytea, bytea, smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_dual_rpc_block_evidence( + uuid, uuid, uuid, numeric, bytea, bytea, smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.acquire_projector_lease( + bigint, text, text, text, text, uuid, bigint, bigint, bigint, bytea, + text, timestamptz, timestamptz, bytea +) to programmable_projector; +grant execute on function programmable_private.append_envio_candidate( + text, uuid, numeric, bytea, bytea, numeric, numeric, bytea, bytea, text, + bytea[], bytea, jsonb, bytea, text, uuid, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_chain_event_occurrence( + uuid, uuid, uuid, text, numeric, timestamptz, text, bytea, uuid, + smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_reward_allocation_fact( + uuid, uuid, bytea, uuid, bytea[], numeric[], bytea, bytea, bytea, bytea, + uuid[], text[], smallint, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.append_reward_allocation_evidence( + uuid, uuid, uuid, text, text, + bytea, bytea, bytea, bytea, bytea, bytea, bytea, text, + bytea, bytea, bytea, bytea, bytea, bytea, bytea, bytea, bytea, bytea, + bytea, + smallint, bytea, bytea, timestamptz, bytea, bytea, bytea +) to programmable_projector; +grant execute on function programmable_private.append_reward_seed_status( + uuid, uuid, uuid, text, bytea, uuid, timestamptz +) to programmable_projector; +grant execute on function + programmable_private.quarantine_conflicting_reward_allocations( + uuid, uuid, uuid, uuid, uuid, uuid, uuid, bytea, timestamptz + ) to programmable_projector; +grant execute on function programmable_private.stage_launch_projection( + uuid, uuid, bytea, bytea, bytea, bytea, bytea, bytea, text, text, + numeric, uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_pool_projection( + uuid, uuid, uuid, bytea, bytea, numeric, integer, bytea, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_account_reward_balance( + uuid, uuid, bytea, bytea, numeric, numeric, uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_pool_fee_configuration( + uuid, uuid, uuid, numeric, numeric, numeric, numeric, numeric, numeric, + uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_fee_accrual_fact( + uuid, uuid, bytea, bytea, numeric, numeric, numeric, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_pool_fee_total( + uuid, uuid, bytea, bytea, numeric, numeric, numeric, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_reward_vault_projection( + uuid, uuid, uuid, bytea, bytea, bytea, bytea, uuid, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_reward_allocation_projection( + uuid, uuid, uuid, uuid, bigint, integer, bytea, bytea, numeric, + numeric, numeric, uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_claim_projection( + uuid, uuid, bytea, text, bytea, bytea, numeric, numeric, numeric, + uuid, numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_payout_change_projection( + uuid, uuid, bytea, bytea, bytea, bytea, bigint, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_initial_buy_custody_projection( + uuid, uuid, uuid, bytea, smallint, integer, integer, bytea, uuid, + numeric, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_initial_buy_vesting_projection( + uuid, uuid, uuid, bytea, bytea, numeric, timestamptz, timestamptz, + uuid, numeric, bytea, timestamptz +) to programmable_projector; + +grant execute on function programmable_private.promote_projection_run( + uuid, uuid, uuid, uuid, text, bigint, bytea, bigint, bigint, bigint, + uuid, uuid, numeric, bytea, numeric, text, uuid[], uuid[], uuid[], uuid[], + text[], bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.rewind_projection_run( + uuid, uuid, uuid, text, bigint, bytea, bigint, bigint, bigint, + uuid, uuid, numeric, bytea, numeric, text, bytea, timestamptz +) to programmable_projector; + +grant execute on function programmable_private.append_dependency_health( + uuid, uuid, text, text, integer, timestamptz, timestamptz, bytea +) to programmable_reconciler; +grant execute on function programmable_private.append_reconciliation_record( + uuid, uuid, text, text, numeric, numeric, bigint, bigint, bytea, + bytea[], timestamptz, timestamptz +) to programmable_reconciler; +grant execute on function programmable_private.append_parity_record( + uuid, uuid, text, bytea, bytea, timestamptz, timestamptz +) to programmable_reconciler; +grant execute on function programmable_private.append_market_snapshot( + uuid, uuid, uuid, uuid, bytea, numeric, bytea, numeric, numeric, numeric, + numeric, numeric, numeric, timestamptz, bytea +) to programmable_reconciler; +grant execute on function programmable_private.append_market_candle( + uuid, uuid, uuid, uuid, bytea, text, timestamptz, timestamptz, numeric, numeric, + numeric, numeric, numeric, numeric, numeric, bytea, bytea +) to programmable_reconciler; +grant execute on function programmable_private.append_portfolio_point( + uuid, uuid, bytea, integer, timestamptz, numeric, bytea +) to programmable_reconciler; +grant select on programmable_private.reconciliation_occurrence_summary_v1, + programmable_private.reconciliation_projection_summary_v1, + programmable_private.checkpoint_summary_v1 + to programmable_reconciler; + +grant execute on function programmable_private.define_profile_hash_version( + smallint, text, bytea, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.set_profile_hash_version_state( + uuid, smallint, text, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.bind_profile_subject( + bytea, smallint, bytea, text, bytea, timestamptz +) to programmable_profile_binder; +grant execute on function programmable_private.rekey_profile_subject( + bytea, smallint, bytea, smallint, bytea, bigint, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.tombstone_profile_binding( + bytea, smallint, bytea, bigint, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.recover_profile_binding( + bytea, smallint, bytea, bigint, bytea, timestamptz +) to programmable_profile_recovery; +grant execute on function programmable_private.mutate_profile( + bytea, smallint, bytea, bigint, bigint, text, text, text, text, + bytea, timestamptz +) to programmable_profile_writer; +grant execute on function programmable_private.append_token_project_metadata_revision( + uuid, bytea, smallint, bytea, bigint, bigint, bytea, bigint, + text, text, text, bytea, timestamptz +) to programmable_profile_writer; +grant execute on function programmable_private.append_project_metadata_link( + uuid, uuid, bytea, smallint, bytea, bigint, bigint, + text, text, integer, bytea, timestamptz +) to programmable_profile_writer; + +grant execute on function programmable_private.prune_run_telemetry( + timestamptz, integer, bytea +) to programmable_maintenance; +grant execute on function programmable_private.prune_market_data( + timestamptz, integer, bytea +) to programmable_maintenance; +grant execute on function programmable_private.prune_parity_records( + timestamptz, integer, bytea +) to programmable_maintenance; + +grant select on programmable_private.recent_launches_v1, + programmable_private.launch_by_token_v1, + programmable_private.launches_by_creator_v1, + programmable_private.market_snapshots_v1, + programmable_private.market_candles_v1, + programmable_private.account_reward_summaries_v1, + programmable_private.creator_reward_summaries_v1, + programmable_private.classic_v3_vault_history_v1, + programmable_private.stock_paired_vault_history_v1, + programmable_private.launch_lookup_v1, + programmable_private.checkpoint_summary_v1, + programmable_private.parity_summary_v1, + programmable_private.health_summary_v1 + to programmable_api_reader; +grant execute on function programmable_private.get_recent_launches_v1( + bigint, integer, bigint, bytea, bytea +) to programmable_api_reader; +grant execute on function programmable_private.get_launch_by_token_v1( + bigint, bytea +) to programmable_api_reader; +grant execute on function programmable_private.get_account_reward_summary_v1( + bigint, bytea +) to programmable_api_reader; + +alter default privileges for role programmable_migrator in schema programmable_private + revoke all on tables from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; +alter default privileges for role programmable_migrator in schema programmable_private + revoke all on sequences from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; +alter default privileges for role programmable_migrator in schema programmable_private + revoke execute on functions from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; +alter default privileges for role programmable_migrator in schema programmable_private + revoke usage on types from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +reset role; diff --git a/supabase/migrations/20260731000700_p0_indexer_projection_hardening.sql b/supabase/migrations/20260731000700_p0_indexer_projection_hardening.sql new file mode 100644 index 00000000..d9d7315c --- /dev/null +++ b/supabase/migrations/20260731000700_p0_indexer_projection_hardening.sql @@ -0,0 +1,4716 @@ +-- P0 hardening for stateless projector resume, shared/dynamic source +-- provenance, delta-safe publications and release-specific event manifests. + +set role programmable_migrator; + +create table programmable_private.release_dynamic_source_templates ( + dynamic_source_template_id uuid primary key, + epoch_id uuid not null + references programmable_private.release_epochs(epoch_id) + on delete restrict, + parent_factory_release_binding_id uuid not null + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + parent_factory_binding_commitment programmable_private.bytes32_value not null, + parent_source_role programmable_private.source_identifier not null, + factory_event_type programmable_private.source_identifier not null, + deployed_address_field programmable_private.source_identifier not null, + deployed_source_role programmable_private.source_identifier not null, + deployed_artifact_creation_code_commitment + programmable_private.bytes32_value not null, + normalized_runtime_code_hash programmable_private.bytes32_value not null, + immutable_references_commitment programmable_private.bytes32_value not null, + immutable_binding_spec jsonb not null, + immutable_binding_commitment programmable_private.bytes32_value not null, + runtime_code_length bigint not null + check (runtime_code_length > 0 and runtime_code_length <= 16777216), + abi_event_set_commitment programmable_private.bytes32_value not null, + template_commitment programmable_private.bytes32_value not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check (deployed_source_role in ('reward_vault', 'vesting_wallet')), + check ( + programmable_private.valid_immutable_binding_spec(immutable_binding_spec) + ), + check (deployed_address_field in ('vault', 'wallet')), + check ( + (deployed_source_role = 'reward_vault' and deployed_address_field = 'vault') + or ( + deployed_source_role = 'vesting_wallet' + and deployed_address_field = 'wallet' + ) + ), + unique (epoch_id, parent_source_role, factory_event_type, deployed_source_role), + unique (epoch_id, template_commitment) +); + +create table programmable_private.dual_rpc_runtime_code_evidence ( + runtime_code_evidence_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + source_address programmable_private.eth_address not null, + deployment_block_evidence_id uuid not null + references programmable_private.dual_rpc_block_evidence(block_evidence_id) + on delete restrict, + deployment_block_number programmable_private.block_number_value not null, + deployment_block_hash programmable_private.bytes32_value not null, + provider_a_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + runtime_code_hash_a programmable_private.bytes32_value not null, + runtime_code_hash_b programmable_private.bytes32_value not null, + agreed_runtime_code_hash programmable_private.bytes32_value not null, + runtime_code_length_a bigint not null + check (runtime_code_length_a > 0 and runtime_code_length_a <= 16777216), + runtime_code_length_b bigint not null + check (runtime_code_length_b > 0 and runtime_code_length_b <= 16777216), + agreed_runtime_code_length bigint not null + check (agreed_runtime_code_length > 0 and agreed_runtime_code_length <= 16777216), + normalized_runtime_code_hash_a programmable_private.bytes32_value not null, + normalized_runtime_code_hash_b programmable_private.bytes32_value not null, + agreed_normalized_runtime_code_hash programmable_private.bytes32_value not null, + immutable_references_commitment programmable_private.bytes32_value not null, + immutable_values bytea[] not null check (cardinality(immutable_values) > 0), + immutable_values_commitment programmable_private.bytes32_value not null, + reconstructed_runtime_code_hash programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version = 2), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + evidence_commitment programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + foreign key (deployment_block_evidence_id, deployment_block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) on delete restrict, + check (provider_a_id <> provider_b_id), + check ( + runtime_code_hash_a = runtime_code_hash_b + and runtime_code_hash_a = agreed_runtime_code_hash + ), + check ( + runtime_code_length_a = runtime_code_length_b + and runtime_code_length_a = agreed_runtime_code_length + ), + check ( + normalized_runtime_code_hash_a = normalized_runtime_code_hash_b + and normalized_runtime_code_hash_a = agreed_normalized_runtime_code_hash + ), + check ( + reconstructed_runtime_code_hash = agreed_runtime_code_hash + and programmable_private.valid_immutable_values(immutable_values) + ), + unique (epoch_id, pointer_generation, source_address, deployment_block_number), + unique (epoch_id, evidence_commitment) +); + +create table programmable_private.dynamic_source_attestations ( + dynamic_source_attestation_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + runtime_code_evidence_id uuid not null + references programmable_private.dual_rpc_runtime_code_evidence( + runtime_code_evidence_id + ) on delete restrict, + dynamic_source_template_id uuid not null + references programmable_private.release_dynamic_source_templates( + dynamic_source_template_id + ) on delete restrict, + parent_factory_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + parent_factory_release_binding_id uuid not null + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + parent_factory_binding_commitment programmable_private.bytes32_value not null, + deployed_source_address programmable_private.eth_address not null, + deployed_source_role programmable_private.source_identifier not null, + deployment_block_number programmable_private.block_number_value not null, + deployed_artifact_creation_code_commitment + programmable_private.bytes32_value not null, + expected_immutable_values_commitment + programmable_private.bytes32_value not null, + factory_configuration_commitment + programmable_private.bytes32_value not null, + constructor_arguments_commitment programmable_private.bytes32_value not null, + local_init_code_hash programmable_private.bytes32_value not null, + runtime_code_hash programmable_private.bytes32_value not null, + abi_event_set_commitment programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version = 2), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + attestation_commitment programmable_private.bytes32_value not null, + verification_run_id uuid not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check (deployed_source_role in ('reward_vault', 'vesting_wallet')), + unique ( + epoch_id, pointer_generation, deployed_source_address, + deployed_source_role + ), + unique (epoch_id, attestation_commitment) +); + +create table programmable_private.envio_candidate_inbox ( + candidate_id programmable_private.envio_candidate_identifier primary key, + chain_id programmable_private.chain_id_value not null, + stream_id programmable_private.source_identifier not null, + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + transaction_hash programmable_private.bytes32_value not null, + transaction_index programmable_private.transaction_index_value not null, + block_global_log_index programmable_private.block_log_index_value not null, + source_address programmable_private.eth_address not null, + contract_name programmable_private.source_identifier not null, + event_signature programmable_private.bytes32_value not null, + event_type programmable_private.source_identifier not null, + ordered_topics bytea[] not null, + raw_data bytea not null, + decoded_payload jsonb not null, + payload_hash programmable_private.bytes32_value not null, + provider_cursor programmable_private.envio_candidate_identifier not null, + provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + first_seen_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + first_seen_at timestamptz not null, + content_commitment programmable_private.bytes32_value not null, + check (programmable_private.valid_topics(ordered_topics)), + check (pg_catalog.octet_length(decoded_payload::text) <= 65536), + check ( + candidate_id = programmable_private.derive_envio_candidate_id( + chain_id, block_hash, transaction_hash, block_global_log_index + ) + ), + check (provider_cursor = candidate_id), + unique ( + candidate_id, chain_id, provider_deployment_id, stream_id, block_number, + block_hash, block_global_log_index + ), + unique (chain_id, block_hash, transaction_hash, block_global_log_index) +); + +create table programmable_private.envio_ingestion_cursor_history ( + cursor_history_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + stream_id programmable_private.source_identifier not null, + generation bigint not null check (generation > 0), + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + block_global_log_index + programmable_private.block_log_index_value not null, + candidate_id programmable_private.envio_candidate_identifier not null + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict, + content_commitment programmable_private.bytes32_value not null, + changed_by_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + changed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + is_rewind boolean not null, + rewound_from_generation bigint, + check ( + (not is_rewind and rewound_from_generation is null) + or (is_rewind and rewound_from_generation is not null + and rewound_from_generation = generation - 1) + ), + unique (chain_id, provider_deployment_id, stream_id, generation) +); + +create table programmable_private.envio_ingestion_cursor_current ( + chain_id programmable_private.chain_id_value not null, + provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + stream_id programmable_private.source_identifier not null, + generation bigint not null check (generation > 0), + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + block_global_log_index + programmable_private.block_log_index_value not null, + candidate_id programmable_private.envio_candidate_identifier not null + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict, + content_commitment programmable_private.bytes32_value not null, + changed_by_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + changed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + cursor_history_id uuid not null unique + references programmable_private.envio_ingestion_cursor_history( + cursor_history_id + ) on delete restrict, + primary key (chain_id, provider_deployment_id, stream_id) +); + +alter table programmable_private.projector_checkpoints + add constraint projector_checkpoints_cursor_candidate_fkey + foreign key (cursor_candidate_id) + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict; + +create table programmable_private.envio_candidate_resolutions ( + candidate_resolution_id uuid primary key, + candidate_id programmable_private.envio_candidate_identifier not null + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + release_binding_id uuid + references programmable_private.release_source_bindings(binding_id) + on delete restrict, + dynamic_source_attestation_id uuid + references programmable_private.dynamic_source_attestations( + dynamic_source_attestation_id + ) on delete restrict, + abi_event_set_commitment programmable_private.bytes32_value not null, + resolution_commitment programmable_private.bytes32_value not null, + resolved_by_run_id uuid not null, + resolved_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (resolved_by_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check ((release_binding_id is null) <> (dynamic_source_attestation_id is null)), + unique (candidate_id, epoch_id, pointer_generation), + unique (epoch_id, resolution_commitment) +); + +create table programmable_private.envio_candidate_status_history ( + decision_id uuid primary key, + candidate_id programmable_private.envio_candidate_identifier not null + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + status programmable_private.envio_candidate_status not null, + attempt_count bigint not null check (attempt_count >= 0), + next_attempt_at timestamptz, + reason_code programmable_private.source_identifier, + reason_commitment programmable_private.bytes32_value not null, + changed_by_run_id uuid not null, + changed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (changed_by_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check ( + (status = 'pending' and next_attempt_at is null and reason_code is null) + or (status = 'deferred' and next_attempt_at is not null + and reason_code is not null) + or (status in ('resolved', 'ignored', 'quarantined') + and next_attempt_at is null + and reason_code is not null) + ), + unique ( + candidate_id, epoch_id, pointer_generation, status, attempt_count + ) +); + +create table programmable_private.envio_candidate_status_current ( + candidate_id programmable_private.envio_candidate_identifier not null + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + status programmable_private.envio_candidate_status not null, + attempt_count bigint not null check (attempt_count >= 0), + next_attempt_at timestamptz, + reason_code programmable_private.source_identifier, + reason_commitment programmable_private.bytes32_value not null, + changed_by_run_id uuid not null, + changed_at timestamptz not null, + decision_id uuid not null unique + references programmable_private.envio_candidate_status_history(decision_id) + on delete restrict, + primary key (candidate_id, epoch_id, pointer_generation), + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (changed_by_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check ( + (status = 'pending' and next_attempt_at is null and reason_code is null) + or (status = 'deferred' and next_attempt_at is not null + and reason_code is not null) + or (status in ('resolved', 'ignored', 'quarantined') + and next_attempt_at is null + and reason_code is not null) + ) +); + +alter table programmable_private.chain_event_occurrence_materializations + add constraint materialization_dynamic_source_attestation_fkey + foreign key (dynamic_source_attestation_id) + references programmable_private.dynamic_source_attestations( + dynamic_source_attestation_id + ) on delete restrict, + add constraint materialization_legacy_candidate_fkey + foreign key (first_seen_envio_candidate_id) + references programmable_private.envio_candidates(candidate_id) + on delete restrict, + add constraint materialization_neutral_candidate_fkey + foreign key (first_seen_neutral_candidate_id) + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict, + add constraint materialization_candidate_resolution_fkey + foreign key (candidate_resolution_id) + references programmable_private.envio_candidate_resolutions( + candidate_resolution_id + ) on delete restrict; + +alter table programmable_private.chain_event_occurrences + drop constraint chain_event_occurrences_first_seen_envio_candidate_id_fkey, + alter column release_binding_id drop not null, + alter column first_seen_envio_candidate_id drop not null, + alter column first_seen_envio_candidate_id type + programmable_private.envio_candidate_identifier + using first_seen_envio_candidate_id::text, + add column dynamic_source_attestation_id uuid + references programmable_private.dynamic_source_attestations( + dynamic_source_attestation_id + ) on delete restrict, + add column first_seen_neutral_candidate_id + programmable_private.envio_candidate_identifier + references programmable_private.envio_candidate_inbox(candidate_id) + on delete restrict, + add column candidate_resolution_id uuid + references programmable_private.envio_candidate_resolutions( + candidate_resolution_id + ) on delete restrict, + add constraint occurrence_exact_source_provenance check ( + (release_binding_id is null) <> (dynamic_source_attestation_id is null) + ), + add constraint occurrence_exact_candidate_provenance check ( + ( + first_seen_envio_candidate_id is not null + and first_seen_neutral_candidate_id is null + and candidate_resolution_id is null + ) + or ( + first_seen_envio_candidate_id is null + and first_seen_neutral_candidate_id is not null + and candidate_resolution_id is not null + ) + ); + +alter table programmable_private.chain_event_occurrences + add constraint chain_event_occurrences_first_seen_envio_candidate_id_fkey + foreign key (first_seen_envio_candidate_id) + references programmable_private.envio_candidates(candidate_id) + on delete restrict; + +create table programmable_private.release_projection_event_rules ( + projection_event_rule_id uuid primary key, + epoch_id uuid not null + references programmable_private.release_epochs(epoch_id) + on delete restrict, + projection_kind programmable_private.source_identifier not null, + source_role programmable_private.source_identifier not null, + event_type programmable_private.source_identifier not null, + rule_commitment programmable_private.bytes32_value not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (epoch_id, projection_kind, source_role, event_type), + unique (epoch_id, rule_commitment) +); + +create table programmable_private.release_launch_completeness_requirements ( + launch_requirement_id uuid primary key, + epoch_id uuid not null + references programmable_private.release_epochs(epoch_id) + on delete restrict, + requirement_ordinal integer not null check (requirement_ordinal >= 0), + occurrence_role programmable_private.source_identifier not null, + event_type programmable_private.source_identifier not null, + required_when programmable_private.source_identifier not null + check (required_when in ('always', 'reward_vault', 'locked_custody', 'eth_funded')), + requirement_commitment programmable_private.bytes32_value not null, + created_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (epoch_id, requirement_ordinal), + unique (epoch_id, occurrence_role, event_type, required_when), + unique (epoch_id, requirement_commitment) +); + +create table programmable_private.launch_projection_occurrence_roles ( + launch_projection_id uuid not null + references programmable_private.launch_projections(launch_projection_id) + on delete restrict, + occurrence_role programmable_private.source_identifier not null, + occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + staged_at timestamptz not null, + primary key (launch_projection_id, occurrence_role), + unique (launch_projection_id, occurrence_id) +); + +create table programmable_private.launch_projection_conditions ( + launch_projection_id uuid primary key + references programmable_private.launch_projections(launch_projection_id) + on delete restrict, + projection_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + eth_funded boolean not null, + staged_at timestamptz not null +); + +create table programmable_private.creator_hook_claim_facts ( + creator_hook_claim_fact_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + pool_id programmable_private.bytes32_value not null, + reward_vault programmable_private.eth_address, + creator programmable_private.eth_address, + recipient programmable_private.eth_address, + quote_asset programmable_private.eth_address, + caller programmable_private.eth_address not null, + amount programmable_private.uint256_value not null, + source_occurrence_id uuid not null, + source_logical_event_id uuid not null, + source_occurrence_block_hash programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check ( + (reward_vault is not null and creator is null and recipient is null) + or (reward_vault is null and creator is not null and recipient is not null + and quote_asset is null) + ), + unique (source_occurrence_id) +); + +create table programmable_private.launcher_hook_claim_facts ( + launcher_hook_claim_fact_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + treasury programmable_private.eth_address not null, + recipient programmable_private.eth_address not null, + quote_asset programmable_private.eth_address, + caller programmable_private.eth_address not null, + amount programmable_private.uint256_value not null, + source_occurrence_id uuid not null, + source_logical_event_id uuid not null, + source_occurrence_block_hash programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + unique (source_occurrence_id) +); + +create table programmable_private.creator_fee_checkpoint_facts ( + creator_fee_checkpoint_fact_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + vault programmable_private.eth_address not null, + pool_id programmable_private.bytes32_value not null, + configuration_epoch bigint not null check (configuration_epoch >= 0), + amount programmable_private.uint256_value not null, + total_creator_fees_received programmable_private.uint256_value not null, + source_occurrence_id uuid not null, + source_logical_event_id uuid not null, + source_occurrence_block_hash programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check (amount <= total_creator_fees_received), + unique (source_occurrence_id) +); + +create table programmable_private.reward_configuration_activation_facts ( + reward_configuration_activation_fact_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + vault programmable_private.eth_address not null, + pool_id programmable_private.bytes32_value not null, + approval_reference programmable_private.bytes32_value not null, + configuration_epoch bigint not null check (configuration_epoch >= 0), + previous_configuration_hash programmable_private.bytes32_value not null, + new_configuration_hash programmable_private.bytes32_value not null, + ordered_beneficiaries bytea[] not null, + ordered_shares_bps integer[] not null, + effective_total_creator_fees_received programmable_private.uint256_value not null, + source_occurrence_id uuid not null, + source_logical_event_id uuid not null, + source_occurrence_block_hash programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key ( + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash + ) references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check ( + programmable_private.valid_beneficiary_set( + ordered_beneficiaries, ordered_shares_bps, 5 + ) + ), + unique (source_occurrence_id), + unique (vault, configuration_epoch, new_configuration_hash) +); + +do $rls$ +declare + table_name text; +begin + foreach table_name in array array[ + 'release_dynamic_source_templates', + 'dual_rpc_runtime_code_evidence', + 'dynamic_source_attestations', + 'envio_candidate_inbox', + 'envio_ingestion_cursor_history', + 'envio_ingestion_cursor_current', + 'envio_candidate_resolutions', + 'envio_candidate_status_history', + 'envio_candidate_status_current', + 'release_projection_event_rules', + 'release_launch_completeness_requirements', + 'launch_projection_occurrence_roles', + 'launch_projection_conditions', + 'creator_hook_claim_facts', + 'launcher_hook_claim_facts', + 'creator_fee_checkpoint_facts', + 'reward_configuration_activation_facts' + ] loop + execute pg_catalog.format( + 'alter table programmable_private.%I enable row level security', + table_name + ); + execute pg_catalog.format( + 'alter table programmable_private.%I force row level security', + table_name + ); + execute pg_catalog.format( + 'create policy %I on programmable_private.%I for all to programmable_migrator using (true) with check (true)', + table_name || '_migrator_all', table_name + ); + end loop; +end +$rls$; + +create function programmable_private.append_release_projection_event_rule( + p_projection_event_rule_id uuid, + p_epoch_id uuid, + p_projection_kind text, + p_source_role text, + p_event_type text, + p_rule_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + existing programmable_private.release_projection_event_rules%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_projection_event_rule_id is null + or pg_catalog.octet_length(p_rule_commitment) <> 32 + or exists ( + select 1 from programmable_private.release_epoch_current + where epoch_id = p_epoch_id + ) + then + raise exception using errcode = '55000', message = 'invalid or active projection event rule'; + end if; + select * into existing + from programmable_private.release_projection_event_rules + where projection_event_rule_id = p_projection_event_rule_id; + if found then + if existing.epoch_id <> p_epoch_id + or existing.projection_kind <> p_projection_kind + or existing.source_role <> p_source_role + or existing.event_type <> p_event_type + or existing.rule_commitment <> p_rule_commitment + then + raise exception using errcode = '23505', message = 'projection event rule replay conflict'; + end if; + return existing.projection_event_rule_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'projection_event_rule.append', p_rule_commitment, null, p_created_at + ); + insert into programmable_private.release_projection_event_rules ( + projection_event_rule_id, epoch_id, projection_kind, source_role, + event_type, rule_commitment, created_at, created_by_audit_id + ) values ( + p_projection_event_rule_id, p_epoch_id, + p_projection_kind::programmable_private.source_identifier, + p_source_role::programmable_private.source_identifier, + p_event_type::programmable_private.source_identifier, + p_rule_commitment::programmable_private.bytes32_value, + p_created_at, created_audit_id + ); + return p_projection_event_rule_id; +end +$function$; + +create function programmable_private.append_release_launch_requirement( + p_launch_requirement_id uuid, + p_epoch_id uuid, + p_requirement_ordinal integer, + p_occurrence_role text, + p_event_type text, + p_required_when text, + p_requirement_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + existing programmable_private.release_launch_completeness_requirements%rowtype; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_launch_requirement_id is null + or p_requirement_ordinal < 0 + or p_required_when not in ('always', 'reward_vault', 'locked_custody', 'eth_funded') + or pg_catalog.octet_length(p_requirement_commitment) <> 32 + or exists ( + select 1 from programmable_private.release_epoch_current + where epoch_id = p_epoch_id + ) + then + raise exception using errcode = '55000', message = 'invalid or active launch requirement'; + end if; + select * into existing + from programmable_private.release_launch_completeness_requirements + where launch_requirement_id = p_launch_requirement_id; + if found then + if existing.epoch_id <> p_epoch_id + or existing.requirement_ordinal <> p_requirement_ordinal + or existing.occurrence_role <> p_occurrence_role + or existing.event_type <> p_event_type + or existing.required_when <> p_required_when + or existing.requirement_commitment <> p_requirement_commitment + then + raise exception using errcode = '23505', message = 'launch requirement replay conflict'; + end if; + return existing.launch_requirement_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'launch_requirement.append', p_requirement_commitment, null, p_created_at + ); + insert into programmable_private.release_launch_completeness_requirements ( + launch_requirement_id, epoch_id, requirement_ordinal, occurrence_role, + event_type, required_when, requirement_commitment, created_at, + created_by_audit_id + ) values ( + p_launch_requirement_id, p_epoch_id, p_requirement_ordinal, + p_occurrence_role::programmable_private.source_identifier, + p_event_type::programmable_private.source_identifier, + p_required_when::programmable_private.source_identifier, + p_requirement_commitment::programmable_private.bytes32_value, + p_created_at, audit_id + ); + return p_launch_requirement_id; +end +$function$; + +create function programmable_private.assert_projection_event_allowed( + p_run_id uuid, + p_occurrence_id uuid, + p_projection_kind text +) +returns void +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + occurrence programmable_private.chain_event_occurrences%rowtype; + materialization programmable_private.chain_event_occurrence_materializations%rowtype; + resolved_source_role text; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid event-writer run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into occurrence from programmable_private.chain_event_occurrences + where occurrence_id = p_occurrence_id; + if not found + or occurrence.chain_id <> header.chain_id + then + raise exception using errcode = '23503', message = 'projection event scope mismatch'; + end if; + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_occurrence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found then + raise exception using errcode = '23503', message = 'projection event scope mismatch'; + end if; + select coalesce(binding.source_role, dynamic_source.deployed_source_role) + into resolved_source_role + from programmable_private.chain_event_occurrence_materializations as selected + left join programmable_private.release_source_bindings as binding + on binding.binding_id = selected.release_binding_id + left join programmable_private.dynamic_source_attestations as dynamic_source + on dynamic_source.dynamic_source_attestation_id = + selected.dynamic_source_attestation_id + where selected.materialization_id = materialization.materialization_id; + if resolved_source_role is null or not exists ( + select 1 from programmable_private.release_projection_event_rules as rule + where rule.epoch_id = header.epoch_id + and rule.projection_kind = p_projection_kind + and rule.source_role = resolved_source_role + and rule.event_type = materialization.event_type + ) then + raise exception using errcode = '23514', message = 'event/source role is outside the release writer allowlist'; + end if; +end +$function$; + +create function programmable_private.stage_launch_occurrence_role( + p_launch_projection_id uuid, + p_occurrence_role text, + p_occurrence_id uuid, + p_staged_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + launch programmable_private.launch_projections%rowtype; + materialization programmable_private.chain_event_occurrence_materializations%rowtype; + actual_role text; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into launch from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id; + if not found then + raise exception using errcode = '23503', message = 'unknown staged launch'; + end if; + perform programmable_private.assert_projection_event_allowed( + launch.projection_run_id, p_occurrence_id, 'launch_requirement' + ); + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_occurrence_id + and epoch_id = launch.epoch_id + and pointer_generation = launch.pointer_generation; + select coalesce(binding.source_role, dynamic_source.deployed_source_role) + into actual_role + from programmable_private.chain_event_occurrence_materializations as selected + left join programmable_private.release_source_bindings as binding + on binding.binding_id = selected.release_binding_id + left join programmable_private.dynamic_source_attestations as dynamic_source + on dynamic_source.dynamic_source_attestation_id = + selected.dynamic_source_attestation_id + where selected.materialization_id = materialization.materialization_id; + if actual_role <> p_occurrence_role + or not exists ( + select 1 + from programmable_private.release_launch_completeness_requirements + where epoch_id = launch.epoch_id + and occurrence_role = p_occurrence_role + and event_type = materialization.event_type + ) + then + raise exception using errcode = '23514', message = 'occurrence does not satisfy a launch requirement'; + end if; + insert into programmable_private.launch_projection_occurrence_roles ( + launch_projection_id, occurrence_role, occurrence_id, + projection_run_id, staged_at + ) values ( + p_launch_projection_id, + p_occurrence_role::programmable_private.source_identifier, + p_occurrence_id, launch.projection_run_id, p_staged_at + ) on conflict (launch_projection_id, occurrence_role) do update + set occurrence_id = excluded.occurrence_id, + staged_at = excluded.staged_at + where programmable_private.launch_projection_occurrence_roles.occurrence_id + = excluded.occurrence_id; + if not found then + raise exception using errcode = '23505', message = 'launch occurrence role replay conflict'; + end if; + return p_launch_projection_id; +end +$function$; + +create function programmable_private.stage_launch_projection_conditions( + p_launch_projection_id uuid, + p_eth_funded boolean, + p_staged_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + launch programmable_private.launch_projections%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into launch from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id; + if not found then + raise exception using errcode = '23503', message = 'unknown staged launch'; + end if; + perform programmable_private.projection_stage_context( + launch.projection_run_id, launch.last_source_occurrence_id, + launch.promoted_block_number, launch.promoted_block_hash + ); + insert into programmable_private.launch_projection_conditions ( + launch_projection_id, projection_run_id, eth_funded, staged_at + ) values ( + p_launch_projection_id, launch.projection_run_id, p_eth_funded, p_staged_at + ) on conflict (launch_projection_id) do update + set staged_at = excluded.staged_at + where programmable_private.launch_projection_conditions.projection_run_id + = excluded.projection_run_id + and programmable_private.launch_projection_conditions.eth_funded + = excluded.eth_funded; + if not found then + raise exception using errcode = '23505', message = 'launch condition replay conflict'; + end if; + return p_launch_projection_id; +end +$function$; + +create function programmable_private.stage_pool_fee_configuration_v2( + p_pool_fee_configuration_id uuid, + p_pool_projection_id uuid, + p_run_id uuid, + p_buy_swap_fee_bps numeric, + p_sell_swap_fee_bps numeric, + p_buy_creator_fee_bps numeric, + p_sell_creator_fee_bps numeric, + p_launcher_fee_bps numeric, + p_transfer_tax_bps numeric, + p_lp_fee_pips numeric, + p_disclosure_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + pool programmable_private.pool_projections%rowtype; + returned_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.projection_stage_context( + p_run_id, p_disclosure_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into pool from programmable_private.pool_projections + where pool_projection_id = p_pool_projection_id + and projection_run_id = p_run_id; + if pool.pool_projection_id is null + or pool.chain_id <> scope.chain_id + or pool.release_id <> scope.release_id + or pool.model_id <> scope.model_id + or pool.epoch_id <> scope.epoch_id + or pool.pointer_generation <> scope.pointer_generation + or pool.promoted_block_number <> scope.promoted_block_number + or pool.promoted_block_hash <> scope.promoted_block_hash + or exists ( + select 1 from pg_catalog.unnest(array[ + p_buy_swap_fee_bps, p_sell_swap_fee_bps, + p_buy_creator_fee_bps, p_sell_creator_fee_bps, + p_launcher_fee_bps, p_transfer_tax_bps + ]) as value + where value is null or value <> pg_catalog.trunc(value) + or value < 0 or value > 10000 + ) + or p_lp_fee_pips is null + or p_lp_fee_pips <> pg_catalog.trunc(p_lp_fee_pips) + or p_lp_fee_pips < 0 or p_lp_fee_pips > 1000000 + or p_buy_creator_fee_bps + p_launcher_fee_bps <> p_buy_swap_fee_bps + or p_sell_creator_fee_bps + p_launcher_fee_bps <> p_sell_swap_fee_bps + then + raise exception using errcode = '23514', message = 'directional pool fee scope or values mismatch'; + end if; + insert into programmable_private.pool_fee_configurations as target ( + pool_fee_configuration_id, pool_projection_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, buy_swap_fee_bps, + sell_swap_fee_bps, buy_creator_fee_bps, sell_creator_fee_bps, + creator_fee_bps, launcher_fee_bps, transfer_tax_bps, lp_fee_pips, + disclosure_source_occurrence_id, disclosure_source_logical_event_id, + disclosure_source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at + ) values ( + p_pool_fee_configuration_id, p_pool_projection_id, scope.chain_id, + scope.release_id, scope.model_id, scope.epoch_id, + scope.pointer_generation, p_buy_swap_fee_bps, p_sell_swap_fee_bps, + p_buy_creator_fee_bps, p_sell_creator_fee_bps, + case when p_buy_creator_fee_bps = p_sell_creator_fee_bps + then p_buy_creator_fee_bps else null end, + p_launcher_fee_bps, p_transfer_tax_bps, p_lp_fee_pips::bigint, + p_disclosure_source_occurrence_id, scope.source_logical_event_id, + scope.source_occurrence_block_hash, p_run_id, + scope.promoted_block_number, scope.promoted_block_hash, p_verified_at + ) on conflict (pool_fee_configuration_id) do update + set pool_fee_configuration_id = excluded.pool_fee_configuration_id + where target is not distinct from excluded + returning pool_fee_configuration_id into returned_id; + if returned_id is null then + raise exception using errcode = '23505', message = 'directional fee replay changed immutable content'; + end if; + perform programmable_private.append_mutation_audit( + 'pool_fee_configuration_v2.stage', p_promoted_block_hash, + p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.event_fact_context( + p_run_id uuid, + p_source_occurrence_id uuid, + p_projection_kind text +) +returns table ( + chain_id bigint, + release_id text, + model_id text, + epoch_id uuid, + pointer_generation bigint, + logical_event_id uuid, + occurrence_block_hash bytea, + source_address bytea +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_projection_event_allowed( + p_run_id, p_source_occurrence_id, p_projection_kind + ); + if not exists ( + select 1 from programmable_private.chain_event_current_canonical + where occurrence_id = p_source_occurrence_id + ) then + raise exception using errcode = '23514', message = 'event fact source is not current canonical'; + end if; + return query + select materialization.chain_id::bigint, materialization.release_id::text, + materialization.model_id::text, materialization.epoch_id, + materialization.pointer_generation, occurrence.logical_event_id, + occurrence.block_hash::bytea, occurrence.source_address::bytea + from programmable_private.run_headers as header + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = header.captured_pointer_generation + and materialization.occurrence_id = p_source_occurrence_id + join programmable_private.chain_event_occurrences as occurrence + on occurrence.occurrence_id = materialization.occurrence_id + where header.run_id = p_run_id; +end +$function$; + +create function programmable_private.append_creator_hook_claim_fact( + p_creator_hook_claim_fact_id uuid, + p_run_id uuid, + p_source_occurrence_id uuid, + p_pool_id bytea, + p_reward_vault bytea, + p_creator bytea, + p_recipient bytea, + p_quote_asset bytea, + p_caller bytea, + p_amount numeric, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + existing programmable_private.creator_hook_claim_facts%rowtype; + amount numeric; + returned_id uuid; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.event_fact_context( + p_run_id, p_source_occurrence_id, 'creator_hook_claim' + ); + amount := programmable_private.validate_uint256(p_amount); + if pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_caller) <> 20 + or (p_reward_vault is not null and pg_catalog.octet_length(p_reward_vault) <> 20) + or (p_creator is not null and pg_catalog.octet_length(p_creator) <> 20) + or (p_recipient is not null and pg_catalog.octet_length(p_recipient) <> 20) + or (p_quote_asset is not null and pg_catalog.octet_length(p_quote_asset) <> 20) + or not ( + (p_reward_vault is not null and p_creator is null and p_recipient is null) + or (p_reward_vault is null and p_creator is not null + and p_recipient is not null and p_quote_asset is null) + ) + then + raise exception using errcode = '22023', message = 'creator claim does not match an allowlisted hook event shape'; + end if; + select * into existing + from programmable_private.creator_hook_claim_facts + where creator_hook_claim_fact_id = p_creator_hook_claim_fact_id; + if found then + if existing.chain_id <> scope.chain_id + or existing.release_id <> scope.release_id + or existing.model_id <> scope.model_id + or existing.epoch_id <> scope.epoch_id + or existing.pointer_generation <> scope.pointer_generation + or existing.pool_id <> p_pool_id + or existing.reward_vault is distinct from p_reward_vault + or existing.creator is distinct from p_creator + or existing.recipient is distinct from p_recipient + or existing.quote_asset is distinct from p_quote_asset + or existing.caller <> p_caller + or existing.amount <> amount + or existing.source_occurrence_id <> p_source_occurrence_id + or existing.source_logical_event_id <> scope.logical_event_id + or existing.source_occurrence_block_hash <> scope.occurrence_block_hash + or existing.verification_run_id <> p_run_id + or existing.verified_at <> p_verified_at + then + raise exception using errcode = '23505', message = 'creator hook claim replay conflict'; + end if; + return existing.creator_hook_claim_fact_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'creator_hook_claim.append', scope.occurrence_block_hash, + p_run_id, p_verified_at + ); + insert into programmable_private.creator_hook_claim_facts as target ( + creator_hook_claim_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, pool_id, reward_vault, creator, recipient, + quote_asset, caller, amount, source_occurrence_id, + source_logical_event_id, source_occurrence_block_hash, + verification_run_id, verified_at, created_by_audit_id + ) values ( + p_creator_hook_claim_fact_id, scope.chain_id, scope.release_id, + scope.model_id, scope.epoch_id, scope.pointer_generation, p_pool_id, + p_reward_vault, p_creator, p_recipient, p_quote_asset, p_caller, amount, + p_source_occurrence_id, scope.logical_event_id, + scope.occurrence_block_hash, p_run_id, p_verified_at, audit_id + ) + returning creator_hook_claim_fact_id into returned_id; + return returned_id; +end +$function$; + +create function programmable_private.append_launcher_hook_claim_fact( + p_launcher_hook_claim_fact_id uuid, + p_run_id uuid, + p_source_occurrence_id uuid, + p_treasury bytea, + p_recipient bytea, + p_quote_asset bytea, + p_caller bytea, + p_amount numeric, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + existing programmable_private.launcher_hook_claim_facts%rowtype; + amount numeric; + returned_id uuid; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.event_fact_context( + p_run_id, p_source_occurrence_id, 'launcher_hook_claim' + ); + amount := programmable_private.validate_uint256(p_amount); + if pg_catalog.octet_length(p_treasury) <> 20 + or pg_catalog.octet_length(p_recipient) <> 20 + or pg_catalog.octet_length(p_caller) <> 20 + or (p_quote_asset is not null and pg_catalog.octet_length(p_quote_asset) <> 20) + then + raise exception using errcode = '22023', message = 'launcher claim does not match an allowlisted hook event shape'; + end if; + select * into existing + from programmable_private.launcher_hook_claim_facts + where launcher_hook_claim_fact_id = p_launcher_hook_claim_fact_id; + if found then + if existing.chain_id <> scope.chain_id + or existing.release_id <> scope.release_id + or existing.model_id <> scope.model_id + or existing.epoch_id <> scope.epoch_id + or existing.pointer_generation <> scope.pointer_generation + or existing.treasury <> p_treasury + or existing.recipient <> p_recipient + or existing.quote_asset is distinct from p_quote_asset + or existing.caller <> p_caller + or existing.amount <> amount + or existing.source_occurrence_id <> p_source_occurrence_id + or existing.source_logical_event_id <> scope.logical_event_id + or existing.source_occurrence_block_hash <> scope.occurrence_block_hash + or existing.verification_run_id <> p_run_id + or existing.verified_at <> p_verified_at + then + raise exception using errcode = '23505', message = 'launcher hook claim replay conflict'; + end if; + return existing.launcher_hook_claim_fact_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'launcher_hook_claim.append', scope.occurrence_block_hash, + p_run_id, p_verified_at + ); + insert into programmable_private.launcher_hook_claim_facts as target ( + launcher_hook_claim_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, treasury, recipient, quote_asset, caller, amount, + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash, verification_run_id, verified_at, + created_by_audit_id + ) values ( + p_launcher_hook_claim_fact_id, scope.chain_id, scope.release_id, + scope.model_id, scope.epoch_id, scope.pointer_generation, p_treasury, + p_recipient, p_quote_asset, p_caller, amount, p_source_occurrence_id, + scope.logical_event_id, scope.occurrence_block_hash, + p_run_id, p_verified_at, audit_id + ) + returning launcher_hook_claim_fact_id into returned_id; + return returned_id; +end +$function$; + +create function programmable_private.append_creator_fee_checkpoint_fact( + p_creator_fee_checkpoint_fact_id uuid, + p_run_id uuid, + p_source_occurrence_id uuid, + p_pool_id bytea, + p_configuration_epoch numeric, + p_amount numeric, + p_total_creator_fees_received numeric, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + existing programmable_private.creator_fee_checkpoint_facts%rowtype; + amount numeric; + total_received numeric; + normalized_epoch bigint; + returned_id uuid; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.event_fact_context( + p_run_id, p_source_occurrence_id, 'creator_fee_checkpoint' + ); + amount := programmable_private.validate_uint256(p_amount); + total_received := programmable_private.validate_uint256( + p_total_creator_fees_received + ); + if pg_catalog.octet_length(p_pool_id) <> 32 + or p_configuration_epoch <> pg_catalog.trunc(p_configuration_epoch) + or p_configuration_epoch < 0 + or p_configuration_epoch > 9223372036854775807 + or amount > total_received + then + raise exception using errcode = '22023', message = 'invalid creator fee checkpoint'; + end if; + normalized_epoch := p_configuration_epoch::bigint; + select * into existing + from programmable_private.creator_fee_checkpoint_facts + where creator_fee_checkpoint_fact_id = p_creator_fee_checkpoint_fact_id; + if found then + if existing.chain_id <> scope.chain_id + or existing.release_id <> scope.release_id + or existing.model_id <> scope.model_id + or existing.epoch_id <> scope.epoch_id + or existing.pointer_generation <> scope.pointer_generation + or existing.vault <> scope.source_address + or existing.pool_id <> p_pool_id + or existing.configuration_epoch <> normalized_epoch + or existing.amount <> amount + or existing.total_creator_fees_received <> total_received + or existing.source_occurrence_id <> p_source_occurrence_id + or existing.source_logical_event_id <> scope.logical_event_id + or existing.source_occurrence_block_hash <> scope.occurrence_block_hash + or existing.verification_run_id <> p_run_id + or existing.verified_at <> p_verified_at + then + raise exception using errcode = '23505', message = 'creator fee checkpoint replay conflict'; + end if; + return existing.creator_fee_checkpoint_fact_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'creator_fee_checkpoint.append', scope.occurrence_block_hash, + p_run_id, p_verified_at + ); + insert into programmable_private.creator_fee_checkpoint_facts as target ( + creator_fee_checkpoint_fact_id, chain_id, release_id, model_id, + epoch_id, pointer_generation, vault, pool_id, configuration_epoch, + amount, total_creator_fees_received, source_occurrence_id, + source_logical_event_id, source_occurrence_block_hash, + verification_run_id, verified_at, created_by_audit_id + ) values ( + p_creator_fee_checkpoint_fact_id, scope.chain_id, scope.release_id, + scope.model_id, scope.epoch_id, scope.pointer_generation, + scope.source_address, p_pool_id, normalized_epoch, amount, + total_received, p_source_occurrence_id, scope.logical_event_id, + scope.occurrence_block_hash, p_run_id, p_verified_at, audit_id + ) + returning creator_fee_checkpoint_fact_id into returned_id; + return returned_id; +end +$function$; + +create function programmable_private.append_reward_configuration_activation_fact( + p_reward_configuration_activation_fact_id uuid, + p_run_id uuid, + p_source_occurrence_id uuid, + p_pool_id bytea, + p_approval_reference bytea, + p_configuration_epoch numeric, + p_previous_configuration_hash bytea, + p_new_configuration_hash bytea, + p_ordered_beneficiaries bytea[], + p_ordered_shares_bps numeric[], + p_effective_total_creator_fees_received numeric, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + scope record; + existing programmable_private.reward_configuration_activation_facts%rowtype; + normalized_epoch bigint; + shares integer[]; + effective_total numeric; + returned_id uuid; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope from programmable_private.event_fact_context( + p_run_id, p_source_occurrence_id, 'reward_configuration_activation' + ); + effective_total := programmable_private.validate_uint256( + p_effective_total_creator_fees_received + ); + if pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_approval_reference) <> 32 + or pg_catalog.octet_length(p_previous_configuration_hash) <> 32 + or pg_catalog.octet_length(p_new_configuration_hash) <> 32 + or p_configuration_epoch <> pg_catalog.trunc(p_configuration_epoch) + or p_configuration_epoch < 0 + or p_configuration_epoch > 9223372036854775807 + or cardinality(p_ordered_beneficiaries) <> cardinality(p_ordered_shares_bps) + or exists ( + select 1 from pg_catalog.unnest(p_ordered_shares_bps) as share + where share <> pg_catalog.trunc(share) + ) + then + raise exception using errcode = '22023', message = 'invalid reward configuration activation'; + end if; + select pg_catalog.array_agg(share::integer order by ordinality) + into shares + from pg_catalog.unnest(p_ordered_shares_bps) + with ordinality as value(share, ordinality); + if not programmable_private.valid_beneficiary_set( + p_ordered_beneficiaries, shares, 5 + ) then + raise exception using errcode = '22023', message = 'invalid activated beneficiary set'; + end if; + normalized_epoch := p_configuration_epoch::bigint; + select * into existing + from programmable_private.reward_configuration_activation_facts + where reward_configuration_activation_fact_id = + p_reward_configuration_activation_fact_id; + if found then + if existing.chain_id <> scope.chain_id + or existing.release_id <> scope.release_id + or existing.model_id <> scope.model_id + or existing.epoch_id <> scope.epoch_id + or existing.pointer_generation <> scope.pointer_generation + or existing.vault <> scope.source_address + or existing.pool_id <> p_pool_id + or existing.approval_reference <> p_approval_reference + or existing.configuration_epoch <> normalized_epoch + or existing.previous_configuration_hash <> + p_previous_configuration_hash + or existing.new_configuration_hash <> p_new_configuration_hash + or existing.ordered_beneficiaries <> p_ordered_beneficiaries + or existing.ordered_shares_bps <> shares + or existing.effective_total_creator_fees_received <> effective_total + or existing.source_occurrence_id <> p_source_occurrence_id + or existing.source_logical_event_id <> scope.logical_event_id + or existing.source_occurrence_block_hash <> scope.occurrence_block_hash + or existing.verification_run_id <> p_run_id + or existing.verified_at <> p_verified_at + then + raise exception using errcode = '23505', message = 'reward configuration activation replay conflict'; + end if; + return existing.reward_configuration_activation_fact_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'reward_configuration_activation.append', scope.occurrence_block_hash, + p_run_id, p_verified_at + ); + insert into programmable_private.reward_configuration_activation_facts as target ( + reward_configuration_activation_fact_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, vault, pool_id, + approval_reference, configuration_epoch, previous_configuration_hash, + new_configuration_hash, ordered_beneficiaries, ordered_shares_bps, + effective_total_creator_fees_received, source_occurrence_id, + source_logical_event_id, source_occurrence_block_hash, + verification_run_id, verified_at, created_by_audit_id + ) values ( + p_reward_configuration_activation_fact_id, scope.chain_id, + scope.release_id, scope.model_id, scope.epoch_id, + scope.pointer_generation, scope.source_address, p_pool_id, + p_approval_reference, normalized_epoch, p_previous_configuration_hash, + p_new_configuration_hash, p_ordered_beneficiaries, shares, + effective_total, p_source_occurrence_id, scope.logical_event_id, + scope.occurrence_block_hash, p_run_id, p_verified_at, audit_id + ) + returning reward_configuration_activation_fact_id into returned_id; + return returned_id; +end +$function$; + +create function programmable_private.enforce_projection_event_rule() +returns trigger +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + source_occurrence_id uuid; + projection_kind text; +begin + source_occurrence_id := case tg_table_name + when 'launch_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'pool_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'pool_fee_configurations' then + (pg_catalog.to_jsonb(new)->>'disclosure_source_occurrence_id')::uuid + when 'fee_accrual_facts' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'pool_fee_totals' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'reward_vault_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'reward_allocation_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'claim_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'payout_change_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'account_reward_balances' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'initial_buy_custody_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'initial_buy_vesting_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + else null + end; + projection_kind := case tg_table_name + when 'launch_projections' then 'launch' + when 'pool_projections' then 'pool' + when 'pool_fee_configurations' then 'pool_fee_configuration' + when 'fee_accrual_facts' then 'fee_accrual' + when 'pool_fee_totals' then 'pool_fee_total' + when 'reward_vault_projections' then 'reward_vault' + when 'reward_allocation_projections' then 'reward_allocation' + when 'claim_projections' then 'claim' + when 'payout_change_projections' then 'payout_change' + when 'account_reward_balances' then 'account_reward_balance' + when 'initial_buy_custody_projections' then 'initial_buy_custody' + when 'initial_buy_vesting_projections' then 'initial_buy_vesting' + else null + end; + perform programmable_private.assert_projection_event_allowed( + new.projection_run_id, source_occurrence_id, projection_kind + ); + return new; +end +$function$; + +do $projection_event_triggers$ +declare + table_name text; +begin + foreach table_name in array array[ + 'launch_projections', 'pool_projections', 'pool_fee_configurations', + 'fee_accrual_facts', 'pool_fee_totals', 'reward_vault_projections', + 'reward_allocation_projections', 'claim_projections', + 'payout_change_projections', 'account_reward_balances', + 'initial_buy_custody_projections', 'initial_buy_vesting_projections' + ] loop + execute pg_catalog.format( + 'create trigger %I before insert on programmable_private.%I for each row execute function programmable_private.enforce_projection_event_rule()', + table_name || '_event_rule', table_name + ); + end loop; +end +$projection_event_triggers$; + +create function programmable_private.enforce_launch_publication_completeness() +returns trigger +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + launch programmable_private.launch_projections%rowtype; + requirement programmable_private.release_launch_completeness_requirements%rowtype; + condition_record programmable_private.launch_projection_conditions%rowtype; + condition_applies boolean; +begin + if not exists ( + select 1 from programmable_private.release_launch_completeness_requirements + where epoch_id = new.epoch_id + ) then + raise exception using errcode = '23514', message = 'release has no launch completeness manifest'; + end if; + for launch in + select * from programmable_private.launch_projections + where projection_run_id = new.run_id + loop + select * into condition_record + from programmable_private.launch_projection_conditions + where launch_projection_id = launch.launch_projection_id + and projection_run_id = launch.projection_run_id; + if not found then + raise exception using errcode = '23514', message = 'launch funding condition was not staged'; + end if; + for requirement in + select * + from programmable_private.release_launch_completeness_requirements + where epoch_id = launch.epoch_id + order by requirement_ordinal + loop + condition_applies := requirement.required_when = 'always' + or (requirement.required_when = 'reward_vault' + and launch.reward_vault is not null) + or (requirement.required_when = 'locked_custody' and exists ( + select 1 from programmable_private.initial_buy_custody_projections + where launch_projection_id = launch.launch_projection_id + and projection_run_id = launch.projection_run_id + and custody_mode <> 0 + )) + or (requirement.required_when = 'eth_funded' + and condition_record.eth_funded); + if condition_applies and not exists ( + select 1 + from programmable_private.launch_projection_occurrence_roles as role + join programmable_private.chain_event_occurrences as occurrence + on occurrence.occurrence_id = role.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = role.occurrence_id + and materialization.epoch_id = launch.epoch_id + and materialization.pointer_generation = launch.pointer_generation + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = occurrence.occurrence_id + and canonical.logical_event_id = occurrence.logical_event_id + where role.launch_projection_id = launch.launch_projection_id + and role.projection_run_id = launch.projection_run_id + and role.occurrence_role = requirement.occurrence_role + and materialization.event_type = requirement.event_type + ) then + raise exception using errcode = '23514', message = 'launch completeness occurrence is missing'; + end if; + end loop; + if launch.reward_vault is not null and ( + not exists ( + select 1 + from programmable_private.release_launch_completeness_requirements + where epoch_id = launch.epoch_id and required_when = 'reward_vault' + ) + or not exists ( + select 1 + from programmable_private.reward_vault_projections as vault + join programmable_private.reward_allocation_current_verified as seed + on seed.allocation_fact_id = vault.current_allocation_fact_id + and seed.vault = vault.vault + where vault.launch_projection_id = launch.launch_projection_id + and vault.projection_run_id = launch.projection_run_id + and vault.vault = launch.reward_vault + ) + ) then + raise exception using errcode = '23514', message = 'reward-vault launch lacks verified seed completeness'; + end if; + if exists ( + select 1 from programmable_private.initial_buy_custody_projections + where launch_projection_id = launch.launch_projection_id + and projection_run_id = launch.projection_run_id + and custody_mode <> 0 + ) and ( + not exists ( + select 1 from programmable_private.release_launch_completeness_requirements + where epoch_id = launch.epoch_id and required_when = 'locked_custody' + ) + or not exists ( + select 1 + from programmable_private.initial_buy_custody_projections as custody + join programmable_private.initial_buy_vesting_projections as vesting + on vesting.custody_projection_id = custody.custody_projection_id + and vesting.projection_run_id = custody.projection_run_id + where custody.launch_projection_id = launch.launch_projection_id + and custody.projection_run_id = launch.projection_run_id + and custody.custody_mode <> 0 + ) + ) then + raise exception using errcode = '23514', message = 'locked custody lacks vesting completeness'; + end if; + if condition_record.eth_funded and not exists ( + select 1 from programmable_private.release_launch_completeness_requirements + where epoch_id = launch.epoch_id and required_when = 'eth_funded' + ) then + raise exception using errcode = '23514', message = 'ETH-funded launch lacks coordinator requirement'; + end if; + end loop; + return new; +end +$function$; + +create trigger projection_publication_launch_completeness +before insert on programmable_private.projection_publications +for each row execute function programmable_private.enforce_launch_publication_completeness(); + +create function programmable_private.append_release_dynamic_source_template( + p_dynamic_source_template_id uuid, + p_epoch_id uuid, + p_parent_factory_release_binding_id uuid, + p_parent_source_role text, + p_factory_event_type text, + p_deployed_address_field text, + p_deployed_source_role text, + p_deployed_artifact_creation_code_commitment bytea, + p_normalized_runtime_code_hash bytea, + p_immutable_references_commitment bytea, + p_immutable_binding_spec jsonb, + p_immutable_binding_commitment bytea, + p_runtime_code_length numeric, + p_abi_event_set_commitment bytea, + p_template_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + audit_id uuid; + existing programmable_private.release_dynamic_source_templates%rowtype; + parent_binding programmable_private.release_source_bindings%rowtype; + normalized_runtime_code_length bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into parent_binding + from programmable_private.release_source_bindings + where binding_id = p_parent_factory_release_binding_id + and epoch_id = p_epoch_id + and source_role = p_parent_source_role + and source_type = 'ethereum_contract' + and source_address is not null; + if p_dynamic_source_template_id is null + or parent_binding.binding_id is null + or p_deployed_address_field not in ('vault', 'wallet') + or p_deployed_source_role not in ('reward_vault', 'vesting_wallet') + or not ( + (p_deployed_source_role = 'reward_vault' + and p_deployed_address_field = 'vault') + or (p_deployed_source_role = 'vesting_wallet' + and p_deployed_address_field = 'wallet') + ) + or pg_catalog.octet_length( + p_deployed_artifact_creation_code_commitment + ) <> 32 + or pg_catalog.octet_length(p_normalized_runtime_code_hash) <> 32 + or pg_catalog.octet_length(p_immutable_references_commitment) <> 32 + or not programmable_private.valid_immutable_binding_spec( + p_immutable_binding_spec + ) + or pg_catalog.octet_length(p_immutable_binding_commitment) <> 32 + or p_runtime_code_length <> pg_catalog.trunc(p_runtime_code_length) + or p_runtime_code_length <= 0 + or p_runtime_code_length > 16777216 + or pg_catalog.octet_length(p_abi_event_set_commitment) <> 32 + or pg_catalog.octet_length(p_template_commitment) <> 32 + or exists ( + select 1 from programmable_private.release_epoch_current + where epoch_id = p_epoch_id + ) + then + raise exception using errcode = '22023', message = 'invalid or active dynamic source template'; + end if; + normalized_runtime_code_length := p_runtime_code_length::bigint; + if not programmable_private.immutable_binding_spec_fits_runtime( + p_immutable_binding_spec, normalized_runtime_code_length + ) then + raise exception using + errcode = '22023', + message = 'immutable binding offsets exceed normalized runtime'; + end if; + select * into existing + from programmable_private.release_dynamic_source_templates + where dynamic_source_template_id = p_dynamic_source_template_id; + if found then + if existing.epoch_id <> p_epoch_id + or existing.parent_factory_release_binding_id + <> p_parent_factory_release_binding_id + or existing.parent_factory_binding_commitment + <> parent_binding.binding_commitment + or existing.parent_source_role <> p_parent_source_role + or existing.factory_event_type <> p_factory_event_type + or existing.deployed_address_field <> p_deployed_address_field + or existing.deployed_source_role <> p_deployed_source_role + or existing.deployed_artifact_creation_code_commitment + <> p_deployed_artifact_creation_code_commitment + or existing.normalized_runtime_code_hash + <> p_normalized_runtime_code_hash + or existing.immutable_references_commitment + <> p_immutable_references_commitment + or existing.immutable_binding_spec <> p_immutable_binding_spec + or existing.immutable_binding_commitment + <> p_immutable_binding_commitment + or existing.runtime_code_length <> normalized_runtime_code_length + or existing.abi_event_set_commitment <> p_abi_event_set_commitment + or existing.template_commitment <> p_template_commitment + then + raise exception using errcode = '23505', message = 'dynamic source template replay conflict'; + end if; + return existing.dynamic_source_template_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'dynamic_source_template.append', p_template_commitment, null, p_created_at + ); + insert into programmable_private.release_dynamic_source_templates ( + dynamic_source_template_id, epoch_id, + parent_factory_release_binding_id, parent_factory_binding_commitment, + parent_source_role, factory_event_type, deployed_address_field, + deployed_source_role, deployed_artifact_creation_code_commitment, + normalized_runtime_code_hash, immutable_references_commitment, + immutable_binding_spec, immutable_binding_commitment, + runtime_code_length, + abi_event_set_commitment, template_commitment, created_at, + created_by_audit_id + ) values ( + p_dynamic_source_template_id, p_epoch_id, + p_parent_factory_release_binding_id, parent_binding.binding_commitment, + p_parent_source_role::programmable_private.source_identifier, + p_factory_event_type::programmable_private.source_identifier, + p_deployed_address_field::programmable_private.source_identifier, + p_deployed_source_role::programmable_private.source_identifier, + p_deployed_artifact_creation_code_commitment::programmable_private.bytes32_value, + p_normalized_runtime_code_hash::programmable_private.bytes32_value, + p_immutable_references_commitment::programmable_private.bytes32_value, + p_immutable_binding_spec, + p_immutable_binding_commitment::programmable_private.bytes32_value, + normalized_runtime_code_length, + p_abi_event_set_commitment::programmable_private.bytes32_value, + p_template_commitment::programmable_private.bytes32_value, + p_created_at, audit_id + ); + return p_dynamic_source_template_id; +end +$function$; + +create function programmable_private.append_dual_rpc_runtime_code_evidence( + p_runtime_code_evidence_id uuid, + p_run_id uuid, + p_source_address bytea, + p_deployment_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_runtime_code_hash_a bytea, + p_runtime_code_hash_b bytea, + p_runtime_code_length_a numeric, + p_runtime_code_length_b numeric, + p_normalized_runtime_code_hash_a bytea, + p_normalized_runtime_code_hash_b bytea, + p_immutable_references_commitment bytea, + p_immutable_values bytea[], + p_immutable_values_commitment bytea, + p_reconstructed_runtime_code_hash bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_evidence_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + block_evidence programmable_private.dual_rpc_block_evidence%rowtype; + existing programmable_private.dual_rpc_runtime_code_evidence%rowtype; + normalized_runtime_code_length bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'runtime_code', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid runtime-code verification run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into block_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_deployment_block_evidence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found + or p_provider_a_id = p_provider_b_id + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_a_id + and provider_type = 'rpc_provider' + ) + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_b_id + and provider_type = 'rpc_provider' + ) + or not exists ( + select 1 + from programmable_private.safe_head_observations as observation + where observation.observation_id = block_evidence.observation_id + and observation.epoch_id = block_evidence.epoch_id + and observation.pointer_generation = block_evidence.pointer_generation + and observation.provider_a_id = p_provider_a_id + and observation.provider_b_id = p_provider_b_id + ) + or pg_catalog.octet_length(p_source_address) <> 20 + or pg_catalog.octet_length(p_runtime_code_hash_a) <> 32 + or p_runtime_code_hash_a <> p_runtime_code_hash_b + or p_runtime_code_length_a is null + or p_runtime_code_length_b is null + or p_runtime_code_length_a <> pg_catalog.trunc(p_runtime_code_length_a) + or p_runtime_code_length_b <> pg_catalog.trunc(p_runtime_code_length_b) + or p_runtime_code_length_a <> p_runtime_code_length_b + or p_runtime_code_length_a <= 0 + or p_runtime_code_length_a > 16777216 + or pg_catalog.octet_length(p_normalized_runtime_code_hash_a) <> 32 + or p_normalized_runtime_code_hash_a + <> p_normalized_runtime_code_hash_b + or pg_catalog.octet_length(p_immutable_references_commitment) <> 32 + or not programmable_private.valid_immutable_values(p_immutable_values) + or pg_catalog.octet_length(p_immutable_values_commitment) <> 32 + or pg_catalog.octet_length(p_reconstructed_runtime_code_hash) <> 32 + or p_reconstructed_runtime_code_hash <> p_runtime_code_hash_a + or pg_catalog.octet_length(p_evidence_commitment) <> 32 + then + raise exception using errcode = '23514', message = 'runtime code lacks exact dual-RPC deployment-block evidence'; + end if; + normalized_runtime_code_length := p_runtime_code_length_a::bigint; + select * into existing + from programmable_private.dual_rpc_runtime_code_evidence + where epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and source_address = p_source_address + and deployment_block_number = block_evidence.block_number; + if found then + if existing.runtime_code_evidence_id <> p_runtime_code_evidence_id + or existing.deployment_block_evidence_id <> p_deployment_block_evidence_id + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.agreed_runtime_code_hash <> p_runtime_code_hash_a + or existing.agreed_runtime_code_length + <> normalized_runtime_code_length + or existing.agreed_normalized_runtime_code_hash + <> p_normalized_runtime_code_hash_a + or existing.immutable_references_commitment + <> p_immutable_references_commitment + or existing.immutable_values <> p_immutable_values + or existing.immutable_values_commitment + <> p_immutable_values_commitment + or existing.reconstructed_runtime_code_hash + <> p_reconstructed_runtime_code_hash + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + or existing.evidence_commitment <> p_evidence_commitment + then + raise exception using errcode = '23505', message = 'runtime code evidence replay conflict'; + end if; + return existing.runtime_code_evidence_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'runtime_code_evidence.append', p_evidence_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.dual_rpc_runtime_code_evidence ( + runtime_code_evidence_id, chain_id, release_id, model_id, source_group, + epoch_id, pointer_generation, source_address, + deployment_block_evidence_id, deployment_block_number, + deployment_block_hash, provider_a_id, provider_b_id, + runtime_code_hash_a, runtime_code_hash_b, agreed_runtime_code_hash, + runtime_code_length_a, runtime_code_length_b, + agreed_runtime_code_length, normalized_runtime_code_hash_a, + normalized_runtime_code_hash_b, agreed_normalized_runtime_code_hash, + immutable_references_commitment, immutable_values, + immutable_values_commitment, reconstructed_runtime_code_hash, + encoding_version, canonical_preimage, content_fingerprint, + evidence_commitment, verification_run_id, verified_at, created_by_audit_id + ) values ( + p_runtime_code_evidence_id, header.chain_id, header.release_id, + header.model_id, header.source_group, header.epoch_id, + header.captured_pointer_generation, + p_source_address::programmable_private.eth_address, + block_evidence.block_evidence_id, block_evidence.block_number, + block_evidence.agreed_block_hash, p_provider_a_id, p_provider_b_id, + p_runtime_code_hash_a::programmable_private.bytes32_value, + p_runtime_code_hash_b::programmable_private.bytes32_value, + p_runtime_code_hash_a::programmable_private.bytes32_value, + normalized_runtime_code_length, normalized_runtime_code_length, + normalized_runtime_code_length, + p_normalized_runtime_code_hash_a::programmable_private.bytes32_value, + p_normalized_runtime_code_hash_b::programmable_private.bytes32_value, + p_normalized_runtime_code_hash_a::programmable_private.bytes32_value, + p_immutable_references_commitment::programmable_private.bytes32_value, + p_immutable_values, + p_immutable_values_commitment::programmable_private.bytes32_value, + p_reconstructed_runtime_code_hash::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_evidence_commitment::programmable_private.bytes32_value, + p_run_id, p_verified_at, audit_id + ); + return p_runtime_code_evidence_id; +end +$function$; + +create function programmable_private.register_dynamic_source_attestation( + p_dynamic_source_attestation_id uuid, + p_run_id uuid, + p_dynamic_source_template_id uuid, + p_parent_factory_occurrence_id uuid, + p_deployed_source_address bytea, + p_deployment_block_number numeric, + p_runtime_code_evidence_id uuid, + p_deployed_artifact_creation_code_commitment bytea, + p_expected_immutable_values_commitment bytea, + p_factory_configuration_commitment bytea, + p_constructor_arguments_commitment bytea, + p_local_init_code_hash bytea, + p_runtime_code_hash bytea, + p_abi_event_set_commitment bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_attestation_commitment bytea, + p_created_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + parent programmable_private.chain_event_occurrences%rowtype; + parent_materialization programmable_private.chain_event_occurrence_materializations%rowtype; + parent_binding programmable_private.release_source_bindings%rowtype; + template programmable_private.release_dynamic_source_templates%rowtype; + code_evidence programmable_private.dual_rpc_runtime_code_evidence%rowtype; + existing programmable_private.dynamic_source_attestations%rowtype; + deployment_block bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'dynamic_attestation', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid dynamic source run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into template + from programmable_private.release_dynamic_source_templates + where dynamic_source_template_id = p_dynamic_source_template_id + and epoch_id = header.epoch_id; + select * into parent + from programmable_private.chain_event_occurrences + where occurrence_id = p_parent_factory_occurrence_id; + select * into parent_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_parent_factory_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if template.dynamic_source_template_id is null + or parent.occurrence_id is null + or parent_materialization.materialization_id is null + or parent.chain_id <> header.chain_id + or parent_materialization.release_id <> header.release_id + or parent_materialization.model_id <> header.model_id + or parent_materialization.source_group <> header.source_group + or parent_materialization.event_type <> template.factory_event_type + or not exists ( + select 1 + from programmable_private.chain_event_current_canonical as canonical + where canonical.occurrence_id = parent.occurrence_id + and canonical.logical_event_id = parent.logical_event_id + and canonical.block_hash = parent.block_hash + ) + then + raise exception using errcode = '23503', message = 'factory deployment occurrence is not current canonical'; + end if; + select * into code_evidence + from programmable_private.dual_rpc_runtime_code_evidence + where runtime_code_evidence_id = p_runtime_code_evidence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and source_address = p_deployed_source_address + and deployment_block_number = parent.block_number + and deployment_block_hash = parent.block_hash + and agreed_runtime_code_hash = p_runtime_code_hash + and agreed_runtime_code_length = template.runtime_code_length + and agreed_normalized_runtime_code_hash + = template.normalized_runtime_code_hash + and immutable_references_commitment + = template.immutable_references_commitment + and reconstructed_runtime_code_hash = agreed_runtime_code_hash + and immutable_values_commitment + = p_expected_immutable_values_commitment; + if not found then + raise exception using errcode = '23514', message = 'dynamic source lacks exact dual-RPC runtime code evidence'; + end if; + select * into parent_binding + from programmable_private.release_source_bindings + where binding_id = template.parent_factory_release_binding_id + and binding_id = parent_materialization.release_binding_id + and epoch_id = header.epoch_id + and source_role = template.parent_source_role + and source_address = parent.source_address + and binding_commitment = template.parent_factory_binding_commitment + and abi_event_set_commitment + = parent_materialization.abi_event_set_commitment + and inclusive_start_block <= parent.block_number; + if not found then + raise exception using errcode = '23514', message = 'factory occurrence lacks exact release binding'; + end if; + if p_deployment_block_number <> pg_catalog.trunc(p_deployment_block_number) + or p_deployment_block_number < 0 + or p_deployment_block_number > 9223372036854775807 + or p_deployment_block_number::bigint <> parent.block_number + or pg_catalog.octet_length(p_deployed_source_address) <> 20 + or pg_catalog.octet_length( + p_deployed_artifact_creation_code_commitment + ) <> 32 + or p_deployed_artifact_creation_code_commitment + <> template.deployed_artifact_creation_code_commitment + or pg_catalog.octet_length( + p_expected_immutable_values_commitment + ) <> 32 + or p_expected_immutable_values_commitment + <> code_evidence.immutable_values_commitment + or pg_catalog.octet_length(p_factory_configuration_commitment) <> 32 + or pg_catalog.lower( + parent_materialization.decoded_payload ->> + (template.immutable_binding_spec ->> 'factoryConfigurationField') + ) is distinct from + '0x' || pg_catalog.encode(p_factory_configuration_commitment, 'hex') + or not programmable_private.immutable_values_match_binding_spec( + template.immutable_binding_spec, + parent_materialization.decoded_payload, + p_deployed_source_address, + code_evidence.immutable_values + ) + or pg_catalog.octet_length(p_constructor_arguments_commitment) <> 32 + or pg_catalog.octet_length(p_local_init_code_hash) <> 32 + or p_local_init_code_hash + = template.deployed_artifact_creation_code_commitment + or pg_catalog.lower( + parent_materialization.decoded_payload ->> template.deployed_address_field + ) is distinct from + '0x' || pg_catalog.encode(p_deployed_source_address, 'hex') + or pg_catalog.octet_length(p_runtime_code_hash) <> 32 + or p_runtime_code_hash <> code_evidence.agreed_runtime_code_hash + or p_abi_event_set_commitment <> template.abi_event_set_commitment + or pg_catalog.octet_length(p_attestation_commitment) <> 32 + then + raise exception using errcode = '23514', message = 'dynamic source attestation does not match factory event and template'; + end if; + deployment_block := p_deployment_block_number::bigint; + select * into existing + from programmable_private.dynamic_source_attestations + where epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and deployed_source_address = p_deployed_source_address + and deployed_source_role = template.deployed_source_role; + if found then + if existing.dynamic_source_attestation_id <> p_dynamic_source_attestation_id + or existing.dynamic_source_template_id <> p_dynamic_source_template_id + or existing.runtime_code_evidence_id <> p_runtime_code_evidence_id + or existing.parent_factory_occurrence_id <> p_parent_factory_occurrence_id + or existing.parent_factory_release_binding_id <> parent_binding.binding_id + or existing.deployed_artifact_creation_code_commitment + <> p_deployed_artifact_creation_code_commitment + or existing.expected_immutable_values_commitment + <> p_expected_immutable_values_commitment + or existing.factory_configuration_commitment + <> p_factory_configuration_commitment + or existing.constructor_arguments_commitment + <> p_constructor_arguments_commitment + or existing.local_init_code_hash <> p_local_init_code_hash + or existing.runtime_code_hash <> p_runtime_code_hash + or existing.abi_event_set_commitment <> p_abi_event_set_commitment + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + or existing.attestation_commitment <> p_attestation_commitment + then + raise exception using errcode = '23505', message = 'dynamic source replay conflict'; + end if; + return existing.dynamic_source_attestation_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'dynamic_source_attestation.append', p_attestation_commitment, + p_run_id, p_created_at + ); + insert into programmable_private.dynamic_source_attestations ( + dynamic_source_attestation_id, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, dynamic_source_template_id, + runtime_code_evidence_id, + parent_factory_occurrence_id, parent_factory_release_binding_id, + parent_factory_binding_commitment, deployed_source_address, + deployed_source_role, deployment_block_number, + deployed_artifact_creation_code_commitment, + expected_immutable_values_commitment, + factory_configuration_commitment, + constructor_arguments_commitment, local_init_code_hash, runtime_code_hash, + abi_event_set_commitment, encoding_version, canonical_preimage, + content_fingerprint, attestation_commitment, verification_run_id, + created_at, created_by_audit_id + ) values ( + p_dynamic_source_attestation_id, header.chain_id, header.release_id, + header.model_id, header.source_group, header.epoch_id, + header.captured_pointer_generation, p_dynamic_source_template_id, + p_runtime_code_evidence_id, + parent.occurrence_id, parent_binding.binding_id, + parent_binding.binding_commitment, + p_deployed_source_address::programmable_private.eth_address, + template.deployed_source_role, + deployment_block::programmable_private.block_number_value, + p_deployed_artifact_creation_code_commitment::programmable_private.bytes32_value, + p_expected_immutable_values_commitment::programmable_private.bytes32_value, + p_factory_configuration_commitment::programmable_private.bytes32_value, + p_constructor_arguments_commitment::programmable_private.bytes32_value, + p_local_init_code_hash::programmable_private.bytes32_value, + p_runtime_code_hash::programmable_private.bytes32_value, + p_abi_event_set_commitment::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_attestation_commitment::programmable_private.bytes32_value, + p_run_id, p_created_at, audit_id + ); + return p_dynamic_source_attestation_id; +end +$function$; + +create function programmable_private.append_release_neutral_envio_candidate( + p_candidate_id text, + p_run_id uuid, + p_block_number numeric, + p_block_hash bytea, + p_transaction_hash bytea, + p_transaction_index numeric, + p_block_global_log_index numeric, + p_source_address bytea, + p_event_signature bytea, + p_event_type text, + p_ordered_topics bytea[], + p_raw_data bytea, + p_decoded_payload jsonb, + p_payload_hash bytea, + p_provider_cursor text, + p_provider_deployment_id uuid, + p_content_commitment bytea, + p_first_seen_at timestamptz default pg_catalog.clock_timestamp(), + p_stream_id text default 'canonical-events', + p_contract_name text default 'unclassified' +) +returns text +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + existing programmable_private.envio_candidate_inbox%rowtype; + normalized_block bigint; + normalized_tx_index bigint; + normalized_log_index bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion'; + if not found then + raise exception using errcode = '23503', message = 'invalid neutral ingestion run'; + end if; + if header.chain_id <> 1 + or header.release_id <> 'envio-control' + or header.model_id <> 'envio-control' + or header.source_group <> 'canonical-events' + or header.epoch_id <> '70000000-0000-0000-0000-000000000002' + or header.captured_pointer_generation <> 1 + then + raise exception using + errcode = '23514', + message = 'neutral Envio inbox requires the dedicated control scope'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + if not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_deployment_id + and provider_type = 'envio_deployment' + ) + or p_block_number <> pg_catalog.trunc(p_block_number) + or p_transaction_index <> pg_catalog.trunc(p_transaction_index) + or p_block_global_log_index <> pg_catalog.trunc(p_block_global_log_index) + or p_block_number < 0 or p_block_number > 9223372036854775807 + or p_transaction_index < 0 or p_transaction_index > 4294967295 + or p_block_global_log_index < 0 or p_block_global_log_index > 4294967295 + or pg_catalog.octet_length(p_block_hash) <> 32 + or pg_catalog.octet_length(p_transaction_hash) <> 32 + or pg_catalog.octet_length(p_source_address) <> 20 + or pg_catalog.octet_length(p_event_signature) <> 32 + or not programmable_private.valid_topics(p_ordered_topics) + or p_raw_data is null + or pg_catalog.octet_length(p_payload_hash) <> 32 + or pg_catalog.octet_length(p_content_commitment) <> 32 + or pg_catalog.octet_length(p_decoded_payload::text) > 65536 + or p_stream_id is null + or pg_catalog.octet_length(p_stream_id) not between 1 and 128 + or p_stream_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or p_contract_name is null + or pg_catalog.octet_length(p_contract_name) not between 1 and 128 + or p_contract_name !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or p_candidate_id is distinct from + programmable_private.derive_envio_candidate_id( + header.chain_id, p_block_hash, p_transaction_hash, + p_block_global_log_index + )::text + or p_provider_cursor is distinct from p_candidate_id + then + raise exception using errcode = '22023', message = 'invalid neutral Envio candidate'; + end if; + normalized_block := p_block_number::bigint; + normalized_tx_index := p_transaction_index::bigint; + normalized_log_index := p_block_global_log_index::bigint; + select * into existing + from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id; + if found then + if existing.chain_id <> header.chain_id + or existing.stream_id <> p_stream_id + or existing.block_number <> normalized_block + or existing.block_hash <> p_block_hash + or existing.transaction_hash <> p_transaction_hash + or existing.transaction_index <> normalized_tx_index + or existing.block_global_log_index <> normalized_log_index + or existing.source_address <> p_source_address + or existing.contract_name <> p_contract_name + or existing.event_signature <> p_event_signature + or existing.event_type <> p_event_type + or existing.ordered_topics <> p_ordered_topics + or existing.raw_data <> p_raw_data + or existing.decoded_payload <> p_decoded_payload + or existing.payload_hash <> p_payload_hash + or existing.provider_cursor <> p_provider_cursor + or existing.provider_deployment_id <> p_provider_deployment_id + or existing.content_commitment <> p_content_commitment + then + raise exception using errcode = '23505', message = 'neutral candidate replay changed immutable content'; + end if; + return existing.candidate_id; + end if; + insert into programmable_private.envio_candidate_inbox ( + candidate_id, chain_id, stream_id, block_number, block_hash, + transaction_hash, + transaction_index, block_global_log_index, source_address, + contract_name, event_signature, event_type, ordered_topics, raw_data, + decoded_payload, + payload_hash, provider_cursor, provider_deployment_id, first_seen_run_id, + first_seen_at, content_commitment + ) values ( + p_candidate_id::programmable_private.envio_candidate_identifier, + header.chain_id, p_stream_id::programmable_private.source_identifier, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + p_transaction_hash::programmable_private.bytes32_value, + normalized_tx_index::programmable_private.transaction_index_value, + normalized_log_index::programmable_private.block_log_index_value, + p_source_address::programmable_private.eth_address, + p_contract_name::programmable_private.source_identifier, + p_event_signature::programmable_private.bytes32_value, + p_event_type::programmable_private.source_identifier, + p_ordered_topics, p_raw_data, p_decoded_payload, + p_payload_hash::programmable_private.bytes32_value, + p_provider_cursor::programmable_private.envio_candidate_identifier, + p_provider_deployment_id, p_run_id, p_first_seen_at, + p_content_commitment::programmable_private.bytes32_value + ); + perform programmable_private.append_mutation_audit( + 'neutral_candidate.append', p_content_commitment, p_run_id, p_first_seen_at + ); + return p_candidate_id; +end +$function$; + +create function programmable_private.get_envio_ingestion_cursor_v1( + p_chain_id bigint, + p_provider_deployment_id uuid, + p_stream_id text +) +returns table ( + generation bigint, + block_number bigint, + block_hash bytea, + block_global_log_index bigint, + candidate_id text +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_chain_id <> 1 + or p_stream_id is null + or pg_catalog.octet_length(p_stream_id) not between 1 and 128 + or p_stream_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_deployment_id + and provider_type = 'envio_deployment' + ) + then + raise exception using errcode = '22023', message = 'invalid Envio cursor scope'; + end if; + return query + select current_cursor.generation, + current_cursor.block_number::bigint, + current_cursor.block_hash::bytea, + current_cursor.block_global_log_index::bigint, + current_cursor.candidate_id::text + from programmable_private.envio_ingestion_cursor_current as current_cursor + where current_cursor.chain_id = p_chain_id + and current_cursor.provider_deployment_id = p_provider_deployment_id + and current_cursor.stream_id = p_stream_id; + if not found then + return query select 0::bigint, null::bigint, null::bytea, + null::bigint, null::text; + end if; +end +$function$; + +create function programmable_private.advance_envio_ingestion_cursor_v1( + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_generation bigint, + p_next_generation bigint, + p_block_number numeric, + p_block_hash bytea, + p_block_global_log_index numeric, + p_candidate_id text, + p_page_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + candidate programmable_private.envio_candidate_inbox%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + normalized_block bigint; + normalized_log_index bigint; + history_id uuid := pg_catalog.gen_random_uuid(); + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or not exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id and status = 'succeeded' + ) then + raise exception using + errcode = '55000', + message = 'Envio cursor advance requires a succeeded neutral ingestion run'; + end if; + if p_expected_generation < 0 + or p_next_generation <> p_expected_generation + 1 + or p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 or p_block_number > 9223372036854775807 + or p_block_global_log_index <> pg_catalog.trunc(p_block_global_log_index) + or p_block_global_log_index < 0 + or p_block_global_log_index > 4294967295 + or pg_catalog.octet_length(p_block_hash) <> 32 + or pg_catalog.octet_length(p_page_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid Envio cursor CAS'; + end if; + normalized_block := p_block_number::bigint; + normalized_log_index := p_block_global_log_index::bigint; + select * into candidate + from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id + and chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and block_number = normalized_block + and block_hash = p_block_hash + and block_global_log_index = normalized_log_index; + if not found then + raise exception using + errcode = '23514', + message = 'Envio cursor does not match its durable final inbox row'; + end if; + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + for update; + if (found and current_cursor.generation <> p_expected_generation) + or (not found and p_expected_generation <> 0) + or ( + current_cursor.generation is not null + and (normalized_block, normalized_log_index, p_candidate_id) + <= ( + current_cursor.block_number::bigint, + current_cursor.block_global_log_index::bigint, + current_cursor.candidate_id::text + ) + ) + then + raise exception using errcode = '40001', message = 'Envio cursor CAS lost or did not advance'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'envio_cursor.advance', p_page_commitment, p_run_id, p_changed_at + ); + insert into programmable_private.envio_ingestion_cursor_history ( + cursor_history_id, chain_id, provider_deployment_id, stream_id, + generation, block_number, block_hash, block_global_log_index, + candidate_id, content_commitment, changed_by_run_id, changed_at, + audit_id, is_rewind, rewound_from_generation + ) values ( + history_id, 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, p_next_generation, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + normalized_log_index::programmable_private.block_log_index_value, + p_candidate_id::programmable_private.envio_candidate_identifier, + p_page_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, false, null + ); + if p_expected_generation = 0 then + insert into programmable_private.envio_ingestion_cursor_current ( + chain_id, provider_deployment_id, stream_id, generation, block_number, + block_hash, block_global_log_index, candidate_id, content_commitment, + changed_by_run_id, changed_at, audit_id, cursor_history_id + ) values ( + 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, p_next_generation, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + normalized_log_index::programmable_private.block_log_index_value, + p_candidate_id::programmable_private.envio_candidate_identifier, + p_page_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, history_id + ) on conflict (chain_id, provider_deployment_id, stream_id) do nothing; + else + update programmable_private.envio_ingestion_cursor_current + set generation = p_next_generation, + block_number = normalized_block, + block_hash = p_block_hash, + block_global_log_index = normalized_log_index, + candidate_id = p_candidate_id, + content_commitment = p_page_commitment, + changed_by_run_id = p_run_id, + changed_at = p_changed_at, + audit_id = created_audit_id, + cursor_history_id = history_id + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_expected_generation; + end if; + if not found then + raise exception using errcode = '40001', message = 'Envio cursor CAS lost'; + end if; + return p_next_generation; +end +$function$; + +create function programmable_private.list_envio_ingestion_cursor_ancestors_v1( + p_chain_id bigint, + p_provider_deployment_id uuid, + p_stream_id text, + p_limit integer +) +returns table ( + generation bigint, + block_number bigint, + block_hash bytea, + block_global_log_index bigint, + candidate_id text +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_chain_id <> 1 or p_limit < 1 or p_limit > 1000 then + raise exception using errcode = '22023', message = 'invalid Envio ancestor request'; + end if; + return query + select history.generation, history.block_number::bigint, + history.block_hash::bytea, + history.block_global_log_index::bigint, history.candidate_id::text + from programmable_private.envio_ingestion_cursor_history as history + where history.chain_id = p_chain_id + and history.provider_deployment_id = p_provider_deployment_id + and history.stream_id = p_stream_id + order by history.generation desc + limit p_limit; +end +$function$; + +create function programmable_private.rewind_envio_ingestion_cursor_v1( + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_generation bigint, + p_next_generation bigint, + p_target_history_generation bigint, + p_reason_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + target_cursor programmable_private.envio_ingestion_cursor_history%rowtype; + history_id uuid := pg_catalog.gen_random_uuid(); + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'rewind' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or not exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id and status = 'succeeded' + ) then + raise exception using + errcode = '55000', + message = 'Envio cursor rewind requires a succeeded neutral rewind run'; + end if; + if p_expected_generation < 1 + or p_next_generation <> p_expected_generation + 1 + or p_target_history_generation < 1 + or p_target_history_generation >= p_expected_generation + or pg_catalog.octet_length(p_reason_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid Envio rewind CAS'; + end if; + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + for update; + select * into target_cursor + from programmable_private.envio_ingestion_cursor_history + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_target_history_generation; + if current_cursor.generation is null + or current_cursor.generation <> p_expected_generation + or target_cursor.cursor_history_id is null + or not exists ( + select 1 from programmable_private.dual_rpc_block_evidence + where verification_run_id = p_run_id + and block_number = target_cursor.block_number + and agreed_block_hash = target_cursor.block_hash + ) + then + raise exception using + errcode = '40001', + message = 'Envio rewind target is stale or lacks dual-RPC evidence'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'envio_cursor.rewind', p_reason_commitment, p_run_id, p_changed_at + ); + insert into programmable_private.envio_ingestion_cursor_history ( + cursor_history_id, chain_id, provider_deployment_id, stream_id, + generation, block_number, block_hash, block_global_log_index, + candidate_id, content_commitment, changed_by_run_id, changed_at, + audit_id, is_rewind, rewound_from_generation + ) values ( + history_id, 1, p_provider_deployment_id, target_cursor.stream_id, + p_next_generation, target_cursor.block_number, target_cursor.block_hash, + target_cursor.block_global_log_index, target_cursor.candidate_id, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, true, p_expected_generation + ); + update programmable_private.envio_ingestion_cursor_current + set generation = p_next_generation, + block_number = target_cursor.block_number, + block_hash = target_cursor.block_hash, + block_global_log_index = target_cursor.block_global_log_index, + candidate_id = target_cursor.candidate_id, + content_commitment = p_reason_commitment, + changed_by_run_id = p_run_id, + changed_at = p_changed_at, + audit_id = created_audit_id, + cursor_history_id = history_id + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_expected_generation; + if not found then + raise exception using errcode = '40001', message = 'Envio rewind cursor CAS lost'; + end if; + return p_next_generation; +end +$function$; + +create function programmable_private.list_projector_candidate_page_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_after_block_number numeric, + p_after_block_global_log_index numeric, + p_after_candidate_id text, + p_limit integer, + p_now timestamptz +) +returns table ( + candidate_id text, + block_number bigint, + block_hash bytea, + transaction_hash bytea, + transaction_index bigint, + block_global_log_index bigint, + source_address bytea, + contract_name text, + event_signature bytea, + event_type text, + ordered_topics bytea[], + raw_data bytea, + decoded_payload jsonb, + payload_hash bytea, + provider_cursor text, + provider_deployment_id uuid, + content_commitment bytea, + status text, + attempt_count bigint, + next_attempt_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + normalized_after_block bigint; + normalized_after_log_index bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_current_epoch( + p_chain_id, p_release_id, p_model_id, p_source_group, + p_epoch_id, p_pointer_generation + ); + if p_limit < 1 or p_limit > 500 or p_now is null + or pg_catalog.octet_length(p_lease_token_hash) <> 32 + or ( + (p_after_block_number is null) + <> (p_after_block_global_log_index is null) + ) + or ((p_after_block_number is null) <> (p_after_candidate_id is null)) + or not exists ( + select 1 from programmable_private.projector_lease_current as lease + where lease.chain_id = p_chain_id + and lease.release_id = p_release_id + and lease.model_id = p_model_id + and lease.source_group = p_source_group + and lease.projector_version = p_projector_version + and lease.epoch_id = p_epoch_id + and lease.pointer_generation = p_pointer_generation + and lease.lease_generation = p_lease_generation + and lease.lease_token_hash = p_lease_token_hash + and lease.expires_at >= p_now + ) + then + raise exception using errcode = '40001', message = 'invalid or stale candidate-page lease'; + end if; + if p_after_block_number is not null then + if p_after_block_number <> pg_catalog.trunc(p_after_block_number) + or p_after_block_number < 0 + or p_after_block_number > 9223372036854775807 + or p_after_block_global_log_index + <> pg_catalog.trunc(p_after_block_global_log_index) + or p_after_block_global_log_index < 0 + or p_after_block_global_log_index > 4294967295 + or pg_catalog.octet_length( + p_after_candidate_id::programmable_private.envio_candidate_identifier + ) > 192 + then + raise exception using errcode = '22023', message = 'invalid candidate-page cursor'; + end if; + normalized_after_block := p_after_block_number::bigint; + normalized_after_log_index := p_after_block_global_log_index::bigint; + end if; + return query + select candidate.candidate_id::text, + candidate.block_number::bigint, candidate.block_hash::bytea, + candidate.transaction_hash::bytea, + candidate.transaction_index::bigint, + candidate.block_global_log_index::bigint, + candidate.source_address::bytea, candidate.contract_name::text, + candidate.event_signature::bytea, candidate.event_type::text, + candidate.ordered_topics, candidate.raw_data, + candidate.decoded_payload, candidate.payload_hash::bytea, + candidate.provider_cursor::text, candidate.provider_deployment_id, + candidate.content_commitment::bytea, + coalesce(current_status.status::text, 'pending'), + coalesce(current_status.attempt_count, 0::bigint), + current_status.next_attempt_at + from programmable_private.envio_candidate_inbox as candidate + left join programmable_private.envio_candidate_status_current as current_status + on current_status.candidate_id = candidate.candidate_id + and current_status.epoch_id = p_epoch_id + and current_status.pointer_generation = p_pointer_generation + where candidate.chain_id = p_chain_id + and ( + p_after_block_number is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + normalized_after_block, normalized_after_log_index, + p_after_candidate_id + ) + ) + and coalesce(current_status.status::text, 'pending') + not in ('resolved', 'ignored', 'quarantined') + and ( + current_status.status is distinct from 'deferred' + or current_status.next_attempt_at <= p_now + ) + order by candidate.block_number, candidate.block_global_log_index, + candidate.candidate_id + limit p_limit; +end +$function$; + +create function programmable_private.defer_envio_candidate_v1( + p_decision_id uuid, + p_run_id uuid, + p_candidate_id text, + p_expected_attempt bigint, + p_next_attempt bigint, + p_next_attempt_at timestamptz, + p_reason_code text, + p_reason_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + existing programmable_private.envio_candidate_status_history%rowtype; + current_status programmable_private.envio_candidate_status_current%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid candidate deferral run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into existing + from programmable_private.envio_candidate_status_history + where decision_id = p_decision_id; + if found then + if existing.candidate_id <> p_candidate_id + or existing.epoch_id <> header.epoch_id + or existing.pointer_generation <> header.captured_pointer_generation + or existing.status <> 'deferred' + or existing.attempt_count <> p_next_attempt + or existing.next_attempt_at <> p_next_attempt_at + or existing.reason_code <> p_reason_code + or existing.reason_commitment <> p_reason_commitment + or existing.changed_by_run_id <> p_run_id + or existing.changed_at <> p_changed_at + then + raise exception using errcode = '23505', message = 'candidate deferral replay conflict'; + end if; + return existing.decision_id; + end if; + if p_expected_attempt < 0 or p_next_attempt <> p_expected_attempt + 1 + or p_next_attempt_at <= p_changed_at + or p_reason_code is null + or pg_catalog.octet_length(p_reason_commitment) <> 32 + or not exists ( + select 1 from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id and chain_id = header.chain_id + ) + then + raise exception using errcode = '22023', message = 'invalid candidate deferral'; + end if; + select * into current_status + from programmable_private.envio_candidate_status_current + where candidate_id = p_candidate_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + for update; + if (current_status.candidate_id is null and p_expected_attempt <> 0) + or (current_status.candidate_id is not null and ( + current_status.attempt_count <> p_expected_attempt + or current_status.status in ('resolved', 'ignored', 'quarantined') + )) + then + raise exception using errcode = '40001', message = 'candidate attempt CAS lost'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'neutral_candidate.defer', p_reason_commitment, p_run_id, p_changed_at + ); + insert into programmable_private.envio_candidate_status_history ( + decision_id, candidate_id, chain_id, release_id, model_id, source_group, + epoch_id, pointer_generation, status, attempt_count, next_attempt_at, + reason_code, reason_commitment, changed_by_run_id, changed_at, audit_id + ) values ( + p_decision_id, + p_candidate_id::programmable_private.envio_candidate_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, 'deferred', + p_next_attempt, p_next_attempt_at, + p_reason_code::programmable_private.source_identifier, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id + ); + insert into programmable_private.envio_candidate_status_current ( + candidate_id, chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, status, attempt_count, next_attempt_at, reason_code, + reason_commitment, changed_by_run_id, changed_at, decision_id + ) values ( + p_candidate_id::programmable_private.envio_candidate_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, 'deferred', + p_next_attempt, p_next_attempt_at, + p_reason_code::programmable_private.source_identifier, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, p_decision_id + ) on conflict (candidate_id, epoch_id, pointer_generation) do update + set status = excluded.status, + attempt_count = excluded.attempt_count, + next_attempt_at = excluded.next_attempt_at, + reason_code = excluded.reason_code, + reason_commitment = excluded.reason_commitment, + changed_by_run_id = excluded.changed_by_run_id, + changed_at = excluded.changed_at, + decision_id = excluded.decision_id + where programmable_private.envio_candidate_status_current.attempt_count + = p_expected_attempt + and programmable_private.envio_candidate_status_current.status + not in ('resolved', 'ignored', 'quarantined'); + if not found then + raise exception using errcode = '40001', message = 'candidate deferral CAS lost'; + end if; + return p_decision_id; +end +$function$; + +create function programmable_private.append_envio_terminal_disposition( + p_decision_id uuid, + p_run_id uuid, + p_candidate_id text, + p_expected_attempt bigint, + p_status text, + p_reason_code text, + p_reason_commitment bytea, + p_changed_at timestamptz +) +returns uuid +language plpgsql +volatile +security invoker +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + existing programmable_private.envio_candidate_status_history%rowtype; + current_status programmable_private.envio_candidate_status_current%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid candidate disposition run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if p_status not in ('ignored', 'quarantined') + or p_expected_attempt < 0 + or p_reason_code is null + or pg_catalog.octet_length(p_reason_commitment) <> 32 + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + or not exists ( + select 1 from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id and chain_id = header.chain_id + ) + then + raise exception using errcode = '22023', message = 'invalid terminal candidate disposition'; + end if; + select * into existing + from programmable_private.envio_candidate_status_history + where decision_id = p_decision_id; + if found then + if existing.candidate_id <> p_candidate_id + or existing.epoch_id <> header.epoch_id + or existing.pointer_generation <> header.captured_pointer_generation + or existing.status::text <> p_status + or existing.attempt_count <> p_expected_attempt + or existing.reason_code <> p_reason_code + or existing.reason_commitment <> p_reason_commitment + or existing.changed_by_run_id <> p_run_id + or existing.changed_at <> p_changed_at + then + raise exception using errcode = '23505', message = 'terminal disposition replay conflict'; + end if; + return existing.decision_id; + end if; + select * into current_status + from programmable_private.envio_candidate_status_current + where candidate_id = p_candidate_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + for update; + if (current_status.candidate_id is null and p_expected_attempt <> 0) + or (current_status.candidate_id is not null and ( + current_status.attempt_count <> p_expected_attempt + or current_status.status in ('resolved', 'ignored', 'quarantined') + )) + then + raise exception using errcode = '40001', message = 'terminal disposition CAS lost'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'neutral_candidate.' || p_status, + p_reason_commitment, p_run_id, p_changed_at + ); + insert into programmable_private.envio_candidate_status_history ( + decision_id, candidate_id, chain_id, release_id, model_id, source_group, + epoch_id, pointer_generation, status, attempt_count, next_attempt_at, + reason_code, reason_commitment, changed_by_run_id, changed_at, audit_id + ) values ( + p_decision_id, + p_candidate_id::programmable_private.envio_candidate_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, + p_status::programmable_private.envio_candidate_status, + p_expected_attempt, null, + p_reason_code::programmable_private.source_identifier, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id + ); + insert into programmable_private.envio_candidate_status_current ( + candidate_id, chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, status, attempt_count, next_attempt_at, reason_code, + reason_commitment, changed_by_run_id, changed_at, decision_id + ) values ( + p_candidate_id::programmable_private.envio_candidate_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, + p_status::programmable_private.envio_candidate_status, + p_expected_attempt, null, + p_reason_code::programmable_private.source_identifier, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, p_decision_id + ) on conflict (candidate_id, epoch_id, pointer_generation) do update + set status = excluded.status, + next_attempt_at = null, + reason_code = excluded.reason_code, + reason_commitment = excluded.reason_commitment, + changed_by_run_id = excluded.changed_by_run_id, + changed_at = excluded.changed_at, + decision_id = excluded.decision_id + where programmable_private.envio_candidate_status_current.attempt_count + = p_expected_attempt + and programmable_private.envio_candidate_status_current.status + not in ('resolved', 'ignored', 'quarantined'); + if not found then + raise exception using errcode = '40001', message = 'terminal disposition CAS lost'; + end if; + return p_decision_id; +end +$function$; + +create function programmable_private.ignore_envio_candidate_v1( + p_decision_id uuid, + p_run_id uuid, + p_candidate_id text, + p_expected_attempt bigint, + p_reason_code text, + p_reason_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language sql +volatile +security definer +set search_path = '' +as $function$ + select programmable_private.append_envio_terminal_disposition( + p_decision_id, p_run_id, p_candidate_id, p_expected_attempt, + 'ignored', p_reason_code, p_reason_commitment, p_changed_at + ) +$function$; + +create function programmable_private.quarantine_envio_candidate_v1( + p_decision_id uuid, + p_run_id uuid, + p_candidate_id text, + p_expected_attempt bigint, + p_reason_code text, + p_reason_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language sql +volatile +security definer +set search_path = '' +as $function$ + select programmable_private.append_envio_terminal_disposition( + p_decision_id, p_run_id, p_candidate_id, p_expected_attempt, + 'quarantined', p_reason_code, p_reason_commitment, p_changed_at + ) +$function$; + +create function programmable_private.resolve_envio_candidate( + p_candidate_resolution_id uuid, + p_run_id uuid, + p_candidate_id text, + p_release_binding_id uuid, + p_dynamic_source_attestation_id uuid, + p_abi_event_set_commitment bytea, + p_resolution_commitment bytea, + p_resolved_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + candidate programmable_private.envio_candidate_inbox%rowtype; + binding programmable_private.release_source_bindings%rowtype; + dynamic_source programmable_private.dynamic_source_attestations%rowtype; + existing programmable_private.envio_candidate_resolutions%rowtype; + current_status programmable_private.envio_candidate_status_current%rowtype; + resolved_attempt bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid candidate resolution run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into candidate from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id; + if not found or candidate.chain_id <> header.chain_id then + raise exception using errcode = '23503', message = 'neutral candidate chain mismatch'; + end if; + if (p_release_binding_id is null) = (p_dynamic_source_attestation_id is null) + or pg_catalog.octet_length(p_abi_event_set_commitment) <> 32 + or pg_catalog.octet_length(p_resolution_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'resolution requires exactly one source provenance'; + end if; + if p_release_binding_id is not null then + select * into binding from programmable_private.release_source_bindings + where binding_id = p_release_binding_id + and epoch_id = header.epoch_id + and source_type = 'ethereum_contract' + and source_address = candidate.source_address + and inclusive_start_block <= candidate.block_number + and abi_event_set_commitment = p_abi_event_set_commitment; + if not found then + raise exception using errcode = '23514', message = 'candidate does not match exact release binding'; + end if; + else + select * into dynamic_source + from programmable_private.dynamic_source_attestations as attestation + where attestation.dynamic_source_attestation_id = p_dynamic_source_attestation_id + and attestation.chain_id = header.chain_id + and attestation.release_id = header.release_id + and attestation.model_id = header.model_id + and attestation.source_group = header.source_group + and attestation.epoch_id = header.epoch_id + and attestation.pointer_generation = header.captured_pointer_generation + and attestation.deployed_source_address = candidate.source_address + and attestation.deployment_block_number <= candidate.block_number + and attestation.abi_event_set_commitment = p_abi_event_set_commitment + and exists ( + select 1 + from programmable_private.chain_event_current_canonical as canonical + where canonical.occurrence_id = attestation.parent_factory_occurrence_id + ); + if not found then + raise exception using errcode = '23514', message = 'candidate does not match current dynamic source attestation'; + end if; + end if; + select * into existing + from programmable_private.envio_candidate_resolutions + where candidate_id = p_candidate_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if found then + if existing.candidate_resolution_id <> p_candidate_resolution_id + or existing.release_binding_id is distinct from p_release_binding_id + or existing.dynamic_source_attestation_id + is distinct from p_dynamic_source_attestation_id + or existing.abi_event_set_commitment <> p_abi_event_set_commitment + or existing.resolution_commitment <> p_resolution_commitment + or existing.resolved_by_run_id <> p_run_id + or existing.resolved_at <> p_resolved_at + or not exists ( + select 1 + from programmable_private.envio_candidate_status_current as status + where status.candidate_id = p_candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + and status.status = 'resolved' + and status.decision_id = p_candidate_resolution_id + ) + then + raise exception using errcode = '23505', message = 'candidate resolution replay conflict'; + end if; + return existing.candidate_resolution_id; + end if; + select * into current_status + from programmable_private.envio_candidate_status_current + where candidate_id = p_candidate_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + for update; + if current_status.status in ('resolved', 'ignored', 'quarantined') then + raise exception using + errcode = '40001', + message = 'terminal candidate disposition cannot be resolved'; + end if; + resolved_attempt := coalesce(current_status.attempt_count, 0::bigint); + audit_id := programmable_private.append_mutation_audit( + 'neutral_candidate.resolve', p_resolution_commitment, p_run_id, p_resolved_at + ); + insert into programmable_private.envio_candidate_resolutions ( + candidate_resolution_id, candidate_id, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, release_binding_id, + dynamic_source_attestation_id, abi_event_set_commitment, + resolution_commitment, resolved_by_run_id, resolved_at, + created_by_audit_id + ) values ( + p_candidate_resolution_id, p_candidate_id, header.chain_id, + header.release_id, header.model_id, header.source_group, header.epoch_id, + header.captured_pointer_generation, p_release_binding_id, + p_dynamic_source_attestation_id, + p_abi_event_set_commitment::programmable_private.bytes32_value, + p_resolution_commitment::programmable_private.bytes32_value, + p_run_id, p_resolved_at, audit_id + ); + insert into programmable_private.envio_candidate_status_history ( + decision_id, candidate_id, chain_id, release_id, model_id, source_group, + epoch_id, pointer_generation, status, attempt_count, next_attempt_at, + reason_code, reason_commitment, changed_by_run_id, changed_at, audit_id + ) values ( + p_candidate_resolution_id, + p_candidate_id::programmable_private.envio_candidate_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, 'resolved', + resolved_attempt, null, 'resolved', + p_resolution_commitment::programmable_private.bytes32_value, + p_run_id, p_resolved_at, audit_id + ); + insert into programmable_private.envio_candidate_status_current ( + candidate_id, chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, status, attempt_count, next_attempt_at, reason_code, + reason_commitment, changed_by_run_id, changed_at, decision_id + ) values ( + p_candidate_id::programmable_private.envio_candidate_identifier, + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation, 'resolved', + resolved_attempt, null, 'resolved', + p_resolution_commitment::programmable_private.bytes32_value, + p_run_id, p_resolved_at, p_candidate_resolution_id + ) on conflict (candidate_id, epoch_id, pointer_generation) do update + set status = excluded.status, + next_attempt_at = null, + reason_code = excluded.reason_code, + reason_commitment = excluded.reason_commitment, + changed_by_run_id = excluded.changed_by_run_id, + changed_at = excluded.changed_at, + decision_id = excluded.decision_id + where programmable_private.envio_candidate_status_current.attempt_count + = resolved_attempt + and programmable_private.envio_candidate_status_current.status + not in ('resolved', 'ignored', 'quarantined'); + if not found then + raise exception using errcode = '40001', message = 'candidate resolve CAS lost'; + end if; + return p_candidate_resolution_id; +end +$function$; + +create function programmable_private.append_chain_event_occurrence( + p_logical_event_id uuid, + p_occurrence_id uuid, + p_run_id uuid, + p_candidate_id text, + p_candidate_resolution_id uuid, + p_receipt_log_ordinal numeric, + p_block_timestamp timestamptz, + p_decoder_version text, + p_abi_event_set_commitment bytea, + p_block_evidence_id uuid, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + candidate programmable_private.envio_candidate_inbox%rowtype; + resolution programmable_private.envio_candidate_resolutions%rowtype; + evidence programmable_private.dual_rpc_block_evidence%rowtype; + identity programmable_private.chain_event_identities%rowtype; + existing programmable_private.chain_event_occurrences%rowtype; + materialization programmable_private.chain_event_occurrence_materializations%rowtype; + dynamic_source programmable_private.dynamic_source_attestations%rowtype; + ordinal bigint; + audit_id uuid; + status_id uuid; + occurrence_inserted boolean := false; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_fingerprint_encoding( + 'occurrence', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid neutral verification run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into candidate + from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id; + select * into resolution + from programmable_private.envio_candidate_resolutions + where candidate_resolution_id = p_candidate_resolution_id + and candidate_id = p_candidate_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and abi_event_set_commitment = p_abi_event_set_commitment; + if candidate.candidate_id is null or resolution.candidate_resolution_id is null then + raise exception using errcode = '23503', message = 'candidate has no exact current-scope resolution'; + end if; + if resolution.dynamic_source_attestation_id is not null then + select * into dynamic_source + from programmable_private.dynamic_source_attestations + where dynamic_source_attestation_id = resolution.dynamic_source_attestation_id; + if not found + or dynamic_source.deployed_source_address <> candidate.source_address + or dynamic_source.deployment_block_number > candidate.block_number + or dynamic_source.abi_event_set_commitment <> p_abi_event_set_commitment + or not exists ( + select 1 from programmable_private.chain_event_current_canonical + where occurrence_id = dynamic_source.parent_factory_occurrence_id + ) + then + raise exception using errcode = '23514', message = 'dynamic occurrence lost factory provenance'; + end if; + end if; + select * into evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id; + if not found + or evidence.epoch_id <> header.epoch_id + or evidence.pointer_generation <> header.captured_pointer_generation + or evidence.block_number <> candidate.block_number + or evidence.agreed_block_hash <> candidate.block_hash + then + raise exception using errcode = '23503', message = 'candidate lacks matching dual-RPC block evidence'; + end if; + if p_receipt_log_ordinal <> pg_catalog.trunc(p_receipt_log_ordinal) + or p_receipt_log_ordinal < 0 + or p_receipt_log_ordinal > 4294967295 + or pg_catalog.octet_length(p_abi_event_set_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid occurrence encoding or ordinal'; + end if; + ordinal := p_receipt_log_ordinal::bigint; + select * into identity + from programmable_private.chain_event_identities + where chain_id = header.chain_id + and transaction_hash = candidate.transaction_hash + and receipt_log_ordinal = ordinal + for share; + if found and identity.logical_event_id <> p_logical_event_id then + raise exception using errcode = '23505', message = 'logical identity UUID changed'; + elsif not found then + insert into programmable_private.chain_event_identities ( + logical_event_id, chain_id, transaction_hash, receipt_log_ordinal, + first_verification_run_id, created_at + ) values ( + p_logical_event_id, header.chain_id, candidate.transaction_hash, + ordinal::programmable_private.receipt_log_ordinal_value, + p_run_id, p_verified_at + ); + end if; + select * into existing + from programmable_private.chain_event_occurrences + where chain_id = header.chain_id + and transaction_hash = candidate.transaction_hash + and receipt_log_ordinal = ordinal + and block_hash = candidate.block_hash; + if found then + if existing.occurrence_id <> p_occurrence_id + or existing.logical_event_id <> p_logical_event_id + or existing.block_number <> candidate.block_number + or existing.block_timestamp <> p_block_timestamp + or existing.transaction_index <> candidate.transaction_index + or existing.source_address <> candidate.source_address + or existing.block_global_log_index <> candidate.block_global_log_index + or existing.event_signature <> candidate.event_signature + or existing.ordered_topics <> candidate.ordered_topics + or existing.raw_data <> candidate.raw_data + then + raise exception using errcode = '23505', message = 'neutral raw occurrence replay changed immutable chain data'; + end if; + else + insert into programmable_private.chain_event_occurrences ( + occurrence_id, logical_event_id, chain_id, transaction_hash, + receipt_log_ordinal, block_number, block_hash, block_timestamp, + transaction_index, source_address, block_global_log_index, + event_signature, event_type, ordered_topics, raw_data, decoded_payload, + payload_hash, decoder_version, abi_event_set_commitment, + release_binding_id, dynamic_source_attestation_id, release_id, model_id, + epoch_id, pointer_generation, first_seen_envio_candidate_id, + first_seen_neutral_candidate_id, candidate_resolution_id, + first_seen_provider_cursor, verification_run_id, block_evidence_id, + encoding_version, canonical_preimage, content_fingerprint, verified_at + ) values ( + p_occurrence_id, p_logical_event_id, header.chain_id, + candidate.transaction_hash, + ordinal::programmable_private.receipt_log_ordinal_value, + candidate.block_number, candidate.block_hash, p_block_timestamp, + candidate.transaction_index, candidate.source_address, + candidate.block_global_log_index, candidate.event_signature, + candidate.event_type, candidate.ordered_topics, candidate.raw_data, + candidate.decoded_payload, candidate.payload_hash, + p_decoder_version::programmable_private.projector_identifier, + p_abi_event_set_commitment::programmable_private.bytes32_value, + resolution.release_binding_id, resolution.dynamic_source_attestation_id, + header.release_id, header.model_id, header.epoch_id, + header.captured_pointer_generation, null, p_candidate_id, + p_candidate_resolution_id, candidate.provider_cursor, p_run_id, + p_block_evidence_id, p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, p_verified_at + ); + occurrence_inserted := true; + end if; + + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if found then + if materialization.chain_id <> header.chain_id + or materialization.release_id <> header.release_id + or materialization.model_id <> header.model_id + or materialization.source_group <> header.source_group + or materialization.release_binding_id + is distinct from resolution.release_binding_id + or materialization.dynamic_source_attestation_id + is distinct from resolution.dynamic_source_attestation_id + or materialization.first_seen_envio_candidate_id is not null + or materialization.first_seen_neutral_candidate_id <> p_candidate_id + or materialization.candidate_resolution_id <> p_candidate_resolution_id + or materialization.decoder_version <> p_decoder_version + or materialization.event_type <> candidate.event_type + or materialization.abi_event_set_commitment <> p_abi_event_set_commitment + or materialization.decoded_payload <> candidate.decoded_payload + or materialization.payload_hash <> candidate.payload_hash + or materialization.first_seen_provider_cursor <> candidate.provider_cursor + or materialization.verification_run_id <> p_run_id + or materialization.block_evidence_id <> p_block_evidence_id + or materialization.encoding_version <> p_encoding_version + or materialization.canonical_preimage <> p_canonical_preimage + or materialization.content_fingerprint <> p_content_fingerprint + then + raise exception using errcode = '23505', message = 'neutral occurrence materialization replay changed exact scope'; + end if; + return p_occurrence_id; + end if; + insert into programmable_private.chain_event_occurrence_materializations ( + materialization_id, occurrence_id, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, release_binding_id, + dynamic_source_attestation_id, first_seen_envio_candidate_id, + first_seen_neutral_candidate_id, candidate_resolution_id, + decoder_version, event_type, abi_event_set_commitment, + decoded_payload, payload_hash, + first_seen_provider_cursor, verification_run_id, block_evidence_id, + encoding_version, canonical_preimage, content_fingerprint, verified_at + ) values ( + p_candidate_resolution_id, p_occurrence_id, header.chain_id, + header.release_id, header.model_id, header.source_group, header.epoch_id, + header.captured_pointer_generation, resolution.release_binding_id, + resolution.dynamic_source_attestation_id, null, p_candidate_id, + p_candidate_resolution_id, + p_decoder_version::programmable_private.projector_identifier, + candidate.event_type, + p_abi_event_set_commitment::programmable_private.bytes32_value, + candidate.decoded_payload, candidate.payload_hash, candidate.provider_cursor, + p_run_id, p_block_evidence_id, p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, p_verified_at + ); + audit_id := programmable_private.append_mutation_audit( + case when occurrence_inserted then 'occurrence.append.resolved' + else 'occurrence.materialize.resolved' end, + p_content_fingerprint, p_run_id, p_verified_at + ); + if not occurrence_inserted then + return p_occurrence_id; + end if; + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.chain_event_occurrence_status_history ( + status_history_id, occurrence_id, logical_event_id, block_hash, status, + safe_head_observation_id, block_evidence_id, decision_run_id, + decision_commitment, decided_at, audit_id + ) values ( + status_id, p_occurrence_id, p_logical_event_id, candidate.block_hash, + 'observed', evidence.observation_id, p_block_evidence_id, p_run_id, + p_content_fingerprint::programmable_private.bytes32_value, + p_verified_at, audit_id + ); + return p_occurrence_id; +end +$function$; + +create function programmable_private.list_projector_checkpoint_ancestors_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_projector_version text, + p_limit integer +) +returns table ( + checkpoint_id uuid, + pointer_generation bigint, + checkpoint_generation bigint, + reorg_generation bigint, + block_number bigint, + block_hash bytea, + cursor_global_log_index bigint, + cursor_candidate_id text +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_chain_id <= 0 or p_limit < 1 or p_limit > 1000 then + raise exception using errcode = '22023', message = 'invalid checkpoint ancestor request'; + end if; + return query + select checkpoint.checkpoint_id, checkpoint.pointer_generation, + checkpoint.checkpoint_generation, checkpoint.reorg_generation, + checkpoint.block_number::bigint, checkpoint.block_hash::bytea, + checkpoint.cursor_block_global_log_index::bigint, + checkpoint.cursor_candidate_id::text + from programmable_private.projector_checkpoints as checkpoint + where checkpoint.chain_id = p_chain_id + and checkpoint.release_id = p_release_id + and checkpoint.model_id = p_model_id + and checkpoint.source_group = p_source_group + and checkpoint.projector_version = p_projector_version + order by checkpoint.checkpoint_generation desc + limit p_limit; +end +$function$; + +create function programmable_private.assert_open_projection_run_v1( + p_run_id uuid +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection'; + if not found or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'projection run is absent or terminal'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); +end +$function$; + +create function programmable_private.get_projector_launch_baseline_v1( + p_run_id uuid, + p_token bytea +) +returns table ( + launch_projection_id uuid, + token bytea, + creator bytea, + launch_transaction_hash bytea, + pool_id bytea, + reward_vault bytea, + launch_hash bytea, + token_name text, + token_symbol text, + total_supply numeric, + last_source_occurrence_id uuid, + pool_projection_id uuid, + currency0 bytea, + currency1 bytea, + pool_key_fee bigint, + tick_spacing integer, + hook bytea, + pool_last_source_occurrence_id uuid, + pool_fee_configuration_id uuid, + buy_swap_fee_bps integer, + sell_swap_fee_bps integer, + buy_creator_fee_bps integer, + sell_creator_fee_bps integer, + launcher_fee_bps integer, + transfer_tax_bps integer, + lp_fee_pips bigint, + disclosure_source_occurrence_id uuid, + custody_projection_id uuid, + custody_address bytea, + custody_mode smallint, + duration_days integer, + cliff_days integer, + custody_configuration_hash bytea, + custody_source_occurrence_id uuid +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_open_projection_run_v1(p_run_id); + if pg_catalog.octet_length(p_token) <> 20 then + raise exception using errcode = '22023', message = 'invalid baseline token'; + end if; + select * into header from programmable_private.run_headers + where run_id = p_run_id; + return query + select launch.launch_projection_id, launch.token::bytea, + launch.creator::bytea, launch.launch_transaction_hash::bytea, + launch.pool_id::bytea, launch.reward_vault::bytea, + launch.launch_hash::bytea, launch.token_name, launch.token_symbol, + launch.total_supply::numeric, launch.last_source_occurrence_id, + pool.pool_projection_id, pool.currency0::bytea, pool.currency1::bytea, + pool.pool_key_fee, pool.tick_spacing, pool.hook::bytea, + pool.last_source_occurrence_id, + fee.pool_fee_configuration_id, fee.buy_swap_fee_bps::integer, + fee.sell_swap_fee_bps::integer, fee.buy_creator_fee_bps::integer, + fee.sell_creator_fee_bps::integer, fee.launcher_fee_bps::integer, + fee.transfer_tax_bps::integer, fee.lp_fee_pips, + fee.disclosure_source_occurrence_id, + custody.custody_projection_id, custody.custody_address::bytea, + custody.custody_mode, custody.duration_days, custody.cliff_days, + custody.configuration_hash::bytea, custody.source_occurrence_id + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as launch_run + on launch_run.run_id = launch.projection_run_id + left join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + left join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = launch.projection_run_id + left join programmable_private.initial_buy_custody_projections as custody + on custody.launch_projection_id = launch.launch_projection_id + and custody.projection_run_id = launch.projection_run_id + where launch.chain_id = header.chain_id + and launch.release_id = header.release_id + and launch.model_id = header.model_id + and launch_run.source_group = header.source_group + and launch.token = p_token; +end +$function$; + +create function programmable_private.get_projector_pool_fee_total_v1( + p_run_id uuid, + p_pool_id bytea, + p_quote_asset bytea +) +returns table ( + gross_total numeric, + creator_fee_total numeric, + launcher_fee_total numeric, + last_source_occurrence_id uuid +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_open_projection_run_v1(p_run_id); + if pg_catalog.octet_length(p_pool_id) <> 32 + or (p_quote_asset is not null and pg_catalog.octet_length(p_quote_asset) <> 20) + then + raise exception using errcode = '22023', message = 'invalid pool fold key'; + end if; + select * into header from programmable_private.run_headers where run_id = p_run_id; + return query + select total.gross_total::numeric, total.creator_fee_total::numeric, + total.launcher_fee_total::numeric, total.last_source_occurrence_id + from programmable_private.current_pool_fee_totals_v1 as total + join programmable_private.run_headers as total_run + on total_run.run_id = total.projection_run_id + where total.chain_id = header.chain_id + and total.release_id = header.release_id + and total.model_id = header.model_id + and total_run.source_group = header.source_group + and total.pool_id = p_pool_id + and total.quote_asset is not distinct from p_quote_asset; +end +$function$; + +create function programmable_private.get_projector_vault_baseline_v1( + p_run_id uuid, + p_vault bytea +) +returns table ( + reward_vault_projection_id uuid, + launch_projection_id uuid, + vault bytea, + pool_id bytea, + quote_asset bytea, + configuration_hash bytea, + current_allocation_fact_id uuid, + last_source_occurrence_id uuid +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_open_projection_run_v1(p_run_id); + if pg_catalog.octet_length(p_vault) <> 20 then + raise exception using errcode = '22023', message = 'invalid vault fold key'; + end if; + select * into header from programmable_private.run_headers where run_id = p_run_id; + return query + select vault.reward_vault_projection_id, vault.launch_projection_id, + vault.vault::bytea, vault.pool_id::bytea, vault.quote_asset::bytea, + vault.configuration_hash::bytea, + vault.current_allocation_fact_id, vault.last_source_occurrence_id + from programmable_private.current_reward_vault_projections_v1 as vault + join programmable_private.run_headers as vault_run + on vault_run.run_id = vault.projection_run_id + where vault.chain_id = header.chain_id + and vault.release_id = header.release_id + and vault.model_id = header.model_id + and vault_run.source_group = header.source_group + and vault.vault = p_vault; +end +$function$; + +create function programmable_private.list_projector_vault_allocations_v1( + p_run_id uuid, + p_vault bytea +) +returns table ( + reward_allocation_projection_id uuid, + reward_vault_projection_id uuid, + allocation_fact_id uuid, + configuration_epoch bigint, + allocation_index integer, + beneficiary bytea, + payout_address bytea, + share_bps integer, + effective_from_block bigint, + effective_to_block bigint, + last_source_occurrence_id uuid +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_open_projection_run_v1(p_run_id); + if pg_catalog.octet_length(p_vault) <> 20 then + raise exception using errcode = '22023', message = 'invalid vault allocation key'; + end if; + select * into header from programmable_private.run_headers where run_id = p_run_id; + return query + select allocation.reward_allocation_projection_id, + allocation.reward_vault_projection_id, + allocation.allocation_fact_id, allocation.configuration_epoch, + allocation.allocation_index, allocation.beneficiary::bytea, + allocation.payout_address::bytea, allocation.share_bps::integer, + allocation.effective_from_block::bigint, + allocation.effective_to_block, + allocation.last_source_occurrence_id + from programmable_private.current_reward_vault_projections_v1 as vault + join programmable_private.run_headers as vault_run + on vault_run.run_id = vault.projection_run_id + join programmable_private.reward_allocation_projections as allocation + on allocation.reward_vault_projection_id = vault.reward_vault_projection_id + and allocation.projection_run_id = vault.projection_run_id + where vault.chain_id = header.chain_id + and vault.release_id = header.release_id + and vault.model_id = header.model_id + and vault_run.source_group = header.source_group + and vault.vault = p_vault + order by allocation.configuration_epoch, allocation.allocation_index, + allocation.beneficiary; +end +$function$; + +create function programmable_private.get_projector_account_reward_balance_v1( + p_run_id uuid, + p_vault bytea, + p_account bytea +) +returns table ( + claimable_accrued numeric, + claimed_total numeric, + last_source_occurrence_id uuid +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_open_projection_run_v1(p_run_id); + if pg_catalog.octet_length(p_vault) <> 20 + or pg_catalog.octet_length(p_account) <> 20 + then + raise exception using errcode = '22023', message = 'invalid account fold key'; + end if; + select * into header from programmable_private.run_headers where run_id = p_run_id; + return query + select balance.claimable_accrued::numeric, balance.claimed_total::numeric, + balance.last_source_occurrence_id + from programmable_private.current_account_reward_balances_v1 as balance + join programmable_private.run_headers as balance_run + on balance_run.run_id = balance.projection_run_id + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance_run.source_group = header.source_group + and balance.vault = p_vault + and balance.account = p_account; +end +$function$; + +create function programmable_private.get_projector_runtime_state_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_projector_version text, + p_provider_types text[], + p_provider_redacted_identities text[], + p_provider_deployment_commitments bytea[], + p_provider_schema_commitments bytea[] +) +returns table ( + epoch_id uuid, + pointer_generation bigint, + provider_deployment_ids uuid[], + provider_types text[], + provider_redacted_identities text[], + lease_generation bigint, + lease_holder_id text, + lease_acquired_at timestamptz, + lease_expires_at timestamptz, + checkpoint_id uuid, + checkpoint_generation bigint, + reorg_generation bigint, + checkpoint_block_number bigint, + checkpoint_block_hash bytea, + checkpoint_cursor_block_global_log_index bigint, + checkpoint_cursor_candidate_id text +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + current_epoch programmable_private.release_epoch_current%rowtype; + current_lease programmable_private.projector_lease_current%rowtype; + current_pointer programmable_private.projector_checkpoint_current%rowtype; + checkpoint programmable_private.projector_checkpoints%rowtype; + resolved_ids uuid[]; + resolved_types text[]; + resolved_identities text[]; + expected_count integer; +begin + perform programmable_private.assert_caller('programmable_projector'); + expected_count := coalesce(pg_catalog.array_length(p_provider_types, 1), 0); + if p_chain_id <= 0 + or expected_count < 1 or expected_count > 8 + or expected_count <> coalesce( + pg_catalog.array_length(p_provider_redacted_identities, 1), 0 + ) + or expected_count <> coalesce( + pg_catalog.array_length(p_provider_deployment_commitments, 1), 0 + ) + or expected_count <> coalesce( + pg_catalog.array_length(p_provider_schema_commitments, 1), 0 + ) + or exists ( + select 1 + from pg_catalog.unnest(p_provider_types) as provider_type + where provider_type not in ( + 'rpc_provider', 'envio_deployment', 'uniswap_subgraph' + ) + ) + or exists ( + select identity + from pg_catalog.unnest(p_provider_redacted_identities) as identity + group by identity having pg_catalog.count(*) <> 1 + ) + then + raise exception using errcode = '22023', message = 'invalid exact provider set'; + end if; + + select * into current_epoch + from programmable_private.release_epoch_current as pointer + where pointer.chain_id = p_chain_id + and pointer.release_id = p_release_id + and pointer.model_id = p_model_id + and pointer.source_group = p_source_group; + if not found then + raise exception using errcode = '23503', message = 'projector scope has no current epoch'; + end if; + + select + pg_catalog.array_agg(provider.provider_deployment_id order by requested.ordinality), + pg_catalog.array_agg(provider.provider_type::text order by requested.ordinality), + pg_catalog.array_agg(provider.redacted_identity::text order by requested.ordinality) + into resolved_ids, resolved_types, resolved_identities + from pg_catalog.generate_series(1, expected_count) as requested(ordinality) + join programmable_private.provider_deployments as provider + on provider.provider_type::text = p_provider_types[requested.ordinality] + and provider.redacted_identity = + p_provider_redacted_identities[requested.ordinality] + and provider.deployment_commitment = + p_provider_deployment_commitments[requested.ordinality] + and provider.schema_commitment = + p_provider_schema_commitments[requested.ordinality]; + if coalesce(pg_catalog.array_length(resolved_ids, 1), 0) <> expected_count then + raise exception using errcode = '23503', message = 'exact provider set is not registered'; + end if; + + select * into current_lease + from programmable_private.projector_lease_current as lease + where lease.chain_id = p_chain_id + and lease.release_id = p_release_id + and lease.model_id = p_model_id + and lease.source_group = p_source_group + and lease.projector_version = p_projector_version; + + select * into current_pointer + from programmable_private.projector_checkpoint_current as pointer + where pointer.chain_id = p_chain_id + and pointer.release_id = p_release_id + and pointer.model_id = p_model_id + and pointer.source_group = p_source_group + and pointer.projector_version = p_projector_version; + if found then + select * into checkpoint + from programmable_private.projector_checkpoints as stored + where stored.checkpoint_id = current_pointer.checkpoint_id; + if not found then + raise exception using errcode = '23503', message = 'current checkpoint identity is missing'; + end if; + end if; + + return query select + current_epoch.epoch_id, + current_epoch.generation, + resolved_ids, + resolved_types, + resolved_identities, + coalesce(current_lease.lease_generation, 0::bigint), + current_lease.holder_id::text, + current_lease.acquired_at, + current_lease.expires_at, + current_pointer.checkpoint_id, + coalesce(current_pointer.checkpoint_generation, 0::bigint), + coalesce(current_pointer.reorg_generation, 0::bigint), + checkpoint.block_number::bigint, + checkpoint.block_hash::bytea, + checkpoint.cursor_block_global_log_index::bigint, + checkpoint.cursor_candidate_id::text; +end +$function$; + +create function programmable_private.advance_projection_entity_current() +returns trigger +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +begin + insert into programmable_private.projection_entity_current as current_entity ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key, + projection_row_id, projection_run_id, publication_id, checkpoint_id, + promoted_block_number, promoted_block_hash, selected_at + ) + select + 'launch', launch.chain_id, launch.release_id, launch.model_id, + run.source_group, pg_catalog.encode(launch.token, 'hex'), + launch.launch_projection_id, launch.projection_run_id, + new.publication_id, new.checkpoint_id, launch.promoted_block_number, + launch.promoted_block_hash, new.published_at + from programmable_private.launch_projections as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + where launch.projection_run_id = new.run_id + on conflict ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key + ) do update set + projection_row_id = excluded.projection_row_id, + projection_run_id = excluded.projection_run_id, + publication_id = excluded.publication_id, + checkpoint_id = excluded.checkpoint_id, + promoted_block_number = excluded.promoted_block_number, + promoted_block_hash = excluded.promoted_block_hash, + selected_at = excluded.selected_at + where current_entity.promoted_block_number < excluded.promoted_block_number + or ( + current_entity.promoted_block_number = excluded.promoted_block_number + and current_entity.selected_at <= excluded.selected_at + ); + + insert into programmable_private.projection_entity_current as current_entity ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key, + projection_row_id, projection_run_id, publication_id, checkpoint_id, + promoted_block_number, promoted_block_hash, selected_at + ) + select + 'reward_vault', vault.chain_id, vault.release_id, vault.model_id, + run.source_group, pg_catalog.encode(vault.vault, 'hex'), + vault.reward_vault_projection_id, vault.projection_run_id, + new.publication_id, new.checkpoint_id, vault.promoted_block_number, + vault.promoted_block_hash, new.published_at + from programmable_private.reward_vault_projections as vault + join programmable_private.run_headers as run + on run.run_id = vault.projection_run_id + where vault.projection_run_id = new.run_id + on conflict ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key + ) do update set + projection_row_id = excluded.projection_row_id, + projection_run_id = excluded.projection_run_id, + publication_id = excluded.publication_id, + checkpoint_id = excluded.checkpoint_id, + promoted_block_number = excluded.promoted_block_number, + promoted_block_hash = excluded.promoted_block_hash, + selected_at = excluded.selected_at + where current_entity.promoted_block_number < excluded.promoted_block_number + or ( + current_entity.promoted_block_number = excluded.promoted_block_number + and current_entity.selected_at <= excluded.selected_at + ); + + insert into programmable_private.projection_entity_current as current_entity ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key, + projection_row_id, projection_run_id, publication_id, checkpoint_id, + promoted_block_number, promoted_block_hash, selected_at + ) + select + 'account_reward_balance', balance.chain_id, balance.release_id, + balance.model_id, run.source_group, + pg_catalog.encode(balance.account, 'hex') || ':' || + pg_catalog.encode(balance.vault, 'hex'), + balance.account_reward_balance_id, balance.projection_run_id, + new.publication_id, new.checkpoint_id, balance.promoted_block_number, + balance.promoted_block_hash, new.published_at + from programmable_private.account_reward_balances as balance + join programmable_private.run_headers as run + on run.run_id = balance.projection_run_id + where balance.projection_run_id = new.run_id + on conflict ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key + ) do update set + projection_row_id = excluded.projection_row_id, + projection_run_id = excluded.projection_run_id, + publication_id = excluded.publication_id, + checkpoint_id = excluded.checkpoint_id, + promoted_block_number = excluded.promoted_block_number, + promoted_block_hash = excluded.promoted_block_hash, + selected_at = excluded.selected_at + where current_entity.promoted_block_number < excluded.promoted_block_number + or ( + current_entity.promoted_block_number = excluded.promoted_block_number + and current_entity.selected_at <= excluded.selected_at + ); + + insert into programmable_private.projection_entity_current as current_entity ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key, + projection_row_id, projection_run_id, publication_id, checkpoint_id, + promoted_block_number, promoted_block_hash, selected_at + ) + select + 'pool_fee_total', fee_total.chain_id, fee_total.release_id, + fee_total.model_id, run.source_group, + pg_catalog.encode(fee_total.pool_id, 'hex') || ':' || + coalesce(pg_catalog.encode(fee_total.quote_asset, 'hex'), 'native'), + fee_total.pool_fee_total_id, fee_total.projection_run_id, + new.publication_id, new.checkpoint_id, fee_total.promoted_block_number, + fee_total.promoted_block_hash, new.published_at + from programmable_private.pool_fee_totals as fee_total + join programmable_private.run_headers as run + on run.run_id = fee_total.projection_run_id + where fee_total.projection_run_id = new.run_id + on conflict ( + entity_kind, chain_id, release_id, model_id, source_group, entity_key + ) do update set + projection_row_id = excluded.projection_row_id, + projection_run_id = excluded.projection_run_id, + publication_id = excluded.publication_id, + checkpoint_id = excluded.checkpoint_id, + promoted_block_number = excluded.promoted_block_number, + promoted_block_hash = excluded.promoted_block_hash, + selected_at = excluded.selected_at + where current_entity.promoted_block_number < excluded.promoted_block_number + or ( + current_entity.promoted_block_number = excluded.promoted_block_number + and current_entity.selected_at <= excluded.selected_at + ); + return new; +end +$function$; + +create trigger projection_publication_advance_entities +after insert on programmable_private.projection_publications +for each row execute function programmable_private.advance_projection_entity_current(); + +create function programmable_private.restore_projection_entity_after_delete() +returns trigger +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + old_source_group text; + old_entity_key text; + old_projection_row_id uuid; + was_current boolean; +begin + select source_group::text into old_source_group + from programmable_private.run_headers where run_id = old.projection_run_id; + if tg_argv[0] = 'launch' then + old_entity_key := pg_catalog.encode(old.token, 'hex'); + old_projection_row_id := old.launch_projection_id; + elsif tg_argv[0] = 'reward_vault' then + old_entity_key := pg_catalog.encode(old.vault, 'hex'); + old_projection_row_id := old.reward_vault_projection_id; + elsif tg_argv[0] = 'account_reward_balance' then + old_entity_key := pg_catalog.encode(old.account, 'hex') || ':' || + pg_catalog.encode(old.vault, 'hex'); + old_projection_row_id := old.account_reward_balance_id; + else + old_entity_key := pg_catalog.encode(old.pool_id, 'hex') || ':' || + coalesce(pg_catalog.encode(old.quote_asset, 'hex'), 'native'); + old_projection_row_id := old.pool_fee_total_id; + end if; + delete from programmable_private.projection_entity_current + where entity_kind = tg_argv[0] + and chain_id = old.chain_id + and release_id = old.release_id + and model_id = old.model_id + and source_group = old_source_group + and entity_key = old_entity_key + and projection_row_id = old_projection_row_id; + was_current := found; + if not was_current then + return old; + end if; + + if tg_argv[0] = 'launch' then + insert into programmable_private.projection_entity_current + select 'launch', launch.chain_id, launch.release_id, launch.model_id, + run.source_group, old_entity_key, launch.launch_projection_id, + launch.projection_run_id, publication.publication_id, + publication.checkpoint_id, launch.promoted_block_number, + launch.promoted_block_hash, publication.published_at + from programmable_private.launch_projections as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + join programmable_private.projection_publications as publication + on publication.run_id = launch.projection_run_id + where launch.chain_id = old.chain_id + and launch.release_id = old.release_id + and launch.model_id = old.model_id + and run.source_group = old_source_group + and launch.token = old.token + order by launch.promoted_block_number desc, publication.published_at desc + limit 1; + elsif tg_argv[0] = 'reward_vault' then + insert into programmable_private.projection_entity_current + select 'reward_vault', vault.chain_id, vault.release_id, vault.model_id, + run.source_group, old_entity_key, vault.reward_vault_projection_id, + vault.projection_run_id, publication.publication_id, + publication.checkpoint_id, vault.promoted_block_number, + vault.promoted_block_hash, publication.published_at + from programmable_private.reward_vault_projections as vault + join programmable_private.run_headers as run + on run.run_id = vault.projection_run_id + join programmable_private.projection_publications as publication + on publication.run_id = vault.projection_run_id + where vault.chain_id = old.chain_id and vault.release_id = old.release_id + and vault.model_id = old.model_id and run.source_group = old_source_group + and vault.vault = old.vault + order by vault.promoted_block_number desc, publication.published_at desc + limit 1; + elsif tg_argv[0] = 'account_reward_balance' then + insert into programmable_private.projection_entity_current + select 'account_reward_balance', balance.chain_id, balance.release_id, + balance.model_id, run.source_group, old_entity_key, + balance.account_reward_balance_id, balance.projection_run_id, + publication.publication_id, publication.checkpoint_id, + balance.promoted_block_number, balance.promoted_block_hash, + publication.published_at + from programmable_private.account_reward_balances as balance + join programmable_private.run_headers as run + on run.run_id = balance.projection_run_id + join programmable_private.projection_publications as publication + on publication.run_id = balance.projection_run_id + where balance.chain_id = old.chain_id and balance.release_id = old.release_id + and balance.model_id = old.model_id and run.source_group = old_source_group + and balance.account = old.account and balance.vault = old.vault + order by balance.promoted_block_number desc, publication.published_at desc + limit 1; + else + insert into programmable_private.projection_entity_current + select 'pool_fee_total', fee_total.chain_id, fee_total.release_id, + fee_total.model_id, run.source_group, old_entity_key, + fee_total.pool_fee_total_id, fee_total.projection_run_id, + publication.publication_id, publication.checkpoint_id, + fee_total.promoted_block_number, fee_total.promoted_block_hash, + publication.published_at + from programmable_private.pool_fee_totals as fee_total + join programmable_private.run_headers as run + on run.run_id = fee_total.projection_run_id + join programmable_private.projection_publications as publication + on publication.run_id = fee_total.projection_run_id + where fee_total.chain_id = old.chain_id and fee_total.release_id = old.release_id + and fee_total.model_id = old.model_id and run.source_group = old_source_group + and fee_total.pool_id = old.pool_id + and fee_total.quote_asset is not distinct from old.quote_asset + order by fee_total.promoted_block_number desc, publication.published_at desc + limit 1; + end if; + return old; +end +$function$; + +create trigger launch_projection_restore_current +after delete on programmable_private.launch_projections +for each row execute function programmable_private.restore_projection_entity_after_delete('launch'); +create trigger reward_vault_projection_restore_current +after delete on programmable_private.reward_vault_projections +for each row execute function programmable_private.restore_projection_entity_after_delete('reward_vault'); +create trigger account_reward_balance_restore_current +after delete on programmable_private.account_reward_balances +for each row execute function programmable_private.restore_projection_entity_after_delete('account_reward_balance'); +create trigger pool_fee_total_restore_current +after delete on programmable_private.pool_fee_totals +for each row execute function programmable_private.restore_projection_entity_after_delete('pool_fee_total'); + +revoke all on function programmable_private.get_projector_runtime_state_v1( + bigint, text, text, text, text, text[], text[], bytea[], bytea[] +) from public; +grant execute on function programmable_private.get_projector_runtime_state_v1( + bigint, text, text, text, text, text[], text[], bytea[], bytea[] +) to programmable_projector; + +grant execute on function programmable_private.derive_envio_candidate_id( + bigint, bytea, bytea, numeric +) to programmable_projector; + +revoke all on function programmable_private.append_release_dynamic_source_template( + uuid, uuid, uuid, text, text, text, text, bytea, bytea, bytea, jsonb, + bytea, numeric, bytea, bytea, timestamptz +) from public; +grant execute on function programmable_private.append_release_dynamic_source_template( + uuid, uuid, uuid, text, text, text, text, bytea, bytea, bytea, jsonb, + bytea, numeric, bytea, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_dual_rpc_runtime_code_evidence( + uuid, uuid, bytea, uuid, uuid, uuid, bytea, bytea, numeric, numeric, + bytea, bytea, bytea, bytea[], bytea, bytea, smallint, bytea, bytea, bytea, + timestamptz +) from public; +grant execute on function programmable_private.append_dual_rpc_runtime_code_evidence( + uuid, uuid, bytea, uuid, uuid, uuid, bytea, bytea, numeric, numeric, + bytea, bytea, bytea, bytea[], bytea, bytea, smallint, bytea, bytea, bytea, + timestamptz +) to programmable_projector; +revoke all on function programmable_private.register_dynamic_source_attestation( + uuid, uuid, uuid, uuid, bytea, numeric, uuid, bytea, bytea, bytea, bytea, + bytea, bytea, bytea, smallint, bytea, bytea, bytea, timestamptz +) from public; +grant execute on function programmable_private.register_dynamic_source_attestation( + uuid, uuid, uuid, uuid, bytea, numeric, uuid, bytea, bytea, bytea, bytea, + bytea, bytea, bytea, smallint, bytea, bytea, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_release_neutral_envio_candidate( + text, uuid, numeric, bytea, bytea, numeric, numeric, bytea, bytea, text, + bytea[], bytea, jsonb, bytea, text, uuid, bytea, timestamptz, text, text +) from public; +grant execute on function programmable_private.append_release_neutral_envio_candidate( + text, uuid, numeric, bytea, bytea, numeric, numeric, bytea, bytea, text, + bytea[], bytea, jsonb, bytea, text, uuid, bytea, timestamptz, text, text +) to programmable_projector; + +revoke all on function programmable_private.get_envio_ingestion_cursor_v1( + bigint, uuid, text +) from public; +grant execute on function programmable_private.get_envio_ingestion_cursor_v1( + bigint, uuid, text +) to programmable_projector; +revoke all on function programmable_private.advance_envio_ingestion_cursor_v1( + uuid, uuid, text, bigint, bigint, numeric, bytea, numeric, text, bytea, + timestamptz +) from public; +grant execute on function programmable_private.advance_envio_ingestion_cursor_v1( + uuid, uuid, text, bigint, bigint, numeric, bytea, numeric, text, bytea, + timestamptz +) to programmable_projector; +revoke all on function programmable_private.list_envio_ingestion_cursor_ancestors_v1( + bigint, uuid, text, integer +) from public; +grant execute on function programmable_private.list_envio_ingestion_cursor_ancestors_v1( + bigint, uuid, text, integer +) to programmable_projector; +revoke all on function programmable_private.rewind_envio_ingestion_cursor_v1( + uuid, uuid, text, bigint, bigint, bigint, bytea, timestamptz +) from public; +grant execute on function programmable_private.rewind_envio_ingestion_cursor_v1( + uuid, uuid, text, bigint, bigint, bigint, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.list_projector_candidate_page_v1( + bigint, text, text, text, uuid, bigint, text, bigint, bytea, numeric, + numeric, text, integer, timestamptz +) from public; +grant execute on function programmable_private.list_projector_candidate_page_v1( + bigint, text, text, text, uuid, bigint, text, bigint, bytea, numeric, + numeric, text, integer, timestamptz +) to programmable_projector; +revoke all on function programmable_private.defer_envio_candidate_v1( + uuid, uuid, text, bigint, bigint, timestamptz, text, bytea, timestamptz +) from public; +grant execute on function programmable_private.defer_envio_candidate_v1( + uuid, uuid, text, bigint, bigint, timestamptz, text, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.ignore_envio_candidate_v1( + uuid, uuid, text, bigint, text, bytea, timestamptz +) from public; +grant execute on function programmable_private.ignore_envio_candidate_v1( + uuid, uuid, text, bigint, text, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.quarantine_envio_candidate_v1( + uuid, uuid, text, bigint, text, bytea, timestamptz +) from public; +grant execute on function programmable_private.quarantine_envio_candidate_v1( + uuid, uuid, text, bigint, text, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.resolve_envio_candidate( + uuid, uuid, text, uuid, uuid, bytea, bytea, timestamptz +) from public; +grant execute on function programmable_private.resolve_envio_candidate( + uuid, uuid, text, uuid, uuid, bytea, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_envio_terminal_disposition( + uuid, uuid, text, bigint, text, text, bytea, timestamptz +) from public; + +revoke all on function programmable_private.list_projector_checkpoint_ancestors_v1( + bigint, text, text, text, text, integer +) from public; +grant execute on function programmable_private.list_projector_checkpoint_ancestors_v1( + bigint, text, text, text, text, integer +) to programmable_projector; +revoke all on function programmable_private.get_projector_launch_baseline_v1( + uuid, bytea +) from public; +grant execute on function programmable_private.get_projector_launch_baseline_v1( + uuid, bytea +) to programmable_projector; +revoke all on function programmable_private.get_projector_pool_fee_total_v1( + uuid, bytea, bytea +) from public; +grant execute on function programmable_private.get_projector_pool_fee_total_v1( + uuid, bytea, bytea +) to programmable_projector; +revoke all on function programmable_private.get_projector_vault_baseline_v1( + uuid, bytea +) from public; +grant execute on function programmable_private.get_projector_vault_baseline_v1( + uuid, bytea +) to programmable_projector; +revoke all on function programmable_private.list_projector_vault_allocations_v1( + uuid, bytea +) from public; +grant execute on function programmable_private.list_projector_vault_allocations_v1( + uuid, bytea +) to programmable_projector; +revoke all on function programmable_private.get_projector_account_reward_balance_v1( + uuid, bytea, bytea +) from public; +grant execute on function programmable_private.get_projector_account_reward_balance_v1( + uuid, bytea, bytea +) to programmable_projector; +revoke all on function programmable_private.assert_open_projection_run_v1( + uuid +) from public; +revoke all on function programmable_private.append_chain_event_occurrence( + uuid, uuid, uuid, text, uuid, numeric, timestamptz, text, bytea, uuid, + smallint, bytea, bytea, timestamptz +) from public; +grant execute on function programmable_private.append_chain_event_occurrence( + uuid, uuid, uuid, text, uuid, numeric, timestamptz, text, bytea, uuid, + smallint, bytea, bytea, timestamptz +) to programmable_projector; + +revoke all on function programmable_private.append_release_projection_event_rule( + uuid, uuid, text, text, text, bytea, timestamptz +) from public; +grant execute on function programmable_private.append_release_projection_event_rule( + uuid, uuid, text, text, text, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_release_launch_requirement( + uuid, uuid, integer, text, text, text, bytea, timestamptz +) from public; +grant execute on function programmable_private.append_release_launch_requirement( + uuid, uuid, integer, text, text, text, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.assert_projection_event_allowed( + uuid, uuid, text +) from public; +grant execute on function programmable_private.assert_projection_event_allowed( + uuid, uuid, text +) to programmable_projector; +revoke all on function programmable_private.stage_launch_occurrence_role( + uuid, text, uuid, timestamptz +) from public; +grant execute on function programmable_private.stage_launch_occurrence_role( + uuid, text, uuid, timestamptz +) to programmable_projector; +revoke all on function programmable_private.stage_launch_projection_conditions( + uuid, boolean, timestamptz +) from public; +grant execute on function programmable_private.stage_launch_projection_conditions( + uuid, boolean, timestamptz +) to programmable_projector; +revoke all on function programmable_private.stage_pool_fee_configuration_v2( + uuid, uuid, uuid, numeric, numeric, numeric, numeric, numeric, numeric, + numeric, uuid, numeric, bytea, timestamptz +) from public; +grant execute on function programmable_private.stage_pool_fee_configuration_v2( + uuid, uuid, uuid, numeric, numeric, numeric, numeric, numeric, numeric, + numeric, uuid, numeric, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_creator_hook_claim_fact( + uuid, uuid, uuid, bytea, bytea, bytea, bytea, bytea, bytea, numeric, + timestamptz +) from public; +grant execute on function programmable_private.append_creator_hook_claim_fact( + uuid, uuid, uuid, bytea, bytea, bytea, bytea, bytea, bytea, numeric, + timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_launcher_hook_claim_fact( + uuid, uuid, uuid, bytea, bytea, bytea, bytea, numeric, timestamptz +) from public; +grant execute on function programmable_private.append_launcher_hook_claim_fact( + uuid, uuid, uuid, bytea, bytea, bytea, bytea, numeric, timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_creator_fee_checkpoint_fact( + uuid, uuid, uuid, bytea, numeric, numeric, numeric, timestamptz +) from public; +grant execute on function programmable_private.append_creator_fee_checkpoint_fact( + uuid, uuid, uuid, bytea, numeric, numeric, numeric, timestamptz +) to programmable_projector; +revoke all on function programmable_private.append_reward_configuration_activation_fact( + uuid, uuid, uuid, bytea, bytea, numeric, bytea, bytea, bytea[], numeric[], + numeric, timestamptz +) from public; +grant execute on function programmable_private.append_reward_configuration_activation_fact( + uuid, uuid, uuid, bytea, bytea, numeric, bytea, bytea, bytea[], numeric[], + numeric, timestamptz +) to programmable_projector; +revoke all on function programmable_private.event_fact_context( + uuid, uuid, text +) from public; +revoke all on function programmable_private.enforce_projection_event_rule() + from public; +revoke all on function programmable_private.enforce_launch_publication_completeness() + from public; +revoke all on function programmable_private.advance_projection_entity_current() + from public; +revoke all on function programmable_private.restore_projection_entity_after_delete() + from public; + +reset role; diff --git a/supabase/migrations/20260731000800_expanded_p0_closure.sql b/supabase/migrations/20260731000800_expanded_p0_closure.sql new file mode 100644 index 00000000..b6ac8f78 --- /dev/null +++ b/supabase/migrations/20260731000800_expanded_p0_closure.sql @@ -0,0 +1,3452 @@ +-- Expanded P0 closure: byte-complete provider evidence, atomic neutral +-- ingestion commits, an evidence-backed genesis rewind target, and the +-- remaining route-parity read-model facts. + +set role programmable_migrator; + +-- Runtime evidence must retain the exact bytes obtained independently from +-- both providers as well as the immutable-reconstructed bytecode. Hashes and +-- commitments alone are insufficient for later replay or codec verification. +alter table programmable_private.dual_rpc_runtime_code_evidence + add column runtime_code_a bytea, + add column runtime_code_b bytea, + add column reconstructed_runtime_code bytea; + +alter table programmable_private.dual_rpc_runtime_code_evidence + alter column runtime_code_a set not null, + alter column runtime_code_b set not null, + alter column reconstructed_runtime_code set not null, + add constraint dual_rpc_runtime_code_exact_bytes_check check ( + runtime_code_a = runtime_code_b + and runtime_code_a = reconstructed_runtime_code + and pg_catalog.octet_length(runtime_code_a) = runtime_code_length_a + and pg_catalog.octet_length(runtime_code_b) = runtime_code_length_b + and pg_catalog.octet_length(reconstructed_runtime_code) + = agreed_runtime_code_length + ); + +drop function programmable_private.append_dual_rpc_runtime_code_evidence( + uuid, uuid, bytea, uuid, uuid, uuid, bytea, bytea, numeric, numeric, + bytea, bytea, bytea, bytea[], bytea, bytea, smallint, bytea, bytea, bytea, + timestamptz +); + +create function programmable_private.append_dual_rpc_runtime_code_evidence( + p_runtime_code_evidence_id uuid, + p_run_id uuid, + p_source_address bytea, + p_deployment_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_runtime_code_hash_a bytea, + p_runtime_code_hash_b bytea, + p_runtime_code_a bytea, + p_runtime_code_b bytea, + p_runtime_code_length_a numeric, + p_runtime_code_length_b numeric, + p_normalized_runtime_code_hash_a bytea, + p_normalized_runtime_code_hash_b bytea, + p_immutable_references_commitment bytea, + p_immutable_values bytea[], + p_immutable_values_commitment bytea, + p_reconstructed_runtime_code bytea, + p_reconstructed_runtime_code_hash bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_evidence_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + block_evidence programmable_private.dual_rpc_block_evidence%rowtype; + existing programmable_private.dual_rpc_runtime_code_evidence%rowtype; + normalized_runtime_code_length bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'runtime_code', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using + errcode = '23503', message = 'invalid runtime-code verification run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into block_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_deployment_block_evidence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found + or p_provider_a_id = p_provider_b_id + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_a_id + and provider_type = 'rpc_provider' + ) + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_b_id + and provider_type = 'rpc_provider' + ) + or not exists ( + select 1 + from programmable_private.safe_head_observations as observation + where observation.observation_id = block_evidence.observation_id + and observation.epoch_id = block_evidence.epoch_id + and observation.pointer_generation = block_evidence.pointer_generation + and observation.provider_a_id = p_provider_a_id + and observation.provider_b_id = p_provider_b_id + ) + or pg_catalog.octet_length(p_source_address) <> 20 + or pg_catalog.octet_length(p_runtime_code_hash_a) <> 32 + or p_runtime_code_hash_a <> p_runtime_code_hash_b + or p_runtime_code_a is null + or p_runtime_code_b is null + or p_runtime_code_a <> p_runtime_code_b + or p_runtime_code_length_a is null + or p_runtime_code_length_b is null + or p_runtime_code_length_a <> pg_catalog.trunc(p_runtime_code_length_a) + or p_runtime_code_length_b <> pg_catalog.trunc(p_runtime_code_length_b) + or p_runtime_code_length_a <> p_runtime_code_length_b + or p_runtime_code_length_a <= 0 + or p_runtime_code_length_a > 16777216 + or pg_catalog.octet_length(p_runtime_code_a) <> p_runtime_code_length_a + or pg_catalog.octet_length(p_runtime_code_b) <> p_runtime_code_length_b + or pg_catalog.octet_length(p_normalized_runtime_code_hash_a) <> 32 + or p_normalized_runtime_code_hash_a <> p_normalized_runtime_code_hash_b + or pg_catalog.octet_length(p_immutable_references_commitment) <> 32 + or not programmable_private.valid_immutable_values(p_immutable_values) + or pg_catalog.octet_length(p_immutable_values_commitment) <> 32 + or p_reconstructed_runtime_code is null + or p_reconstructed_runtime_code <> p_runtime_code_a + or pg_catalog.octet_length(p_reconstructed_runtime_code) + <> p_runtime_code_length_a + or pg_catalog.octet_length(p_reconstructed_runtime_code_hash) <> 32 + or p_reconstructed_runtime_code_hash <> p_runtime_code_hash_a + or pg_catalog.octet_length(p_evidence_commitment) <> 32 + then + raise exception using + errcode = '23514', + message = 'runtime code lacks byte-complete dual-RPC deployment-block evidence'; + end if; + normalized_runtime_code_length := p_runtime_code_length_a::bigint; + select * into existing + from programmable_private.dual_rpc_runtime_code_evidence + where epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and source_address = p_source_address + and deployment_block_number = block_evidence.block_number; + if found then + if existing.runtime_code_evidence_id <> p_runtime_code_evidence_id + or existing.deployment_block_evidence_id + <> p_deployment_block_evidence_id + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.agreed_runtime_code_hash <> p_runtime_code_hash_a + or existing.runtime_code_a <> p_runtime_code_a + or existing.runtime_code_b <> p_runtime_code_b + or existing.agreed_runtime_code_length + <> normalized_runtime_code_length + or existing.agreed_normalized_runtime_code_hash + <> p_normalized_runtime_code_hash_a + or existing.immutable_references_commitment + <> p_immutable_references_commitment + or existing.immutable_values <> p_immutable_values + or existing.immutable_values_commitment + <> p_immutable_values_commitment + or existing.reconstructed_runtime_code + <> p_reconstructed_runtime_code + or existing.reconstructed_runtime_code_hash + <> p_reconstructed_runtime_code_hash + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + or existing.evidence_commitment <> p_evidence_commitment + then + raise exception using + errcode = '23505', message = 'runtime code evidence replay conflict'; + end if; + return existing.runtime_code_evidence_id; + end if; + audit_id := programmable_private.append_mutation_audit( + 'runtime_code_evidence.append', p_evidence_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.dual_rpc_runtime_code_evidence ( + runtime_code_evidence_id, chain_id, release_id, model_id, source_group, + epoch_id, pointer_generation, source_address, + deployment_block_evidence_id, deployment_block_number, + deployment_block_hash, provider_a_id, provider_b_id, + runtime_code_hash_a, runtime_code_hash_b, agreed_runtime_code_hash, + runtime_code_a, runtime_code_b, + runtime_code_length_a, runtime_code_length_b, + agreed_runtime_code_length, normalized_runtime_code_hash_a, + normalized_runtime_code_hash_b, agreed_normalized_runtime_code_hash, + immutable_references_commitment, immutable_values, + immutable_values_commitment, reconstructed_runtime_code, + reconstructed_runtime_code_hash, encoding_version, canonical_preimage, + content_fingerprint, evidence_commitment, verification_run_id, + verified_at, created_by_audit_id + ) values ( + p_runtime_code_evidence_id, header.chain_id, header.release_id, + header.model_id, header.source_group, header.epoch_id, + header.captured_pointer_generation, + p_source_address::programmable_private.eth_address, + block_evidence.block_evidence_id, block_evidence.block_number, + block_evidence.agreed_block_hash, p_provider_a_id, p_provider_b_id, + p_runtime_code_hash_a::programmable_private.bytes32_value, + p_runtime_code_hash_b::programmable_private.bytes32_value, + p_runtime_code_hash_a::programmable_private.bytes32_value, + p_runtime_code_a, p_runtime_code_b, + normalized_runtime_code_length, normalized_runtime_code_length, + normalized_runtime_code_length, + p_normalized_runtime_code_hash_a::programmable_private.bytes32_value, + p_normalized_runtime_code_hash_b::programmable_private.bytes32_value, + p_normalized_runtime_code_hash_a::programmable_private.bytes32_value, + p_immutable_references_commitment::programmable_private.bytes32_value, + p_immutable_values, + p_immutable_values_commitment::programmable_private.bytes32_value, + p_reconstructed_runtime_code, + p_reconstructed_runtime_code_hash::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_evidence_commitment::programmable_private.bytes32_value, + p_run_id, p_verified_at, audit_id + ); + return p_runtime_code_evidence_id; +end +$function$; + +-- A cursor is never allowed to infer completeness from Envio alone. Each +-- advancing page carries a bounded, independently queried dual-RPC log set; +-- both provider sets and the durable inbox set must match exactly and in +-- canonical order. +insert into programmable_private.provider_evidence_encoding_subtypes ( + evidence_subtype, encoding_version, subtype_tag, frame_prefix, + definition_commitment +) values ( + 'log_coverage', 2, 5, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320005', + 'hex' + ), + pg_catalog.decode( + '4ab7460cb321503613935191917c46872c9e3c9a681b2d4b349b6187f4dc0aec', + 'hex' + ) +); + +create table programmable_private.dual_rpc_log_coverage_evidence ( + log_coverage_evidence_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + stream_id programmable_private.source_identifier not null, + expected_cursor_generation bigint not null + check (expected_cursor_generation >= 0), + next_cursor_generation bigint not null + check (next_cursor_generation = expected_cursor_generation + 1), + previous_block_number programmable_private.block_number_value, + -- The shared block_log_index_value domain deliberately rejects NULL. A + -- genesis predecessor has no log ordinal, so this nullable field uses the + -- base type plus the same unsigned-32-bit bound instead. + previous_block_global_log_index bigint + check ( + previous_block_global_log_index is null + or previous_block_global_log_index between 0 and 4294967295 + ), + previous_candidate_id programmable_private.envio_candidate_identifier, + from_block_number programmable_private.block_number_value not null, + to_block_number programmable_private.block_number_value not null, + final_block_hash programmable_private.bytes32_value not null, + final_block_global_log_index programmable_private.block_log_index_value not null, + final_candidate_id programmable_private.envio_candidate_identifier not null, + safe_head_observation_id uuid not null + references programmable_private.safe_head_observations(observation_id) + on delete restrict, + final_block_evidence_id uuid not null, + provider_a_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + filter_commitment programmable_private.bytes32_value not null, + ordered_log_commitments_a bytea[] not null, + ordered_log_commitments_b bytea[] not null, + ordered_inbox_commitments bytea[] not null, + page_commitment programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version = 2), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + evidence_commitment programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key ( + final_block_evidence_id, safe_head_observation_id, epoch_id, chain_id, + pointer_generation + ) references programmable_private.dual_rpc_block_evidence( + block_evidence_id, observation_id, epoch_id, chain_id, pointer_generation + ) on delete restrict, + foreign key (final_block_evidence_id, final_block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check (provider_a_id <> provider_b_id), + check ( + from_block_number <= to_block_number + and to_block_number - from_block_number <= 1999 + ), + check ( + pg_catalog.cardinality(ordered_log_commitments_a) between 1 and 2000 + and programmable_private.valid_topics(ordered_log_commitments_a) + and ordered_log_commitments_a = ordered_log_commitments_b + and ordered_log_commitments_a = ordered_inbox_commitments + ), + check ( + (expected_cursor_generation = 0 + and previous_block_number is not null + and previous_block_global_log_index is null + and previous_candidate_id is null) + or + (expected_cursor_generation > 0 + and previous_block_number is not null + and ( + (previous_block_global_log_index is null + and previous_candidate_id is null) + or + (previous_block_global_log_index is not null + and previous_candidate_id is not null) + )) + ), + unique ( + chain_id, provider_deployment_id, stream_id, next_cursor_generation + ), + unique (verification_run_id, evidence_commitment) +); + +create table programmable_private.envio_ingestion_cursor_genesis_points ( + genesis_point_id uuid primary key, + chain_id programmable_private.chain_id_value not null check (chain_id = 1), + provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + stream_id programmable_private.source_identifier not null, + anchor_block_evidence_id uuid not null, + anchor_block_number programmable_private.block_number_value not null, + anchor_block_hash programmable_private.bytes32_value not null, + content_commitment programmable_private.bytes32_value not null, + registered_by_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + registered_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (anchor_block_evidence_id, anchor_block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) on delete restrict, + unique (chain_id, provider_deployment_id, stream_id) +); + +alter table programmable_private.envio_ingestion_cursor_history + alter column block_global_log_index type bigint + using block_global_log_index::bigint, + alter column block_global_log_index drop not null, + alter column candidate_id drop not null, + add column is_genesis boolean not null default false, + add column genesis_point_id uuid + references programmable_private.envio_ingestion_cursor_genesis_points( + genesis_point_id + ) on delete restrict, + add column log_coverage_evidence_id uuid + references programmable_private.dual_rpc_log_coverage_evidence( + log_coverage_evidence_id + ) on delete restrict; + +alter table programmable_private.envio_ingestion_cursor_history + add constraint envio_cursor_history_log_index_check check ( + block_global_log_index is null + or block_global_log_index between 0 and 4294967295 + ), + add constraint envio_cursor_history_point_shape_check check ( + (is_genesis and genesis_point_id is not null + and block_global_log_index is null and candidate_id is null + and log_coverage_evidence_id is null) + or + (not is_genesis and genesis_point_id is null + and block_global_log_index is not null and candidate_id is not null + and ((is_rewind and log_coverage_evidence_id is null) + or (not is_rewind and log_coverage_evidence_id is not null))) + ); + +alter table programmable_private.envio_ingestion_cursor_current + alter column block_global_log_index type bigint + using block_global_log_index::bigint, + alter column block_global_log_index drop not null, + alter column candidate_id drop not null, + add column is_genesis boolean not null default false, + add column is_rewind boolean not null default false, + add column genesis_point_id uuid + references programmable_private.envio_ingestion_cursor_genesis_points( + genesis_point_id + ) on delete restrict, + add column log_coverage_evidence_id uuid + references programmable_private.dual_rpc_log_coverage_evidence( + log_coverage_evidence_id + ) on delete restrict; + +alter table programmable_private.envio_ingestion_cursor_current + add constraint envio_cursor_current_log_index_check check ( + block_global_log_index is null + or block_global_log_index between 0 and 4294967295 + ), + add constraint envio_cursor_current_point_shape_check check ( + (is_genesis and genesis_point_id is not null + and block_global_log_index is null and candidate_id is null + and log_coverage_evidence_id is null) + or + (not is_genesis and genesis_point_id is null + and block_global_log_index is not null and candidate_id is not null + and ((is_rewind and log_coverage_evidence_id is null) + or (not is_rewind and log_coverage_evidence_id is not null))) + ); + +create type programmable_private.envio_candidate_page_item_v1 as ( + candidate_id text, + block_number numeric, + block_hash bytea, + transaction_hash bytea, + transaction_index numeric, + block_global_log_index numeric, + source_address bytea, + event_signature bytea, + event_type text, + ordered_topics bytea[], + raw_data bytea, + decoded_payload jsonb, + payload_hash bytea, + provider_cursor text, + content_commitment bytea, + first_seen_at timestamptz, + contract_name text +); + +create function programmable_private.register_envio_ingestion_genesis_v1( + p_genesis_point_id uuid, + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_anchor_block_evidence_id uuid, + p_content_commitment bytea, + p_registered_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + evidence programmable_private.dual_rpc_block_evidence%rowtype; + existing programmable_private.envio_ingestion_cursor_genesis_points%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'rewind') + and chain_id = 1 + and release_id = 'envio-control' + and model_id = 'envio-control' + and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found + or not exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id and status = 'succeeded' + ) + or p_genesis_point_id is null + or pg_catalog.octet_length(p_content_commitment) <> 32 + or p_stream_id is null + or pg_catalog.octet_length(p_stream_id) not between 1 and 128 + or p_stream_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_deployment_id + and provider_type = 'envio_deployment' + ) + then + raise exception using + errcode = '23514', message = 'invalid succeeded neutral genesis run'; + end if; + select * into evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_anchor_block_evidence_id + and chain_id = 1 + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and verification_run_id = p_run_id; + if not found then + raise exception using + errcode = '23514', message = 'genesis anchor lacks same-run dual-RPC evidence'; + end if; + select * into existing + from programmable_private.envio_ingestion_cursor_genesis_points + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id; + if found then + if existing.genesis_point_id <> p_genesis_point_id + or existing.anchor_block_evidence_id <> p_anchor_block_evidence_id + or existing.anchor_block_number <> evidence.block_number + or existing.anchor_block_hash <> evidence.agreed_block_hash + or existing.content_commitment <> p_content_commitment + or existing.registered_by_run_id <> p_run_id + or existing.registered_at <> p_registered_at + then + raise exception using + errcode = '23505', message = 'Envio genesis point replay conflict'; + end if; + return existing.genesis_point_id; + end if; + if exists ( + select 1 from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + ) then + raise exception using + errcode = '55000', message = 'Envio genesis must precede cursor history'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'envio_cursor.genesis.register', p_content_commitment, + p_run_id, p_registered_at + ); + insert into programmable_private.envio_ingestion_cursor_genesis_points ( + genesis_point_id, chain_id, provider_deployment_id, stream_id, + anchor_block_evidence_id, anchor_block_number, anchor_block_hash, + content_commitment, registered_by_run_id, registered_at, audit_id + ) values ( + p_genesis_point_id, 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, + evidence.block_evidence_id, evidence.block_number, + evidence.agreed_block_hash, + p_content_commitment::programmable_private.bytes32_value, + p_run_id, p_registered_at, created_audit_id + ); + return p_genesis_point_id; +end +$function$; + +create function programmable_private.append_dual_rpc_log_coverage_evidence( + p_log_coverage_evidence_id uuid, + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_cursor_generation bigint, + p_next_cursor_generation bigint, + p_from_block_number numeric, + p_to_block_number numeric, + p_final_block_hash bytea, + p_final_block_global_log_index numeric, + p_final_candidate_id text, + p_safe_head_observation_id uuid, + p_final_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_filter_commitment bytea, + p_ordered_log_commitments_a bytea[], + p_ordered_log_commitments_b bytea[], + p_page_commitment bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_evidence_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + genesis programmable_private.envio_ingestion_cursor_genesis_points%rowtype; + evidence programmable_private.dual_rpc_block_evidence%rowtype; + observation programmable_private.safe_head_observations%rowtype; + final_candidate programmable_private.envio_candidate_inbox%rowtype; + existing programmable_private.dual_rpc_log_coverage_evidence%rowtype; + normalized_from bigint; + normalized_to bigint; + normalized_final_log bigint; + previous_block bigint; + previous_log bigint; + previous_candidate text; + inbox_commitments bytea[]; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'log_coverage', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using + errcode = '55000', message = 'log coverage requires an open neutral ingestion run'; + end if; + if p_expected_cursor_generation < 0 + or p_next_cursor_generation <> p_expected_cursor_generation + 1 + or p_from_block_number <> pg_catalog.trunc(p_from_block_number) + or p_to_block_number <> pg_catalog.trunc(p_to_block_number) + or p_from_block_number < 0 + or p_to_block_number < p_from_block_number + or p_to_block_number - p_from_block_number > 1999 + or p_final_block_global_log_index + <> pg_catalog.trunc(p_final_block_global_log_index) + or p_final_block_global_log_index < 0 + or p_final_block_global_log_index > 4294967295 + or pg_catalog.octet_length(p_final_block_hash) <> 32 + or pg_catalog.octet_length(p_filter_commitment) <> 32 + or pg_catalog.octet_length(p_page_commitment) <> 32 + or pg_catalog.octet_length(p_evidence_commitment) <> 32 + or pg_catalog.cardinality(p_ordered_log_commitments_a) + not between 1 and 2000 + or not programmable_private.valid_topics(p_ordered_log_commitments_a) + or p_ordered_log_commitments_a <> p_ordered_log_commitments_b + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_deployment_id + and provider_type = 'envio_deployment' + ) + then + raise exception using + errcode = '22023', message = 'invalid bounded dual-RPC log coverage'; + end if; + normalized_from := p_from_block_number::bigint; + normalized_to := p_to_block_number::bigint; + normalized_final_log := p_final_block_global_log_index::bigint; + + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + for share; + if current_cursor.generation is null then + if p_expected_cursor_generation <> 0 then + raise exception using errcode = '40001', message = 'log-coverage cursor CAS lost'; + end if; + select * into genesis + from programmable_private.envio_ingestion_cursor_genesis_points + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id; + if not found or normalized_from <> genesis.anchor_block_number + 1 then + raise exception using + errcode = '23514', message = 'log coverage does not start after registered genesis'; + end if; + previous_block := genesis.anchor_block_number; + previous_log := null; + previous_candidate := null; + else + if current_cursor.generation <> p_expected_cursor_generation then + raise exception using errcode = '40001', message = 'log-coverage cursor CAS lost'; + end if; + previous_block := current_cursor.block_number; + previous_log := current_cursor.block_global_log_index; + previous_candidate := current_cursor.candidate_id; + if (current_cursor.is_genesis and normalized_from <> previous_block + 1) + or (not current_cursor.is_genesis and normalized_from <> previous_block) + then + raise exception using + errcode = '23514', message = 'log coverage does not continue the current cursor'; + end if; + end if; + + select * into final_candidate + from programmable_private.envio_candidate_inbox + where candidate_id = p_final_candidate_id + and chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and block_number = normalized_to + and block_hash = p_final_block_hash + and block_global_log_index = normalized_final_log; + if not found then + raise exception using + errcode = '23514', message = 'log coverage final candidate is not durable'; + end if; + + select * into evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_final_block_evidence_id + and observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and chain_id = 1 + and block_number = normalized_to + and agreed_block_hash = p_final_block_hash; + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and chain_id = 1 + and provider_a_id = p_provider_a_id + and provider_b_id = p_provider_b_id + and safe_block_number >= normalized_to; + if evidence.block_evidence_id is null or observation.observation_id is null then + raise exception using + errcode = '23514', message = 'log coverage lacks exact dual-RPC range evidence'; + end if; + + select pg_catalog.array_agg( + candidate.content_commitment::bytea + order by candidate.block_number, candidate.block_global_log_index, + candidate.candidate_id + ) into inbox_commitments + from programmable_private.envio_candidate_inbox as candidate + where candidate.chain_id = 1 + and candidate.provider_deployment_id = p_provider_deployment_id + and candidate.stream_id = p_stream_id + and candidate.block_number between normalized_from and normalized_to + and ( + previous_candidate is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > (previous_block, previous_log, previous_candidate) + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (normalized_to, normalized_final_log, p_final_candidate_id); + if coalesce(inbox_commitments, array[]::bytea[]) + is distinct from p_ordered_log_commitments_a + then + raise exception using + errcode = '23514', + message = 'Envio inbox omits or changes a dual-RPC-covered log'; + end if; + + select * into existing + from programmable_private.dual_rpc_log_coverage_evidence + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and next_cursor_generation = p_next_cursor_generation; + if found then + if existing.log_coverage_evidence_id <> p_log_coverage_evidence_id + or existing.verification_run_id <> p_run_id + or existing.expected_cursor_generation <> p_expected_cursor_generation + or existing.previous_block_number <> previous_block + or existing.previous_block_global_log_index is distinct from previous_log + or existing.previous_candidate_id is distinct from previous_candidate + or existing.from_block_number <> normalized_from + or existing.to_block_number <> normalized_to + or existing.final_block_hash <> p_final_block_hash + or existing.final_block_global_log_index <> normalized_final_log + or existing.final_candidate_id <> p_final_candidate_id + or existing.safe_head_observation_id <> p_safe_head_observation_id + or existing.final_block_evidence_id <> p_final_block_evidence_id + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.filter_commitment <> p_filter_commitment + or existing.ordered_log_commitments_a <> p_ordered_log_commitments_a + or existing.page_commitment <> p_page_commitment + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + or existing.evidence_commitment <> p_evidence_commitment + then + raise exception using + errcode = '23505', message = 'log coverage evidence replay conflict'; + end if; + return existing.log_coverage_evidence_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'dual_rpc_log_coverage.append', p_evidence_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.dual_rpc_log_coverage_evidence ( + log_coverage_evidence_id, chain_id, epoch_id, pointer_generation, + provider_deployment_id, stream_id, + expected_cursor_generation, next_cursor_generation, + previous_block_number, previous_block_global_log_index, + previous_candidate_id, from_block_number, to_block_number, + final_block_hash, final_block_global_log_index, final_candidate_id, + safe_head_observation_id, final_block_evidence_id, + provider_a_id, provider_b_id, filter_commitment, + ordered_log_commitments_a, ordered_log_commitments_b, + ordered_inbox_commitments, page_commitment, + encoding_version, canonical_preimage, content_fingerprint, + evidence_commitment, verification_run_id, verified_at, + created_by_audit_id + ) values ( + p_log_coverage_evidence_id, 1, header.epoch_id, + header.captured_pointer_generation, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, + p_expected_cursor_generation, p_next_cursor_generation, + previous_block::programmable_private.block_number_value, + previous_log, + previous_candidate::programmable_private.envio_candidate_identifier, + normalized_from::programmable_private.block_number_value, + normalized_to::programmable_private.block_number_value, + p_final_block_hash::programmable_private.bytes32_value, + normalized_final_log::programmable_private.block_log_index_value, + p_final_candidate_id::programmable_private.envio_candidate_identifier, + p_safe_head_observation_id, p_final_block_evidence_id, + p_provider_a_id, p_provider_b_id, + p_filter_commitment::programmable_private.bytes32_value, + p_ordered_log_commitments_a, p_ordered_log_commitments_b, + inbox_commitments, + p_page_commitment::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_evidence_commitment::programmable_private.bytes32_value, + p_run_id, p_verified_at, created_audit_id + ); + return p_log_coverage_evidence_id; +end +$function$; + +create or replace function programmable_private.advance_envio_ingestion_cursor_v1( + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_generation bigint, + p_next_generation bigint, + p_block_number numeric, + p_block_hash bytea, + p_block_global_log_index numeric, + p_candidate_id text, + p_page_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + candidate programmable_private.envio_candidate_inbox%rowtype; + coverage programmable_private.dual_rpc_log_coverage_evidence%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + normalized_block bigint; + normalized_log_index bigint; + history_id uuid := pg_catalog.gen_random_uuid(); + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or not exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id and status = 'succeeded' + ) then + raise exception using + errcode = '55000', + message = 'Envio cursor advance requires a succeeded neutral ingestion run'; + end if; + if p_expected_generation < 0 + or p_next_generation <> p_expected_generation + 1 + or p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 or p_block_number > 9223372036854775807 + or p_block_global_log_index <> pg_catalog.trunc(p_block_global_log_index) + or p_block_global_log_index < 0 + or p_block_global_log_index > 4294967295 + or pg_catalog.octet_length(p_block_hash) <> 32 + or pg_catalog.octet_length(p_page_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid Envio cursor CAS'; + end if; + normalized_block := p_block_number::bigint; + normalized_log_index := p_block_global_log_index::bigint; + select * into candidate + from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id + and chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and block_number = normalized_block + and block_hash = p_block_hash + and block_global_log_index = normalized_log_index; + select * into coverage + from programmable_private.dual_rpc_log_coverage_evidence + where verification_run_id = p_run_id + and chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and expected_cursor_generation = p_expected_generation + and next_cursor_generation = p_next_generation + and to_block_number = normalized_block + and final_block_hash = p_block_hash + and final_block_global_log_index = normalized_log_index + and final_candidate_id = p_candidate_id + and page_commitment = p_page_commitment; + if candidate.candidate_id is null or coverage.log_coverage_evidence_id is null then + raise exception using + errcode = '23514', + message = 'Envio cursor lacks durable inbox or exact dual-RPC log coverage'; + end if; + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + for update; + if (found and current_cursor.generation <> p_expected_generation) + or (not found and p_expected_generation <> 0) + or ( + current_cursor.generation is not null + and ( + (current_cursor.is_genesis + and normalized_block <= current_cursor.block_number) + or + (not current_cursor.is_genesis + and (normalized_block, normalized_log_index, p_candidate_id) + <= ( + current_cursor.block_number::bigint, + current_cursor.block_global_log_index::bigint, + current_cursor.candidate_id::text + )) + ) + ) + then + raise exception using + errcode = '40001', message = 'Envio cursor CAS lost or did not advance'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'envio_cursor.advance', p_page_commitment, p_run_id, p_changed_at + ); + insert into programmable_private.envio_ingestion_cursor_history ( + cursor_history_id, chain_id, provider_deployment_id, stream_id, + generation, block_number, block_hash, block_global_log_index, + candidate_id, content_commitment, changed_by_run_id, changed_at, + audit_id, is_rewind, rewound_from_generation, is_genesis, + genesis_point_id, log_coverage_evidence_id + ) values ( + history_id, 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, p_next_generation, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + normalized_log_index::programmable_private.block_log_index_value, + p_candidate_id::programmable_private.envio_candidate_identifier, + p_page_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, false, null, false, null, + coverage.log_coverage_evidence_id + ); + if p_expected_generation = 0 then + insert into programmable_private.envio_ingestion_cursor_current ( + chain_id, provider_deployment_id, stream_id, generation, block_number, + block_hash, block_global_log_index, candidate_id, content_commitment, + changed_by_run_id, changed_at, audit_id, cursor_history_id, + is_genesis, is_rewind, genesis_point_id, log_coverage_evidence_id + ) values ( + 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, p_next_generation, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + normalized_log_index::programmable_private.block_log_index_value, + p_candidate_id::programmable_private.envio_candidate_identifier, + p_page_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, history_id, + false, false, null, coverage.log_coverage_evidence_id + ) on conflict (chain_id, provider_deployment_id, stream_id) do nothing; + else + update programmable_private.envio_ingestion_cursor_current + set generation = p_next_generation, + block_number = normalized_block, + block_hash = p_block_hash, + block_global_log_index = normalized_log_index, + candidate_id = p_candidate_id, + content_commitment = p_page_commitment, + changed_by_run_id = p_run_id, + changed_at = p_changed_at, + audit_id = created_audit_id, + cursor_history_id = history_id, + is_genesis = false, + is_rewind = false, + genesis_point_id = null, + log_coverage_evidence_id = coverage.log_coverage_evidence_id + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_expected_generation; + end if; + if not found then + raise exception using errcode = '40001', message = 'Envio cursor CAS lost'; + end if; + return p_next_generation; +end +$function$; + +create or replace function programmable_private.rewind_envio_ingestion_cursor_v1( + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_generation bigint, + p_next_generation bigint, + p_target_history_generation bigint, + p_reason_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + target_cursor programmable_private.envio_ingestion_cursor_history%rowtype; + genesis programmable_private.envio_ingestion_cursor_genesis_points%rowtype; + target_block bigint; + target_hash bytea; + target_log bigint; + target_candidate text; + target_is_genesis boolean; + target_genesis_point_id uuid; + history_id uuid := pg_catalog.gen_random_uuid(); + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'rewind' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or not exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id and status = 'succeeded' + ) then + raise exception using + errcode = '55000', + message = 'Envio cursor rewind requires a succeeded neutral rewind run'; + end if; + if p_expected_generation < 1 + or p_next_generation <> p_expected_generation + 1 + or p_target_history_generation < 0 + or p_target_history_generation >= p_expected_generation + or pg_catalog.octet_length(p_reason_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid Envio rewind CAS'; + end if; + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + for update; + if current_cursor.generation is null + or current_cursor.generation <> p_expected_generation + then + raise exception using errcode = '40001', message = 'Envio rewind cursor CAS lost'; + end if; + + if p_target_history_generation = 0 then + select * into genesis + from programmable_private.envio_ingestion_cursor_genesis_points + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id; + if not found then + raise exception using + errcode = '23514', message = 'Envio rewind has no registered genesis target'; + end if; + target_block := genesis.anchor_block_number; + target_hash := genesis.anchor_block_hash; + target_log := null; + target_candidate := null; + target_is_genesis := true; + target_genesis_point_id := genesis.genesis_point_id; + else + select * into target_cursor + from programmable_private.envio_ingestion_cursor_history + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_target_history_generation; + if not found then + raise exception using + errcode = '40001', message = 'Envio rewind history target is stale'; + end if; + target_block := target_cursor.block_number; + target_hash := target_cursor.block_hash; + target_log := target_cursor.block_global_log_index; + target_candidate := target_cursor.candidate_id; + target_is_genesis := target_cursor.is_genesis; + target_genesis_point_id := target_cursor.genesis_point_id; + end if; + + if not exists ( + select 1 from programmable_private.dual_rpc_block_evidence + where verification_run_id = p_run_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and block_number = target_block + and agreed_block_hash = target_hash + ) then + raise exception using + errcode = '40001', + message = 'Envio rewind target lacks fresh dual-RPC evidence'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'envio_cursor.rewind', p_reason_commitment, p_run_id, p_changed_at + ); + insert into programmable_private.envio_ingestion_cursor_history ( + cursor_history_id, chain_id, provider_deployment_id, stream_id, + generation, block_number, block_hash, block_global_log_index, + candidate_id, content_commitment, changed_by_run_id, changed_at, + audit_id, is_rewind, rewound_from_generation, is_genesis, + genesis_point_id, log_coverage_evidence_id + ) values ( + history_id, 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, p_next_generation, + target_block::programmable_private.block_number_value, + target_hash::programmable_private.bytes32_value, + target_log, + target_candidate::programmable_private.envio_candidate_identifier, + p_reason_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, true, + p_expected_generation, target_is_genesis, target_genesis_point_id, null + ); + update programmable_private.envio_ingestion_cursor_current + set generation = p_next_generation, + block_number = target_block, + block_hash = target_hash, + block_global_log_index = target_log, + candidate_id = target_candidate, + content_commitment = p_reason_commitment, + changed_by_run_id = p_run_id, + changed_at = p_changed_at, + audit_id = created_audit_id, + cursor_history_id = history_id, + is_genesis = target_is_genesis, + is_rewind = true, + genesis_point_id = target_genesis_point_id, + log_coverage_evidence_id = null + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_expected_generation; + if not found then + raise exception using errcode = '40001', message = 'Envio rewind cursor CAS lost'; + end if; + return p_next_generation; +end +$function$; + +create function programmable_private.commit_envio_ingestion_page_v1( + p_outcome_id uuid, + p_log_coverage_evidence_id uuid, + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_generation bigint, + p_next_generation bigint, + p_from_block_number numeric, + p_candidates programmable_private.envio_candidate_page_item_v1[], + p_safe_head_observation_id uuid, + p_final_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_filter_commitment bytea, + p_ordered_log_commitments_a bytea[], + p_ordered_log_commitments_b bytea[], + p_page_commitment bytea, + p_result_commitment bytea, + p_coverage_encoding_version smallint, + p_coverage_canonical_preimage bytea, + p_coverage_content_fingerprint bytea, + p_coverage_evidence_commitment bytea, + p_finished_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + item programmable_private.envio_candidate_page_item_v1; + previous_item programmable_private.envio_candidate_page_item_v1; + final_item programmable_private.envio_candidate_page_item_v1; + item_count integer; +begin + perform programmable_private.assert_caller('programmable_projector'); + item_count := pg_catalog.cardinality(p_candidates); + if item_count not between 1 and 2000 + or p_outcome_id is null + or p_log_coverage_evidence_id is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + then + raise exception using + errcode = '22023', message = 'invalid atomic Envio page commit'; + end if; + foreach item in array p_candidates loop + if previous_item.candidate_id is not null and ( + item.block_number, item.block_global_log_index, item.candidate_id + ) <= ( + previous_item.block_number, + previous_item.block_global_log_index, + previous_item.candidate_id + ) then + raise exception using + errcode = '22023', message = 'Envio page candidates are not strictly ordered'; + end if; + perform programmable_private.append_release_neutral_envio_candidate( + item.candidate_id, p_run_id, item.block_number, item.block_hash, + item.transaction_hash, item.transaction_index, + item.block_global_log_index, item.source_address, + item.event_signature, item.event_type, item.ordered_topics, + item.raw_data, item.decoded_payload, item.payload_hash, + item.provider_cursor, p_provider_deployment_id, + item.content_commitment, item.first_seen_at, + p_stream_id, item.contract_name + ); + previous_item := item; + final_item := item; + end loop; + perform programmable_private.append_dual_rpc_log_coverage_evidence( + p_log_coverage_evidence_id, p_run_id, p_provider_deployment_id, + p_stream_id, p_expected_generation, p_next_generation, + p_from_block_number, final_item.block_number, final_item.block_hash, + final_item.block_global_log_index, final_item.candidate_id, + p_safe_head_observation_id, p_final_block_evidence_id, + p_provider_a_id, p_provider_b_id, p_filter_commitment, + p_ordered_log_commitments_a, p_ordered_log_commitments_b, + p_page_commitment, p_coverage_encoding_version, + p_coverage_canonical_preimage, p_coverage_content_fingerprint, + p_coverage_evidence_commitment, p_finished_at + ); + perform programmable_private.append_run_outcome( + p_outcome_id, p_run_id, 'succeeded', p_result_commitment, p_finished_at + ); + return programmable_private.advance_envio_ingestion_cursor_v1( + p_run_id, p_provider_deployment_id, p_stream_id, + p_expected_generation, p_next_generation, + final_item.block_number, final_item.block_hash, + final_item.block_global_log_index, final_item.candidate_id, + p_page_commitment, p_finished_at + ); +end +$function$; + +-- A normalized template hash intentionally ignores linked immutable slots. A +-- release may additionally pin the exact instance hash, but a NULL exact hash +-- is never an authorization shortcut: the instance must be bound to the +-- factory deployment, launch, pool and assets below before any event from it +-- can be materialized. +alter table programmable_private.release_dynamic_source_templates + add column expected_instance_runtime_code_hash bytea, + add constraint dynamic_template_expected_instance_hash_check check ( + expected_instance_runtime_code_hash is null + or pg_catalog.octet_length(expected_instance_runtime_code_hash) = 32 + ); + +create function programmable_private.json_hex_bytes_v1( + p_payload jsonb, + p_field text, + p_octet_length integer +) +returns bytea +language sql +immutable +strict +security invoker +set search_path = '' +as $function$ + select case + when p_octet_length between 1 and 4096 + and p_payload ? p_field + and p_payload ->> p_field + ~ ('^0x[0-9a-f]{' || (p_octet_length * 2)::text || '}$') + then pg_catalog.decode(pg_catalog.substr(p_payload ->> p_field, 3), 'hex') + else null::bytea + end +$function$; + +create table programmable_private.dynamic_source_release_asset_bindings ( + dynamic_source_release_asset_binding_id uuid primary key, + dynamic_source_attestation_id uuid not null unique + references programmable_private.dynamic_source_attestations( + dynamic_source_attestation_id + ) on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + parent_factory_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + launch_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + pool_occurrence_id uuid not null + references programmable_private.chain_event_occurrences(occurrence_id) + on delete restrict, + deployed_source_address programmable_private.eth_address not null, + pool_id programmable_private.bytes32_value not null, + token programmable_private.eth_address not null, + hook programmable_private.eth_address not null, + quote_asset programmable_private.eth_address not null, + runtime_code_evidence_id uuid not null + references programmable_private.dual_rpc_runtime_code_evidence( + runtime_code_evidence_id + ) on delete restrict, + template_commitment programmable_private.bytes32_value not null, + binding_commitment programmable_private.bytes32_value not null, + verification_run_id uuid not null, + verified_at timestamptz not null, + created_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (verification_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check (token <> quote_asset), + unique (epoch_id, pointer_generation, deployed_source_address), + unique (epoch_id, binding_commitment) +); + +create function programmable_private.bind_dynamic_source_release_asset_v1( + p_dynamic_source_release_asset_binding_id uuid, + p_run_id uuid, + p_dynamic_source_attestation_id uuid, + p_launch_occurrence_id uuid, + p_pool_occurrence_id uuid, + p_pool_id bytea, + p_token bytea, + p_hook bytea, + p_quote_asset bytea, + p_binding_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + attestation programmable_private.dynamic_source_attestations%rowtype; + template programmable_private.release_dynamic_source_templates%rowtype; + runtime programmable_private.dual_rpc_runtime_code_evidence%rowtype; + parent_materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + launch_materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + pool_materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + parent_occurrence programmable_private.chain_event_occurrences%rowtype; + launch_occurrence programmable_private.chain_event_occurrences%rowtype; + pool_occurrence programmable_private.chain_event_occurrences%rowtype; + launch_role text; + pool_role text; + existing programmable_private.dynamic_source_release_asset_bindings%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using + errcode = '55000', message = 'dynamic asset binding requires an open verification run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if p_dynamic_source_release_asset_binding_id is null + or pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_token) <> 20 + or pg_catalog.octet_length(p_hook) <> 20 + or pg_catalog.octet_length(p_quote_asset) <> 20 + or p_token = p_quote_asset + or pg_catalog.octet_length(p_binding_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid dynamic release asset binding'; + end if; + select * into attestation + from programmable_private.dynamic_source_attestations + where dynamic_source_attestation_id = p_dynamic_source_attestation_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into template + from programmable_private.release_dynamic_source_templates + where dynamic_source_template_id = attestation.dynamic_source_template_id + and epoch_id = header.epoch_id; + select * into runtime + from programmable_private.dual_rpc_runtime_code_evidence + where runtime_code_evidence_id = attestation.runtime_code_evidence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and source_address = attestation.deployed_source_address; + if attestation.dynamic_source_attestation_id is null + or template.dynamic_source_template_id is null + or runtime.runtime_code_evidence_id is null + or runtime.runtime_code_a <> runtime.runtime_code_b + or runtime.runtime_code_a <> runtime.reconstructed_runtime_code + or runtime.agreed_runtime_code_hash <> attestation.runtime_code_hash + or runtime.agreed_normalized_runtime_code_hash + <> template.normalized_runtime_code_hash + or runtime.immutable_references_commitment + <> template.immutable_references_commitment + or runtime.immutable_values_commitment + <> attestation.expected_immutable_values_commitment + or ( + template.expected_instance_runtime_code_hash is not null + and template.expected_instance_runtime_code_hash + <> runtime.agreed_runtime_code_hash + ) + then + raise exception using + errcode = '23514', + message = 'dynamic source lacks exact bytecode, template or immutable evidence'; + end if; + + select * into parent_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = attestation.parent_factory_occurrence_id; + select * into parent_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = attestation.parent_factory_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into launch_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = p_launch_occurrence_id; + select * into launch_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_launch_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into pool_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = p_pool_occurrence_id; + select * into pool_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_pool_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select binding.source_role into launch_role + from programmable_private.release_source_bindings as binding + where binding.binding_id = launch_materialization.release_binding_id; + select binding.source_role into pool_role + from programmable_private.release_source_bindings as binding + where binding.binding_id = pool_materialization.release_binding_id; + + if parent_occurrence.occurrence_id is null + or launch_occurrence.occurrence_id is null + or pool_occurrence.occurrence_id is null + or parent_materialization.release_binding_id + <> attestation.parent_factory_release_binding_id + or parent_materialization.event_type <> template.factory_event_type + or launch_role not in ('launcher', 'coordinator') + or pool_role <> 'hook' + or parent_occurrence.block_number <> attestation.deployment_block_number + or launch_occurrence.block_number > parent_occurrence.block_number + or pool_occurrence.block_number > parent_occurrence.block_number + or ( + select pg_catalog.count(*) + from programmable_private.chain_event_current_canonical + where occurrence_id = any(array[ + parent_occurrence.occurrence_id, + launch_occurrence.occurrence_id, + pool_occurrence.occurrence_id + ]::uuid[]) + ) <> 3 + or programmable_private.json_hex_bytes_v1( + parent_materialization.decoded_payload, + template.deployed_address_field, 20 + ) <> attestation.deployed_source_address + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'token', 20 + ) <> p_token + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'poolId', 32 + ) <> p_pool_id + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'hook', 20 + ) <> p_hook + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'quoteAsset', 20 + ) <> p_quote_asset + or programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'poolId', 32 + ) <> p_pool_id + or programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'hook', 20 + ) <> p_hook + or not ( + programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency0', 20 + ) = p_token + and programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency1', 20 + ) = p_quote_asset + or programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency1', 20 + ) = p_token + and programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency0', 20 + ) = p_quote_asset + ) + then + raise exception using + errcode = '23514', + message = 'factory, launch and pool payloads do not bind the exact dynamic source assets'; + end if; + + select * into existing + from programmable_private.dynamic_source_release_asset_bindings + where dynamic_source_attestation_id = p_dynamic_source_attestation_id; + if found then + if existing.dynamic_source_release_asset_binding_id + <> p_dynamic_source_release_asset_binding_id + or existing.launch_occurrence_id <> p_launch_occurrence_id + or existing.pool_occurrence_id <> p_pool_occurrence_id + or existing.pool_id <> p_pool_id + or existing.token <> p_token + or existing.hook <> p_hook + or existing.quote_asset <> p_quote_asset + or existing.binding_commitment <> p_binding_commitment + or existing.verification_run_id <> p_run_id + or existing.verified_at <> p_verified_at + then + raise exception using + errcode = '23505', message = 'dynamic asset binding replay conflict'; + end if; + return existing.dynamic_source_release_asset_binding_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'dynamic_source_asset_binding.append', p_binding_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.dynamic_source_release_asset_bindings ( + dynamic_source_release_asset_binding_id, dynamic_source_attestation_id, + chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, parent_factory_occurrence_id, launch_occurrence_id, + pool_occurrence_id, deployed_source_address, pool_id, token, hook, + quote_asset, runtime_code_evidence_id, template_commitment, + binding_commitment, verification_run_id, verified_at, + created_by_audit_id + ) values ( + p_dynamic_source_release_asset_binding_id, + attestation.dynamic_source_attestation_id, + attestation.chain_id, attestation.release_id, attestation.model_id, + attestation.source_group, attestation.epoch_id, + attestation.pointer_generation, attestation.parent_factory_occurrence_id, + p_launch_occurrence_id, p_pool_occurrence_id, + attestation.deployed_source_address, + p_pool_id::programmable_private.bytes32_value, + p_token::programmable_private.eth_address, + p_hook::programmable_private.eth_address, + p_quote_asset::programmable_private.eth_address, + runtime.runtime_code_evidence_id, template.template_commitment, + p_binding_commitment::programmable_private.bytes32_value, + p_run_id, p_verified_at, created_audit_id + ); + return p_dynamic_source_release_asset_binding_id; +end +$function$; + +create function programmable_private.enforce_dynamic_source_asset_binding_v1() +returns trigger +language plpgsql +volatile +security invoker +set search_path = '' +as $function$ +begin + if new.dynamic_source_attestation_id is not null and not exists ( + select 1 + from programmable_private.dynamic_source_release_asset_bindings as binding + where binding.dynamic_source_attestation_id = + new.dynamic_source_attestation_id + and binding.chain_id = new.chain_id + and binding.release_id = new.release_id + and binding.model_id = new.model_id + and binding.source_group = new.source_group + and binding.epoch_id = new.epoch_id + and binding.pointer_generation = new.pointer_generation + and exists ( + select 1 from programmable_private.chain_event_current_canonical + where occurrence_id = binding.parent_factory_occurrence_id + ) + and exists ( + select 1 from programmable_private.chain_event_current_canonical + where occurrence_id = binding.launch_occurrence_id + ) + and exists ( + select 1 from programmable_private.chain_event_current_canonical + where occurrence_id = binding.pool_occurrence_id + ) + ) then + raise exception using + errcode = '23514', + message = 'dynamic occurrence lacks current exact release/factory/pool/asset binding'; + end if; + return new; +end +$function$; + +create trigger require_dynamic_source_asset_binding +before insert on programmable_private.chain_event_occurrence_materializations +for each row execute function + programmable_private.enforce_dynamic_source_asset_binding_v1(); + +-- Launch position and liquidity are projection facts with their own canonical +-- source occurrence. Keeping them separate preserves the v1 DTO while making +-- the richer token-detail route evidence-complete. +create table programmable_private.launch_position_liquidity_facts ( + launch_position_liquidity_fact_id uuid primary key, + launch_projection_id uuid not null unique + references programmable_private.launch_projections(launch_projection_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + token programmable_private.eth_address not null, + pool_id programmable_private.bytes32_value not null, + position_recipient programmable_private.eth_address not null, + position_token_id programmable_private.uint256_value not null, + token_liquidity_amount programmable_private.uint256_value not null, + locked_token_dust programmable_private.uint256_value not null, + initial_sqrt_price_x96 programmable_private.uint256_value not null, + initial_tick integer not null check (initial_tick between -887272 and 887272), + tick_lower integer not null check (tick_lower between -887272 and 887272), + tick_upper integer not null check (tick_upper between -887272 and 887272), + source_occurrence_id uuid not null, + source_logical_event_id uuid not null, + source_occurrence_block_hash programmable_private.bytes32_value not null, + projection_run_id uuid not null, + fact_commitment programmable_private.bytes32_value not null, + verified_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash) + references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + foreign key (projection_run_id, epoch_id, pointer_generation) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + check (tick_lower < initial_tick and initial_tick < tick_upper), + unique (epoch_id, pointer_generation, token), + unique (epoch_id, fact_commitment) +); + +create function programmable_private.stage_launch_position_liquidity_v1( + p_launch_position_liquidity_fact_id uuid, + p_launch_projection_id uuid, + p_run_id uuid, + p_position_recipient bytea, + p_position_token_id numeric, + p_token_liquidity_amount numeric, + p_locked_token_dust numeric, -- gitleaks:allow (next identifier is a price field, not a credential) + p_initial_sqrt_price_x96 numeric, + p_initial_tick integer, + p_tick_lower integer, + p_tick_upper integer, + p_source_occurrence_id uuid, + p_fact_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + launch programmable_private.launch_projections%rowtype; + occurrence programmable_private.chain_event_occurrences%rowtype; + materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + position_id numeric; + liquidity_amount numeric; + locked_dust numeric; + sqrt_price numeric; + existing programmable_private.launch_position_liquidity_facts%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection'; + if not found or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using + errcode = '55000', message = 'launch liquidity requires an open projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + select * into launch from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id + and projection_run_id = p_run_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = p_source_occurrence_id; + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_source_occurrence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + position_id := programmable_private.validate_uint256(p_position_token_id); + liquidity_amount := + programmable_private.validate_uint256(p_token_liquidity_amount); + locked_dust := programmable_private.validate_uint256(p_locked_token_dust); + sqrt_price := programmable_private.validate_uint256(p_initial_sqrt_price_x96); + if launch.launch_projection_id is null + or occurrence.occurrence_id is null + or occurrence.block_number > launch.promoted_block_number + or pg_catalog.octet_length(p_position_recipient) <> 20 + or p_initial_tick not between -887272 and 887272 + or p_tick_lower not between -887272 and 887272 + or p_tick_upper not between -887272 and 887272 + or not (p_tick_lower < p_initial_tick and p_initial_tick < p_tick_upper) + or liquidity_amount + locked_dust > launch.total_supply + or pg_catalog.octet_length(p_fact_commitment) <> 32 + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'poolId', 32 + ) <> launch.pool_id + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'token', 20 + ) <> launch.token + then + raise exception using + errcode = '23514', + message = 'launch position/liquidity lacks exact canonical source', + detail = pg_catalog.format( + 'launch=%s occurrence=%s materialization=%s source_block=%s promoted=%s recipient_length=%s ticks=%s amount_ok=%s pool_match=%s token_match=%s', + launch.launch_projection_id is not null, + occurrence.occurrence_id is not null, + materialization.materialization_id is not null, + occurrence.block_number, launch.promoted_block_number, + pg_catalog.octet_length(p_position_recipient), + p_tick_lower < p_initial_tick and p_initial_tick < p_tick_upper, + liquidity_amount + locked_dust <= launch.total_supply, + programmable_private.json_hex_bytes_v1(materialization.decoded_payload, 'poolId', 32) = launch.pool_id, + programmable_private.json_hex_bytes_v1(materialization.decoded_payload, 'token', 20) = launch.token + ); + end if; + select * into existing + from programmable_private.launch_position_liquidity_facts + where launch_projection_id = p_launch_projection_id; + if found then + if existing.launch_position_liquidity_fact_id + <> p_launch_position_liquidity_fact_id + or existing.position_recipient <> p_position_recipient + or existing.position_token_id <> position_id + or existing.token_liquidity_amount <> liquidity_amount + or existing.locked_token_dust <> locked_dust + or existing.initial_sqrt_price_x96 <> sqrt_price + or existing.initial_tick <> p_initial_tick + or existing.tick_lower <> p_tick_lower + or existing.tick_upper <> p_tick_upper + or existing.source_occurrence_id <> p_source_occurrence_id + or existing.fact_commitment <> p_fact_commitment + then + raise exception using + errcode = '23505', message = 'launch liquidity replay conflict'; + end if; + return existing.launch_position_liquidity_fact_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'launch_position_liquidity.stage', p_fact_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.launch_position_liquidity_facts ( + launch_position_liquidity_fact_id, launch_projection_id, + chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, token, pool_id, position_recipient, + position_token_id, token_liquidity_amount, locked_token_dust, + initial_sqrt_price_x96, initial_tick, tick_lower, tick_upper, + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash, projection_run_id, + fact_commitment, verified_at, audit_id + ) values ( + p_launch_position_liquidity_fact_id, launch.launch_projection_id, + launch.chain_id, launch.release_id, launch.model_id, header.source_group, + launch.epoch_id, launch.pointer_generation, launch.token, launch.pool_id, + p_position_recipient::programmable_private.eth_address, + position_id::programmable_private.uint256_value, + liquidity_amount::programmable_private.uint256_value, + locked_dust::programmable_private.uint256_value, + sqrt_price::programmable_private.uint256_value, + p_initial_tick, p_tick_lower, p_tick_upper, + occurrence.occurrence_id, occurrence.logical_event_id, + occurrence.block_hash, p_run_id, + p_fact_commitment::programmable_private.bytes32_value, + p_verified_at, created_audit_id + ); + return p_launch_position_liquidity_fact_id; +end +$function$; + +create view programmable_private.launch_by_token_v2 +with (security_invoker = false, security_barrier = true) +as +select + launch.*, + liquidity.position_recipient, + liquidity.position_token_id, + liquidity.token_liquidity_amount, + liquidity.locked_token_dust, + liquidity.initial_sqrt_price_x96, + liquidity.initial_tick, + liquidity.tick_lower, + liquidity.tick_upper, + liquidity.source_occurrence_id as liquidity_source_occurrence_id, + liquidity.source_occurrence_block_hash as liquidity_source_block_hash, + liquidity.fact_commitment as liquidity_fact_commitment +from programmable_private.launch_by_token_v1 as launch +join programmable_private.launch_position_liquidity_facts as liquidity + on liquidity.chain_id = launch.chain_id + and liquidity.release_id = launch.release_id + and liquidity.model_id = launch.model_id + and liquidity.source_group = launch.source_group + and liquidity.epoch_id = launch.epoch_id + and liquidity.pointer_generation = launch.pointer_generation + and liquidity.projection_run_id = launch.projection_run_id + and liquidity.token = launch.token + and liquidity.pool_id = launch.pool_id +join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = liquidity.source_occurrence_id + and canonical.logical_event_id = liquidity.source_logical_event_id + and canonical.block_hash = liquidity.source_occurrence_block_hash; + +create view programmable_private.launches_by_creator_v2 +with (security_invoker = false, security_barrier = true) +as +select + launch.*, + liquidity.position_recipient, + liquidity.position_token_id, + liquidity.token_liquidity_amount, + liquidity.locked_token_dust, + liquidity.initial_sqrt_price_x96, + liquidity.initial_tick, + liquidity.tick_lower, + liquidity.tick_upper, + liquidity.source_occurrence_id as liquidity_source_occurrence_id, + liquidity.source_occurrence_block_hash as liquidity_source_block_hash, + liquidity.fact_commitment as liquidity_fact_commitment +from programmable_private.launches_by_creator_v1 as launch +join programmable_private.launch_position_liquidity_facts as liquidity + on liquidity.chain_id = launch.chain_id + and liquidity.release_id = launch.release_id + and liquidity.model_id = launch.model_id + and liquidity.source_group = launch.source_group + and liquidity.epoch_id = launch.epoch_id + and liquidity.pointer_generation = launch.pointer_generation + and liquidity.projection_run_id = launch.projection_run_id + and liquidity.token = launch.token + and liquidity.pool_id = launch.pool_id +join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = liquidity.source_occurrence_id + and canonical.logical_event_id = liquidity.source_logical_event_id + and canonical.block_hash = liquidity.source_occurrence_block_hash; + +-- USD values are only publishable with an exact, dual-RPC Chainlink ETH/USD +-- observation. Raw eth_call return bytes are retained so a later audit can +-- replay the ABI decoding rather than trusting denormalized price fields. +create table programmable_private.global_eth_usd_snapshots ( + global_market_snapshot_id uuid primary key, + chain_id programmable_private.chain_id_value not null check (chain_id = 1), + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + feed_address programmable_private.eth_address not null check ( + feed_address = pg_catalog.decode( + '5f4ec3df9cbd43714fe2740f5e3616155c5b8419', 'hex' + ) + ), + feed_round_id programmable_private.uint256_value not null, + answer numeric not null check (answer > 0), + decimals smallint not null check (decimals between 0 and 36), + feed_updated_at timestamptz not null, + block_evidence_id uuid not null, + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + safe_head_observation_id uuid not null, + provider_a_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + rpc_result_a bytea not null, + rpc_result_b bytea not null, + source_query_commitment programmable_private.bytes32_value not null, + result_commitment programmable_private.bytes32_value not null, + reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + observed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key ( + block_evidence_id, safe_head_observation_id, epoch_id, chain_id, + pointer_generation + ) references programmable_private.dual_rpc_block_evidence( + block_evidence_id, observation_id, epoch_id, chain_id, pointer_generation + ) on delete restrict, + foreign key (block_evidence_id, block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + check (provider_a_id <> provider_b_id), + check (rpc_result_a = rpc_result_b and pg_catalog.octet_length(rpc_result_a) > 0), + unique (epoch_id, pointer_generation, block_hash), + unique (epoch_id, result_commitment) +); + +create table programmable_private.market_snapshot_details ( + market_snapshot_id uuid primary key + references programmable_private.market_snapshots(market_snapshot_id) + on delete restrict, + tick integer not null check (tick between -887272 and 887272), + token0_price numeric not null check (token0_price >= 0), + token1_price numeric not null check (token1_price >= 0), + tvl_token0 numeric not null check (tvl_token0 >= 0), + tvl_token1 numeric not null check (tvl_token1 >= 0), + tvl_usd numeric not null check (tvl_usd >= 0), + transaction_count bigint not null check (transaction_count >= 0), + global_market_snapshot_id uuid not null + references programmable_private.global_eth_usd_snapshots( + global_market_snapshot_id + ) on delete restrict, + detail_commitment programmable_private.bytes32_value not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (detail_commitment) +); + +create table programmable_private.market_block_closes ( + market_block_close_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + pool_id programmable_private.bytes32_value not null, + source_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + block_evidence_id uuid not null, + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + block_timestamp timestamptz not null, + last_transaction_hash programmable_private.bytes32_value not null, + last_transaction_index programmable_private.transaction_index_value not null, + last_block_global_log_index + programmable_private.block_log_index_value not null, + last_source_occurrence_id uuid not null, + last_source_logical_event_id uuid not null, + last_source_occurrence_block_hash + programmable_private.bytes32_value not null, + sqrt_price_x96 programmable_private.uint256_value not null, + liquidity programmable_private.uint256_value not null, + tick integer not null check (tick between -887272 and 887272), + token0_price numeric not null check (token0_price >= 0), + token1_price numeric not null check (token1_price >= 0), + volume_token0 numeric not null check (volume_token0 >= 0), + volume_token1 numeric not null check (volume_token1 >= 0), + volume_usd numeric not null check (volume_usd >= 0), + fees_usd numeric not null check (fees_usd >= 0), + tvl_usd numeric not null check (tvl_usd >= 0), + transaction_count bigint not null check (transaction_count >= 0), + global_market_snapshot_id uuid not null + references programmable_private.global_eth_usd_snapshots( + global_market_snapshot_id + ) on delete restrict, + reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + source_query_commitment programmable_private.bytes32_value not null, + close_commitment programmable_private.bytes32_value not null, + observed_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (last_source_occurrence_id, last_source_logical_event_id, + last_source_occurrence_block_hash) + references programmable_private.chain_event_occurrences( + occurrence_id, logical_event_id, block_hash + ) on delete restrict, + foreign key (block_evidence_id, block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + unique (chain_id, pool_id, block_hash), + unique (epoch_id, close_commitment) +); + +create index market_block_close_chart_idx + on programmable_private.market_block_closes( + chain_id, pool_id, block_number, last_block_global_log_index + ); + +create table programmable_private.market_candle_details ( + market_candle_id uuid primary key + references programmable_private.market_candles(market_candle_id) + on delete restrict, + closing_market_block_close_id uuid not null + references programmable_private.market_block_closes(market_block_close_id) + on delete restrict, + close_sqrt_price_x96 programmable_private.uint256_value not null, + close_liquidity programmable_private.uint256_value not null, + close_tick integer not null check (close_tick between -887272 and 887272), + close_token0_price numeric not null check (close_token0_price >= 0), + close_token1_price numeric not null check (close_token1_price >= 0), + close_tvl_usd numeric not null check (close_tvl_usd >= 0), + fees_usd numeric not null check (fees_usd >= 0), + transaction_count bigint not null check (transaction_count >= 0), + global_market_snapshot_id uuid not null + references programmable_private.global_eth_usd_snapshots( + global_market_snapshot_id + ) on delete restrict, + detail_commitment programmable_private.bytes32_value not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique (detail_commitment) +); + +create function programmable_private.market_reconciliation_context_v1( + p_reconciliation_id uuid, + p_block_evidence_id uuid, + p_block_hash bytea +) +returns table ( + run_id uuid, + chain_id bigint, + release_id text, + model_id text, + source_group text, + epoch_id uuid, + pointer_generation bigint, + block_number bigint, + safe_head_observation_id uuid +) +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + evidence programmable_private.dual_rpc_block_evidence%rowtype; +begin + select * into reconciliation + from programmable_private.reconciliation_records + where reconciliation_id = p_reconciliation_id and mismatch_count = 0; + select * into header from programmable_private.run_headers + where run_id = reconciliation.run_id and run_kind = 'reconciliation'; + select * into evidence from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id + and agreed_block_hash = p_block_hash; + if reconciliation.reconciliation_id is null + or header.run_id is null + or evidence.block_evidence_id is null + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = header.run_id + ) + or evidence.chain_id <> header.chain_id + or evidence.epoch_id <> header.epoch_id + or evidence.pointer_generation <> header.captured_pointer_generation + or evidence.block_number not between + reconciliation.source_from_block and reconciliation.source_to_block + then + raise exception using + errcode = '23514', message = 'market fact lacks open exact reconciliation and block evidence'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + return query select + header.run_id, header.chain_id::bigint, header.release_id::text, + header.model_id::text, header.source_group::text, header.epoch_id, + header.captured_pointer_generation, evidence.block_number::bigint, + evidence.observation_id; +end +$function$; + +create function programmable_private.append_global_eth_usd_snapshot_v1( + p_global_market_snapshot_id uuid, + p_reconciliation_id uuid, + p_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_feed_round_id numeric, + p_answer numeric, + p_decimals smallint, + p_feed_updated_at timestamptz, + p_rpc_result_a bytea, + p_rpc_result_b bytea, + p_source_query_commitment bytea, + p_result_commitment bytea, + p_observed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + context record; + observation programmable_private.safe_head_observations%rowtype; + normalized_round numeric; + existing programmable_private.global_eth_usd_snapshots%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_block_evidence_id, + (select agreed_block_hash from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id) + ); + select * into observation from programmable_private.safe_head_observations + where observation_id = context.safe_head_observation_id + and provider_a_id = p_provider_a_id + and provider_b_id = p_provider_b_id; + normalized_round := programmable_private.validate_uint256(p_feed_round_id); + if context.run_id is null + or observation.observation_id is null + or p_provider_a_id = p_provider_b_id + or p_answer <= 0 + or p_answer::text in ('NaN', 'Infinity', '-Infinity') + or p_decimals not between 0 and 36 + or p_feed_updated_at > p_observed_at + or p_rpc_result_a is null or pg_catalog.octet_length(p_rpc_result_a) = 0 + or p_rpc_result_a <> p_rpc_result_b + or pg_catalog.octet_length(p_source_query_commitment) <> 32 + or pg_catalog.octet_length(p_result_commitment) <> 32 + then + raise exception using errcode = '23514', message = 'invalid exact ETH/USD snapshot'; + end if; + select * into existing from programmable_private.global_eth_usd_snapshots + where global_market_snapshot_id = p_global_market_snapshot_id; + if found then + if existing.reconciliation_id <> p_reconciliation_id + or existing.block_evidence_id <> p_block_evidence_id + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.feed_round_id <> normalized_round + or existing.answer <> p_answer + or existing.decimals <> p_decimals + or existing.feed_updated_at <> p_feed_updated_at + or existing.rpc_result_a <> p_rpc_result_a + or existing.source_query_commitment <> p_source_query_commitment + or existing.result_commitment <> p_result_commitment + then + raise exception using errcode = '23505', message = 'ETH/USD snapshot replay conflict'; + end if; + return existing.global_market_snapshot_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'global_eth_usd_snapshot.append', p_result_commitment, + context.run_id, p_observed_at + ); + insert into programmable_private.global_eth_usd_snapshots ( + global_market_snapshot_id, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, feed_address, + feed_round_id, answer, decimals, feed_updated_at, block_evidence_id, + block_number, block_hash, safe_head_observation_id, + provider_a_id, provider_b_id, rpc_result_a, rpc_result_b, + source_query_commitment, result_commitment, reconciliation_id, + observed_at, audit_id + ) values ( + p_global_market_snapshot_id, context.chain_id, + context.release_id::programmable_private.release_identifier, + context.model_id::programmable_private.model_identifier, + context.source_group::programmable_private.source_identifier, + context.epoch_id, context.pointer_generation, + pg_catalog.decode('5f4ec3df9cbd43714fe2740f5e3616155c5b8419', 'hex'), + normalized_round::programmable_private.uint256_value, + p_answer, p_decimals, p_feed_updated_at, p_block_evidence_id, + context.block_number::programmable_private.block_number_value, + (select agreed_block_hash from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id), + context.safe_head_observation_id, p_provider_a_id, p_provider_b_id, + p_rpc_result_a, p_rpc_result_b, + p_source_query_commitment::programmable_private.bytes32_value, + p_result_commitment::programmable_private.bytes32_value, + p_reconciliation_id, p_observed_at, created_audit_id + ); + return p_global_market_snapshot_id; +end +$function$; + +create function programmable_private.append_market_snapshot_details_v1( + p_market_snapshot_id uuid, + p_global_market_snapshot_id uuid, + p_tick integer, + p_token0_price numeric, + p_token1_price numeric, + p_tvl_token0 numeric, + p_tvl_token1 numeric, + p_tvl_usd numeric, + p_transaction_count bigint, + p_detail_commitment bytea, + p_recorded_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + snapshot programmable_private.market_snapshots%rowtype; + global_snapshot programmable_private.global_eth_usd_snapshots%rowtype; + context record; + existing programmable_private.market_snapshot_details%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into snapshot from programmable_private.market_snapshots + where market_snapshot_id = p_market_snapshot_id; + select * into context from programmable_private.market_reconciliation_context_v1( + snapshot.reconciliation_id, snapshot.block_evidence_id, snapshot.block_hash + ); + select * into global_snapshot + from programmable_private.global_eth_usd_snapshots + where global_market_snapshot_id = p_global_market_snapshot_id + and chain_id = snapshot.chain_id + and release_id = context.release_id + and model_id = context.model_id + and source_group = context.source_group + and epoch_id = context.epoch_id + and pointer_generation = context.pointer_generation + and block_number <= snapshot.block_number; + if snapshot.market_snapshot_id is null or context.run_id is null + or global_snapshot.global_market_snapshot_id is null + or p_tick not between -887272 and 887272 + or least(p_token0_price, p_token1_price, p_tvl_token0, p_tvl_token1, + p_tvl_usd) < 0 + or p_token0_price::text in ('NaN', 'Infinity', '-Infinity') + or p_token1_price::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_token0::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_token1::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_transaction_count < 0 + or pg_catalog.octet_length(p_detail_commitment) <> 32 + then + raise exception using errcode = '23514', message = 'invalid market snapshot detail'; + end if; + select * into existing from programmable_private.market_snapshot_details + where market_snapshot_id = p_market_snapshot_id; + if found then + if existing.global_market_snapshot_id <> p_global_market_snapshot_id + or existing.tick <> p_tick + or existing.token0_price <> p_token0_price + or existing.token1_price <> p_token1_price + or existing.tvl_token0 <> p_tvl_token0 + or existing.tvl_token1 <> p_tvl_token1 + or existing.tvl_usd <> p_tvl_usd + or existing.transaction_count <> p_transaction_count + or existing.detail_commitment <> p_detail_commitment + then + raise exception using errcode = '23505', message = 'market snapshot detail replay conflict'; + end if; + return p_market_snapshot_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'market_snapshot_detail.append', p_detail_commitment, + context.run_id, p_recorded_at + ); + insert into programmable_private.market_snapshot_details ( + market_snapshot_id, tick, token0_price, token1_price, + tvl_token0, tvl_token1, tvl_usd, transaction_count, + global_market_snapshot_id, detail_commitment, audit_id + ) values ( + p_market_snapshot_id, p_tick, p_token0_price, p_token1_price, + p_tvl_token0, p_tvl_token1, p_tvl_usd, p_transaction_count, + p_global_market_snapshot_id, + p_detail_commitment::programmable_private.bytes32_value, + created_audit_id + ); + return p_market_snapshot_id; +end +$function$; + +create function programmable_private.append_market_block_close_v1( + p_market_block_close_id uuid, + p_reconciliation_id uuid, + p_source_deployment_id uuid, + p_block_evidence_id uuid, + p_pool_id bytea, + p_last_source_occurrence_id uuid, + p_sqrt_price_x96 numeric, + p_liquidity numeric, + p_tick integer, + p_token0_price numeric, + p_token1_price numeric, + p_volume_token0 numeric, + p_volume_token1 numeric, + p_volume_usd numeric, + p_fees_usd numeric, + p_tvl_usd numeric, + p_transaction_count bigint, + p_global_market_snapshot_id uuid, + p_source_query_commitment bytea, + p_close_commitment bytea, + p_observed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + evidence programmable_private.dual_rpc_block_evidence%rowtype; + context record; + occurrence programmable_private.chain_event_occurrences%rowtype; + materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + global_snapshot programmable_private.global_eth_usd_snapshots%rowtype; + normalized_sqrt numeric; + normalized_liquidity numeric; + existing programmable_private.market_block_closes%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into evidence from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id; + select * into context from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_block_evidence_id, evidence.agreed_block_hash + ); + select * into occurrence from programmable_private.chain_event_occurrences + where occurrence_id = p_last_source_occurrence_id + and block_number = evidence.block_number + and block_hash = evidence.agreed_block_hash; + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_last_source_occurrence_id + and chain_id = context.chain_id + and release_id = context.release_id + and model_id = context.model_id + and source_group = context.source_group + and epoch_id = context.epoch_id + and pointer_generation = context.pointer_generation; + select * into global_snapshot + from programmable_private.global_eth_usd_snapshots + where global_market_snapshot_id = p_global_market_snapshot_id + and chain_id = context.chain_id + and release_id = context.release_id + and model_id = context.model_id + and source_group = context.source_group + and epoch_id = context.epoch_id + and pointer_generation = context.pointer_generation + and block_number <= context.block_number; + normalized_sqrt := programmable_private.validate_uint256(p_sqrt_price_x96); + normalized_liquidity := programmable_private.validate_uint256(p_liquidity); + if context.run_id is null or occurrence.occurrence_id is null + or materialization.materialization_id is null + or global_snapshot.global_market_snapshot_id is null + or not exists ( + select 1 from programmable_private.chain_event_current_canonical + where occurrence_id = p_last_source_occurrence_id + ) + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_source_deployment_id + and provider_type = 'uniswap_subgraph' + ) + or pg_catalog.octet_length(p_pool_id) <> 32 + or p_tick not between -887272 and 887272 + or least(p_token0_price, p_token1_price, p_volume_token0, + p_volume_token1, p_volume_usd, p_fees_usd, p_tvl_usd) < 0 + or p_token0_price::text in ('NaN', 'Infinity', '-Infinity') + or p_token1_price::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_token0::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_token1::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_fees_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_transaction_count < 0 + or pg_catalog.octet_length(p_source_query_commitment) <> 32 + or pg_catalog.octet_length(p_close_commitment) <> 32 + then + raise exception using errcode = '23514', message = 'invalid exact per-block market close'; + end if; + select * into existing from programmable_private.market_block_closes + where market_block_close_id = p_market_block_close_id; + if found then + if existing.reconciliation_id <> p_reconciliation_id + or existing.source_deployment_id <> p_source_deployment_id + or existing.block_evidence_id <> p_block_evidence_id + or existing.pool_id <> p_pool_id + or existing.last_source_occurrence_id <> p_last_source_occurrence_id + or existing.sqrt_price_x96 <> normalized_sqrt + or existing.liquidity <> normalized_liquidity + or existing.tick <> p_tick + or existing.close_commitment <> p_close_commitment + then + raise exception using errcode = '23505', message = 'market block close replay conflict'; + end if; + return existing.market_block_close_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'market_block_close.append', p_close_commitment, + context.run_id, p_observed_at + ); + insert into programmable_private.market_block_closes ( + market_block_close_id, chain_id, release_id, model_id, source_group, + epoch_id, pointer_generation, pool_id, source_deployment_id, + block_evidence_id, block_number, block_hash, block_timestamp, + last_transaction_hash, last_transaction_index, + last_block_global_log_index, last_source_occurrence_id, + last_source_logical_event_id, last_source_occurrence_block_hash, + sqrt_price_x96, liquidity, tick, token0_price, token1_price, + volume_token0, volume_token1, volume_usd, fees_usd, tvl_usd, + transaction_count, global_market_snapshot_id, reconciliation_id, + source_query_commitment, close_commitment, observed_at, audit_id + ) values ( + p_market_block_close_id, context.chain_id, + context.release_id::programmable_private.release_identifier, + context.model_id::programmable_private.model_identifier, + context.source_group::programmable_private.source_identifier, + context.epoch_id, context.pointer_generation, + p_pool_id::programmable_private.bytes32_value, p_source_deployment_id, + p_block_evidence_id, context.block_number, + evidence.agreed_block_hash, occurrence.block_timestamp, + occurrence.transaction_hash, occurrence.transaction_index, + occurrence.block_global_log_index, occurrence.occurrence_id, + occurrence.logical_event_id, occurrence.block_hash, + normalized_sqrt::programmable_private.uint256_value, + normalized_liquidity::programmable_private.uint256_value, + p_tick, p_token0_price, p_token1_price, p_volume_token0, + p_volume_token1, p_volume_usd, p_fees_usd, p_tvl_usd, + p_transaction_count, p_global_market_snapshot_id, + p_reconciliation_id, + p_source_query_commitment::programmable_private.bytes32_value, + p_close_commitment::programmable_private.bytes32_value, + p_observed_at, created_audit_id + ); + return p_market_block_close_id; +end +$function$; + +create function programmable_private.append_market_candle_details_v1( + p_market_candle_id uuid, + p_closing_market_block_close_id uuid, + p_detail_commitment bytea, + p_recorded_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + candle programmable_private.market_candles%rowtype; + close_fact programmable_private.market_block_closes%rowtype; + context record; + existing programmable_private.market_candle_details%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into candle from programmable_private.market_candles + where market_candle_id = p_market_candle_id; + select * into context from programmable_private.market_reconciliation_context_v1( + candle.reconciliation_id, candle.source_block_evidence_id, + candle.source_block_hash + ); + select * into close_fact from programmable_private.market_block_closes + where market_block_close_id = p_closing_market_block_close_id + and chain_id = candle.chain_id + and pool_id = candle.pool_id + and reconciliation_id = candle.reconciliation_id + and block_number <= candle.source_block_number + and block_timestamp < candle.period_end; + if candle.market_candle_id is null or context.run_id is null + or close_fact.market_block_close_id is null + or pg_catalog.octet_length(p_detail_commitment) <> 32 + then + raise exception using errcode = '23514', message = 'invalid candle close detail'; + end if; + select * into existing from programmable_private.market_candle_details + where market_candle_id = p_market_candle_id; + if found then + if existing.closing_market_block_close_id + <> p_closing_market_block_close_id + or existing.detail_commitment <> p_detail_commitment + then + raise exception using errcode = '23505', message = 'market candle detail replay conflict'; + end if; + return p_market_candle_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'market_candle_detail.append', p_detail_commitment, + context.run_id, p_recorded_at + ); + insert into programmable_private.market_candle_details ( + market_candle_id, closing_market_block_close_id, + close_sqrt_price_x96, close_liquidity, close_tick, + close_token0_price, close_token1_price, close_tvl_usd, + fees_usd, transaction_count, global_market_snapshot_id, + detail_commitment, audit_id + ) values ( + p_market_candle_id, close_fact.market_block_close_id, + close_fact.sqrt_price_x96, close_fact.liquidity, close_fact.tick, + close_fact.token0_price, close_fact.token1_price, close_fact.tvl_usd, + close_fact.fees_usd, close_fact.transaction_count, + close_fact.global_market_snapshot_id, + p_detail_commitment::programmable_private.bytes32_value, + created_audit_id + ); + return p_market_candle_id; +end +$function$; + +create view programmable_private.global_eth_usd_snapshots_v1 +with (security_invoker = false, security_barrier = true) +as +select snapshot.* +from programmable_private.global_eth_usd_snapshots as snapshot +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = snapshot.reconciliation_id + and reconciliation.mismatch_count = 0 +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + and run.chain_id = snapshot.chain_id + and run.release_id = snapshot.release_id + and run.model_id = snapshot.model_id + and run.source_group = snapshot.source_group + and run.epoch_id = snapshot.epoch_id + and run.captured_pointer_generation = snapshot.pointer_generation +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = snapshot.chain_id + and current_epoch.release_id = snapshot.release_id + and current_epoch.model_id = snapshot.model_id + and current_epoch.source_group = snapshot.source_group + and current_epoch.epoch_id = snapshot.epoch_id + and current_epoch.generation = snapshot.pointer_generation; + +create view programmable_private.market_snapshots_v2 +with (security_invoker = false, security_barrier = true) +as +select snapshot.*, detail.tick, detail.token0_price, detail.token1_price, + detail.tvl_token0, detail.tvl_token1, detail.tvl_usd, + detail.transaction_count, detail.global_market_snapshot_id, + detail.detail_commitment +from programmable_private.market_snapshots_v1 as snapshot +join programmable_private.market_snapshot_details as detail + on detail.market_snapshot_id = snapshot.market_snapshot_id +join programmable_private.global_eth_usd_snapshots_v1 as global_snapshot + on global_snapshot.global_market_snapshot_id = + detail.global_market_snapshot_id; + +create view programmable_private.market_block_closes_v1 +with (security_invoker = false, security_barrier = true) +as +select launch.token, close_fact.* +from programmable_private.market_block_closes as close_fact +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = close_fact.reconciliation_id + and reconciliation.mismatch_count = 0 +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = close_fact.chain_id + and current_epoch.release_id = close_fact.release_id + and current_epoch.model_id = close_fact.model_id + and current_epoch.source_group = close_fact.source_group + and current_epoch.epoch_id = close_fact.epoch_id + and current_epoch.generation = close_fact.pointer_generation +join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = close_fact.last_source_occurrence_id + and canonical.logical_event_id = close_fact.last_source_logical_event_id + and canonical.block_hash = close_fact.last_source_occurrence_block_hash +join programmable_private.global_eth_usd_snapshots_v1 as global_snapshot + on global_snapshot.global_market_snapshot_id = + close_fact.global_market_snapshot_id +join programmable_private.launch_by_token_v1 as launch + on launch.chain_id = close_fact.chain_id + and launch.release_id = close_fact.release_id + and launch.model_id = close_fact.model_id + and launch.source_group = close_fact.source_group + and launch.epoch_id = close_fact.epoch_id + and launch.pointer_generation = close_fact.pointer_generation + and launch.pool_id = close_fact.pool_id; + +create view programmable_private.market_candles_v2 +with (security_invoker = false, security_barrier = true) +as +select candle.*, detail.closing_market_block_close_id, + detail.close_sqrt_price_x96, detail.close_liquidity, + detail.close_tick, detail.close_token0_price, + detail.close_token1_price, detail.close_tvl_usd, + detail.fees_usd, detail.transaction_count, + detail.global_market_snapshot_id, detail.detail_commitment +from programmable_private.market_candles_v1 as candle +join programmable_private.market_candle_details as detail + on detail.market_candle_id = candle.market_candle_id +join programmable_private.market_block_closes_v1 as close_fact + on close_fact.market_block_close_id = + detail.closing_market_block_close_id; + +create index claim_projection_recipient_history_idx + on programmable_private.claim_projections( + chain_id, recipient, promoted_block_number desc, claim_projection_id + ); + +-- Claims are append-only published facts, not merely the latest balance. The +-- launch join intentionally follows the current vault identity across delta +-- projection runs; requiring the claim and launch to share a run would erase +-- valid history whenever only rewards changed. +create view programmable_private.claim_history_v1 +with (security_invoker = false, security_barrier = true) +as +select + claim.claim_projection_id, + claim.chain_id, + claim.release_id, + claim.model_id, + run.source_group, + claim.epoch_id, + claim.pointer_generation, + launch.token, + launch.token_name, + launch.token_symbol, + launch.creator, + launch.pool_id, + launch.hook, + launch.quote_asset, + claim.vault, + claim.claimant_kind, + claim.beneficiary, + claim.recipient, + claim.amount, + claim.beneficiary_total_claimed, + claim.vault_total_received, + occurrence.transaction_hash, + occurrence.block_number, + occurrence.block_hash, + occurrence.block_timestamp, + occurrence.transaction_index::bigint as transaction_index, + occurrence.block_global_log_index::bigint as block_global_log_index, + occurrence.receipt_log_ordinal::bigint as receipt_log_ordinal, + claim.source_occurrence_id, + claim.source_logical_event_id, + claim.projection_run_id, + claim.promoted_block_number, + claim.promoted_block_hash, + claim.verified_at +from programmable_private.claim_projections as claim +join programmable_private.run_headers as run + on run.run_id = claim.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = claim.chain_id + and run.release_id = claim.release_id + and run.model_id = claim.model_id + and run.epoch_id = claim.epoch_id + and run.captured_pointer_generation = claim.pointer_generation +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id and outcome.status = 'succeeded' +join programmable_private.projection_publications as publication + on publication.run_id = run.run_id + and publication.epoch_id = run.epoch_id + and publication.pointer_generation = run.captured_pointer_generation + and publication.target_block_number = claim.promoted_block_number + and publication.target_block_hash = claim.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'creator-profile' + and route.chain_id = run.chain_id + and route.release_id = run.release_id + and route.model_id = run.model_id + and route.source_group = run.source_group + and route.epoch_id = run.epoch_id + and route.pointer_generation = run.captured_pointer_generation + and route.checkpoint_id is not null + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = claim.source_occurrence_id + and canonical.logical_event_id = claim.source_logical_event_id + and canonical.block_hash = claim.source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 as occurrence + on occurrence.occurrence_id = claim.source_occurrence_id + and occurrence.logical_event_id = claim.source_logical_event_id + and occurrence.block_hash = claim.source_occurrence_block_hash + and occurrence.chain_id = run.chain_id + and occurrence.release_id = run.release_id + and occurrence.model_id = run.model_id + and occurrence.source_group = run.source_group + and occurrence.epoch_id = run.epoch_id + and occurrence.pointer_generation = run.captured_pointer_generation +join programmable_private.launches_by_creator_v1 as launch + on launch.chain_id = claim.chain_id + and launch.release_id = claim.release_id + and launch.model_id = claim.model_id + and launch.source_group = run.source_group + and launch.epoch_id = claim.epoch_id + and launch.pointer_generation = claim.pointer_generation + and launch.reward_vault = claim.vault; + +create function programmable_private.get_claim_history_v1( + p_chain_id numeric, + p_account bytea, + p_limit integer default 50, + p_before_block numeric default null, + p_before_log_index numeric default null +) +returns setof programmable_private.claim_history_v1 +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + normalized_chain bigint; + normalized_before_block bigint; + normalized_before_log bigint; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id <> pg_catalog.trunc(p_chain_id) + or p_chain_id < 1 or p_chain_id > 9223372036854775807 + or pg_catalog.octet_length(p_account) <> 20 + or p_limit not between 1 and 100 + or (p_before_block is null) <> (p_before_log_index is null) + or (p_before_block is not null and ( + p_before_block <> pg_catalog.trunc(p_before_block) + or p_before_block < 0 or p_before_block > 9223372036854775807 + or p_before_log_index <> pg_catalog.trunc(p_before_log_index) + or p_before_log_index < 0 or p_before_log_index > 4294967295 + )) + then + raise exception using errcode = '22023', message = 'invalid claim-history query'; + end if; + normalized_chain := p_chain_id::bigint; + normalized_before_block := p_before_block::bigint; + normalized_before_log := p_before_log_index::bigint; + return query + select history.* + from programmable_private.claim_history_v1 as history + where history.chain_id = normalized_chain + and (history.beneficiary = p_account or history.recipient = p_account) + and ( + normalized_before_block is null + or (history.block_number::bigint, history.block_global_log_index) + < (normalized_before_block, normalized_before_log) + ) + order by history.block_number desc, + history.block_global_log_index desc, + history.claim_projection_id desc + limit p_limit; +end +$function$; + +-- Readiness is bound to the exact canonical checkpoint identity. Epoch, +-- generation and block tuples are not substitutes because two projector +-- versions can materialize different rows at the same chain position. +create or replace view programmable_private.checkpoint_summary_v1 +with (security_invoker = false, security_barrier = true) +as +select + checkpoint.chain_id, + checkpoint.release_id, + checkpoint.model_id, + checkpoint.source_group, + checkpoint.projector_version, + checkpoint.epoch_id, + checkpoint.pointer_generation, + checkpoint.lease_generation, + checkpoint.checkpoint_generation, + checkpoint.reorg_generation, + checkpoint.block_number, + checkpoint.block_hash, + checkpoint.cursor_block_global_log_index, + checkpoint.cursor_candidate_id, + checkpoint.created_at, + checkpoint.checkpoint_id +from programmable_private.projector_checkpoint_current as current_checkpoint +join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_checkpoint.checkpoint_id + and checkpoint.chain_id = current_checkpoint.chain_id + and checkpoint.release_id = current_checkpoint.release_id + and checkpoint.model_id = current_checkpoint.model_id + and checkpoint.source_group = current_checkpoint.source_group + and checkpoint.projector_version = current_checkpoint.projector_version + and checkpoint.checkpoint_generation = + current_checkpoint.checkpoint_generation + and checkpoint.reorg_generation = current_checkpoint.reorg_generation +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = checkpoint.chain_id + and current_epoch.release_id = checkpoint.release_id + and current_epoch.model_id = checkpoint.model_id + and current_epoch.source_group = checkpoint.source_group + and current_epoch.epoch_id = checkpoint.epoch_id + and current_epoch.generation = checkpoint.pointer_generation; + +-- Stateless workers must reconstruct their exact release contract without +-- base-table access. This reader returns one exact current epoch plus four +-- deterministically ordered JSON arrays. Numeric EVM ordinals are rendered as +-- decimal strings and bytes as 0x-prefixed lowercase hex so no JavaScript +-- number coercion or driver-specific bytea rendering can change the manifest. +create function programmable_private.get_projector_release_manifest_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint +) +returns table ( + epoch_id uuid, + pointer_generation bigint, + epoch_commitment bytea, + artifact_creation_code_commitment bytea, + source_bindings jsonb, + dynamic_source_templates jsonb, + projection_event_rules jsonb, + launch_completeness_requirements jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_current_epoch( + p_chain_id, p_release_id, p_model_id, p_source_group, + p_epoch_id, p_pointer_generation + ); + return query + select + epoch.epoch_id, + p_pointer_generation, + epoch.epoch_commitment::bytea, + epoch.artifact_creation_code_commitment::bytea, + coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'binding_id', binding.binding_id::text, + 'source_name', binding.source_name::text, + 'source_role', binding.source_role::text, + 'source_type', binding.source_type::text, + 'source_address', case when binding.source_address is null then null + else '0x' || pg_catalog.encode(binding.source_address, 'hex') end, + 'recovery_selector', case when binding.recovery_selector is null then null + else '0x' || pg_catalog.encode(binding.recovery_selector, 'hex') end, + 'inclusive_start_block', binding.inclusive_start_block::text, + 'abi_event_set_commitment', + '0x' || pg_catalog.encode(binding.abi_event_set_commitment, 'hex'), + 'artifact_creation_code_commitment', + '0x' || pg_catalog.encode( + binding.artifact_creation_code_commitment, 'hex' + ), + 'binding_commitment', + '0x' || pg_catalog.encode(binding.binding_commitment, 'hex') + ) order by binding.source_role, binding.source_name, binding.binding_id + ) + from programmable_private.release_source_bindings as binding + where binding.epoch_id = epoch.epoch_id + ), '[]'::jsonb), + coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'dynamic_source_template_id', template.dynamic_source_template_id::text, + 'parent_factory_release_binding_id', + template.parent_factory_release_binding_id::text, + 'parent_factory_binding_commitment', + '0x' || pg_catalog.encode( + template.parent_factory_binding_commitment, 'hex' + ), + 'parent_source_role', template.parent_source_role::text, + 'factory_event_type', template.factory_event_type::text, + 'deployed_address_field', template.deployed_address_field::text, + 'deployed_source_role', template.deployed_source_role::text, + 'deployed_artifact_creation_code_commitment', + '0x' || pg_catalog.encode( + template.deployed_artifact_creation_code_commitment, 'hex' + ), + 'normalized_runtime_code_hash', + '0x' || pg_catalog.encode( + template.normalized_runtime_code_hash, 'hex' + ), + 'expected_instance_runtime_code_hash', + case when template.expected_instance_runtime_code_hash is null + then null else '0x' || pg_catalog.encode( + template.expected_instance_runtime_code_hash, 'hex' + ) end, + 'immutable_references_commitment', + '0x' || pg_catalog.encode( + template.immutable_references_commitment, 'hex' + ), + 'immutable_binding_spec', template.immutable_binding_spec, + 'immutable_binding_commitment', + '0x' || pg_catalog.encode( + template.immutable_binding_commitment, 'hex' + ), + 'runtime_code_length', template.runtime_code_length::text, + 'abi_event_set_commitment', + '0x' || pg_catalog.encode(template.abi_event_set_commitment, 'hex'), + 'template_commitment', + '0x' || pg_catalog.encode(template.template_commitment, 'hex') + ) order by template.parent_source_role, + template.factory_event_type, + template.deployed_source_role, + template.dynamic_source_template_id + ) + from programmable_private.release_dynamic_source_templates as template + where template.epoch_id = epoch.epoch_id + ), '[]'::jsonb), + coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'projection_event_rule_id', rule.projection_event_rule_id::text, + 'projection_kind', rule.projection_kind::text, + 'source_role', rule.source_role::text, + 'event_type', rule.event_type::text, + 'rule_commitment', + '0x' || pg_catalog.encode(rule.rule_commitment, 'hex') + ) order by rule.projection_kind, rule.source_role, + rule.event_type, rule.projection_event_rule_id + ) + from programmable_private.release_projection_event_rules as rule + where rule.epoch_id = epoch.epoch_id + ), '[]'::jsonb), + coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'launch_requirement_id', requirement.launch_requirement_id::text, + 'requirement_ordinal', requirement.requirement_ordinal, + 'occurrence_role', requirement.occurrence_role::text, + 'event_type', requirement.event_type::text, + 'required_when', requirement.required_when::text, + 'requirement_commitment', + '0x' || pg_catalog.encode( + requirement.requirement_commitment, 'hex' + ) + ) order by requirement.requirement_ordinal, + requirement.launch_requirement_id + ) + from programmable_private.release_launch_completeness_requirements + as requirement + where requirement.epoch_id = epoch.epoch_id + ), '[]'::jsonb) + from programmable_private.release_epochs as epoch + where epoch.epoch_id = p_epoch_id + and epoch.chain_id = p_chain_id + and epoch.release_id = p_release_id + and epoch.model_id = p_model_id + and epoch.source_group = p_source_group; +end +$function$; + +-- Only fully asset-bound attestations whose factory, launch and pool +-- occurrences remain current canonical can authorize dynamic log filters. +create function programmable_private.get_projector_dynamic_source_attestations_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint +) +returns table ( + dynamic_source_attestation_id uuid, + dynamic_source_template_id uuid, + runtime_code_evidence_id uuid, + deployed_source_address bytea, + deployed_source_role text, + deployment_block_number bigint, + runtime_code_hash bytea, + normalized_runtime_code_hash bytea, + expected_instance_runtime_code_hash bytea, + runtime_code_length bigint, + immutable_references_commitment bytea, + immutable_binding_spec jsonb, + immutable_binding_commitment bytea, + abi_event_set_commitment bytea, + template_commitment bytea, + attestation_commitment bytea, + parent_factory_occurrence_id uuid, + parent_factory_release_binding_id uuid, + parent_factory_binding_commitment bytea, + dynamic_source_release_asset_binding_id uuid, + launch_occurrence_id uuid, + pool_occurrence_id uuid, + token bytea, + pool_id bytea, + hook bytea, + quote_asset bytea, + asset_binding_commitment bytea +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_current_epoch( + p_chain_id, p_release_id, p_model_id, p_source_group, + p_epoch_id, p_pointer_generation + ); + return query + select + attestation.dynamic_source_attestation_id, + attestation.dynamic_source_template_id, + attestation.runtime_code_evidence_id, + attestation.deployed_source_address::bytea, + attestation.deployed_source_role::text, + attestation.deployment_block_number::bigint, + attestation.runtime_code_hash::bytea, + template.normalized_runtime_code_hash::bytea, + template.expected_instance_runtime_code_hash::bytea, + template.runtime_code_length::bigint, + template.immutable_references_commitment::bytea, + template.immutable_binding_spec, + template.immutable_binding_commitment::bytea, + attestation.abi_event_set_commitment::bytea, + template.template_commitment::bytea, + attestation.attestation_commitment::bytea, + attestation.parent_factory_occurrence_id, + attestation.parent_factory_release_binding_id, + attestation.parent_factory_binding_commitment::bytea, + binding.dynamic_source_release_asset_binding_id, + binding.launch_occurrence_id, + binding.pool_occurrence_id, + binding.token::bytea, + binding.pool_id::bytea, + binding.hook::bytea, + binding.quote_asset::bytea, + binding.binding_commitment::bytea + from programmable_private.dynamic_source_attestations as attestation + join programmable_private.release_dynamic_source_templates as template + on template.dynamic_source_template_id = + attestation.dynamic_source_template_id + and template.epoch_id = attestation.epoch_id + join programmable_private.dynamic_source_release_asset_bindings as binding + on binding.dynamic_source_attestation_id = + attestation.dynamic_source_attestation_id + and binding.chain_id = attestation.chain_id + and binding.release_id = attestation.release_id + and binding.model_id = attestation.model_id + and binding.source_group = attestation.source_group + and binding.epoch_id = attestation.epoch_id + and binding.pointer_generation = attestation.pointer_generation + join programmable_private.chain_event_current_canonical as parent_current + on parent_current.occurrence_id = attestation.parent_factory_occurrence_id + join programmable_private.chain_event_current_canonical as launch_current + on launch_current.occurrence_id = binding.launch_occurrence_id + join programmable_private.chain_event_current_canonical as pool_current + on pool_current.occurrence_id = binding.pool_occurrence_id + where attestation.chain_id = p_chain_id + and attestation.release_id = p_release_id + and attestation.model_id = p_model_id + and attestation.source_group = p_source_group + and attestation.epoch_id = p_epoch_id + and attestation.pointer_generation = p_pointer_generation + order by attestation.deployed_source_address, + attestation.deployed_source_role, + attestation.dynamic_source_attestation_id; +end +$function$; + +-- Terminal decisions are omitted from the ordinary work page, so a restarted +-- worker needs a separately paged, lease-fenced recovery stream to reproduce +-- the exact ordered decision IDs required by promotion folds. +create function programmable_private.list_projector_candidate_dispositions_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_after_block_number numeric, + p_after_block_global_log_index numeric, + p_after_candidate_id text, + p_limit integer, + p_now timestamptz +) +returns table ( + candidate_id text, + block_number bigint, + block_hash bytea, + transaction_hash bytea, + transaction_index bigint, + block_global_log_index bigint, + status text, + attempt_count bigint, + decision_id uuid, + reason_code text, + reason_commitment bytea, + changed_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + normalized_after_block bigint; + normalized_after_log_index bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_current_epoch( + p_chain_id, p_release_id, p_model_id, p_source_group, + p_epoch_id, p_pointer_generation + ); + if p_limit < 1 or p_limit > 500 or p_now is null + or pg_catalog.octet_length(p_lease_token_hash) <> 32 + or ((p_after_block_number is null) + <> (p_after_block_global_log_index is null)) + or ((p_after_block_number is null) <> (p_after_candidate_id is null)) + or not exists ( + select 1 from programmable_private.projector_lease_current as lease + where lease.chain_id = p_chain_id + and lease.release_id = p_release_id + and lease.model_id = p_model_id + and lease.source_group = p_source_group + and lease.projector_version = p_projector_version + and lease.epoch_id = p_epoch_id + and lease.pointer_generation = p_pointer_generation + and lease.lease_generation = p_lease_generation + and lease.lease_token_hash = p_lease_token_hash + and lease.expires_at >= p_now + ) + then + raise exception using + errcode = '40001', message = 'invalid or stale disposition-page lease'; + end if; + if p_after_block_number is not null then + if p_after_block_number <> pg_catalog.trunc(p_after_block_number) + or p_after_block_number < 0 + or p_after_block_number > 9223372036854775807 + or p_after_block_global_log_index + <> pg_catalog.trunc(p_after_block_global_log_index) + or p_after_block_global_log_index < 0 + or p_after_block_global_log_index > 4294967295 + or pg_catalog.octet_length( + p_after_candidate_id::programmable_private.envio_candidate_identifier + ) > 192 + then + raise exception using + errcode = '22023', message = 'invalid disposition-page cursor'; + end if; + normalized_after_block := p_after_block_number::bigint; + normalized_after_log_index := p_after_block_global_log_index::bigint; + end if; + return query + select + candidate.candidate_id::text, + candidate.block_number::bigint, + candidate.block_hash::bytea, + candidate.transaction_hash::bytea, + candidate.transaction_index::bigint, + candidate.block_global_log_index::bigint, + disposition.status::text, + disposition.attempt_count::bigint, + disposition.decision_id, + disposition.reason_code::text, + disposition.reason_commitment::bytea, + disposition.changed_at + from programmable_private.envio_candidate_status_current as disposition + join programmable_private.envio_candidate_inbox as candidate + on candidate.candidate_id = disposition.candidate_id + and candidate.chain_id = disposition.chain_id + where disposition.chain_id = p_chain_id + and disposition.release_id = p_release_id + and disposition.model_id = p_model_id + and disposition.source_group = p_source_group + and disposition.epoch_id = p_epoch_id + and disposition.pointer_generation = p_pointer_generation + and disposition.status in ('resolved', 'ignored', 'quarantined') + and ( + p_after_block_number is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + normalized_after_block, normalized_after_log_index, + p_after_candidate_id + ) + ) + order by candidate.block_number, candidate.block_global_log_index, + candidate.candidate_id + limit p_limit; +end +$function$; + +-- Close the migration under the same deny-by-default role model as the prior +-- schema. Base tables are never granted to runtime roles; only named definer +-- functions and stable barrier views form the service API. +do $expanded_p0_rls$ +declare + table_name text; +begin + foreach table_name in array array[ + 'dual_rpc_log_coverage_evidence', + 'envio_ingestion_cursor_genesis_points', + 'dynamic_source_release_asset_bindings', + 'launch_position_liquidity_facts', + 'global_eth_usd_snapshots', + 'market_snapshot_details', + 'market_block_closes', + 'market_candle_details' + ] loop + execute pg_catalog.format( + 'alter table programmable_private.%I enable row level security', + table_name + ); + execute pg_catalog.format( + 'alter table programmable_private.%I force row level security', + table_name + ); + execute pg_catalog.format( + 'create policy %I on programmable_private.%I for all ' || + 'to programmable_migrator using (true) with check (true)', + table_name || '_migrator_all', table_name + ); + end loop; +end +$expanded_p0_rls$; + +do $expanded_p0_immutable$ +declare + table_name text; +begin + foreach table_name in array array[ + 'dual_rpc_log_coverage_evidence', + 'envio_ingestion_cursor_genesis_points', + 'dynamic_source_release_asset_bindings', + 'global_eth_usd_snapshots', + 'market_block_closes' + ] loop + execute pg_catalog.format( + 'create trigger reject_immutable_mutation before update or delete ' || + 'on programmable_private.%I for each row execute function ' || + 'programmable_private.reject_immutable_mutation()', + table_name + ); + end loop; + foreach table_name in array array[ + 'market_snapshot_details', 'market_candle_details' + ] loop + execute pg_catalog.format( + 'create trigger reject_immutable_update before update ' || + 'on programmable_private.%I for each row execute function ' || + 'programmable_private.reject_immutable_mutation()', + table_name + ); + end loop; +end +$expanded_p0_immutable$; + +revoke all on function programmable_private.append_dual_rpc_runtime_code_evidence( + uuid, uuid, bytea, uuid, uuid, uuid, bytea, bytea, bytea, bytea, + numeric, numeric, bytea, bytea, bytea, bytea[], bytea, bytea, bytea, + smallint, bytea, bytea, bytea, timestamptz +) from public; +grant execute on function programmable_private.append_dual_rpc_runtime_code_evidence( + uuid, uuid, bytea, uuid, uuid, uuid, bytea, bytea, bytea, bytea, + numeric, numeric, bytea, bytea, bytea, bytea[], bytea, bytea, bytea, + smallint, bytea, bytea, bytea, timestamptz +) to programmable_projector; + +revoke all on function programmable_private.advance_envio_ingestion_cursor_v1( + uuid, uuid, text, bigint, bigint, numeric, bytea, numeric, text, bytea, + timestamptz +) from programmable_projector; +grant usage on type programmable_private.envio_candidate_page_item_v1 + to programmable_projector; +grant execute on function programmable_private.register_envio_ingestion_genesis_v1( + uuid, uuid, uuid, text, uuid, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.commit_envio_ingestion_page_v1( + uuid, uuid, uuid, uuid, text, bigint, bigint, numeric, + programmable_private.envio_candidate_page_item_v1[], uuid, uuid, + uuid, uuid, bytea, bytea[], bytea[], bytea, bytea, smallint, + bytea, bytea, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.bind_dynamic_source_release_asset_v1( + uuid, uuid, uuid, uuid, uuid, bytea, bytea, bytea, bytea, bytea, + timestamptz +) to programmable_projector; +grant execute on function programmable_private.stage_launch_position_liquidity_v1( + uuid, uuid, uuid, bytea, numeric, numeric, numeric, numeric, + integer, integer, integer, uuid, bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.get_projector_release_manifest_v1( + bigint, text, text, text, uuid, bigint +) to programmable_projector; +grant execute on function programmable_private.get_projector_dynamic_source_attestations_v1( + bigint, text, text, text, uuid, bigint +) to programmable_projector; +grant execute on function programmable_private.list_projector_candidate_dispositions_v1( + bigint, text, text, text, uuid, bigint, text, bigint, bytea, + numeric, numeric, text, integer, timestamptz +) to programmable_projector; + +grant execute on function programmable_private.append_global_eth_usd_snapshot_v1( + uuid, uuid, uuid, uuid, uuid, numeric, numeric, smallint, + timestamptz, bytea, bytea, bytea, bytea, timestamptz +) to programmable_reconciler; +grant execute on function programmable_private.append_market_snapshot_details_v1( + uuid, uuid, integer, numeric, numeric, numeric, numeric, numeric, + bigint, bytea, timestamptz +) to programmable_reconciler; +grant execute on function programmable_private.append_market_block_close_v1( + uuid, uuid, uuid, uuid, bytea, uuid, numeric, numeric, integer, + numeric, numeric, numeric, numeric, numeric, numeric, numeric, bigint, + uuid, bytea, bytea, timestamptz +) to programmable_reconciler; +grant execute on function programmable_private.append_market_candle_details_v1( + uuid, uuid, bytea, timestamptz +) to programmable_reconciler; + +grant select on + programmable_private.launch_by_token_v2, + programmable_private.launches_by_creator_v2, + programmable_private.global_eth_usd_snapshots_v1, + programmable_private.market_snapshots_v2, + programmable_private.market_block_closes_v1, + programmable_private.market_candles_v2, + programmable_private.claim_history_v1 +to programmable_api_reader; +grant execute on function programmable_private.get_claim_history_v1( + numeric, bytea, integer, numeric, numeric +) to programmable_api_reader; + +revoke all on all functions in schema programmable_private from public; + +reset role; diff --git a/supabase/migrations/20260731175501_atomic_empty_envio_coverage_pages.sql b/supabase/migrations/20260731175501_atomic_empty_envio_coverage_pages.sql new file mode 100644 index 00000000..f1bf7dfc --- /dev/null +++ b/supabase/migrations/20260731175501_atomic_empty_envio_coverage_pages.sql @@ -0,0 +1,11011 @@ +-- Persist exact empty dual-RPC coverage pages without fabricating an Envio +-- candidate. Existing non-empty provider-evidence-v2 frames remain unchanged. +-- An empty tag-5 frame uses an empty ordered commitment array plus the unique +-- end-of-block marker (u32::max, "empty-page"). The durable cursor stores the +-- covered block/hash with a NULL log/candidate pair, so continuation starts at +-- the following block while rewinds can still restore the exact boundary. + +set role programmable_migrator; + +-- One verification run is one canonical dual-RPC snapshot. It cannot attest +-- two different hashes for the same block and later choose whichever matches +-- a retained inbox fork. +alter table programmable_private.dual_rpc_block_evidence + add constraint dual_rpc_block_evidence_run_block_key + unique (verification_run_id, block_number); + +alter table programmable_private.dual_rpc_log_coverage_evidence + drop constraint dual_rpc_log_coverage_evidence_check3, + add constraint dual_rpc_log_coverage_exact_page_shape_check check ( + pg_catalog.cardinality(ordered_log_commitments_a) between 0 and 2000 + and programmable_private.valid_topics(ordered_log_commitments_a) + and ordered_log_commitments_a = ordered_log_commitments_b + and ordered_log_commitments_a = ordered_inbox_commitments + and ( + ( + pg_catalog.cardinality(ordered_log_commitments_a) = 0 + and final_block_global_log_index = 4294967295 + and final_candidate_id = 'empty-page' + ) + or + ( + pg_catalog.cardinality(ordered_log_commitments_a) between 1 and 2000 + and final_candidate_id <> 'empty-page' + ) + ) + ); + +alter table programmable_private.envio_ingestion_cursor_history + drop constraint envio_cursor_history_point_shape_check, + add constraint envio_cursor_history_point_shape_check check ( + ( + is_genesis and genesis_point_id is not null + and block_global_log_index is null and candidate_id is null + and log_coverage_evidence_id is null + ) + or + ( + not is_genesis and genesis_point_id is null + and ( + (block_global_log_index is null and candidate_id is null) + or + (block_global_log_index is not null and candidate_id is not null) + ) + and ( + (is_rewind and log_coverage_evidence_id is null) + or + (not is_rewind and log_coverage_evidence_id is not null) + ) + ) + ); + +alter table programmable_private.envio_ingestion_cursor_current + drop constraint envio_cursor_current_point_shape_check, + add constraint envio_cursor_current_point_shape_check check ( + ( + is_genesis and genesis_point_id is not null + and block_global_log_index is null and candidate_id is null + and log_coverage_evidence_id is null + ) + or + ( + not is_genesis and genesis_point_id is null + and ( + (block_global_log_index is null and candidate_id is null) + or + (block_global_log_index is not null and candidate_id is not null) + ) + and ( + (is_rewind and log_coverage_evidence_id is null) + or + (not is_rewind and log_coverage_evidence_id is not null) + ) + ) + ); + +create or replace function programmable_private.append_dual_rpc_log_coverage_evidence( + p_log_coverage_evidence_id uuid, + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_cursor_generation bigint, + p_next_cursor_generation bigint, + p_from_block_number numeric, + p_to_block_number numeric, + p_final_block_hash bytea, + p_final_block_global_log_index numeric, + p_final_candidate_id text, + p_safe_head_observation_id uuid, + p_final_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_filter_commitment bytea, + p_ordered_log_commitments_a bytea[], + p_ordered_log_commitments_b bytea[], + p_page_commitment bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_evidence_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + genesis programmable_private.envio_ingestion_cursor_genesis_points%rowtype; + evidence programmable_private.dual_rpc_block_evidence%rowtype; + observation programmable_private.safe_head_observations%rowtype; + final_candidate programmable_private.envio_candidate_inbox%rowtype; + existing programmable_private.dual_rpc_log_coverage_evidence%rowtype; + normalized_from bigint; + normalized_to bigint; + normalized_final_log bigint; + previous_block bigint; + previous_log bigint; + previous_candidate text; + inbox_commitments bytea[]; + is_empty_page boolean; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'log_coverage', p_encoding_version, p_canonical_preimage, + p_content_fingerprint + ); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using + errcode = '55000', + message = 'log coverage requires an open neutral ingestion run'; + end if; + if p_ordered_log_commitments_a is null + or p_ordered_log_commitments_b is null + or p_final_block_global_log_index is null + or p_final_candidate_id is null + or p_from_block_number is null + or p_to_block_number is null + or p_expected_cursor_generation < 0 + or p_next_cursor_generation <> p_expected_cursor_generation + 1 + or p_from_block_number <> pg_catalog.trunc(p_from_block_number) + or p_to_block_number <> pg_catalog.trunc(p_to_block_number) + or p_from_block_number < 0 + or p_to_block_number < p_from_block_number + or p_to_block_number - p_from_block_number > 1999 + or p_final_block_global_log_index + <> pg_catalog.trunc(p_final_block_global_log_index) + or p_final_block_global_log_index < 0 + or p_final_block_global_log_index > 4294967295 + or pg_catalog.octet_length(p_final_block_hash) <> 32 + or pg_catalog.octet_length(p_filter_commitment) <> 32 + or pg_catalog.octet_length(p_page_commitment) <> 32 + or pg_catalog.octet_length(p_evidence_commitment) <> 32 + or pg_catalog.cardinality(p_ordered_log_commitments_a) + not between 0 and 2000 + or not programmable_private.valid_topics(p_ordered_log_commitments_a) + or p_ordered_log_commitments_a <> p_ordered_log_commitments_b + or ( + pg_catalog.cardinality(p_ordered_log_commitments_a) = 0 + and ( + p_final_block_global_log_index <> 4294967295 + or p_final_candidate_id <> 'empty-page' + ) + ) + or ( + pg_catalog.cardinality(p_ordered_log_commitments_a) > 0 + and p_final_candidate_id = 'empty-page' + ) + or not exists ( + select 1 from programmable_private.provider_deployments + where provider_deployment_id = p_provider_deployment_id + and provider_type = 'envio_deployment' + ) + then + raise exception using + errcode = '22023', message = 'invalid bounded dual-RPC log coverage'; + end if; + normalized_from := p_from_block_number::bigint; + normalized_to := p_to_block_number::bigint; + normalized_final_log := p_final_block_global_log_index::bigint; + is_empty_page := + pg_catalog.cardinality(p_ordered_log_commitments_a) = 0; + + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + for share; + if current_cursor.generation is null then + if p_expected_cursor_generation <> 0 then + raise exception using + errcode = '40001', message = 'log-coverage cursor CAS lost'; + end if; + select * into genesis + from programmable_private.envio_ingestion_cursor_genesis_points + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id; + if not found or normalized_from <> genesis.anchor_block_number + 1 then + raise exception using + errcode = '23514', + message = 'log coverage does not start after registered genesis'; + end if; + previous_block := genesis.anchor_block_number; + previous_log := null; + previous_candidate := null; + else + if current_cursor.generation <> p_expected_cursor_generation then + raise exception using + errcode = '40001', message = 'log-coverage cursor CAS lost'; + end if; + previous_block := current_cursor.block_number; + previous_log := current_cursor.block_global_log_index; + previous_candidate := current_cursor.candidate_id; + if ( + previous_log is null and previous_candidate is null + and normalized_from <> previous_block + 1 + ) + or ( + previous_log is not null and previous_candidate is not null + and normalized_from <> previous_block + ) + or (previous_log is null) <> (previous_candidate is null) + then + raise exception using + errcode = '23514', + message = 'log coverage does not continue the current cursor'; + end if; + end if; + + if not is_empty_page then + select * into final_candidate + from programmable_private.envio_candidate_inbox + where candidate_id = p_final_candidate_id + and chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and block_number = normalized_to + and block_hash = p_final_block_hash + and block_global_log_index = normalized_final_log; + if not found then + raise exception using + errcode = '23514', + message = 'log coverage final candidate is not durable'; + end if; + end if; + + select * into evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_final_block_evidence_id + and observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and chain_id = 1 + and block_number = normalized_to + and agreed_block_hash = p_final_block_hash; + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and chain_id = 1 + and provider_a_id = p_provider_a_id + and provider_b_id = p_provider_b_id + and safe_block_number >= normalized_to; + if evidence.block_evidence_id is null + or observation.observation_id is null + then + raise exception using + errcode = '23514', + message = 'log coverage lacks exact dual-RPC range evidence'; + end if; + + select pg_catalog.array_agg( + candidate.content_commitment::bytea + order by candidate.block_number, candidate.block_global_log_index, + candidate.candidate_id + ) into inbox_commitments + from programmable_private.envio_candidate_inbox as candidate + join programmable_private.dual_rpc_block_evidence as canonical_block + on canonical_block.verification_run_id = p_run_id + and canonical_block.observation_id = p_safe_head_observation_id + and canonical_block.epoch_id = header.epoch_id + and canonical_block.pointer_generation = + header.captured_pointer_generation + and canonical_block.chain_id = 1 + and canonical_block.block_number = candidate.block_number + and canonical_block.agreed_block_hash = candidate.block_hash + where candidate.chain_id = 1 + and candidate.provider_deployment_id = p_provider_deployment_id + and candidate.stream_id = p_stream_id + and candidate.block_number between normalized_from and normalized_to + and ( + previous_candidate is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > (previous_block, previous_log, previous_candidate) + ) + and ( + is_empty_page + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (normalized_to, normalized_final_log, p_final_candidate_id) + ); + inbox_commitments := coalesce( + inbox_commitments, array[]::bytea[] + ); + if inbox_commitments is distinct from p_ordered_log_commitments_a then + raise exception using + errcode = '23514', + message = 'Envio inbox omits or changes a dual-RPC-covered log'; + end if; + + select * into existing + from programmable_private.dual_rpc_log_coverage_evidence + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and next_cursor_generation = p_next_cursor_generation; + if found then + if existing.log_coverage_evidence_id <> p_log_coverage_evidence_id + or existing.verification_run_id <> p_run_id + or existing.expected_cursor_generation + <> p_expected_cursor_generation + or existing.previous_block_number <> previous_block + or existing.previous_block_global_log_index + is distinct from previous_log + or existing.previous_candidate_id is distinct from previous_candidate + or existing.from_block_number <> normalized_from + or existing.to_block_number <> normalized_to + or existing.final_block_hash <> p_final_block_hash + or existing.final_block_global_log_index <> normalized_final_log + or existing.final_candidate_id <> p_final_candidate_id + or existing.safe_head_observation_id <> p_safe_head_observation_id + or existing.final_block_evidence_id <> p_final_block_evidence_id + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.filter_commitment <> p_filter_commitment + or existing.ordered_log_commitments_a + <> p_ordered_log_commitments_a + or existing.ordered_log_commitments_b + <> p_ordered_log_commitments_b + or existing.ordered_inbox_commitments <> inbox_commitments + or existing.page_commitment <> p_page_commitment + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + or existing.evidence_commitment <> p_evidence_commitment + or existing.verified_at <> p_verified_at + then + raise exception using + errcode = '23505', message = 'log coverage evidence replay conflict'; + end if; + return existing.log_coverage_evidence_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'dual_rpc_log_coverage.append', p_evidence_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.dual_rpc_log_coverage_evidence ( + log_coverage_evidence_id, chain_id, epoch_id, pointer_generation, + provider_deployment_id, stream_id, + expected_cursor_generation, next_cursor_generation, + previous_block_number, previous_block_global_log_index, + previous_candidate_id, from_block_number, to_block_number, + final_block_hash, final_block_global_log_index, final_candidate_id, + safe_head_observation_id, final_block_evidence_id, + provider_a_id, provider_b_id, filter_commitment, + ordered_log_commitments_a, ordered_log_commitments_b, + ordered_inbox_commitments, page_commitment, + encoding_version, canonical_preimage, content_fingerprint, + evidence_commitment, verification_run_id, verified_at, + created_by_audit_id + ) values ( + p_log_coverage_evidence_id, 1, header.epoch_id, + header.captured_pointer_generation, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, + p_expected_cursor_generation, p_next_cursor_generation, + previous_block::programmable_private.block_number_value, + previous_log, + previous_candidate::programmable_private.envio_candidate_identifier, + normalized_from::programmable_private.block_number_value, + normalized_to::programmable_private.block_number_value, + p_final_block_hash::programmable_private.bytes32_value, + normalized_final_log::programmable_private.block_log_index_value, + p_final_candidate_id::programmable_private.envio_candidate_identifier, + p_safe_head_observation_id, p_final_block_evidence_id, + p_provider_a_id, p_provider_b_id, + p_filter_commitment::programmable_private.bytes32_value, + p_ordered_log_commitments_a, p_ordered_log_commitments_b, + inbox_commitments, + p_page_commitment::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint::programmable_private.bytes32_value, + p_evidence_commitment::programmable_private.bytes32_value, + p_run_id, p_verified_at, created_audit_id + ); + return p_log_coverage_evidence_id; +end +$function$; + +create or replace function programmable_private.advance_envio_ingestion_cursor_v1( + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_generation bigint, + p_next_generation bigint, + p_block_number numeric, + p_block_hash bytea, + p_block_global_log_index numeric, + p_candidate_id text, + p_page_commitment bytea, + p_changed_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + candidate programmable_private.envio_candidate_inbox%rowtype; + coverage programmable_private.dual_rpc_log_coverage_evidence%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + normalized_block bigint; + normalized_log_index bigint; + is_empty_page boolean; + history_id uuid := pg_catalog.gen_random_uuid(); + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or not exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id and status = 'succeeded' + ) then + raise exception using + errcode = '55000', + message = 'Envio cursor advance requires a succeeded neutral ingestion run'; + end if; + if p_block_number is null + or p_block_global_log_index is null + or p_candidate_id is null + or p_expected_generation < 0 + or p_next_generation <> p_expected_generation + 1 + or p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 or p_block_number > 9223372036854775807 + or p_block_global_log_index <> pg_catalog.trunc(p_block_global_log_index) + or p_block_global_log_index < 0 + or p_block_global_log_index > 4294967295 + or pg_catalog.octet_length(p_block_hash) <> 32 + or pg_catalog.octet_length(p_page_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid Envio cursor CAS'; + end if; + normalized_block := p_block_number::bigint; + normalized_log_index := p_block_global_log_index::bigint; + select * into coverage + from programmable_private.dual_rpc_log_coverage_evidence + where verification_run_id = p_run_id + and chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and expected_cursor_generation = p_expected_generation + and next_cursor_generation = p_next_generation + and to_block_number = normalized_block + and final_block_hash = p_block_hash + and final_block_global_log_index = normalized_log_index + and final_candidate_id = p_candidate_id + and page_commitment = p_page_commitment; + if coverage.log_coverage_evidence_id is null then + raise exception using + errcode = '23514', + message = 'Envio cursor lacks exact dual-RPC log coverage'; + end if; + is_empty_page := + pg_catalog.cardinality(coverage.ordered_log_commitments_a) = 0; + if not is_empty_page then + select * into candidate + from programmable_private.envio_candidate_inbox + where candidate_id = p_candidate_id + and chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and block_number = normalized_block + and block_hash = p_block_hash + and block_global_log_index = normalized_log_index; + if candidate.candidate_id is null then + raise exception using + errcode = '23514', + message = 'Envio cursor lacks durable final inbox candidate'; + end if; + end if; + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + for update; + if (found and current_cursor.generation <> p_expected_generation) + or (not found and p_expected_generation <> 0) + or ( + current_cursor.generation is not null + and ( + ( + current_cursor.block_global_log_index is null + and normalized_block <= current_cursor.block_number + ) + or + ( + current_cursor.block_global_log_index is not null + and is_empty_page + and normalized_block < current_cursor.block_number + ) + or + ( + current_cursor.block_global_log_index is not null + and not is_empty_page + and (normalized_block, normalized_log_index, p_candidate_id) + <= ( + current_cursor.block_number::bigint, + current_cursor.block_global_log_index::bigint, + current_cursor.candidate_id::text + ) + ) + ) + ) + then + raise exception using + errcode = '40001', message = 'Envio cursor CAS lost or did not advance'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'envio_cursor.advance', p_page_commitment, p_run_id, p_changed_at + ); + insert into programmable_private.envio_ingestion_cursor_history ( + cursor_history_id, chain_id, provider_deployment_id, stream_id, + generation, block_number, block_hash, block_global_log_index, + candidate_id, content_commitment, changed_by_run_id, changed_at, + audit_id, is_rewind, rewound_from_generation, is_genesis, + genesis_point_id, log_coverage_evidence_id + ) values ( + history_id, 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, p_next_generation, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + case when is_empty_page then null + else normalized_log_index::programmable_private.block_log_index_value + end, + case when is_empty_page then null + else p_candidate_id::programmable_private.envio_candidate_identifier + end, + p_page_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, false, null, false, null, + coverage.log_coverage_evidence_id + ); + if p_expected_generation = 0 then + insert into programmable_private.envio_ingestion_cursor_current ( + chain_id, provider_deployment_id, stream_id, generation, block_number, + block_hash, block_global_log_index, candidate_id, content_commitment, + changed_by_run_id, changed_at, audit_id, cursor_history_id, + is_genesis, is_rewind, genesis_point_id, log_coverage_evidence_id + ) values ( + 1, p_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, p_next_generation, + normalized_block::programmable_private.block_number_value, + p_block_hash::programmable_private.bytes32_value, + case when is_empty_page then null + else normalized_log_index::programmable_private.block_log_index_value + end, + case when is_empty_page then null + else p_candidate_id::programmable_private.envio_candidate_identifier + end, + p_page_commitment::programmable_private.bytes32_value, + p_run_id, p_changed_at, created_audit_id, history_id, + false, false, null, coverage.log_coverage_evidence_id + ) on conflict (chain_id, provider_deployment_id, stream_id) do nothing; + else + update programmable_private.envio_ingestion_cursor_current + set generation = p_next_generation, + block_number = normalized_block, + block_hash = p_block_hash, + block_global_log_index = case when is_empty_page then null + else normalized_log_index + end, + candidate_id = case when is_empty_page then null + else p_candidate_id::programmable_private.envio_candidate_identifier + end, + content_commitment = p_page_commitment, + changed_by_run_id = p_run_id, + changed_at = p_changed_at, + audit_id = created_audit_id, + cursor_history_id = history_id, + is_genesis = false, + is_rewind = false, + genesis_point_id = null, + log_coverage_evidence_id = coverage.log_coverage_evidence_id + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_expected_generation; + end if; + if not found then + raise exception using errcode = '40001', message = 'Envio cursor CAS lost'; + end if; + return p_next_generation; +end +$function$; + +create or replace function programmable_private.commit_envio_ingestion_page_v1( + p_outcome_id uuid, + p_log_coverage_evidence_id uuid, + p_run_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_generation bigint, + p_next_generation bigint, + p_from_block_number numeric, + p_candidates programmable_private.envio_candidate_page_item_v1[], + p_safe_head_observation_id uuid, + p_final_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_filter_commitment bytea, + p_ordered_log_commitments_a bytea[], + p_ordered_log_commitments_b bytea[], + p_page_commitment bytea, + p_result_commitment bytea, + p_coverage_encoding_version smallint, + p_coverage_canonical_preimage bytea, + p_coverage_content_fingerprint bytea, + p_coverage_evidence_commitment bytea, + p_finished_at timestamptz default pg_catalog.clock_timestamp() +) +returns bigint +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + item programmable_private.envio_candidate_page_item_v1; + previous_item programmable_private.envio_candidate_page_item_v1; + final_item programmable_private.envio_candidate_page_item_v1; + existing_outcome programmable_private.run_lifecycle_outcomes%rowtype; + existing_coverage programmable_private.dual_rpc_log_coverage_evidence%rowtype; + existing_history programmable_private.envio_ingestion_cursor_history%rowtype; + final_evidence programmable_private.dual_rpc_block_evidence%rowtype; + item_count integer; + is_empty_page boolean; + final_block_number bigint; + final_block_hash bytea; + final_log_index bigint; + final_candidate_id text; + input_commitments bytea[] := array[]::bytea[]; + locked_genesis_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + item_count := pg_catalog.cardinality(p_candidates); + if p_candidates is null + or item_count not between 0 and 2000 + or p_ordered_log_commitments_a is null + or p_ordered_log_commitments_b is null + or p_outcome_id is null + or p_log_coverage_evidence_id is null + or p_finished_at is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + then + raise exception using + errcode = '22023', message = 'invalid atomic Envio page commit'; + end if; + is_empty_page := item_count = 0; + if is_empty_page + and ( + pg_catalog.cardinality(p_ordered_log_commitments_a) <> 0 + or pg_catalog.cardinality(p_ordered_log_commitments_b) <> 0 + ) + then + raise exception using + errcode = '22023', + message = 'empty Envio page reported one or more RPC logs'; + end if; + foreach item in array p_candidates loop + if previous_item.candidate_id is not null and ( + item.block_number, item.block_global_log_index, item.candidate_id + ) <= ( + previous_item.block_number, + previous_item.block_global_log_index, + previous_item.candidate_id + ) then + raise exception using + errcode = '22023', + message = 'Envio page candidates are not strictly ordered'; + end if; + input_commitments := pg_catalog.array_append( + input_commitments, item.content_commitment + ); + previous_item := item; + final_item := item; + end loop; + + select genesis.genesis_point_id into locked_genesis_id + from programmable_private.envio_ingestion_cursor_genesis_points as genesis + where genesis.chain_id = 1 + and genesis.provider_deployment_id = p_provider_deployment_id + and genesis.stream_id = p_stream_id + for update; + if not found then + raise exception using + errcode = '23514', + message = 'atomic Envio page requires registered genesis'; + end if; + + if is_empty_page then + select * into final_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_final_block_evidence_id + and observation_id = p_safe_head_observation_id + and verification_run_id = p_run_id + and chain_id = 1; + if not found then + raise exception using + errcode = '23514', + message = 'empty Envio page lacks same-run final block evidence'; + end if; + final_block_number := final_evidence.block_number; + final_block_hash := final_evidence.agreed_block_hash; + final_log_index := 4294967295; + final_candidate_id := 'empty-page'; + else + final_block_number := final_item.block_number::bigint; + final_block_hash := final_item.block_hash; + final_log_index := final_item.block_global_log_index::bigint; + final_candidate_id := final_item.candidate_id; + end if; + + select * into existing_outcome + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id; + if found then + select * into existing_coverage + from programmable_private.dual_rpc_log_coverage_evidence + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and next_cursor_generation = p_next_generation; + select * into existing_history + from programmable_private.envio_ingestion_cursor_history + where chain_id = 1 + and provider_deployment_id = p_provider_deployment_id + and stream_id = p_stream_id + and generation = p_next_generation; + if existing_outcome.outcome_id <> p_outcome_id + or existing_outcome.status <> 'succeeded' + or existing_outcome.result_commitment <> p_result_commitment + or existing_outcome.finished_at <> p_finished_at + or existing_coverage.log_coverage_evidence_id is null + or existing_coverage.log_coverage_evidence_id + <> p_log_coverage_evidence_id + or existing_coverage.verification_run_id <> p_run_id + or existing_coverage.expected_cursor_generation + <> p_expected_generation + or existing_coverage.next_cursor_generation <> p_next_generation + or existing_coverage.from_block_number <> p_from_block_number + or existing_coverage.to_block_number <> final_block_number + or existing_coverage.final_block_hash <> final_block_hash + or existing_coverage.final_block_global_log_index <> final_log_index + or existing_coverage.final_candidate_id <> final_candidate_id + or existing_coverage.safe_head_observation_id + <> p_safe_head_observation_id + or existing_coverage.final_block_evidence_id + <> p_final_block_evidence_id + or existing_coverage.provider_a_id <> p_provider_a_id + or existing_coverage.provider_b_id <> p_provider_b_id + or existing_coverage.filter_commitment <> p_filter_commitment + or existing_coverage.ordered_log_commitments_a + <> p_ordered_log_commitments_a + or existing_coverage.ordered_log_commitments_b + <> p_ordered_log_commitments_b + or existing_coverage.ordered_inbox_commitments <> input_commitments + or existing_coverage.page_commitment <> p_page_commitment + or existing_coverage.encoding_version + <> p_coverage_encoding_version + or existing_coverage.canonical_preimage + <> p_coverage_canonical_preimage + or existing_coverage.content_fingerprint + <> p_coverage_content_fingerprint + or existing_coverage.evidence_commitment + <> p_coverage_evidence_commitment + or existing_coverage.verified_at <> p_finished_at + or existing_history.cursor_history_id is null + or existing_history.block_number <> final_block_number + or existing_history.block_hash <> final_block_hash + or existing_history.content_commitment <> p_page_commitment + or existing_history.changed_by_run_id <> p_run_id + or existing_history.changed_at <> p_finished_at + or existing_history.is_rewind + or existing_history.is_genesis + or existing_history.log_coverage_evidence_id + <> p_log_coverage_evidence_id + or ( + is_empty_page and ( + existing_history.block_global_log_index is not null + or existing_history.candidate_id is not null + ) + ) + or ( + not is_empty_page and ( + existing_history.block_global_log_index <> final_log_index + or existing_history.candidate_id <> final_candidate_id + ) + ) + then + raise exception using + errcode = '23505', message = 'atomic Envio page replay conflict'; + end if; + foreach item in array p_candidates loop + if not exists ( + select 1 from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = item.candidate_id + and candidate.chain_id = 1 + and candidate.provider_deployment_id = p_provider_deployment_id + and candidate.stream_id = p_stream_id + and candidate.content_commitment = item.content_commitment + ) then + raise exception using + errcode = '23505', message = 'atomic Envio page replay conflict'; + end if; + end loop; + return p_next_generation; + end if; + + foreach item in array p_candidates loop + perform programmable_private.append_release_neutral_envio_candidate( + item.candidate_id, p_run_id, item.block_number, item.block_hash, + item.transaction_hash, item.transaction_index, + item.block_global_log_index, item.source_address, + item.event_signature, item.event_type, item.ordered_topics, + item.raw_data, item.decoded_payload, item.payload_hash, + item.provider_cursor, p_provider_deployment_id, + item.content_commitment, item.first_seen_at, + p_stream_id, item.contract_name + ); + end loop; + perform programmable_private.append_dual_rpc_log_coverage_evidence( + p_log_coverage_evidence_id, p_run_id, p_provider_deployment_id, + p_stream_id, p_expected_generation, p_next_generation, + p_from_block_number, final_block_number, final_block_hash, + final_log_index, final_candidate_id, + p_safe_head_observation_id, p_final_block_evidence_id, + p_provider_a_id, p_provider_b_id, p_filter_commitment, + p_ordered_log_commitments_a, p_ordered_log_commitments_b, + p_page_commitment, p_coverage_encoding_version, + p_coverage_canonical_preimage, p_coverage_content_fingerprint, + p_coverage_evidence_commitment, p_finished_at + ); + perform programmable_private.append_run_outcome( + p_outcome_id, p_run_id, 'succeeded', p_result_commitment, p_finished_at + ); + return programmable_private.advance_envio_ingestion_cursor_v1( + p_run_id, p_provider_deployment_id, p_stream_id, + p_expected_generation, p_next_generation, + final_block_number, final_block_hash, + final_log_index, final_candidate_id, + p_page_commitment, p_finished_at + ); +end +$function$; + +-- Missing materializations previously left a record variable full of NULLs; +-- ordinary `<>` predicates then evaluated to NULL and could let the composite +-- IF fall through. Reject every missing row/role first and use null-safe +-- comparisons for decoded payload fields. +create or replace function programmable_private.bind_dynamic_source_release_asset_v1( + p_dynamic_source_release_asset_binding_id uuid, + p_run_id uuid, + p_dynamic_source_attestation_id uuid, + p_launch_occurrence_id uuid, + p_pool_occurrence_id uuid, + p_pool_id bytea, + p_token bytea, + p_hook bytea, + p_quote_asset bytea, + p_binding_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + attestation programmable_private.dynamic_source_attestations%rowtype; + template programmable_private.release_dynamic_source_templates%rowtype; + runtime programmable_private.dual_rpc_runtime_code_evidence%rowtype; + parent_materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + launch_materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + pool_materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + parent_occurrence programmable_private.chain_event_occurrences%rowtype; + launch_occurrence programmable_private.chain_event_occurrences%rowtype; + pool_occurrence programmable_private.chain_event_occurrences%rowtype; + launch_role text; + pool_role text; + existing programmable_private.dynamic_source_release_asset_bindings%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using + errcode = '55000', + message = 'dynamic asset binding requires an open verification run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if p_dynamic_source_release_asset_binding_id is null + or pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_token) <> 20 + or pg_catalog.octet_length(p_hook) <> 20 + or pg_catalog.octet_length(p_quote_asset) <> 20 + or p_token = p_quote_asset + or pg_catalog.octet_length(p_binding_commitment) <> 32 + then + raise exception using + errcode = '22023', message = 'invalid dynamic release asset binding'; + end if; + select * into attestation + from programmable_private.dynamic_source_attestations + where dynamic_source_attestation_id = p_dynamic_source_attestation_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into template + from programmable_private.release_dynamic_source_templates + where dynamic_source_template_id = attestation.dynamic_source_template_id + and epoch_id = header.epoch_id; + select * into runtime + from programmable_private.dual_rpc_runtime_code_evidence + where runtime_code_evidence_id = attestation.runtime_code_evidence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation + and source_address = attestation.deployed_source_address; + if attestation.dynamic_source_attestation_id is null + or template.dynamic_source_template_id is null + or runtime.runtime_code_evidence_id is null + or runtime.runtime_code_a is distinct from runtime.runtime_code_b + or runtime.runtime_code_a + is distinct from runtime.reconstructed_runtime_code + or runtime.agreed_runtime_code_hash + is distinct from attestation.runtime_code_hash + or runtime.agreed_normalized_runtime_code_hash + is distinct from template.normalized_runtime_code_hash + or runtime.immutable_references_commitment + is distinct from template.immutable_references_commitment + or runtime.immutable_values_commitment + is distinct from attestation.expected_immutable_values_commitment + or ( + template.expected_instance_runtime_code_hash is not null + and template.expected_instance_runtime_code_hash + is distinct from runtime.agreed_runtime_code_hash + ) + then + raise exception using + errcode = '23514', + message = 'dynamic source lacks exact bytecode, template or immutable evidence'; + end if; + + select * into parent_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = attestation.parent_factory_occurrence_id; + select * into parent_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = attestation.parent_factory_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into launch_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = p_launch_occurrence_id; + select * into launch_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_launch_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into pool_occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = p_pool_occurrence_id; + select * into pool_materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_pool_occurrence_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select binding.source_role into launch_role + from programmable_private.release_source_bindings as binding + where binding.binding_id = launch_materialization.release_binding_id; + select binding.source_role into pool_role + from programmable_private.release_source_bindings as binding + where binding.binding_id = pool_materialization.release_binding_id; + + if parent_occurrence.occurrence_id is null + or launch_occurrence.occurrence_id is null + or pool_occurrence.occurrence_id is null + or parent_materialization.materialization_id is null + or launch_materialization.materialization_id is null + or pool_materialization.materialization_id is null + or launch_role is null + or pool_role is null + then + raise exception using + errcode = '23514', + message = 'dynamic source parent, launch or pool materialization is missing'; + end if; + if parent_materialization.release_binding_id + is distinct from attestation.parent_factory_release_binding_id + or parent_materialization.event_type + is distinct from template.factory_event_type + or launch_role not in ('launcher', 'coordinator') + or pool_role <> 'hook' + or parent_occurrence.block_number + is distinct from attestation.deployment_block_number + or launch_occurrence.block_number > parent_occurrence.block_number + or pool_occurrence.block_number > parent_occurrence.block_number + or ( + select pg_catalog.count(*) + from programmable_private.chain_event_current_canonical + where occurrence_id = any(array[ + parent_occurrence.occurrence_id, + launch_occurrence.occurrence_id, + pool_occurrence.occurrence_id + ]::uuid[]) + ) <> 3 + or programmable_private.json_hex_bytes_v1( + parent_materialization.decoded_payload, + template.deployed_address_field, 20 + ) is distinct from attestation.deployed_source_address + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'token', 20 + ) is distinct from p_token + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'poolId', 32 + ) is distinct from p_pool_id + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'hook', 20 + ) is distinct from p_hook + or programmable_private.json_hex_bytes_v1( + launch_materialization.decoded_payload, 'quoteAsset', 20 + ) is distinct from p_quote_asset + or programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'poolId', 32 + ) is distinct from p_pool_id + or programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'hook', 20 + ) is distinct from p_hook + or not ( + programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency0', 20 + ) is not distinct from p_token + and programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency1', 20 + ) is not distinct from p_quote_asset + or programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency1', 20 + ) is not distinct from p_token + and programmable_private.json_hex_bytes_v1( + pool_materialization.decoded_payload, 'currency0', 20 + ) is not distinct from p_quote_asset + ) + then + raise exception using + errcode = '23514', + message = 'factory, launch and pool payloads do not bind the exact dynamic source assets'; + end if; + + select * into existing + from programmable_private.dynamic_source_release_asset_bindings + where dynamic_source_attestation_id = p_dynamic_source_attestation_id; + if found then + if existing.dynamic_source_release_asset_binding_id + <> p_dynamic_source_release_asset_binding_id + or existing.launch_occurrence_id <> p_launch_occurrence_id + or existing.pool_occurrence_id <> p_pool_occurrence_id + or existing.pool_id <> p_pool_id + or existing.token <> p_token + or existing.hook <> p_hook + or existing.quote_asset <> p_quote_asset + or existing.binding_commitment <> p_binding_commitment + or existing.verification_run_id <> p_run_id + or existing.verified_at <> p_verified_at + then + raise exception using + errcode = '23505', message = 'dynamic asset binding replay conflict'; + end if; + return existing.dynamic_source_release_asset_binding_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'dynamic_source_asset_binding.append', p_binding_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.dynamic_source_release_asset_bindings ( + dynamic_source_release_asset_binding_id, dynamic_source_attestation_id, + chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, parent_factory_occurrence_id, launch_occurrence_id, + pool_occurrence_id, deployed_source_address, pool_id, token, hook, + quote_asset, runtime_code_evidence_id, template_commitment, + binding_commitment, verification_run_id, verified_at, + created_by_audit_id + ) values ( + p_dynamic_source_release_asset_binding_id, + attestation.dynamic_source_attestation_id, + attestation.chain_id, attestation.release_id, attestation.model_id, + attestation.source_group, attestation.epoch_id, + attestation.pointer_generation, attestation.parent_factory_occurrence_id, + p_launch_occurrence_id, p_pool_occurrence_id, + attestation.deployed_source_address, + p_pool_id::programmable_private.bytes32_value, + p_token::programmable_private.eth_address, + p_hook::programmable_private.eth_address, + p_quote_asset::programmable_private.eth_address, + runtime.runtime_code_evidence_id, template.template_commitment, + p_binding_commitment::programmable_private.bytes32_value, + p_run_id, p_verified_at, created_audit_id + ); + return p_dynamic_source_release_asset_binding_id; +end +$function$; + +-- Classic V2/V3 manifests place the one-sided launch position exactly at the +-- upper tick boundary. Other models retain the strict-interior invariant; a +-- lower-boundary relaxation is intentionally not allowlisted. +alter table programmable_private.launch_position_liquidity_facts + drop constraint launch_position_liquidity_facts_check, + add constraint launch_position_liquidity_exact_tick_policy_check check ( + ( + tick_lower < initial_tick and initial_tick < tick_upper + ) + or + ( + release_id in ('classic-v2', 'classic-v3') + and model_id = release_id + and tick_lower < initial_tick + and initial_tick = tick_upper + ) + ); + +create or replace function programmable_private.stage_launch_position_liquidity_v1( + p_launch_position_liquidity_fact_id uuid, + p_launch_projection_id uuid, + p_run_id uuid, + p_position_recipient bytea, + p_position_token_id numeric, + p_token_liquidity_amount numeric, + p_locked_token_dust numeric, -- gitleaks:allow + p_initial_sqrt_price_x96 numeric, + p_initial_tick integer, + p_tick_lower integer, + p_tick_upper integer, + p_source_occurrence_id uuid, + p_fact_commitment bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + launch programmable_private.launch_projections%rowtype; + occurrence programmable_private.chain_event_occurrences%rowtype; + materialization + programmable_private.chain_event_occurrence_materializations%rowtype; + position_id numeric; + liquidity_amount numeric; + locked_dust numeric; + sqrt_price numeric; + tick_policy_ok boolean; + existing programmable_private.launch_position_liquidity_facts%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection'; + if not found or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using + errcode = '55000', message = 'launch liquidity requires an open projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + select * into launch from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id + and projection_run_id = p_run_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + select * into occurrence + from programmable_private.chain_event_occurrences + where occurrence_id = p_source_occurrence_id; + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_source_occurrence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + position_id := programmable_private.validate_uint256(p_position_token_id); + liquidity_amount := + programmable_private.validate_uint256(p_token_liquidity_amount); + locked_dust := programmable_private.validate_uint256(p_locked_token_dust); + sqrt_price := programmable_private.validate_uint256(p_initial_sqrt_price_x96); + tick_policy_ok := + p_tick_lower < p_initial_tick and p_initial_tick < p_tick_upper + or ( + header.release_id in ('classic-v2', 'classic-v3') + and header.model_id = header.release_id + and p_tick_lower < p_initial_tick + and p_initial_tick = p_tick_upper + ); + if launch.launch_projection_id is null + or occurrence.occurrence_id is null + or materialization.materialization_id is null + or occurrence.block_number > launch.promoted_block_number + or pg_catalog.octet_length(p_position_recipient) <> 20 + or p_initial_tick not between -887272 and 887272 + or p_tick_lower not between -887272 and 887272 + or p_tick_upper not between -887272 and 887272 + or not coalesce(tick_policy_ok, false) + or liquidity_amount is null + or locked_dust is null + or liquidity_amount + locked_dust > launch.total_supply + or pg_catalog.octet_length(p_fact_commitment) <> 32 + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'poolId', 32 + ) is distinct from launch.pool_id + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'token', 20 + ) is distinct from launch.token + then + raise exception using + errcode = '23514', + message = 'launch position/liquidity lacks exact canonical source'; + end if; + select * into existing + from programmable_private.launch_position_liquidity_facts + where launch_projection_id = p_launch_projection_id; + if found then + if existing.launch_position_liquidity_fact_id + <> p_launch_position_liquidity_fact_id + or existing.position_recipient <> p_position_recipient + or existing.position_token_id <> position_id + or existing.token_liquidity_amount <> liquidity_amount + or existing.locked_token_dust <> locked_dust + or existing.initial_sqrt_price_x96 <> sqrt_price + or existing.initial_tick <> p_initial_tick + or existing.tick_lower <> p_tick_lower + or existing.tick_upper <> p_tick_upper + or existing.source_occurrence_id <> p_source_occurrence_id + or existing.fact_commitment <> p_fact_commitment + then + raise exception using + errcode = '23505', message = 'launch liquidity replay conflict'; + end if; + return existing.launch_position_liquidity_fact_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'launch_position_liquidity.stage', p_fact_commitment, + p_run_id, p_verified_at + ); + insert into programmable_private.launch_position_liquidity_facts ( + launch_position_liquidity_fact_id, launch_projection_id, + chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, token, pool_id, position_recipient, + position_token_id, token_liquidity_amount, locked_token_dust, + initial_sqrt_price_x96, initial_tick, tick_lower, tick_upper, + source_occurrence_id, source_logical_event_id, + source_occurrence_block_hash, projection_run_id, + fact_commitment, verified_at, audit_id + ) values ( + p_launch_position_liquidity_fact_id, launch.launch_projection_id, + launch.chain_id, launch.release_id, launch.model_id, header.source_group, + launch.epoch_id, launch.pointer_generation, launch.token, launch.pool_id, + p_position_recipient::programmable_private.eth_address, + position_id::programmable_private.uint256_value, + liquidity_amount::programmable_private.uint256_value, + locked_dust::programmable_private.uint256_value, + sqrt_price::programmable_private.uint256_value, + p_initial_tick, p_tick_lower, p_tick_upper, + occurrence.occurrence_id, occurrence.logical_event_id, + occurrence.block_hash, p_run_id, + p_fact_commitment::programmable_private.bytes32_value, + p_verified_at, created_audit_id + ); + return p_launch_position_liquidity_fact_id; +end +$function$; + +-- Decode the exact 5-word AggregatorV3Interface.latestRoundData() return. +-- PostgreSQL numeric preserves all 256 bits; no float or JavaScript-number +-- coercion participates in the persisted price fields. +create or replace function programmable_private.market_reconciliation_context_v1( + p_reconciliation_id uuid, + p_block_evidence_id uuid, + p_block_hash bytea +) +returns table ( + run_id uuid, + chain_id bigint, + release_id text, + model_id text, + source_group text, + epoch_id uuid, + pointer_generation bigint, + block_number bigint, + safe_head_observation_id uuid +) +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + evidence programmable_private.dual_rpc_block_evidence%rowtype; +begin + select reconciliation_row.* into reconciliation + from programmable_private.reconciliation_records as reconciliation_row + where reconciliation_row.reconciliation_id = p_reconciliation_id + and reconciliation_row.mismatch_count = 0; + select header_row.* into header + from programmable_private.run_headers as header_row + where header_row.run_id = reconciliation.run_id + and header_row.run_kind = 'reconciliation'; + select evidence_row.* into evidence + from programmable_private.dual_rpc_block_evidence as evidence_row + where evidence_row.block_evidence_id = p_block_evidence_id + and evidence_row.agreed_block_hash = p_block_hash; + if reconciliation.reconciliation_id is null + or header.run_id is null + or evidence.block_evidence_id is null + or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes as outcome + where outcome.run_id = header.run_id + ) + or evidence.chain_id <> header.chain_id + or evidence.epoch_id <> header.epoch_id + or evidence.pointer_generation <> header.captured_pointer_generation + or evidence.block_number not between + reconciliation.source_from_block and reconciliation.source_to_block + then + raise exception using + errcode = '23514', + message = 'market fact lacks open exact reconciliation and block evidence'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + return query select + header.run_id, header.chain_id::bigint, header.release_id::text, + header.model_id::text, header.source_group::text, header.epoch_id, + header.captured_pointer_generation, evidence.block_number::bigint, + evidence.observation_id; +end +$function$; + +create function programmable_private.abi_uint256_word_v1( + p_result bytea, + p_word_index integer +) +returns numeric +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +declare + decoded numeric := 0; + byte_offset integer; +begin + if pg_catalog.octet_length(p_result) <> 160 + or p_word_index not between 0 and 4 + then + raise exception using + errcode = '22023', message = 'invalid latestRoundData ABI payload'; + end if; + for byte_offset in 0..31 loop + decoded := decoded * 256 + pg_catalog.get_byte( + p_result, p_word_index * 32 + byte_offset + ); + end loop; + return decoded; +end +$function$; + +create function programmable_private.abi_int256_word_v1( + p_result bytea, + p_word_index integer +) +returns numeric +language sql +immutable +strict +security invoker +set search_path = '' +as $function$ + select case + when decoded >= + 57896044618658097711785492504343953926634992332820282019728792003956564819968::numeric + then decoded - + 115792089237316195423570985008687907853269984665640564039457584007913129639936::numeric + else decoded + end + from ( + select programmable_private.abi_uint256_word_v1( + p_result, p_word_index + ) as decoded + ) as word +$function$; + +alter table programmable_private.global_eth_usd_snapshots + add column feed_started_at timestamptz, + add column feed_answered_in_round numeric, + add column rpc_decoding_version smallint, + add constraint global_eth_usd_decoded_round_shape_check check ( + ( + rpc_decoding_version is null + and feed_started_at is null + and feed_answered_in_round is null + ) + or + ( + rpc_decoding_version = 1 + and feed_started_at is not null + and feed_answered_in_round is not null + and feed_answered_in_round >= feed_round_id + and feed_started_at <= feed_updated_at + and observed_at >= feed_updated_at + and observed_at - feed_updated_at <= interval '1 hour' + and decimals = 8 + and pg_catalog.octet_length(rpc_result_a) = 160 + ) + ); + +create or replace function programmable_private.append_global_eth_usd_snapshot_v1( + p_global_market_snapshot_id uuid, + p_reconciliation_id uuid, + p_block_evidence_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_feed_round_id numeric, + p_answer numeric, + p_decimals smallint, + p_feed_updated_at timestamptz, + p_rpc_result_a bytea, + p_rpc_result_b bytea, + p_source_query_commitment bytea, + p_result_commitment bytea, + p_observed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + context record; + observation programmable_private.safe_head_observations%rowtype; + normalized_round numeric; + decoded_round numeric; + decoded_answer numeric; + decoded_started_at numeric; + decoded_updated_at numeric; + decoded_answered_in_round numeric; + decoded_started_timestamp timestamptz; + decoded_updated_timestamp timestamptz; + existing programmable_private.global_eth_usd_snapshots%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_block_evidence_id, + ( + select agreed_block_hash + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id + ) + ); + select * into observation + from programmable_private.safe_head_observations + where observation_id = context.safe_head_observation_id + and provider_a_id = p_provider_a_id + and provider_b_id = p_provider_b_id; + normalized_round := programmable_private.validate_uint256(p_feed_round_id); + if p_rpc_result_a is null + or p_rpc_result_b is null + or pg_catalog.octet_length(p_rpc_result_a) <> 160 + or p_rpc_result_a <> p_rpc_result_b + then + raise exception using + errcode = '23514', message = 'invalid exact ETH/USD RPC result'; + end if; + decoded_round := + programmable_private.abi_uint256_word_v1(p_rpc_result_a, 0); + decoded_answer := + programmable_private.abi_int256_word_v1(p_rpc_result_a, 1); + decoded_started_at := + programmable_private.abi_uint256_word_v1(p_rpc_result_a, 2); + decoded_updated_at := + programmable_private.abi_uint256_word_v1(p_rpc_result_a, 3); + decoded_answered_in_round := + programmable_private.abi_uint256_word_v1(p_rpc_result_a, 4); + if decoded_started_at > 253402300799 + or decoded_updated_at > 253402300799 + then + raise exception using + errcode = '22008', message = 'Chainlink timestamp is out of range'; + end if; + decoded_started_timestamp := pg_catalog.to_timestamp( + decoded_started_at::double precision + ); + decoded_updated_timestamp := pg_catalog.to_timestamp( + decoded_updated_at::double precision + ); + if context.run_id is null + or observation.observation_id is null + or p_provider_a_id = p_provider_b_id + or normalized_round is null + or decoded_round <> normalized_round + or decoded_round <= 0 + or decoded_round > 1208925819614629174706175::numeric + or decoded_answer is distinct from p_answer + or decoded_answer <= 0 + or p_answer::text in ('NaN', 'Infinity', '-Infinity') + or p_decimals <> 8 + or decoded_started_at <= 0 + or decoded_updated_at < decoded_started_at + or decoded_answered_in_round < decoded_round + or decoded_answered_in_round > 1208925819614629174706175::numeric + or p_feed_updated_at is distinct from decoded_updated_timestamp + or p_observed_at < decoded_updated_timestamp + or p_observed_at - decoded_updated_timestamp > interval '1 hour' + or pg_catalog.octet_length(p_source_query_commitment) <> 32 + or pg_catalog.octet_length(p_result_commitment) <> 32 + then + raise exception using + errcode = '23514', message = 'invalid exact ETH/USD snapshot'; + end if; + select * into existing + from programmable_private.global_eth_usd_snapshots + where global_market_snapshot_id = p_global_market_snapshot_id; + if found then + if existing.reconciliation_id <> p_reconciliation_id + or existing.block_evidence_id <> p_block_evidence_id + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.feed_round_id <> normalized_round + or existing.answer <> decoded_answer + or existing.decimals <> p_decimals + or existing.feed_started_at is distinct from decoded_started_timestamp + or existing.feed_updated_at <> decoded_updated_timestamp + or existing.feed_answered_in_round <> decoded_answered_in_round + or existing.rpc_result_a <> p_rpc_result_a + or existing.rpc_result_b <> p_rpc_result_b + or existing.source_query_commitment <> p_source_query_commitment + or existing.result_commitment <> p_result_commitment + or existing.rpc_decoding_version <> 1 + or existing.observed_at <> p_observed_at + then + raise exception using + errcode = '23505', message = 'ETH/USD snapshot replay conflict'; + end if; + return existing.global_market_snapshot_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'global_eth_usd_snapshot.append', p_result_commitment, + context.run_id, p_observed_at + ); + insert into programmable_private.global_eth_usd_snapshots ( + global_market_snapshot_id, chain_id, release_id, model_id, + source_group, epoch_id, pointer_generation, feed_address, + feed_round_id, answer, decimals, feed_started_at, feed_updated_at, + feed_answered_in_round, rpc_decoding_version, + block_evidence_id, block_number, block_hash, + safe_head_observation_id, provider_a_id, provider_b_id, + rpc_result_a, rpc_result_b, source_query_commitment, + result_commitment, reconciliation_id, observed_at, audit_id + ) values ( + p_global_market_snapshot_id, context.chain_id, + context.release_id::programmable_private.release_identifier, + context.model_id::programmable_private.model_identifier, + context.source_group::programmable_private.source_identifier, + context.epoch_id, context.pointer_generation, + pg_catalog.decode('5f4ec3df9cbd43714fe2740f5e3616155c5b8419', 'hex'), + decoded_round::programmable_private.uint256_value, + decoded_answer, p_decimals, decoded_started_timestamp, + decoded_updated_timestamp, decoded_answered_in_round, + 1, p_block_evidence_id, + context.block_number::programmable_private.block_number_value, + ( + select agreed_block_hash + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id + ), + context.safe_head_observation_id, p_provider_a_id, p_provider_b_id, + p_rpc_result_a, p_rpc_result_b, + p_source_query_commitment::programmable_private.bytes32_value, + p_result_commitment::programmable_private.bytes32_value, + p_reconciliation_id, p_observed_at, created_audit_id + ); + return p_global_market_snapshot_id; +end +$function$; + +create or replace view programmable_private.global_eth_usd_snapshots_v1 +with (security_invoker = false, security_barrier = true) +as +select snapshot.* +from programmable_private.global_eth_usd_snapshots as snapshot +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = snapshot.reconciliation_id + and reconciliation.mismatch_count = 0 +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + and run.chain_id = snapshot.chain_id + and run.release_id = snapshot.release_id + and run.model_id = snapshot.model_id + and run.source_group = snapshot.source_group + and run.epoch_id = snapshot.epoch_id + and run.captured_pointer_generation = snapshot.pointer_generation +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = snapshot.chain_id + and current_epoch.release_id = snapshot.release_id + and current_epoch.model_id = snapshot.model_id + and current_epoch.source_group = snapshot.source_group + and current_epoch.epoch_id = snapshot.epoch_id + and current_epoch.generation = snapshot.pointer_generation +where snapshot.rpc_decoding_version = 1; + +create function programmable_private.enforce_decoded_eth_usd_snapshot_v1() +returns trigger +language plpgsql +volatile +security invoker +set search_path = '' +as $function$ +begin + if not exists ( + select 1 + from programmable_private.global_eth_usd_snapshots + where global_market_snapshot_id = new.global_market_snapshot_id + and rpc_decoding_version = 1 + and observed_at >= feed_updated_at + and observed_at - feed_updated_at <= interval '1 hour' + ) then + raise exception using + errcode = '23514', + message = 'market USD fact lacks decoded fresh Chainlink evidence'; + end if; + return new; +end +$function$; + +create trigger require_decoded_eth_usd_snapshot +before insert or update of global_market_snapshot_id +on programmable_private.market_snapshot_details +for each row execute function + programmable_private.enforce_decoded_eth_usd_snapshot_v1(); + +create trigger require_decoded_eth_usd_snapshot +before insert or update of global_market_snapshot_id +on programmable_private.market_block_closes +for each row execute function + programmable_private.enforce_decoded_eth_usd_snapshot_v1(); + +create trigger require_decoded_eth_usd_snapshot +before insert or update of global_market_snapshot_id +on programmable_private.market_candle_details +for each row execute function + programmable_private.enforce_decoded_eth_usd_snapshot_v1(); + +-- Fee-only batches start from a pool identity rather than a token identity. +-- Resolve that pool only through the current launch entity in the exact open +-- projection scope; duplicate current materializations are corruption and +-- therefore fail closed rather than selecting an arbitrary row. +create function programmable_private.get_projector_pool_baseline_by_id_v1( + p_projection_run_id uuid, + p_pool_id bytea +) +returns table ( + pool_projection_id uuid, + launch_projection_id uuid, + token bytea, + creator bytea, + reward_vault bytea, + currency0 bytea, + currency1 bytea, + pool_key_fee bigint, + tick_spacing integer, + hook bytea, + pool_fee_configuration_id uuid, + buy_swap_fee_bps integer, + sell_swap_fee_bps integer, + buy_creator_fee_bps integer, + sell_creator_fee_bps integer, + creator_fee_bps integer, + launcher_fee_bps integer, + transfer_tax_bps integer, + lp_fee_pips bigint, + source_projection_run_id uuid, + promoted_block_number bigint, + promoted_block_hash bytea, + last_source_occurrence_id uuid +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + candidate_count bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_open_projection_run_v1( + p_projection_run_id + ); + select * into header + from programmable_private.run_headers + where run_id = p_projection_run_id and run_kind = 'projection'; + if p_pool_id is null or pg_catalog.octet_length(p_pool_id) <> 32 then + raise exception using + errcode = '22023', message = 'invalid projector pool baseline key'; + end if; + select pg_catalog.count(*) into candidate_count + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as source_run + on source_run.run_id = launch.projection_run_id + and source_run.chain_id = header.chain_id + and source_run.release_id = header.release_id + and source_run.model_id = header.model_id + and source_run.source_group = header.source_group + and source_run.epoch_id = header.epoch_id + and source_run.captured_pointer_generation = + header.captured_pointer_generation + join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + and pool.chain_id = header.chain_id + and pool.release_id = header.release_id + and pool.model_id = header.model_id + and pool.epoch_id = header.epoch_id + and pool.pointer_generation = header.captured_pointer_generation + and pool.pool_id = p_pool_id; + if candidate_count > 1 then + raise exception using + errcode = '23514', message = 'projector pool baseline is ambiguous'; + end if; + if candidate_count = 0 then + return; + end if; + return query + select + pool.pool_projection_id, + launch.launch_projection_id, + launch.token::bytea, + launch.creator::bytea, + launch.reward_vault::bytea, + pool.currency0::bytea, + pool.currency1::bytea, + pool.pool_key_fee, + pool.tick_spacing, + pool.hook::bytea, + fee.pool_fee_configuration_id, + fee.buy_swap_fee_bps::integer, + fee.sell_swap_fee_bps::integer, + fee.buy_creator_fee_bps::integer, + fee.sell_creator_fee_bps::integer, + fee.creator_fee_bps::integer, + fee.launcher_fee_bps::integer, + fee.transfer_tax_bps::integer, + fee.lp_fee_pips, + launch.projection_run_id, + launch.promoted_block_number::bigint, + launch.promoted_block_hash::bytea, + pool.last_source_occurrence_id + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as source_run + on source_run.run_id = launch.projection_run_id + and source_run.chain_id = header.chain_id + and source_run.release_id = header.release_id + and source_run.model_id = header.model_id + and source_run.source_group = header.source_group + and source_run.epoch_id = header.epoch_id + and source_run.captured_pointer_generation = + header.captured_pointer_generation + join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + and pool.chain_id = header.chain_id + and pool.release_id = header.release_id + and pool.model_id = header.model_id + and pool.epoch_id = header.epoch_id + and pool.pointer_generation = header.captured_pointer_generation + and pool.pool_id = p_pool_id + left join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = pool.projection_run_id + and fee.chain_id = pool.chain_id + and fee.release_id = pool.release_id + and fee.model_id = pool.model_id + and fee.epoch_id = pool.epoch_id + and fee.pointer_generation = pool.pointer_generation; +end +$function$; + +-- Capability reader for staging a newly discovered reward vault. It exposes +-- only the one allocation/evidence pair that would pass the promotion gate in +-- the exact open projection scope. Zero rows means not ready; more than one +-- eligible pair is ambiguous and therefore fails closed. +create function programmable_private.get_projector_verified_reward_seed_v1( + p_projection_run_id uuid, + p_vault bytea +) +returns table ( + allocation_fact_id uuid, + allocation_evidence_id uuid, + factory_occurrence_id uuid, + vault bytea, + ordered_beneficiaries bytea[], + ordered_shares_bps integer[], + allocation_hash bytea, + configuration_hash bytea, + active_configuration_hash bytea, + fact_content_fingerprint bytea, + evidence_content_fingerprint bytea, + evidence_version text, + recovery_method text, + evidence_verified_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + eligible_count bigint; + selected_fact_id uuid; + selected_evidence_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_open_projection_run_v1( + p_projection_run_id + ); + select * into header + from programmable_private.run_headers + where run_id = p_projection_run_id and run_kind = 'projection'; + if p_vault is null or pg_catalog.octet_length(p_vault) <> 20 then + raise exception using + errcode = '22023', message = 'invalid reward-seed vault'; + end if; + + select pg_catalog.count(*), + pg_catalog.min(candidate.allocation_fact_id::text)::uuid, + pg_catalog.min(candidate.allocation_evidence_id::text)::uuid + into eligible_count, selected_fact_id, selected_evidence_id + from ( + select distinct on (fact.allocation_fact_id) + fact.allocation_fact_id, evidence.allocation_evidence_id + from programmable_private.reward_allocation_facts as fact + join programmable_private.run_headers as fact_run + on fact_run.run_id = fact.verification_run_id + and fact_run.chain_id = header.chain_id + and fact_run.release_id = header.release_id + and fact_run.model_id = header.model_id + and fact_run.source_group = header.source_group + and fact_run.epoch_id = header.epoch_id + and fact_run.captured_pointer_generation = + header.captured_pointer_generation + join programmable_private.release_source_bindings as factory_binding + on factory_binding.binding_id = fact.factory_release_binding_id + and factory_binding.epoch_id = header.epoch_id + and factory_binding.source_role = 'vault_factory' + and factory_binding.binding_commitment = + fact.factory_release_binding_commitment + join programmable_private.reward_allocation_evidence as evidence + on evidence.allocation_fact_id = fact.allocation_fact_id + and evidence.is_recomputation_attested + and evidence.recomputed_allocation_hash = fact.allocation_hash + and evidence.recomputed_configuration_hash = fact.configuration_hash + and evidence.recomputed_active_configuration_hash + is not distinct from fact.active_configuration_hash + join programmable_private.run_headers as evidence_run + on evidence_run.run_id = evidence.verification_run_id + and evidence_run.chain_id = header.chain_id + and evidence_run.release_id = header.release_id + and evidence_run.model_id = header.model_id + and evidence_run.source_group = header.source_group + and evidence_run.epoch_id = header.epoch_id + and evidence_run.captured_pointer_generation = + header.captured_pointer_generation + join programmable_private.release_source_bindings as recovery_binding + on recovery_binding.binding_id = evidence.recovery_release_binding_id + and recovery_binding.epoch_id = header.epoch_id + and recovery_binding.binding_commitment = + evidence.recovery_release_binding_commitment + and recovery_binding.source_role = case evidence.recovery_method + when 'launcher_calldata' then 'launcher' + when 'coordinator_calldata' then 'coordinator' + when 'factory_calldata' then 'factory' + else 'vault_factory' + end + left join programmable_private.chain_event_current_canonical as canonical + on canonical.logical_event_id = fact.factory_logical_event_id + join programmable_private.chain_event_materialized_occurrences_v1 + as factory_occurrence + on factory_occurrence.occurrence_id = fact.factory_occurrence_id + and factory_occurrence.logical_event_id = fact.factory_logical_event_id + and factory_occurrence.block_hash = fact.factory_occurrence_block_hash + and factory_occurrence.chain_id = header.chain_id + and factory_occurrence.release_id = header.release_id + and factory_occurrence.model_id = header.model_id + and factory_occurrence.source_group = header.source_group + and factory_occurrence.epoch_id = header.epoch_id + and factory_occurrence.pointer_generation = + header.captured_pointer_generation + where fact.chain_id = header.chain_id + and fact.release_id = header.release_id + and fact.model_id = header.model_id + and fact.epoch_id = header.epoch_id + and fact.pointer_generation = header.captured_pointer_generation + and fact.vault = p_vault + -- First promotion has no canonical pointer yet. The verified fact and + -- exact epoch materialization are sufficient for staging; promotion + -- still binds this occurrence to the target safe-head evidence before + -- selecting it as canonical. An existing competing canonical fork is + -- never accepted here. + and ( + canonical.logical_event_id is null + or ( + canonical.occurrence_id = fact.factory_occurrence_id + and canonical.block_hash = fact.factory_occurrence_block_hash + ) + ) + and not exists ( + select 1 + from programmable_private.reward_allocation_status_history + as rejected + where rejected.allocation_fact_id = fact.allocation_fact_id + and rejected.status in ( + 'quarantined', 'orphaned', 'conflicted', 'revoked' + ) + ) + and not exists ( + select 1 + from programmable_private.reward_allocation_current_verified + as conflicting + where conflicting.factory_occurrence_id = fact.factory_occurrence_id + and conflicting.vault = fact.vault + and conflicting.allocation_fact_id <> fact.allocation_fact_id + ) + and not exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences + as required + where required.allocation_fact_id = fact.allocation_fact_id + and not exists ( + select 1 + from programmable_private.chain_event_materialized_occurrences_v1 + as required_occurrence + left join programmable_private.chain_event_current_canonical + as required_canonical + on required_canonical.logical_event_id = + required_occurrence.logical_event_id + join programmable_private.release_source_bindings + as required_binding + on required_binding.binding_id = required.release_binding_id + and required_binding.epoch_id = header.epoch_id + and required_binding.source_role = required.occurrence_role + and required_binding.binding_commitment = + required.release_binding_commitment + where required_occurrence.occurrence_id = required.occurrence_id + and required_occurrence.chain_id = header.chain_id + and required_occurrence.release_id = header.release_id + and required_occurrence.model_id = header.model_id + and required_occurrence.source_group = header.source_group + and required_occurrence.epoch_id = header.epoch_id + and required_occurrence.pointer_generation = + header.captured_pointer_generation + and required_occurrence.release_binding_id = + required.release_binding_id + and ( + required_canonical.logical_event_id is null + or ( + required_canonical.occurrence_id = + required_occurrence.occurrence_id + and required_canonical.block_hash = + required_occurrence.block_hash + ) + ) + ) + ) + order by + fact.allocation_fact_id, + exists ( + select 1 + from programmable_private.reward_allocation_current_verified + as selected + where selected.allocation_fact_id = fact.allocation_fact_id + and selected.allocation_evidence_id = + evidence.allocation_evidence_id + ) desc, + (evidence.historical_enrichment_status = 'matched') desc, + case evidence.recovery_method + when 'historical_getters' then 0 + when 'launcher_calldata' then 1 + when 'coordinator_calldata' then 2 + when 'factory_calldata' then 3 + else 4 + end, + evidence.evidence_version desc, + evidence.verified_at desc, + evidence.allocation_evidence_id + ) as candidate; + + if eligible_count > 1 then + raise exception using + errcode = '23514', + message = 'reward-seed selection is ambiguous'; + end if; + if eligible_count = 0 then + return; + end if; + return query + select + fact.allocation_fact_id, + evidence.allocation_evidence_id, + fact.factory_occurrence_id, + fact.vault::bytea, + fact.ordered_beneficiaries, + fact.ordered_shares_bps, + fact.allocation_hash::bytea, + fact.configuration_hash::bytea, + fact.active_configuration_hash::bytea, + fact.content_fingerprint::bytea, + evidence.content_fingerprint::bytea, + evidence.evidence_version::text, + evidence.recovery_method::text, + evidence.verified_at + from programmable_private.reward_allocation_facts as fact + join programmable_private.reward_allocation_evidence as evidence + on evidence.allocation_evidence_id = selected_evidence_id + and evidence.allocation_fact_id = fact.allocation_fact_id + where fact.allocation_fact_id = selected_fact_id; +end +$function$; + +-- A route row is readable only while its checkpoint is still the exact +-- current checkpoint for the same scope, projector, epoch and pointer. The +-- route table itself deliberately remains mutable through the fenced route +-- writers; this view is the single fail-closed read boundary. +create view programmable_private.route_eligibility_current_exact_v1 +with (security_invoker = false, security_barrier = true) +as +select + route.route_key, + route.chain_id, + route.release_id, + route.model_id, + route.source_group, + route.epoch_id, + route.pointer_generation, + route.status, + route.route_mode, + route.checkpoint_id, + route.history_id, + route.changed_at, + checkpoint.projector_version, + checkpoint.checkpoint_generation, + checkpoint.reorg_generation, + checkpoint.block_number as checkpoint_block_number, + checkpoint.block_hash as checkpoint_block_hash, + checkpoint.created_at as checkpoint_created_at, + observation.safe_block_number, + observation.safe_block_number - checkpoint.block_number + as checkpoint_confirmations +from programmable_private.route_eligibility_current as route +join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = route.checkpoint_id + and checkpoint.chain_id = route.chain_id + and checkpoint.release_id = route.release_id + and checkpoint.model_id = route.model_id + and checkpoint.source_group = route.source_group + and checkpoint.epoch_id = route.epoch_id + and checkpoint.pointer_generation = route.pointer_generation +join programmable_private.projector_checkpoint_current as current_checkpoint + on current_checkpoint.chain_id = checkpoint.chain_id + and current_checkpoint.release_id = checkpoint.release_id + and current_checkpoint.model_id = checkpoint.model_id + and current_checkpoint.source_group = checkpoint.source_group + and current_checkpoint.projector_version = checkpoint.projector_version + and current_checkpoint.checkpoint_id = checkpoint.checkpoint_id + and current_checkpoint.checkpoint_generation = + checkpoint.checkpoint_generation + and current_checkpoint.reorg_generation = checkpoint.reorg_generation +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = route.chain_id + and current_epoch.release_id = route.release_id + and current_epoch.model_id = route.model_id + and current_epoch.source_group = route.source_group + and current_epoch.epoch_id = route.epoch_id + and current_epoch.generation = route.pointer_generation +join programmable_private.safe_head_observations as observation + on observation.observation_id = checkpoint.safe_head_observation_id + and observation.chain_id = checkpoint.chain_id + and observation.release_id = checkpoint.release_id + and observation.model_id = checkpoint.model_id + and observation.source_group = checkpoint.source_group + and observation.epoch_id = checkpoint.epoch_id + and observation.pointer_generation = checkpoint.pointer_generation +where observation.safe_block_number >= checkpoint.block_number; + +-- Recreate every existing direct route-gated view against the exact boundary. +-- The catalog-driven rewrite keeps the published DTO column lists stable and +-- fails the migration if a direct dependency is left behind. +do $exact_route_views$ +declare + route_view record; + route_view_definition text; + rewritten_count integer := 0; +begin + for route_view in + select c.relname + from pg_catalog.pg_class as c + join pg_catalog.pg_namespace as n on n.oid = c.relnamespace + where n.nspname = 'programmable_private' + and c.relkind = 'v' + and c.relname <> 'route_eligibility_current_exact_v1' + and pg_catalog.pg_get_viewdef(c.oid, true) + ~ 'programmable_private\.route_eligibility_current([^_A-Za-z0-9]|$)' + loop + select pg_catalog.pg_get_viewdef( + pg_catalog.format('programmable_private.%I', route_view.relname)::regclass, + true + ) into route_view_definition; + route_view_definition := pg_catalog.replace( + route_view_definition, + 'programmable_private.route_eligibility_current', + 'programmable_private.route_eligibility_current_exact_v1' + ); + execute pg_catalog.format( + 'create or replace view programmable_private.%I ' + || 'with (security_invoker = false, security_barrier = true) as %s', + route_view.relname, + route_view_definition + ); + rewritten_count := rewritten_count + 1; + end loop; + if rewritten_count <> 8 then + raise exception using + errcode = '55000', + message = 'unexpected direct route-gated view inventory', + detail = pg_catalog.format('rewritten=%s expected=8', rewritten_count); + end if; + if exists ( + select 1 + from pg_catalog.pg_class as c + join pg_catalog.pg_namespace as n on n.oid = c.relnamespace + where n.nspname = 'programmable_private' + and c.relkind = 'v' + and c.relname <> 'route_eligibility_current_exact_v1' + and pg_catalog.pg_get_viewdef(c.oid, true) + ~ 'programmable_private\.route_eligibility_current([^_A-Za-z0-9]|$)' + ) then + raise exception using + errcode = '55000', message = 'non-exact route-gated view remains'; + end if; +end +$exact_route_views$; + +-- parity_records proves DTO equality, but historically did not identify the +-- checkpoint whose DTO was compared. This immutable binding makes that +-- checkpoint identity explicit and replayable. +create table programmable_private.route_checkpoint_parity_bindings ( + parity_binding_id uuid primary key, + parity_record_id uuid not null unique + references programmable_private.parity_records(parity_record_id) + on delete restrict, + reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + route_key programmable_private.source_identifier not null, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + projector_version programmable_private.projector_identifier not null, + checkpoint_generation bigint not null check (checkpoint_generation > 0), + reorg_generation bigint not null check (reorg_generation >= 0), + checkpoint_block_number programmable_private.block_number_value not null, + checkpoint_block_hash programmable_private.bytes32_value not null, + binding_commitment programmable_private.bytes32_value not null, + bound_by_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + bound_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + unique ( + route_key, chain_id, release_id, model_id, source_group, + epoch_id, pointer_generation, checkpoint_id, parity_record_id + ) +); + +alter table programmable_private.route_checkpoint_parity_bindings + enable row level security; +alter table programmable_private.route_checkpoint_parity_bindings + force row level security; +create policy migrator_owner_all + on programmable_private.route_checkpoint_parity_bindings + for all to programmable_migrator using (true) with check (true); + +create trigger reject_immutable_update +before update on programmable_private.route_checkpoint_parity_bindings +for each row execute function + programmable_private.reject_immutable_mutation(); + +create function programmable_private.bind_route_checkpoint_parity_v1( + p_parity_binding_id uuid, + p_parity_record_id uuid, + p_checkpoint_id uuid, + p_binding_commitment bytea, + p_bound_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + parity programmable_private.parity_records%rowtype; + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + route programmable_private.route_eligibility_current_exact_v1%rowtype; + existing programmable_private.route_checkpoint_parity_bindings%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into parity + from programmable_private.parity_records + where parity_record_id = p_parity_record_id; + select * into reconciliation + from programmable_private.reconciliation_records + where reconciliation_id = parity.reconciliation_id; + select * into header + from programmable_private.run_headers + where run_id = reconciliation.run_id + and run_kind = 'reconciliation'; + select * into route + from programmable_private.route_eligibility_current_exact_v1 + where route_key = parity.route_key + and chain_id = reconciliation.chain_id + and release_id = reconciliation.release_id + and model_id = reconciliation.model_id + and source_group = header.source_group + and epoch_id = reconciliation.epoch_id + and pointer_generation = reconciliation.pointer_generation + and checkpoint_id = p_checkpoint_id; + if parity.parity_record_id is null + or reconciliation.reconciliation_id is null + or header.run_id is null + or route.route_key is null + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = header.run_id + ) + or reconciliation.source_to_block <> route.checkpoint_block_number + or reconciliation.source_from_block > route.checkpoint_block_number + or pg_catalog.octet_length(p_binding_commitment) <> 32 + or p_bound_at < parity.compared_at + then + raise exception using + errcode = '23514', + message = 'parity lacks exact current checkpoint provenance'; + end if; + select * into existing + from programmable_private.route_checkpoint_parity_bindings + where parity_binding_id = p_parity_binding_id + or parity_record_id = p_parity_record_id; + if found then + if existing.parity_binding_id <> p_parity_binding_id + or existing.parity_record_id <> p_parity_record_id + or existing.reconciliation_id <> parity.reconciliation_id + or existing.route_key <> parity.route_key + or existing.chain_id <> route.chain_id + or existing.release_id <> route.release_id + or existing.model_id <> route.model_id + or existing.source_group <> route.source_group + or existing.epoch_id <> route.epoch_id + or existing.pointer_generation <> route.pointer_generation + or existing.checkpoint_id <> route.checkpoint_id + or existing.projector_version <> route.projector_version + or existing.checkpoint_generation <> route.checkpoint_generation + or existing.reorg_generation <> route.reorg_generation + or existing.checkpoint_block_number <> + route.checkpoint_block_number + or existing.checkpoint_block_hash <> route.checkpoint_block_hash + or existing.binding_commitment <> p_binding_commitment + or existing.bound_by_run_id <> header.run_id + or existing.bound_at <> p_bound_at + then + raise exception using + errcode = '23505', message = 'parity binding replay conflict'; + end if; + return existing.parity_binding_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'route_checkpoint_parity.bind', p_binding_commitment, + header.run_id, p_bound_at + ); + insert into programmable_private.route_checkpoint_parity_bindings ( + parity_binding_id, parity_record_id, reconciliation_id, route_key, + chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, checkpoint_id, projector_version, + checkpoint_generation, reorg_generation, checkpoint_block_number, + checkpoint_block_hash, binding_commitment, bound_by_run_id, + bound_at, audit_id + ) values ( + p_parity_binding_id, p_parity_record_id, parity.reconciliation_id, + parity.route_key, route.chain_id, route.release_id, route.model_id, + route.source_group, route.epoch_id, route.pointer_generation, + route.checkpoint_id, route.projector_version, + route.checkpoint_generation, route.reorg_generation, + route.checkpoint_block_number, route.checkpoint_block_hash, + p_binding_commitment::programmable_private.bytes32_value, + header.run_id, p_bound_at, created_audit_id + ); + return p_parity_binding_id; +end +$function$; + +create view programmable_private.route_snapshot_readiness_v1 +with (security_invoker = false, security_barrier = true) +as +select + route.route_key, + route.chain_id, + route.release_id, + route.model_id, + route.source_group, + route.status as route_status, + route.status as eligibility_status, + route.route_mode, + route.history_id as route_history_id, + route.changed_at as route_changed_at, + route.projector_version, + route.epoch_id, + route.pointer_generation, + route.checkpoint_id, + route.checkpoint_generation, + route.reorg_generation, + route.checkpoint_block_number, + route.checkpoint_block_hash, + route.checkpoint_created_at, + route.safe_block_number, + route.checkpoint_confirmations, + case + when parity.parity_record_id is null then 'missing' + when not parity.is_match or parity.mismatch_count > 0 then 'mismatch' + when parity.parity_binding_id is null then 'stale' + when parity.checkpoint_id <> route.checkpoint_id + or parity.projector_version <> route.projector_version + or parity.checkpoint_generation <> route.checkpoint_generation + or parity.reorg_generation <> route.reorg_generation + or parity.checkpoint_block_number <> route.checkpoint_block_number + or parity.checkpoint_block_hash <> route.checkpoint_block_hash + then 'stale' + when parity.run_status is null then 'pending' + when parity.run_status <> 'succeeded' then 'stale' + else 'current' + end::text as parity_status, + parity.parity_record_id, + parity.reconciliation_id, + parity.is_match as parity_is_match, + parity.legacy_dto_hash, + parity.indexed_dto_hash, + parity.compared_at as parity_compared_at, + parity.resolved_at as parity_resolved_at, + parity.source_from_block as parity_source_from_block, + parity.source_to_block as parity_source_to_block, + parity.evidence_commitment as parity_evidence_commitment, + parity.mismatch_count as reconciliation_mismatch_count, + parity.recorded_at as reconciliation_recorded_at, + parity.reconciliation_resolved_at, + parity.checkpoint_id as parity_checkpoint_id, + parity.checkpoint_generation as parity_checkpoint_generation, + parity.reorg_generation as parity_reorg_generation, + parity.checkpoint_block_number as parity_block_number, + parity.checkpoint_block_hash as parity_block_hash, + parity.parity_binding_id, + parity.binding_commitment as parity_binding_commitment, + parity.bound_at as parity_bound_at +from programmable_private.route_eligibility_current_exact_v1 as route +left join lateral ( + select + record.parity_record_id, + record.reconciliation_id, + record.is_match, + record.legacy_dto_hash, + record.indexed_dto_hash, + record.compared_at, + record.resolved_at, + reconciliation.source_from_block, + reconciliation.source_to_block, + reconciliation.evidence_commitment, + reconciliation.mismatch_count, + reconciliation.recorded_at, + reconciliation.resolved_at as reconciliation_resolved_at, + binding.parity_binding_id, + binding.checkpoint_id, + binding.projector_version, + binding.checkpoint_generation, + binding.reorg_generation, + binding.checkpoint_block_number, + binding.checkpoint_block_hash, + binding.binding_commitment, + binding.bound_at, + outcome.status as run_status + from programmable_private.parity_records as record + join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = record.reconciliation_id + and reconciliation.chain_id = route.chain_id + and reconciliation.release_id = route.release_id + and reconciliation.model_id = route.model_id + and reconciliation.epoch_id = route.epoch_id + and reconciliation.pointer_generation = route.pointer_generation + join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + and run.source_group = route.source_group + left join programmable_private.route_checkpoint_parity_bindings as binding + on binding.parity_record_id = record.parity_record_id + and binding.reconciliation_id = reconciliation.reconciliation_id + and binding.route_key = record.route_key + left join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id + where record.route_key = route.route_key + order by record.compared_at desc, record.parity_record_id desc + limit 1 +) as parity on true; + +-- Consolidated token DTO evidence. Unsupported or not-yet-materialized +-- values remain NULL and payload_complete remains false; API adapters must not +-- substitute estimates. This lets readiness and payload completeness fail +-- independently while keeping every exposed value tied to the exact route, +-- projection publication and source occurrence. +create view programmable_private.route_token_projections_v1 +with (security_invoker = false, security_barrier = true) +as +select + readiness.route_key, + readiness.chain_id, + readiness.release_id, + readiness.model_id, + readiness.source_group, + readiness.route_status, + readiness.route_mode, + readiness.parity_status, + readiness.parity_record_id, + readiness.reconciliation_id, + readiness.parity_evidence_commitment, + readiness.parity_binding_id, + readiness.parity_binding_commitment, + readiness.projector_version, + readiness.epoch_id, + readiness.pointer_generation, + readiness.checkpoint_id, + readiness.checkpoint_generation, + readiness.reorg_generation, + readiness.checkpoint_block_number, + readiness.checkpoint_block_hash, + readiness.checkpoint_created_at, + readiness.safe_block_number, + readiness.checkpoint_confirmations, + launch.projection_run_id, + publication_audit.input_commitment as publication_commitment, + launch.promoted_block_number, + launch.promoted_block_hash, + source_occurrence.block_number as launch_source_block_number, + source_occurrence.block_hash as launch_source_block_hash, + source_occurrence.block_global_log_index::bigint + as launch_source_block_global_log_index, + launch.launch_block_timestamp, + launch.launch_transaction_index, + launch.launch_receipt_log_ordinal, + launch.token, + launch.creator, + launch.launch_transaction_hash, + launch.pool_id, + launch.reward_vault, + launch.launch_hash, + launch.token_name, + launch.token_symbol, + launch.total_supply, + launch.currency0, + launch.currency1, + launch.hook, + launch.quote_asset, + launch.pool_key_fee, + launch.tick_spacing, + launch.buy_swap_fee_bps, + launch.sell_swap_fee_bps, + launch.buy_creator_fee_bps, + launch.sell_creator_fee_bps, + launch.creator_fee_bps, + launch.launcher_fee_bps, + launch.transfer_tax_bps, + launch.lp_fee_pips, + launch.project_name, + launch.project_description, + launch.project_logo_reference, + launch.project_metadata_revision, + launch.project_metadata_created_at, + launch.project_links, + launch.position_recipient, + launch.position_token_id, + launch.token_liquidity_amount, + launch.locked_token_dust, + launch.initial_sqrt_price_x96, + launch.initial_tick, + launch.tick_lower, + launch.tick_upper, + case + when source_occurrence.decoded_payload ->> 'protocolFeePips' + ~ '^(0|[1-9][0-9]{0,6})$' + then (source_occurrence.decoded_payload ->> 'protocolFeePips')::bigint + else null + end as protocol_fee_pips, + source_occurrence.decoded_payload -> 'extraData' as metadata_extra_data, + market.market_snapshot_id, + market.block_number as market_block_number, + market.block_hash as market_block_hash, + market.sqrt_price_x96 as market_sqrt_price_x96, + market.liquidity as market_liquidity, + market.tick as market_tick, + market.token0_price as market_token0_price, + market.token1_price as market_token1_price, + market.tvl_token0 as market_tvl_token0, + market.tvl_token1 as market_tvl_token1, + market.tvl_usd as market_tvl_usd, + market.market_volume_token0, + market.market_volume_token1, + market.market_volume_usd, + case + when launch.currency0 = pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + then market.market_volume_token0 + when launch.currency1 = pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + then market.market_volume_token1 + else null + end as market_volume_native, + market.transaction_count as market_swap_count, + fee_total.gross_total as accrued_gross_total, + fee_total.creator_fee_total as accrued_creator_total, + fee_total.launcher_fee_total as accrued_launcher_total, + creator_balance.claimable_accrued as creator_claimable_accrued, + launch.quote_asset as stock_quote_address, + source_occurrence.decoded_payload ->> 'quoteSymbol' + as stock_quote_symbol, + source_occurrence.decoded_payload ->> 'quoteName' as stock_quote_name, + case + when source_occurrence.decoded_payload ->> 'quoteDecimals' + ~ '^(0|[1-9][0-9]?)$' + and (source_occurrence.decoded_payload ->> 'quoteDecimals')::integer + between 0 and 36 + then (source_occurrence.decoded_payload ->> 'quoteDecimals')::integer + else null + end as stock_quote_decimals, + source_occurrence.decoded_payload ->> 'quoteCurrency' + as stock_quote_currency, + case + when launch.quote_asset = launch.currency0 then 'currency0' + when launch.quote_asset = launch.currency1 then 'currency1' + else null + end as stock_quote_currency_side, + case + when launch.quote_asset = launch.currency0 + then market.market_volume_token0 + when launch.quote_asset = launch.currency1 + then market.market_volume_token1 + else null + end as stock_quote_volume_total, + fee_total.gross_total as stock_quote_accrued_total, + initial_buy.custody_projection_id as initial_buy_custody_projection_id, + initial_buy.custody_address as initial_buy_custody_address, + initial_buy.custody_mode as initial_buy_custody_mode, + initial_buy.duration_days as initial_buy_duration_days, + initial_buy.cliff_days as initial_buy_cliff_days, + initial_buy.initial_buy_amount, + case + when source_occurrence.decoded_payload ->> 'initialBuyNativeWei' + ~ '^(0|[1-9][0-9]{0,77})$' + then source_occurrence.decoded_payload ->> 'initialBuyNativeWei' + end as initial_buy_native_wei, + case + when source_occurrence.decoded_payload ->> 'initialBuyQuoteRaw' + ~ '^(0|[1-9][0-9]{0,77})$' + then source_occurrence.decoded_payload ->> 'initialBuyQuoteRaw' + end as initial_buy_quote_raw, + ( + readiness.route_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' + and publication_audit.audit_id is not null + and source_occurrence.occurrence_id is not null + and launch.position_token_id is not null + and market.market_snapshot_id is not null + and market.market_volume_usd is not null + and market.transaction_count between 0 and 9007199254740991 + and source_occurrence.decoded_payload ->> 'protocolFeePips' + ~ '^(0|[1-9][0-9]{0,6})$' + and launch.buy_swap_fee_bps is not null + and launch.sell_swap_fee_bps is not null + and launch.buy_creator_fee_bps is not null + and launch.sell_creator_fee_bps is not null + and launch.launcher_fee_bps is not null + and launch.transfer_tax_bps is not null + and launch.lp_fee_pips is not null + and ( + readiness.model_id not like 'stock-paired%' + or ( + launch.quote_asset is not null + and source_occurrence.decoded_payload ->> 'quoteSymbol' is not null + and source_occurrence.decoded_payload ->> 'quoteName' is not null + and source_occurrence.decoded_payload ->> 'quoteDecimals' + ~ '^(0|[1-9][0-9]?)$' + and source_occurrence.decoded_payload ->> 'quoteCurrency' is not null + ) + ) + ) as payload_complete +from programmable_private.launch_by_token_v2 as launch +join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = 'explore-token' + and readiness.chain_id = launch.chain_id + and readiness.release_id = launch.release_id + and readiness.model_id = launch.model_id + and readiness.source_group = launch.source_group + and readiness.epoch_id = launch.epoch_id + and readiness.pointer_generation = launch.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = launch.projection_run_id + and publication.epoch_id = launch.epoch_id + and publication.pointer_generation = launch.pointer_generation + and publication.target_block_number = launch.promoted_block_number + and publication.target_block_hash = launch.promoted_block_hash +join programmable_private.mutation_audits as publication_audit + on publication_audit.audit_id = publication.audit_id +join programmable_private.current_launch_projections_v1 as launch_projection + on launch_projection.projection_run_id = launch.projection_run_id + and launch_projection.chain_id = launch.chain_id + and launch_projection.release_id = launch.release_id + and launch_projection.model_id = launch.model_id + and launch_projection.epoch_id = launch.epoch_id + and launch_projection.pointer_generation = launch.pointer_generation + and launch_projection.token = launch.token +join programmable_private.chain_event_materialized_occurrences_v1 + as source_occurrence + on source_occurrence.occurrence_id = + launch_projection.last_source_occurrence_id + and source_occurrence.logical_event_id = + launch_projection.last_source_logical_event_id + and source_occurrence.block_hash = + launch_projection.last_source_occurrence_block_hash + and source_occurrence.chain_id = launch.chain_id + and source_occurrence.release_id = launch.release_id + and source_occurrence.model_id = launch.model_id + and source_occurrence.source_group = launch.source_group + and source_occurrence.epoch_id = launch.epoch_id + and source_occurrence.pointer_generation = launch.pointer_generation +left join lateral ( + select snapshot.* + from programmable_private.market_snapshots_v2 as snapshot + where snapshot.chain_id = launch.chain_id + and snapshot.release_id = launch.release_id + and snapshot.model_id = launch.model_id + and snapshot.token = launch.token + and snapshot.pool_id = launch.pool_id + and snapshot.block_number <= readiness.checkpoint_block_number + order by snapshot.block_number desc, snapshot.market_snapshot_id desc + limit 1 +) as market on true +left join programmable_private.current_pool_fee_totals_v1 as fee_total + on fee_total.chain_id = launch.chain_id + and fee_total.release_id = launch.release_id + and fee_total.model_id = launch.model_id + and fee_total.epoch_id = launch.epoch_id + and fee_total.pointer_generation = launch.pointer_generation + and fee_total.pool_id = launch.pool_id + and fee_total.quote_asset is not distinct from launch.quote_asset +left join programmable_private.current_account_reward_balances_v1 + as creator_balance + on creator_balance.chain_id = launch.chain_id + and creator_balance.release_id = launch.release_id + and creator_balance.model_id = launch.model_id + and creator_balance.epoch_id = launch.epoch_id + and creator_balance.pointer_generation = launch.pointer_generation + and creator_balance.account = launch.creator + and creator_balance.vault = launch.reward_vault +left join lateral ( + select + custody.custody_projection_id, + custody.custody_address, + custody.custody_mode, + custody.duration_days, + custody.cliff_days, + pg_catalog.sum(vesting.amount) as initial_buy_amount + from programmable_private.initial_buy_custody_projections as custody + left join programmable_private.initial_buy_vesting_projections as vesting + on vesting.custody_projection_id = custody.custody_projection_id + and vesting.projection_run_id = custody.projection_run_id + and vesting.chain_id = custody.chain_id + and vesting.release_id = custody.release_id + and vesting.model_id = custody.model_id + and vesting.epoch_id = custody.epoch_id + and vesting.pointer_generation = custody.pointer_generation + where custody.launch_projection_id = launch_projection.launch_projection_id + and custody.projection_run_id = launch_projection.projection_run_id + group by custody.custody_projection_id, custody.custody_address, + custody.custody_mode, custody.duration_days, custody.cliff_days +) as initial_buy on true; + +-- Public DTO construction is centralized so list, detail, creator and feed +-- readers cannot drift in address formatting, fee semantics or omission +-- rules. Optional legacy keys are absent unless their normalized evidence is +-- present; json nulls are never used to make an incomplete projection appear +-- complete. +create function programmable_private.build_public_launcher_token_v1( + p_row jsonb +) +returns jsonb +language sql +stable +strict +security definer +set search_path = '' +as $function$ + with normalized as ( + select + p_row as row_value, + case + when p_row ->> 'quote_asset' = + '\\x0000000000000000000000000000000000000000' + and p_row ->> 'token' = p_row ->> 'currency0' + then (p_row ->> 'market_token0_price')::numeric + when p_row ->> 'quote_asset' = + '\\x0000000000000000000000000000000000000000' + and p_row ->> 'token' = p_row ->> 'currency1' + then (p_row ->> 'market_token1_price')::numeric + else null + end as token_price_native, + case + when p_row ->> 'quote_asset' is not null + and p_row ->> 'quote_asset' <> + '\\x0000000000000000000000000000000000000000' + and p_row ->> 'token' = p_row ->> 'currency0' + then (p_row ->> 'market_token0_price')::numeric + when p_row ->> 'quote_asset' is not null + and p_row ->> 'quote_asset' <> + '\\x0000000000000000000000000000000000000000' + and p_row ->> 'token' = p_row ->> 'currency1' + then (p_row ->> 'market_token1_price')::numeric + else null + end as token_price_quote, + case + when p_row ->> 'currency0' = + '\\x0000000000000000000000000000000000000000' + then (p_row ->> 'market_volume_token0')::numeric + when p_row ->> 'currency1' = + '\\x0000000000000000000000000000000000000000' + then (p_row ->> 'market_volume_token1')::numeric + else null + end as gross_volume_native, + case + when p_row ->> 'quote_asset' = p_row ->> 'currency0' + then (p_row ->> 'market_volume_token0')::numeric + when p_row ->> 'quote_asset' = p_row ->> 'currency1' + then (p_row ->> 'market_volume_token1')::numeric + else null + end as gross_volume_quote + ), derived as ( + select normalized.*, + case when token_price_native is not null then + pg_catalog.trunc(token_price_native * 1000000000000000000)::numeric + end as token_price_native_wei, + case when token_price_quote is not null then + pg_catalog.trunc(token_price_quote * 1000000000000000000)::numeric + end as token_price_quote_wad, + case when token_price_native is not null then + pg_catalog.trunc( + (p_row ->> 'total_supply')::numeric * token_price_native + )::numeric + end as market_cap_native_wei, + case when token_price_quote is not null then + pg_catalog.trunc( + (p_row ->> 'total_supply')::numeric * token_price_quote + )::numeric + end as market_cap_quote_wad + from normalized + ), enriched as ( + select derived.*, + case + when market_cap_native_wei is not null + and p_row ->> 'global_answer' is not null + and p_row ->> 'global_decimals' is not null + then pg_catalog.trunc( + market_cap_native_wei * (p_row ->> 'global_answer')::numeric + / pg_catalog.power(10::numeric, + (p_row ->> 'global_decimals')::integer) + )::numeric + end as market_cap_usd_wad + from derived + ) + select pg_catalog.jsonb_strip_nulls( + pg_catalog.jsonb_build_object( + 'id', (p_row ->> 'chain_id') || ':0x' + || pg_catalog.substr(p_row ->> 'token', 3), + 'name', p_row ->> 'token_name', + 'symbol', p_row ->> 'token_symbol', + 'tokenAddress', '0x' || pg_catalog.substr(p_row ->> 'token', 3), + 'hookAddress', '0x' || pg_catalog.substr(p_row ->> 'hook', 3), + 'poolId', '0x' || pg_catalog.substr(p_row ->> 'pool_id', 3), + 'totalSwapFeeBps', greatest( + (p_row ->> 'buy_swap_fee_bps')::integer, + (p_row ->> 'sell_swap_fee_bps')::integer + ), + 'launchedAt', pg_catalog.to_char( + (p_row ->> 'launch_block_timestamp')::timestamptz + at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ), + 'liquidityPath', 'meme', + 'description', p_row ->> 'project_description', + 'imageUrl', p_row ->> 'project_logo_reference', + 'links', case + when pg_catalog.jsonb_typeof(p_row -> 'project_links') = 'array' + and pg_catalog.jsonb_array_length(p_row -> 'project_links') > 0 + then ( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'kind', link ->> 'kind', 'url', link ->> 'url' + ) order by (link ->> 'displayOrder')::integer, link ->> 'kind' + ) + from pg_catalog.jsonb_array_elements( + p_row -> 'project_links' + ) as link + where link ->> 'kind' in ('website', 'x', 'telegram') + ) + end, + 'creatorAddress', '0x' || pg_catalog.substr( + p_row ->> 'creator', 3 + ), + 'positionRecipient', case + when p_row ->> 'position_recipient' is not null + then '0x' || pg_catalog.substr(p_row ->> 'position_recipient', 3) + end, + 'positionTokenId', p_row ->> 'position_token_id', + 'launchHash', '0x' || pg_catalog.substr(p_row ->> 'launch_hash', 3), + 'launchBlockNumber', p_row ->> 'launch_source_block_number', + 'launchTransactionHash', '0x' || pg_catalog.substr( + p_row ->> 'launch_transaction_hash', 3 + ), + 'launchTransactionIndex', + (p_row ->> 'launch_transaction_index')::integer, + 'launchLogIndex', + (p_row ->> 'launch_source_block_global_log_index')::bigint, + 'totalSupply', ( + (p_row ->> 'total_supply')::numeric / 1000000000000000000 + )::text, + 'totalSupplyRaw', p_row ->> 'total_supply', + 'tokenDecimals', 18, + 'tokenLiquidityAmountRaw', p_row ->> 'token_liquidity_amount', + 'lockedTokenDustRaw', p_row ->> 'locked_token_dust' + ) || pg_catalog.jsonb_build_object( + 'tokenPriceEth', token_price_native::text, + 'tokenPriceEthWei', token_price_native_wei::text, + 'marketCapEth', + (market_cap_native_wei / 1000000000000000000)::text, + 'marketCapEthWei', market_cap_native_wei::text, + 'indexedMarketCapEth', + (market_cap_native_wei / 1000000000000000000)::text, + 'indexedMarketCapEthWei', market_cap_native_wei::text, + 'indexedMarketCapUsdWad', market_cap_usd_wad::text, + 'indexedValuationBlockNumber', p_row ->> 'market_block_number', + 'grossVolumeEth', gross_volume_native::text, + 'grossVolumeWei', pg_catalog.trunc( + gross_volume_native * 1000000000000000000 + )::text, + 'creatorFeesGeneratedEth', ( + (p_row ->> 'accrued_creator_total')::numeric + / 1000000000000000000 + )::text, + 'creatorFeesGeneratedWei', p_row ->> 'accrued_creator_total', + 'launcherFeesGeneratedEth', ( + (p_row ->> 'accrued_launcher_total')::numeric + / 1000000000000000000 + )::text, + 'launcherFeesGeneratedWei', p_row ->> 'accrued_launcher_total', + 'creatorFeesAccruedEth', ( + (p_row ->> 'accrued_creator_total')::numeric + / 1000000000000000000 + )::text, + 'creatorFeesAccruedWei', p_row ->> 'accrued_creator_total', + 'swapCount', (p_row ->> 'market_swap_count')::bigint, + 'currentTick', (p_row ->> 'market_tick')::integer, + 'initialTick', (p_row ->> 'initial_tick')::integer, + 'tickLower', (p_row ->> 'tick_lower')::integer, + 'tickUpper', (p_row ->> 'tick_upper')::integer, + 'activeLiquidity', p_row ->> 'market_liquidity', + 'protocolFeePips', (p_row ->> 'protocol_fee_pips')::bigint, + 'lpFeePips', p_row ->> 'lp_fee_pips', + 'buyHookFeeBps', (p_row ->> 'buy_swap_fee_bps')::integer, + 'sellHookFeeBps', (p_row ->> 'sell_swap_fee_bps')::integer, + 'creatorFeeBps', (p_row ->> 'creator_fee_bps')::integer, + 'buyCreatorFeeBps', (p_row ->> 'buy_creator_fee_bps')::integer, + 'sellCreatorFeeBps', (p_row ->> 'sell_creator_fee_bps')::integer, + 'programmableFeeBps', (p_row ->> 'launcher_fee_bps')::integer, + 'launcherFeeBps', (p_row ->> 'launcher_fee_bps')::integer, + 'transferTaxBps', (p_row ->> 'transfer_tax_bps')::integer, + 'launchModel', case + when p_row ->> 'model_id' like 'stock-paired%' then 'stock-paired' + else 'classic' + end, + 'launchModelVersion', p_row ->> 'release_id', + 'rewardVaultAddress', case + when p_row ->> 'reward_vault' is not null + then '0x' || pg_catalog.substr(p_row ->> 'reward_vault', 3) + end, + 'metadataExtraData', p_row -> 'metadata_extra_data' + ) || pg_catalog.jsonb_build_object( + 'quoteAssetAddress', case + when p_row ->> 'stock_quote_address' is not null + then '0x' || pg_catalog.substr(p_row ->> 'stock_quote_address', 3) + end, + 'quoteAssetSymbol', p_row ->> 'stock_quote_symbol', + 'quoteAssetName', p_row ->> 'stock_quote_name', + 'quoteIsCurrency0', case + when p_row ->> 'stock_quote_currency_side' = 'currency0' then true + when p_row ->> 'stock_quote_currency_side' = 'currency1' then false + end, + 'tokenPriceQuote', token_price_quote::text, + 'tokenPriceQuoteWad', token_price_quote_wad::text, + 'marketCapQuote', + (market_cap_quote_wad / 1000000000000000000)::text, + 'marketCapQuoteWad', market_cap_quote_wad::text, + 'grossVolumeQuote', gross_volume_quote::text, + 'grossVolumeQuoteRaw', pg_catalog.trunc( + gross_volume_quote * 1000000000000000000 + )::text, + 'creatorFeesGeneratedQuote', ( + (p_row ->> 'stock_quote_accrued_total')::numeric + / pg_catalog.power( + 10::numeric, (p_row ->> 'stock_quote_decimals')::integer + ) + )::text, + 'creatorFeesGeneratedQuoteRaw', p_row ->> 'stock_quote_accrued_total', + 'programmableFeesGeneratedQuote', ( + (p_row ->> 'accrued_launcher_total')::numeric + / pg_catalog.power( + 10::numeric, (p_row ->> 'stock_quote_decimals')::integer + ) + )::text, + 'programmableFeesGeneratedQuoteRaw', p_row ->> 'accrued_launcher_total', + 'creatorFeesAccruedQuote', ( + (p_row ->> 'stock_quote_accrued_total')::numeric + / pg_catalog.power( + 10::numeric, (p_row ->> 'stock_quote_decimals')::integer + ) + )::text, + 'creatorFeesAccruedQuoteRaw', p_row ->> 'stock_quote_accrued_total', + 'fdvUsdWad', market_cap_usd_wad::text + ) + ) + from enriched +$function$; + +-- The detail payload is the strictest token materialization. It returns no +-- row until the exact explore-token checkpoint has current matching parity, +-- the projection publication is bound, the launch/pool/liquidity sources are +-- canonical, market data is covered by the checkpoint and Stock-Paired rows +-- also carry their dynamic asset and initial-buy bindings. +create view programmable_private.public_explore_token_v1 +with (security_invoker = false, security_barrier = true) +as +select + token.route_key, + token.chain_id, + token.release_id, + token.model_id, + token.source_group, + token.projector_version, + token.epoch_id, + token.pointer_generation, + token.checkpoint_id, + token.checkpoint_generation, + token.reorg_generation, + token.checkpoint_block_number, + token.checkpoint_block_hash, + token.safe_block_number, + token.checkpoint_confirmations, + token.parity_status, + token.parity_record_id, + token.reconciliation_id, + token.parity_evidence_commitment, + token.parity_binding_id, + token.parity_binding_commitment, + token.projection_run_id, + token.publication_commitment, + 200::integer as http_status, + pg_catalog.jsonb_build_object( + 'status', 'ready', + 'token', programmable_private.build_public_launcher_token_v1( + pg_catalog.to_jsonb(token.*) + || pg_catalog.jsonb_build_object( + 'global_answer', global_snapshot.answer, + 'global_decimals', global_snapshot.decimals + ) + ), + 'snapshot', pg_catalog.jsonb_strip_nulls( + pg_catalog.jsonb_build_object( + 'chainId', token.chain_id, + 'blockNumber', token.checkpoint_block_number::text, + 'blockHash', '0x' || pg_catalog.encode( + token.checkpoint_block_hash, 'hex' + ), + 'confirmations', token.checkpoint_confirmations, + 'ethUsdQuote', case + when global_snapshot.global_market_snapshot_id is not null then + pg_catalog.jsonb_build_object( + 'feedAddress', '0x' || pg_catalog.encode( + global_snapshot.feed_address, 'hex' + ), + 'roundId', global_snapshot.feed_round_id::text, + 'answer', global_snapshot.answer::text, + 'decimals', global_snapshot.decimals, + 'updatedAt', pg_catalog.to_char( + global_snapshot.feed_updated_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) + ) + end + ) + ) + ) as payload, + true as payload_complete +from programmable_private.route_token_projections_v1 as token +join programmable_private.market_snapshots_v2 as market + on market.market_snapshot_id = token.market_snapshot_id + and market.block_number <= token.checkpoint_block_number +join programmable_private.reconciliation_records as market_reconciliation + on market_reconciliation.reconciliation_id = market.reconciliation_id + and market_reconciliation.source_to_block = token.checkpoint_block_number + and market_reconciliation.mismatch_count = 0 +join programmable_private.global_eth_usd_snapshots_v1 as global_snapshot + on global_snapshot.global_market_snapshot_id = + market.global_market_snapshot_id +left join programmable_private.dynamic_source_release_asset_bindings + as stock_binding + on stock_binding.launch_occurrence_id = ( + select launch_projection.last_source_occurrence_id + from programmable_private.current_launch_projections_v1 + as launch_projection + where launch_projection.projection_run_id = token.projection_run_id + and launch_projection.token = token.token + ) + and stock_binding.chain_id = token.chain_id + and stock_binding.release_id = token.release_id + and stock_binding.model_id = token.model_id + and stock_binding.source_group = token.source_group + and stock_binding.epoch_id = token.epoch_id + and stock_binding.pointer_generation = token.pointer_generation + and stock_binding.token = token.token + and stock_binding.pool_id = token.pool_id + and stock_binding.hook = token.hook + and stock_binding.quote_asset = token.quote_asset +where token.route_status = 'eligible' + and token.route_mode = 'indexed' + and token.parity_status = 'current' + and token.payload_complete + and ( + token.model_id not like 'stock-paired%' + or ( + stock_binding.dynamic_source_release_asset_binding_id is not null + and token.initial_buy_custody_projection_id is not null + and token.initial_buy_amount is not null + ) + ); + +-- The application adapter owns all presentation formatting and +-- release-specific Legacy DTO shape. Replace the transient serializer above +-- with the frozen raw IndexedRouteEnvelopeV2 contract; SQL supplies only +-- atomic values and immutable evidence. +drop view programmable_private.public_explore_token_v1; +drop function programmable_private.build_public_launcher_token_v1(jsonb); + +create function programmable_private.build_indexed_token_projection_v2( + p_row jsonb +) +returns jsonb +language sql +stable +strict +security definer +set search_path = '' +as $function$ + with values as ( + select + case + when p_row ->> 'token' = p_row ->> 'currency0' + then (p_row ->> 'market_token0_price')::numeric + when p_row ->> 'token' = p_row ->> 'currency1' + then (p_row ->> 'market_token1_price')::numeric + end as token_price_quote, + case + when p_row ->> 'quote_asset' = p_row ->> 'currency0' + then (p_row ->> 'market_volume_token0')::numeric + when p_row ->> 'quote_asset' = p_row ->> 'currency1' + then (p_row ->> 'market_volume_token1')::numeric + end as volume_quote, + case + when p_row ->> 'stock_quote_decimals' + ~ '^(0|[1-9][0-9]?)$' + then (p_row ->> 'stock_quote_decimals')::integer + end as quote_decimals, + p_row ->> 'stock_quote_address_hex' = + '0x0000000000000000000000000000000000000000' + as is_native_quote + ), atomic as ( + select values.*, + case when token_price_quote is not null then + pg_catalog.trunc( + token_price_quote * 1000000000000000000 + )::numeric + end as token_price_quote_wad, + case when token_price_quote is not null then + pg_catalog.trunc( + (p_row ->> 'total_supply')::numeric * token_price_quote + )::numeric + end as market_cap_quote_wad, + case when volume_quote is not null and quote_decimals is not null then + pg_catalog.trunc( + volume_quote * pg_catalog.power(10::numeric, quote_decimals) + )::numeric + end as volume_quote_raw, + case when is_native_quote and token_price_quote is not null then + pg_catalog.trunc( + token_price_quote * 1000000000000000000 + )::numeric + end as token_price_native_wei, + case when is_native_quote and token_price_quote is not null then + pg_catalog.trunc( + (p_row ->> 'total_supply')::numeric * token_price_quote + )::numeric + end as market_cap_native_wei, + case when is_native_quote + and p_row ->> 'market_volume_native' is not null + then pg_catalog.trunc( + (p_row ->> 'market_volume_native')::numeric + * 1000000000000000000 + )::numeric end as volume_native_wei + from values + ) + select pg_catalog.jsonb_build_object( + 'source', pg_catalog.jsonb_build_object( + 'routeKey', p_row ->> 'route_key', + 'chainId', (p_row ->> 'chain_id')::bigint, + 'releaseVersion', p_row ->> 'release_id', + 'modelVersion', p_row ->> 'model_id', + 'sourceGroup', p_row ->> 'source_group', + 'projectorVersion', p_row ->> 'projector_version', + 'epochId', p_row ->> 'epoch_id', + 'pointerGeneration', p_row ->> 'pointer_generation', + 'checkpointId', p_row ->> 'checkpoint_id', + 'checkpointGeneration', p_row ->> 'checkpoint_generation', + 'reorgGeneration', p_row ->> 'reorg_generation', + 'checkpointBlockNumber', p_row ->> 'checkpoint_block_number', + 'checkpointBlockHash', p_row ->> 'checkpoint_block_hash_hex', + 'snapshotCommitment', p_row ->> 'snapshot_commitment_hex', + 'projectionRunId', p_row ->> 'projection_run_id', + 'publicationCommitment', p_row ->> 'publication_commitment_hex', + 'promotedBlockNumber', p_row ->> 'promoted_block_number', + 'promotedBlockHash', p_row ->> 'promoted_block_hash_hex' + ), + 'tokenAddress', p_row ->> 'token_hex', + 'hookAddress', p_row ->> 'hook_hex', + 'poolId', p_row ->> 'pool_id_hex', + 'creatorAddress', p_row ->> 'creator_hex', + 'positionRecipient', p_row ->> 'position_recipient_hex', + 'positionTokenId', p_row ->> 'position_token_id', + 'rewardVaultAddress', p_row ->> 'reward_vault_hex', + 'launchHash', p_row ->> 'launch_hash_hex', + 'launchBlockNumber', p_row ->> 'launch_source_block_number', + 'launchTransactionHash', p_row ->> 'launch_transaction_hash_hex', + 'launchTransactionIndex', + (p_row ->> 'launch_transaction_index')::bigint, + 'launchLogIndex', + (p_row ->> 'launch_receipt_log_ordinal')::bigint, + 'launchedAt', p_row ->> 'launch_timestamp_iso', + 'name', p_row ->> 'token_name', + 'symbol', p_row ->> 'token_symbol', + -- All five allowlisted public releases bind UERC20's immutable 18-decimal + -- token standard. Unsupported release/model pairs never reach a public + -- envelope. + 'decimals', 18, + 'totalSupplyRaw', p_row ->> 'total_supply', + 'metadata', case + when p_row ->> 'project_metadata_revision' is not null + and p_row ->> 'project_metadata_created_at' is not null + and p_row ->> 'metadata_extra_data_hex' + ~ '^0x([0-9a-f][0-9a-f])*$' + then pg_catalog.jsonb_build_object( + 'revision', p_row ->> 'project_metadata_revision', + 'createdAt', p_row ->> 'project_metadata_created_at_iso', + 'description', p_row -> 'project_description', + 'imageUrl', p_row -> 'project_logo_reference', + 'links', coalesce(p_row -> 'project_links', '[]'::jsonb), + 'extraData', p_row ->> 'metadata_extra_data_hex' + ) + else null + end, + 'liquidity', pg_catalog.jsonb_build_object( + 'tokenLiquidityAmountRaw', p_row ->> 'token_liquidity_amount', + 'lockedTokenDustRaw', p_row ->> 'locked_token_dust', + 'currentTick', p_row -> 'market_tick', + 'initialTick', p_row -> 'initial_tick', + 'tickLower', p_row -> 'tick_lower', + 'tickUpper', p_row -> 'tick_upper', + 'activeLiquidity', p_row ->> 'market_liquidity' + ), + 'fees', pg_catalog.jsonb_build_object( + 'totalSwapFeeBps', greatest( + (p_row ->> 'buy_swap_fee_bps')::integer, + (p_row ->> 'sell_swap_fee_bps')::integer + ), + 'buySwapFeeBps', (p_row ->> 'buy_swap_fee_bps')::integer, + 'sellSwapFeeBps', (p_row ->> 'sell_swap_fee_bps')::integer, + 'buyCreatorFeeBps', (p_row ->> 'buy_creator_fee_bps')::integer, + 'sellCreatorFeeBps', (p_row ->> 'sell_creator_fee_bps')::integer, + 'launcherFeeBps', (p_row ->> 'launcher_fee_bps')::integer, + 'transferTaxBps', (p_row ->> 'transfer_tax_bps')::integer, + 'lpFeePips', (p_row ->> 'lp_fee_pips')::integer, + 'protocolFeePips', (p_row ->> 'protocol_fee_pips')::integer + ), + 'market', pg_catalog.jsonb_build_object( + 'tokenPriceNativeWei', token_price_native_wei::text, + 'marketCapNativeWei', market_cap_native_wei::text, + 'indexedMarketCapNativeWei', market_cap_native_wei::text, + 'indexedMarketCapUsdWad', null, + 'indexedValuationBlockNumber', p_row ->> 'market_block_number', + 'fdvUsdWad', null, + 'grossVolumeNativeWei', volume_native_wei::text, + 'creatorFeesGeneratedNativeWei', case + when is_native_quote then p_row ->> 'accrued_creator_total' + else null + end, + 'launcherFeesGeneratedNativeWei', case + when is_native_quote then p_row ->> 'accrued_launcher_total' + else null + end, + 'creatorFeesAccruedNativeWei', case + when is_native_quote then p_row ->> 'creator_claimable_accrued' + else null + end, + 'swapCount', p_row -> 'market_swap_count' + ), + 'quote', case + when p_row ->> 'model_id' = 'stock-paired' + and quote_decimals is not null + and p_row ->> 'stock_quote_symbol' is not null + and p_row ->> 'stock_quote_name' is not null + and token_price_quote_wad is not null + and market_cap_quote_wad is not null + and volume_quote_raw is not null + and p_row ->> 'creator_claimable_accrued' is not null + then pg_catalog.jsonb_build_object( + 'address', p_row ->> 'stock_quote_address_hex', + 'symbol', p_row ->> 'stock_quote_symbol', + 'name', p_row ->> 'stock_quote_name', + 'decimals', quote_decimals, + 'isCurrency0', + (p_row ->> 'stock_quote_currency_side') = 'currency0', + 'tokenPriceQuoteWad', token_price_quote_wad::text, + 'marketCapQuoteWad', market_cap_quote_wad::text, + 'grossVolumeQuoteRaw', volume_quote_raw::text, + 'creatorFeesGeneratedQuoteRaw', + p_row ->> 'accrued_creator_total', + 'programmableFeesGeneratedQuoteRaw', + p_row ->> 'accrued_launcher_total', + 'creatorFeesAccruedQuoteRaw', + p_row ->> 'creator_claimable_accrued' + ) + else null + end, + 'initialBuy', case + when p_row ->> 'initial_buy_native_wei' is not null + and p_row ->> 'initial_buy_amount' is not null + then pg_catalog.jsonb_build_object( + 'nativeWei', p_row ->> 'initial_buy_native_wei', + 'quoteRaw', p_row ->> 'initial_buy_quote_raw', + 'tokenRaw', p_row ->> 'initial_buy_amount' + ) + else null + end, + 'uniswapV4Pool', case + when p_row ->> 'market_snapshot_id' is not null then + pg_catalog.jsonb_build_object( + 'source', 'official-uniswap-v4-subgraph', + 'indexedBlockNumber', p_row ->> 'market_block_number', + 'indexedBlockHash', p_row ->> 'market_block_hash_hex', + 'volumeUsdWad', pg_catalog.trunc( + (p_row ->> 'market_volume_usd')::numeric + * 1000000000000000000 + )::text, + 'tvlUsdWad', pg_catalog.trunc( + (p_row ->> 'market_tvl_usd')::numeric + * 1000000000000000000 + )::text, + 'transactionCount', p_row ->> 'market_swap_count', + 'liquidity', p_row ->> 'market_liquidity', + 'sqrtPriceX96', p_row ->> 'market_sqrt_price_x96', + 'tick', (p_row ->> 'market_tick')::integer, + 'feeTierPips', p_row ->> 'pool_key_fee' + ) + else null + end + ) + from atomic +$function$; + +-- Route snapshots are materialized for the exact release sets accepted by the +-- frozen V2 adapters. A route may have more than one public scope (for +-- example creator-profile all-releases and Stock-only, or launch-lookup +-- Classic-v3 and Stock-only), so snapshot_scope is part of the key. Each row +-- exists only when every release in that exact scope is current at one +-- immutable checkpoint. +create view programmable_private.public_route_snapshots_v2 +with (security_invoker = false, security_barrier = true) +as +with scopes(route_key, snapshot_scope, release_ids, expected_count) as ( + values + ( + 'explore-list'::text, 'all-supported'::text, + array[ + 'classic-v2', 'classic-v3', 'stock-paired-v1', + 'stock-paired-v2', 'stock-paired-v3' + ]::text[], 5::bigint + ), + ( + 'explore-token', 'all-supported', + array[ + 'classic-v2', 'classic-v3', 'stock-paired-v1', + 'stock-paired-v2', 'stock-paired-v3' + ]::text[], 5 + ), + ( + 'explore-chart', 'all-supported', + array[ + 'classic-v2', 'classic-v3', 'stock-paired-v1', + 'stock-paired-v2', 'stock-paired-v3' + ]::text[], 5 + ), + ( + 'creator-profile', 'all-supported', + array[ + 'classic-v2', 'classic-v3', 'stock-paired-v1', + 'stock-paired-v2', 'stock-paired-v3' + ]::text[], 5 + ), + ( + 'creator-profile', 'stock-paired', + array[ + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ]::text[], 3 + ), + ( + 'classic-v3-profile', 'classic-v3', + array['classic-v3']::text[], 1 + ), + ( + 'launch-lookup', 'classic-v3', + array['classic-v3']::text[], 1 + ), + ( + 'launch-lookup', 'stock-paired', + array[ + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ]::text[], 3 + ) +) +select + readiness.route_key, + scopes.snapshot_scope, + readiness.chain_id, + readiness.checkpoint_block_number, + readiness.checkpoint_block_hash, + pg_catalog.min(readiness.safe_block_number) + as safe_block_number, + pg_catalog.min(readiness.checkpoint_confirmations) + as checkpoint_confirmations, + pg_catalog.max(readiness.checkpoint_created_at) + as snapshot_captured_at, + pg_catalog.min(readiness.parity_bound_at) as reconciled_at, + '0x' || pg_catalog.encode( + readiness.checkpoint_block_hash, 'hex' + ) as snapshot_commitment_hex, + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'routeKey', readiness.route_key, + 'chainId', readiness.chain_id, + 'releaseVersion', readiness.release_id, + 'modelVersion', readiness.model_id, + 'sourceGroup', readiness.source_group, + 'projectorVersion', readiness.projector_version, + 'epochId', readiness.epoch_id, + 'pointerGeneration', readiness.pointer_generation::text, + 'checkpointId', readiness.checkpoint_id, + 'checkpointGeneration', readiness.checkpoint_generation::text, + 'reorgGeneration', readiness.reorg_generation::text, + 'checkpointBlockNumber', + readiness.checkpoint_block_number::text, + 'checkpointBlockHash', '0x' || pg_catalog.encode( + readiness.checkpoint_block_hash, 'hex' + ) + ) order by readiness.release_id, readiness.model_id + ) as release_pointers, + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'model', readiness.model_id, + 'releaseVersion', readiness.release_id + ) order by readiness.release_id, readiness.model_id + ) as record_scopes, + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'releaseVersion', readiness.release_id, + 'modelVersion', readiness.model_id, + 'parityRecordId', readiness.parity_record_id, + 'reconciliationId', readiness.reconciliation_id, + 'parityEvidenceCommitment', '0x' || pg_catalog.encode( + readiness.parity_evidence_commitment, 'hex' + ), + 'parityBindingId', readiness.parity_binding_id, + 'parityBindingCommitment', '0x' || pg_catalog.encode( + readiness.parity_binding_commitment, 'hex' + ), + 'parityBoundAt', pg_catalog.to_char( + readiness.parity_bound_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) + ) order by readiness.release_id, readiness.model_id + ) as route_evidence +from scopes +join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = scopes.route_key + and readiness.release_id = any(scopes.release_ids) +where readiness.route_status = 'eligible' + and readiness.eligibility_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' + and ( + readiness.release_id = 'classic-v2' + and readiness.model_id = 'classic' + or readiness.release_id = 'classic-v3' + and readiness.model_id = 'classic' + or readiness.release_id = 'stock-paired-v1' + and readiness.model_id = 'stock-paired' + or readiness.release_id = 'stock-paired-v2' + and readiness.model_id = 'stock-paired' + or readiness.release_id = 'stock-paired-v3' + and readiness.model_id = 'stock-paired' + ) +group by readiness.route_key, scopes.snapshot_scope, + scopes.expected_count, readiness.chain_id, + readiness.checkpoint_block_number, readiness.checkpoint_block_hash +having pg_catalog.count(*) = scopes.expected_count + and pg_catalog.count(distinct ( + readiness.release_id, readiness.model_id + )) = scopes.expected_count; + +create view programmable_private.public_explore_token_v1 +with (security_invoker = false, security_barrier = true) +as +with materialized as ( + select token.*, + '0x' || pg_catalog.encode(token.checkpoint_block_hash, 'hex') + as checkpoint_block_hash_hex, + snapshot.snapshot_commitment_hex + as snapshot_commitment_hex, + '0x' || pg_catalog.encode(token.publication_commitment, 'hex') + as publication_commitment_hex, + '0x' || pg_catalog.encode(token.promoted_block_hash, 'hex') + as promoted_block_hash_hex, + '0x' || pg_catalog.encode(token.token, 'hex') as token_hex, + '0x' || pg_catalog.encode(token.hook, 'hex') as hook_hex, + '0x' || pg_catalog.encode(token.pool_id, 'hex') as pool_id_hex, + '0x' || pg_catalog.encode(token.creator, 'hex') as creator_hex, + case when token.position_recipient is not null then + '0x' || pg_catalog.encode(token.position_recipient, 'hex') + end as position_recipient_hex, + case when token.reward_vault is not null then + '0x' || pg_catalog.encode(token.reward_vault, 'hex') + end as reward_vault_hex, + '0x' || pg_catalog.encode(token.launch_hash, 'hex') as launch_hash_hex, + '0x' || pg_catalog.encode(token.launch_transaction_hash, 'hex') + as launch_transaction_hash_hex, + '0x' || pg_catalog.encode(token.market_block_hash, 'hex') + as market_block_hash_hex, + '0x' || pg_catalog.encode(token.stock_quote_address, 'hex') + as stock_quote_address_hex, + case + when pg_catalog.jsonb_typeof(token.metadata_extra_data) = 'string' + and token.metadata_extra_data #>> '{}' + ~ '^0x([0-9a-fA-F][0-9a-fA-F])*$' + then pg_catalog.lower(token.metadata_extra_data #>> '{}') + end as metadata_extra_data_hex, + pg_catalog.to_char( + token.launch_block_timestamp at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) as launch_timestamp_iso, + pg_catalog.to_char( + token.project_metadata_created_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) as project_metadata_created_at_iso, + pg_catalog.to_char( + snapshot.snapshot_captured_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) as checkpoint_created_at_iso, + snapshot.release_pointers, + snapshot.route_evidence, + snapshot.record_scopes as snapshot_record_scopes, + pg_catalog.to_jsonb(token) as base_json + from programmable_private.route_token_projections_v1 as token + join programmable_private.public_route_snapshots_v2 as snapshot + on snapshot.route_key = 'explore-token' + and snapshot.snapshot_scope = 'all-supported' + and snapshot.chain_id = token.chain_id + and snapshot.checkpoint_block_number = token.checkpoint_block_number + and snapshot.checkpoint_block_hash = token.checkpoint_block_hash + where token.route_status = 'eligible' + and token.route_mode = 'indexed' + and token.parity_status = 'current' + and token.payload_complete + and token.release_id in ( + 'classic-v2', 'classic-v3', 'stock-paired-v1', + 'stock-paired-v2', 'stock-paired-v3' + ) + and ( + token.release_id like 'classic-%' and token.model_id = 'classic' + or token.release_id like 'stock-paired-%' + and token.model_id = 'stock-paired' + ) +), raw as ( + select materialized.*, + base_json || pg_catalog.to_jsonb(materialized) + as projection_json + from materialized +) +select + raw.route_key, + raw.chain_id, + raw.release_id, + raw.model_id, + raw.source_group, + raw.projector_version, + raw.epoch_id, + raw.pointer_generation, + raw.checkpoint_id, + raw.checkpoint_generation, + raw.reorg_generation, + raw.checkpoint_block_number, + raw.checkpoint_block_hash, + raw.safe_block_number, + raw.checkpoint_confirmations, + raw.parity_status, + raw.parity_record_id, + raw.reconciliation_id, + raw.parity_evidence_commitment, + raw.parity_binding_id, + raw.parity_binding_commitment, + raw.projection_run_id, + raw.publication_commitment, + raw.checkpoint_block_number as comparison_checkpoint_block_number, + raw.checkpoint_block_hash as comparison_checkpoint_block_hash, + 1::bigint as record_count, + pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object( + 'model', raw.model_id, 'releaseVersion', raw.release_id + ) + ) as record_scopes, + 200::integer as http_status, + pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', pg_catalog.jsonb_strip_nulls( + pg_catalog.jsonb_build_object( + 'adapterVersion', 'indexed-route-adapters-v2', + 'snapshotCommitment', raw.snapshot_commitment_hex, + 'chainId', raw.chain_id, + 'blockNumber', raw.checkpoint_block_number::text, + 'blockHash', raw.checkpoint_block_hash_hex, + 'confirmations', raw.checkpoint_confirmations, + 'capturedAt', raw.checkpoint_created_at_iso, + 'releasePointers', raw.release_pointers + ) + ), + 'data', pg_catalog.jsonb_build_object( + 'address', raw.token_hex, + 'token', programmable_private.build_indexed_token_projection_v2( + raw.projection_json + ) + ) + ) as payload, + true as payload_complete +from raw; + +create function programmable_private.retarget_indexed_token_projection_v2( + p_projection jsonb, + p_pointer jsonb, + p_snapshot_commitment text +) +returns jsonb +language sql +immutable +strict +security definer +set search_path = '' +as $function$ + select p_projection || pg_catalog.jsonb_build_object( + 'source', (p_projection -> 'source') || p_pointer + || pg_catalog.jsonb_build_object( + 'snapshotCommitment', p_snapshot_commitment, + 'projectionRunId', p_projection #>> '{source,projectionRunId}', + 'publicationCommitment', + p_projection #>> '{source,publicationCommitment}', + 'promotedBlockNumber', + p_projection #>> '{source,promotedBlockNumber}', + 'promotedBlockHash', + p_projection #>> '{source,promotedBlockHash}' + ) + ) +$function$; + +-- One row per exact Explore-list token. The projection body is reused only +-- when both list and detail routes share the same immutable checkpoint; its +-- row source is replaced with the list route pointer and commitment. +create view programmable_private.public_explore_list_v1 +with (security_invoker = false, security_barrier = true) +as +select + readiness.route_key, + detail.chain_id, + detail.release_id, + detail.model_id, + detail.source_group, + readiness.projector_version, + readiness.epoch_id, + readiness.pointer_generation, + readiness.checkpoint_id, + readiness.checkpoint_generation, + readiness.reorg_generation, + readiness.checkpoint_block_number, + readiness.checkpoint_block_hash, + readiness.safe_block_number, + readiness.checkpoint_confirmations, + readiness.parity_status, + readiness.parity_record_id, + readiness.reconciliation_id, + readiness.parity_evidence_commitment, + readiness.parity_binding_id, + readiness.parity_binding_commitment, + detail.projection_run_id, + detail.publication_commitment, + detail.payload #>> '{data,token,tokenAddress}' as token_address, + detail.payload #>> '{data,token,name}' as token_name, + detail.payload #>> '{data,token,symbol}' as token_symbol, + (detail.payload #>> '{data,token,launchBlockNumber}')::bigint + as launch_block_number, + (detail.payload #>> '{data,token,launchTransactionIndex}')::integer + as launch_transaction_index, + (detail.payload #>> '{data,token,launchLogIndex}')::integer + as launch_log_index, + detail.payload #>> '{data,token,launchTransactionHash}' + as launch_transaction_hash, + coalesce( + detail.payload #>> '{data,token,market,indexedMarketCapUsdWad}', + detail.payload #>> '{data,token,market,fdvUsdWad}' + ) as market_cap_usd_wad, + coalesce( + detail.payload #>> '{data,token,market,indexedMarketCapNativeWei}', + detail.payload #>> '{data,token,market,marketCapNativeWei}' + ) as market_cap_native_wei, + programmable_private.retarget_indexed_token_projection_v2( + detail.payload #> '{data,token}', pointer.value, + snapshot.snapshot_commitment_hex + ) as token_projection, + detail.payload #>> '{data,token,market,launcherFeesGeneratedNativeWei}' + as launcher_fees_accrued_wei, + snapshot.release_pointers, + snapshot.record_scopes as snapshot_record_scopes, + snapshot.route_evidence, + snapshot.snapshot_commitment_hex, + snapshot.snapshot_captured_at, + snapshot.checkpoint_confirmations as snapshot_confirmations, + readiness.checkpoint_block_number as comparison_checkpoint_block_number, + readiness.checkpoint_block_hash as comparison_checkpoint_block_hash, + 200::integer as http_status, + true as payload_complete +from programmable_private.public_explore_token_v1 as detail +join programmable_private.public_route_snapshots_v2 as snapshot + on snapshot.route_key = 'explore-list' + and snapshot.snapshot_scope = 'all-supported' + and snapshot.chain_id = detail.chain_id + and snapshot.checkpoint_block_number = detail.checkpoint_block_number + and snapshot.checkpoint_block_hash = detail.checkpoint_block_hash +join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = 'explore-list' + and readiness.chain_id = detail.chain_id + and readiness.release_id = detail.release_id + and readiness.model_id = detail.model_id + and readiness.source_group = detail.source_group + and readiness.checkpoint_block_number = detail.checkpoint_block_number + and readiness.checkpoint_block_hash = detail.checkpoint_block_hash + and readiness.route_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' +join lateral pg_catalog.jsonb_array_elements( + snapshot.release_pointers +) as pointer(value) + on pointer.value ->> 'releaseVersion' = detail.release_id + and pointer.value ->> 'modelVersion' = detail.model_id + and pointer.value ->> 'sourceGroup' = detail.source_group; + +create function programmable_private.get_public_explore_page_v1( + p_chain_id bigint, + p_query text, + p_sort text, + p_requested_page integer, + p_page_size integer, + p_start_after jsonb default null +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + snapshot programmable_private.public_route_snapshots_v2%rowtype; + normalized_query text; + valuation_unit text; + filtered_count bigint; + current_count bigint; + total_pages bigint; + resolved_page bigint; + selected_count bigint; + selected_tokens jsonb; + selected_scopes jsonb; + selected_launcher_fees numeric; + end_cursor jsonb; + cursor_cap numeric; + cursor_block bigint; + cursor_transaction_index integer; + cursor_log_index integer; + cursor_transaction_hash text; + cursor_token_address text; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) + or p_sort not in ( + 'newest', 'oldest', 'market-cap', 'market-cap-asc' + ) + or p_requested_page < 1 + or p_page_size not between 1 and 100 + or p_query is null + or pg_catalog.octet_length(p_query) > 256 + then + raise exception using + errcode = '22023', message = 'invalid Explore page request'; + end if; + normalized_query := pg_catalog.lower( + pg_catalog.regexp_replace(pg_catalog.btrim(p_query), '^\$', '') + ); + select * into snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'explore-list' + and snapshot_scope = 'all-supported' + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_count + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + where launch.chain_id = p_chain_id + and launch.release_id in ( + 'classic-v2', 'classic-v3', 'stock-paired-v1', + 'stock-paired-v2', 'stock-paired-v3' + ) + and ( + launch.release_id like 'classic-%' and launch.model_id = 'classic' + or launch.release_id like 'stock-paired-%' + and launch.model_id = 'stock-paired' + ) + and launch.epoch_id in ( + select (pointer ->> 'epochId')::uuid + from pg_catalog.jsonb_array_elements( + snapshot.release_pointers + ) as pointer + ); + if current_count <> ( + select pg_catalog.count(*) + from programmable_private.public_explore_list_v1 + where chain_id = p_chain_id + and checkpoint_block_number = snapshot.checkpoint_block_number + and checkpoint_block_hash = snapshot.checkpoint_block_hash + ) then + return; + end if; + + select case + when p_sort in ('market-cap', 'market-cap-asc') + and pg_catalog.bool_and(market_cap_usd_wad is not null) + then 'usd-wad' + when p_sort in ('market-cap', 'market-cap-asc') + and pg_catalog.bool_and(market_cap_native_wei is not null) + then 'native-wei' + when p_sort in ('market-cap', 'market-cap-asc') then null + else null + end into valuation_unit + from programmable_private.public_explore_list_v1 + where chain_id = p_chain_id + and checkpoint_block_number = snapshot.checkpoint_block_number + and checkpoint_block_hash = snapshot.checkpoint_block_hash; + if p_sort in ('market-cap', 'market-cap-asc') + and valuation_unit is null + then return; end if; + + if p_requested_page = 1 and p_start_after is not null + or p_requested_page > 1 and p_start_after is null + then + raise exception using + errcode = '22023', message = 'Explore page cursor is inconsistent'; + end if; + if p_start_after is not null then + if p_start_after ->> 'adapterVersion' <> + 'indexed-route-adapters-v2' + or p_start_after ->> 'snapshotCommitment' <> + snapshot.snapshot_commitment_hex + or p_start_after ->> 'normalizedQuery' <> normalized_query + or p_start_after ->> 'sort' <> p_sort + or (p_start_after ->> 'pageSize')::integer <> p_page_size + or p_start_after ->> 'valuationUnit' is distinct from valuation_unit + then + raise exception using + errcode = '22023', message = 'Explore page cursor scope mismatch'; + end if; + cursor_cap := (p_start_after #>> '{position,marketCapAtomic}')::numeric; + cursor_block := + (p_start_after #>> '{position,launchBlockNumber}')::bigint; + cursor_transaction_index := + (p_start_after #>> '{position,launchTransactionIndex}')::integer; + cursor_log_index := + (p_start_after #>> '{position,launchLogIndex}')::integer; + cursor_transaction_hash := + p_start_after #>> '{position,launchTransactionHash}'; + cursor_token_address := + p_start_after #>> '{position,tokenAddress}'; + end if; + + select pg_catalog.count(*) into filtered_count + from programmable_private.public_explore_list_v1 as item + where item.chain_id = p_chain_id + and item.checkpoint_block_number = snapshot.checkpoint_block_number + and item.checkpoint_block_hash = snapshot.checkpoint_block_hash + and ( + normalized_query = '' + or pg_catalog.lower(item.token_name) like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_symbol) like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_address) like '%' || normalized_query || '%' + ); + total_pages := pg_catalog.ceil( + filtered_count::numeric / p_page_size + )::bigint; + resolved_page := case + when total_pages = 0 then 1 + else least(p_requested_page::bigint, total_pages) + end; + if resolved_page <> p_requested_page then return; end if; + + with candidates as ( + select item.*, + case when valuation_unit = 'usd-wad' + then item.market_cap_usd_wad::numeric + when valuation_unit = 'native-wei' + then item.market_cap_native_wei::numeric + end as market_cap_atomic + from programmable_private.public_explore_list_v1 as item + where item.chain_id = p_chain_id + and item.checkpoint_block_number = snapshot.checkpoint_block_number + and item.checkpoint_block_hash = snapshot.checkpoint_block_hash + and ( + normalized_query = '' + or pg_catalog.lower(item.token_name) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_symbol) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_address) + like '%' || normalized_query || '%' + ) + ), after_cursor as ( + select * from candidates + where p_start_after is null + or p_sort = 'newest' and ( + launch_block_number, launch_transaction_index, launch_log_index, + launch_transaction_hash, token_address + ) < ( + cursor_block, cursor_transaction_index, cursor_log_index, + cursor_transaction_hash, cursor_token_address + ) + or p_sort = 'oldest' and ( + launch_block_number, launch_transaction_index, launch_log_index, + launch_transaction_hash, token_address + ) > ( + cursor_block, cursor_transaction_index, cursor_log_index, + cursor_transaction_hash, cursor_token_address + ) + or p_sort = 'market-cap' and ( + market_cap_atomic < cursor_cap + or market_cap_atomic = cursor_cap and ( + launch_block_number, launch_transaction_index, launch_log_index, + launch_transaction_hash, token_address + ) < ( + cursor_block, cursor_transaction_index, cursor_log_index, + cursor_transaction_hash, cursor_token_address + ) + ) + or p_sort = 'market-cap-asc' and ( + market_cap_atomic > cursor_cap + or market_cap_atomic = cursor_cap and ( + launch_block_number, launch_transaction_index, launch_log_index, + launch_transaction_hash, token_address + ) < ( + cursor_block, cursor_transaction_index, cursor_log_index, + cursor_transaction_hash, cursor_token_address + ) + ) + ), page_rows as ( + select * from after_cursor + order by + case when p_sort = 'market-cap' then market_cap_atomic end desc, + case when p_sort = 'market-cap-asc' then market_cap_atomic end asc, + case when p_sort = 'oldest' then launch_block_number end asc, + case when p_sort <> 'oldest' then launch_block_number end desc, + case when p_sort = 'oldest' then launch_transaction_index end asc, + case when p_sort <> 'oldest' then launch_transaction_index end desc, + case when p_sort = 'oldest' then launch_log_index end asc, + case when p_sort <> 'oldest' then launch_log_index end desc, + case when p_sort = 'oldest' then launch_transaction_hash end asc, + case when p_sort <> 'oldest' then launch_transaction_hash end desc, + case when p_sort = 'oldest' then token_address end asc, + case when p_sort <> 'oldest' then token_address end desc + limit p_page_size + ), aggregate_page as ( + select pg_catalog.count(*) as page_count, + coalesce( + pg_catalog.jsonb_agg(token_projection order by + case when p_sort = 'market-cap' then market_cap_atomic end desc, + case when p_sort = 'market-cap-asc' then market_cap_atomic end asc, + case when p_sort = 'oldest' then launch_block_number end asc, + case when p_sort <> 'oldest' then launch_block_number end desc, + case when p_sort = 'oldest' then launch_transaction_index end asc, + case when p_sort <> 'oldest' then launch_transaction_index end desc, + case when p_sort = 'oldest' then launch_log_index end asc, + case when p_sort <> 'oldest' then launch_log_index end desc, + case when p_sort = 'oldest' then token_address end asc, + case when p_sort <> 'oldest' then token_address end desc + ), '[]'::jsonb + ) as tokens, + coalesce(pg_catalog.sum( + coalesce(launcher_fees_accrued_wei, '0')::numeric + ), 0) as launcher_fees, + coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'model', model_id, 'releaseVersion', release_id + ) order by release_id + ), '[]'::jsonb) as scopes, + ( + select pg_catalog.jsonb_build_object( + 'adapterVersion', 'indexed-route-adapters-v2', + 'snapshotCommitment', snapshot.snapshot_commitment_hex, + 'normalizedQuery', normalized_query, + 'sort', p_sort, + 'pageSize', p_page_size, + 'valuationUnit', valuation_unit, + 'position', pg_catalog.jsonb_build_object( + 'marketCapAtomic', market_cap_atomic::text, + 'launchBlockNumber', launch_block_number::text, + 'launchTransactionIndex', launch_transaction_index, + 'launchLogIndex', launch_log_index, + 'launchTransactionHash', launch_transaction_hash, + 'tokenAddress', token_address + ) + ) + from page_rows + order by + case when p_sort = 'market-cap' then market_cap_atomic end asc, + case when p_sort = 'market-cap-asc' then market_cap_atomic end desc, + case when p_sort = 'oldest' then launch_block_number end desc, + case when p_sort <> 'oldest' then launch_block_number end asc + limit 1 + ) as final_cursor + from page_rows + ) + select page_count, tokens, scopes, launcher_fees, final_cursor + into selected_count, selected_tokens, selected_scopes, + selected_launcher_fees, end_cursor + from aggregate_page; + if selected_count <> least( + p_page_size::bigint, + filtered_count - ((resolved_page - 1) * p_page_size) + ) then return; end if; + + http_status := 200; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', pg_catalog.jsonb_build_object( + 'adapterVersion', 'indexed-route-adapters-v2', + 'snapshotCommitment', snapshot.snapshot_commitment_hex, + 'chainId', snapshot.chain_id, + 'blockNumber', snapshot.checkpoint_block_number::text, + 'blockHash', '0x' || pg_catalog.encode( + snapshot.checkpoint_block_hash, 'hex' + ), + 'confirmations', snapshot.checkpoint_confirmations, + 'capturedAt', pg_catalog.to_char( + snapshot.snapshot_captured_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ), + 'releasePointers', snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'request', pg_catalog.jsonb_build_object( + 'query', pg_catalog.btrim(p_query), + 'sort', p_sort, + 'requestedPage', p_requested_page, + 'pageSize', p_page_size + ), + 'page', pg_catalog.jsonb_build_object( + 'resolvedPage', resolved_page, + 'totalCount', filtered_count::text, + 'valuationUnit', valuation_unit, + 'startAfter', p_start_after, + 'endAt', end_cursor + ), + 'launcherFeesAccruedWei', selected_launcher_fees::text, + 'tokens', selected_tokens + ) + ); + payload_complete := true; + record_count := selected_count; + record_scopes := selected_scopes; + comparison_checkpoint_block_number := snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := snapshot.checkpoint_block_hash; + route_evidence := snapshot.route_evidence; + return next; +end +$function$; + +-- Replace the internal cursor-taking draft with the frozen page-number API. +-- The database derives the prior-page boundary atomically from the exact +-- ordering and publishes it as startAfter; callers never supply a cursor. +drop function programmable_private.get_public_explore_page_v1( + bigint, text, text, integer, integer, jsonb +); + +create function programmable_private.build_public_snapshot_identity_v2( + p_snapshot_commitment text, + p_chain_id bigint, + p_block_number bigint, + p_block_hash bytea, + p_confirmations bigint, + p_captured_at timestamptz, + p_release_pointers jsonb +) +returns jsonb +language sql +stable +strict +security definer +set search_path = '' +as $function$ + select pg_catalog.jsonb_build_object( + 'adapterVersion', 'indexed-route-adapters-v2', + 'snapshotCommitment', p_snapshot_commitment, + 'chainId', p_chain_id, + 'blockNumber', p_block_number::text, + 'blockHash', '0x' || pg_catalog.encode(p_block_hash, 'hex'), + 'confirmations', p_confirmations, + 'capturedAt', pg_catalog.to_char( + p_captured_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ), + 'releasePointers', p_release_pointers + ) +$function$; + +create function programmable_private.build_public_explore_cursor_v1( + p_snapshot_commitment text, + p_normalized_query text, + p_sort text, + p_page_size integer, + p_valuation_unit text, + p_market_cap_atomic numeric, + p_launch_block_number bigint, + p_launch_transaction_index bigint, + p_launch_log_index bigint, + p_launch_transaction_hash text, + p_token_address text +) +returns jsonb +language sql +immutable +security definer +set search_path = '' +as $function$ + select case when p_launch_block_number is null then null else + pg_catalog.jsonb_build_object( + 'adapterVersion', 'indexed-route-adapters-v2', + 'snapshotCommitment', p_snapshot_commitment, + 'normalizedQuery', p_normalized_query, + 'sort', p_sort, + 'pageSize', p_page_size, + 'valuationUnit', p_valuation_unit, + 'position', pg_catalog.jsonb_build_object( + 'marketCapAtomic', p_market_cap_atomic::text, + 'launchBlockNumber', p_launch_block_number::text, + 'launchTransactionIndex', p_launch_transaction_index, + 'launchLogIndex', p_launch_log_index, + 'launchTransactionHash', p_launch_transaction_hash, + 'tokenAddress', p_token_address + ) + ) + end +$function$; + +create function programmable_private.get_public_explore_page_v1( + p_chain_id bigint, + p_query text, + p_sort text, + p_requested_page integer, + p_page_size integer +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + normalized_query text; + valuation_unit text; + filtered_count bigint; + current_count bigint; + total_pages bigint; + resolved_page bigint; + selected_count bigint; + selected_tokens jsonb; + selected_scopes jsonb; + launcher_fees numeric; + start_cursor jsonb; + end_cursor jsonb; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) + or p_sort not in ( + 'newest', 'oldest', 'market-cap', 'market-cap-asc' + ) + or p_requested_page < 1 + or p_page_size not between 1 and 100 + or p_query is null + or pg_catalog.octet_length(p_query) > 256 + then + raise exception using + errcode = '22023', message = 'invalid Explore page request'; + end if; + normalized_query := pg_catalog.lower( + pg_catalog.regexp_replace(pg_catalog.btrim(p_query), '^\$', '') + ); + + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'explore-list' + and snapshot_scope = 'all-supported' + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_count + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = launch.release_id + and pointer.value ->> 'modelVersion' = launch.model_id + and pointer.value ->> 'sourceGroup' = run.source_group + and (pointer.value ->> 'epochId')::uuid = launch.epoch_id + and (pointer.value ->> 'pointerGeneration')::bigint = + launch.pointer_generation + where launch.chain_id = p_chain_id; + if current_count <> ( + select pg_catalog.count(*) + from programmable_private.public_explore_list_v1 + where chain_id = p_chain_id + and checkpoint_block_number = + route_snapshot.checkpoint_block_number + and checkpoint_block_hash = route_snapshot.checkpoint_block_hash + ) then + return; + end if; + + select pg_catalog.count(*) into filtered_count + from programmable_private.public_explore_list_v1 as item + where item.chain_id = p_chain_id + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash + and ( + normalized_query = '' + or pg_catalog.lower(item.token_name) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_symbol) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_address) + like '%' || normalized_query || '%' + ); + + if p_sort in ('market-cap', 'market-cap-asc') then + if filtered_count = 0 then + valuation_unit := 'native-wei'; + else + select case + when pg_catalog.bool_and(market_cap_usd_wad is not null) + then 'usd-wad' + when pg_catalog.bool_and(market_cap_native_wei is not null) + then 'native-wei' + end into valuation_unit + from programmable_private.public_explore_list_v1 as item + where item.chain_id = p_chain_id + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash + and ( + normalized_query = '' + or pg_catalog.lower(item.token_name) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_symbol) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_address) + like '%' || normalized_query || '%' + ); + if valuation_unit is null then return; end if; + end if; + end if; + + total_pages := pg_catalog.ceil( + filtered_count::numeric / p_page_size + )::bigint; + resolved_page := case + when total_pages = 0 then 1 + else least(p_requested_page::bigint, total_pages) + end; + + with candidates as ( + select item.*, + case + when valuation_unit = 'usd-wad' + then item.market_cap_usd_wad::numeric + when valuation_unit = 'native-wei' + then item.market_cap_native_wei::numeric + end as market_cap_atomic + from programmable_private.public_explore_list_v1 as item + where item.chain_id = p_chain_id + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash + and ( + normalized_query = '' + or pg_catalog.lower(item.token_name) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_symbol) + like '%' || normalized_query || '%' + or pg_catalog.lower(item.token_address) + like '%' || normalized_query || '%' + ) + ), ordered as ( + select candidates.*, + pg_catalog.row_number() over (order by + case when p_sort = 'market-cap' + then market_cap_atomic end desc, + case when p_sort = 'market-cap-asc' + then market_cap_atomic end asc, + case when p_sort = 'oldest' + then launch_block_number end asc, + case when p_sort <> 'oldest' + then launch_block_number end desc, + case when p_sort = 'oldest' + then launch_transaction_index end asc, + case when p_sort <> 'oldest' + then launch_transaction_index end desc, + case when p_sort = 'oldest' + then launch_log_index end asc, + case when p_sort <> 'oldest' + then launch_log_index end desc, + case when p_sort = 'oldest' + then launch_transaction_hash end asc, + case when p_sort <> 'oldest' + then launch_transaction_hash end desc, + case when p_sort = 'oldest' + then token_address end asc, + case when p_sort <> 'oldest' + then token_address end desc + ) as row_ordinal + from candidates + ), selected as ( + select * from ordered + where row_ordinal > (resolved_page - 1) * p_page_size + and row_ordinal <= resolved_page * p_page_size + ), selected_aggregate as ( + select + pg_catalog.count(*) as selected_count, + coalesce( + pg_catalog.jsonb_agg(token_projection order by row_ordinal), + '[]'::jsonb + ) as selected_tokens + from selected + ), selected_scope as ( + select coalesce( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'model', scope.model_id, + 'releaseVersion', scope.release_id + ) order by scope.release_id, scope.model_id + ), '[]'::jsonb + ) as scopes + from ( + select distinct release_id, model_id from selected + ) as scope + ), cursor_rows as ( + select + programmable_private.build_public_explore_cursor_v1( + route_snapshot.snapshot_commitment_hex, + normalized_query, p_sort, p_page_size, valuation_unit, + start_row.market_cap_atomic, + start_row.launch_block_number, + start_row.launch_transaction_index, + start_row.launch_log_index, + start_row.launch_transaction_hash, + start_row.token_address + ) as start_cursor, + programmable_private.build_public_explore_cursor_v1( + route_snapshot.snapshot_commitment_hex, + normalized_query, p_sort, p_page_size, valuation_unit, + end_row.market_cap_atomic, + end_row.launch_block_number, + end_row.launch_transaction_index, + end_row.launch_log_index, + end_row.launch_transaction_hash, + end_row.token_address + ) as end_cursor + from (values (true)) as singleton(value) + left join ordered as start_row + on start_row.row_ordinal = (resolved_page - 1) * p_page_size + and resolved_page > 1 + left join ordered as end_row + on end_row.row_ordinal = least( + resolved_page * p_page_size, filtered_count + ) + ) + select aggregate.selected_count, aggregate.selected_tokens, + scope.scopes, cursors.start_cursor, cursors.end_cursor + into selected_count, selected_tokens, selected_scopes, + start_cursor, end_cursor + from selected_aggregate as aggregate + cross join selected_scope as scope + cross join cursor_rows as cursors; + + if selected_count <> least( + p_page_size::bigint, + pg_catalog.greatest( + 0::bigint, + filtered_count - ((resolved_page - 1) * p_page_size) + ) + ) then return; end if; + if (resolved_page = 1) <> (start_cursor is null) + or (selected_count = 0) <> (end_cursor is null) + then return; end if; + + select coalesce(pg_catalog.sum( + coalesce(item.launcher_fees_accrued_wei, '0')::numeric + ), 0) into launcher_fees + from programmable_private.public_explore_list_v1 as item + where item.chain_id = p_chain_id + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash; + + http_status := 200; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'request', pg_catalog.jsonb_build_object( + 'query', pg_catalog.btrim(p_query), + 'sort', p_sort, + 'requestedPage', p_requested_page, + 'pageSize', p_page_size + ), + 'page', pg_catalog.jsonb_build_object( + 'resolvedPage', resolved_page, + 'totalCount', filtered_count::text, + 'valuationUnit', valuation_unit, + 'startAfter', start_cursor, + 'endAt', end_cursor + ), + 'launcherFeesAccruedWei', launcher_fees::text, + 'tokens', selected_tokens + ) + ); + payload_complete := true; + record_count := selected_count; + record_scopes := selected_scopes; + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +-- Complete, uncapped feed for the indexer cutover comparator. It emits one +-- row only when all five public releases share one current checkpoint, every +-- current launch has a complete raw token projection, and per-record source, +-- publication and parity commitments are present. +create function programmable_private.get_public_indexer_feed_v1( + p_chain_id bigint +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb, + snapshot jsonb, + tokens jsonb, + record_sources jsonb, + captured_at timestamptz, + reconciled_at timestamptz, + snapshot_commitment bytea +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + current_count bigint; + feed_count bigint; + feed_tokens jsonb; + feed_sources jsonb; + feed_scopes jsonb; + feed_snapshot jsonb; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) then + raise exception using + errcode = '22023', message = 'invalid public indexer feed chain'; + end if; + + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'explore-list' + and snapshot_scope = 'all-supported' + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_count + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = launch.release_id + and pointer.value ->> 'modelVersion' = launch.model_id + and pointer.value ->> 'sourceGroup' = run.source_group + and (pointer.value ->> 'epochId')::uuid = launch.epoch_id + and (pointer.value ->> 'pointerGeneration')::bigint = + launch.pointer_generation + where launch.chain_id = p_chain_id; + + with feed_rows as ( + select item.*, + ( + select evidence.value + from pg_catalog.jsonb_array_elements( + item.route_evidence + ) as evidence(value) + where evidence.value ->> 'releaseVersion' = item.release_id + and evidence.value ->> 'modelVersion' = item.model_id + limit 1 + ) as parity_evidence + from programmable_private.public_explore_list_v1 as item + where item.chain_id = p_chain_id + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash + ), aggregate_rows as ( + select + pg_catalog.count(*) as feed_count, + coalesce( + pg_catalog.jsonb_agg(token_projection order by + launch_block_number, + launch_transaction_index, + launch_log_index, + launch_transaction_hash, + token_address + ), '[]'::jsonb + ) as feed_tokens, + coalesce( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'tokenAddress', token_address, + 'source', token_projection -> 'source', + 'parity', parity_evidence + ) order by + launch_block_number, + launch_transaction_index, + launch_log_index, + launch_transaction_hash, + token_address + ), '[]'::jsonb + ) as feed_sources, + pg_catalog.bool_and( + payload_complete + and token_projection #>> '{source,publicationCommitment}' is not null + and parity_evidence ->> 'parityEvidenceCommitment' is not null + and parity_evidence ->> 'parityBindingCommitment' is not null + ) as all_complete + from feed_rows + ), scope_rows as ( + select coalesce( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'model', scope.model_id, + 'releaseVersion', scope.release_id + ) order by scope.release_id, scope.model_id + ), '[]'::jsonb + ) as scopes + from ( + select distinct release_id, model_id from feed_rows + ) as scope + ) + select rows.feed_count, rows.feed_tokens, rows.feed_sources, + scopes.scopes + into feed_count, feed_tokens, feed_sources, feed_scopes + from aggregate_rows as rows + cross join scope_rows as scopes + where coalesce(rows.all_complete, true); + if not found or feed_count <> current_count then return; end if; + + feed_snapshot := + programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ) || pg_catalog.jsonb_build_object( + 'safeBlockNumber', route_snapshot.safe_block_number::text, + 'reconciledAt', pg_catalog.to_char( + route_snapshot.reconciled_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) + ); + + http_status := 200; + snapshot := feed_snapshot; + tokens := feed_tokens; + record_sources := feed_sources; + captured_at := route_snapshot.snapshot_captured_at; + reconciled_at := route_snapshot.reconciled_at; + snapshot_commitment := route_snapshot.checkpoint_block_hash; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', feed_snapshot, + 'data', pg_catalog.jsonb_build_object( + 'tokens', feed_tokens, + 'recordSources', feed_sources + ) + ); + payload_complete := true; + record_count := feed_count; + record_scopes := feed_scopes; + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +create function programmable_private.decode_public_address_v1(p_value text) +returns bytea +language plpgsql +immutable +strict +security definer +set search_path = '' +as $function$ +begin + if p_value !~ '^0x[0-9a-fA-F]{40}$' then + raise exception using + errcode = '22023', message = 'invalid public address'; + end if; + return pg_catalog.decode( + pg_catalog.substr(pg_catalog.lower(p_value), 3), 'hex' + ); +end +$function$; + +create function programmable_private.decode_public_bytes32_v1(p_value text) +returns bytea +language plpgsql +immutable +strict +security definer +set search_path = '' +as $function$ +begin + if p_value !~ '^0x[0-9a-fA-F]{64}$' then + raise exception using + errcode = '22023', message = 'invalid public bytes32'; + end if; + return pg_catalog.decode( + pg_catalog.substr(pg_catalog.lower(p_value), 3), 'hex' + ); +end +$function$; + +create function programmable_private.get_public_explore_token_v1( + p_chain_id bigint, + p_address text +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + token_address bytea; + canonical_address text; + current_count bigint; + selected programmable_private.public_explore_token_v1%rowtype; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) then + raise exception using + errcode = '22023', message = 'invalid token-detail chain'; + end if; + token_address := programmable_private.decode_public_address_v1(p_address); + canonical_address := '0x' || pg_catalog.encode(token_address, 'hex'); + + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'explore-token' + and snapshot_scope = 'all-supported' + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_count + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = launch.release_id + and pointer.value ->> 'modelVersion' = launch.model_id + and pointer.value ->> 'sourceGroup' = run.source_group + and (pointer.value ->> 'epochId')::uuid = launch.epoch_id + and (pointer.value ->> 'pointerGeneration')::bigint = + launch.pointer_generation + where launch.chain_id = p_chain_id + and launch.token = token_address; + if current_count > 1 then + raise exception using + errcode = '23514', message = 'token-detail projection is ambiguous'; + end if; + + select * into selected + from programmable_private.public_explore_token_v1 as detail + where detail.chain_id = p_chain_id + and detail.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and detail.checkpoint_block_hash = route_snapshot.checkpoint_block_hash + and detail.payload #>> '{data,address}' = canonical_address; + + if current_count = 1 and not found then return; end if; + if current_count = 0 then + http_status := 404; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'address', canonical_address, + 'token', null + ) + ); + payload_complete := true; + record_count := 0; + record_scopes := '[]'::jsonb; + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := + route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; + return; + end if; + + http_status := selected.http_status; + payload := selected.payload; + payload_complete := selected.payload_complete; + record_count := selected.record_count; + record_scopes := selected.record_scopes; + comparison_checkpoint_block_number := + selected.comparison_checkpoint_block_number; + comparison_checkpoint_block_hash := + selected.comparison_checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +create view programmable_private.public_explore_chart_v1 +with (security_invoker = false, security_barrier = true) +as +select + chart_readiness.chain_id, + token.release_id, + token.model_id, + token.source_group, + token.token, + token.pool_id, + token.currency0, + token.currency1, + chart_snapshot.checkpoint_block_number, + chart_snapshot.checkpoint_block_hash, + chart_snapshot.snapshot_commitment_hex, + chart_snapshot.release_pointers, + chart_snapshot.record_scopes as snapshot_record_scopes, + chart_snapshot.route_evidence, + chart_snapshot.snapshot_captured_at, + chart_snapshot.safe_block_number, + chart_snapshot.checkpoint_confirmations, + programmable_private.retarget_indexed_token_projection_v2( + detail.payload #> '{data,token}', pointer.value, + chart_snapshot.snapshot_commitment_hex + ) -> 'source' as row_source, + close_fact.market_block_close_id, + close_fact.block_number, + close_fact.block_hash, + close_fact.block_timestamp, + close_fact.transaction_count, + case + when token.token = token.currency0 + and token.currency1 = + pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + then close_fact.token0_price + when token.token = token.currency1 + and token.currency0 = + pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + then close_fact.token1_price + end as token_price_native, + case + when token.currency0 = + pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + then close_fact.volume_token0 + when token.currency1 = + pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + then close_fact.volume_token1 + end as volume_native, + close_fact.volume_usd, + global_snapshot.answer as eth_usd_answer, + global_snapshot.decimals as eth_usd_decimals +from programmable_private.route_token_projections_v1 as token +join programmable_private.public_explore_token_v1 as detail + on detail.chain_id = token.chain_id + and detail.release_id = token.release_id + and detail.model_id = token.model_id + and detail.source_group = token.source_group + and detail.epoch_id = token.epoch_id + and detail.pointer_generation = token.pointer_generation + and detail.payload #>> '{data,token,tokenAddress}' = + '0x' || pg_catalog.encode(token.token, 'hex') +join programmable_private.public_route_snapshots_v2 as chart_snapshot + on chart_snapshot.route_key = 'explore-chart' + and chart_snapshot.snapshot_scope = 'all-supported' + and chart_snapshot.chain_id = token.chain_id + and chart_snapshot.checkpoint_block_number = token.checkpoint_block_number + and chart_snapshot.checkpoint_block_hash = token.checkpoint_block_hash +join programmable_private.route_snapshot_readiness_v1 as chart_readiness + on chart_readiness.route_key = 'explore-chart' + and chart_readiness.chain_id = token.chain_id + and chart_readiness.release_id = token.release_id + and chart_readiness.model_id = token.model_id + and chart_readiness.source_group = token.source_group + and chart_readiness.epoch_id = token.epoch_id + and chart_readiness.pointer_generation = token.pointer_generation + and chart_readiness.checkpoint_block_number = + chart_snapshot.checkpoint_block_number + and chart_readiness.checkpoint_block_hash = + chart_snapshot.checkpoint_block_hash + and chart_readiness.route_status = 'eligible' + and chart_readiness.route_mode = 'indexed' + and chart_readiness.parity_status = 'current' +join lateral pg_catalog.jsonb_array_elements( + chart_snapshot.release_pointers +) as pointer(value) + on pointer.value ->> 'releaseVersion' = token.release_id + and pointer.value ->> 'modelVersion' = token.model_id + and pointer.value ->> 'sourceGroup' = token.source_group +left join programmable_private.market_block_closes_v1 as close_fact + on token.model_id = 'classic' + and close_fact.chain_id = token.chain_id + and close_fact.release_id = token.release_id + and close_fact.model_id = token.model_id + and close_fact.epoch_id = token.epoch_id + and close_fact.pointer_generation = token.pointer_generation + and close_fact.token = token.token + and close_fact.pool_id = token.pool_id + and close_fact.block_number <= chart_snapshot.checkpoint_block_number +left join programmable_private.global_eth_usd_snapshots_v1 + as global_snapshot + on global_snapshot.global_market_snapshot_id = + close_fact.global_market_snapshot_id +where token.route_key = 'explore-token' + and token.route_status = 'eligible' + and token.route_mode = 'indexed' + and token.parity_status = 'current' + and token.payload_complete; + +create function programmable_private.get_public_token_chart_v1( + p_chain_id bigint, + p_address text, + p_range text +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + token_address bytea; + canonical_address text; + target record; + points jsonb; + point_count bigint; + swap_count numeric; + volume_native_wei numeric; + volume_usd_wad numeric; + range_start timestamptz; + baseline_transactions numeric; + baseline_volume_native numeric; + baseline_volume_usd numeric; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) + or p_range not in ('1h', '1d', '1w', 'all') + then + raise exception using + errcode = '22023', message = 'invalid token-chart request'; + end if; + token_address := programmable_private.decode_public_address_v1(p_address); + canonical_address := '0x' || pg_catalog.encode(token_address, 'hex'); + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'explore-chart' + and snapshot_scope = 'all-supported' + and chain_id = p_chain_id; + if not found then return; end if; + + select chart.* into target + from programmable_private.public_explore_chart_v1 as chart + where chart.chain_id = p_chain_id + and chart.token = token_address + and chart.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and chart.checkpoint_block_hash = route_snapshot.checkpoint_block_hash + order by chart.block_number nulls last + limit 1; + if not found then return; end if; + + if target.model_id = 'stock-paired' then + points := '[]'::jsonb; + point_count := 0; + swap_count := 0; + volume_native_wei := 0; + volume_usd_wad := null; + else + range_start := case p_range + when '1h' then route_snapshot.snapshot_captured_at - interval '1 hour' + when '1d' then route_snapshot.snapshot_captured_at - interval '1 day' + when '1w' then route_snapshot.snapshot_captured_at - interval '7 days' + else null + end; + if range_start is not null then + select + coalesce(close_row.transaction_count, 0), + coalesce(close_row.volume_native, 0), + coalesce(close_row.volume_usd, 0) + into baseline_transactions, baseline_volume_native, + baseline_volume_usd + from programmable_private.public_explore_chart_v1 as close_row + where close_row.chain_id = p_chain_id + and close_row.token = token_address + and close_row.block_timestamp < range_start + order by close_row.block_number desc + limit 1; + if not found then + baseline_transactions := 0; + baseline_volume_native := 0; + baseline_volume_usd := 0; + end if; + else + baseline_transactions := 0; + baseline_volume_native := 0; + baseline_volume_usd := 0; + end if; + + with ranged as ( + select chart.*, + pg_catalog.trunc( + chart.token_price_native * 1000000000000000000 + )::numeric as price_native_wei, + pg_catalog.trunc( + chart.token_price_native * 1000000000000000000 + * chart.eth_usd_answer + / pg_catalog.power(10::numeric, chart.eth_usd_decimals) + )::numeric as price_usd_wad + from programmable_private.public_explore_chart_v1 as chart + where chart.chain_id = p_chain_id + and chart.token = token_address + and chart.market_block_close_id is not null + and chart.block_number <= route_snapshot.checkpoint_block_number + and (range_start is null or chart.block_timestamp >= range_start) + ), exact_points as ( + select * from ranged + where price_native_wei > 0 + and price_usd_wad >= 0 + ) + select + coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'blockNumber', point.block_number::text, + 'priceNativeWei', point.price_native_wei::text, + 'priceUsdWad', point.price_usd_wad::text + ) order by point.block_number + ), '[]'::jsonb), + pg_catalog.count(*), + pg_catalog.greatest( + coalesce(pg_catalog.max(point.transaction_count), 0) + - baseline_transactions, + 0 + ), + pg_catalog.trunc(pg_catalog.greatest( + coalesce(pg_catalog.max(point.volume_native), 0) + - baseline_volume_native, + 0 + ) * 1000000000000000000), + pg_catalog.trunc(pg_catalog.greatest( + coalesce(pg_catalog.max(point.volume_usd), 0) + - baseline_volume_usd, + 0 + ) * 1000000000000000000) + into points, point_count, swap_count, + volume_native_wei, volume_usd_wad + from exact_points as point; + end if; + + http_status := 200; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'address', canonical_address, + 'range', p_range, + 'source', target.row_source, + 'poolId', '0x' || pg_catalog.encode(target.pool_id, 'hex'), + 'points', points, + 'swapCount', swap_count::text, + 'volumeNativeWei', volume_native_wei::text, + 'volumeUsdWad', volume_usd_wad::text + ) + ); + payload_complete := true; + record_count := point_count; + record_scopes := pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object( + 'model', target.model_id, + 'releaseVersion', target.release_id + ) + ); + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +create view programmable_private.public_creator_profile_v1 +with (security_invoker = false, security_barrier = true) +as +with token_rows as ( + select + token.chain_id, + token.creator as account, + token.release_id, + token.model_id, + token.source_group, + token.checkpoint_block_number, + token.checkpoint_block_hash, + 'token'::text as record_kind, + programmable_private.retarget_indexed_token_projection_v2( + detail.payload #> '{data,token}', pointer.value, + snapshot.snapshot_commitment_hex + ) as record_payload, + snapshot.release_pointers, + snapshot.record_scopes as snapshot_record_scopes, + snapshot.route_evidence, + snapshot.snapshot_commitment_hex, + snapshot.snapshot_captured_at, + snapshot.safe_block_number, + snapshot.checkpoint_confirmations + from programmable_private.route_token_projections_v1 as token + join programmable_private.public_explore_token_v1 as detail + on detail.chain_id = token.chain_id + and detail.release_id = token.release_id + and detail.model_id = token.model_id + and detail.source_group = token.source_group + and detail.epoch_id = token.epoch_id + and detail.pointer_generation = token.pointer_generation + and detail.payload #>> '{data,token,tokenAddress}' = + '0x' || pg_catalog.encode(token.token, 'hex') + join programmable_private.public_route_snapshots_v2 as snapshot + on snapshot.route_key = 'creator-profile' + and snapshot.snapshot_scope = 'all-supported' + and snapshot.chain_id = token.chain_id + and snapshot.checkpoint_block_number = token.checkpoint_block_number + and snapshot.checkpoint_block_hash = token.checkpoint_block_hash + join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = 'creator-profile' + and readiness.chain_id = token.chain_id + and readiness.release_id = token.release_id + and readiness.model_id = token.model_id + and readiness.source_group = token.source_group + and readiness.epoch_id = token.epoch_id + and readiness.pointer_generation = token.pointer_generation + and readiness.checkpoint_block_number = token.checkpoint_block_number + and readiness.checkpoint_block_hash = token.checkpoint_block_hash + and readiness.route_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' + join lateral pg_catalog.jsonb_array_elements( + snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = token.release_id + and pointer.value ->> 'modelVersion' = token.model_id + and pointer.value ->> 'sourceGroup' = token.source_group + where token.route_key = 'explore-token' + and token.route_status = 'eligible' + and token.route_mode = 'indexed' + and token.parity_status = 'current' + and token.payload_complete +), claim_rows as ( + select + claim.chain_id, + claim.creator as account, + claim.release_id, + claim.model_id, + claim.source_group, + snapshot.checkpoint_block_number, + snapshot.checkpoint_block_hash, + 'claim'::text as record_kind, + pg_catalog.jsonb_build_object( + 'source', pointer.value || pg_catalog.jsonb_build_object( + 'snapshotCommitment', snapshot.snapshot_commitment_hex, + 'projectionRunId', claim.projection_run_id, + 'publicationCommitment', '0x' || pg_catalog.encode( + publication_audit.input_commitment, 'hex' + ), + 'promotedBlockNumber', claim.promoted_block_number::text, + 'promotedBlockHash', '0x' || pg_catalog.encode( + claim.promoted_block_hash, 'hex' + ) + ), + 'poolId', '0x' || pg_catalog.encode(claim.pool_id, 'hex'), + 'tokenAddress', '0x' || pg_catalog.encode(claim.token, 'hex'), + 'creatorAddress', '0x' || pg_catalog.encode(claim.creator, 'hex'), + 'recipientAddress', '0x' || pg_catalog.encode(claim.recipient, 'hex'), + 'callerAddress', '0x' || pg_catalog.encode(fact.caller, 'hex'), + 'amountWei', claim.amount::text, + 'blockNumber', claim.block_number::text, + 'transactionHash', '0x' || pg_catalog.encode( + claim.transaction_hash, 'hex' + ), + 'transactionIndex', claim.transaction_index, + 'logIndex', claim.receipt_log_ordinal, + 'claimedAt', pg_catalog.to_char( + claim.block_timestamp at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) + ) as record_payload, + snapshot.release_pointers, + snapshot.record_scopes as snapshot_record_scopes, + snapshot.route_evidence, + snapshot.snapshot_commitment_hex, + snapshot.snapshot_captured_at, + snapshot.safe_block_number, + snapshot.checkpoint_confirmations + from programmable_private.claim_history_v1 as claim + join programmable_private.creator_hook_claim_facts as fact + on fact.chain_id = claim.chain_id + and fact.release_id = claim.release_id + and fact.model_id = claim.model_id + and fact.epoch_id = claim.epoch_id + and fact.pointer_generation = claim.pointer_generation + and fact.source_occurrence_id = claim.source_occurrence_id + and fact.source_logical_event_id = claim.source_logical_event_id + and fact.source_occurrence_block_hash = claim.block_hash + and fact.pool_id = claim.pool_id + and fact.creator = claim.creator + and fact.recipient = claim.recipient + and fact.amount = claim.amount + join programmable_private.projection_publications as publication + on publication.run_id = claim.projection_run_id + and publication.epoch_id = claim.epoch_id + and publication.pointer_generation = claim.pointer_generation + and publication.target_block_number = claim.promoted_block_number + and publication.target_block_hash = claim.promoted_block_hash + join programmable_private.mutation_audits as publication_audit + on publication_audit.audit_id = publication.audit_id + join programmable_private.public_route_snapshots_v2 as snapshot + on snapshot.route_key = 'creator-profile' + and snapshot.snapshot_scope = 'all-supported' + and snapshot.chain_id = claim.chain_id + join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = 'creator-profile' + and readiness.chain_id = claim.chain_id + and readiness.release_id = claim.release_id + and readiness.model_id = claim.model_id + and readiness.source_group = claim.source_group + and readiness.epoch_id = claim.epoch_id + and readiness.pointer_generation = claim.pointer_generation + and readiness.checkpoint_block_number = snapshot.checkpoint_block_number + and readiness.checkpoint_block_hash = snapshot.checkpoint_block_hash + and readiness.route_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' + join lateral pg_catalog.jsonb_array_elements( + snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = claim.release_id + and pointer.value ->> 'modelVersion' = claim.model_id + and pointer.value ->> 'sourceGroup' = claim.source_group + where claim.release_id = 'classic-v2' + and claim.model_id = 'classic' + and claim.claimant_kind = 'creator' +) +select * from token_rows +union all +select * from claim_rows; + +create function programmable_private.get_public_creator_profile_v1( + p_chain_id bigint, + p_account text +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + account_address bytea; + canonical_account text; + current_token_count bigint; + current_claim_count bigint; + public_token_count bigint; + public_claim_count bigint; + profile_tokens jsonb; + profile_claims jsonb; + profile_scopes jsonb; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) then + raise exception using + errcode = '22023', message = 'invalid creator-profile chain'; + end if; + account_address := programmable_private.decode_public_address_v1(p_account); + canonical_account := '0x' || pg_catalog.encode(account_address, 'hex'); + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'creator-profile' + and snapshot_scope = 'all-supported' + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_token_count + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = launch.release_id + and pointer.value ->> 'modelVersion' = launch.model_id + and pointer.value ->> 'sourceGroup' = run.source_group + and (pointer.value ->> 'epochId')::uuid = launch.epoch_id + and (pointer.value ->> 'pointerGeneration')::bigint = + launch.pointer_generation + where launch.chain_id = p_chain_id + and launch.creator = account_address; + + select pg_catalog.count(*) into current_claim_count + from programmable_private.claim_history_v1 as claim + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = claim.release_id + and pointer.value ->> 'modelVersion' = claim.model_id + and pointer.value ->> 'sourceGroup' = claim.source_group + and (pointer.value ->> 'epochId')::uuid = claim.epoch_id + and (pointer.value ->> 'pointerGeneration')::bigint = + claim.pointer_generation + where claim.chain_id = p_chain_id + and claim.release_id = 'classic-v2' + and claim.model_id = 'classic' + and claim.claimant_kind = 'creator' + and claim.creator = account_address; + + with records as ( + select * + from programmable_private.public_creator_profile_v1 as item + where item.chain_id = p_chain_id + and item.account = account_address + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash + ), aggregates as ( + select + pg_catalog.count(*) filter ( + where record_kind = 'token' + ) as token_count, + pg_catalog.count(*) filter ( + where record_kind = 'claim' + ) as claim_count, + coalesce(pg_catalog.jsonb_agg(record_payload order by + record_payload #>> '{source,promotedBlockNumber}', + record_payload #>> '{tokenAddress}' + ) filter (where record_kind = 'token'), '[]'::jsonb) as tokens, + coalesce(pg_catalog.jsonb_agg(record_payload order by + (record_payload ->> 'blockNumber')::bigint desc, + (record_payload ->> 'transactionIndex')::bigint desc, + (record_payload ->> 'logIndex')::bigint desc, + record_payload ->> 'transactionHash' desc + ) filter (where record_kind = 'claim'), '[]'::jsonb) as claims + from records + ), scopes as ( + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'model', scope.model_id, + 'releaseVersion', scope.release_id + ) order by scope.release_id, scope.model_id + ), '[]'::jsonb) as value + from ( + select distinct release_id, model_id from records + ) as scope + ) + select aggregate.token_count, aggregate.claim_count, + aggregate.tokens, aggregate.claims, scopes.value + into public_token_count, public_claim_count, + profile_tokens, profile_claims, profile_scopes + from aggregates as aggregate cross join scopes; + if public_token_count <> current_token_count + or public_claim_count <> current_claim_count + then return; end if; + + http_status := 200; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'account', canonical_account, + 'tokens', profile_tokens, + 'claims', profile_claims + ) + ); + payload_complete := true; + record_count := public_token_count + public_claim_count; + record_scopes := profile_scopes; + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +create view programmable_private.public_classic_v3_profile_v1 +with (security_invoker = false, security_barrier = true) +as +select + reward.chain_id, + reward.account, + reward.release_id, + reward.model_id, + reward.vault, + snapshot.checkpoint_block_number, + snapshot.checkpoint_block_hash, + pg_catalog.jsonb_build_object( + 'source', programmable_private.retarget_indexed_token_projection_v2( + detail.payload #> '{data,token}', pointer.value, + snapshot.snapshot_commitment_hex + ) -> 'source', + 'tokenAddress', detail.payload #>> '{data,token,tokenAddress}', + 'tokenName', detail.payload #>> '{data,token,name}', + 'tokenSymbol', detail.payload #>> '{data,token,symbol}', + 'poolId', detail.payload #>> '{data,token,poolId}', + 'vaultAddress', '0x' || pg_catalog.encode(reward.vault, 'hex'), + 'claimableWei', reward.claimable_accrued::text, + 'claimedWei', reward.claimed_total::text, + 'buySwapFeeBps', + (detail.payload #>> '{data,token,fees,buySwapFeeBps}')::integer, + 'sellSwapFeeBps', + (detail.payload #>> '{data,token,fees,sellSwapFeeBps}')::integer, + 'platformFeeBps', + (detail.payload #>> '{data,token,fees,launcherFeeBps}')::integer, + 'allocations', pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'allocationIndex', allocation.allocation_index, + 'beneficiary', '0x' || pg_catalog.encode( + allocation.beneficiary, 'hex' + ), + 'payoutAddress', '0x' || pg_catalog.encode( + allocation.payout_address, 'hex' + ), + 'shareBps', allocation.share_bps + ) order by allocation.allocation_index + ), + 'launchTransactionHash', + detail.payload #>> '{data,token,launchTransactionHash}' + ) as reward_payload, + snapshot.release_pointers, + snapshot.record_scopes as snapshot_record_scopes, + snapshot.route_evidence, + snapshot.snapshot_commitment_hex, + snapshot.snapshot_captured_at, + snapshot.safe_block_number, + snapshot.checkpoint_confirmations +from programmable_private.account_reward_summaries_v1 as reward +join programmable_private.route_token_projections_v1 as token + on token.route_key = 'explore-token' + and token.chain_id = reward.chain_id + and token.release_id = reward.release_id + and token.model_id = reward.model_id + and token.token = reward.token + and token.pool_id = reward.pool_id + and token.reward_vault = reward.vault + and token.route_status = 'eligible' + and token.route_mode = 'indexed' + and token.parity_status = 'current' + and token.payload_complete +join programmable_private.public_explore_token_v1 as detail + on detail.chain_id = token.chain_id + and detail.release_id = token.release_id + and detail.model_id = token.model_id + and detail.source_group = token.source_group + and detail.epoch_id = token.epoch_id + and detail.pointer_generation = token.pointer_generation + and detail.payload #>> '{data,token,tokenAddress}' = + '0x' || pg_catalog.encode(token.token, 'hex') +join programmable_private.public_route_snapshots_v2 as snapshot + on snapshot.route_key = 'classic-v3-profile' + and snapshot.snapshot_scope = 'classic-v3' + and snapshot.chain_id = reward.chain_id + and snapshot.checkpoint_block_number = token.checkpoint_block_number + and snapshot.checkpoint_block_hash = token.checkpoint_block_hash +join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = 'classic-v3-profile' + and readiness.chain_id = token.chain_id + and readiness.release_id = token.release_id + and readiness.model_id = token.model_id + and readiness.source_group = token.source_group + and readiness.epoch_id = token.epoch_id + and readiness.pointer_generation = token.pointer_generation + and readiness.checkpoint_block_number = snapshot.checkpoint_block_number + and readiness.checkpoint_block_hash = snapshot.checkpoint_block_hash + and readiness.route_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' +join lateral pg_catalog.jsonb_array_elements( + snapshot.release_pointers +) as pointer(value) + on pointer.value ->> 'releaseVersion' = token.release_id + and pointer.value ->> 'modelVersion' = token.model_id + and pointer.value ->> 'sourceGroup' = token.source_group +join programmable_private.classic_v3_vault_history_v1 as allocation + on allocation.chain_id = reward.chain_id + and allocation.release_id = reward.release_id + and allocation.model_id = reward.model_id + and allocation.vault = reward.vault + and allocation.pool_id = reward.pool_id + and allocation.effective_to_block is null +where reward.release_id = 'classic-v3' + and reward.model_id = 'classic' + and exists ( + select 1 + from programmable_private.classic_v3_vault_history_v1 as owned + where owned.chain_id = reward.chain_id + and owned.release_id = reward.release_id + and owned.model_id = reward.model_id + and owned.vault = reward.vault + and owned.pool_id = reward.pool_id + and owned.effective_to_block is null + and owned.beneficiary = reward.account + ) +group by + reward.chain_id, reward.account, reward.release_id, reward.model_id, + reward.vault, reward.claimable_accrued, reward.claimed_total, + detail.payload, pointer.value, snapshot.checkpoint_block_number, + snapshot.checkpoint_block_hash, snapshot.release_pointers, + snapshot.record_scopes, snapshot.route_evidence, + snapshot.snapshot_commitment_hex, snapshot.snapshot_captured_at, + snapshot.safe_block_number, snapshot.checkpoint_confirmations +having pg_catalog.count(*) between 1 and 5 + and pg_catalog.count(distinct allocation.allocation_index) = + pg_catalog.count(*) + and pg_catalog.sum(allocation.share_bps) = 10000 + and pg_catalog.bool_and( + allocation.beneficiary = allocation.payout_address + ); + +create function programmable_private.get_public_classic_v3_profile_v1( + p_chain_id bigint, + p_account text +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + account_address bytea; + canonical_account text; + current_count bigint; + public_count bigint; + rewards jsonb; + scopes jsonb; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) then + raise exception using + errcode = '22023', message = 'invalid Classic-v3 profile chain'; + end if; + account_address := programmable_private.decode_public_address_v1(p_account); + canonical_account := '0x' || pg_catalog.encode(account_address, 'hex'); + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'classic-v3-profile' + and snapshot_scope = 'classic-v3' + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_count + from programmable_private.account_reward_summaries_v1 as reward + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = reward.release_id + and pointer.value ->> 'modelVersion' = reward.model_id + where reward.chain_id = p_chain_id + and reward.release_id = 'classic-v3' + and reward.model_id = 'classic' + and reward.account = account_address + and exists ( + select 1 + from programmable_private.classic_v3_vault_history_v1 as owned + where owned.chain_id = reward.chain_id + and owned.release_id = reward.release_id + and owned.model_id = reward.model_id + and owned.vault = reward.vault + and owned.pool_id = reward.pool_id + and owned.effective_to_block is null + and owned.beneficiary = reward.account + ); + + select pg_catalog.count(*), + coalesce(pg_catalog.jsonb_agg(reward.reward_payload order by + reward.reward_payload ->> 'vaultAddress' + ), '[]'::jsonb), + coalesce(pg_catalog.jsonb_agg(distinct pg_catalog.jsonb_build_object( + 'model', reward.model_id, + 'releaseVersion', reward.release_id + )), '[]'::jsonb) + into public_count, rewards, scopes + from programmable_private.public_classic_v3_profile_v1 as reward + where reward.chain_id = p_chain_id + and reward.account = account_address + and reward.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and reward.checkpoint_block_hash = route_snapshot.checkpoint_block_hash; + if public_count <> current_count then return; end if; + + http_status := 200; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'account', canonical_account, + 'chainId', p_chain_id, + 'rewards', rewards + ) + ); + payload_complete := true; + record_count := public_count; + record_scopes := scopes; + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +create view programmable_private.public_stock_paired_profile_v1 +with (security_invoker = false, security_barrier = true) +as +select + reward.chain_id, + reward.account, + reward.release_id, + reward.model_id, + reward.vault, + snapshot.checkpoint_block_number, + snapshot.checkpoint_block_hash, + pg_catalog.jsonb_build_object( + 'source', programmable_private.retarget_indexed_token_projection_v2( + detail.payload #> '{data,token}', pointer.value, + snapshot.snapshot_commitment_hex + ) -> 'source', + 'tokenAddress', detail.payload #>> '{data,token,tokenAddress}', + 'tokenName', detail.payload #>> '{data,token,name}', + 'tokenSymbol', detail.payload #>> '{data,token,symbol}', + 'imageUrl', detail.payload #>> '{data,token,metadata,imageUrl}', + 'hookAddress', detail.payload #>> '{data,token,hookAddress}', + 'poolId', detail.payload #>> '{data,token,poolId}', + 'vaultAddress', '0x' || pg_catalog.encode(reward.vault, 'hex'), + 'quoteAsset', detail.payload #>> '{data,token,quote,address}', + 'quoteAssetSymbol', detail.payload #>> '{data,token,quote,symbol}', + 'beneficiary', '0x' || pg_catalog.encode( + owned.beneficiary, 'hex' + ), + 'payoutAddress', '0x' || pg_catalog.encode( + owned.payout_address, 'hex' + ), + 'shareBps', owned.share_bps, + 'claimableRaw', reward.claimable_accrued::text, + 'claimedRaw', reward.claimed_total::text, + 'generatedRaw', reward.entitled::text, + 'creatorFeesPendingRaw', reward.claimable_accrued::text, + 'beneficiaries', allocation.allocations, + 'buySwapFeeBps', + (detail.payload #>> '{data,token,fees,buySwapFeeBps}')::integer, + 'sellSwapFeeBps', + (detail.payload #>> '{data,token,fees,sellSwapFeeBps}')::integer, + 'programmableFeeBps', + (detail.payload #>> '{data,token,fees,launcherFeeBps}')::integer, + 'launchTransactionHash', + detail.payload #>> '{data,token,launchTransactionHash}', + 'estimate', null + ) as reward_payload, + snapshot.release_pointers, + snapshot.record_scopes as snapshot_record_scopes, + snapshot.route_evidence, + snapshot.snapshot_commitment_hex, + snapshot.snapshot_captured_at, + snapshot.safe_block_number, + snapshot.checkpoint_confirmations +from programmable_private.account_reward_summaries_v1 as reward +join programmable_private.route_token_projections_v1 as token + on token.route_key = 'explore-token' + and token.chain_id = reward.chain_id + and token.release_id = reward.release_id + and token.model_id = reward.model_id + and token.token = reward.token + and token.pool_id = reward.pool_id + and token.reward_vault = reward.vault + and token.route_status = 'eligible' + and token.route_mode = 'indexed' + and token.parity_status = 'current' + and token.payload_complete +join programmable_private.public_explore_token_v1 as detail + on detail.chain_id = token.chain_id + and detail.release_id = token.release_id + and detail.model_id = token.model_id + and detail.source_group = token.source_group + and detail.epoch_id = token.epoch_id + and detail.pointer_generation = token.pointer_generation + and detail.payload #>> '{data,token,tokenAddress}' = + '0x' || pg_catalog.encode(token.token, 'hex') +join programmable_private.public_route_snapshots_v2 as snapshot + on snapshot.route_key = 'creator-profile' + and snapshot.snapshot_scope = 'stock-paired' + and snapshot.chain_id = reward.chain_id + and snapshot.checkpoint_block_number = token.checkpoint_block_number + and snapshot.checkpoint_block_hash = token.checkpoint_block_hash +join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = 'creator-profile' + and readiness.chain_id = token.chain_id + and readiness.release_id = token.release_id + and readiness.model_id = token.model_id + and readiness.source_group = token.source_group + and readiness.epoch_id = token.epoch_id + and readiness.pointer_generation = token.pointer_generation + and readiness.checkpoint_block_number = snapshot.checkpoint_block_number + and readiness.checkpoint_block_hash = snapshot.checkpoint_block_hash + and readiness.route_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' +join lateral pg_catalog.jsonb_array_elements( + snapshot.release_pointers +) as pointer(value) + on pointer.value ->> 'releaseVersion' = token.release_id + and pointer.value ->> 'modelVersion' = token.model_id + and pointer.value ->> 'sourceGroup' = token.source_group +join programmable_private.stock_paired_vault_history_v1 as owned + on owned.chain_id = reward.chain_id + and owned.release_id = reward.release_id + and owned.model_id = reward.model_id + and owned.vault = reward.vault + and owned.pool_id = reward.pool_id + and owned.effective_to_block is null + and owned.beneficiary = reward.account +join lateral ( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'beneficiary', '0x' || pg_catalog.encode( + item.beneficiary, 'hex' + ), + 'payoutAddress', '0x' || pg_catalog.encode( + item.payout_address, 'hex' + ), + 'shareBps', item.share_bps + ) order by item.allocation_index + ) as allocations, + pg_catalog.count(*) as allocation_count, + pg_catalog.sum(item.share_bps) as total_share_bps + from programmable_private.stock_paired_vault_history_v1 as item + where item.chain_id = reward.chain_id + and item.release_id = reward.release_id + and item.model_id = reward.model_id + and item.vault = reward.vault + and item.pool_id = reward.pool_id + and item.effective_to_block is null +) as allocation on allocation.allocation_count between 1 and 8 + and allocation.total_share_bps = 10000 +where reward.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and reward.model_id = 'stock-paired' + and detail.payload #>> '{data,token,quote,address}' is not null + and detail.payload #>> '{data,token,quote,symbol}' is not null; + +create function programmable_private.get_public_stock_paired_profile_v1( + p_chain_id bigint, + p_account text +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + account_address bytea; + canonical_account text; + current_count bigint; + public_count bigint; + rewards jsonb; + scopes jsonb; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id <> 1 then + raise exception using + errcode = '22023', message = 'invalid Stock-paired profile chain'; + end if; + account_address := programmable_private.decode_public_address_v1(p_account); + canonical_account := '0x' || pg_catalog.encode(account_address, 'hex'); + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'creator-profile' + and snapshot_scope = 'stock-paired' + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_count + from programmable_private.account_reward_summaries_v1 as reward + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = reward.release_id + and pointer.value ->> 'modelVersion' = reward.model_id + where reward.chain_id = p_chain_id + and reward.model_id = 'stock-paired' + and reward.account = account_address + and exists ( + select 1 + from programmable_private.stock_paired_vault_history_v1 as owned + where owned.chain_id = reward.chain_id + and owned.release_id = reward.release_id + and owned.model_id = reward.model_id + and owned.vault = reward.vault + and owned.pool_id = reward.pool_id + and owned.effective_to_block is null + and owned.beneficiary = reward.account + ); + + select pg_catalog.count(*), + coalesce(pg_catalog.jsonb_agg(reward.reward_payload order by + reward.reward_payload ->> 'vaultAddress' + ), '[]'::jsonb), + coalesce(pg_catalog.jsonb_agg(distinct pg_catalog.jsonb_build_object( + 'model', reward.model_id, + 'releaseVersion', reward.release_id + )), '[]'::jsonb) + into public_count, rewards, scopes + from programmable_private.public_stock_paired_profile_v1 as reward + where reward.chain_id = p_chain_id + and reward.account = account_address + and reward.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and reward.checkpoint_block_hash = route_snapshot.checkpoint_block_hash; + if public_count <> current_count then return; end if; + + http_status := 200; + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'account', canonical_account, + 'chainId', p_chain_id, + 'rewards', rewards + ) + ); + payload_complete := true; + record_count := public_count; + record_scopes := scopes; + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +create view programmable_private.public_launch_lookup_v1 +with (security_invoker = false, security_barrier = true) +as +select + token.chain_id, + case + when token.release_id = 'classic-v3' then 'classic-v3' + else 'stock-paired' + end::text as surface, + token.creator as account, + token.launch_transaction_hash, + token.release_id, + token.model_id, + token.source_group, + snapshot.checkpoint_block_number, + snapshot.checkpoint_block_hash, + programmable_private.retarget_indexed_token_projection_v2( + detail.payload #> '{data,token}', pointer.value, + snapshot.snapshot_commitment_hex + ) as token_payload, + snapshot.release_pointers, + snapshot.record_scopes as snapshot_record_scopes, + snapshot.route_evidence, + snapshot.snapshot_commitment_hex, + snapshot.snapshot_captured_at, + snapshot.safe_block_number, + snapshot.checkpoint_confirmations +from programmable_private.route_token_projections_v1 as token +join programmable_private.public_explore_token_v1 as detail + on detail.chain_id = token.chain_id + and detail.release_id = token.release_id + and detail.model_id = token.model_id + and detail.source_group = token.source_group + and detail.epoch_id = token.epoch_id + and detail.pointer_generation = token.pointer_generation + and detail.payload #>> '{data,token,tokenAddress}' = + '0x' || pg_catalog.encode(token.token, 'hex') +join programmable_private.public_route_snapshots_v2 as snapshot + on snapshot.route_key = 'launch-lookup' + and snapshot.snapshot_scope = case + when token.release_id = 'classic-v3' then 'classic-v3' + else 'stock-paired' + end + and snapshot.chain_id = token.chain_id + and snapshot.checkpoint_block_number = token.checkpoint_block_number + and snapshot.checkpoint_block_hash = token.checkpoint_block_hash +join programmable_private.route_snapshot_readiness_v1 as readiness + on readiness.route_key = 'launch-lookup' + and readiness.chain_id = token.chain_id + and readiness.release_id = token.release_id + and readiness.model_id = token.model_id + and readiness.source_group = token.source_group + and readiness.epoch_id = token.epoch_id + and readiness.pointer_generation = token.pointer_generation + and readiness.checkpoint_block_number = snapshot.checkpoint_block_number + and readiness.checkpoint_block_hash = snapshot.checkpoint_block_hash + and readiness.route_status = 'eligible' + and readiness.route_mode = 'indexed' + and readiness.parity_status = 'current' +join lateral pg_catalog.jsonb_array_elements( + snapshot.release_pointers +) as pointer(value) + on pointer.value ->> 'releaseVersion' = token.release_id + and pointer.value ->> 'modelVersion' = token.model_id + and pointer.value ->> 'sourceGroup' = token.source_group +where token.route_key = 'explore-token' + and token.route_status = 'eligible' + and token.route_mode = 'indexed' + and token.parity_status = 'current' + and token.payload_complete + and ( + token.release_id = 'classic-v3' and token.model_id = 'classic' + or token.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and token.model_id = 'stock-paired' + ); + +create function programmable_private.get_public_launch_lookup_v1( + p_chain_id bigint, + p_surface text, + p_account text, + p_transaction_hash text +) +returns table ( + http_status integer, + payload jsonb, + payload_complete boolean, + record_count bigint, + record_scopes jsonb, + comparison_checkpoint_block_number bigint, + comparison_checkpoint_block_hash bytea, + route_evidence jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + route_snapshot programmable_private.public_route_snapshots_v2%rowtype; + account_address bytea; + transaction_hash bytea; + canonical_account text; + canonical_transaction_hash text; + current_count bigint; + public_count bigint; + selected programmable_private.public_launch_lookup_v1%rowtype; + resolution text; +begin + perform programmable_private.assert_caller('programmable_api_reader'); + if p_chain_id not in (1, 11155111) + or p_surface not in ('classic-v3', 'stock-paired') + then + raise exception using + errcode = '22023', message = 'invalid launch-lookup request'; + end if; + account_address := programmable_private.decode_public_address_v1(p_account); + transaction_hash := programmable_private.decode_public_bytes32_v1( + p_transaction_hash + ); + canonical_account := '0x' || pg_catalog.encode(account_address, 'hex'); + canonical_transaction_hash := + '0x' || pg_catalog.encode(transaction_hash, 'hex'); + + select * into route_snapshot + from programmable_private.public_route_snapshots_v2 + where route_key = 'launch-lookup' + and snapshot_scope = p_surface + and chain_id = p_chain_id; + if not found then return; end if; + + select pg_catalog.count(*) into current_count + from programmable_private.current_launch_projections_v1 as launch + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + join lateral pg_catalog.jsonb_array_elements( + route_snapshot.release_pointers + ) as pointer(value) + on pointer.value ->> 'releaseVersion' = launch.release_id + and pointer.value ->> 'modelVersion' = launch.model_id + and pointer.value ->> 'sourceGroup' = run.source_group + and (pointer.value ->> 'epochId')::uuid = launch.epoch_id + and (pointer.value ->> 'pointerGeneration')::bigint = + launch.pointer_generation + where launch.chain_id = p_chain_id + and launch.creator = account_address + and launch.launch_transaction_hash = transaction_hash; + if current_count > 1 then + raise exception using + errcode = '23514', message = 'launch lookup is ambiguous'; + end if; + + select pg_catalog.count(*) into public_count + from programmable_private.public_launch_lookup_v1 as item + where item.chain_id = p_chain_id + and item.surface = p_surface + and item.account = account_address + and item.launch_transaction_hash = transaction_hash + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash; + if public_count > 1 then + raise exception using + errcode = '23514', message = 'public launch lookup is ambiguous'; + end if; + if current_count = 1 and public_count = 0 then return; end if; + + if public_count = 1 then + select * into selected + from programmable_private.public_launch_lookup_v1 as item + where item.chain_id = p_chain_id + and item.surface = p_surface + and item.account = account_address + and item.launch_transaction_hash = transaction_hash + and item.checkpoint_block_number = + route_snapshot.checkpoint_block_number + and item.checkpoint_block_hash = route_snapshot.checkpoint_block_hash; + resolution := 'found'; + http_status := 200; + record_count := 1; + record_scopes := pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object( + 'model', selected.model_id, + 'releaseVersion', selected.release_id + ) + ); + else + resolution := case + when p_surface = 'classic-v3' then 'not-found' + else 'pending' + end; + http_status := case + when p_surface = 'classic-v3' then 200 + else 202 + end; + record_count := 0; + record_scopes := '[]'::jsonb; + end if; + + payload := pg_catalog.jsonb_build_object( + 'status', 'ready', + 'snapshot', programmable_private.build_public_snapshot_identity_v2( + route_snapshot.snapshot_commitment_hex, + route_snapshot.chain_id, + route_snapshot.checkpoint_block_number, + route_snapshot.checkpoint_block_hash, + route_snapshot.checkpoint_confirmations, + route_snapshot.snapshot_captured_at, + route_snapshot.release_pointers + ), + 'data', pg_catalog.jsonb_build_object( + 'surface', p_surface, + 'account', canonical_account, + 'transactionHash', canonical_transaction_hash, + 'resolution', resolution, + 'token', case when public_count = 1 + then selected.token_payload else null end + ) + ); + payload_complete := true; + comparison_checkpoint_block_number := + route_snapshot.checkpoint_block_number; + comparison_checkpoint_block_hash := route_snapshot.checkpoint_block_hash; + route_evidence := route_snapshot.route_evidence; + return next; +end +$function$; + +create view programmable_private.read_model_performance_eligible_launches_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + run.source_group, + launch.creator as account, + launch.launch_transaction_hash as transaction_hash, + launch.token as token_address, + launch.projection_run_id, + launch.promoted_block_number, + launch.promoted_block_hash +from programmable_private.current_launch_projections_v1 as launch +join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = launch.chain_id + and run.release_id = launch.release_id + and run.model_id = launch.model_id + and run.epoch_id = launch.epoch_id + and run.captured_pointer_generation = launch.pointer_generation +join programmable_private.route_eligibility_current_exact_v1 as route + on route.route_key = 'explore-token' + and route.chain_id = launch.chain_id + and route.release_id = launch.release_id + and route.model_id = launch.model_id + and route.source_group = run.source_group + and route.epoch_id = launch.epoch_id + and route.pointer_generation = launch.pointer_generation + and route.status = 'eligible' + and route.route_mode = 'indexed' +where launch.is_complete + and ( + launch.release_id in ('classic-v2', 'classic-v3') + and launch.model_id = 'classic' + or launch.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and launch.model_id = 'stock-paired' + ); + +create function programmable_private.get_read_model_performance_dataset_v1( + p_chain_id bigint +) +returns table ( + generated_at timestamptz, + launch_count bigint, + eligible_launch_count bigint, + chain_event_count bigint, + market_snapshot_count bigint, + market_candle_count bigint, + account_count bigint, + reward_row_count bigint, + candidate_count bigint, + release_coverage jsonb, + eligible_launches jsonb, + token_addresses jsonb, + account_addresses jsonb, + account_evidence jsonb, + classic_launches jsonb, + stock_launches jsonb, + candidate_ids jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_chain_id not in (1, 11155111) then + raise exception using + errcode = '22023', message = 'invalid performance dataset chain'; + end if; + + return query + with eligible as materialized ( + select launch.* + from programmable_private.read_model_performance_eligible_launches_v1 + as launch + where launch.chain_id = p_chain_id + ), release_counts as ( + select + pg_catalog.count(*) as total_count, + pg_catalog.count(*) filter ( + where release_id = 'classic-v2' + ) as classic_v2_count, + pg_catalog.count(*) filter ( + where release_id = 'classic-v3' + ) as classic_v3_count, + pg_catalog.count(*) filter ( + where release_id = 'stock-paired-v1' + ) as stock_v1_count, + pg_catalog.count(*) filter ( + where release_id = 'stock-paired-v2' + ) as stock_v2_count, + pg_catalog.count(*) filter ( + where release_id = 'stock-paired-v3' + ) as stock_v3_count, + pg_catalog.count(distinct token_address) as unique_tokens, + pg_catalog.count(distinct transaction_hash) as unique_transactions + from eligible + ), eligible_payload as ( + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'account', '0x' || pg_catalog.encode(account, 'hex'), + 'transactionHash', '0x' || pg_catalog.encode( + transaction_hash, 'hex' + ), + 'tokenAddress', '0x' || pg_catalog.encode( + token_address, 'hex' + ), + 'releaseVersion', release_id + ) order by release_id, token_address, transaction_hash, account + ), '[]'::jsonb) as value + from eligible + ), token_sample as ( + select coalesce(pg_catalog.jsonb_agg(sample.address order by sample.address), + '[]'::jsonb) as value, + pg_catalog.count(*) as sample_count + from ( + select distinct + '0x' || pg_catalog.encode(token_address, 'hex') as address + from eligible + order by address + limit 100 + ) as sample + ), live_accounts as materialized ( + select account, + pg_catalog.sum(profile_rows)::bigint as profile_rows, + pg_catalog.sum(reward_rows)::bigint as reward_rows + from ( + select account, pg_catalog.count(*)::bigint as profile_rows, + 0::bigint as reward_rows + from eligible + group by account + union all + select balance.account, 0::bigint, + pg_catalog.count(*)::bigint + from programmable_private.current_account_reward_balances_v1 as balance + where balance.chain_id = p_chain_id + and ( + balance.release_id in ('classic-v2', 'classic-v3') + and balance.model_id = 'classic' + or balance.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and balance.model_id = 'stock-paired' + ) + group by balance.account + ) as evidence + group by account + ), account_sample as ( + select coalesce(pg_catalog.jsonb_agg(sample.address order by sample.address), + '[]'::jsonb) as value, + coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'account', sample.address, + 'profileRows', sample.profile_rows, + 'rewardRows', sample.reward_rows + ) order by sample.address + ), '[]'::jsonb) as evidence, + pg_catalog.count(*) as sample_count, + coalesce(pg_catalog.sum(sample.profile_rows), 0) as profile_rows, + coalesce(pg_catalog.sum(sample.reward_rows), 0) as reward_rows, + pg_catalog.bool_and( + sample.profile_rows >= 0 + and sample.reward_rows >= 0 + and sample.profile_rows + sample.reward_rows > 0 + ) as all_backed + from ( + select '0x' || pg_catalog.encode(account, 'hex') as address, + profile_rows, reward_rows + from live_accounts + order by account + limit 100 + ) as sample + ), classic_sample as ( + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'account', '0x' || pg_catalog.encode(sample.account, 'hex'), + 'transactionHash', '0x' || pg_catalog.encode( + sample.transaction_hash, 'hex' + ) + ) order by sample.account, sample.transaction_hash + ), '[]'::jsonb) as value, + pg_catalog.count(*) as sample_count + from ( + select distinct account, transaction_hash + from eligible + where release_id = 'classic-v3' and model_id = 'classic' + order by account, transaction_hash + limit 32 + ) as sample + ), stock_sample as ( + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'account', '0x' || pg_catalog.encode(sample.account, 'hex'), + 'transactionHash', '0x' || pg_catalog.encode( + sample.transaction_hash, 'hex' + ) + ) order by sample.account, sample.transaction_hash + ), '[]'::jsonb) as value, + pg_catalog.count(*) as sample_count + from ( + select distinct account, transaction_hash + from eligible + where model_id = 'stock-paired' + order by account, transaction_hash + limit 32 + ) as sample + ), canonical_candidates as materialized ( + select + occurrence.first_seen_envio_candidate_id as candidate_id, + occurrence.block_number, + occurrence.transaction_hash, + occurrence.transaction_index, + occurrence.block_global_log_index, + occurrence.source_address, + pg_catalog.row_number() over ( + partition by occurrence.block_number + order by occurrence.transaction_index, + occurrence.block_global_log_index, + occurrence.transaction_hash, + occurrence.first_seen_envio_candidate_id + ) as block_rank + from programmable_private.chain_event_current_canonical as canonical + join programmable_private.chain_event_materialized_occurrences_v1 + as occurrence + on occurrence.occurrence_id = canonical.occurrence_id + and occurrence.logical_event_id = canonical.logical_event_id + and occurrence.block_hash = canonical.block_hash + join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = occurrence.chain_id + and current_epoch.release_id = occurrence.release_id + and current_epoch.model_id = occurrence.model_id + and current_epoch.source_group = occurrence.source_group + and current_epoch.epoch_id = occurrence.epoch_id + and current_epoch.generation = occurrence.pointer_generation + where occurrence.chain_id = p_chain_id + and occurrence.first_seen_envio_candidate_id is not null + and ( + occurrence.release_id in ('classic-v2', 'classic-v3') + and occurrence.model_id = 'classic' + or occurrence.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and occurrence.model_id = 'stock-paired' + ) + ), selected_candidates as materialized ( + select * + from canonical_candidates + where block_rank = 1 + order by block_number, transaction_index, block_global_log_index, + transaction_hash, candidate_id + limit 8 + ), candidate_sample as ( + select coalesce(pg_catalog.jsonb_agg(candidate_id order by + block_number, transaction_index, block_global_log_index, + transaction_hash, candidate_id + ), '[]'::jsonb) as value, + pg_catalog.count(*) as sample_count, + pg_catalog.count(distinct block_number) as distinct_blocks, + pg_catalog.count(distinct transaction_hash) as distinct_transactions, + pg_catalog.count(distinct (block_number, source_address)) + as distinct_block_sources + from selected_candidates + ), evidence_counts as ( + select + ( + select pg_catalog.count(distinct canonical.occurrence_id) + from programmable_private.chain_event_current_canonical as canonical + join programmable_private.chain_event_materialized_occurrences_v1 + as occurrence + on occurrence.occurrence_id = canonical.occurrence_id + and occurrence.logical_event_id = canonical.logical_event_id + and occurrence.block_hash = canonical.block_hash + join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = occurrence.chain_id + and current_epoch.release_id = occurrence.release_id + and current_epoch.model_id = occurrence.model_id + and current_epoch.source_group = occurrence.source_group + and current_epoch.epoch_id = occurrence.epoch_id + and current_epoch.generation = occurrence.pointer_generation + where occurrence.chain_id = p_chain_id + and ( + occurrence.release_id in ('classic-v2', 'classic-v3') + and occurrence.model_id = 'classic' + or occurrence.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and occurrence.model_id = 'stock-paired' + ) + ) as chain_events, + ( + select pg_catalog.count(*) + from programmable_private.market_snapshots_v2 as snapshot + where snapshot.chain_id = p_chain_id + and ( + snapshot.release_id in ('classic-v2', 'classic-v3') + and snapshot.model_id = 'classic' + or snapshot.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and snapshot.model_id = 'stock-paired' + ) + ) as market_snapshots, + ( + select pg_catalog.count(*) + from programmable_private.market_candles_v2 as candle + where candle.chain_id = p_chain_id + and ( + candle.release_id in ('classic-v2', 'classic-v3') + and candle.model_id = 'classic' + or candle.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and candle.model_id = 'stock-paired' + ) + ) as market_candles, + (select pg_catalog.count(*) from live_accounts) as accounts, + ( + select pg_catalog.count(*) + from programmable_private.current_account_reward_balances_v1 + as reward + where reward.chain_id = p_chain_id + and ( + reward.release_id in ('classic-v2', 'classic-v3') + and reward.model_id = 'classic' + or reward.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and reward.model_id = 'stock-paired' + ) + ) as reward_rows, + (select pg_catalog.count(*) from canonical_candidates) + as candidates + ) + select + pg_catalog.transaction_timestamp(), + release_counts.total_count, + release_counts.total_count, + evidence_counts.chain_events, + evidence_counts.market_snapshots, + evidence_counts.market_candles, + evidence_counts.accounts, + evidence_counts.reward_rows, + evidence_counts.candidates, + pg_catalog.jsonb_build_object( + 'classic-v2', release_counts.classic_v2_count, + 'classic-v3', release_counts.classic_v3_count, + 'stock-paired-v1', release_counts.stock_v1_count, + 'stock-paired-v2', release_counts.stock_v2_count, + 'stock-paired-v3', release_counts.stock_v3_count + ), + eligible_payload.value, + token_sample.value, + account_sample.value, + account_sample.evidence, + classic_sample.value, + stock_sample.value, + candidate_sample.value + from release_counts + cross join eligible_payload + cross join token_sample + cross join account_sample + cross join classic_sample + cross join stock_sample + cross join candidate_sample + cross join evidence_counts + where release_counts.total_count >= 200 + and release_counts.classic_v2_count > 0 + and release_counts.classic_v3_count > 0 + and release_counts.stock_v1_count > 0 + and release_counts.stock_v2_count > 0 + and release_counts.stock_v3_count > 0 + and release_counts.total_count = + release_counts.classic_v2_count + + release_counts.classic_v3_count + + release_counts.stock_v1_count + + release_counts.stock_v2_count + + release_counts.stock_v3_count + and release_counts.unique_tokens = release_counts.total_count + and release_counts.unique_transactions = release_counts.total_count + and token_sample.sample_count = 100 + and account_sample.sample_count = 100 + and account_sample.all_backed + and account_sample.profile_rows <= release_counts.total_count + and account_sample.reward_rows <= evidence_counts.reward_rows + and classic_sample.sample_count = 32 + and stock_sample.sample_count = 32 + and candidate_sample.sample_count = 8 + and candidate_sample.distinct_blocks = 8 + and candidate_sample.distinct_transactions = 8 + and candidate_sample.distinct_block_sources = 8; +end +$function$; + +create function programmable_private.get_projector_reward_state_by_vault_v1( + p_projection_run_id uuid, + p_vault bytea +) +returns table ( + chain_id bigint, + release_id text, + model_id text, + source_group text, + epoch_id uuid, + pointer_generation bigint, + checkpoint_id uuid, + projector_version text, + checkpoint_generation bigint, + reorg_generation bigint, + checkpoint_block_number bigint, + checkpoint_block_hash bytea, + reward_vault_projection_id uuid, + allocation_fact_id uuid, + allocation_evidence_id uuid, + vault bytea, + pool_id bytea, + quote_asset bytea, + configuration_hash bytea, + active_configuration_hash bytea, + total_creator_fees_received numeric, + configuration_epoch bigint, + allocation_index integer, + beneficiary bytea, + payout_address bytea, + share_bps integer, + claimable_accrued numeric, + claimed_total numeric, + baseline_projection_run_id uuid, + baseline_publication_commitment bytea, + baseline_promoted_block_number bigint, + baseline_promoted_block_hash bytea, + balance_projection_run_id uuid, + balance_publication_commitment bytea, + balance_promoted_block_number bigint, + balance_promoted_block_hash bytea, + vault_source_occurrence_id uuid, + vault_source_logical_event_id uuid, + vault_source_block_hash bytea, + allocation_source_occurrence_id uuid, + allocation_source_logical_event_id uuid, + allocation_source_block_hash bytea, + balance_source_occurrence_id uuid, + balance_source_logical_event_id uuid, + balance_source_block_hash bytea, + verified_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + baseline record; + raw_vault_count bigint; + baseline_count bigint; + active_allocation_count bigint; + eligible_row_count bigint; + unique_allocation_count bigint; + unique_beneficiary_count bigint; + configuration_epoch_count bigint; + total_share_bps bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_open_projection_run_v1( + p_projection_run_id + ); + select * into header + from programmable_private.run_headers + where run_id = p_projection_run_id and run_kind = 'projection'; + if p_vault is null or pg_catalog.octet_length(p_vault) <> 20 then + raise exception using + errcode = '22023', message = 'invalid reward-state vault'; + end if; + + select pg_catalog.count(*) into raw_vault_count + from programmable_private.current_reward_vault_projections_v1 as current_vault + join programmable_private.run_headers as current_run + on current_run.run_id = current_vault.projection_run_id + and current_run.run_kind = 'projection' + and current_run.source_group = header.source_group + where current_vault.chain_id = header.chain_id + and current_vault.release_id = header.release_id + and current_vault.model_id = header.model_id + and current_vault.epoch_id = header.epoch_id + and current_vault.pointer_generation = + header.captured_pointer_generation + and current_vault.vault = p_vault; + if raw_vault_count > 1 then + raise exception using + errcode = '23514', message = 'reward-state vault is ambiguous'; + end if; + if raw_vault_count = 0 then return; end if; + + select pg_catalog.count(*) into baseline_count + from programmable_private.current_reward_vault_projections_v1 as current_vault + join programmable_private.projection_entity_current as entity + on entity.entity_kind = 'reward_vault' + and entity.projection_row_id = current_vault.reward_vault_projection_id + and entity.projection_run_id = current_vault.projection_run_id + and entity.chain_id = current_vault.chain_id + and entity.release_id = current_vault.release_id + and entity.model_id = current_vault.model_id + and entity.source_group = header.source_group + join programmable_private.projection_publications as publication + on publication.publication_id = entity.publication_id + and publication.run_id = current_vault.projection_run_id + and publication.checkpoint_id = entity.checkpoint_id + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = entity.checkpoint_id + and checkpoint.chain_id = current_vault.chain_id + and checkpoint.release_id = current_vault.release_id + and checkpoint.model_id = current_vault.model_id + and checkpoint.source_group = header.source_group + and checkpoint.epoch_id = header.epoch_id + and checkpoint.pointer_generation = header.captured_pointer_generation + join programmable_private.projector_checkpoint_current as current_checkpoint + on current_checkpoint.chain_id = checkpoint.chain_id + and current_checkpoint.release_id = checkpoint.release_id + and current_checkpoint.model_id = checkpoint.model_id + and current_checkpoint.source_group = checkpoint.source_group + and current_checkpoint.projector_version = checkpoint.projector_version + join programmable_private.projector_checkpoints as current_cursor + on current_cursor.checkpoint_id = current_checkpoint.checkpoint_id + and current_cursor.chain_id = checkpoint.chain_id + and current_cursor.release_id = checkpoint.release_id + and current_cursor.model_id = checkpoint.model_id + and current_cursor.source_group = checkpoint.source_group + and current_cursor.projector_version = checkpoint.projector_version + and current_cursor.epoch_id = header.epoch_id + and current_cursor.pointer_generation = + header.captured_pointer_generation + and current_cursor.checkpoint_generation = + current_checkpoint.checkpoint_generation + and current_cursor.reorg_generation = current_checkpoint.reorg_generation + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = current_vault.last_source_occurrence_id + and canonical.logical_event_id = + current_vault.last_source_logical_event_id + and canonical.block_hash = + current_vault.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as vault_source + on vault_source.occurrence_id = current_vault.last_source_occurrence_id + and vault_source.logical_event_id = + current_vault.last_source_logical_event_id + and vault_source.block_hash = + current_vault.last_source_occurrence_block_hash + and vault_source.chain_id = current_vault.chain_id + and vault_source.release_id = current_vault.release_id + and vault_source.model_id = current_vault.model_id + and vault_source.source_group = header.source_group + and vault_source.epoch_id = current_vault.epoch_id + and vault_source.pointer_generation = current_vault.pointer_generation + where current_vault.chain_id = header.chain_id + and current_vault.release_id = header.release_id + and current_vault.model_id = header.model_id + and current_vault.epoch_id = header.epoch_id + and current_vault.pointer_generation = + header.captured_pointer_generation + and current_vault.vault = p_vault + and current_cursor.reorg_generation = checkpoint.reorg_generation + and ( + current_cursor.block_number, + current_cursor.cursor_block_global_log_index + ) >= ( + checkpoint.block_number, + checkpoint.cursor_block_global_log_index + ) + and programmable_private.has_current_verified_reward_seed( + current_vault.projection_run_id, current_vault.vault + ); + if baseline_count <> 1 then + raise exception using + errcode = '23514', + message = 'reward-state baseline is not exact-current'; + end if; + + select + current_vault.reward_vault_projection_id, + current_vault.current_allocation_fact_id, + current_vault.projection_run_id, + current_vault.promoted_block_number, + current_vault.promoted_block_hash, + entity.checkpoint_id, + checkpoint.projector_version, + checkpoint.checkpoint_generation, + checkpoint.reorg_generation, + checkpoint.block_number, + checkpoint.block_hash, + publication_audit.input_commitment + into baseline + from programmable_private.current_reward_vault_projections_v1 as current_vault + join programmable_private.projection_entity_current as entity + on entity.entity_kind = 'reward_vault' + and entity.projection_row_id = current_vault.reward_vault_projection_id + and entity.projection_run_id = current_vault.projection_run_id + and entity.source_group = header.source_group + join programmable_private.projection_publications as publication + on publication.publication_id = entity.publication_id + and publication.run_id = current_vault.projection_run_id + and publication.checkpoint_id = entity.checkpoint_id + join programmable_private.mutation_audits as publication_audit + on publication_audit.audit_id = publication.audit_id + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = entity.checkpoint_id + join programmable_private.projector_checkpoint_current as current_checkpoint + on current_checkpoint.chain_id = checkpoint.chain_id + and current_checkpoint.release_id = checkpoint.release_id + and current_checkpoint.model_id = checkpoint.model_id + and current_checkpoint.source_group = checkpoint.source_group + and current_checkpoint.projector_version = checkpoint.projector_version + join programmable_private.projector_checkpoints as current_cursor + on current_cursor.checkpoint_id = current_checkpoint.checkpoint_id + and current_cursor.chain_id = checkpoint.chain_id + and current_cursor.release_id = checkpoint.release_id + and current_cursor.model_id = checkpoint.model_id + and current_cursor.source_group = checkpoint.source_group + and current_cursor.projector_version = checkpoint.projector_version + and current_cursor.epoch_id = header.epoch_id + and current_cursor.pointer_generation = + header.captured_pointer_generation + and current_cursor.checkpoint_generation = + current_checkpoint.checkpoint_generation + and current_cursor.reorg_generation = current_checkpoint.reorg_generation + where current_vault.chain_id = header.chain_id + and current_vault.release_id = header.release_id + and current_vault.model_id = header.model_id + and current_vault.epoch_id = header.epoch_id + and current_vault.pointer_generation = + header.captured_pointer_generation + and current_vault.vault = p_vault + and current_cursor.reorg_generation = checkpoint.reorg_generation + and ( + current_cursor.block_number, + current_cursor.cursor_block_global_log_index + ) >= ( + checkpoint.block_number, + checkpoint.cursor_block_global_log_index + ); + + select pg_catalog.count(*), + pg_catalog.count(distinct allocation.allocation_index), + pg_catalog.count(distinct allocation.beneficiary), + pg_catalog.count(distinct allocation.configuration_epoch), + coalesce(pg_catalog.sum(allocation.share_bps), 0) + into active_allocation_count, unique_allocation_count, + unique_beneficiary_count, configuration_epoch_count, total_share_bps + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + baseline.reward_vault_projection_id + and allocation.projection_run_id = baseline.projection_run_id + and allocation.allocation_fact_id = baseline.current_allocation_fact_id + and allocation.effective_to_block is null; + if active_allocation_count < 1 + or active_allocation_count > ( + case when header.release_id = 'classic-v3' then 5 else 8 end + ) + or unique_allocation_count <> active_allocation_count + or ( + header.release_id <> 'classic-v3' + and unique_beneficiary_count <> active_allocation_count + ) + or configuration_epoch_count <> 1 + or total_share_bps <> 10000 + then + raise exception using + errcode = '23514', + message = 'reward-state active allocation set is incomplete'; + end if; + + select pg_catalog.count(*) into eligible_row_count + from programmable_private.reward_allocation_projections as allocation + join programmable_private.chain_event_current_canonical + as allocation_canonical + on allocation_canonical.occurrence_id = + allocation.last_source_occurrence_id + and allocation_canonical.logical_event_id = + allocation.last_source_logical_event_id + and allocation_canonical.block_hash = + allocation.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as allocation_source + on allocation_source.occurrence_id = allocation.last_source_occurrence_id + and allocation_source.logical_event_id = + allocation.last_source_logical_event_id + and allocation_source.block_hash = + allocation.last_source_occurrence_block_hash + and allocation_source.chain_id = allocation.chain_id + and allocation_source.release_id = allocation.release_id + and allocation_source.model_id = allocation.model_id + and allocation_source.source_group = header.source_group + and allocation_source.epoch_id = allocation.epoch_id + and allocation_source.pointer_generation = allocation.pointer_generation + join programmable_private.current_account_reward_balances_v1 as balance + on balance.chain_id = allocation.chain_id + and balance.release_id = allocation.release_id + and balance.model_id = allocation.model_id + and balance.epoch_id = allocation.epoch_id + and balance.pointer_generation = allocation.pointer_generation + and balance.vault = p_vault + and balance.account = case + when header.release_id = 'classic-v3' + then allocation.payout_address + else allocation.beneficiary + end + join programmable_private.projection_entity_current as balance_entity + on balance_entity.entity_kind = 'account_reward_balance' + and balance_entity.projection_row_id = balance.account_reward_balance_id + and balance_entity.projection_run_id = balance.projection_run_id + and balance_entity.source_group = header.source_group + join programmable_private.projection_publications as balance_publication + on balance_publication.publication_id = balance_entity.publication_id + and balance_publication.run_id = balance.projection_run_id + join programmable_private.chain_event_current_canonical as balance_canonical + on balance_canonical.occurrence_id = balance.last_source_occurrence_id + and balance_canonical.logical_event_id = + balance.last_source_logical_event_id + and balance_canonical.block_hash = + balance.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as balance_source + on balance_source.occurrence_id = balance.last_source_occurrence_id + and balance_source.logical_event_id = balance.last_source_logical_event_id + and balance_source.block_hash = balance.last_source_occurrence_block_hash + and balance_source.chain_id = balance.chain_id + and balance_source.release_id = balance.release_id + and balance_source.model_id = balance.model_id + and balance_source.source_group = header.source_group + and balance_source.epoch_id = balance.epoch_id + and balance_source.pointer_generation = balance.pointer_generation + where allocation.reward_vault_projection_id = + baseline.reward_vault_projection_id + and allocation.projection_run_id = baseline.projection_run_id + and allocation.allocation_fact_id = baseline.current_allocation_fact_id + and allocation.effective_to_block is null; + if eligible_row_count <> active_allocation_count then + raise exception using + errcode = '23514', + message = 'reward-state balance or canonical provenance is incomplete'; + end if; + + return query + select + allocation.chain_id::bigint, + allocation.release_id::text, + allocation.model_id::text, + header.source_group::text, + allocation.epoch_id, + allocation.pointer_generation, + baseline.checkpoint_id::uuid, + baseline.projector_version::text, + baseline.checkpoint_generation::bigint, + baseline.reorg_generation::bigint, + baseline.block_number::bigint, + baseline.block_hash::bytea, + vault_projection.reward_vault_projection_id, + vault_projection.current_allocation_fact_id, + ( + select verified_seed.allocation_evidence_id + from programmable_private.reward_allocation_current_verified + as verified_seed + where verified_seed.allocation_fact_id = + vault_projection.current_allocation_fact_id + and verified_seed.vault = vault_projection.vault + )::uuid, + vault_projection.vault::bytea, + vault_projection.pool_id::bytea, + vault_projection.quote_asset::bytea, + vault_projection.configuration_hash::bytea, + coalesce( + vault_projection.active_configuration_hash, + vault_projection.configuration_hash + )::bytea, + coalesce( + vault_projection.total_creator_fees_received, + ( + select pg_catalog.sum( + current_balance.claimable_accrued + current_balance.claimed_total + ) + from programmable_private.current_account_reward_balances_v1 + as current_balance + where current_balance.chain_id = vault_projection.chain_id + and current_balance.release_id = vault_projection.release_id + and current_balance.model_id = vault_projection.model_id + and current_balance.epoch_id = vault_projection.epoch_id + and current_balance.pointer_generation = + vault_projection.pointer_generation + and current_balance.vault = vault_projection.vault + ), + 0 + )::numeric, + allocation.configuration_epoch, + allocation.allocation_index, + case + when header.release_id = 'classic-v3' + then allocation.payout_address + else allocation.beneficiary + end::bytea, + allocation.payout_address::bytea, + allocation.share_bps::integer, + balance.claimable_accrued::numeric, + balance.claimed_total::numeric, + vault_projection.projection_run_id, + baseline.input_commitment::bytea, + vault_projection.promoted_block_number::bigint, + vault_projection.promoted_block_hash::bytea, + balance.projection_run_id, + balance_publication_audit.input_commitment::bytea, + balance.promoted_block_number::bigint, + balance.promoted_block_hash::bytea, + vault_projection.last_source_occurrence_id, + vault_projection.last_source_logical_event_id, + vault_projection.last_source_occurrence_block_hash::bytea, + allocation.last_source_occurrence_id, + allocation.last_source_logical_event_id, + allocation.last_source_occurrence_block_hash::bytea, + balance.last_source_occurrence_id, + balance.last_source_logical_event_id, + balance.last_source_occurrence_block_hash::bytea, + greatest( + vault_projection.verified_at, + allocation.verified_at, + balance.verified_at + ) + from programmable_private.current_reward_vault_projections_v1 + as vault_projection + join programmable_private.reward_allocation_projections as allocation + on allocation.reward_vault_projection_id = + vault_projection.reward_vault_projection_id + and allocation.projection_run_id = vault_projection.projection_run_id + and allocation.allocation_fact_id = + vault_projection.current_allocation_fact_id + and allocation.effective_to_block is null + join programmable_private.chain_event_current_canonical + as allocation_canonical + on allocation_canonical.occurrence_id = + allocation.last_source_occurrence_id + and allocation_canonical.logical_event_id = + allocation.last_source_logical_event_id + and allocation_canonical.block_hash = + allocation.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as allocation_source + on allocation_source.occurrence_id = allocation.last_source_occurrence_id + and allocation_source.logical_event_id = + allocation.last_source_logical_event_id + and allocation_source.block_hash = + allocation.last_source_occurrence_block_hash + and allocation_source.chain_id = allocation.chain_id + and allocation_source.release_id = allocation.release_id + and allocation_source.model_id = allocation.model_id + and allocation_source.source_group = header.source_group + and allocation_source.epoch_id = allocation.epoch_id + and allocation_source.pointer_generation = allocation.pointer_generation + join programmable_private.current_account_reward_balances_v1 as balance + on balance.chain_id = allocation.chain_id + and balance.release_id = allocation.release_id + and balance.model_id = allocation.model_id + and balance.epoch_id = allocation.epoch_id + and balance.pointer_generation = allocation.pointer_generation + and balance.vault = vault_projection.vault + and balance.account = case + when header.release_id = 'classic-v3' + then allocation.payout_address + else allocation.beneficiary + end + join programmable_private.projection_entity_current as balance_entity + on balance_entity.entity_kind = 'account_reward_balance' + and balance_entity.projection_row_id = balance.account_reward_balance_id + and balance_entity.projection_run_id = balance.projection_run_id + and balance_entity.source_group = header.source_group + join programmable_private.projection_publications as balance_publication + on balance_publication.publication_id = balance_entity.publication_id + and balance_publication.run_id = balance.projection_run_id + join programmable_private.mutation_audits as balance_publication_audit + on balance_publication_audit.audit_id = balance_publication.audit_id + join programmable_private.chain_event_current_canonical as balance_canonical + on balance_canonical.occurrence_id = balance.last_source_occurrence_id + and balance_canonical.logical_event_id = + balance.last_source_logical_event_id + and balance_canonical.block_hash = + balance.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as balance_source + on balance_source.occurrence_id = balance.last_source_occurrence_id + and balance_source.logical_event_id = balance.last_source_logical_event_id + and balance_source.block_hash = balance.last_source_occurrence_block_hash + and balance_source.chain_id = balance.chain_id + and balance_source.release_id = balance.release_id + and balance_source.model_id = balance.model_id + and balance_source.source_group = header.source_group + and balance_source.epoch_id = balance.epoch_id + and balance_source.pointer_generation = balance.pointer_generation + where vault_projection.reward_vault_projection_id = + baseline.reward_vault_projection_id + order by allocation.allocation_index; +end +$function$; + +alter table programmable_private.reward_vault_projections + add column snapshot_kind text, + add column configuration_epoch bigint check (configuration_epoch > 0), + add column active_configuration_hash programmable_private.bytes32_value, + add column total_creator_fees_received + programmable_private.uint256_value, + add column baseline_reward_vault_projection_id uuid + references programmable_private.reward_vault_projections( + reward_vault_projection_id + ) on delete restrict, + add column baseline_checkpoint_id uuid + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + add column baseline_checkpoint_generation bigint + check (baseline_checkpoint_generation > 0), + add column baseline_reorg_generation bigint + check (baseline_reorg_generation >= 0), + add constraint reward_vault_snapshot_shape check ( + ( + snapshot_kind is null + and configuration_epoch is null + and active_configuration_hash is null + and total_creator_fees_received is null + and baseline_reward_vault_projection_id is null + and baseline_checkpoint_id is null + and baseline_checkpoint_generation is null + and baseline_reorg_generation is null + ) + or ( + snapshot_kind = 'initial_seed' + and configuration_epoch is not null + and active_configuration_hash is not null + and total_creator_fees_received is not null + and baseline_reward_vault_projection_id is null + and baseline_checkpoint_id is null + and baseline_checkpoint_generation is null + and baseline_reorg_generation is null + ) + or ( + snapshot_kind = 'exact_current' + and configuration_epoch is not null + and active_configuration_hash is not null + and total_creator_fees_received is not null + and baseline_reward_vault_projection_id is not null + and baseline_checkpoint_id is not null + and baseline_checkpoint_generation is not null + and baseline_reorg_generation is not null + ) + ); + +alter table programmable_private.account_reward_balances + add column payout_address programmable_private.eth_address; + +create or replace view + programmable_private.current_reward_vault_projections_v1 +with (security_invoker = false, security_barrier = true) +as +select vault.* +from programmable_private.projection_entity_current as current_entity +join programmable_private.reward_vault_projections as vault + on vault.reward_vault_projection_id = current_entity.projection_row_id + and vault.projection_run_id = current_entity.projection_run_id +where current_entity.entity_kind = 'reward_vault'; + +create or replace view + programmable_private.current_account_reward_balances_v1 +with (security_invoker = false, security_barrier = true) +as +select balance.* +from programmable_private.projection_entity_current as current_entity +join programmable_private.account_reward_balances as balance + on balance.account_reward_balance_id = current_entity.projection_row_id + and balance.projection_run_id = current_entity.projection_run_id +where current_entity.entity_kind = 'account_reward_balance'; + +-- A current reward snapshot may be published by a later projection run than +-- the immutable launch it belongs to. Keep launch identity exact without +-- requiring both rows to share a run or target block. +create or replace view programmable_private.classic_v3_vault_history_v1 +with (security_invoker = false, security_barrier = true) +as +select + vault.chain_id, + vault.release_id, + vault.model_id, + vault.vault, + vault.pool_id, + coalesce( + vault.active_configuration_hash, + vault.configuration_hash + ) as configuration_hash, + allocation.configuration_epoch, + allocation.allocation_index, + allocation.beneficiary, + allocation.payout_address, + allocation.share_bps, + allocation.effective_from_block, + allocation.effective_to_block, + vault.promoted_block_number, + vault.promoted_block_hash, + vault.verified_at +from programmable_private.current_reward_vault_projections_v1 as vault +join programmable_private.current_launch_projections_v1 as launch + on launch.launch_projection_id = vault.launch_projection_id + and launch.chain_id = vault.chain_id + and launch.release_id = vault.release_id + and launch.model_id = vault.model_id + and launch.epoch_id = vault.epoch_id + and launch.pointer_generation = vault.pointer_generation + and launch.reward_vault = vault.vault + and launch.pool_id = vault.pool_id + and launch.is_complete +join programmable_private.run_headers as run + on run.run_id = vault.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = vault.chain_id + and run.release_id = vault.release_id + and run.model_id = vault.model_id + and run.epoch_id = vault.epoch_id + and run.captured_pointer_generation = vault.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = vault.projection_run_id + and publication.epoch_id = vault.epoch_id + and publication.pointer_generation = vault.pointer_generation + and publication.target_block_number = vault.promoted_block_number + and publication.target_block_hash = vault.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'classic-v3-profile' + and route.chain_id = run.chain_id + and route.release_id = run.release_id + and route.model_id = run.model_id + and route.source_group = run.source_group + and route.epoch_id = run.epoch_id + and route.pointer_generation = run.captured_pointer_generation + and route.checkpoint_id = publication.checkpoint_id + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as launch_canonical + on launch_canonical.logical_event_id = launch.last_source_logical_event_id + and launch_canonical.occurrence_id = launch.last_source_occurrence_id + and launch_canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 + as launch_source + on launch_source.occurrence_id = launch.last_source_occurrence_id + and launch_source.logical_event_id = launch.last_source_logical_event_id + and launch_source.block_hash = launch.last_source_occurrence_block_hash + and launch_source.chain_id = run.chain_id + and launch_source.release_id = run.release_id + and launch_source.model_id = run.model_id + and launch_source.source_group = run.source_group + and launch_source.epoch_id = run.epoch_id + and launch_source.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_current_verified as verified_seed + on verified_seed.allocation_fact_id = vault.current_allocation_fact_id + and verified_seed.vault = vault.vault +join programmable_private.reward_allocation_facts as seed_fact + on seed_fact.allocation_fact_id = verified_seed.allocation_fact_id + and seed_fact.factory_occurrence_id = verified_seed.factory_occurrence_id + and seed_fact.vault = verified_seed.vault + and seed_fact.chain_id = run.chain_id + and seed_fact.release_id = run.release_id + and seed_fact.model_id = run.model_id + and seed_fact.epoch_id = run.epoch_id + and seed_fact.pointer_generation = run.captured_pointer_generation +join programmable_private.chain_event_current_canonical as seed_canonical + on seed_canonical.logical_event_id = seed_fact.factory_logical_event_id + and seed_canonical.occurrence_id = seed_fact.factory_occurrence_id + and seed_canonical.block_hash = seed_fact.factory_occurrence_block_hash +join programmable_private.chain_event_current_canonical as vault_canonical + on vault_canonical.logical_event_id = vault.last_source_logical_event_id + and vault_canonical.occurrence_id = vault.last_source_occurrence_id + and vault_canonical.block_hash = vault.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 + as vault_source + on vault_source.occurrence_id = vault.last_source_occurrence_id + and vault_source.logical_event_id = vault.last_source_logical_event_id + and vault_source.block_hash = vault.last_source_occurrence_block_hash + and vault_source.chain_id = run.chain_id + and vault_source.release_id = run.release_id + and vault_source.model_id = run.model_id + and vault_source.source_group = run.source_group + and vault_source.epoch_id = run.epoch_id + and vault_source.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_projections as allocation + on allocation.reward_vault_projection_id = vault.reward_vault_projection_id + and allocation.projection_run_id = vault.projection_run_id + and allocation.allocation_fact_id = seed_fact.allocation_fact_id + and allocation.chain_id = run.chain_id + and allocation.release_id = run.release_id + and allocation.model_id = run.model_id + and allocation.epoch_id = run.epoch_id + and allocation.pointer_generation = run.captured_pointer_generation + and allocation.promoted_block_number = publication.target_block_number + and allocation.promoted_block_hash = publication.target_block_hash +join programmable_private.chain_event_current_canonical as allocation_canonical + on allocation_canonical.logical_event_id = + allocation.last_source_logical_event_id + and allocation_canonical.occurrence_id = allocation.last_source_occurrence_id + and allocation_canonical.block_hash = + allocation.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 + as allocation_source + on allocation_source.occurrence_id = allocation.last_source_occurrence_id + and allocation_source.logical_event_id = + allocation.last_source_logical_event_id + and allocation_source.block_hash = allocation.last_source_occurrence_block_hash + and allocation_source.chain_id = run.chain_id + and allocation_source.release_id = run.release_id + and allocation_source.model_id = run.model_id + and allocation_source.source_group = run.source_group + and allocation_source.epoch_id = run.epoch_id + and allocation_source.pointer_generation = run.captured_pointer_generation +where vault.release_id = 'classic-v3' + and not exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences as required + join programmable_private.chain_event_materialized_occurrences_v1 + as required_occurrence + on required_occurrence.occurrence_id = required.occurrence_id + left join programmable_private.chain_event_current_canonical + as required_canonical + on required_canonical.logical_event_id = + required_occurrence.logical_event_id + and required_canonical.occurrence_id = required_occurrence.occurrence_id + and required_canonical.block_hash = required_occurrence.block_hash + where required.allocation_fact_id = seed_fact.allocation_fact_id + and ( + required_occurrence.chain_id <> run.chain_id + or required_occurrence.release_id <> run.release_id + or required_occurrence.model_id <> run.model_id + or required_occurrence.source_group <> run.source_group + or required_occurrence.epoch_id <> run.epoch_id + or required_occurrence.pointer_generation <> + run.captured_pointer_generation + or required_canonical.occurrence_id is null + ) + ); + +create or replace view programmable_private.stock_paired_vault_history_v1 +with (security_invoker = false, security_barrier = true) +as +select + vault.chain_id, + vault.release_id, + vault.model_id, + vault.vault, + vault.pool_id, + vault.quote_asset, + coalesce( + vault.active_configuration_hash, + vault.configuration_hash + ) as configuration_hash, + allocation.configuration_epoch, + allocation.allocation_index, + allocation.beneficiary, + allocation.payout_address, + allocation.share_bps, + allocation.effective_from_block, + allocation.effective_to_block, + vault.promoted_block_number, + vault.promoted_block_hash, + vault.verified_at +from programmable_private.current_reward_vault_projections_v1 as vault +join programmable_private.current_launch_projections_v1 as launch + on launch.launch_projection_id = vault.launch_projection_id + and launch.chain_id = vault.chain_id + and launch.release_id = vault.release_id + and launch.model_id = vault.model_id + and launch.epoch_id = vault.epoch_id + and launch.pointer_generation = vault.pointer_generation + and launch.reward_vault = vault.vault + and launch.pool_id = vault.pool_id + and launch.is_complete +join programmable_private.run_headers as run + on run.run_id = vault.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = vault.chain_id + and run.release_id = vault.release_id + and run.model_id = vault.model_id + and run.epoch_id = vault.epoch_id + and run.captured_pointer_generation = vault.pointer_generation +join programmable_private.projection_publications as publication + on publication.run_id = vault.projection_run_id + and publication.epoch_id = vault.epoch_id + and publication.pointer_generation = vault.pointer_generation + and publication.target_block_number = vault.promoted_block_number + and publication.target_block_hash = vault.promoted_block_hash +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.route_eligibility_current as route + on route.route_key = 'creator-profile' + and route.chain_id = run.chain_id + and route.release_id = run.release_id + and route.model_id = run.model_id + and route.source_group = run.source_group + and route.epoch_id = run.epoch_id + and route.pointer_generation = run.captured_pointer_generation + and route.checkpoint_id = publication.checkpoint_id + and route.status = 'eligible' + and route.route_mode = 'indexed' +join programmable_private.chain_event_current_canonical as launch_canonical + on launch_canonical.logical_event_id = launch.last_source_logical_event_id + and launch_canonical.occurrence_id = launch.last_source_occurrence_id + and launch_canonical.block_hash = launch.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 + as launch_source + on launch_source.occurrence_id = launch.last_source_occurrence_id + and launch_source.logical_event_id = launch.last_source_logical_event_id + and launch_source.block_hash = launch.last_source_occurrence_block_hash + and launch_source.chain_id = run.chain_id + and launch_source.release_id = run.release_id + and launch_source.model_id = run.model_id + and launch_source.source_group = run.source_group + and launch_source.epoch_id = run.epoch_id + and launch_source.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_current_verified as verified_seed + on verified_seed.allocation_fact_id = vault.current_allocation_fact_id + and verified_seed.vault = vault.vault +join programmable_private.reward_allocation_facts as seed_fact + on seed_fact.allocation_fact_id = verified_seed.allocation_fact_id + and seed_fact.factory_occurrence_id = verified_seed.factory_occurrence_id + and seed_fact.vault = verified_seed.vault + and seed_fact.chain_id = run.chain_id + and seed_fact.release_id = run.release_id + and seed_fact.model_id = run.model_id + and seed_fact.epoch_id = run.epoch_id + and seed_fact.pointer_generation = run.captured_pointer_generation +join programmable_private.chain_event_current_canonical as seed_canonical + on seed_canonical.logical_event_id = seed_fact.factory_logical_event_id + and seed_canonical.occurrence_id = seed_fact.factory_occurrence_id + and seed_canonical.block_hash = seed_fact.factory_occurrence_block_hash +join programmable_private.chain_event_current_canonical as vault_canonical + on vault_canonical.logical_event_id = vault.last_source_logical_event_id + and vault_canonical.occurrence_id = vault.last_source_occurrence_id + and vault_canonical.block_hash = vault.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 + as vault_source + on vault_source.occurrence_id = vault.last_source_occurrence_id + and vault_source.logical_event_id = vault.last_source_logical_event_id + and vault_source.block_hash = vault.last_source_occurrence_block_hash + and vault_source.chain_id = run.chain_id + and vault_source.release_id = run.release_id + and vault_source.model_id = run.model_id + and vault_source.source_group = run.source_group + and vault_source.epoch_id = run.epoch_id + and vault_source.pointer_generation = run.captured_pointer_generation +join programmable_private.reward_allocation_projections as allocation + on allocation.reward_vault_projection_id = vault.reward_vault_projection_id + and allocation.projection_run_id = vault.projection_run_id + and allocation.allocation_fact_id = seed_fact.allocation_fact_id + and allocation.chain_id = run.chain_id + and allocation.release_id = run.release_id + and allocation.model_id = run.model_id + and allocation.epoch_id = run.epoch_id + and allocation.pointer_generation = run.captured_pointer_generation + and allocation.promoted_block_number = publication.target_block_number + and allocation.promoted_block_hash = publication.target_block_hash +join programmable_private.chain_event_current_canonical as allocation_canonical + on allocation_canonical.logical_event_id = + allocation.last_source_logical_event_id + and allocation_canonical.occurrence_id = allocation.last_source_occurrence_id + and allocation_canonical.block_hash = + allocation.last_source_occurrence_block_hash +join programmable_private.chain_event_materialized_occurrences_v1 + as allocation_source + on allocation_source.occurrence_id = allocation.last_source_occurrence_id + and allocation_source.logical_event_id = + allocation.last_source_logical_event_id + and allocation_source.block_hash = allocation.last_source_occurrence_block_hash + and allocation_source.chain_id = run.chain_id + and allocation_source.release_id = run.release_id + and allocation_source.model_id = run.model_id + and allocation_source.source_group = run.source_group + and allocation_source.epoch_id = run.epoch_id + and allocation_source.pointer_generation = run.captured_pointer_generation +where vault.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and not exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences as required + join programmable_private.chain_event_materialized_occurrences_v1 + as required_occurrence + on required_occurrence.occurrence_id = required.occurrence_id + left join programmable_private.chain_event_current_canonical + as required_canonical + on required_canonical.logical_event_id = + required_occurrence.logical_event_id + and required_canonical.occurrence_id = required_occurrence.occurrence_id + and required_canonical.block_hash = required_occurrence.block_hash + where required.allocation_fact_id = seed_fact.allocation_fact_id + and ( + required_occurrence.chain_id <> run.chain_id + or required_occurrence.release_id <> run.release_id + or required_occurrence.model_id <> run.model_id + or required_occurrence.source_group <> run.source_group + or required_occurrence.epoch_id <> run.epoch_id + or required_occurrence.pointer_generation <> + run.captured_pointer_generation + or required_canonical.occurrence_id is null + ) + ); + +-- Allocation and balance rows in an exact-current snapshot are a complete +-- restatement of the vault state, not separate event deltas. The parent vault +-- event remains release-allowlisted; child rows may reuse that exact source +-- only after the authorized parent snapshot has been staged. +create or replace function programmable_private.enforce_projection_event_rule() +returns trigger +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + source_occurrence_id uuid; + projection_kind text; +begin + source_occurrence_id := case tg_table_name + when 'launch_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'pool_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'pool_fee_configurations' then + (pg_catalog.to_jsonb(new)->>'disclosure_source_occurrence_id')::uuid + when 'fee_accrual_facts' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'pool_fee_totals' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'reward_vault_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'reward_allocation_projections' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'claim_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'payout_change_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'account_reward_balances' then + (pg_catalog.to_jsonb(new)->>'last_source_occurrence_id')::uuid + when 'initial_buy_custody_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + when 'initial_buy_vesting_projections' then + (pg_catalog.to_jsonb(new)->>'source_occurrence_id')::uuid + else null + end; + if tg_table_name in ( + 'reward_allocation_projections', 'account_reward_balances' + ) + and exists ( + select 1 + from programmable_private.reward_vault_projections as parent + where parent.projection_run_id = new.projection_run_id + and parent.snapshot_kind = 'exact_current' + and parent.last_source_occurrence_id = source_occurrence_id + and ( + tg_table_name <> 'reward_allocation_projections' + or parent.reward_vault_projection_id = + (pg_catalog.to_jsonb(new)->>'reward_vault_projection_id')::uuid + ) + and ( + tg_table_name <> 'account_reward_balances' + or parent.vault = + (pg_catalog.to_jsonb(new)->>'vault')::bytea + ) + ) + then + return new; + end if; + projection_kind := case tg_table_name + when 'launch_projections' then 'launch' + when 'pool_projections' then 'pool' + when 'pool_fee_configurations' then 'pool_fee_configuration' + when 'fee_accrual_facts' then 'fee_accrual' + when 'pool_fee_totals' then 'pool_fee_total' + when 'reward_vault_projections' then 'reward_vault' + when 'reward_allocation_projections' then 'reward_allocation' + when 'claim_projections' then 'claim' + when 'payout_change_projections' then 'payout_change' + when 'account_reward_balances' then 'account_reward_balance' + when 'initial_buy_custody_projections' then 'initial_buy_custody' + when 'initial_buy_vesting_projections' then 'initial_buy_vesting' + else null + end; + perform programmable_private.assert_projection_event_allowed( + new.projection_run_id, source_occurrence_id, projection_kind + ); + return new; +end +$function$; + +create index account_reward_balance_vault_reader_idx + on programmable_private.account_reward_balances ( + chain_id, release_id, model_id, vault, epoch_id, + pointer_generation, account, projection_run_id + ); + +create function programmable_private.stage_current_reward_snapshot_v1( + p_run_id uuid, + p_vault bytea, + p_pool_id bytea, + p_initial_allocation_fact_id uuid, + p_configuration_epoch bigint, + p_active_configuration_hash bytea, + p_total_creator_fees_received numeric, + p_allocation_indices integer[], + p_beneficiaries bytea[], + p_payout_addresses bytea[], + p_shares_bps numeric[], + p_balance_accounts bytea[], + p_balance_payout_addresses bytea[], + p_claimable_accrued numeric[], + p_claimed_totals numeric[], + p_snapshot_source_occurrence_id uuid, + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + scope record; + source record; + baseline record; + launch record; + seed programmable_private.reward_allocation_facts%rowtype; + activation programmable_private.reward_configuration_activation_facts%rowtype; + existing_vault programmable_private.reward_vault_projections%rowtype; + prior_balance record; + allocation_count integer; + balance_count integer; + baseline_count bigint; + launch_count bigint; + prior_configuration_epoch bigint; + prior_active_configuration_hash bytea; + prior_total numeric := 0; + total_share_bps numeric := 0; + total_balance_value numeric := 0; + normalized_total numeric; + normalized_claimable numeric; + normalized_claimed numeric; + baseline_kind text; + baseline_launch_projection_id uuid; + baseline_quote_asset bytea; + baseline_reward_vault_projection_id uuid; + baseline_checkpoint_id uuid; + baseline_checkpoint_generation bigint; + baseline_reorg_generation bigint; + prior_indices integer[]; + prior_beneficiaries bytea[]; + prior_payout_addresses bytea[]; + prior_shares numeric[]; + changed_positions integer := 0; + changed_position integer; + allocation_id uuid; + balance_id uuid; + returned_id uuid; + idx integer; + prior_idx integer; + zero_address bytea := pg_catalog.decode( + '0000000000000000000000000000000000000000', 'hex' + ); +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into scope + from programmable_private.projection_stage_context( + p_run_id, p_snapshot_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection' + for share; + if not found then + raise exception using + errcode = '23503', message = 'invalid reward snapshot run'; + end if; + select + occurrence.source_address, + occurrence.block_number, + occurrence.block_global_log_index, + occurrence.logical_event_id, + occurrence.block_hash, + materialization.event_type, + materialization.decoded_payload + into source + from programmable_private.chain_event_occurrences as occurrence + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = occurrence.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + where occurrence.occurrence_id = p_snapshot_source_occurrence_id; + if source.logical_event_id is null + or header.release_id not in ( + 'classic-v3', + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + or pg_catalog.octet_length(p_vault) <> 20 + or p_vault = zero_address + or pg_catalog.octet_length(p_pool_id) <> 32 + or p_configuration_epoch is null + or p_configuration_epoch <= 0 + or pg_catalog.octet_length(p_active_configuration_hash) <> 32 + then + raise exception using + errcode = '22023', message = 'invalid current reward snapshot'; + end if; + normalized_total := programmable_private.validate_uint256( + p_total_creator_fees_received + ); + allocation_count := coalesce( + pg_catalog.cardinality(p_allocation_indices), 0 + ); + balance_count := coalesce(pg_catalog.cardinality(p_balance_accounts), 0); + if allocation_count < 1 + or allocation_count > ( + case + when header.release_id = 'classic-v3' then 5 + else 8 + end + ) + or pg_catalog.cardinality(p_beneficiaries) <> allocation_count + or pg_catalog.cardinality(p_payout_addresses) <> allocation_count + or pg_catalog.cardinality(p_shares_bps) <> allocation_count + or balance_count < 1 + or pg_catalog.cardinality(p_balance_payout_addresses) <> balance_count + or pg_catalog.cardinality(p_claimable_accrued) <> balance_count + or pg_catalog.cardinality(p_claimed_totals) <> balance_count + then + raise exception using + errcode = '22023', message = 'reward snapshot arrays are incomplete'; + end if; + + for idx in 1..allocation_count loop + if p_allocation_indices[idx] <> idx - 1 + or pg_catalog.octet_length(p_beneficiaries[idx]) <> 20 + or p_beneficiaries[idx] = zero_address + or pg_catalog.octet_length(p_payout_addresses[idx]) <> 20 + or p_payout_addresses[idx] = zero_address + or p_shares_bps[idx] is null + or p_shares_bps[idx] <> pg_catalog.trunc(p_shares_bps[idx]) + or p_shares_bps[idx] <= 0 + or p_shares_bps[idx] > 10000 + or ( + header.release_id = 'classic-v3' + and p_payout_addresses[idx] <> p_beneficiaries[idx] + ) + then + raise exception using + errcode = '22023', message = 'invalid active reward allocation'; + end if; + if header.release_id <> 'classic-v3' and idx > 1 then + for prior_idx in 1..idx - 1 loop + if p_beneficiaries[prior_idx] = p_beneficiaries[idx] then + raise exception using + errcode = '22023', + message = 'non-Classic beneficiaries must remain unique'; + end if; + end loop; + end if; + total_share_bps := total_share_bps + p_shares_bps[idx]; + end loop; + if total_share_bps <> 10000 then + raise exception using + errcode = '22023', message = 'reward snapshot shares must total 10000'; + end if; + + for idx in 1..balance_count loop + if pg_catalog.octet_length(p_balance_accounts[idx]) <> 20 + or p_balance_accounts[idx] = zero_address + or pg_catalog.octet_length(p_balance_payout_addresses[idx]) <> 20 + or p_balance_payout_addresses[idx] = zero_address + or ( + header.release_id = 'classic-v3' + and p_balance_payout_addresses[idx] <> p_balance_accounts[idx] + ) + then + raise exception using + errcode = '22023', message = 'invalid reward balance account'; + end if; + if idx > 1 + and p_balance_accounts[idx - 1] >= p_balance_accounts[idx] + then + raise exception using + errcode = '22023', + message = 'reward balance accounts must be unique and ordered'; + end if; + normalized_claimable := programmable_private.validate_uint256( + p_claimable_accrued[idx] + ); + normalized_claimed := programmable_private.validate_uint256( + p_claimed_totals[idx] + ); + total_balance_value := total_balance_value + + normalized_claimable + normalized_claimed; + end loop; + if total_balance_value <> normalized_total then + raise exception using + errcode = '23514', + message = 'reward balances do not reconcile to total received'; + end if; + for idx in 1..allocation_count loop + prior_idx := pg_catalog.array_position( + p_balance_accounts, p_beneficiaries[idx] + ); + if prior_idx is null then + raise exception using + errcode = '23514', + message = 'every active beneficiary needs a balance row'; + end if; + if header.release_id <> 'classic-v3' + and p_balance_payout_addresses[prior_idx] <> + p_payout_addresses[idx] + then + raise exception using + errcode = '23514', + message = 'active beneficiary payout does not match its balance'; + end if; + end loop; + + select pg_catalog.count(*) into baseline_count + from programmable_private.current_reward_vault_projections_v1 as vault + join programmable_private.run_headers as baseline_run + on baseline_run.run_id = vault.projection_run_id + and baseline_run.run_kind = 'projection' + and baseline_run.chain_id = vault.chain_id + and baseline_run.release_id = vault.release_id + and baseline_run.model_id = vault.model_id + and baseline_run.source_group = header.source_group + and baseline_run.epoch_id = vault.epoch_id + and baseline_run.captured_pointer_generation = vault.pointer_generation + join programmable_private.projection_entity_current as entity + on entity.entity_kind = 'reward_vault' + and entity.projection_row_id = vault.reward_vault_projection_id + and entity.projection_run_id = vault.projection_run_id + and entity.chain_id = vault.chain_id + and entity.release_id = vault.release_id + and entity.model_id = vault.model_id + and entity.source_group = header.source_group + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = entity.checkpoint_id + and checkpoint.chain_id = vault.chain_id + and checkpoint.release_id = vault.release_id + and checkpoint.model_id = vault.model_id + and checkpoint.source_group = header.source_group + and checkpoint.epoch_id = header.epoch_id + and checkpoint.pointer_generation = header.captured_pointer_generation + join programmable_private.projector_checkpoint_current as current_checkpoint + on current_checkpoint.chain_id = checkpoint.chain_id + and current_checkpoint.release_id = checkpoint.release_id + and current_checkpoint.model_id = checkpoint.model_id + and current_checkpoint.source_group = checkpoint.source_group + and current_checkpoint.projector_version = checkpoint.projector_version + join programmable_private.projector_checkpoints as current_cursor + on current_cursor.checkpoint_id = current_checkpoint.checkpoint_id + and current_cursor.chain_id = checkpoint.chain_id + and current_cursor.release_id = checkpoint.release_id + and current_cursor.model_id = checkpoint.model_id + and current_cursor.source_group = checkpoint.source_group + and current_cursor.projector_version = checkpoint.projector_version + and current_cursor.epoch_id = header.epoch_id + and current_cursor.pointer_generation = + header.captured_pointer_generation + and current_cursor.checkpoint_generation = + current_checkpoint.checkpoint_generation + and current_cursor.reorg_generation = current_checkpoint.reorg_generation + where vault.chain_id = header.chain_id + and vault.release_id = header.release_id + and vault.model_id = header.model_id + and vault.epoch_id = header.epoch_id + and vault.pointer_generation = header.captured_pointer_generation + and vault.vault = p_vault + and vault.pool_id = p_pool_id + and ( + current_cursor.block_number, + current_cursor.cursor_block_global_log_index + ) >= ( + checkpoint.block_number, + checkpoint.cursor_block_global_log_index + ) + and programmable_private.has_current_verified_reward_seed( + vault.projection_run_id, vault.vault + ); + if baseline_count > 1 then + raise exception using + errcode = '23514', message = 'current reward baseline is ambiguous'; + end if; + + if baseline_count = 1 then + baseline_kind := 'exact_current'; + select + vault.reward_vault_projection_id, + vault.launch_projection_id, + vault.quote_asset, + vault.current_allocation_fact_id, + vault.configuration_epoch, + vault.active_configuration_hash, + vault.total_creator_fees_received, + current_cursor.checkpoint_id, + current_cursor.checkpoint_generation, + current_cursor.reorg_generation, + current_cursor.block_number, + current_cursor.cursor_block_global_log_index, + seed_fact.active_configuration_hash as seed_active_hash, + seed_fact.configuration_hash as seed_configuration_hash + into baseline + from programmable_private.current_reward_vault_projections_v1 as vault + join programmable_private.run_headers as baseline_run + on baseline_run.run_id = vault.projection_run_id + and baseline_run.run_kind = 'projection' + and baseline_run.source_group = header.source_group + join programmable_private.projection_entity_current as entity + on entity.entity_kind = 'reward_vault' + and entity.projection_row_id = vault.reward_vault_projection_id + and entity.projection_run_id = vault.projection_run_id + and entity.source_group = header.source_group + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = entity.checkpoint_id + and checkpoint.epoch_id = header.epoch_id + and checkpoint.pointer_generation = header.captured_pointer_generation + join programmable_private.projector_checkpoint_current + as current_checkpoint + on current_checkpoint.chain_id = checkpoint.chain_id + and current_checkpoint.release_id = checkpoint.release_id + and current_checkpoint.model_id = checkpoint.model_id + and current_checkpoint.source_group = checkpoint.source_group + and current_checkpoint.projector_version = checkpoint.projector_version + join programmable_private.projector_checkpoints as current_cursor + on current_cursor.checkpoint_id = current_checkpoint.checkpoint_id + and current_cursor.chain_id = checkpoint.chain_id + and current_cursor.release_id = checkpoint.release_id + and current_cursor.model_id = checkpoint.model_id + and current_cursor.source_group = checkpoint.source_group + and current_cursor.projector_version = checkpoint.projector_version + and current_cursor.epoch_id = header.epoch_id + and current_cursor.pointer_generation = + header.captured_pointer_generation + and current_cursor.checkpoint_generation = + current_checkpoint.checkpoint_generation + and current_cursor.reorg_generation = + current_checkpoint.reorg_generation + join programmable_private.reward_allocation_facts as seed_fact + on seed_fact.allocation_fact_id = vault.current_allocation_fact_id + and seed_fact.epoch_id = vault.epoch_id + and seed_fact.pointer_generation = vault.pointer_generation + and seed_fact.vault = vault.vault + where vault.chain_id = header.chain_id + and vault.release_id = header.release_id + and vault.model_id = header.model_id + and vault.epoch_id = header.epoch_id + and vault.pointer_generation = header.captured_pointer_generation + and vault.vault = p_vault + and vault.pool_id = p_pool_id + and ( + current_cursor.block_number, + current_cursor.cursor_block_global_log_index + ) >= ( + checkpoint.block_number, + checkpoint.cursor_block_global_log_index + ); + if p_initial_allocation_fact_id <> + baseline.current_allocation_fact_id + or ( + source.block_number, + source.block_global_log_index + ) <= ( + baseline.block_number, + baseline.cursor_block_global_log_index + ) + or source.source_address <> p_vault + then + raise exception using + errcode = '23514', + message = 'reward snapshot does not extend the exact current baseline'; + end if; + baseline_launch_projection_id := baseline.launch_projection_id; + baseline_quote_asset := baseline.quote_asset; + baseline_reward_vault_projection_id := + baseline.reward_vault_projection_id; + baseline_checkpoint_id := baseline.checkpoint_id; + baseline_checkpoint_generation := baseline.checkpoint_generation; + baseline_reorg_generation := baseline.reorg_generation; + select * into seed + from programmable_private.reward_allocation_facts as fact + where fact.allocation_fact_id = p_initial_allocation_fact_id; + if seed.allocation_fact_id is null then + raise exception using + errcode = '23514', message = 'verified initial reward seed is missing'; + end if; + + select + coalesce( + baseline.configuration_epoch, + pg_catalog.max(allocation.configuration_epoch), + 1 + ), + coalesce( + baseline.active_configuration_hash, + baseline.seed_active_hash, + baseline.seed_configuration_hash + ), + pg_catalog.array_agg( + allocation.allocation_index order by allocation.allocation_index + ), + pg_catalog.array_agg( + case when header.release_id = 'classic-v3' + then allocation.payout_address::bytea + else allocation.beneficiary::bytea end + order by allocation.allocation_index + ), + pg_catalog.array_agg( + allocation.payout_address::bytea + order by allocation.allocation_index + ), + pg_catalog.array_agg( + allocation.share_bps::numeric + order by allocation.allocation_index + ) + into prior_configuration_epoch, prior_active_configuration_hash, + prior_indices, prior_beneficiaries, prior_payout_addresses, + prior_shares + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + baseline.reward_vault_projection_id + and allocation.effective_to_block is null; + if prior_indices is null then + raise exception using + errcode = '23514', message = 'current reward allocation is missing'; + end if; + prior_total := baseline.total_creator_fees_received; + if prior_total is null then + select coalesce( + pg_catalog.sum(balance.claimable_accrued + balance.claimed_total), 0 + ) into prior_total + from programmable_private.current_account_reward_balances_v1 as balance + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = p_vault; + end if; + if normalized_total < prior_total then + raise exception using + errcode = '23514', + message = 'reward snapshot total cannot move backward'; + end if; + for prior_balance in + select * + from programmable_private.current_account_reward_balances_v1 as balance + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = p_vault + loop + idx := pg_catalog.array_position( + p_balance_accounts, prior_balance.account::bytea + ); + if idx is null then + raise exception using + errcode = '23514', + message = 'historical reward balance cannot be omitted'; + end if; + if idx is not null and ( + p_claimed_totals[idx] < prior_balance.claimed_total + or p_claimable_accrued[idx] + p_claimed_totals[idx] + < prior_balance.claimable_accrued + prior_balance.claimed_total + ) then + raise exception using + errcode = '23514', + message = 'historical reward totals cannot move backward'; + end if; + end loop; + + if p_configuration_epoch = prior_configuration_epoch + and p_active_configuration_hash = + prior_active_configuration_hash + and p_allocation_indices = prior_indices + and p_beneficiaries = prior_beneficiaries + and p_payout_addresses = prior_payout_addresses + and p_shares_bps = prior_shares + then + null; + elsif header.release_id = 'classic-v3' + and source.event_type = 'PayoutWalletChanged' + then + if p_configuration_epoch <> prior_configuration_epoch + 1 + or p_allocation_indices <> prior_indices + or p_shares_bps <> prior_shares + or pg_catalog.cardinality(p_beneficiaries) + <> pg_catalog.cardinality(prior_beneficiaries) + then + raise exception using + errcode = '23514', message = 'invalid Classic payout transition'; + end if; + for idx in 1..allocation_count loop + if p_beneficiaries[idx] <> prior_beneficiaries[idx] + or p_payout_addresses[idx] <> prior_payout_addresses[idx] + then + changed_positions := changed_positions + 1; + changed_position := idx; + end if; + end loop; + if changed_positions <> 1 + or programmable_private.json_hex_bytes_v1( + source.decoded_payload, 'poolId', 32 + ) is distinct from p_pool_id + or (source.decoded_payload ->> 'allocationIndex')::numeric + is distinct from (changed_position - 1)::numeric + or programmable_private.json_hex_bytes_v1( + source.decoded_payload, 'previousPayoutWallet', 20 + ) is distinct from prior_beneficiaries[changed_position] + or programmable_private.json_hex_bytes_v1( + source.decoded_payload, 'newPayoutWallet', 20 + ) is distinct from p_beneficiaries[changed_position] + or (source.decoded_payload ->> 'shareBps')::numeric + is distinct from p_shares_bps[changed_position] + or (source.decoded_payload ->> 'configurationEpoch')::numeric + is distinct from p_configuration_epoch::numeric + or programmable_private.json_hex_bytes_v1( + source.decoded_payload, 'activeConfigurationHash', 32 + ) is distinct from p_active_configuration_hash + or ( + source.decoded_payload ->> 'effectiveTotalCreatorFeesReceived' + )::numeric is distinct from normalized_total + then + raise exception using + errcode = '23514', + message = 'Classic payout transition lacks exact event evidence'; + end if; + elsif header.release_id = 'classic-v3' + and source.event_type = 'CtoRewardConfigurationActivated' + then + select * into activation + from programmable_private.reward_configuration_activation_facts as fact + where fact.source_occurrence_id = p_snapshot_source_occurrence_id + and fact.verification_run_id = p_run_id + and fact.vault = p_vault + and fact.pool_id = p_pool_id; + if activation.reward_configuration_activation_fact_id is null + or activation.configuration_epoch <> p_configuration_epoch + or activation.previous_configuration_hash <> + prior_active_configuration_hash + or activation.new_configuration_hash <> + p_active_configuration_hash + or activation.ordered_beneficiaries <> p_beneficiaries + or activation.ordered_shares_bps::numeric[] <> p_shares_bps + or activation.effective_total_creator_fees_received <> + normalized_total + then + raise exception using + errcode = '23514', + message = 'CTO reward transition lacks exact activation evidence'; + end if; + elsif header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and source.event_type = 'PayoutAddressUpdated' + then + if p_configuration_epoch <> prior_configuration_epoch + or p_active_configuration_hash <> + prior_active_configuration_hash + or p_allocation_indices <> prior_indices + or p_beneficiaries <> prior_beneficiaries + or p_shares_bps <> prior_shares + then + raise exception using + errcode = '23514', message = 'invalid payout-address transition'; + end if; + for idx in 1..allocation_count loop + if p_payout_addresses[idx] <> prior_payout_addresses[idx] then + changed_positions := changed_positions + 1; + changed_position := idx; + end if; + end loop; + if changed_positions <> 1 + or programmable_private.json_hex_bytes_v1( + source.decoded_payload, 'beneficiary', 20 + ) is distinct from p_beneficiaries[changed_position] + or programmable_private.json_hex_bytes_v1( + source.decoded_payload, 'previousPayoutAddress', 20 + ) is distinct from prior_payout_addresses[changed_position] + or programmable_private.json_hex_bytes_v1( + source.decoded_payload, 'newPayoutAddress', 20 + ) is distinct from p_payout_addresses[changed_position] + then + raise exception using + errcode = '23514', + message = 'payout transition lacks exact event evidence'; + end if; + else + raise exception using + errcode = '23514', + message = 'reward configuration changed without canonical evidence'; + end if; + else + baseline_kind := 'initial_seed'; + select pg_catalog.count(*) into launch_count + from programmable_private.launch_projections as candidate + where candidate.projection_run_id = p_run_id + and candidate.chain_id = header.chain_id + and candidate.release_id = header.release_id + and candidate.model_id = header.model_id + and candidate.epoch_id = header.epoch_id + and candidate.pointer_generation = header.captured_pointer_generation + and candidate.reward_vault = p_vault + and candidate.pool_id = p_pool_id + and candidate.promoted_block_number = scope.promoted_block_number + and candidate.promoted_block_hash = scope.promoted_block_hash + and candidate.is_complete; + if launch_count <> 1 then + raise exception using + errcode = '23514', + message = 'initial reward snapshot requires one staged launch'; + end if; + select * into launch + from programmable_private.launch_projections as candidate + where candidate.projection_run_id = p_run_id + and candidate.reward_vault = p_vault + and candidate.pool_id = p_pool_id + and candidate.promoted_block_number = scope.promoted_block_number + and candidate.promoted_block_hash = scope.promoted_block_hash + and candidate.is_complete; + select * into seed + from programmable_private.reward_allocation_facts as fact + where fact.allocation_fact_id = p_initial_allocation_fact_id + and fact.chain_id = header.chain_id + and fact.release_id = header.release_id + and fact.model_id = header.model_id + and fact.epoch_id = header.epoch_id + and fact.pointer_generation = header.captured_pointer_generation + and fact.vault = p_vault; + if seed.allocation_fact_id is null + or not exists ( + select 1 + from programmable_private.reward_allocation_evidence as evidence + where evidence.allocation_fact_id = seed.allocation_fact_id + and evidence.recomputed_allocation_hash = seed.allocation_hash + and evidence.recomputed_configuration_hash = + seed.configuration_hash + and evidence.recomputed_active_configuration_hash + is not distinct from seed.active_configuration_hash + and evidence.is_recomputation_attested + ) + or p_configuration_epoch <> 1 + or p_active_configuration_hash <> coalesce( + seed.active_configuration_hash, seed.configuration_hash + ) + or p_beneficiaries <> seed.ordered_beneficiaries + or p_payout_addresses <> seed.ordered_beneficiaries + or p_shares_bps <> seed.ordered_shares_bps::numeric[] + or p_allocation_indices <> + array( + select value - 1 + from pg_catalog.generate_series( + 1, pg_catalog.cardinality(seed.ordered_beneficiaries) + ) as value + ) + or not ( + source.source_address = p_vault + or p_snapshot_source_occurrence_id = seed.factory_occurrence_id + ) + then + raise exception using + errcode = '23514', + message = 'initial reward snapshot does not match verified seed'; + end if; + baseline_launch_projection_id := launch.launch_projection_id; + select case + when pool.currency0 = launch.token then nullif(pool.currency1, zero_address) + else nullif(pool.currency0, zero_address) + end into baseline_quote_asset + from programmable_private.pool_projections as pool + where pool.projection_run_id = p_run_id + and pool.launch_projection_id = launch.launch_projection_id + and pool.pool_id = p_pool_id; + end if; + + for idx in 1..balance_count loop + if pg_catalog.array_position( + p_beneficiaries, p_balance_accounts[idx] + ) is null and not exists ( + select 1 + from programmable_private.current_account_reward_balances_v1 as balance + where baseline_kind = 'exact_current' + and balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = p_vault + and balance.account = p_balance_accounts[idx] + ) then + raise exception using + errcode = '23514', + message = 'reward snapshot introduced an unknown historical account'; + end if; + end loop; + + select * into existing_vault + from programmable_private.reward_vault_projections as candidate + where candidate.chain_id = header.chain_id + and candidate.vault = p_vault + and candidate.projection_run_id = p_run_id; + if found then + if existing_vault.pool_id <> p_pool_id + or existing_vault.current_allocation_fact_id <> + p_initial_allocation_fact_id + or existing_vault.snapshot_kind <> baseline_kind + or existing_vault.configuration_epoch <> p_configuration_epoch + or existing_vault.active_configuration_hash <> + p_active_configuration_hash + or existing_vault.total_creator_fees_received <> normalized_total + or existing_vault.last_source_occurrence_id <> + p_snapshot_source_occurrence_id + or existing_vault.promoted_block_number <> + scope.promoted_block_number + or existing_vault.promoted_block_hash <> scope.promoted_block_hash + or ( + select pg_catalog.array_agg( + allocation.allocation_index order by allocation.allocation_index + ) + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + existing_vault.reward_vault_projection_id + ) is distinct from p_allocation_indices + or ( + select pg_catalog.array_agg( + allocation.beneficiary::bytea order by allocation.allocation_index + ) + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + existing_vault.reward_vault_projection_id + ) is distinct from p_beneficiaries + or ( + select pg_catalog.array_agg( + allocation.payout_address::bytea + order by allocation.allocation_index + ) + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + existing_vault.reward_vault_projection_id + ) is distinct from p_payout_addresses + or ( + select pg_catalog.array_agg( + allocation.share_bps::numeric order by allocation.allocation_index + ) + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + existing_vault.reward_vault_projection_id + ) is distinct from p_shares_bps + or ( + select pg_catalog.array_agg( + balance.account::bytea order by balance.account + ) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + ) is distinct from p_balance_accounts + or ( + select pg_catalog.array_agg( + balance.payout_address::bytea order by balance.account + ) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + ) is distinct from p_balance_payout_addresses + or ( + select pg_catalog.array_agg( + balance.claimable_accrued::numeric order by balance.account + ) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + ) is distinct from p_claimable_accrued + or ( + select pg_catalog.array_agg( + balance.claimed_total::numeric order by balance.account + ) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + ) is distinct from p_claimed_totals + then + raise exception using + errcode = '23505', message = 'reward snapshot replay changed content'; + end if; + return existing_vault.reward_vault_projection_id; + end if; + + returned_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_vault_projections ( + reward_vault_projection_id, launch_projection_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, vault, pool_id, quote_asset, + configuration_hash, current_allocation_fact_id, + last_source_logical_event_id, last_source_occurrence_id, + last_source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at, + snapshot_kind, configuration_epoch, active_configuration_hash, + total_creator_fees_received, baseline_reward_vault_projection_id, + baseline_checkpoint_id, baseline_checkpoint_generation, + baseline_reorg_generation + ) values ( + returned_id, baseline_launch_projection_id, header.chain_id, + header.release_id, header.model_id, header.epoch_id, + header.captured_pointer_generation, + p_vault::programmable_private.eth_address, + p_pool_id::programmable_private.bytes32_value, + case when baseline_quote_asset is null then null + else baseline_quote_asset::programmable_private.eth_address end, + seed.configuration_hash, + p_initial_allocation_fact_id, + scope.source_logical_event_id, p_snapshot_source_occurrence_id, + scope.source_occurrence_block_hash, p_run_id, + scope.promoted_block_number, scope.promoted_block_hash, p_verified_at, + baseline_kind, p_configuration_epoch, + p_active_configuration_hash::programmable_private.bytes32_value, + normalized_total::programmable_private.uint256_value, + baseline_reward_vault_projection_id, baseline_checkpoint_id, + baseline_checkpoint_generation, baseline_reorg_generation + ); + + for idx in 1..allocation_count loop + allocation_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_allocation_projections ( + reward_allocation_projection_id, reward_vault_projection_id, + allocation_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, configuration_epoch, allocation_index, + beneficiary, payout_address, share_bps, effective_from_block, + effective_to_block, last_source_logical_event_id, + last_source_occurrence_id, last_source_occurrence_block_hash, + projection_run_id, promoted_block_number, promoted_block_hash, + verified_at + ) values ( + allocation_id, returned_id, p_initial_allocation_fact_id, + header.chain_id, header.release_id, header.model_id, header.epoch_id, + header.captured_pointer_generation, p_configuration_epoch, + p_allocation_indices[idx], + p_beneficiaries[idx]::programmable_private.eth_address, + p_payout_addresses[idx]::programmable_private.eth_address, + p_shares_bps[idx]::programmable_private.basis_points, + source.block_number::programmable_private.block_number_value, + null, scope.source_logical_event_id, + p_snapshot_source_occurrence_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ); + end loop; + + for idx in 1..balance_count loop + balance_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.account_reward_balances ( + account_reward_balance_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, account, vault, payout_address, + claimable_accrued, claimed_total, last_source_logical_event_id, + last_source_occurrence_id, last_source_occurrence_block_hash, + projection_run_id, promoted_block_number, promoted_block_hash, + verified_at + ) values ( + balance_id, header.chain_id, header.release_id, header.model_id, + header.epoch_id, header.captured_pointer_generation, + p_balance_accounts[idx]::programmable_private.eth_address, + p_vault::programmable_private.eth_address, + p_balance_payout_addresses[idx]::programmable_private.eth_address, + p_claimable_accrued[idx]::programmable_private.uint256_value, + p_claimed_totals[idx]::programmable_private.uint256_value, + scope.source_logical_event_id, p_snapshot_source_occurrence_id, + scope.source_occurrence_block_hash, p_run_id, + scope.promoted_block_number, scope.promoted_block_hash, p_verified_at + ); + end loop; + perform programmable_private.append_mutation_audit( + 'reward_current_snapshot.stage', p_active_configuration_hash, + p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.get_projector_reward_balances_by_vault_v1( + p_projection_run_id uuid, + p_vault bytea +) +returns table ( + chain_id bigint, + release_id text, + model_id text, + source_group text, + epoch_id uuid, + pointer_generation bigint, + checkpoint_id uuid, + projector_version text, + checkpoint_generation bigint, + reorg_generation bigint, + checkpoint_block_number bigint, + checkpoint_block_hash bytea, + reward_vault_projection_id uuid, + allocation_fact_id uuid, + allocation_evidence_id uuid, + vault bytea, + pool_id bytea, + quote_asset bytea, + configuration_hash bytea, + active_configuration_hash bytea, + configuration_epoch bigint, + total_creator_fees_received numeric, + account_reward_balance_id uuid, + account bytea, + payout_address bytea, + payout_source_kind text, + payout_configuration_epoch bigint, + claimable_accrued numeric, + claimed_total numeric, + baseline_projection_run_id uuid, + baseline_publication_commitment bytea, + baseline_promoted_block_number bigint, + baseline_promoted_block_hash bytea, + balance_projection_run_id uuid, + balance_publication_commitment bytea, + balance_promoted_block_number bigint, + balance_promoted_block_hash bytea, + payout_projection_run_id uuid, + payout_publication_commitment bytea, + payout_promoted_block_number bigint, + payout_promoted_block_hash bytea, + vault_source_occurrence_id uuid, + vault_source_logical_event_id uuid, + vault_source_block_hash bytea, + payout_source_occurrence_id uuid, + payout_source_logical_event_id uuid, + payout_source_block_hash bytea, + balance_source_occurrence_id uuid, + balance_source_logical_event_id uuid, + balance_source_block_hash bytea, + verified_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + baseline record; + raw_vault_count bigint; + baseline_count bigint; + raw_balance_count bigint; + eligible_balance_count bigint; + unique_account_count bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_open_projection_run_v1( + p_projection_run_id + ); + select * into header + from programmable_private.run_headers + where run_id = p_projection_run_id and run_kind = 'projection'; + if p_vault is null or pg_catalog.octet_length(p_vault) <> 20 then + raise exception using + errcode = '22023', message = 'invalid reward-balance vault'; + end if; + + select pg_catalog.count(*) into raw_vault_count + from programmable_private.current_reward_vault_projections_v1 as current_vault + join programmable_private.run_headers as current_run + on current_run.run_id = current_vault.projection_run_id + and current_run.run_kind = 'projection' + and current_run.chain_id = current_vault.chain_id + and current_run.release_id = current_vault.release_id + and current_run.model_id = current_vault.model_id + and current_run.epoch_id = current_vault.epoch_id + and current_run.captured_pointer_generation = + current_vault.pointer_generation + and current_run.source_group = header.source_group + where current_vault.chain_id = header.chain_id + and current_vault.release_id = header.release_id + and current_vault.model_id = header.model_id + and current_vault.epoch_id = header.epoch_id + and current_vault.pointer_generation = + header.captured_pointer_generation + and current_vault.vault = p_vault; + if raw_vault_count > 1 then + raise exception using + errcode = '23514', message = 'reward-balance vault is ambiguous'; + end if; + if raw_vault_count = 0 then return; end if; + + select pg_catalog.count(*) into baseline_count + from programmable_private.current_reward_vault_projections_v1 as current_vault + join programmable_private.run_headers as current_run + on current_run.run_id = current_vault.projection_run_id + and current_run.run_kind = 'projection' + and current_run.chain_id = current_vault.chain_id + and current_run.release_id = current_vault.release_id + and current_run.model_id = current_vault.model_id + and current_run.epoch_id = current_vault.epoch_id + and current_run.captured_pointer_generation = + current_vault.pointer_generation + and current_run.source_group = header.source_group + join programmable_private.projection_entity_current as entity + on entity.entity_kind = 'reward_vault' + and entity.projection_row_id = current_vault.reward_vault_projection_id + and entity.projection_run_id = current_vault.projection_run_id + and entity.chain_id = current_vault.chain_id + and entity.release_id = current_vault.release_id + and entity.model_id = current_vault.model_id + and entity.source_group = header.source_group + join programmable_private.projection_publications as publication + on publication.publication_id = entity.publication_id + and publication.run_id = current_vault.projection_run_id + and publication.epoch_id = current_vault.epoch_id + and publication.pointer_generation = current_vault.pointer_generation + and publication.checkpoint_id = entity.checkpoint_id + and publication.target_block_number = + current_vault.promoted_block_number + and publication.target_block_hash = current_vault.promoted_block_hash + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = entity.checkpoint_id + and checkpoint.chain_id = current_vault.chain_id + and checkpoint.release_id = current_vault.release_id + and checkpoint.model_id = current_vault.model_id + and checkpoint.source_group = header.source_group + and checkpoint.epoch_id = header.epoch_id + and checkpoint.pointer_generation = header.captured_pointer_generation + and checkpoint.block_number = publication.target_block_number + and checkpoint.block_hash = publication.target_block_hash + join programmable_private.projector_checkpoint_current as current_checkpoint + on current_checkpoint.chain_id = checkpoint.chain_id + and current_checkpoint.release_id = checkpoint.release_id + and current_checkpoint.model_id = checkpoint.model_id + and current_checkpoint.source_group = checkpoint.source_group + and current_checkpoint.projector_version = checkpoint.projector_version + join programmable_private.projector_checkpoints as current_cursor + on current_cursor.checkpoint_id = current_checkpoint.checkpoint_id + and current_cursor.chain_id = checkpoint.chain_id + and current_cursor.release_id = checkpoint.release_id + and current_cursor.model_id = checkpoint.model_id + and current_cursor.source_group = checkpoint.source_group + and current_cursor.projector_version = checkpoint.projector_version + and current_cursor.epoch_id = header.epoch_id + and current_cursor.pointer_generation = + header.captured_pointer_generation + and current_cursor.checkpoint_generation = + current_checkpoint.checkpoint_generation + and current_cursor.reorg_generation = current_checkpoint.reorg_generation + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = current_vault.last_source_occurrence_id + and canonical.logical_event_id = + current_vault.last_source_logical_event_id + and canonical.block_hash = + current_vault.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as vault_source + on vault_source.occurrence_id = current_vault.last_source_occurrence_id + and vault_source.logical_event_id = + current_vault.last_source_logical_event_id + and vault_source.block_hash = + current_vault.last_source_occurrence_block_hash + and vault_source.chain_id = current_vault.chain_id + and vault_source.release_id = current_vault.release_id + and vault_source.model_id = current_vault.model_id + and vault_source.source_group = header.source_group + and vault_source.epoch_id = current_vault.epoch_id + and vault_source.pointer_generation = current_vault.pointer_generation + and vault_source.block_number <= checkpoint.block_number + where current_vault.chain_id = header.chain_id + and current_vault.release_id = header.release_id + and current_vault.model_id = header.model_id + and current_vault.epoch_id = header.epoch_id + and current_vault.pointer_generation = + header.captured_pointer_generation + and current_vault.vault = p_vault + and current_cursor.reorg_generation = checkpoint.reorg_generation + and ( + current_cursor.block_number, + current_cursor.cursor_block_global_log_index + ) >= ( + checkpoint.block_number, + checkpoint.cursor_block_global_log_index + ) + and programmable_private.has_current_verified_reward_seed( + current_vault.projection_run_id, current_vault.vault + ); + if baseline_count <> 1 then + raise exception using + errcode = '23514', + message = 'reward-balance baseline is not exact-current'; + end if; + + select + current_vault.reward_vault_projection_id, + current_vault.current_allocation_fact_id, + current_vault.projection_run_id, + current_vault.promoted_block_number, + current_vault.promoted_block_hash, + current_vault.pool_id, + current_vault.quote_asset, + current_vault.configuration_hash, + coalesce( + current_vault.active_configuration_hash, + current_vault.configuration_hash + ) as active_configuration_hash, + coalesce( + current_vault.configuration_epoch, + ( + select pg_catalog.max(allocation.configuration_epoch) + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + current_vault.reward_vault_projection_id + and allocation.projection_run_id = current_vault.projection_run_id + ) + ) as configuration_epoch, + coalesce( + current_vault.total_creator_fees_received, + ( + select pg_catalog.sum( + current_balance.claimable_accrued + current_balance.claimed_total + ) + from programmable_private.current_account_reward_balances_v1 + as current_balance + where current_balance.chain_id = current_vault.chain_id + and current_balance.release_id = current_vault.release_id + and current_balance.model_id = current_vault.model_id + and current_balance.epoch_id = current_vault.epoch_id + and current_balance.pointer_generation = + current_vault.pointer_generation + and current_balance.vault = current_vault.vault + ), + 0 + ) as total_creator_fees_received, + current_vault.last_source_occurrence_id, + current_vault.last_source_logical_event_id, + current_vault.last_source_occurrence_block_hash, + current_vault.verified_at as vault_verified_at, + entity.checkpoint_id, + checkpoint.projector_version, + checkpoint.checkpoint_generation, + checkpoint.reorg_generation, + checkpoint.block_number, + checkpoint.block_hash, + publication_audit.input_commitment + into baseline + from programmable_private.current_reward_vault_projections_v1 as current_vault + join programmable_private.run_headers as current_run + on current_run.run_id = current_vault.projection_run_id + and current_run.run_kind = 'projection' + and current_run.chain_id = current_vault.chain_id + and current_run.release_id = current_vault.release_id + and current_run.model_id = current_vault.model_id + and current_run.epoch_id = current_vault.epoch_id + and current_run.captured_pointer_generation = + current_vault.pointer_generation + and current_run.source_group = header.source_group + join programmable_private.projection_entity_current as entity + on entity.entity_kind = 'reward_vault' + and entity.projection_row_id = current_vault.reward_vault_projection_id + and entity.projection_run_id = current_vault.projection_run_id + and entity.chain_id = current_vault.chain_id + and entity.release_id = current_vault.release_id + and entity.model_id = current_vault.model_id + and entity.source_group = header.source_group + join programmable_private.projection_publications as publication + on publication.publication_id = entity.publication_id + and publication.run_id = current_vault.projection_run_id + and publication.epoch_id = current_vault.epoch_id + and publication.pointer_generation = current_vault.pointer_generation + and publication.checkpoint_id = entity.checkpoint_id + and publication.target_block_number = + current_vault.promoted_block_number + and publication.target_block_hash = current_vault.promoted_block_hash + join programmable_private.mutation_audits as publication_audit + on publication_audit.audit_id = publication.audit_id + and publication_audit.run_id = current_vault.projection_run_id + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = entity.checkpoint_id + and checkpoint.chain_id = current_vault.chain_id + and checkpoint.release_id = current_vault.release_id + and checkpoint.model_id = current_vault.model_id + and checkpoint.source_group = header.source_group + and checkpoint.epoch_id = header.epoch_id + and checkpoint.pointer_generation = header.captured_pointer_generation + and checkpoint.block_number = publication.target_block_number + and checkpoint.block_hash = publication.target_block_hash + join programmable_private.projector_checkpoint_current as current_checkpoint + on current_checkpoint.chain_id = checkpoint.chain_id + and current_checkpoint.release_id = checkpoint.release_id + and current_checkpoint.model_id = checkpoint.model_id + and current_checkpoint.source_group = checkpoint.source_group + and current_checkpoint.projector_version = checkpoint.projector_version + join programmable_private.projector_checkpoints as current_cursor + on current_cursor.checkpoint_id = current_checkpoint.checkpoint_id + and current_cursor.chain_id = checkpoint.chain_id + and current_cursor.release_id = checkpoint.release_id + and current_cursor.model_id = checkpoint.model_id + and current_cursor.source_group = checkpoint.source_group + and current_cursor.projector_version = checkpoint.projector_version + and current_cursor.epoch_id = header.epoch_id + and current_cursor.pointer_generation = + header.captured_pointer_generation + and current_cursor.checkpoint_generation = + current_checkpoint.checkpoint_generation + and current_cursor.reorg_generation = current_checkpoint.reorg_generation + where current_vault.chain_id = header.chain_id + and current_vault.release_id = header.release_id + and current_vault.model_id = header.model_id + and current_vault.epoch_id = header.epoch_id + and current_vault.pointer_generation = + header.captured_pointer_generation + and current_vault.vault = p_vault + and current_cursor.reorg_generation = checkpoint.reorg_generation + and ( + current_cursor.block_number, + current_cursor.cursor_block_global_log_index + ) >= ( + checkpoint.block_number, + checkpoint.cursor_block_global_log_index + ); + + select + pg_catalog.count(*), + pg_catalog.count(distinct balance.account) + into raw_balance_count, unique_account_count + from programmable_private.current_account_reward_balances_v1 as balance + join programmable_private.run_headers as balance_run + on balance_run.run_id = balance.projection_run_id + and balance_run.run_kind = 'projection' + and balance_run.chain_id = balance.chain_id + and balance_run.release_id = balance.release_id + and balance_run.model_id = balance.model_id + and balance_run.source_group = header.source_group + and balance_run.epoch_id = balance.epoch_id + and balance_run.captured_pointer_generation = balance.pointer_generation + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = p_vault; + if raw_balance_count < 1 or unique_account_count <> raw_balance_count then + raise exception using + errcode = '23514', + message = 'reward-balance account set is incomplete or ambiguous'; + end if; + + return query + select + balance.chain_id::bigint, + balance.release_id::text, + balance.model_id::text, + header.source_group::text, + balance.epoch_id, + balance.pointer_generation, + baseline.checkpoint_id::uuid, + baseline.projector_version::text, + baseline.checkpoint_generation::bigint, + baseline.reorg_generation::bigint, + baseline.block_number::bigint, + baseline.block_hash::bytea, + baseline.reward_vault_projection_id::uuid, + baseline.current_allocation_fact_id::uuid, + ( + select verified_seed.allocation_evidence_id + from programmable_private.reward_allocation_current_verified + as verified_seed + where verified_seed.allocation_fact_id = + baseline.current_allocation_fact_id + and verified_seed.vault = p_vault + )::uuid, + p_vault::bytea, + baseline.pool_id::bytea, + baseline.quote_asset::bytea, + baseline.configuration_hash::bytea, + baseline.active_configuration_hash::bytea, + baseline.configuration_epoch::bigint, + baseline.total_creator_fees_received::numeric, + balance.account_reward_balance_id, + balance.account::bytea, + payout.payout_address::bytea, + payout.payout_source_kind::text, + payout.configuration_epoch::bigint, + balance.claimable_accrued::numeric, + balance.claimed_total::numeric, + baseline.projection_run_id::uuid, + baseline.input_commitment::bytea, + baseline.promoted_block_number::bigint, + baseline.promoted_block_hash::bytea, + balance.projection_run_id, + balance_publication_audit.input_commitment::bytea, + balance.promoted_block_number::bigint, + balance.promoted_block_hash::bytea, + payout.payout_projection_run_id::uuid, + payout.payout_publication_commitment::bytea, + payout.payout_promoted_block_number::bigint, + payout.payout_promoted_block_hash::bytea, + baseline.last_source_occurrence_id::uuid, + baseline.last_source_logical_event_id::uuid, + baseline.last_source_occurrence_block_hash::bytea, + payout.payout_source_occurrence_id::uuid, + payout.payout_source_logical_event_id::uuid, + payout.payout_source_block_hash::bytea, + balance.last_source_occurrence_id, + balance.last_source_logical_event_id, + balance.last_source_occurrence_block_hash::bytea, + greatest( + baseline.vault_verified_at, + balance.verified_at, + balance_source.verified_at + ) + from programmable_private.current_account_reward_balances_v1 as balance + join programmable_private.run_headers as balance_run + on balance_run.run_id = balance.projection_run_id + and balance_run.run_kind = 'projection' + and balance_run.chain_id = balance.chain_id + and balance_run.release_id = balance.release_id + and balance_run.model_id = balance.model_id + and balance_run.source_group = header.source_group + and balance_run.epoch_id = balance.epoch_id + and balance_run.captured_pointer_generation = balance.pointer_generation + join programmable_private.projection_entity_current as balance_entity + on balance_entity.entity_kind = 'account_reward_balance' + and balance_entity.projection_row_id = balance.account_reward_balance_id + and balance_entity.projection_run_id = balance.projection_run_id + and balance_entity.chain_id = balance.chain_id + and balance_entity.release_id = balance.release_id + and balance_entity.model_id = balance.model_id + and balance_entity.source_group = header.source_group + and balance_entity.checkpoint_id = baseline.checkpoint_id + join programmable_private.projection_publications as balance_publication + on balance_publication.publication_id = balance_entity.publication_id + and balance_publication.run_id = balance.projection_run_id + and balance_publication.epoch_id = balance.epoch_id + and balance_publication.pointer_generation = balance.pointer_generation + and balance_publication.checkpoint_id = baseline.checkpoint_id + and balance_publication.target_block_number = + balance.promoted_block_number + and balance_publication.target_block_hash = balance.promoted_block_hash + join programmable_private.mutation_audits as balance_publication_audit + on balance_publication_audit.audit_id = balance_publication.audit_id + and balance_publication_audit.run_id = balance.projection_run_id + join programmable_private.projector_checkpoints as balance_checkpoint + on balance_checkpoint.checkpoint_id = balance_entity.checkpoint_id + and balance_checkpoint.chain_id = balance.chain_id + and balance_checkpoint.release_id = balance.release_id + and balance_checkpoint.model_id = balance.model_id + and balance_checkpoint.source_group = header.source_group + and balance_checkpoint.epoch_id = header.epoch_id + and balance_checkpoint.pointer_generation = + header.captured_pointer_generation + and balance_checkpoint.projector_version = baseline.projector_version + and balance_checkpoint.checkpoint_generation = + baseline.checkpoint_generation + and balance_checkpoint.reorg_generation = baseline.reorg_generation + and balance_checkpoint.block_number = baseline.block_number + and balance_checkpoint.block_hash = baseline.block_hash + join programmable_private.projector_checkpoint_current + as current_checkpoint + on current_checkpoint.chain_id = balance_checkpoint.chain_id + and current_checkpoint.release_id = balance_checkpoint.release_id + and current_checkpoint.model_id = balance_checkpoint.model_id + and current_checkpoint.source_group = balance_checkpoint.source_group + and current_checkpoint.projector_version = + balance_checkpoint.projector_version + and current_checkpoint.checkpoint_id = balance_checkpoint.checkpoint_id + and current_checkpoint.checkpoint_generation = + balance_checkpoint.checkpoint_generation + and current_checkpoint.reorg_generation = + balance_checkpoint.reorg_generation + join programmable_private.chain_event_current_canonical + as balance_canonical + on balance_canonical.occurrence_id = balance.last_source_occurrence_id + and balance_canonical.logical_event_id = + balance.last_source_logical_event_id + and balance_canonical.block_hash = + balance.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as balance_source + on balance_source.occurrence_id = balance.last_source_occurrence_id + and balance_source.logical_event_id = balance.last_source_logical_event_id + and balance_source.block_hash = balance.last_source_occurrence_block_hash + and balance_source.chain_id = balance.chain_id + and balance_source.release_id = balance.release_id + and balance_source.model_id = balance.model_id + and balance_source.source_group = header.source_group + and balance_source.epoch_id = balance.epoch_id + and balance_source.pointer_generation = balance.pointer_generation + and balance_source.block_number <= balance_checkpoint.block_number + join lateral ( + select candidate.* + from ( + select + coalesce(balance.payout_address, balance.account)::bytea + as payout_address, + 'balance_account'::text as payout_source_kind, + baseline.configuration_epoch::bigint as configuration_epoch, + balance.projection_run_id as payout_projection_run_id, + balance_publication_audit.input_commitment::bytea + as payout_publication_commitment, + balance.promoted_block_number::bigint + as payout_promoted_block_number, + balance.promoted_block_hash::bytea as payout_promoted_block_hash, + balance.last_source_occurrence_id as payout_source_occurrence_id, + balance.last_source_logical_event_id + as payout_source_logical_event_id, + balance.last_source_occurrence_block_hash::bytea + as payout_source_block_hash, + balance_source.block_number::bigint as payout_source_block_number, + balance_source.block_global_log_index::bigint + as payout_source_global_log_index, + 2::integer as payout_source_priority + where balance.release_id = 'classic-v3' + + union all + + select + payout_change.new_payout_address::bytea as payout_address, + 'payout_change'::text as payout_source_kind, + payout_change.configuration_epoch::bigint as configuration_epoch, + payout_change.projection_run_id as payout_projection_run_id, + payout_audit.input_commitment::bytea + as payout_publication_commitment, + payout_change.promoted_block_number::bigint + as payout_promoted_block_number, + payout_change.promoted_block_hash::bytea + as payout_promoted_block_hash, + payout_change.source_occurrence_id + as payout_source_occurrence_id, + payout_change.source_logical_event_id + as payout_source_logical_event_id, + payout_change.source_occurrence_block_hash::bytea + as payout_source_block_hash, + payout_source.block_number::bigint as payout_source_block_number, + payout_source.block_global_log_index::bigint + as payout_source_global_log_index, + 1::integer as payout_source_priority + from programmable_private.payout_change_projections as payout_change + join programmable_private.run_headers as payout_run + on payout_run.run_id = payout_change.projection_run_id + and payout_run.run_kind = 'projection' + and payout_run.chain_id = payout_change.chain_id + and payout_run.release_id = payout_change.release_id + and payout_run.model_id = payout_change.model_id + and payout_run.source_group = header.source_group + and payout_run.epoch_id = payout_change.epoch_id + and payout_run.captured_pointer_generation = + payout_change.pointer_generation + join programmable_private.projection_publications + as payout_publication + on payout_publication.run_id = payout_change.projection_run_id + and payout_publication.epoch_id = payout_change.epoch_id + and payout_publication.pointer_generation = + payout_change.pointer_generation + and payout_publication.target_block_number = + payout_change.promoted_block_number + and payout_publication.target_block_hash = + payout_change.promoted_block_hash + join programmable_private.mutation_audits as payout_audit + on payout_audit.audit_id = payout_publication.audit_id + and payout_audit.run_id = payout_change.projection_run_id + join programmable_private.projector_checkpoints as payout_checkpoint + on payout_checkpoint.checkpoint_id = payout_publication.checkpoint_id + and payout_checkpoint.chain_id = payout_change.chain_id + and payout_checkpoint.release_id = payout_change.release_id + and payout_checkpoint.model_id = payout_change.model_id + and payout_checkpoint.source_group = header.source_group + and payout_checkpoint.epoch_id = header.epoch_id + and payout_checkpoint.pointer_generation = + header.captured_pointer_generation + and payout_checkpoint.block_number = + payout_publication.target_block_number + and payout_checkpoint.block_hash = payout_publication.target_block_hash + and payout_checkpoint.block_number <= baseline.block_number + join programmable_private.chain_event_current_canonical + as payout_canonical + on payout_canonical.occurrence_id = + payout_change.source_occurrence_id + and payout_canonical.logical_event_id = + payout_change.source_logical_event_id + and payout_canonical.block_hash = + payout_change.source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as payout_source + on payout_source.occurrence_id = payout_change.source_occurrence_id + and payout_source.logical_event_id = + payout_change.source_logical_event_id + and payout_source.block_hash = + payout_change.source_occurrence_block_hash + and payout_source.chain_id = payout_change.chain_id + and payout_source.release_id = payout_change.release_id + and payout_source.model_id = payout_change.model_id + and payout_source.source_group = header.source_group + and payout_source.epoch_id = payout_change.epoch_id + and payout_source.pointer_generation = payout_change.pointer_generation + and payout_source.block_number <= payout_checkpoint.block_number + where payout_change.chain_id = balance.chain_id + and payout_change.release_id = balance.release_id + and payout_change.model_id = balance.model_id + and payout_change.epoch_id = balance.epoch_id + and payout_change.pointer_generation = balance.pointer_generation + and payout_change.vault = balance.vault + and payout_change.beneficiary = balance.account + + union all + + select + allocation.payout_address::bytea, + 'allocation'::text, + allocation.configuration_epoch::bigint, + allocation.projection_run_id, + allocation_audit.input_commitment::bytea, + allocation.promoted_block_number::bigint, + allocation.promoted_block_hash::bytea, + allocation.last_source_occurrence_id, + allocation.last_source_logical_event_id, + allocation.last_source_occurrence_block_hash::bytea, + allocation_source.block_number::bigint, + allocation_source.block_global_log_index::bigint, + 0::integer + from programmable_private.reward_allocation_projections as allocation + join programmable_private.reward_vault_projections as allocation_vault + on allocation_vault.reward_vault_projection_id = + allocation.reward_vault_projection_id + and allocation_vault.projection_run_id = allocation.projection_run_id + and allocation_vault.chain_id = allocation.chain_id + and allocation_vault.release_id = allocation.release_id + and allocation_vault.model_id = allocation.model_id + and allocation_vault.epoch_id = allocation.epoch_id + and allocation_vault.pointer_generation = allocation.pointer_generation + join programmable_private.run_headers as allocation_run + on allocation_run.run_id = allocation.projection_run_id + and allocation_run.run_kind = 'projection' + and allocation_run.chain_id = allocation.chain_id + and allocation_run.release_id = allocation.release_id + and allocation_run.model_id = allocation.model_id + and allocation_run.source_group = header.source_group + and allocation_run.epoch_id = allocation.epoch_id + and allocation_run.captured_pointer_generation = + allocation.pointer_generation + join programmable_private.projection_publications + as allocation_publication + on allocation_publication.run_id = allocation.projection_run_id + and allocation_publication.epoch_id = allocation.epoch_id + and allocation_publication.pointer_generation = + allocation.pointer_generation + and allocation_publication.target_block_number = + allocation.promoted_block_number + and allocation_publication.target_block_hash = + allocation.promoted_block_hash + join programmable_private.mutation_audits as allocation_audit + on allocation_audit.audit_id = allocation_publication.audit_id + and allocation_audit.run_id = allocation.projection_run_id + join programmable_private.projector_checkpoints + as allocation_checkpoint + on allocation_checkpoint.checkpoint_id = + allocation_publication.checkpoint_id + and allocation_checkpoint.chain_id = allocation.chain_id + and allocation_checkpoint.release_id = allocation.release_id + and allocation_checkpoint.model_id = allocation.model_id + and allocation_checkpoint.source_group = header.source_group + and allocation_checkpoint.epoch_id = header.epoch_id + and allocation_checkpoint.pointer_generation = + header.captured_pointer_generation + and allocation_checkpoint.block_number = + allocation_publication.target_block_number + and allocation_checkpoint.block_hash = + allocation_publication.target_block_hash + and allocation_checkpoint.block_number <= baseline.block_number + join programmable_private.chain_event_current_canonical + as allocation_canonical + on allocation_canonical.occurrence_id = + allocation.last_source_occurrence_id + and allocation_canonical.logical_event_id = + allocation.last_source_logical_event_id + and allocation_canonical.block_hash = + allocation.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as allocation_source + on allocation_source.occurrence_id = + allocation.last_source_occurrence_id + and allocation_source.logical_event_id = + allocation.last_source_logical_event_id + and allocation_source.block_hash = + allocation.last_source_occurrence_block_hash + and allocation_source.chain_id = allocation.chain_id + and allocation_source.release_id = allocation.release_id + and allocation_source.model_id = allocation.model_id + and allocation_source.source_group = header.source_group + and allocation_source.epoch_id = allocation.epoch_id + and allocation_source.pointer_generation = allocation.pointer_generation + and allocation_source.block_number <= + allocation_checkpoint.block_number + where allocation.chain_id = balance.chain_id + and allocation.release_id = balance.release_id + and allocation.model_id = balance.model_id + and allocation.epoch_id = balance.epoch_id + and allocation.pointer_generation = balance.pointer_generation + and allocation_vault.vault = balance.vault + and allocation.beneficiary = balance.account + ) as candidate + order by + candidate.payout_source_block_number desc, + candidate.payout_source_global_log_index desc, + candidate.payout_source_priority desc, + candidate.payout_source_occurrence_id desc, + candidate.payout_projection_run_id desc + limit 1 + ) as payout on true + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = p_vault + and balance.projection_run_id = baseline.projection_run_id + order by balance.account; + get diagnostics eligible_balance_count = row_count; + if eligible_balance_count <> raw_balance_count then + raise exception using + errcode = '23514', + message = 'reward-balance or payout set is not checkpoint-exact'; + end if; +end +$function$; + +create function programmable_private.promote_projection_run_v2( + p_promotion_mode text, + p_publication_id uuid, + p_checkpoint_id uuid, + p_outcome_id uuid, + p_run_id uuid, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_expected_checkpoint_generation bigint, + p_next_checkpoint_generation bigint, + p_reorg_generation bigint, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea, + p_cursor_block_global_log_index numeric, + p_cursor_candidate_id text, + p_occurrence_ids uuid[], + p_allocation_fact_ids uuid[], + p_allocation_evidence_ids uuid[], + p_candidate_disposition_ids uuid[], + p_route_keys text[], + p_result_commitment bytea, + p_published_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + observation programmable_private.safe_head_observations%rowtype; + target_evidence programmable_private.dual_rpc_block_evidence%rowtype; + current_checkpoint + programmable_private.projector_checkpoint_current%rowtype; + previous_checkpoint programmable_private.projector_checkpoints%rowtype; + staged_vault programmable_private.reward_vault_projections%rowtype; + baseline_vault programmable_private.reward_vault_projections%rowtype; + seed_current + programmable_private.reward_allocation_current_verified%rowtype; + terminal_occurrence_id uuid; + selected_route_key text; + group_event record; + target_block bigint; + cursor_log_index bigint; + group_transaction_hash bytea; + group_block_number bigint; + group_occurrence_count bigint; + complete_group_occurrence_ids uuid[]; + audit_id uuid; + status_id uuid; + route_history_id uuid; + ordered_occurrence_ids uuid[]; + ordered_fact_ids uuid[]; + ordered_disposition_ids uuid[]; + required_disposition_ids uuid[]; + ordered_route_keys text[]; + ordered_projection_rows text[]; + projection_row_count bigint; + vault_count bigint; + allocation_count bigint; + balance_count bigint; + claim_count bigint; + claim_event_count bigint; + checkpoint_count bigint; + terminal_reward_event_count bigint; + unique_beneficiary_count bigint; + total_share_bps numeric; + total_balance_value numeric; + checkpoint_amount_total numeric; + checkpoint_terminal_total numeric; + baseline_total_creator_fees numeric; + baseline_configuration_epoch bigint; +begin + if p_promotion_mode = 'full_launch' then + return programmable_private.promote_projection_run( + p_publication_id, p_checkpoint_id, p_outcome_id, p_run_id, + p_projector_version, p_lease_generation, p_lease_token_hash, + p_expected_checkpoint_generation, p_next_checkpoint_generation, + p_reorg_generation, p_safe_head_observation_id, + p_target_block_evidence_id, p_target_block_number, + p_target_block_hash, p_cursor_block_global_log_index, + p_cursor_candidate_id, p_occurrence_ids, p_allocation_fact_ids, + p_allocation_evidence_ids, p_candidate_disposition_ids, + p_route_keys, p_result_commitment, p_published_at + ); + end if; + if p_promotion_mode <> 'reward_snapshot_delta' then + raise exception using + errcode = '22023', message = 'unknown projection promotion mode'; + end if; + + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'projection' + for update; + if not found then + raise exception using + errcode = '23503', message = 'invalid projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation + ); + if header.release_id not in ( + 'classic-v3', + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'reward snapshot run is not promotable'; + end if; + if not exists ( + select 1 + from programmable_private.projector_lease_current as lease + where lease.chain_id = header.chain_id + and lease.release_id = header.release_id + and lease.model_id = header.model_id + and lease.source_group = header.source_group + and lease.projector_version = p_projector_version + and lease.epoch_id = header.epoch_id + and lease.pointer_generation = header.captured_pointer_generation + and lease.lease_generation = p_lease_generation + and lease.lease_token_hash = p_lease_token_hash + and lease.expires_at >= p_published_at + ) then + raise exception using + errcode = '40001', message = 'stale projector lease'; + end if; + if p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or p_cursor_block_global_log_index < 0 + or p_cursor_block_global_log_index <> + pg_catalog.trunc(p_cursor_block_global_log_index) + or p_cursor_block_global_log_index > 4294967295 + or p_cursor_candidate_id is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + or p_next_checkpoint_generation <> + p_expected_checkpoint_generation + 1 + or coalesce(pg_catalog.cardinality(p_occurrence_ids), 0) not between 1 and 16 + or pg_catalog.cardinality(p_allocation_fact_ids) <> 1 + or pg_catalog.cardinality(p_allocation_evidence_ids) <> 1 + or coalesce(pg_catalog.cardinality(p_route_keys), 0) = 0 + then + raise exception using + errcode = '22023', message = 'invalid reward delta promotion request'; + end if; + select pg_catalog.array_agg(item order by item) + into ordered_fact_ids + from ( + select distinct item + from pg_catalog.unnest(p_allocation_fact_ids) as item + ) as unique_items; + select pg_catalog.array_agg(item order by item) + into ordered_disposition_ids + from ( + select distinct item + from pg_catalog.unnest(p_candidate_disposition_ids) as item + ) as unique_items; + select pg_catalog.array_agg(item order by item) + into ordered_route_keys + from ( + select distinct item from pg_catalog.unnest(p_route_keys) as item + ) as unique_items; + if p_allocation_fact_ids is distinct from ordered_fact_ids + or p_candidate_disposition_ids is distinct from + coalesce(ordered_disposition_ids, array[]::uuid[]) + or p_route_keys is distinct from ordered_route_keys + or exists ( + select 1 from pg_catalog.unnest(p_occurrence_ids) as item + where item is null + ) + or pg_catalog.cardinality(p_occurrence_ids) <> + ( + select pg_catalog.count(distinct item) + from pg_catalog.unnest(p_occurrence_ids) as item + ) + or exists ( + select 1 from pg_catalog.unnest(p_allocation_fact_ids) as item + where item is null + ) + or exists ( + select 1 from pg_catalog.unnest(p_allocation_evidence_ids) as item + where item is null + ) + or exists ( + select 1 from pg_catalog.unnest(p_candidate_disposition_ids) as item + where item is null + ) + or exists ( + select 1 from pg_catalog.unnest(p_route_keys) as item + where item is null + ) + then + raise exception using + errcode = '22023', + message = 'promotion arrays must be non-null and canonically ordered'; + end if; + terminal_occurrence_id := p_occurrence_ids[ + pg_catalog.array_upper(p_occurrence_ids, 1) + ]; + + target_block := p_target_block_number::bigint; + cursor_log_index := p_cursor_block_global_log_index::bigint; + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_safe_head_observation_id; + if not found + or observation.epoch_id <> header.epoch_id + or observation.pointer_generation <> + header.captured_pointer_generation + or target_block > observation.safe_block_number + then + raise exception using + errcode = '23514', message = 'target is outside accepted safe head'; + end if; + select * into target_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_target_block_evidence_id + and observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found + or target_evidence.block_number <> target_block + or target_evidence.agreed_block_hash <> p_target_block_hash + then + raise exception using + errcode = '23514', + message = 'target/checkpoint hash is not bound evidence'; + end if; + select * into current_checkpoint + from programmable_private.projector_checkpoint_current + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + for update; + if not found + or current_checkpoint.checkpoint_generation <> + p_expected_checkpoint_generation + or current_checkpoint.reorg_generation <> p_reorg_generation + or p_expected_checkpoint_generation = 0 + then + raise exception using + errcode = '40001', message = 'reward delta checkpoint CAS lost'; + end if; + select * into previous_checkpoint + from programmable_private.projector_checkpoints + where checkpoint_id = current_checkpoint.checkpoint_id; + if not found + or previous_checkpoint.epoch_id <> header.epoch_id + or previous_checkpoint.pointer_generation <> + header.captured_pointer_generation + then + raise exception using + errcode = '23503', message = 'current checkpoint identity is missing'; + end if; + if ( + target_block, cursor_log_index, p_cursor_candidate_id + ) <= ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) then + raise exception using + errcode = '23514', message = 'reward delta cursor did not advance'; + end if; + if not exists ( + select 1 from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = p_cursor_candidate_id + and candidate.chain_id = header.chain_id + and candidate.block_number = target_block + and candidate.block_hash = p_target_block_hash + and candidate.block_global_log_index = cursor_log_index + ) then + raise exception using + errcode = '23514', + message = 'checkpoint cursor does not match its exact inbox row'; + end if; + if exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + left join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + where candidate.chain_id = header.chain_id + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id) + and coalesce(status.status::text, 'pending') not in ( + 'resolved', 'ignored', 'quarantined' + ) + ) then + raise exception using + errcode = '23514', + message = 'checkpoint cursor cannot pass a pending candidate'; + end if; + select pg_catalog.array_agg(status.decision_id order by status.decision_id) + into required_disposition_ids + from programmable_private.envio_candidate_inbox as candidate + join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + and status.status in ('resolved', 'ignored', 'quarantined') + where candidate.chain_id = header.chain_id + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id); + if p_candidate_disposition_ids is distinct from + coalesce(required_disposition_ids, array[]::uuid[]) + then + raise exception using + errcode = '23514', + message = 'candidate disposition manifest is incomplete'; + end if; + + if exists ( + select 1 from programmable_private.launch_projections + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.pool_projections + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.pool_fee_configurations + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.fee_accrual_facts + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.pool_fee_totals + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.payout_change_projections + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.initial_buy_custody_projections + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.initial_buy_vesting_projections + where projection_run_id = p_run_id + ) + then + raise exception using + errcode = '23514', + message = 'reward delta cannot contain another projection mode'; + end if; + select pg_catalog.count(*) into vault_count + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id; + if vault_count <> 1 then + raise exception using + errcode = '23514', + message = 'reward delta requires one complete vault snapshot'; + end if; + select * into staged_vault + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id; + if staged_vault.snapshot_kind <> 'exact_current' + or staged_vault.chain_id <> header.chain_id + or staged_vault.release_id <> header.release_id + or staged_vault.model_id <> header.model_id + or staged_vault.epoch_id <> header.epoch_id + or staged_vault.pointer_generation <> + header.captured_pointer_generation + or staged_vault.promoted_block_number <> target_block + or staged_vault.promoted_block_hash <> p_target_block_hash + or staged_vault.last_source_occurrence_id <> terminal_occurrence_id + or staged_vault.baseline_checkpoint_id <> + current_checkpoint.checkpoint_id + or staged_vault.baseline_checkpoint_generation <> + current_checkpoint.checkpoint_generation + or staged_vault.baseline_reorg_generation <> + current_checkpoint.reorg_generation + or staged_vault.current_allocation_fact_id <> + p_allocation_fact_ids[1] + or staged_vault.configuration_epoch is null + or staged_vault.active_configuration_hash is null + or staged_vault.total_creator_fees_received is null + then + raise exception using + errcode = '23514', + message = 'reward delta vault is not checkpoint-exact'; + end if; + select * into baseline_vault + from programmable_private.current_reward_vault_projections_v1 as baseline + where baseline.reward_vault_projection_id = + staged_vault.baseline_reward_vault_projection_id + and baseline.chain_id = header.chain_id + and baseline.release_id = header.release_id + and baseline.model_id = header.model_id + and baseline.epoch_id = header.epoch_id + and baseline.pointer_generation = header.captured_pointer_generation + and baseline.vault = staged_vault.vault + and baseline.pool_id = staged_vault.pool_id; + if not found + or baseline_vault.launch_projection_id <> + staged_vault.launch_projection_id + or baseline_vault.current_allocation_fact_id <> + staged_vault.current_allocation_fact_id + or not programmable_private.has_current_verified_reward_seed( + baseline_vault.projection_run_id, baseline_vault.vault + ) + or not exists ( + select 1 + from programmable_private.current_launch_projections_v1 as launch + where launch.launch_projection_id = baseline_vault.launch_projection_id + and launch.chain_id = header.chain_id + and launch.release_id = header.release_id + and launch.model_id = header.model_id + and launch.epoch_id = header.epoch_id + and launch.pointer_generation = + header.captured_pointer_generation + and launch.reward_vault = baseline_vault.vault + and launch.pool_id = baseline_vault.pool_id + and launch.is_complete + ) + then + raise exception using + errcode = '23514', + message = 'reward delta baseline is stale or incomplete'; + end if; + select * into seed_current + from programmable_private.reward_allocation_current_verified + where allocation_fact_id = p_allocation_fact_ids[1] + and allocation_evidence_id = p_allocation_evidence_ids[1] + and vault = staged_vault.vault; + if not found then + raise exception using + errcode = '23514', + message = 'reward delta initial seed is not current verified'; + end if; + + select + coalesce( + baseline_vault.total_creator_fees_received, + ( + select pg_catalog.sum( + balance.claimable_accrued + balance.claimed_total + ) + from programmable_private.current_account_reward_balances_v1 + as balance + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = + header.captured_pointer_generation + and balance.vault = staged_vault.vault + ), + 0 + ), + coalesce( + baseline_vault.configuration_epoch, + ( + select pg_catalog.max(allocation.configuration_epoch) + from programmable_private.reward_allocation_projections + as allocation + where allocation.reward_vault_projection_id = + baseline_vault.reward_vault_projection_id + and allocation.projection_run_id = + baseline_vault.projection_run_id + and allocation.effective_to_block is null + ) + ) + into baseline_total_creator_fees, baseline_configuration_epoch; + if baseline_configuration_epoch is null then + raise exception using + errcode = '23514', + message = 'reward delta baseline configuration is incomplete'; + end if; + + select source.transaction_hash, source.block_number::bigint + into group_transaction_hash, group_block_number + from programmable_private.chain_event_occurrences as source + where source.occurrence_id = p_occurrence_ids[1]; + if not found then + raise exception using + errcode = '23503', message = 'reward event group is missing'; + end if; + select + pg_catalog.count(*), + pg_catalog.array_agg( + source.occurrence_id + order by source.block_number, + source.block_global_log_index, source.occurrence_id + ) + into group_occurrence_count, ordered_occurrence_ids + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation; + if group_occurrence_count <> pg_catalog.cardinality(p_occurrence_ids) + or ordered_occurrence_ids is distinct from p_occurrence_ids + or group_block_number <> target_block + or exists ( + select 1 + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + where source.chain_id <> header.chain_id + or source.source_address <> staged_vault.vault + or source.transaction_hash <> group_transaction_hash + or source.block_number <> group_block_number + or source.block_hash <> p_target_block_hash + or ( + source.block_number, + source.block_global_log_index + ) <= ( + previous_checkpoint.block_number, + previous_checkpoint.cursor_block_global_log_index + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward event group is partial, mixed, stale or misordered'; + end if; + + select pg_catalog.array_agg( + source.occurrence_id + order by source.block_number, + source.block_global_log_index, source.occurrence_id + ) + into complete_group_occurrence_ids + from programmable_private.chain_event_occurrences as source + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + where source.chain_id = header.chain_id + and source.source_address = staged_vault.vault + and source.transaction_hash = group_transaction_hash + and source.block_number = group_block_number + and source.block_hash = p_target_block_hash; + if complete_group_occurrence_ids is distinct from p_occurrence_ids + or not exists ( + select 1 + from programmable_private.chain_event_occurrence_materializations + as terminal_materialization + where terminal_materialization.occurrence_id = + terminal_occurrence_id + and terminal_materialization.chain_id = header.chain_id + and terminal_materialization.release_id = header.release_id + and terminal_materialization.model_id = header.model_id + and terminal_materialization.source_group = header.source_group + and terminal_materialization.epoch_id = header.epoch_id + and terminal_materialization.pointer_generation = + header.captured_pointer_generation + and coalesce( + terminal_materialization.first_seen_neutral_candidate_id::text, + terminal_materialization.first_seen_envio_candidate_id::text + ) = p_cursor_candidate_id + ) + then + raise exception using + errcode = '23514', + message = 'reward transaction group or cursor is incomplete'; + end if; + + for group_event in + select + source.*, + materialization.event_type as materialized_event_type, + materialization.decoded_payload as materialized_payload, + materialization.block_evidence_id as materialized_block_evidence_id, + materialization.release_binding_id as materialized_binding_id, + materialization.dynamic_source_attestation_id + as materialized_dynamic_source_id + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + order by source.block_number, + source.block_global_log_index, source.occurrence_id + loop + if not exists ( + select 1 + from programmable_private.dual_rpc_block_evidence as evidence + where evidence.block_evidence_id = + group_event.materialized_block_evidence_id + and evidence.observation_id = p_safe_head_observation_id + and evidence.epoch_id = header.epoch_id + and evidence.pointer_generation = + header.captured_pointer_generation + and evidence.block_number = group_event.block_number + and evidence.agreed_block_hash = group_event.block_hash + ) + or coalesce( + ( + select binding.source_role::text + from programmable_private.release_source_bindings as binding + where binding.binding_id = group_event.materialized_binding_id + ), + ( + select dynamic_source.deployed_source_role::text + from programmable_private.dynamic_source_attestations + as dynamic_source + where dynamic_source.dynamic_source_attestation_id = + group_event.materialized_dynamic_source_id + ) + ) is distinct from 'reward_vault' + or ( + header.release_id = 'classic-v3' + and group_event.materialized_event_type not in ( + 'CreatorFeesCheckpointed', 'BeneficiaryFeesClaimed', + 'PayoutWalletChanged', 'CtoRewardConfigurationActivated' + ) + ) + or ( + header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and group_event.materialized_event_type not in ( + 'BeneficiaryFeesClaimed', 'PayoutAddressUpdated' + ) + ) + or exists ( + select 1 + from programmable_private.chain_event_current_canonical as current + where current.logical_event_id = group_event.logical_event_id + and current.occurrence_id <> group_event.occurrence_id + ) + then + raise exception using + errcode = '23514', + message = 'reward event group lacks exact canonical evidence'; + end if; + end loop; + + select pg_catalog.count(*) into terminal_reward_event_count + from programmable_private.chain_event_occurrence_materializations + as materialization + where materialization.occurrence_id = any(p_occurrence_ids) + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + and materialization.event_type in ( + 'BeneficiaryFeesClaimed', 'PayoutWalletChanged', + 'CtoRewardConfigurationActivated', 'PayoutAddressUpdated' + ); + if terminal_reward_event_count > 1 + or ( + terminal_reward_event_count = 1 + and not exists ( + select 1 + from programmable_private.chain_event_occurrence_materializations + as terminal_materialization + where terminal_materialization.occurrence_id = + terminal_occurrence_id + and terminal_materialization.chain_id = header.chain_id + and terminal_materialization.release_id = header.release_id + and terminal_materialization.model_id = header.model_id + and terminal_materialization.source_group = header.source_group + and terminal_materialization.epoch_id = header.epoch_id + and terminal_materialization.pointer_generation = + header.captured_pointer_generation + and terminal_materialization.event_type in ( + 'BeneficiaryFeesClaimed', 'PayoutWalletChanged', + 'CtoRewardConfigurationActivated', 'PayoutAddressUpdated' + ) + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward transaction has ambiguous terminal semantics'; + end if; + + select + pg_catalog.count(*), + coalesce(pg_catalog.sum( + (materialization.decoded_payload ->> 'amount')::numeric + ), 0), + ( + pg_catalog.array_agg( + (materialization.decoded_payload ->> + 'totalCreatorFeesReceived')::numeric + order by source.block_global_log_index desc, + source.occurrence_id desc + ) + )[1] + into checkpoint_count, checkpoint_amount_total, + checkpoint_terminal_total + from programmable_private.chain_event_occurrence_materializations + as materialization + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = materialization.occurrence_id + where materialization.occurrence_id = any(p_occurrence_ids) + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + and materialization.event_type = 'CreatorFeesCheckpointed'; + if header.release_id = 'classic-v3' and ( + checkpoint_count > 1 + or checkpoint_amount_total < 0 + or staged_vault.total_creator_fees_received < + baseline_total_creator_fees + or ( + checkpoint_count = 0 + and staged_vault.total_creator_fees_received <> + baseline_total_creator_fees + ) + or ( + checkpoint_count > 0 + and ( + baseline_total_creator_fees + checkpoint_amount_total <> + staged_vault.total_creator_fees_received + or checkpoint_terminal_total <> + staged_vault.total_creator_fees_received + ) + ) + or exists ( + select 1 + from programmable_private.chain_event_occurrence_materializations + as checkpoint + where checkpoint.occurrence_id = any(p_occurrence_ids) + and checkpoint.chain_id = header.chain_id + and checkpoint.release_id = header.release_id + and checkpoint.model_id = header.model_id + and checkpoint.source_group = header.source_group + and checkpoint.epoch_id = header.epoch_id + and checkpoint.pointer_generation = + header.captured_pointer_generation + and checkpoint.event_type = 'CreatorFeesCheckpointed' + and ( + programmable_private.json_hex_bytes_v1( + checkpoint.decoded_payload, 'poolId', 32 + ) is distinct from staged_vault.pool_id + or (checkpoint.decoded_payload ->> + 'configurationEpoch')::numeric is distinct from + baseline_configuration_epoch::numeric + or (checkpoint.decoded_payload ->> 'amount')::numeric <= 0 + ) + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward checkpoint group does not reconcile'; + end if; + + select + pg_catalog.count(*), + pg_catalog.count(distinct allocation.beneficiary), + coalesce(pg_catalog.sum(allocation.share_bps), 0) + into allocation_count, unique_beneficiary_count, total_share_bps + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + and allocation.allocation_fact_id = + staged_vault.current_allocation_fact_id + and allocation.chain_id = header.chain_id + and allocation.release_id = header.release_id + and allocation.model_id = header.model_id + and allocation.epoch_id = header.epoch_id + and allocation.pointer_generation = header.captured_pointer_generation + and allocation.promoted_block_number = target_block + and allocation.promoted_block_hash = p_target_block_hash + and allocation.last_source_occurrence_id = terminal_occurrence_id + and allocation.configuration_epoch = + staged_vault.configuration_epoch + and allocation.effective_to_block is null; + if allocation_count < 1 + or allocation_count > (case + when header.release_id = 'classic-v3' then 5 else 8 + end) + or total_share_bps <> 10000 + or ( + header.release_id <> 'classic-v3' + and unique_beneficiary_count <> allocation_count + ) + or ( + select pg_catalog.count(distinct allocation.allocation_index) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + ) <> allocation_count + or ( + select pg_catalog.min(allocation.allocation_index) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + ) <> 0 + or ( + select pg_catalog.max(allocation.allocation_index) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + ) <> allocation_count - 1 + or exists ( + select 1 + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id <> + staged_vault.reward_vault_projection_id + ) + or ( + header.release_id = 'classic-v3' + and exists ( + select 1 + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.beneficiary <> allocation.payout_address + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward delta allocation set is incomplete'; + end if; + + select + pg_catalog.count(*), + coalesce(pg_catalog.sum( + balance.claimable_accrued + balance.claimed_total + ), 0) + into balance_count, total_balance_value + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = staged_vault.vault + and balance.promoted_block_number = target_block + and balance.promoted_block_hash = p_target_block_hash + and balance.last_source_occurrence_id = terminal_occurrence_id; + if balance_count < 1 + or total_balance_value <> staged_vault.total_creator_fees_received + or ( + select pg_catalog.count(distinct balance.account) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + ) <> balance_count + or exists ( + select 1 + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and ( + balance.vault <> staged_vault.vault + or ( + header.release_id = 'classic-v3' + and balance.payout_address <> balance.account + ) + ) + ) + or exists ( + select 1 + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and not exists ( + select 1 + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = staged_vault.vault + and balance.account = case + when header.release_id = 'classic-v3' + then allocation.payout_address + else allocation.beneficiary + end + and ( + header.release_id = 'classic-v3' + or balance.payout_address = allocation.payout_address + ) + ) + ) + or exists ( + select 1 + from programmable_private.current_account_reward_balances_v1 + as prior_balance + where prior_balance.chain_id = header.chain_id + and prior_balance.release_id = header.release_id + and prior_balance.model_id = header.model_id + and prior_balance.epoch_id = header.epoch_id + and prior_balance.pointer_generation = + header.captured_pointer_generation + and prior_balance.vault = staged_vault.vault + and not exists ( + select 1 + from programmable_private.account_reward_balances as next_balance + where next_balance.projection_run_id = p_run_id + and next_balance.vault = staged_vault.vault + and next_balance.account = prior_balance.account + and next_balance.claimed_total >= prior_balance.claimed_total + and next_balance.claimable_accrued + next_balance.claimed_total + >= prior_balance.claimable_accrued + + prior_balance.claimed_total + ) + ) + or exists ( + select 1 + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and not exists ( + select 1 + from programmable_private.current_account_reward_balances_v1 + as prior_balance + where prior_balance.chain_id = header.chain_id + and prior_balance.release_id = header.release_id + and prior_balance.model_id = header.model_id + and prior_balance.epoch_id = header.epoch_id + and prior_balance.pointer_generation = + header.captured_pointer_generation + and prior_balance.vault = staged_vault.vault + and prior_balance.account = balance.account + ) + and not exists ( + select 1 + from programmable_private.reward_allocation_projections + as active_allocation + where active_allocation.projection_run_id = p_run_id + and ( + case when header.release_id = 'classic-v3' + then active_allocation.payout_address + else active_allocation.beneficiary + end + ) = balance.account + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward delta balance set is incomplete or nonmonotonic'; + end if; + + select pg_catalog.count(*) into claim_count + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id; + select pg_catalog.count(*) into claim_event_count + from programmable_private.chain_event_occurrence_materializations + as materialization + where materialization.occurrence_id = any(p_occurrence_ids) + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + and materialization.event_type = 'BeneficiaryFeesClaimed'; + if claim_count <> claim_event_count + or claim_count > 1 + or ( + select pg_catalog.count(distinct claim.source_occurrence_id) + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + ) <> claim_count + or exists ( + select 1 + from programmable_private.claim_projections as claim + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = claim.source_occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = materialization.occurrence_id + where claim.projection_run_id = p_run_id + and ( + claim.chain_id <> header.chain_id + or claim.release_id <> header.release_id + or claim.model_id <> header.model_id + or claim.epoch_id <> header.epoch_id + or claim.pointer_generation <> + header.captured_pointer_generation + or claim.vault <> staged_vault.vault + or claim.claimant_kind <> 'beneficiary' + or claim.amount <= 0 + or claim.vault_total_received <> + staged_vault.total_creator_fees_received + or claim.promoted_block_number <> target_block + or claim.promoted_block_hash <> p_target_block_hash + or claim.source_occurrence_id <> terminal_occurrence_id + or claim.source_logical_event_id <> source.logical_event_id + or claim.source_occurrence_block_hash <> source.block_hash + or materialization.event_type <> 'BeneficiaryFeesClaimed' + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'beneficiary', 20 + ) is distinct from claim.beneficiary + or (materialization.decoded_payload ->> 'amount')::numeric + is distinct from claim.amount + or ( + materialization.decoded_payload ->> + 'beneficiaryTotalClaimed' + )::numeric is distinct from claim.beneficiary_total_claimed + or (materialization.decoded_payload ->> + 'vaultTotalReceived')::numeric + is distinct from claim.vault_total_received + or ( + header.release_id = 'classic-v3' + and claim.recipient <> claim.beneficiary + ) + or ( + header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and ( + programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'payoutAddress', 20 + ) is distinct from claim.recipient + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'quoteAsset', 20 + ) is distinct from staged_vault.quote_asset + ) + ) + or not exists ( + select 1 + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = staged_vault.vault + and balance.account = claim.beneficiary + and balance.claimed_total = + claim.beneficiary_total_claimed + and ( + header.release_id = 'classic-v3' + or balance.payout_address = claim.recipient + ) + ) + or claim.beneficiary_total_claimed <> + coalesce( + ( + select prior.claimed_total + from programmable_private.current_account_reward_balances_v1 + as prior + where prior.chain_id = header.chain_id + and prior.release_id = header.release_id + and prior.model_id = header.model_id + and prior.epoch_id = header.epoch_id + and prior.pointer_generation = + header.captured_pointer_generation + and prior.vault = staged_vault.vault + and prior.account = claim.beneficiary + ), + 0 + ) + claim.amount + ) + ) + or exists ( + select 1 + from programmable_private.chain_event_occurrence_materializations + as materialization + where materialization.occurrence_id = any(p_occurrence_ids) + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + and materialization.event_type = 'BeneficiaryFeesClaimed' + and not exists ( + select 1 + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + and claim.source_occurrence_id = materialization.occurrence_id + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward claim rows do not reconcile to the transaction'; + end if; + + if header.release_id = 'classic-v3' and exists ( + with baseline_allocations as ( + select + allocation.allocation_index, + allocation.payout_address::bytea as account, + allocation.share_bps::numeric as share_bps, + pg_catalog.max(allocation.allocation_index) over () as last_index + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + baseline_vault.reward_vault_projection_id + and allocation.projection_run_id = + baseline_vault.projection_run_id + and allocation.effective_to_block is null + ), non_last_total as ( + select coalesce(pg_catalog.sum( + case + when allocation_index < last_index then + pg_catalog.div(checkpoint_amount_total * share_bps, 10000) + else 0 + end + ), 0) as amount + from baseline_allocations + ), checkpoint_credits as ( + select + allocation.account, + pg_catalog.sum( + case + when allocation.allocation_index = allocation.last_index then + checkpoint_amount_total - non_last_total.amount + else pg_catalog.div( + checkpoint_amount_total * allocation.share_bps, 10000 + ) + end + ) as amount + from baseline_allocations as allocation + cross join non_last_total + group by allocation.account + ), transaction_claims as ( + select claim.beneficiary::bytea as account, claim.amount::numeric + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + ) + select 1 + from programmable_private.account_reward_balances as next_balance + left join programmable_private.current_account_reward_balances_v1 + as prior_balance + on prior_balance.chain_id = header.chain_id + and prior_balance.release_id = header.release_id + and prior_balance.model_id = header.model_id + and prior_balance.epoch_id = header.epoch_id + and prior_balance.pointer_generation = + header.captured_pointer_generation + and prior_balance.vault = staged_vault.vault + and prior_balance.account = next_balance.account + left join checkpoint_credits as credit + on credit.account = next_balance.account + left join transaction_claims as transaction_claim + on transaction_claim.account = next_balance.account + where next_balance.projection_run_id = p_run_id + and next_balance.vault = staged_vault.vault + and ( + next_balance.claimed_total <> + coalesce(prior_balance.claimed_total, 0) + + coalesce(transaction_claim.amount, 0) + or next_balance.claimable_accrued <> + coalesce(prior_balance.claimable_accrued, 0) + + coalesce(credit.amount, 0) + - coalesce(transaction_claim.amount, 0) + ) + ) then + raise exception using + errcode = '23514', + message = 'Classic reward balances do not match the exact transaction'; + end if; + + if header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and ( + ( + claim_count = 0 + and staged_vault.total_creator_fees_received <> + baseline_total_creator_fees + ) + or exists ( + with active_allocations as ( + select + allocation.allocation_index, + allocation.beneficiary::bytea as account, + allocation.share_bps::numeric as share_bps, + pg_catalog.max(allocation.allocation_index) over () + as last_index + from programmable_private.reward_allocation_projections + as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + and allocation.effective_to_block is null + ), non_last_total as ( + select coalesce(pg_catalog.sum( + case + when allocation_index < last_index then + pg_catalog.div( + staged_vault.total_creator_fees_received * share_bps, + 10000 + ) + else 0 + end + ), 0) as amount + from active_allocations + ), entitlements as ( + select + allocation.account, + case + when allocation.allocation_index = allocation.last_index then + staged_vault.total_creator_fees_received + - non_last_total.amount + else pg_catalog.div( + staged_vault.total_creator_fees_received + * allocation.share_bps, + 10000 + ) + end as amount + from active_allocations as allocation + cross join non_last_total + ), transaction_claims as ( + select claim.beneficiary::bytea as account, claim.amount::numeric + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + ) + select 1 + from programmable_private.account_reward_balances as next_balance + left join entitlements as entitlement + on entitlement.account = next_balance.account + left join programmable_private.current_account_reward_balances_v1 + as prior_balance + on prior_balance.chain_id = header.chain_id + and prior_balance.release_id = header.release_id + and prior_balance.model_id = header.model_id + and prior_balance.epoch_id = header.epoch_id + and prior_balance.pointer_generation = + header.captured_pointer_generation + and prior_balance.vault = staged_vault.vault + and prior_balance.account = next_balance.account + left join transaction_claims as transaction_claim + on transaction_claim.account = next_balance.account + where next_balance.projection_run_id = p_run_id + and next_balance.vault = staged_vault.vault + and ( + entitlement.account is null + or next_balance.claimed_total <> + coalesce(prior_balance.claimed_total, 0) + + coalesce(transaction_claim.amount, 0) + or next_balance.claimable_accrued <> + entitlement.amount + - coalesce(prior_balance.claimed_total, 0) + - coalesce(transaction_claim.amount, 0) + ) + ) + ) + then + raise exception using + errcode = '23514', + message = 'Stock-Paired reward balances do not match the exact state'; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'projection.promote.reward_snapshot_delta', + p_result_commitment, p_run_id, p_published_at + ); + select + pg_catalog.array_agg( + pg_catalog.format('%s:%s', staged.row_kind, staged.row_id) + order by staged.row_kind, staged.row_id + ), + pg_catalog.count(*) + into ordered_projection_rows, projection_row_count + from ( + select 'reward_vault'::text as row_kind, + reward_vault_projection_id as row_id + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + union all + select 'reward_allocation', reward_allocation_projection_id + from programmable_private.reward_allocation_projections + where projection_run_id = p_run_id + union all + select 'account_reward_balance', account_reward_balance_id + from programmable_private.account_reward_balances + where projection_run_id = p_run_id + union all + select 'claim', claim_projection_id + from programmable_private.claim_projections + where projection_run_id = p_run_id + ) as staged; + if projection_row_count <> + 1 + allocation_count + balance_count + claim_count + then + raise exception using + errcode = '23514', message = 'reward delta manifest is incomplete'; + end if; + insert into programmable_private.projection_fold_manifests ( + run_id, epoch_id, pointer_generation, target_block_number, + target_block_hash, ordered_occurrence_ids, + ordered_allocation_fact_ids, ordered_allocation_evidence_ids, + ordered_candidate_disposition_ids, ordered_route_keys, + cursor_block_global_log_index, cursor_candidate_id, + ordered_projection_rows, projection_row_count, + result_commitment, created_at, audit_id + ) values ( + p_run_id, header.epoch_id, header.captured_pointer_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_occurrence_ids, p_allocation_fact_ids, p_allocation_evidence_ids, + p_candidate_disposition_ids, p_route_keys, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + ordered_projection_rows, projection_row_count, + p_result_commitment::programmable_private.bytes32_value, + p_published_at, audit_id + ); + for group_event in + select source.*, materialization.block_evidence_id + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + order by source.block_number, + source.block_global_log_index, source.occurrence_id + loop + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.chain_event_occurrence_status_history ( + status_history_id, occurrence_id, logical_event_id, block_hash, + status, safe_head_observation_id, block_evidence_id, + decision_run_id, decision_commitment, decided_at, audit_id + ) values ( + status_id, group_event.occurrence_id, group_event.logical_event_id, + group_event.block_hash, 'canonical', p_safe_head_observation_id, + group_event.block_evidence_id, p_run_id, + p_result_commitment, p_published_at, audit_id + ); + insert into programmable_private.chain_event_current_canonical ( + logical_event_id, occurrence_id, block_hash, status_history_id, + selected_by_run_id, selected_at + ) values ( + group_event.logical_event_id, group_event.occurrence_id, + group_event.block_hash, status_id, p_run_id, p_published_at + ) + on conflict (logical_event_id) do update + set status_history_id = excluded.status_history_id, + selected_by_run_id = excluded.selected_by_run_id, + selected_at = excluded.selected_at + where programmable_private.chain_event_current_canonical.occurrence_id + = excluded.occurrence_id; + if not found then + raise exception using + errcode = '23505', message = 'canonical pointer conflict'; + end if; + end loop; + + insert into programmable_private.run_lifecycle_outcomes ( + outcome_id, run_id, status, result_commitment, caller_role, + finished_at, audit_id + ) values ( + p_outcome_id, p_run_id, 'succeeded', + p_result_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_published_at, audit_id + ); + insert into programmable_private.projector_checkpoints ( + checkpoint_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, lease_generation, + checkpoint_generation, reorg_generation, block_number, block_hash, + cursor_block_global_log_index, cursor_candidate_id, + safe_head_observation_id, target_block_evidence_id, run_id, + terminal_outcome_id, created_at + ) values ( + p_checkpoint_id, header.chain_id, header.release_id, header.model_id, + header.source_group, + p_projector_version::programmable_private.projector_identifier, + header.epoch_id, header.captured_pointer_generation, + p_lease_generation, p_next_checkpoint_generation, + p_reorg_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + p_safe_head_observation_id, p_target_block_evidence_id, + p_run_id, p_outcome_id, p_published_at + ); + update programmable_private.projector_checkpoint_current + set checkpoint_id = p_checkpoint_id, + checkpoint_generation = p_next_checkpoint_generation, + reorg_generation = p_reorg_generation, + changed_at = p_published_at + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + and checkpoint_generation = p_expected_checkpoint_generation + and reorg_generation = p_reorg_generation; + if not found then + raise exception using + errcode = '40001', message = 'checkpoint CAS lost'; + end if; + insert into programmable_private.projection_publications ( + publication_id, run_id, epoch_id, pointer_generation, checkpoint_id, + terminal_outcome_id, target_block_number, target_block_hash, + published_at, audit_id + ) values ( + p_publication_id, p_run_id, header.epoch_id, + header.captured_pointer_generation, p_checkpoint_id, p_outcome_id, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_published_at, audit_id + ); + foreach selected_route_key in array p_route_keys loop + route_history_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, + model_id, source_group, epoch_id, pointer_generation, status, + route_mode, checkpoint_id, reason_commitment, changed_by_run_id, + changed_at, audit_id + ) values ( + route_history_id, + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, 'eligible', 'indexed', + p_checkpoint_id, + p_result_commitment::programmable_private.bytes32_value, + p_run_id, p_published_at, audit_id + ); + insert into programmable_private.route_eligibility_current ( + route_key, chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, status, route_mode, checkpoint_id, history_id, + changed_at + ) values ( + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, 'eligible', 'indexed', + p_checkpoint_id, route_history_id, p_published_at + ) + on conflict ( + route_key, chain_id, release_id, model_id, source_group + ) do update + set epoch_id = excluded.epoch_id, + pointer_generation = excluded.pointer_generation, + status = excluded.status, + route_mode = excluded.route_mode, + checkpoint_id = excluded.checkpoint_id, + history_id = excluded.history_id, + changed_at = excluded.changed_at + where programmable_private.route_eligibility_current + .pointer_generation <= excluded.pointer_generation; + if not found then + raise exception using + errcode = '40001', + message = 'stale route eligibility generation'; + end if; + end loop; + return p_publication_id; +end +$function$; + +revoke all on programmable_private.route_eligibility_current_exact_v1, + programmable_private.route_snapshot_readiness_v1, + programmable_private.route_token_projections_v1, + programmable_private.route_checkpoint_parity_bindings, + programmable_private.public_route_snapshots_v2, + programmable_private.public_explore_token_v1, + programmable_private.public_explore_list_v1, + programmable_private.public_explore_chart_v1, + programmable_private.public_creator_profile_v1, + programmable_private.public_classic_v3_profile_v1, + programmable_private.public_stock_paired_profile_v1, + programmable_private.public_launch_lookup_v1, + programmable_private.read_model_performance_eligible_launches_v1 +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant select on programmable_private.route_eligibility_current_exact_v1, + programmable_private.route_snapshot_readiness_v1, + programmable_private.route_token_projections_v1, + programmable_private.public_route_snapshots_v2, + programmable_private.public_explore_token_v1, + programmable_private.public_explore_list_v1, + programmable_private.public_explore_chart_v1, + programmable_private.public_creator_profile_v1, + programmable_private.public_classic_v3_profile_v1, + programmable_private.public_stock_paired_profile_v1, + programmable_private.public_launch_lookup_v1 +to programmable_api_reader; + +revoke all on function + programmable_private.build_indexed_token_projection_v2(jsonb), + programmable_private.retarget_indexed_token_projection_v2(jsonb,jsonb,text), + programmable_private.build_public_snapshot_identity_v2( + text,bigint,bigint,bytea,bigint,timestamptz,jsonb + ), + programmable_private.build_public_explore_cursor_v1( + text,text,text,integer,text,numeric,bigint,bigint,bigint,text,text + ), + programmable_private.decode_public_address_v1(text), + programmable_private.decode_public_bytes32_v1(text) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on function + programmable_private.get_public_explore_page_v1( + bigint,text,text,integer,integer + ), + programmable_private.get_public_explore_token_v1(bigint,text), + programmable_private.get_public_token_chart_v1(bigint,text,text), + programmable_private.get_public_creator_profile_v1(bigint,text), + programmable_private.get_public_classic_v3_profile_v1(bigint,text), + programmable_private.get_public_stock_paired_profile_v1(bigint,text), + programmable_private.get_public_launch_lookup_v1( + bigint,text,text,text + ), + programmable_private.get_public_indexer_feed_v1(bigint) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.get_public_explore_page_v1( + bigint,text,text,integer,integer + ), + programmable_private.get_public_explore_token_v1(bigint,text), + programmable_private.get_public_token_chart_v1(bigint,text,text), + programmable_private.get_public_creator_profile_v1(bigint,text), + programmable_private.get_public_classic_v3_profile_v1(bigint,text), + programmable_private.get_public_stock_paired_profile_v1(bigint,text), + programmable_private.get_public_launch_lookup_v1( + bigint,text,text,text + ), + programmable_private.get_public_indexer_feed_v1(bigint) +to programmable_api_reader; + +revoke all on function + programmable_private.get_read_model_performance_dataset_v1(bigint) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.get_read_model_performance_dataset_v1(bigint) +to programmable_projector; + +revoke all on function + programmable_private.get_projector_reward_state_by_vault_v1(uuid, bytea), + programmable_private.get_projector_reward_balances_by_vault_v1(uuid, bytea) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.get_projector_reward_state_by_vault_v1(uuid, bytea), + programmable_private.get_projector_reward_balances_by_vault_v1(uuid, bytea) +to programmable_projector; + +revoke all on function + programmable_private.stage_current_reward_snapshot_v1( + uuid, bytea, bytea, uuid, bigint, bytea, numeric, + integer[], bytea[], bytea[], numeric[], bytea[], bytea[], + numeric[], numeric[], uuid, numeric, bytea, timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.stage_current_reward_snapshot_v1( + uuid, bytea, bytea, uuid, bigint, bytea, numeric, + integer[], bytea[], bytea[], numeric[], bytea[], bytea[], + numeric[], numeric[], uuid, numeric, bytea, timestamptz + ) +to programmable_projector; + +revoke all on function + programmable_private.promote_projection_run_v2( + text, uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.promote_projection_run_v2( + text, uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, timestamptz + ) +to programmable_projector; + +revoke all on function programmable_private.bind_route_checkpoint_parity_v1( + uuid, uuid, uuid, bytea, timestamptz +) from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.bind_route_checkpoint_parity_v1( + uuid, uuid, uuid, bytea, timestamptz + ) to programmable_reconciler; + +revoke all on function + programmable_private.get_projector_verified_reward_seed_v1(uuid, bytea) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.get_projector_verified_reward_seed_v1(uuid, bytea) +to programmable_projector; + +revoke all on function + programmable_private.get_projector_pool_baseline_by_id_v1(uuid, bytea) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.get_projector_pool_baseline_by_id_v1(uuid, bytea) +to programmable_projector; + +revoke all on function programmable_private.abi_uint256_word_v1( + bytea, integer +) from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on function programmable_private.abi_int256_word_v1( + bytea, integer +) from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on function + programmable_private.enforce_decoded_eth_usd_snapshot_v1() +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on function programmable_private.append_dual_rpc_log_coverage_evidence( + uuid, uuid, uuid, text, bigint, bigint, numeric, numeric, bytea, + numeric, text, uuid, uuid, uuid, uuid, bytea, bytea[], bytea[], + bytea, smallint, bytea, bytea, bytea, timestamptz +) from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on function programmable_private.advance_envio_ingestion_cursor_v1( + uuid, uuid, text, bigint, bigint, numeric, bytea, numeric, text, bytea, + timestamptz +) from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on function programmable_private.commit_envio_ingestion_page_v1( + uuid, uuid, uuid, uuid, text, bigint, bigint, numeric, + programmable_private.envio_candidate_page_item_v1[], uuid, uuid, + uuid, uuid, bytea, bytea[], bytea[], bytea, bytea, smallint, + bytea, bytea, bytea, timestamptz +) from public, anon, authenticated, service_role, + programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function programmable_private.commit_envio_ingestion_page_v1( + uuid, uuid, uuid, uuid, text, bigint, bigint, numeric, + programmable_private.envio_candidate_page_item_v1[], uuid, uuid, + uuid, uuid, bytea, bytea[], bytea[], bytea, bytea, smallint, + bytea, bytea, bytea, timestamptz +) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260731202904_release_probe_nonce_consumption.sql b/supabase/migrations/20260731202904_release_probe_nonce_consumption.sql new file mode 100644 index 00000000..e3490776 --- /dev/null +++ b/supabase/migrations/20260731202904_release_probe_nonce_consumption.sql @@ -0,0 +1,347 @@ +-- Distributed replay protection for privileged public-route release probes. +-- +-- The application connects as programmable_release_probe_nonce_login, verifies +-- that exact session_user, then uses SET LOCAL ROLE +-- programmable_release_probe_nonce and verifies that exact current_role before +-- calling the function below. The function is SECURITY DEFINER, so current_role +-- changes to the owner inside its body; current_setting('role', true) preserves +-- the explicitly selected capability and is the in-function role assertion. + +do $bootstrap$ +begin + if not exists ( + select 1 from pg_catalog.pg_roles + where rolname = 'programmable_release_probe_nonce' + ) then + create role programmable_release_probe_nonce + nologin nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; + if not exists ( + select 1 from pg_catalog.pg_roles + where rolname = 'programmable_release_probe_nonce_login' + ) then + create role programmable_release_probe_nonce_login + login password null nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; +end +$bootstrap$; + +alter role programmable_release_probe_nonce + nologin nocreatedb nocreaterole noinherit; +alter role programmable_release_probe_nonce_login + login password null nocreatedb nocreaterole noinherit; + +do $posture$ +begin + if exists ( + select 1 + from pg_catalog.pg_roles + where rolname = any (array[ + 'programmable_release_probe_nonce', + 'programmable_release_probe_nonce_login' + ]::name[]) + and (rolsuper or rolreplication or rolbypassrls) + ) then + raise exception 'programmable release-probe role posture is privileged'; + end if; +end +$posture$; + +grant programmable_release_probe_nonce + to programmable_release_probe_nonce_login + with inherit false, set true; + +create schema if not exists programmable_release_probe_private + authorization programmable_migrator; +alter schema programmable_release_probe_private owner to programmable_migrator; +revoke all on schema programmable_release_probe_private +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_api_reader_login, programmable_projector_login, + programmable_reconciler_login, + programmable_release_probe_nonce_login; + +set role programmable_migrator; + +alter default privileges for role programmable_migrator +in schema programmable_release_probe_private + revoke all on tables from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_api_reader_login, + programmable_projector_login, programmable_reconciler_login, + programmable_release_probe_nonce, + programmable_release_probe_nonce_login; +alter default privileges for role programmable_migrator +in schema programmable_release_probe_private + revoke all on sequences from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_api_reader_login, + programmable_projector_login, programmable_reconciler_login, + programmable_release_probe_nonce, + programmable_release_probe_nonce_login; +alter default privileges for role programmable_migrator +in schema programmable_release_probe_private + revoke execute on functions from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_api_reader_login, + programmable_projector_login, programmable_reconciler_login, + programmable_release_probe_nonce, + programmable_release_probe_nonce_login; +alter default privileges for role programmable_migrator +in schema programmable_release_probe_private + revoke usage on types from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_api_reader_login, + programmable_projector_login, programmable_reconciler_login, + programmable_release_probe_nonce, + programmable_release_probe_nonce_login; + +create type programmable_release_probe_private.release_probe_route_key_v1 +as enum ( + 'explore-list', + 'explore-token', + 'explore-chart', + 'creator-profile', + 'classic-v3-profile', + 'launch-lookup' +); + +create table programmable_release_probe_private.release_probe_nonce_consumptions_v1 ( + route_key + programmable_release_probe_private.release_probe_route_key_v1 not null, + nonce_digest bytea not null, + issued_at timestamptz not null, + expires_at timestamptz not null, + consumed_at timestamptz not null, + constraint release_probe_nonce_consumptions_v1_pkey + primary key (route_key, nonce_digest), + constraint release_probe_nonce_digest_check check ( + pg_catalog.octet_length(nonce_digest) = 32 + ), + constraint release_probe_nonce_finite_time_check check ( + pg_catalog.isfinite(issued_at) + and pg_catalog.isfinite(expires_at) + and pg_catalog.isfinite(consumed_at) + ), + constraint release_probe_nonce_ttl_check check ( + expires_at >= issued_at + interval '1 second' + and expires_at <= issued_at + interval '5 minutes' + ), + constraint release_probe_nonce_consumed_window_check check ( + issued_at <= consumed_at + interval '30 seconds' + and consumed_at < expires_at + ) +); + +comment on table + programmable_release_probe_private.release_probe_nonce_consumptions_v1 +is + 'Consumed release-probe SHA-256 nonces. Rows expire within five minutes, pruning removes at most 256 expired rows per call, and each route retains at most 4096 rows.'; + +alter table programmable_release_probe_private.release_probe_nonce_consumptions_v1 + enable row level security; +alter table programmable_release_probe_private.release_probe_nonce_consumptions_v1 + force row level security; + +create policy release_probe_nonce_consumptions_v1_migrator_all +on programmable_release_probe_private.release_probe_nonce_consumptions_v1 +for all +to programmable_migrator +using (true) +with check (true); + +-- Equality by route plus the expiry range is the complete pruning access path. +create index release_probe_nonce_consumptions_v1_expiry_idx +on programmable_release_probe_private.release_probe_nonce_consumptions_v1 ( + route_key, + expires_at, + nonce_digest +); + +create function programmable_release_probe_private.consume_release_probe_nonce_v1( + p_route_key text, + p_nonce_digest bytea, + p_issued_at timestamptz, + p_expires_at timestamptz +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + active_role text := pg_catalog.current_setting('role', true); + database_now timestamptz := pg_catalog.clock_timestamp(); + route_lock_slot integer; + validated_route + programmable_release_probe_private.release_probe_route_key_v1; + inserted boolean; +begin + -- A privileged role that can SET ROLE is still not the runtime identity. + -- Both the dedicated login and its explicitly selected SET-only capability + -- are required, including for superuser-originated test or admin sessions. + if session_user::text <> 'programmable_release_probe_nonce_login' + or active_role is distinct from 'programmable_release_probe_nonce' + then + raise exception using + errcode = '42501', + message = 'release probe nonce requires its dedicated runtime identity'; + end if; + + route_lock_slot := case p_route_key + when 'explore-list' then 1 + when 'explore-token' then 2 + when 'explore-chart' then 3 + when 'creator-profile' then 4 + when 'classic-v3-profile' then 5 + when 'launch-lookup' then 6 + else null + end; + + if route_lock_slot is null + or p_nonce_digest is null + or pg_catalog.octet_length(p_nonce_digest) <> 32 + or p_issued_at is null + or p_expires_at is null + or not pg_catalog.isfinite(p_issued_at) + or not pg_catalog.isfinite(p_expires_at) + or p_expires_at < p_issued_at + interval '1 second' + or p_expires_at > p_issued_at + interval '5 minutes' + or p_issued_at > database_now + interval '30 seconds' + or p_expires_at <= database_now + then + raise exception using + errcode = '22023', + message = 'invalid release probe nonce envelope'; + end if; + + validated_route := + p_route_key::programmable_release_probe_private.release_probe_route_key_v1; + + -- One transaction-scoped lock per route makes the bounded capacity check + -- exact under concurrency. Different routes never block one another. + perform pg_catalog.pg_advisory_xact_lock(1347571538, route_lock_slot); + + -- A queued caller must still be fresh when it owns the route lock. Never + -- authorize or stamp a row using the pre-lock clock sample. + database_now := pg_catalog.clock_timestamp(); + if p_issued_at > database_now + interval '30 seconds' + or p_expires_at <= database_now + then + raise exception using + errcode = '22023', + message = 'invalid release probe nonce envelope'; + end if; + + -- Pruning is deliberately bounded. The repeated expiry predicate in the + -- DELETE prevents a selected row from being removed if its value changes, + -- while the hard per-route ceiling below prevents unbounded retained state. + with expired as materialized ( + select nonce.route_key, nonce.nonce_digest + from programmable_release_probe_private.release_probe_nonce_consumptions_v1 as nonce + where nonce.route_key = validated_route + and nonce.expires_at <= database_now + order by nonce.expires_at, nonce.nonce_digest + limit 256 + for update skip locked + ) + delete from programmable_release_probe_private.release_probe_nonce_consumptions_v1 as nonce + using expired + where nonce.route_key = expired.route_key + and nonce.nonce_digest = expired.nonce_digest + and nonce.expires_at <= database_now; + + if ( + select pg_catalog.count(*) = 4096 + from ( + select 1 + from programmable_release_probe_private.release_probe_nonce_consumptions_v1 as nonce + where nonce.route_key = validated_route + limit 4096 + ) as bounded_route_rows + ) then + return false; + end if; + + insert into programmable_release_probe_private.release_probe_nonce_consumptions_v1 ( + route_key, + nonce_digest, + issued_at, + expires_at, + consumed_at + ) values ( + validated_route, + p_nonce_digest, + p_issued_at, + p_expires_at, + database_now + ) + on conflict (route_key, nonce_digest) do nothing + returning true into inserted; + + return coalesce(inserted, false); +end +$function$; + +comment on function programmable_release_probe_private.consume_release_probe_nonce_v1( + text, bytea, timestamptz, timestamptz +) is + 'Connect with session_user programmable_release_probe_nonce_login; in one transaction SET LOCAL ROLE programmable_release_probe_nonce and verify current_role before calling. Returns true once and false on replay or bounded capacity.'; + +revoke all on table + programmable_release_probe_private.release_probe_nonce_consumptions_v1 +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_api_reader_login, programmable_projector_login, + programmable_reconciler_login, + programmable_release_probe_nonce, + programmable_release_probe_nonce_login; + +revoke all on type + programmable_release_probe_private.release_probe_route_key_v1 +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_api_reader_login, programmable_projector_login, + programmable_reconciler_login, + programmable_release_probe_nonce, + programmable_release_probe_nonce_login; + +revoke all on function + programmable_release_probe_private.consume_release_probe_nonce_v1( + text, bytea, timestamptz, timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_api_reader_login, programmable_projector_login, + programmable_reconciler_login, + programmable_release_probe_nonce, + programmable_release_probe_nonce_login; + +grant usage on schema programmable_release_probe_private + to programmable_release_probe_nonce; +grant execute on function + programmable_release_probe_private.consume_release_probe_nonce_v1( + text, bytea, timestamptz, timestamptz + ) + to programmable_release_probe_nonce; + +reset role; diff --git a/supabase/migrations/20260731203900_projector_runtime_singleton_lease.sql b/supabase/migrations/20260731203900_projector_runtime_singleton_lease.sql new file mode 100644 index 00000000..366e8fb4 --- /dev/null +++ b/supabase/migrations/20260731203900_projector_runtime_singleton_lease.sql @@ -0,0 +1,478 @@ +-- Process-independent singleton fencing for the canonical projector runtime. +-- The caller never supplies a scope key: this migration owns the one production +-- scope, and every acquisition advances a monotonic fencing generation. + +reset role; + +do $bootstrap_projector_runtime_roles$ +begin + if not exists ( + select 1 from pg_catalog.pg_roles + where rolname = 'programmable_projector_runtime' + ) then + create role programmable_projector_runtime + nologin nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; + + if not exists ( + select 1 from pg_catalog.pg_roles + where rolname = 'programmable_projector_runtime_login' + ) then + create role programmable_projector_runtime_login + login password null nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; +end +$bootstrap_projector_runtime_roles$; + +alter role programmable_projector_runtime + nologin nocreatedb nocreaterole noinherit; +alter role programmable_projector_runtime_login + login password null nocreatedb nocreaterole noinherit; + +do $posture$ +begin + if exists ( + select 1 + from pg_catalog.pg_roles + where rolname = any (array[ + 'programmable_projector_runtime', + 'programmable_projector_runtime_login' + ]::name[]) + and (rolsuper or rolreplication or rolbypassrls) + ) then + raise exception 'programmable projector-runtime role posture is privileged'; + end if; +end +$posture$; + +grant programmable_projector_runtime to programmable_projector_runtime_login + with inherit false, set true; + +set role programmable_migrator; + +create table programmable_private.projector_runtime_lease_current ( + singleton_key text primary key + check (singleton_key = 'canonical-projector-runtime-v1'), + lease_generation bigint not null default 0 + check (lease_generation >= 0), + -- The bootstrap generation has no holder. The shared source_identifier + -- domain intentionally rejects NULL, so the current row uses text and + -- repeats the exact domain grammar in the active-generation constraint. + holder_id text, + lease_token_hash programmable_private.bytes32_value, + acquired_at timestamptz, + expires_at timestamptz, + released_at timestamptz, + acquisition_commitment programmable_private.bytes32_value, + release_commitment programmable_private.bytes32_value, + changed_by_audit_id uuid + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + ( + lease_generation = 0 + and holder_id is null + and lease_token_hash is null + and acquired_at is null + and expires_at is null + and released_at is null + and acquisition_commitment is null + and release_commitment is null + and changed_by_audit_id is null + ) + or + ( + lease_generation > 0 + and holder_id is not null + and pg_catalog.octet_length(holder_id) between 1 and 128 + and holder_id ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + and lease_token_hash is not null + and acquired_at is not null + and expires_at is not null + and acquisition_commitment is not null + and changed_by_audit_id is not null + and expires_at > acquired_at + and expires_at <= acquired_at + interval '90 seconds' + and lease_token_hash <> pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + and acquisition_commitment + <> pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + and ( + (released_at is null and release_commitment is null) + or ( + released_at is not null + and release_commitment is not null + and released_at >= acquired_at + and release_commitment + <> pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ) + ) + ) + ) +); + +create table programmable_private.projector_runtime_lease_history ( + lease_history_id uuid primary key, + singleton_key text not null + check (singleton_key = 'canonical-projector-runtime-v1'), + event_kind text not null check (event_kind in ('acquired', 'released')), + lease_generation bigint not null check (lease_generation > 0), + holder_id programmable_private.source_identifier not null, + lease_token_hash programmable_private.bytes32_value not null, + acquired_at timestamptz not null, + expires_at timestamptz not null, + event_at timestamptz not null, + input_commitment programmable_private.bytes32_value not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check (expires_at > acquired_at), + check (expires_at <= acquired_at + interval '90 seconds'), + check (event_at >= acquired_at), + check (lease_token_hash <> pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex')), + check (input_commitment <> pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex')), + unique (singleton_key, lease_generation, event_kind), + unique (audit_id) +); + +insert into programmable_private.projector_runtime_lease_current ( + singleton_key +) values ( + 'canonical-projector-runtime-v1' +); + +alter table programmable_private.projector_runtime_lease_current + enable row level security; +alter table programmable_private.projector_runtime_lease_current + force row level security; +create policy projector_runtime_lease_current_migrator_all + on programmable_private.projector_runtime_lease_current + for all to programmable_migrator using (true) with check (true); + +alter table programmable_private.projector_runtime_lease_history + enable row level security; +alter table programmable_private.projector_runtime_lease_history + force row level security; +create policy projector_runtime_lease_history_migrator_all + on programmable_private.projector_runtime_lease_history + for all to programmable_migrator using (true) with check (true); + +create trigger reject_immutable_mutation +before update or delete +on programmable_private.projector_runtime_lease_history +for each row execute function programmable_private.reject_immutable_mutation(); + +create function programmable_private.try_acquire_projector_runtime_lease_v1( + p_holder_id text, + p_lease_token_hash bytea, + p_acquired_at timestamptz, + p_expires_at timestamptz, + p_input_commitment bytea +) +returns table ( + acquired boolean, + lease_generation bigint, + acquired_at timestamptz, + expires_at timestamptz +) +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + current_lease programmable_private.projector_runtime_lease_current%rowtype; + server_now timestamptz := pg_catalog.clock_timestamp(); + requested_ttl interval; + next_generation bigint; + server_acquired_at timestamptz; + server_expires_at timestamptz; + acquisition_audit_id uuid; +begin + perform programmable_private.assert_caller( + 'programmable_projector_runtime' + ); + + requested_ttl := p_expires_at - p_acquired_at; + if p_holder_id is null + or pg_catalog.octet_length(p_holder_id) not between 1 and 128 + or p_holder_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or p_lease_token_hash is null + or pg_catalog.octet_length(p_lease_token_hash) <> 32 + or p_lease_token_hash = pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_input_commitment is null + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_input_commitment = pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_acquired_at is null + or p_expires_at is null + or requested_ttl <= interval '0 seconds' + or requested_ttl > interval '90 seconds' + or p_acquired_at < server_now - interval '30 seconds' + or p_acquired_at > server_now + interval '30 seconds' + then + raise exception using + errcode = '22023', + message = 'invalid projector runtime lease acquisition'; + end if; + + select lease.* into strict current_lease + from programmable_private.projector_runtime_lease_current as lease + where lease.singleton_key = 'canonical-projector-runtime-v1' + for update; + + if current_lease.lease_generation > 0 + and current_lease.released_at is null + and current_lease.expires_at > server_now + then + acquired := false; + lease_generation := current_lease.lease_generation; + acquired_at := current_lease.acquired_at; + expires_at := current_lease.expires_at; + return next; + return; + end if; + + next_generation := current_lease.lease_generation + 1; + server_acquired_at := server_now; + server_expires_at := server_now + requested_ttl; + acquisition_audit_id := programmable_private.append_mutation_audit( + 'projector_runtime_lease.acquire', + p_input_commitment, + null, + server_now + ); + + update programmable_private.projector_runtime_lease_current as lease + set lease_generation = next_generation, + holder_id = p_holder_id::programmable_private.source_identifier, + lease_token_hash = + p_lease_token_hash::programmable_private.bytes32_value, + acquired_at = server_acquired_at, + expires_at = server_expires_at, + released_at = null, + acquisition_commitment = + p_input_commitment::programmable_private.bytes32_value, + release_commitment = null, + changed_by_audit_id = acquisition_audit_id + where lease.singleton_key = 'canonical-projector-runtime-v1'; + + insert into programmable_private.projector_runtime_lease_history ( + lease_history_id, singleton_key, event_kind, lease_generation, + holder_id, lease_token_hash, acquired_at, expires_at, event_at, + input_commitment, audit_id + ) values ( + pg_catalog.gen_random_uuid(), 'canonical-projector-runtime-v1', + 'acquired', next_generation, + p_holder_id::programmable_private.source_identifier, + p_lease_token_hash::programmable_private.bytes32_value, + server_acquired_at, server_expires_at, server_now, + p_input_commitment::programmable_private.bytes32_value, + acquisition_audit_id + ); + + acquired := true; + lease_generation := next_generation; + acquired_at := server_acquired_at; + expires_at := server_expires_at; + return next; +end +$function$; + +create function programmable_private.assert_projector_runtime_lease_v1( + p_holder_id text, + p_lease_generation bigint, + p_lease_token_hash bytea +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + active_caller name := programmable_private.caller_role_name(); + current_lease programmable_private.projector_runtime_lease_current%rowtype; +begin + if active_caller not in ( + 'programmable_projector_runtime'::name, + 'programmable_projector'::name + ) then + raise exception using + errcode = '42501', + message = 'function requires projector lease assertion capability'; + end if; + + if p_holder_id is null + or pg_catalog.octet_length(p_holder_id) not between 1 and 128 + or p_holder_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or p_lease_generation is null + or p_lease_generation <= 0 + or p_lease_token_hash is null + or pg_catalog.octet_length(p_lease_token_hash) <> 32 + or p_lease_token_hash = pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + then + raise exception using + errcode = '22023', + message = 'invalid projector runtime lease assertion'; + end if; + + select lease.* into strict current_lease + from programmable_private.projector_runtime_lease_current as lease + where lease.singleton_key = 'canonical-projector-runtime-v1' + for update; + + return current_lease.lease_generation = p_lease_generation + and current_lease.holder_id = p_holder_id + and current_lease.lease_token_hash = p_lease_token_hash + and current_lease.released_at is null + and current_lease.expires_at > pg_catalog.clock_timestamp(); +end +$function$; + +comment on function + programmable_private.assert_projector_runtime_lease_v1( + text, bigint, bytea + ) is + 'Must execute on the same connection and inside the same transaction as projector stage and promote writes; its FOR UPDATE lock fences takeover until commit.'; + +create function programmable_private.release_projector_runtime_lease_v1( + p_holder_id text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_released_at timestamptz, + p_input_commitment bytea +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + current_lease programmable_private.projector_runtime_lease_current%rowtype; + server_now timestamptz := pg_catalog.clock_timestamp(); + release_audit_id uuid; +begin + perform programmable_private.assert_caller( + 'programmable_projector_runtime' + ); + + if p_holder_id is null + or pg_catalog.octet_length(p_holder_id) not between 1 and 128 + or p_holder_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or p_lease_generation is null + or p_lease_generation <= 0 + or p_lease_token_hash is null + or pg_catalog.octet_length(p_lease_token_hash) <> 32 + or p_lease_token_hash = pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_released_at is null + or p_released_at < server_now - interval '30 seconds' + or p_released_at > server_now + interval '30 seconds' + or p_input_commitment is null + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_input_commitment = pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + then + raise exception using + errcode = '22023', + message = 'invalid projector runtime lease release'; + end if; + + select lease.* into strict current_lease + from programmable_private.projector_runtime_lease_current as lease + where lease.singleton_key = 'canonical-projector-runtime-v1' + for update; + + if current_lease.lease_generation <> p_lease_generation + or current_lease.holder_id <> p_holder_id + or current_lease.lease_token_hash <> p_lease_token_hash + or current_lease.released_at is not null + then + return false; + end if; + + release_audit_id := programmable_private.append_mutation_audit( + 'projector_runtime_lease.release', + p_input_commitment, + null, + server_now + ); + + update programmable_private.projector_runtime_lease_current as lease + set released_at = server_now, + release_commitment = + p_input_commitment::programmable_private.bytes32_value, + changed_by_audit_id = release_audit_id + where lease.singleton_key = 'canonical-projector-runtime-v1' + and lease.lease_generation = p_lease_generation + and lease.holder_id = p_holder_id + and lease.lease_token_hash = p_lease_token_hash + and lease.released_at is null; + + if not found then + raise exception using + errcode = '40001', + message = 'projector runtime lease release CAS lost'; + end if; + + insert into programmable_private.projector_runtime_lease_history ( + lease_history_id, singleton_key, event_kind, lease_generation, + holder_id, lease_token_hash, acquired_at, expires_at, event_at, + input_commitment, audit_id + ) values ( + pg_catalog.gen_random_uuid(), 'canonical-projector-runtime-v1', + 'released', current_lease.lease_generation, + current_lease.holder_id, current_lease.lease_token_hash, + current_lease.acquired_at, current_lease.expires_at, server_now, + p_input_commitment::programmable_private.bytes32_value, + release_audit_id + ); + + return true; +end +$function$; + +revoke all on table + programmable_private.projector_runtime_lease_current, + programmable_private.projector_runtime_lease_history +from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_projector, + programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +revoke all on function + programmable_private.try_acquire_projector_runtime_lease_v1( + text, bytea, timestamptz, timestamptz, bytea + ), + programmable_private.assert_projector_runtime_lease_v1( + text, bigint, bytea + ), + programmable_private.release_projector_runtime_lease_v1( + text, bigint, bytea, timestamptz, bytea + ) +from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_projector, + programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant usage on schema programmable_private + to programmable_projector_runtime; + +grant execute on function + programmable_private.try_acquire_projector_runtime_lease_v1( + text, bytea, timestamptz, timestamptz, bytea + ), + programmable_private.release_projector_runtime_lease_v1( + text, bigint, bytea, timestamptz, bytea + ) +to programmable_projector_runtime; + +grant execute on function + programmable_private.assert_projector_runtime_lease_v1( + text, bigint, bytea + ) +to programmable_projector_runtime, programmable_projector; + +reset role; diff --git a/supabase/migrations/20260731222000_reconciler_preparity_contract.sql b/supabase/migrations/20260731222000_reconciler_preparity_contract.sql new file mode 100644 index 00000000..f9c2a623 --- /dev/null +++ b/supabase/migrations/20260731222000_reconciler_preparity_contract.sql @@ -0,0 +1,610 @@ +-- Exact-checkpoint bootstrap contract for the server-only reconciler. +-- +-- The reconciler intentionally receives no general table privileges. One +-- narrow reader exposes only the exact current checkpoint, its applicable +-- route bindings, the immutable projection fold manifest and the current +-- entity identities needed to prepare an independent comparison. One writer +-- appends the reconciliation, all route parity rows, exact checkpoint +-- bindings and the terminal outcome in a single transaction. + +reset role; +set role programmable_migrator; + +create function programmable_private.get_reconciler_preparity_contract_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_checkpoint_id uuid, + p_checkpoint_block_number numeric, + p_checkpoint_block_hash bytea, + p_maximum_entity_count integer default 10000 +) +returns table ( + chain_id bigint, + release_id text, + model_id text, + source_group text, + projector_version text, + epoch_id uuid, + pointer_generation bigint, + checkpoint_id uuid, + checkpoint_generation bigint, + reorg_generation bigint, + checkpoint_block_number bigint, + checkpoint_block_hash bytea, + route_keys text[], + route_contract jsonb, + projection_contract jsonb, + current_entities jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + expected_route_keys text[]; + checkpoint programmable_private.projector_checkpoints%rowtype; + manifest programmable_private.projection_fold_manifests%rowtype; + resolved_route_keys text[]; + resolved_route_contract jsonb; + resolved_entities jsonb; + entity_count bigint; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_chain_id is null + or p_chain_id <= 0 + or p_release_id is null + or p_model_id is null + or p_source_group is null + or p_epoch_id is null + or p_pointer_generation is null + or p_pointer_generation <= 0 + or p_checkpoint_id is null + or p_checkpoint_block_number is null + or p_checkpoint_block_number <> pg_catalog.trunc( + p_checkpoint_block_number + ) + or p_checkpoint_block_number < 0 + or p_checkpoint_block_number > 9223372036854775807 + or p_checkpoint_block_hash is null + or pg_catalog.octet_length(p_checkpoint_block_hash) <> 32 + or p_maximum_entity_count is null + or p_maximum_entity_count < 1 + or p_maximum_entity_count > 10000 + then + raise exception using + errcode = '22023', + message = 'invalid reconciler pre-parity checkpoint request'; + end if; + + expected_route_keys := case + when p_release_id = 'classic-v2' and p_model_id = 'classic' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile' + ]::text[] + when p_release_id = 'classic-v3' and p_model_id = 'classic' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile', + 'classic-v3-profile', 'launch-lookup' + ]::text[] + when p_release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and p_model_id = 'stock-paired' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile', + 'launch-lookup' + ]::text[] + else null + end; + if expected_route_keys is null then + raise exception using + errcode = '0A000', + message = 'reconciler release and model are not supported'; + end if; + + select checkpoint_row.* into checkpoint + from programmable_private.projector_checkpoints as checkpoint_row + join programmable_private.projector_checkpoint_current as current_checkpoint + on current_checkpoint.chain_id = checkpoint_row.chain_id + and current_checkpoint.release_id = checkpoint_row.release_id + and current_checkpoint.model_id = checkpoint_row.model_id + and current_checkpoint.source_group = checkpoint_row.source_group + and current_checkpoint.projector_version = + checkpoint_row.projector_version + and current_checkpoint.checkpoint_id = checkpoint_row.checkpoint_id + and current_checkpoint.checkpoint_generation = + checkpoint_row.checkpoint_generation + and current_checkpoint.reorg_generation = checkpoint_row.reorg_generation + join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = checkpoint_row.chain_id + and current_epoch.release_id = checkpoint_row.release_id + and current_epoch.model_id = checkpoint_row.model_id + and current_epoch.source_group = checkpoint_row.source_group + and current_epoch.epoch_id = checkpoint_row.epoch_id + and current_epoch.generation = checkpoint_row.pointer_generation + where checkpoint_row.chain_id = p_chain_id + and checkpoint_row.release_id = p_release_id + and checkpoint_row.model_id = p_model_id + and checkpoint_row.source_group = p_source_group + and checkpoint_row.epoch_id = p_epoch_id + and checkpoint_row.pointer_generation = p_pointer_generation + and checkpoint_row.checkpoint_id = p_checkpoint_id + and checkpoint_row.block_number = p_checkpoint_block_number::bigint + and checkpoint_row.block_hash = p_checkpoint_block_hash; + if not found then + raise exception using + errcode = '55000', + message = 'requested checkpoint is not exact and current'; + end if; + + select + pg_catalog.array_agg( + route.route_key::text order by expected.ordinal + ), + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'routeKey', route.route_key::text, + 'status', route.status::text, + 'routeMode', route.route_mode::text, + 'checkpointId', route.checkpoint_id, + 'checkpointGeneration', route.checkpoint_generation::text, + 'reorgGeneration', route.reorg_generation::text, + 'checkpointBlockNumber', route.checkpoint_block_number::text, + 'checkpointBlockHash', '0x' || pg_catalog.encode( + route.checkpoint_block_hash, 'hex' + ), + 'changedAt', pg_catalog.to_char( + route.changed_at at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ) + ) order by expected.ordinal + ) + into resolved_route_keys, resolved_route_contract + from pg_catalog.unnest(expected_route_keys) with ordinality + as expected(route_key, ordinal) + join programmable_private.route_eligibility_current_exact_v1 as route + on route.route_key = expected.route_key + and route.chain_id = checkpoint.chain_id + and route.release_id = checkpoint.release_id + and route.model_id = checkpoint.model_id + and route.source_group = checkpoint.source_group + and route.epoch_id = checkpoint.epoch_id + and route.pointer_generation = checkpoint.pointer_generation + and route.checkpoint_id = checkpoint.checkpoint_id + and route.projector_version = checkpoint.projector_version + and route.checkpoint_generation = checkpoint.checkpoint_generation + and route.reorg_generation = checkpoint.reorg_generation + and route.checkpoint_block_number = checkpoint.block_number + and route.checkpoint_block_hash = checkpoint.block_hash + and route.status = 'eligible' + and route.route_mode = 'indexed'; + if resolved_route_keys is distinct from expected_route_keys then + raise exception using + errcode = '55000', + message = 'exact checkpoint does not cover every reconciler route'; + end if; + + select manifest_row.* into manifest + from programmable_private.projection_fold_manifests as manifest_row + where manifest_row.run_id = checkpoint.run_id + and manifest_row.epoch_id = checkpoint.epoch_id + and manifest_row.pointer_generation = checkpoint.pointer_generation + and manifest_row.target_block_number = checkpoint.block_number + and manifest_row.target_block_hash = checkpoint.block_hash; + if not found then + raise exception using + errcode = '55000', + message = 'exact checkpoint projection manifest is unavailable'; + end if; + + select pg_catalog.count(*) into entity_count + from programmable_private.projection_entity_current as entity + where entity.chain_id = checkpoint.chain_id + and entity.release_id = checkpoint.release_id + and entity.model_id = checkpoint.model_id + and entity.source_group = checkpoint.source_group; + if entity_count > p_maximum_entity_count then + raise exception using + errcode = '54000', + message = 'reconciler pre-parity entity limit exceeded'; + end if; + + select coalesce( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'entityKind', entity.entity_kind::text, + 'entityKey', entity.entity_key, + 'projectionRowId', entity.projection_row_id, + 'projectionRunId', entity.projection_run_id, + 'publicationId', entity.publication_id, + 'checkpointId', entity.checkpoint_id, + 'promotedBlockNumber', entity.promoted_block_number::text, + 'promotedBlockHash', '0x' || pg_catalog.encode( + entity.promoted_block_hash, 'hex' + ) + ) order by entity.entity_kind, entity.entity_key + ), + '[]'::jsonb + ) into resolved_entities + from programmable_private.projection_entity_current as entity + where entity.chain_id = checkpoint.chain_id + and entity.release_id = checkpoint.release_id + and entity.model_id = checkpoint.model_id + and entity.source_group = checkpoint.source_group; + + return query select + checkpoint.chain_id::bigint, + checkpoint.release_id::text, + checkpoint.model_id::text, + checkpoint.source_group::text, + checkpoint.projector_version::text, + checkpoint.epoch_id, + checkpoint.pointer_generation, + checkpoint.checkpoint_id, + checkpoint.checkpoint_generation, + checkpoint.reorg_generation, + checkpoint.block_number::bigint, + checkpoint.block_hash::bytea, + resolved_route_keys, + resolved_route_contract, + pg_catalog.jsonb_build_object( + 'runId', manifest.run_id, + 'targetBlockNumber', manifest.target_block_number::text, + 'targetBlockHash', '0x' || pg_catalog.encode( + manifest.target_block_hash, 'hex' + ), + 'orderedOccurrenceIds', manifest.ordered_occurrence_ids, + 'orderedAllocationFactIds', manifest.ordered_allocation_fact_ids, + 'orderedAllocationEvidenceIds', + manifest.ordered_allocation_evidence_ids, + 'orderedCandidateDispositionIds', + manifest.ordered_candidate_disposition_ids, + 'orderedRouteKeys', manifest.ordered_route_keys, + 'orderedProjectionRows', manifest.ordered_projection_rows, + 'projectionRowCount', manifest.projection_row_count::text, + 'resultCommitment', '0x' || pg_catalog.encode( + manifest.result_commitment, 'hex' + ) + ), + resolved_entities; +end +$function$; + +create function programmable_private.commit_reconciler_preparity_result_v1( + p_run_id uuid, + p_reconciliation_id uuid, + p_parity_record_ids uuid[], + p_parity_binding_ids uuid[], + p_outcome_id uuid, + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_checkpoint_id uuid, + p_checkpoint_block_number numeric, + p_checkpoint_block_hash bytea, + p_worker_version text, + p_route_keys text[], + p_legacy_dto_hashes bytea[], + p_indexed_dto_hashes bytea[], + p_route_evidence_commitments bytea[], + p_parity_binding_commitments bytea[], + p_request_commitment bytea, + p_reconciliation_evidence_commitment bytea, + p_result_commitment bytea, + p_started_at timestamptz, + p_compared_at timestamptz, + p_finished_at timestamptz +) +returns jsonb +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + expected_route_keys text[]; + expected_route_count integer; + route_index integer; + mismatch_count bigint := 0; + mismatch_commitments bytea[] := array[]::bytea[]; + terminal_status text; + locked_route_count bigint; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + expected_route_keys := case + when p_release_id = 'classic-v2' and p_model_id = 'classic' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile' + ]::text[] + when p_release_id = 'classic-v3' and p_model_id = 'classic' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile', + 'classic-v3-profile', 'launch-lookup' + ]::text[] + when p_release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and p_model_id = 'stock-paired' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile', + 'launch-lookup' + ]::text[] + else null + end; + expected_route_count := pg_catalog.cardinality(expected_route_keys); + if p_run_id is null + or p_reconciliation_id is null + or p_outcome_id is null + or p_chain_id is null + or p_chain_id <= 0 + or p_release_id is null + or p_model_id is null + or p_source_group is null + or p_epoch_id is null + or p_pointer_generation is null + or p_pointer_generation <= 0 + or p_checkpoint_id is null + or p_checkpoint_block_number is null + or p_checkpoint_block_number <> pg_catalog.trunc( + p_checkpoint_block_number + ) + or p_checkpoint_block_number < 0 + or p_checkpoint_block_number > 9223372036854775807 + or p_checkpoint_block_hash is null + or pg_catalog.octet_length(p_checkpoint_block_hash) <> 32 + or p_worker_version is null + or p_request_commitment is null + or pg_catalog.octet_length(p_request_commitment) <> 32 + or p_reconciliation_evidence_commitment is null + or pg_catalog.octet_length( + p_reconciliation_evidence_commitment + ) <> 32 + or p_result_commitment is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + or p_started_at is null + or p_compared_at is null + or p_finished_at is null + or p_started_at > p_compared_at + or p_compared_at > p_finished_at + or expected_route_keys is null + or p_route_keys is distinct from expected_route_keys + or pg_catalog.cardinality(p_parity_record_ids) + is distinct from expected_route_count + or pg_catalog.cardinality(p_parity_binding_ids) + is distinct from expected_route_count + or pg_catalog.cardinality(p_legacy_dto_hashes) + is distinct from expected_route_count + or pg_catalog.cardinality(p_indexed_dto_hashes) + is distinct from expected_route_count + or pg_catalog.cardinality(p_route_evidence_commitments) + is distinct from expected_route_count + or pg_catalog.cardinality(p_parity_binding_commitments) + is distinct from expected_route_count + or exists ( + select 1 + from pg_catalog.unnest( + array[p_run_id, p_reconciliation_id, p_outcome_id] + || p_parity_record_ids || p_parity_binding_ids + ) as supplied_id(value) + where supplied_id.value is null + ) + or ( + select pg_catalog.count(distinct supplied_id.value) + from pg_catalog.unnest( + array[p_run_id, p_reconciliation_id, p_outcome_id] + || p_parity_record_ids || p_parity_binding_ids + ) as supplied_id(value) + ) <> (3 + expected_route_count * 2) + or exists ( + select 1 + from pg_catalog.unnest( + p_legacy_dto_hashes || p_indexed_dto_hashes + || p_route_evidence_commitments + || p_parity_binding_commitments + ) as supplied_hash(value) + where supplied_hash.value is null + or pg_catalog.octet_length(supplied_hash.value) <> 32 + ) + then + raise exception using + errcode = '22023', + message = 'invalid atomic reconciler pre-parity result'; + end if; + + -- Hold the mutable current pointers until the complete append commits. + perform 1 + from programmable_private.release_epoch_current as current_epoch + where current_epoch.chain_id = p_chain_id + and current_epoch.release_id = p_release_id + and current_epoch.model_id = p_model_id + and current_epoch.source_group = p_source_group + and current_epoch.epoch_id = p_epoch_id + and current_epoch.generation = p_pointer_generation + for share; + if not found then + raise exception using + errcode = '40001', + message = 'reconciler epoch changed before commit'; + end if; + + perform 1 + from programmable_private.projector_checkpoint_current as current_checkpoint + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_checkpoint.checkpoint_id + and checkpoint.chain_id = current_checkpoint.chain_id + and checkpoint.release_id = current_checkpoint.release_id + and checkpoint.model_id = current_checkpoint.model_id + and checkpoint.source_group = current_checkpoint.source_group + and checkpoint.projector_version = current_checkpoint.projector_version + and checkpoint.checkpoint_generation = + current_checkpoint.checkpoint_generation + and checkpoint.reorg_generation = current_checkpoint.reorg_generation + where checkpoint.chain_id = p_chain_id + and checkpoint.release_id = p_release_id + and checkpoint.model_id = p_model_id + and checkpoint.source_group = p_source_group + and checkpoint.epoch_id = p_epoch_id + and checkpoint.pointer_generation = p_pointer_generation + and checkpoint.checkpoint_id = p_checkpoint_id + and checkpoint.block_number = p_checkpoint_block_number::bigint + and checkpoint.block_hash = p_checkpoint_block_hash + for share of current_checkpoint, checkpoint; + if not found then + raise exception using + errcode = '40001', + message = 'reconciler checkpoint changed before commit'; + end if; + + select pg_catalog.count(*) into locked_route_count + from programmable_private.route_eligibility_current as route + join pg_catalog.unnest(expected_route_keys) as expected(route_key) + on expected.route_key = route.route_key + where route.chain_id = p_chain_id + and route.release_id = p_release_id + and route.model_id = p_model_id + and route.source_group = p_source_group + and route.epoch_id = p_epoch_id + and route.pointer_generation = p_pointer_generation + and route.checkpoint_id = p_checkpoint_id + and route.status = 'eligible' + and route.route_mode = 'indexed'; + if locked_route_count <> expected_route_count then + raise exception using + errcode = '40001', + message = 'reconciler route coverage changed before commit'; + end if; + + for route_index in 1..expected_route_count loop + if p_legacy_dto_hashes[route_index] + <> p_indexed_dto_hashes[route_index] + then + mismatch_count := mismatch_count + 1; + mismatch_commitments := pg_catalog.array_append( + mismatch_commitments, + p_route_evidence_commitments[route_index] + ); + end if; + end loop; + terminal_status := case + when mismatch_count = 0 then 'succeeded' + else 'failed' + end; + + perform programmable_private.open_run( + p_run_id, + 'reconciliation', + p_chain_id, + p_release_id, + p_model_id, + p_source_group, + p_epoch_id, + p_pointer_generation, + p_worker_version, + p_request_commitment, + p_started_at + ); + perform programmable_private.append_reconciliation_record( + p_reconciliation_id, + p_run_id, + 'exact-checkpoint-route-parity-v1', + case when mismatch_count = 0 then 'info' else 'warning' end, + p_checkpoint_block_number, + p_checkpoint_block_number, + expected_route_count, + mismatch_count, + p_reconciliation_evidence_commitment, + mismatch_commitments, + null, + p_compared_at + ); + + for route_index in 1..expected_route_count loop + perform programmable_private.append_parity_record( + p_parity_record_ids[route_index], + p_reconciliation_id, + p_route_keys[route_index], + p_legacy_dto_hashes[route_index], + p_indexed_dto_hashes[route_index], + p_compared_at, + null + ); + perform programmable_private.bind_route_checkpoint_parity_v1( + p_parity_binding_ids[route_index], + p_parity_record_ids[route_index], + p_checkpoint_id, + p_parity_binding_commitments[route_index], + p_finished_at + ); + end loop; + + perform programmable_private.append_run_outcome( + p_outcome_id, + p_run_id, + terminal_status, + p_result_commitment, + p_finished_at + ); + + return pg_catalog.jsonb_build_object( + 'runId', p_run_id, + 'reconciliationId', p_reconciliation_id, + 'checkpointId', p_checkpoint_id, + 'checkpointBlockNumber', p_checkpoint_block_number::bigint::text, + 'checkpointBlockHash', '0x' || pg_catalog.encode( + p_checkpoint_block_hash, 'hex' + ), + 'routeCount', expected_route_count, + 'mismatchCount', mismatch_count, + 'status', terminal_status + ); +end +$function$; + +comment on function programmable_private.get_reconciler_preparity_contract_v1( + bigint, text, text, text, uuid, bigint, uuid, numeric, bytea, integer +) is + 'Returns only the exact current checkpoint contract required for independent pre-parity comparison. It does not require or manufacture prior parity.'; + +comment on function programmable_private.commit_reconciler_preparity_result_v1( + uuid, uuid, uuid[], uuid[], uuid, bigint, text, text, text, uuid, bigint, + uuid, numeric, bytea, text, text[], bytea[], bytea[], bytea[], bytea[], + bytea, bytea, bytea, timestamptz, timestamptz, timestamptz +) is + 'Atomically appends exact-checkpoint reconciliation, six route parity rows, their checkpoint bindings and one terminal outcome.'; + +revoke all on function + programmable_private.get_reconciler_preparity_contract_v1( + bigint, text, text, text, uuid, bigint, uuid, numeric, bytea, integer + ) from public, anon, authenticated, service_role, + programmable_projector, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; +revoke all on function + programmable_private.commit_reconciler_preparity_result_v1( + uuid, uuid, uuid[], uuid[], uuid, bigint, text, text, text, uuid, bigint, + uuid, numeric, bytea, text, text[], bytea[], bytea[], bytea[], bytea[], + bytea, bytea, bytea, timestamptz, timestamptz, timestamptz + ) from public, anon, authenticated, service_role, + programmable_projector, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function + programmable_private.get_reconciler_preparity_contract_v1( + bigint, text, text, text, uuid, bigint, uuid, numeric, bytea, integer + ) to programmable_reconciler; + +grant execute on function + programmable_private.commit_reconciler_preparity_result_v1( + uuid, uuid, uuid[], uuid[], uuid, bigint, text, text, text, uuid, bigint, + uuid, numeric, bytea, text, text[], bytea[], bytea[], bytea[], bytea[], + bytea, bytea, bytea, timestamptz, timestamptz, timestamptz + ) to programmable_reconciler; + +reset role; diff --git a/supabase/migrations/20260731223000_market_projector_contract.sql b/supabase/migrations/20260731223000_market_projector_contract.sql new file mode 100644 index 00000000..61da2b45 --- /dev/null +++ b/supabase/migrations/20260731223000_market_projector_contract.sql @@ -0,0 +1,3396 @@ +-- Exact control-plane contract for the server-only market projector. +-- Market values remain append-only reconciliation facts. This migration adds +-- only the narrow discovery, anchor-resolution, and CAS cursor surfaces that +-- are required to project them without direct table access or guessed IDs. + +set role programmable_migrator; + +-- Market evidence is append-only per reconciliation lineage. A source epoch +-- or pointer transition may legitimately observe the same canonical pool fact +-- again. Global ETH/USD snapshots remain singular per epoch, pointer, and +-- canonical block because consumers resolve exactly one snapshot for that +-- context. +alter table programmable_private.global_eth_usd_snapshots + drop constraint global_eth_usd_snapshots_epoch_id_result_commitment_key, + add constraint global_eth_usd_snapshots_pointer_result_key + unique (epoch_id, pointer_generation, result_commitment); + +alter table programmable_private.market_snapshots + drop constraint market_snapshots_chain_id_pool_id_source_deployment_id_bloc_key, + add constraint market_snapshots_reconciliation_fact_key + unique ( + chain_id, pool_id, source_deployment_id, block_hash, reconciliation_id + ); + +alter table programmable_private.market_candles + drop constraint market_candles_chain_id_pool_id_interval_period_start_sourc_key, + add constraint market_candles_reconciliation_fact_key + unique ( + chain_id, pool_id, interval, period_start, source_block_hash, + reconciliation_id + ); + +alter table programmable_private.market_block_closes + drop constraint market_block_closes_chain_id_pool_id_block_hash_key, + drop constraint market_block_closes_epoch_id_close_commitment_key, + add constraint market_block_closes_reconciliation_block_key + unique (chain_id, pool_id, block_hash, reconciliation_id), + add constraint market_block_closes_reconciliation_commitment_key + unique (epoch_id, close_commitment, reconciliation_id); + +-- Pool discovery is event-driven. This index prevents an unrelated release +-- event from turning every launched pool into pending market work. +create index chain_event_materializations_market_pool_idx + on programmable_private.chain_event_occurrence_materializations ( + chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, + (pg_catalog.lower(decoded_payload ->> 'poolId')), + occurrence_id + ) + where event_type in ('NativeSwapFeesAccrued', 'QuoteSwapFeesAccrued'); + +create function programmable_private.is_market_fee_event_v1( + p_model_id text, + p_event_type text +) +returns boolean +language sql +immutable +strict +parallel safe +set search_path = '' +as $function$ + select case p_model_id + when 'classic' then p_event_type = 'NativeSwapFeesAccrued' + when 'stock-paired' then p_event_type = 'QuoteSwapFeesAccrued' + else false + end +$function$; + +create table programmable_private.market_projector_runtime_lease_current ( + singleton_key text primary key + check (singleton_key = 'canonical-market-projector-runtime-v1'), + lease_generation bigint not null default 0 + check (lease_generation >= 0), + holder_id text, + lease_token_hash programmable_private.bytes32_value, + acquired_at timestamptz, + expires_at timestamptz, + released_at timestamptz, + acquisition_commitment programmable_private.bytes32_value, + release_commitment programmable_private.bytes32_value, + changed_by_audit_id uuid + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + ( + lease_generation = 0 + and holder_id is null + and lease_token_hash is null + and acquired_at is null + and expires_at is null + and released_at is null + and acquisition_commitment is null + and release_commitment is null + and changed_by_audit_id is null + ) + or + ( + lease_generation > 0 + and holder_id is not null + and pg_catalog.octet_length(holder_id) between 1 and 128 + and holder_id ~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + and lease_token_hash is not null + and acquired_at is not null + and expires_at is not null + and expires_at > acquired_at + and expires_at <= acquired_at + interval '90 seconds' + and acquisition_commitment is not null + and changed_by_audit_id is not null + and lease_token_hash <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + and acquisition_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + and ( + (released_at is null and release_commitment is null) + or ( + released_at is not null + and released_at >= acquired_at + and release_commitment is not null + and release_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ) + ) + ) + ) +); + +create table programmable_private.market_projector_runtime_lease_history ( + lease_history_id uuid primary key, + singleton_key text not null + check (singleton_key = 'canonical-market-projector-runtime-v1'), + event_kind text not null check (event_kind in ('acquired', 'released')), + lease_generation bigint not null check (lease_generation > 0), + holder_id programmable_private.source_identifier not null, + lease_token_hash programmable_private.bytes32_value not null, + acquired_at timestamptz not null, + expires_at timestamptz not null, + event_at timestamptz not null, + input_commitment programmable_private.bytes32_value not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check (expires_at > acquired_at), + check (expires_at <= acquired_at + interval '90 seconds'), + check (event_at >= acquired_at), + unique (singleton_key, lease_generation, event_kind), + unique (audit_id) +); + +insert into programmable_private.market_projector_runtime_lease_current ( + singleton_key +) values ('canonical-market-projector-runtime-v1'); + +create table programmable_private.market_projector_cursor_history ( + market_cursor_id uuid primary key, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + pool_id programmable_private.bytes32_value not null, + epoch_id uuid not null, + pointer_generation bigint not null check (pointer_generation > 0), + cursor_generation bigint not null check (cursor_generation > 0), + reorg_generation bigint not null check (reorg_generation >= 0), + source_checkpoint_id uuid not null + references programmable_private.projector_checkpoints(checkpoint_id) + on delete restrict, + source_checkpoint_generation bigint not null + check (source_checkpoint_generation > 0), + source_reorg_generation bigint not null + check (source_reorg_generation >= 0), + block_evidence_id uuid not null, + block_number programmable_private.block_number_value not null, + block_hash programmable_private.bytes32_value not null, + provider_cursor text not null, + hour_coverage_end timestamptz, + day_coverage_end timestamptz, + page_commitment programmable_private.bytes32_value not null, + reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + advanced_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + foreign key (epoch_id, chain_id, release_id, model_id, source_group) + references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key (block_evidence_id, block_hash) + references programmable_private.dual_rpc_block_evidence( + block_evidence_id, agreed_block_hash + ) on delete restrict, + check ( + pg_catalog.octet_length(provider_cursor) between 1 and 256 + and provider_cursor ~ '^[A-Za-z0-9][A-Za-z0-9._:/#-]*$' + ), + check ( + page_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ), + check ( + hour_coverage_end is null + or hour_coverage_end = pg_catalog.date_trunc('hour', hour_coverage_end) + ), + check ( + day_coverage_end is null + or day_coverage_end = pg_catalog.date_trunc('day', day_coverage_end) + ), + unique ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, cursor_generation + ), + unique (epoch_id, pool_id, page_commitment) +); + +create table programmable_private.market_projector_cursor_current ( + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + pool_id programmable_private.bytes32_value not null, + market_cursor_id uuid not null unique + references programmable_private.market_projector_cursor_history( + market_cursor_id + ) on delete restrict, + cursor_generation bigint not null check (cursor_generation > 0), + reorg_generation bigint not null check (reorg_generation >= 0), + changed_at timestamptz not null, + changed_by_audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + primary key ( + chain_id, release_id, model_id, source_group, projector_version, pool_id + ) +); + +create table programmable_private.market_snapshot_lineage_memberships ( + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + pool_id programmable_private.bytes32_value not null, + reorg_generation bigint not null check (reorg_generation >= 0), + market_snapshot_id uuid not null + references programmable_private.market_snapshots(market_snapshot_id) + on delete restrict, + attached_reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + attached_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + primary key ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, reorg_generation, market_snapshot_id + ) +); + +create table programmable_private.market_candle_lineage_memberships ( + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + pool_id programmable_private.bytes32_value not null, + reorg_generation bigint not null check (reorg_generation >= 0), + market_candle_id uuid not null + references programmable_private.market_candles(market_candle_id) + on delete restrict, + attached_reconciliation_id uuid not null + references programmable_private.reconciliation_records(reconciliation_id) + on delete restrict, + attached_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + primary key ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, reorg_generation, market_candle_id + ) +); + +alter table programmable_private.market_projector_cursor_history + enable row level security; +alter table programmable_private.market_projector_cursor_history + force row level security; +create policy market_projector_cursor_history_migrator_all + on programmable_private.market_projector_cursor_history + for all to programmable_migrator using (true) with check (true); + +alter table programmable_private.market_projector_cursor_current + enable row level security; +alter table programmable_private.market_projector_cursor_current + force row level security; +create policy market_projector_cursor_current_migrator_all + on programmable_private.market_projector_cursor_current + for all to programmable_migrator using (true) with check (true); + +alter table programmable_private.market_snapshot_lineage_memberships + enable row level security; +alter table programmable_private.market_snapshot_lineage_memberships + force row level security; +create policy market_snapshot_lineage_memberships_migrator_all + on programmable_private.market_snapshot_lineage_memberships + for all to programmable_migrator using (true) with check (true); + +alter table programmable_private.market_candle_lineage_memberships + enable row level security; +alter table programmable_private.market_candle_lineage_memberships + force row level security; +create policy market_candle_lineage_memberships_migrator_all + on programmable_private.market_candle_lineage_memberships + for all to programmable_migrator using (true) with check (true); + +alter table programmable_private.market_projector_runtime_lease_current + enable row level security; +alter table programmable_private.market_projector_runtime_lease_current + force row level security; +create policy market_projector_runtime_lease_current_migrator_all + on programmable_private.market_projector_runtime_lease_current + for all to programmable_migrator using (true) with check (true); + +alter table programmable_private.market_projector_runtime_lease_history + enable row level security; +alter table programmable_private.market_projector_runtime_lease_history + force row level security; +create policy market_projector_runtime_lease_history_migrator_all + on programmable_private.market_projector_runtime_lease_history + for all to programmable_migrator using (true) with check (true); + +create trigger reject_immutable_mutation +before update or delete +on programmable_private.market_projector_runtime_lease_history +for each row execute function programmable_private.reject_immutable_mutation(); + +create function + programmable_private.try_acquire_market_projector_runtime_lease_v1( + p_holder_id text, + p_lease_token_hash bytea, + p_acquired_at timestamptz, + p_expires_at timestamptz, + p_input_commitment bytea + ) +returns table ( + acquired boolean, + lease_generation bigint, + acquired_at timestamptz, + expires_at timestamptz +) +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + current_lease + programmable_private.market_projector_runtime_lease_current%rowtype; + server_now timestamptz := pg_catalog.clock_timestamp(); + requested_ttl interval := p_expires_at - p_acquired_at; + next_generation bigint; + server_expires_at timestamptz; + acquisition_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_holder_id is null + or pg_catalog.octet_length(p_holder_id) not between 1 and 128 + or p_holder_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or p_lease_token_hash is null + or pg_catalog.octet_length(p_lease_token_hash) <> 32 + or p_lease_token_hash = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_input_commitment is null + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_input_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_acquired_at is null + or p_expires_at is null + or requested_ttl <= interval '0 seconds' + or requested_ttl > interval '90 seconds' + or p_acquired_at < server_now - interval '30 seconds' + or p_acquired_at > server_now + interval '30 seconds' + then + raise exception using errcode = '22023', + message = 'invalid market projector lease acquisition'; + end if; + + select lease.* into strict current_lease + from programmable_private.market_projector_runtime_lease_current as lease + where lease.singleton_key = 'canonical-market-projector-runtime-v1' + for update; + if current_lease.lease_generation > 0 + and current_lease.released_at is null + and current_lease.expires_at > server_now + then + acquired := false; + lease_generation := current_lease.lease_generation; + acquired_at := current_lease.acquired_at; + expires_at := current_lease.expires_at; + return next; + return; + end if; + + next_generation := current_lease.lease_generation + 1; + server_expires_at := server_now + requested_ttl; + acquisition_audit_id := programmable_private.append_mutation_audit( + 'market_projector_runtime_lease.acquire', p_input_commitment, + null, server_now + ); + update programmable_private.market_projector_runtime_lease_current as lease + set lease_generation = next_generation, + holder_id = p_holder_id::programmable_private.source_identifier, + lease_token_hash = + p_lease_token_hash::programmable_private.bytes32_value, + acquired_at = server_now, + expires_at = server_expires_at, + released_at = null, + acquisition_commitment = + p_input_commitment::programmable_private.bytes32_value, + release_commitment = null, + changed_by_audit_id = acquisition_audit_id + where lease.singleton_key = 'canonical-market-projector-runtime-v1'; + insert into programmable_private.market_projector_runtime_lease_history ( + lease_history_id, singleton_key, event_kind, lease_generation, + holder_id, lease_token_hash, acquired_at, expires_at, event_at, + input_commitment, audit_id + ) values ( + pg_catalog.gen_random_uuid(), 'canonical-market-projector-runtime-v1', + 'acquired', next_generation, + p_holder_id::programmable_private.source_identifier, + p_lease_token_hash::programmable_private.bytes32_value, + server_now, server_expires_at, server_now, + p_input_commitment::programmable_private.bytes32_value, + acquisition_audit_id + ); + acquired := true; + lease_generation := next_generation; + acquired_at := server_now; + expires_at := server_expires_at; + return next; +end +$function$; + +create function programmable_private.assert_market_projector_runtime_lease_v1( + p_holder_id text, + p_lease_generation bigint, + p_lease_token_hash bytea +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + current_lease + programmable_private.market_projector_runtime_lease_current%rowtype; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select lease.* into strict current_lease + from programmable_private.market_projector_runtime_lease_current as lease + where lease.singleton_key = 'canonical-market-projector-runtime-v1' + for update; + return p_holder_id is not null + and p_lease_generation is not null + and p_lease_token_hash is not null + and current_lease.lease_generation = p_lease_generation + and current_lease.holder_id = p_holder_id + and current_lease.lease_token_hash = p_lease_token_hash + and current_lease.released_at is null + and current_lease.expires_at > pg_catalog.clock_timestamp(); +end +$function$; + +create function + programmable_private.release_market_projector_runtime_lease_v1( + p_holder_id text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_released_at timestamptz, + p_input_commitment bytea + ) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + current_lease + programmable_private.market_projector_runtime_lease_current%rowtype; + server_now timestamptz := pg_catalog.clock_timestamp(); + release_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_released_at is null + or p_released_at < server_now - interval '30 seconds' + or p_released_at > server_now + interval '30 seconds' + or p_input_commitment is null + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '22023', + message = 'invalid market projector lease release'; + end if; + select lease.* into strict current_lease + from programmable_private.market_projector_runtime_lease_current as lease + where lease.singleton_key = 'canonical-market-projector-runtime-v1' + for update; + if current_lease.lease_generation <> p_lease_generation + or current_lease.holder_id <> p_holder_id + or current_lease.lease_token_hash <> p_lease_token_hash + or current_lease.released_at is not null + then + return false; + end if; + release_audit_id := programmable_private.append_mutation_audit( + 'market_projector_runtime_lease.release', p_input_commitment, + null, server_now + ); + update programmable_private.market_projector_runtime_lease_current as lease + set released_at = server_now, + release_commitment = + p_input_commitment::programmable_private.bytes32_value, + changed_by_audit_id = release_audit_id + where lease.singleton_key = 'canonical-market-projector-runtime-v1'; + insert into programmable_private.market_projector_runtime_lease_history ( + lease_history_id, singleton_key, event_kind, lease_generation, + holder_id, lease_token_hash, acquired_at, expires_at, event_at, + input_commitment, audit_id + ) values ( + pg_catalog.gen_random_uuid(), 'canonical-market-projector-runtime-v1', + 'released', current_lease.lease_generation, + current_lease.holder_id::programmable_private.source_identifier, + current_lease.lease_token_hash, + current_lease.acquired_at, current_lease.expires_at, server_now, + p_input_commitment::programmable_private.bytes32_value, + release_audit_id + ); + return true; +end +$function$; + +create trigger reject_immutable_mutation +before update or delete +on programmable_private.market_projector_cursor_history +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger reject_immutable_mutation +before update or delete +on programmable_private.market_snapshot_lineage_memberships +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger reject_immutable_mutation +before update or delete +on programmable_private.market_candle_lineage_memberships +for each row execute function programmable_private.reject_immutable_mutation(); + +create index market_projector_cursor_fact_visibility_idx + on programmable_private.market_projector_cursor_history ( + reconciliation_id, pool_id, reorg_generation, cursor_generation + ); + +-- Successful facts remain visible only while their page belongs to the active +-- market-projector reorg lineage. This keeps append-only evidence auditable +-- without exposing a same-epoch orphan after the cursor rewinds. +create or replace view programmable_private.market_snapshots_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.pool_id, + snapshot.market_snapshot_id, + snapshot.source_deployment_id, + provider.deployment_commitment as source_deployment_commitment, + provider.schema_commitment as source_schema_commitment, + snapshot.block_evidence_id, + snapshot.block_number, + snapshot.block_hash, + snapshot.sqrt_price_x96, + snapshot.liquidity, + snapshot.market_volume_token0, + snapshot.market_volume_token1, + snapshot.market_volume_usd, + snapshot.hook_gross_volume, + snapshot.observed_at, + reconciliation.reconciliation_id, + reconciliation.evidence_commitment as reconciliation_evidence_commitment, + outcome.finished_at as reconciled_at +from programmable_private.market_snapshots as snapshot +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = snapshot.reconciliation_id + and reconciliation.chain_id = snapshot.chain_id + and reconciliation.mismatch_count = 0 + and snapshot.block_number between + reconciliation.source_from_block and reconciliation.source_to_block +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + and run.chain_id = reconciliation.chain_id + and run.release_id = reconciliation.release_id + and run.model_id = reconciliation.model_id + and run.epoch_id = reconciliation.epoch_id + and run.captured_pointer_generation = reconciliation.pointer_generation +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id + and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.provider_deployments as provider + on provider.provider_deployment_id = snapshot.source_deployment_id + and provider.provider_type = 'uniswap_subgraph' +join programmable_private.dual_rpc_block_evidence as block_evidence + on block_evidence.block_evidence_id = snapshot.block_evidence_id + and block_evidence.chain_id = run.chain_id + and block_evidence.epoch_id = run.epoch_id + and block_evidence.pointer_generation = run.captured_pointer_generation + and block_evidence.block_number = snapshot.block_number + and block_evidence.agreed_block_hash = snapshot.block_hash +join programmable_private.run_lifecycle_outcomes as evidence_outcome + on evidence_outcome.run_id = block_evidence.verification_run_id + and evidence_outcome.status = 'succeeded' +join programmable_private.safe_head_observations as observation + on observation.observation_id = block_evidence.observation_id + and observation.chain_id = run.chain_id + and observation.release_id = run.release_id + and observation.model_id = run.model_id + and observation.source_group = run.source_group + and observation.epoch_id = run.epoch_id + and observation.pointer_generation = run.captured_pointer_generation +join programmable_private.launch_by_token_v1 as launch + on launch.chain_id = run.chain_id + and launch.release_id = run.release_id + and launch.model_id = run.model_id + and launch.source_group = run.source_group + and launch.epoch_id = run.epoch_id + and launch.pointer_generation = run.captured_pointer_generation + and launch.pool_id = snapshot.pool_id +where not exists ( + select 1 + from programmable_private.market_projector_cursor_current as current_cursor + where current_cursor.chain_id = run.chain_id + and current_cursor.release_id = run.release_id + and current_cursor.model_id = run.model_id + and current_cursor.source_group = run.source_group + and current_cursor.pool_id = snapshot.pool_id +) or exists ( + select 1 + from programmable_private.market_snapshot_lineage_memberships as membership + join programmable_private.market_projector_cursor_current as current_cursor + on current_cursor.chain_id = membership.chain_id + and current_cursor.release_id = membership.release_id + and current_cursor.model_id = membership.model_id + and current_cursor.source_group = membership.source_group + and current_cursor.projector_version = membership.projector_version + and current_cursor.pool_id = membership.pool_id + and current_cursor.reorg_generation = membership.reorg_generation + join programmable_private.market_projector_cursor_history as cursor_history + on cursor_history.market_cursor_id = current_cursor.market_cursor_id + join programmable_private.projector_checkpoints as bound_source_checkpoint + on bound_source_checkpoint.checkpoint_id = + cursor_history.source_checkpoint_id + join programmable_private.projector_checkpoint_current as source_tip + on source_tip.chain_id = cursor_history.chain_id + and source_tip.release_id = cursor_history.release_id + and source_tip.model_id = cursor_history.model_id + and source_tip.source_group = cursor_history.source_group + and source_tip.projector_version = + bound_source_checkpoint.projector_version + join programmable_private.projector_checkpoints as source_tip_checkpoint + on source_tip_checkpoint.checkpoint_id = source_tip.checkpoint_id + and source_tip_checkpoint.epoch_id = cursor_history.epoch_id + and source_tip_checkpoint.pointer_generation = + cursor_history.pointer_generation + and source_tip_checkpoint.reorg_generation = + cursor_history.source_reorg_generation + and source_tip_checkpoint.cursor_block_global_log_index = 4294967295 + and source_tip_checkpoint.cursor_candidate_id = 'empty-page' + where membership.market_snapshot_id = snapshot.market_snapshot_id + and membership.chain_id = run.chain_id + and membership.release_id = run.release_id + and membership.model_id = run.model_id + and membership.source_group = run.source_group + and membership.pool_id = snapshot.pool_id +); + +create or replace view programmable_private.market_candles_v1 +with (security_invoker = false, security_barrier = true) +as +select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.pool_id, + candle.market_candle_id, + candle.source_deployment_id, + provider.deployment_commitment as source_deployment_commitment, + provider.schema_commitment as source_schema_commitment, + candle.source_block_evidence_id, + candle.source_block_number, + candle.source_block_hash, + candle.interval, + candle.period_start, + candle.period_end, + candle.open, + candle.high, + candle.low, + candle.close, + candle.volume_token0, + candle.volume_token1, + candle.volume_usd, + reconciliation.reconciliation_id, + reconciliation.evidence_commitment as reconciliation_evidence_commitment, + outcome.finished_at as reconciled_at +from programmable_private.market_candles as candle +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = candle.reconciliation_id + and reconciliation.chain_id = candle.chain_id + and reconciliation.mismatch_count = 0 + and candle.source_block_number between + reconciliation.source_from_block and reconciliation.source_to_block +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + and run.chain_id = reconciliation.chain_id + and run.release_id = reconciliation.release_id + and run.model_id = reconciliation.model_id + and run.epoch_id = reconciliation.epoch_id + and run.captured_pointer_generation = reconciliation.pointer_generation +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id + and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = run.chain_id + and current_epoch.release_id = run.release_id + and current_epoch.model_id = run.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = run.epoch_id + and current_epoch.generation = run.captured_pointer_generation +join programmable_private.provider_deployments as provider + on provider.provider_deployment_id = candle.source_deployment_id + and provider.provider_type = 'uniswap_subgraph' +join programmable_private.dual_rpc_block_evidence as block_evidence + on block_evidence.block_evidence_id = candle.source_block_evidence_id + and block_evidence.chain_id = run.chain_id + and block_evidence.epoch_id = run.epoch_id + and block_evidence.pointer_generation = run.captured_pointer_generation + and block_evidence.block_number = candle.source_block_number + and block_evidence.agreed_block_hash = candle.source_block_hash +join programmable_private.run_lifecycle_outcomes as evidence_outcome + on evidence_outcome.run_id = block_evidence.verification_run_id + and evidence_outcome.status = 'succeeded' +join programmable_private.safe_head_observations as observation + on observation.observation_id = block_evidence.observation_id + and observation.chain_id = run.chain_id + and observation.release_id = run.release_id + and observation.model_id = run.model_id + and observation.source_group = run.source_group + and observation.epoch_id = run.epoch_id + and observation.pointer_generation = run.captured_pointer_generation +join programmable_private.launch_by_token_v1 as launch + on launch.chain_id = run.chain_id + and launch.release_id = run.release_id + and launch.model_id = run.model_id + and launch.source_group = run.source_group + and launch.epoch_id = run.epoch_id + and launch.pointer_generation = run.captured_pointer_generation + and launch.pool_id = candle.pool_id +where not exists ( + select 1 + from programmable_private.market_projector_cursor_current as current_cursor + where current_cursor.chain_id = run.chain_id + and current_cursor.release_id = run.release_id + and current_cursor.model_id = run.model_id + and current_cursor.source_group = run.source_group + and current_cursor.pool_id = candle.pool_id +) or exists ( + select 1 + from programmable_private.market_candle_lineage_memberships as membership + join programmable_private.market_projector_cursor_current as current_cursor + on current_cursor.chain_id = membership.chain_id + and current_cursor.release_id = membership.release_id + and current_cursor.model_id = membership.model_id + and current_cursor.source_group = membership.source_group + and current_cursor.projector_version = membership.projector_version + and current_cursor.pool_id = membership.pool_id + and current_cursor.reorg_generation = membership.reorg_generation + join programmable_private.market_projector_cursor_history as cursor_history + on cursor_history.market_cursor_id = current_cursor.market_cursor_id + join programmable_private.projector_checkpoints as bound_source_checkpoint + on bound_source_checkpoint.checkpoint_id = + cursor_history.source_checkpoint_id + join programmable_private.projector_checkpoint_current as source_tip + on source_tip.chain_id = cursor_history.chain_id + and source_tip.release_id = cursor_history.release_id + and source_tip.model_id = cursor_history.model_id + and source_tip.source_group = cursor_history.source_group + and source_tip.projector_version = + bound_source_checkpoint.projector_version + join programmable_private.projector_checkpoints as source_tip_checkpoint + on source_tip_checkpoint.checkpoint_id = source_tip.checkpoint_id + and source_tip_checkpoint.epoch_id = cursor_history.epoch_id + and source_tip_checkpoint.pointer_generation = + cursor_history.pointer_generation + and source_tip_checkpoint.reorg_generation = + cursor_history.source_reorg_generation + and source_tip_checkpoint.cursor_block_global_log_index = 4294967295 + and source_tip_checkpoint.cursor_candidate_id = 'empty-page' + where membership.market_candle_id = candle.market_candle_id + and membership.chain_id = run.chain_id + and membership.release_id = run.release_id + and membership.model_id = run.model_id + and membership.source_group = run.source_group + and membership.pool_id = candle.pool_id +); + +-- A block may contain more than one fee event for the same pool. The state +-- query is block-wide, so only the last canonical occurrence is a valid close. +-- Earlier same-block observations stay in the audit ledger but never become a +-- second public chart point. +create or replace view programmable_private.market_block_closes_v1 +with (security_invoker = false, security_barrier = true) +as +select launch.token, close_fact.* +from programmable_private.market_block_closes as close_fact +join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = close_fact.reconciliation_id + and reconciliation.mismatch_count = 0 +join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' +join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id and outcome.status = 'succeeded' +join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = close_fact.chain_id + and current_epoch.release_id = close_fact.release_id + and current_epoch.model_id = close_fact.model_id + and current_epoch.source_group = close_fact.source_group + and current_epoch.epoch_id = close_fact.epoch_id + and current_epoch.generation = close_fact.pointer_generation +join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = close_fact.last_source_occurrence_id + and canonical.logical_event_id = close_fact.last_source_logical_event_id + and canonical.block_hash = close_fact.last_source_occurrence_block_hash +join programmable_private.global_eth_usd_snapshots_v1 as global_snapshot + on global_snapshot.global_market_snapshot_id = + close_fact.global_market_snapshot_id +join programmable_private.launch_by_token_v1 as launch + on launch.chain_id = close_fact.chain_id + and launch.release_id = close_fact.release_id + and launch.model_id = close_fact.model_id + and launch.source_group = close_fact.source_group + and launch.epoch_id = close_fact.epoch_id + and launch.pointer_generation = close_fact.pointer_generation + and launch.pool_id = close_fact.pool_id +where not exists ( + select 1 + from programmable_private.market_block_closes as later_close + join programmable_private.reconciliation_records as later_reconciliation + on later_reconciliation.reconciliation_id = later_close.reconciliation_id + and later_reconciliation.mismatch_count = 0 + join programmable_private.run_headers as later_run + on later_run.run_id = later_reconciliation.run_id + and later_run.run_kind = 'reconciliation' + join programmable_private.run_lifecycle_outcomes as later_outcome + on later_outcome.run_id = later_run.run_id + and later_outcome.status = 'succeeded' + join programmable_private.chain_event_current_canonical as later_canonical + on later_canonical.occurrence_id = later_close.last_source_occurrence_id + and later_canonical.logical_event_id = + later_close.last_source_logical_event_id + and later_canonical.block_hash = + later_close.last_source_occurrence_block_hash + where later_close.chain_id = close_fact.chain_id + and later_close.release_id = close_fact.release_id + and later_close.model_id = close_fact.model_id + and later_close.source_group = close_fact.source_group + and later_close.epoch_id = close_fact.epoch_id + and later_close.pointer_generation = close_fact.pointer_generation + and later_close.pool_id = close_fact.pool_id + and later_close.block_hash = close_fact.block_hash + and ( + later_close.last_transaction_index, + later_close.last_block_global_log_index, + later_close.market_block_close_id + ) > ( + close_fact.last_transaction_index, + close_fact.last_block_global_log_index, + close_fact.market_block_close_id + ) +); + +create function programmable_private.resolve_market_graph_provider_v1( + p_redacted_identity text, + p_deployment_commitment bytea, + p_schema_commitment bytea +) +returns uuid +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + resolved_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_redacted_identity is null + or pg_catalog.octet_length(p_deployment_commitment) <> 32 + or pg_catalog.octet_length(p_schema_commitment) <> 32 + then + raise exception using + errcode = '22023', message = 'invalid exact market provider identity'; + end if; + select provider.provider_deployment_id into strict resolved_id + from programmable_private.provider_deployments as provider + where provider.provider_type = 'uniswap_subgraph' + and provider.redacted_identity = p_redacted_identity + and provider.deployment_commitment = p_deployment_commitment + and provider.schema_commitment = p_schema_commitment; + return resolved_id; +exception + when no_data_found then + raise exception using + errcode = '23503', message = 'exact market provider is not registered'; + when too_many_rows then + raise exception using + errcode = '23514', message = 'ambiguous exact market provider identity'; +end +$function$; + +create function programmable_private.list_market_projector_pools_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_source_projector_version text, + p_market_projector_version text, + p_limit integer +) +returns table ( + epoch_id uuid, + pointer_generation bigint, + source_checkpoint_id uuid, + source_checkpoint_generation bigint, + source_reorg_generation bigint, + source_checkpoint_block_number bigint, + source_checkpoint_block_hash bytea, + source_checkpoint_block_evidence_id uuid, + token bytea, + pool_id bytea, + currency0 bytea, + currency1 bytea, + hook bytea, + pool_key_fee bigint, + tick_spacing integer, + token0_decimals smallint, + token1_decimals smallint, + total_supply numeric, + launch_block_number bigint, + launch_block_timestamp timestamptz, + market_cursor_id uuid, + cursor_epoch_id uuid, + cursor_pointer_generation bigint, + cursor_generation bigint, + cursor_reorg_generation bigint, + cursor_source_checkpoint_id uuid, + cursor_source_checkpoint_generation bigint, + cursor_source_reorg_generation bigint, + cursor_block_evidence_id uuid, + cursor_block_number bigint, + cursor_block_hash bytea, + provider_cursor text, + hour_coverage_end timestamptz, + day_coverage_end timestamptz, + page_commitment bytea, + advanced_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_chain_id <= 0 or p_limit not between 1 and 20 + or p_market_projector_version is null + or pg_catalog.octet_length(p_market_projector_version) not between 1 and 128 + or p_market_projector_version !~ '^[A-Za-z0-9][A-Za-z0-9._+:/-]*$' + then + raise exception using + errcode = '22023', message = 'invalid market pool page'; + end if; + if not exists ( + select 1 + from programmable_private.projector_checkpoint_current as current_pointer + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_pointer.checkpoint_id + join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = checkpoint.chain_id + and current_epoch.release_id = checkpoint.release_id + and current_epoch.model_id = checkpoint.model_id + and current_epoch.source_group = checkpoint.source_group + and current_epoch.epoch_id = checkpoint.epoch_id + and current_epoch.generation = checkpoint.pointer_generation + where current_pointer.chain_id = p_chain_id + and current_pointer.release_id = p_release_id + and current_pointer.model_id = p_model_id + and current_pointer.source_group = p_source_group + and current_pointer.projector_version = p_source_projector_version + and checkpoint.cursor_block_global_log_index = 4294967295 + and checkpoint.cursor_candidate_id = 'empty-page' + ) then + raise exception using + errcode = '23503', message = 'market source checkpoint is unavailable'; + end if; + return query + select + checkpoint.epoch_id, + checkpoint.pointer_generation, + checkpoint.checkpoint_id, + checkpoint.checkpoint_generation, + checkpoint.reorg_generation, + checkpoint.block_number::bigint, + checkpoint.block_hash::bytea, + checkpoint.target_block_evidence_id, + launch.token::bytea, + launch.pool_id::bytea, + launch.currency0::bytea, + launch.currency1::bytea, + launch.hook::bytea, + launch.pool_key_fee::bigint, + launch.tick_spacing::integer, + 18::smallint, + 18::smallint, + launch.total_supply::numeric, + launch.promoted_block_number::bigint, + launch.launch_block_timestamp, + cursor_history.market_cursor_id, + cursor_history.epoch_id, + cursor_history.pointer_generation, + cursor_history.cursor_generation, + cursor_history.reorg_generation, + cursor_history.source_checkpoint_id, + cursor_history.source_checkpoint_generation, + cursor_history.source_reorg_generation, + cursor_history.block_evidence_id, + cursor_history.block_number::bigint, + cursor_history.block_hash::bytea, + cursor_history.provider_cursor, + cursor_history.hour_coverage_end, + cursor_history.day_coverage_end, + cursor_history.page_commitment::bytea, + cursor_history.advanced_at + from programmable_private.projector_checkpoint_current as current_pointer + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_pointer.checkpoint_id + join programmable_private.launch_by_token_v2 as launch + on launch.chain_id = checkpoint.chain_id + and launch.release_id = checkpoint.release_id + and launch.model_id = checkpoint.model_id + and launch.source_group = checkpoint.source_group + and launch.epoch_id = checkpoint.epoch_id + and launch.pointer_generation = checkpoint.pointer_generation + left join programmable_private.market_projector_cursor_current as current_cursor + on current_cursor.chain_id = checkpoint.chain_id + and current_cursor.release_id = checkpoint.release_id + and current_cursor.model_id = checkpoint.model_id + and current_cursor.source_group = checkpoint.source_group + and current_cursor.projector_version = p_market_projector_version + and current_cursor.pool_id = launch.pool_id + left join programmable_private.market_projector_cursor_history as cursor_history + on cursor_history.market_cursor_id = current_cursor.market_cursor_id + where current_pointer.chain_id = p_chain_id + and current_pointer.release_id = p_release_id + and current_pointer.model_id = p_model_id + and current_pointer.source_group = p_source_group + and current_pointer.projector_version = p_source_projector_version + and checkpoint.cursor_block_global_log_index = 4294967295 + and checkpoint.cursor_candidate_id = 'empty-page' + and ( + cursor_history.market_cursor_id is null + or cursor_history.epoch_id <> checkpoint.epoch_id + or cursor_history.pointer_generation <> checkpoint.pointer_generation + or cursor_history.source_reorg_generation < checkpoint.reorg_generation + or exists ( + select 1 + from programmable_private.chain_event_occurrence_materializations + as pending_materialization + join programmable_private.chain_event_occurrences + as pending_occurrence + on pending_occurrence.occurrence_id = + pending_materialization.occurrence_id + and pending_occurrence.chain_id = checkpoint.chain_id + join programmable_private.chain_event_current_canonical + as pending_canonical + on pending_canonical.occurrence_id = pending_occurrence.occurrence_id + and pending_canonical.logical_event_id = + pending_occurrence.logical_event_id + and pending_canonical.block_hash = pending_occurrence.block_hash + where pending_materialization.chain_id = checkpoint.chain_id + and pending_materialization.release_id = checkpoint.release_id + and pending_materialization.model_id = checkpoint.model_id + and pending_materialization.source_group = checkpoint.source_group + and pending_materialization.epoch_id = checkpoint.epoch_id + and pending_materialization.pointer_generation = + checkpoint.pointer_generation + and programmable_private.is_market_fee_event_v1( + checkpoint.model_id, pending_materialization.event_type + ) + and pg_catalog.lower( + pending_materialization.decoded_payload ->> 'poolId' + ) = '0x' || pg_catalog.encode(launch.pool_id, 'hex') + and pending_occurrence.block_number <= checkpoint.block_number + and ( + cursor_history.market_cursor_id is null + or pending_occurrence.block_number > cursor_history.block_number + or ( + pending_occurrence.block_number = cursor_history.block_number + and not exists ( + select 1 + from programmable_private.market_block_closes + as projected_close + where projected_close.chain_id = checkpoint.chain_id + and projected_close.release_id = checkpoint.release_id + and projected_close.model_id = checkpoint.model_id + and projected_close.source_group = checkpoint.source_group + and projected_close.epoch_id = checkpoint.epoch_id + and projected_close.pointer_generation = + checkpoint.pointer_generation + and projected_close.pool_id = launch.pool_id + and projected_close.last_source_occurrence_id = + pending_occurrence.occurrence_id + ) + ) + ) + ) + ) + order by + case + when cursor_history.market_cursor_id is not null and ( + cursor_history.epoch_id <> checkpoint.epoch_id + or cursor_history.pointer_generation <> checkpoint.pointer_generation + or cursor_history.source_reorg_generation < checkpoint.reorg_generation + ) then 0 + when cursor_history.market_cursor_id is null then 1 + else 2 + end, + cursor_history.advanced_at asc nulls first, + ( + checkpoint.block_number - coalesce( + cursor_history.block_number, launch.promoted_block_number + ) + ) desc, + launch.pool_id + limit p_limit; +end +$function$; + +create function programmable_private.resolve_market_block_evidence_v1( + p_reconciliation_id uuid, + p_block_number numeric, + p_block_hash bytea, + p_provider_a_id uuid, + p_provider_b_id uuid +) +returns uuid +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + normalized_block bigint; + resolved_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 + or p_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_block_hash) <> 32 + or p_provider_a_id is null or p_provider_b_id is null + or p_provider_a_id = p_provider_b_id + then + raise exception using + errcode = '22023', message = 'invalid market block evidence lookup'; + end if; + normalized_block := p_block_number::bigint; + select record.* into reconciliation + from programmable_private.reconciliation_records as record + where record.reconciliation_id = p_reconciliation_id + and record.mismatch_count = 0; + select run.* into header + from programmable_private.run_headers as run + where run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation'; + if header.run_id is null + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes as outcome + where outcome.run_id = header.run_id + ) + or normalized_block not between reconciliation.source_from_block + and reconciliation.source_to_block + then + raise exception using + errcode = '23514', message = 'market evidence lookup lacks open reconciliation'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 + from programmable_private.dual_rpc_block_evidence as evidence + join programmable_private.safe_head_observations as observation + on observation.observation_id = evidence.observation_id + where evidence.chain_id = header.chain_id + and evidence.epoch_id = header.epoch_id + and evidence.pointer_generation = header.captured_pointer_generation + and evidence.block_number = normalized_block + and observation.release_id = header.release_id + and observation.model_id = header.model_id + and observation.source_group = header.source_group + and observation.provider_a_id = p_provider_a_id + and observation.provider_b_id = p_provider_b_id + and evidence.agreed_block_hash <> p_block_hash + ) then + raise exception using + errcode = '23514', message = 'ambiguous market block identity'; + end if; + select evidence.block_evidence_id into resolved_id + from programmable_private.dual_rpc_block_evidence as evidence + join programmable_private.safe_head_observations as observation + on observation.observation_id = evidence.observation_id + where evidence.chain_id = header.chain_id + and evidence.epoch_id = header.epoch_id + and evidence.pointer_generation = header.captured_pointer_generation + and evidence.block_number = normalized_block + and evidence.agreed_block_hash = p_block_hash + and observation.release_id = header.release_id + and observation.model_id = header.model_id + and observation.source_group = header.source_group + and observation.provider_a_id = p_provider_a_id + and observation.provider_b_id = p_provider_b_id + order by evidence.verified_at desc, evidence.block_evidence_id desc + limit 1; + if resolved_id is null then + raise exception using + errcode = '23503', message = 'exact market block evidence is unavailable'; + end if; + return resolved_id; +end +$function$; + +create function programmable_private.resolve_market_close_anchor_v1( + p_reconciliation_id uuid, + p_pool_id bytea, + p_block_number numeric, + p_block_hash bytea +) +returns table ( + occurrence_id uuid, + logical_event_id uuid, + block_evidence_id uuid, + block_timestamp timestamptz, + transaction_hash bytea, + transaction_index bigint, + block_global_log_index bigint +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + normalized_block bigint; + candidate_count bigint; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if pg_catalog.octet_length(p_pool_id) <> 32 + or p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 + or p_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_block_hash) <> 32 + then + raise exception using + errcode = '22023', message = 'invalid market close anchor lookup'; + end if; + normalized_block := p_block_number::bigint; + select record.* into reconciliation + from programmable_private.reconciliation_records as record + where record.reconciliation_id = p_reconciliation_id + and record.mismatch_count = 0; + select run.* into header + from programmable_private.run_headers as run + where run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation'; + if header.run_id is null + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes as outcome + where outcome.run_id = header.run_id + ) + or normalized_block not between reconciliation.source_from_block + and reconciliation.source_to_block + then + raise exception using + errcode = '23514', message = 'market close lookup lacks open reconciliation'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + select pg_catalog.count(*) into candidate_count + from programmable_private.chain_event_occurrences as occurrence + join programmable_private.chain_event_occurrence_materializations as materialization + on materialization.occurrence_id = occurrence.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = header.captured_pointer_generation + and programmable_private.is_market_fee_event_v1( + header.model_id, materialization.event_type + ) + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = occurrence.occurrence_id + and canonical.logical_event_id = occurrence.logical_event_id + and canonical.block_hash = occurrence.block_hash + where occurrence.chain_id = header.chain_id + and occurrence.block_number = normalized_block + and occurrence.block_hash = p_block_hash + and pg_catalog.lower(materialization.decoded_payload ->> 'poolId') = + '0x' || pg_catalog.encode(p_pool_id, 'hex'); + if candidate_count = 0 then + raise exception using + errcode = '23503', message = 'canonical market close anchor is unavailable'; + end if; + return query + select + occurrence.occurrence_id, + occurrence.logical_event_id, + materialization.block_evidence_id, + occurrence.block_timestamp, + occurrence.transaction_hash::bytea, + occurrence.transaction_index::bigint, + occurrence.block_global_log_index::bigint + from programmable_private.chain_event_occurrences as occurrence + join programmable_private.chain_event_occurrence_materializations as materialization + on materialization.occurrence_id = occurrence.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = header.captured_pointer_generation + and programmable_private.is_market_fee_event_v1( + header.model_id, materialization.event_type + ) + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = occurrence.occurrence_id + and canonical.logical_event_id = occurrence.logical_event_id + and canonical.block_hash = occurrence.block_hash + where occurrence.chain_id = header.chain_id + and occurrence.block_number = normalized_block + and occurrence.block_hash = p_block_hash + and pg_catalog.lower(materialization.decoded_payload ->> 'poolId') = + '0x' || pg_catalog.encode(p_pool_id, 'hex') + order by occurrence.transaction_index desc, + occurrence.block_global_log_index desc, occurrence.occurrence_id desc + limit 1; +end +$function$; + +create function programmable_private.get_market_block_evidence_context_v1( + p_reconciliation_id uuid, + p_block_evidence_id uuid +) +returns table ( + provider_a_id uuid, + provider_b_id uuid, + provider_a_identity text, + provider_b_identity text, + provider_a_endpoint_commitment bytea, + provider_b_endpoint_commitment bytea, + provider_a_origin_commitment bytea, + provider_b_origin_commitment bytea, + block_number bigint, + block_hash bytea, + safe_block_number bigint +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + evidence programmable_private.dual_rpc_block_evidence%rowtype; + context record; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id; + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_block_evidence_id, evidence.agreed_block_hash + ); + if context.run_id is null then + raise exception using + errcode = '23514', message = 'market evidence context is unavailable'; + end if; + return query + select observation.provider_a_id, observation.provider_b_id, + provider_a.redacted_identity::text, + provider_b.redacted_identity::text, + metadata_a.endpoint_url_commitment::bytea, + metadata_b.endpoint_url_commitment::bytea, + metadata_a.endpoint_origin_commitment::bytea, + metadata_b.endpoint_origin_commitment::bytea, + evidence.block_number::bigint, evidence.agreed_block_hash::bytea, + observation.safe_block_number::bigint + from programmable_private.safe_head_observations as observation + join programmable_private.provider_deployments as provider_a + on provider_a.provider_deployment_id = observation.provider_a_id + and provider_a.provider_type = 'rpc_provider' + join programmable_private.provider_deployments as provider_b + on provider_b.provider_deployment_id = observation.provider_b_id + and provider_b.provider_type = 'rpc_provider' + join programmable_private.rpc_provider_deployment_metadata as metadata_a + on metadata_a.provider_deployment_id = provider_a.provider_deployment_id + and metadata_a.chain_id = context.chain_id + and metadata_a.vendor = 'alchemy' + and metadata_a.vendor_order = 1 + join programmable_private.rpc_provider_deployment_metadata as metadata_b + on metadata_b.provider_deployment_id = provider_b.provider_deployment_id + and metadata_b.chain_id = context.chain_id + and metadata_b.vendor = 'quicknode' + and metadata_b.vendor_order = 2 + where observation.observation_id = context.safe_head_observation_id; +end +$function$; + +create function programmable_private.get_market_global_snapshot_v1( + p_reconciliation_id uuid, + p_block_evidence_id uuid +) +returns uuid +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + evidence programmable_private.dual_rpc_block_evidence%rowtype; + context record; + resolved_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_block_evidence_id; + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_block_evidence_id, evidence.agreed_block_hash + ); + select snapshot.global_market_snapshot_id into resolved_id + from programmable_private.global_eth_usd_snapshots_v1 as snapshot + where snapshot.chain_id = context.chain_id + and snapshot.release_id = context.release_id + and snapshot.model_id = context.model_id + and snapshot.source_group = context.source_group + and snapshot.epoch_id = context.epoch_id + and snapshot.pointer_generation = context.pointer_generation + and snapshot.block_evidence_id = p_block_evidence_id + and snapshot.block_hash = evidence.agreed_block_hash; + return resolved_id; +end +$function$; + +create function programmable_private.list_market_close_anchors_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_source_projector_version text, + p_pool_id bytea, + p_from_block_exclusive numeric, + p_to_block_inclusive numeric, + p_limit integer, + p_after_block numeric default null +) +returns table ( + occurrence_id uuid, + logical_event_id uuid, + block_evidence_id uuid, + block_number bigint, + block_hash bytea, + block_timestamp timestamptz, + transaction_hash bytea, + transaction_index bigint, + block_global_log_index bigint +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + normalized_from bigint; + normalized_to bigint; + normalized_after bigint; + checkpoint programmable_private.projector_checkpoints%rowtype; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_chain_id <= 0 or pg_catalog.octet_length(p_pool_id) <> 32 + or p_from_block_exclusive <> pg_catalog.trunc(p_from_block_exclusive) + or p_to_block_inclusive <> pg_catalog.trunc(p_to_block_inclusive) + or p_from_block_exclusive < 0 + or p_to_block_inclusive < p_from_block_exclusive + or p_to_block_inclusive > 9223372036854775807 + or p_limit not between 1 and 128 + or ( + p_after_block is not null + and ( + p_after_block <> pg_catalog.trunc(p_after_block) + or p_after_block < p_from_block_exclusive + or p_after_block > p_to_block_inclusive + ) + ) + then + raise exception using + errcode = '22023', message = 'invalid market close anchor page'; + end if; + normalized_from := p_from_block_exclusive::bigint; + normalized_to := p_to_block_inclusive::bigint; + normalized_after := coalesce(p_after_block::bigint, normalized_from); + select stored.* into checkpoint + from programmable_private.projector_checkpoint_current as current_checkpoint + join programmable_private.projector_checkpoints as stored + on stored.checkpoint_id = current_checkpoint.checkpoint_id + join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = stored.chain_id + and current_epoch.release_id = stored.release_id + and current_epoch.model_id = stored.model_id + and current_epoch.source_group = stored.source_group + and current_epoch.epoch_id = stored.epoch_id + and current_epoch.generation = stored.pointer_generation + where current_checkpoint.chain_id = p_chain_id + and current_checkpoint.release_id = p_release_id + and current_checkpoint.model_id = p_model_id + and current_checkpoint.source_group = p_source_group + and current_checkpoint.projector_version = p_source_projector_version; + if checkpoint.checkpoint_id is null + or normalized_to > checkpoint.block_number + then + raise exception using + errcode = '23514', message = 'market close page exceeds current checkpoint'; + end if; + return query + select selected.occurrence_id, selected.logical_event_id, + selected.block_evidence_id, selected.block_number, + selected.block_hash, selected.block_timestamp, + selected.transaction_hash, selected.transaction_index, + selected.block_global_log_index + from ( + select distinct on (occurrence.block_number, occurrence.block_hash) + occurrence.occurrence_id, + occurrence.logical_event_id, + materialization.block_evidence_id, + occurrence.block_number::bigint, + occurrence.block_hash::bytea, + occurrence.block_timestamp, + occurrence.transaction_hash::bytea, + occurrence.transaction_index::bigint, + occurrence.block_global_log_index::bigint + from programmable_private.chain_event_occurrences as occurrence + join programmable_private.chain_event_occurrence_materializations as materialization + on materialization.occurrence_id = occurrence.occurrence_id + and materialization.chain_id = checkpoint.chain_id + and materialization.release_id = checkpoint.release_id + and materialization.model_id = checkpoint.model_id + and materialization.source_group = checkpoint.source_group + and materialization.epoch_id = checkpoint.epoch_id + and materialization.pointer_generation = checkpoint.pointer_generation + and programmable_private.is_market_fee_event_v1( + checkpoint.model_id, materialization.event_type + ) + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = occurrence.occurrence_id + and canonical.logical_event_id = occurrence.logical_event_id + and canonical.block_hash = occurrence.block_hash + where occurrence.chain_id = checkpoint.chain_id + and occurrence.block_number > normalized_after + and occurrence.block_number <= normalized_to + and pg_catalog.lower(materialization.decoded_payload ->> 'poolId') = + '0x' || pg_catalog.encode(p_pool_id, 'hex') + order by occurrence.block_number, occurrence.block_hash, + occurrence.transaction_index desc, + occurrence.block_global_log_index desc, + occurrence.occurrence_id desc + ) as selected + order by selected.block_number, selected.block_global_log_index + limit p_limit; +end +$function$; + +create function programmable_private.resolve_market_candle_close_v1( + p_reconciliation_id uuid, + p_pool_id bytea, + p_period_start timestamptz, + p_period_end timestamptz +) +returns uuid +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + resolved_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if pg_catalog.octet_length(p_pool_id) <> 32 + or p_period_end <= p_period_start + or p_period_end > p_period_start + interval '1 day' + then + raise exception using + errcode = '22023', message = 'invalid market candle close lookup'; + end if; + select record.* into reconciliation + from programmable_private.reconciliation_records as record + where record.reconciliation_id = p_reconciliation_id + and record.mismatch_count = 0; + select run.* into header + from programmable_private.run_headers as run + where run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation'; + if header.run_id is null + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes as outcome + where outcome.run_id = header.run_id + ) + then + raise exception using + errcode = '23514', message = 'market candle close lookup lacks open reconciliation'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + select close_fact.market_block_close_id into resolved_id + from programmable_private.market_block_closes as close_fact + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = close_fact.last_source_occurrence_id + and canonical.logical_event_id = close_fact.last_source_logical_event_id + and canonical.block_hash = close_fact.last_source_occurrence_block_hash + join programmable_private.reconciliation_records as close_reconciliation + on close_reconciliation.reconciliation_id = close_fact.reconciliation_id + and close_reconciliation.mismatch_count = 0 + join programmable_private.run_headers as close_run + on close_run.run_id = close_reconciliation.run_id + and close_run.run_kind = 'reconciliation' + left join programmable_private.run_lifecycle_outcomes as close_outcome + on close_outcome.run_id = close_run.run_id + where close_fact.chain_id = header.chain_id + and close_fact.release_id = header.release_id + and close_fact.model_id = header.model_id + and close_fact.source_group = header.source_group + and close_fact.epoch_id = header.epoch_id + and close_fact.pointer_generation = header.captured_pointer_generation + and close_fact.pool_id = p_pool_id + and close_fact.block_timestamp >= p_period_start + and close_fact.block_timestamp < p_period_end + and ( + close_fact.reconciliation_id = p_reconciliation_id + or close_outcome.status = 'succeeded' + ) + order by close_fact.block_number desc, + close_fact.last_block_global_log_index desc, + close_fact.market_block_close_id desc + limit 1; + if resolved_id is null then + raise exception using + errcode = '23503', message = 'market candle has no canonical closing swap'; + end if; + return resolved_id; +end +$function$; + +create function programmable_private.market_fact_reconciliation_usable_v1( + p_fact_reconciliation_id uuid, + p_current_reconciliation_id uuid +) +returns boolean +language sql +stable +security invoker +set search_path = '' +as $function$ + select p_fact_reconciliation_id = p_current_reconciliation_id + or exists ( + select 1 + from programmable_private.reconciliation_records as reconciliation + join programmable_private.run_headers as run + on run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + join programmable_private.run_lifecycle_outcomes as outcome + on outcome.run_id = run.run_id + and outcome.status = 'succeeded' + where reconciliation.reconciliation_id = p_fact_reconciliation_id + and reconciliation.mismatch_count = 0 + ) +$function$; + +create function programmable_private.attach_market_snapshot_lineage_v1( + p_reconciliation_id uuid, + p_projector_version text, + p_reorg_generation bigint, + p_market_snapshot_id uuid, + p_membership_commitment bytea, + p_attached_at timestamptz +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + snapshot programmable_private.market_snapshots%rowtype; + context record; + existing programmable_private.market_snapshot_lineage_memberships%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select candidate.* into snapshot + from programmable_private.market_snapshots as candidate + where candidate.market_snapshot_id = p_market_snapshot_id; + if snapshot.market_snapshot_id is null then + raise exception using + errcode = '23503', message = 'market snapshot is unavailable'; + end if; + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, snapshot.block_evidence_id, snapshot.block_hash + ); + if context.run_id is null + or p_reorg_generation < 0 + or p_projector_version is null + or pg_catalog.octet_length(p_projector_version) not between 1 and 128 + or p_projector_version !~ '^[A-Za-z0-9][A-Za-z0-9._+:/-]*$' + or pg_catalog.octet_length(p_membership_commitment) <> 32 + or snapshot.chain_id <> context.chain_id + or not programmable_private.market_fact_reconciliation_usable_v1( + snapshot.reconciliation_id, p_reconciliation_id + ) + or not exists ( + select 1 + from programmable_private.launch_by_token_v2 as launch + where launch.chain_id = context.chain_id + and launch.release_id = context.release_id + and launch.model_id = context.model_id + and launch.source_group = context.source_group + and launch.epoch_id = context.epoch_id + and launch.pointer_generation = context.pointer_generation + and launch.pool_id = snapshot.pool_id + ) + then + raise exception using + errcode = '23514', message = 'invalid snapshot lineage membership'; + end if; + select membership.* into existing + from programmable_private.market_snapshot_lineage_memberships as membership + where membership.chain_id = context.chain_id + and membership.release_id = context.release_id + and membership.model_id = context.model_id + and membership.source_group = context.source_group + and membership.projector_version = p_projector_version + and membership.pool_id = snapshot.pool_id + and membership.reorg_generation = p_reorg_generation + and membership.market_snapshot_id = p_market_snapshot_id; + if found then + if ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_membership_commitment + then + raise exception using + errcode = '23505', message = 'snapshot lineage replay conflict'; + end if; + return p_market_snapshot_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'market_snapshot_lineage.attach', p_membership_commitment, + context.run_id, p_attached_at + ); + insert into programmable_private.market_snapshot_lineage_memberships ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, reorg_generation, market_snapshot_id, + attached_reconciliation_id, attached_at, audit_id + ) values ( + context.chain_id, + context.release_id::programmable_private.release_identifier, + context.model_id::programmable_private.model_identifier, + context.source_group::programmable_private.source_identifier, + p_projector_version::programmable_private.projector_identifier, + snapshot.pool_id, p_reorg_generation, p_market_snapshot_id, + p_reconciliation_id, p_attached_at, created_audit_id + ); + return p_market_snapshot_id; +end +$function$; + +create function programmable_private.attach_market_candle_lineage_v1( + p_reconciliation_id uuid, + p_projector_version text, + p_reorg_generation bigint, + p_market_candle_id uuid, + p_membership_commitment bytea, + p_attached_at timestamptz +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + candle programmable_private.market_candles%rowtype; + context record; + existing programmable_private.market_candle_lineage_memberships%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select candidate.* into candle + from programmable_private.market_candles as candidate + where candidate.market_candle_id = p_market_candle_id; + if candle.market_candle_id is null then + raise exception using + errcode = '23503', message = 'market candle is unavailable'; + end if; + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, candle.source_block_evidence_id, + candle.source_block_hash + ); + if context.run_id is null + or p_reorg_generation < 0 + or p_projector_version is null + or pg_catalog.octet_length(p_projector_version) not between 1 and 128 + or p_projector_version !~ '^[A-Za-z0-9][A-Za-z0-9._+:/-]*$' + or pg_catalog.octet_length(p_membership_commitment) <> 32 + or candle.chain_id <> context.chain_id + or not programmable_private.market_fact_reconciliation_usable_v1( + candle.reconciliation_id, p_reconciliation_id + ) + or not exists ( + select 1 + from programmable_private.launch_by_token_v2 as launch + where launch.chain_id = context.chain_id + and launch.release_id = context.release_id + and launch.model_id = context.model_id + and launch.source_group = context.source_group + and launch.epoch_id = context.epoch_id + and launch.pointer_generation = context.pointer_generation + and launch.pool_id = candle.pool_id + ) + then + raise exception using + errcode = '23514', message = 'invalid candle lineage membership'; + end if; + select membership.* into existing + from programmable_private.market_candle_lineage_memberships as membership + where membership.chain_id = context.chain_id + and membership.release_id = context.release_id + and membership.model_id = context.model_id + and membership.source_group = context.source_group + and membership.projector_version = p_projector_version + and membership.pool_id = candle.pool_id + and membership.reorg_generation = p_reorg_generation + and membership.market_candle_id = p_market_candle_id; + if found then + if ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_membership_commitment + then + raise exception using + errcode = '23505', message = 'candle lineage replay conflict'; + end if; + return p_market_candle_id; + end if; + created_audit_id := programmable_private.append_mutation_audit( + 'market_candle_lineage.attach', p_membership_commitment, + context.run_id, p_attached_at + ); + insert into programmable_private.market_candle_lineage_memberships ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, reorg_generation, market_candle_id, + attached_reconciliation_id, attached_at, audit_id + ) values ( + context.chain_id, + context.release_id::programmable_private.release_identifier, + context.model_id::programmable_private.model_identifier, + context.source_group::programmable_private.source_identifier, + p_projector_version::programmable_private.projector_identifier, + candle.pool_id, p_reorg_generation, p_market_candle_id, + p_reconciliation_id, p_attached_at, created_audit_id + ); + return p_market_candle_id; +end +$function$; + +create function programmable_private.append_market_snapshot_v2( + p_market_snapshot_id uuid, + p_reconciliation_id uuid, + p_source_deployment_id uuid, + p_block_evidence_id uuid, + p_pool_id bytea, + p_block_number numeric, + p_block_hash bytea, + p_sqrt_price_x96 numeric, + p_liquidity numeric, + p_market_volume_token0 numeric, + p_market_volume_token1 numeric, + p_market_volume_usd numeric, + p_hook_gross_volume numeric, + p_observed_at timestamptz, + p_input_commitment bytea +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + existing programmable_private.market_snapshots%rowtype; + context record; + normalized_sqrt numeric; + normalized_liquidity numeric; + normalized_hook_volume numeric; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_block_evidence_id, p_block_hash + ); + normalized_sqrt := programmable_private.validate_uint256(p_sqrt_price_x96); + normalized_liquidity := programmable_private.validate_uint256(p_liquidity); + if p_hook_gross_volume is not null then + normalized_hook_volume := + programmable_private.validate_uint256(p_hook_gross_volume); + end if; + select candidate.* into existing + from programmable_private.market_snapshots as candidate + where candidate.market_snapshot_id = p_market_snapshot_id; + if found then + if context.run_id is null + or not programmable_private.market_fact_reconciliation_usable_v1( + existing.reconciliation_id, p_reconciliation_id + ) + or existing.chain_id <> context.chain_id + or existing.source_deployment_id <> p_source_deployment_id + or existing.block_evidence_id <> p_block_evidence_id + or existing.pool_id <> p_pool_id + or existing.block_number <> p_block_number + or existing.block_hash <> p_block_hash + or existing.sqrt_price_x96 <> normalized_sqrt + or existing.liquidity <> normalized_liquidity + or existing.market_volume_token0 <> p_market_volume_token0 + or existing.market_volume_token1 <> p_market_volume_token1 + or existing.market_volume_usd is distinct from p_market_volume_usd + or existing.hook_gross_volume is distinct from normalized_hook_volume + or existing.observed_at <> p_observed_at + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_input_commitment + then + raise exception using + errcode = '23505', message = 'market snapshot replay changed content'; + end if; + return p_market_snapshot_id; + end if; + return programmable_private.append_market_snapshot( + p_market_snapshot_id, p_reconciliation_id, p_source_deployment_id, + p_block_evidence_id, p_pool_id, p_block_number, p_block_hash, + p_sqrt_price_x96, p_liquidity, p_market_volume_token0, + p_market_volume_token1, p_market_volume_usd, p_hook_gross_volume, + p_observed_at, p_input_commitment + ); +end +$function$; + +create function programmable_private.append_market_candle_v2( + p_market_candle_id uuid, + p_reconciliation_id uuid, + p_source_deployment_id uuid, + p_source_block_evidence_id uuid, + p_pool_id bytea, + p_interval text, + p_period_start timestamptz, + p_period_end timestamptz, + p_open numeric, + p_high numeric, + p_low numeric, + p_close numeric, + p_volume_token0 numeric, + p_volume_token1 numeric, + p_volume_usd numeric, + p_source_block_hash bytea, + p_input_commitment bytea +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + existing programmable_private.market_candles%rowtype; + context record; + requested_interval programmable_private.market_interval; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_source_block_evidence_id, p_source_block_hash + ); + requested_interval := p_interval::programmable_private.market_interval; + select candidate.* into existing + from programmable_private.market_candles as candidate + where candidate.market_candle_id = p_market_candle_id; + if found then + if context.run_id is null + or not programmable_private.market_fact_reconciliation_usable_v1( + existing.reconciliation_id, p_reconciliation_id + ) + or existing.chain_id <> context.chain_id + or existing.source_deployment_id <> p_source_deployment_id + or existing.source_block_evidence_id <> p_source_block_evidence_id + or existing.source_block_number <> context.block_number + or existing.pool_id <> p_pool_id + or existing.interval <> requested_interval + or existing.period_start <> p_period_start + or existing.period_end <> p_period_end + or existing.open <> p_open + or existing.high <> p_high + or existing.low <> p_low + or existing.close <> p_close + or existing.volume_token0 <> p_volume_token0 + or existing.volume_token1 <> p_volume_token1 + or existing.volume_usd is distinct from p_volume_usd + or existing.source_block_hash <> p_source_block_hash + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_input_commitment + then + raise exception using + errcode = '23505', message = 'market candle replay changed content'; + end if; + return p_market_candle_id; + end if; + return programmable_private.append_market_candle( + p_market_candle_id, p_reconciliation_id, p_source_deployment_id, + p_source_block_evidence_id, p_pool_id, p_interval, p_period_start, + p_period_end, p_open, p_high, p_low, p_close, p_volume_token0, + p_volume_token1, p_volume_usd, p_source_block_hash, + p_input_commitment + ); +end +$function$; + +-- V1 allowed a market fact to reuse any earlier ETH/USD observation from the +-- same release epoch. The projector requires an exact observation for the +-- fact's block, so V2 narrows the capability before delegating the append. +create function programmable_private.append_market_snapshot_details_v2( + p_market_snapshot_id uuid, + p_reconciliation_id uuid, + p_projector_version text, + p_reorg_generation bigint, + p_global_market_snapshot_id uuid, + p_tick integer, + p_token0_price numeric, + p_token1_price numeric, + p_tvl_token0 numeric, + p_tvl_token1 numeric, + p_tvl_usd numeric, + p_transaction_count bigint, + p_detail_commitment bytea, + p_recorded_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + snapshot programmable_private.market_snapshots%rowtype; + context record; + existing programmable_private.market_snapshot_details%rowtype; + exact_global_id uuid; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select candidate.* into snapshot + from programmable_private.market_snapshots as candidate + where candidate.market_snapshot_id = p_market_snapshot_id; + if snapshot.market_snapshot_id is null then + raise exception using + errcode = '23503', message = 'market snapshot is unavailable'; + end if; + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, snapshot.block_evidence_id, + snapshot.block_hash + ); + select global_snapshot.global_market_snapshot_id into exact_global_id + from programmable_private.global_eth_usd_snapshots as global_snapshot + join programmable_private.reconciliation_records as global_reconciliation + on global_reconciliation.reconciliation_id = + global_snapshot.reconciliation_id + and global_reconciliation.mismatch_count = 0 + join programmable_private.run_headers as global_run + on global_run.run_id = global_reconciliation.run_id + and global_run.run_kind = 'reconciliation' + left join programmable_private.run_lifecycle_outcomes as global_outcome + on global_outcome.run_id = global_run.run_id + where global_snapshot.global_market_snapshot_id = + p_global_market_snapshot_id + and global_snapshot.chain_id = snapshot.chain_id + and global_snapshot.release_id = context.release_id + and global_snapshot.model_id = context.model_id + and global_snapshot.source_group = context.source_group + and global_snapshot.epoch_id = context.epoch_id + and global_snapshot.pointer_generation = context.pointer_generation + and global_snapshot.block_evidence_id = snapshot.block_evidence_id + and global_snapshot.block_number = snapshot.block_number + and global_snapshot.block_hash = snapshot.block_hash + and ( + global_snapshot.reconciliation_id = p_reconciliation_id + or global_outcome.status = 'succeeded' + ); + if context.run_id is null + or exact_global_id is null + or not programmable_private.market_fact_reconciliation_usable_v1( + snapshot.reconciliation_id, p_reconciliation_id + ) + or p_tick not between -887272 and 887272 + or least(p_token0_price, p_token1_price, p_tvl_token0, p_tvl_token1, + p_tvl_usd) < 0 + or p_token0_price::text in ('NaN', 'Infinity', '-Infinity') + or p_token1_price::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_token0::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_token1::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_transaction_count < 0 + or pg_catalog.octet_length(p_detail_commitment) <> 32 + then + raise exception using + errcode = '23514', message = 'snapshot lacks exact ETH/USD block evidence'; + end if; + select detail.* into existing + from programmable_private.market_snapshot_details as detail + where detail.market_snapshot_id = p_market_snapshot_id; + if found then + if existing.global_market_snapshot_id <> p_global_market_snapshot_id + or existing.tick <> p_tick + or existing.token0_price <> p_token0_price + or existing.token1_price <> p_token1_price + or existing.tvl_token0 <> p_tvl_token0 + or existing.tvl_token1 <> p_tvl_token1 + or existing.tvl_usd <> p_tvl_usd + or existing.transaction_count <> p_transaction_count + or existing.detail_commitment <> p_detail_commitment + then + raise exception using + errcode = '23505', message = 'market snapshot detail replay conflict'; + end if; + else + created_audit_id := programmable_private.append_mutation_audit( + 'market_snapshot_detail_v2.append', p_detail_commitment, + context.run_id, p_recorded_at + ); + insert into programmable_private.market_snapshot_details ( + market_snapshot_id, tick, token0_price, token1_price, + tvl_token0, tvl_token1, tvl_usd, transaction_count, + global_market_snapshot_id, detail_commitment, audit_id + ) values ( + p_market_snapshot_id, p_tick, p_token0_price, p_token1_price, + p_tvl_token0, p_tvl_token1, p_tvl_usd, p_transaction_count, + p_global_market_snapshot_id, + p_detail_commitment::programmable_private.bytes32_value, + created_audit_id + ); + end if; + perform programmable_private.attach_market_snapshot_lineage_v1( + p_reconciliation_id, p_projector_version, p_reorg_generation, + p_market_snapshot_id, p_detail_commitment, p_recorded_at + ); + return p_market_snapshot_id; +end +$function$; + +create function programmable_private.append_market_block_close_v2( + p_market_block_close_id uuid, + p_reconciliation_id uuid, + p_source_deployment_id uuid, + p_block_evidence_id uuid, + p_pool_id bytea, + p_last_source_occurrence_id uuid, + p_sqrt_price_x96 numeric, + p_liquidity numeric, + p_tick integer, + p_token0_price numeric, + p_token1_price numeric, + p_volume_token0 numeric, + p_volume_token1 numeric, + p_volume_usd numeric, + p_fees_usd numeric, + p_tvl_usd numeric, + p_transaction_count bigint, + p_global_market_snapshot_id uuid, + p_source_query_commitment bytea, + p_close_commitment bytea, + p_observed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + evidence programmable_private.dual_rpc_block_evidence%rowtype; + context record; + occurrence programmable_private.chain_event_occurrences%rowtype; + existing programmable_private.market_block_closes%rowtype; + normalized_sqrt numeric; + normalized_liquidity numeric; + exact_global_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select candidate.* into evidence + from programmable_private.dual_rpc_block_evidence as candidate + where candidate.block_evidence_id = p_block_evidence_id; + select * into context + from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, p_block_evidence_id, evidence.agreed_block_hash + ); + select global_snapshot.global_market_snapshot_id into exact_global_id + from programmable_private.global_eth_usd_snapshots as global_snapshot + join programmable_private.reconciliation_records as global_reconciliation + on global_reconciliation.reconciliation_id = + global_snapshot.reconciliation_id + and global_reconciliation.mismatch_count = 0 + join programmable_private.run_headers as global_run + on global_run.run_id = global_reconciliation.run_id + and global_run.run_kind = 'reconciliation' + left join programmable_private.run_lifecycle_outcomes as global_outcome + on global_outcome.run_id = global_run.run_id + where global_snapshot.global_market_snapshot_id = + p_global_market_snapshot_id + and global_snapshot.chain_id = context.chain_id + and global_snapshot.release_id = context.release_id + and global_snapshot.model_id = context.model_id + and global_snapshot.source_group = context.source_group + and global_snapshot.epoch_id = context.epoch_id + and global_snapshot.pointer_generation = context.pointer_generation + and global_snapshot.block_evidence_id = p_block_evidence_id + and global_snapshot.block_number = evidence.block_number + and global_snapshot.block_hash = evidence.agreed_block_hash + and ( + global_snapshot.reconciliation_id = p_reconciliation_id + or global_outcome.status = 'succeeded' + ); + select source_occurrence.* into occurrence + from programmable_private.chain_event_occurrences as source_occurrence + join programmable_private.chain_event_occurrence_materializations as materialization + on materialization.occurrence_id = source_occurrence.occurrence_id + and materialization.chain_id = context.chain_id + and materialization.release_id = context.release_id + and materialization.model_id = context.model_id + and materialization.source_group = context.source_group + and materialization.epoch_id = context.epoch_id + and materialization.pointer_generation = context.pointer_generation + and programmable_private.is_market_fee_event_v1( + context.model_id, materialization.event_type + ) + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = source_occurrence.occurrence_id + and canonical.logical_event_id = source_occurrence.logical_event_id + and canonical.block_hash = source_occurrence.block_hash + where source_occurrence.occurrence_id = p_last_source_occurrence_id + and source_occurrence.chain_id = context.chain_id + and source_occurrence.block_number = evidence.block_number + and source_occurrence.block_hash = evidence.agreed_block_hash + and pg_catalog.lower(materialization.decoded_payload ->> 'poolId') = + '0x' || pg_catalog.encode(p_pool_id, 'hex'); + normalized_sqrt := programmable_private.validate_uint256(p_sqrt_price_x96); + normalized_liquidity := programmable_private.validate_uint256(p_liquidity); + if context.run_id is null + or exact_global_id is null + or occurrence.occurrence_id is null + or not exists ( + select 1 + from programmable_private.provider_deployments as provider + where provider.provider_deployment_id = p_source_deployment_id + and provider.provider_type = 'uniswap_subgraph' + ) + or pg_catalog.octet_length(p_pool_id) <> 32 + or p_tick not between -887272 and 887272 + or least(p_token0_price, p_token1_price, p_volume_token0, + p_volume_token1, p_volume_usd, p_fees_usd, p_tvl_usd) < 0 + or p_token0_price::text in ('NaN', 'Infinity', '-Infinity') + or p_token1_price::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_token0::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_token1::text in ('NaN', 'Infinity', '-Infinity') + or p_volume_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_fees_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_tvl_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_transaction_count < 0 + or pg_catalog.octet_length(p_source_query_commitment) <> 32 + or pg_catalog.octet_length(p_close_commitment) <> 32 + then + raise exception using + errcode = '23514', message = 'close lacks exact canonical block evidence'; + end if; + select candidate.* into existing + from programmable_private.market_block_closes as candidate + where candidate.market_block_close_id = p_market_block_close_id; + if found then + if not programmable_private.market_fact_reconciliation_usable_v1( + existing.reconciliation_id, p_reconciliation_id + ) + or existing.chain_id <> context.chain_id + or existing.release_id <> context.release_id + or existing.model_id <> context.model_id + or existing.source_group <> context.source_group + or existing.epoch_id <> context.epoch_id + or existing.pointer_generation <> context.pointer_generation + or existing.pool_id <> p_pool_id + or existing.source_deployment_id <> p_source_deployment_id + or existing.block_evidence_id <> p_block_evidence_id + or existing.block_number <> evidence.block_number + or existing.block_hash <> evidence.agreed_block_hash + or existing.block_timestamp <> occurrence.block_timestamp + or existing.last_transaction_hash <> occurrence.transaction_hash + or existing.last_transaction_index <> occurrence.transaction_index + or existing.last_block_global_log_index <> + occurrence.block_global_log_index + or existing.last_source_occurrence_id <> occurrence.occurrence_id + or existing.last_source_logical_event_id <> occurrence.logical_event_id + or existing.last_source_occurrence_block_hash <> occurrence.block_hash + or existing.sqrt_price_x96 <> normalized_sqrt + or existing.liquidity <> normalized_liquidity + or existing.tick <> p_tick + or existing.token0_price <> p_token0_price + or existing.token1_price <> p_token1_price + or existing.volume_token0 <> p_volume_token0 + or existing.volume_token1 <> p_volume_token1 + or existing.volume_usd <> p_volume_usd + or existing.fees_usd <> p_fees_usd + or existing.tvl_usd <> p_tvl_usd + or existing.transaction_count <> p_transaction_count + or existing.global_market_snapshot_id <> p_global_market_snapshot_id + or existing.source_query_commitment <> p_source_query_commitment + or existing.close_commitment <> p_close_commitment + or existing.observed_at <> p_observed_at + or ( + select audit.input_commitment + from programmable_private.mutation_audits as audit + where audit.audit_id = existing.audit_id + ) <> p_close_commitment + then + raise exception using + errcode = '23505', message = 'market block close replay conflict'; + end if; + return p_market_block_close_id; + end if; + return programmable_private.append_market_block_close_v1( + p_market_block_close_id, p_reconciliation_id, + p_source_deployment_id, p_block_evidence_id, p_pool_id, + p_last_source_occurrence_id, p_sqrt_price_x96, p_liquidity, + p_tick, p_token0_price, p_token1_price, p_volume_token0, + p_volume_token1, p_volume_usd, p_fees_usd, p_tvl_usd, + p_transaction_count, p_global_market_snapshot_id, + p_source_query_commitment, p_close_commitment, p_observed_at + ); +end +$function$; + +create function programmable_private.get_market_projector_cursor_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_projector_version text, + p_pool_id bytea +) +returns table ( + market_cursor_id uuid, + epoch_id uuid, + pointer_generation bigint, + cursor_generation bigint, + reorg_generation bigint, + source_checkpoint_id uuid, + source_checkpoint_generation bigint, + source_reorg_generation bigint, + block_evidence_id uuid, + block_number bigint, + block_hash bytea, + provider_cursor text, + hour_coverage_end timestamptz, + day_coverage_end timestamptz, + page_commitment bytea, + advanced_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_chain_id <= 0 or pg_catalog.octet_length(p_pool_id) <> 32 then + raise exception using + errcode = '22023', message = 'invalid market cursor identity'; + end if; + return query + select + history.market_cursor_id, + history.epoch_id, + history.pointer_generation, + history.cursor_generation, + history.reorg_generation, + history.source_checkpoint_id, + history.source_checkpoint_generation, + history.source_reorg_generation, + history.block_evidence_id, + history.block_number::bigint, + history.block_hash::bytea, + history.provider_cursor, + history.hour_coverage_end, + history.day_coverage_end, + history.page_commitment::bytea, + history.advanced_at + from programmable_private.market_projector_cursor_current as current_cursor + join programmable_private.market_projector_cursor_history as history + on history.market_cursor_id = current_cursor.market_cursor_id + where current_cursor.chain_id = p_chain_id + and current_cursor.release_id = p_release_id + and current_cursor.model_id = p_model_id + and current_cursor.source_group = p_source_group + and current_cursor.projector_version = p_projector_version + and current_cursor.pool_id = p_pool_id; +end +$function$; + +create function programmable_private.advance_market_projector_cursor_v1( + p_market_cursor_id uuid, + p_reconciliation_id uuid, + p_source_projector_version text, + p_market_projector_version text, + p_pool_id bytea, + p_expected_cursor_generation bigint, + p_next_cursor_generation bigint, + p_expected_reorg_generation bigint, + p_next_reorg_generation bigint, + p_source_checkpoint_id uuid, + p_source_checkpoint_generation bigint, + p_source_reorg_generation bigint, + p_target_block_evidence_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea, + p_provider_cursor text, + p_hour_coverage_end timestamptz, + p_day_coverage_end timestamptz, + p_page_commitment bytea, + p_advanced_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + reconciliation programmable_private.reconciliation_records%rowtype; + header programmable_private.run_headers%rowtype; + source_checkpoint programmable_private.projector_checkpoints%rowtype; + target_evidence programmable_private.dual_rpc_block_evidence%rowtype; + current_pointer programmable_private.market_projector_cursor_current%rowtype; + previous_cursor programmable_private.market_projector_cursor_history%rowtype; + launch record; + normalized_target bigint; + is_rewind boolean; + coverage_from_exclusive bigint; + snapshot_backfill_audit_id uuid; + candle_backfill_audit_id uuid; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + if p_market_cursor_id is null + or pg_catalog.octet_length(p_pool_id) <> 32 + or p_expected_cursor_generation < 0 + or p_next_cursor_generation <> p_expected_cursor_generation + 1 + or p_expected_reorg_generation < 0 + or p_next_reorg_generation < p_expected_reorg_generation + or p_source_checkpoint_generation <= 0 + or p_source_reorg_generation < 0 + or p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or pg_catalog.octet_length(p_provider_cursor) not between 1 and 256 + or p_provider_cursor !~ '^[A-Za-z0-9][A-Za-z0-9._:/#-]*$' + or pg_catalog.octet_length(p_page_commitment) <> 32 + or p_page_commitment = pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or (p_hour_coverage_end is not null and + p_hour_coverage_end <> pg_catalog.date_trunc('hour', p_hour_coverage_end)) + or (p_day_coverage_end is not null and + p_day_coverage_end <> pg_catalog.date_trunc('day', p_day_coverage_end)) + then + raise exception using + errcode = '22023', message = 'invalid market cursor CAS request'; + end if; + normalized_target := p_target_block_number::bigint; + select record.* into reconciliation + from programmable_private.reconciliation_records as record + where record.reconciliation_id = p_reconciliation_id + and record.mismatch_count = 0; + select run.* into header + from programmable_private.run_headers as run + where run.run_id = reconciliation.run_id + and run.run_kind = 'reconciliation' + for update; + if header.run_id is null + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes as outcome + where outcome.run_id = header.run_id + ) + or normalized_target not between reconciliation.source_from_block + and reconciliation.source_to_block + then + raise exception using + errcode = '23514', message = 'market cursor lacks open exact reconciliation'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + select checkpoint.* into source_checkpoint + from programmable_private.projector_checkpoint_current as current_checkpoint + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_checkpoint.checkpoint_id + where current_checkpoint.chain_id = header.chain_id + and current_checkpoint.release_id = header.release_id + and current_checkpoint.model_id = header.model_id + and current_checkpoint.source_group = header.source_group + and current_checkpoint.projector_version = p_source_projector_version + and current_checkpoint.checkpoint_id = p_source_checkpoint_id + and current_checkpoint.checkpoint_generation = p_source_checkpoint_generation + and current_checkpoint.reorg_generation = p_source_reorg_generation; + if source_checkpoint.checkpoint_id is null + or source_checkpoint.epoch_id <> header.epoch_id + or source_checkpoint.pointer_generation <> + header.captured_pointer_generation + or source_checkpoint.cursor_block_global_log_index <> 4294967295 + or source_checkpoint.cursor_candidate_id <> 'empty-page' + or normalized_target > source_checkpoint.block_number + then + raise exception using + errcode = '40001', message = 'stale market source checkpoint'; + end if; + select evidence.* into target_evidence + from programmable_private.dual_rpc_block_evidence as evidence + where evidence.block_evidence_id = p_target_block_evidence_id + and evidence.chain_id = header.chain_id + and evidence.epoch_id = header.epoch_id + and evidence.pointer_generation = header.captured_pointer_generation + and evidence.block_number = normalized_target + and evidence.agreed_block_hash = p_target_block_hash; + if target_evidence.block_evidence_id is null then + raise exception using + errcode = '23514', message = 'market cursor target lacks exact block evidence'; + end if; + select launch_row.* into launch + from programmable_private.launch_by_token_v2 as launch_row + where launch_row.chain_id = header.chain_id + and launch_row.release_id = header.release_id + and launch_row.model_id = header.model_id + and launch_row.source_group = header.source_group + and launch_row.epoch_id = header.epoch_id + and launch_row.pointer_generation = header.captured_pointer_generation + and launch_row.pool_id = p_pool_id; + if launch.pool_id is null + or normalized_target < launch.promoted_block_number + then + raise exception using + errcode = '23503', message = 'market cursor pool is not a current launch'; + end if; + select * into current_pointer + from programmable_private.market_projector_cursor_current + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_market_projector_version + and pool_id = p_pool_id + for update; + if found then + if current_pointer.cursor_generation <> p_expected_cursor_generation + or current_pointer.reorg_generation <> p_expected_reorg_generation + or p_expected_cursor_generation = 0 + then + raise exception using + errcode = '40001', message = 'market cursor CAS lost'; + end if; + select * into previous_cursor + from programmable_private.market_projector_cursor_history + where market_cursor_id = current_pointer.market_cursor_id; + if previous_cursor.market_cursor_id is null then + raise exception using + errcode = '23503', message = 'market cursor history is missing'; + end if; + elsif p_expected_cursor_generation <> 0 + or p_expected_reorg_generation <> 0 + or p_next_reorg_generation <> 0 + then + raise exception using + errcode = '40001', message = 'market cursor CAS lost'; + end if; + is_rewind := previous_cursor.market_cursor_id is not null + and p_next_reorg_generation > p_expected_reorg_generation; + coverage_from_exclusive := case + when previous_cursor.market_cursor_id is null or is_rewind + then launch.promoted_block_number - 1 + when p_source_checkpoint_generation > + previous_cursor.source_checkpoint_generation + and source_checkpoint.block_number = previous_cursor.block_number + then previous_cursor.block_number - 1 + else previous_cursor.block_number + end; + -- The first V2 cursor is the atomic cutover point for an already populated + -- database. Preserve every complete legacy fact that is visible before the + -- pointer exists, then fail closed if the backfill is incomplete. New/open + -- facts are attached by the V2 detail append entrypoints. + if previous_cursor.market_cursor_id is null then + if exists ( + select 1 + from programmable_private.market_snapshots_v1 as snapshot + join programmable_private.market_snapshot_details as detail + on detail.market_snapshot_id = snapshot.market_snapshot_id + where snapshot.chain_id = header.chain_id + and snapshot.release_id = header.release_id + and snapshot.model_id = header.model_id + and snapshot.pool_id = p_pool_id + ) then + snapshot_backfill_audit_id := programmable_private.append_mutation_audit( + 'market_snapshot_lineage.backfill', p_page_commitment, + header.run_id, p_advanced_at + ); + insert into programmable_private.market_snapshot_lineage_memberships ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, reorg_generation, market_snapshot_id, + attached_reconciliation_id, attached_at, audit_id + ) + select + snapshot.chain_id, + snapshot.release_id, + snapshot.model_id, + header.source_group, + p_market_projector_version::programmable_private.projector_identifier, + snapshot.pool_id, + 0, + snapshot.market_snapshot_id, + p_reconciliation_id, + p_advanced_at, + snapshot_backfill_audit_id + from programmable_private.market_snapshots_v1 as snapshot + join programmable_private.market_snapshot_details as detail + on detail.market_snapshot_id = snapshot.market_snapshot_id + where snapshot.chain_id = header.chain_id + and snapshot.release_id = header.release_id + and snapshot.model_id = header.model_id + and snapshot.pool_id = p_pool_id + on conflict do nothing; + end if; + if exists ( + select 1 + from programmable_private.market_candles_v1 as candle + join programmable_private.market_candle_details as detail + on detail.market_candle_id = candle.market_candle_id + where candle.chain_id = header.chain_id + and candle.release_id = header.release_id + and candle.model_id = header.model_id + and candle.pool_id = p_pool_id + ) then + candle_backfill_audit_id := programmable_private.append_mutation_audit( + 'market_candle_lineage.backfill', p_page_commitment, + header.run_id, p_advanced_at + ); + insert into programmable_private.market_candle_lineage_memberships ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, reorg_generation, market_candle_id, + attached_reconciliation_id, attached_at, audit_id + ) + select + candle.chain_id, + candle.release_id, + candle.model_id, + header.source_group, + p_market_projector_version::programmable_private.projector_identifier, + candle.pool_id, + 0, + candle.market_candle_id, + p_reconciliation_id, + p_advanced_at, + candle_backfill_audit_id + from programmable_private.market_candles_v1 as candle + join programmable_private.market_candle_details as detail + on detail.market_candle_id = candle.market_candle_id + where candle.chain_id = header.chain_id + and candle.release_id = header.release_id + and candle.model_id = header.model_id + and candle.pool_id = p_pool_id + on conflict do nothing; + end if; + if exists ( + select 1 + from programmable_private.market_snapshots_v1 as snapshot + join programmable_private.market_snapshot_details as detail + on detail.market_snapshot_id = snapshot.market_snapshot_id + where snapshot.chain_id = header.chain_id + and snapshot.release_id = header.release_id + and snapshot.model_id = header.model_id + and snapshot.pool_id = p_pool_id + and not exists ( + select 1 + from programmable_private.market_snapshot_lineage_memberships as membership + where membership.chain_id = header.chain_id + and membership.release_id = header.release_id + and membership.model_id = header.model_id + and membership.source_group = header.source_group + and membership.projector_version = p_market_projector_version + and membership.pool_id = p_pool_id + and membership.reorg_generation = 0 + and membership.market_snapshot_id = snapshot.market_snapshot_id + ) + ) or exists ( + select 1 + from programmable_private.market_candles_v1 as candle + join programmable_private.market_candle_details as detail + on detail.market_candle_id = candle.market_candle_id + where candle.chain_id = header.chain_id + and candle.release_id = header.release_id + and candle.model_id = header.model_id + and candle.pool_id = p_pool_id + and not exists ( + select 1 + from programmable_private.market_candle_lineage_memberships as membership + where membership.chain_id = header.chain_id + and membership.release_id = header.release_id + and membership.model_id = header.model_id + and membership.source_group = header.source_group + and membership.projector_version = p_market_projector_version + and membership.pool_id = p_pool_id + and membership.reorg_generation = 0 + and membership.market_candle_id = candle.market_candle_id + ) + ) then + raise exception using + errcode = '23514', message = 'legacy market lineage backfill is incomplete'; + end if; + end if; + if previous_cursor.market_cursor_id is not null then + if is_rewind then + if p_next_reorg_generation <> p_expected_reorg_generation + 1 + or ( + header.epoch_id = previous_cursor.epoch_id + and header.captured_pointer_generation < + previous_cursor.pointer_generation + ) + or not ( + header.epoch_id <> previous_cursor.epoch_id + or header.captured_pointer_generation > + previous_cursor.pointer_generation + or p_source_reorg_generation > + previous_cursor.source_reorg_generation + ) + then + raise exception using + errcode = '23514', message = 'market rewind lacks a newer canonical generation'; + end if; + elsif p_next_reorg_generation <> p_expected_reorg_generation + or header.epoch_id <> previous_cursor.epoch_id + or header.captured_pointer_generation <> + previous_cursor.pointer_generation + or p_source_reorg_generation <> previous_cursor.source_reorg_generation + or p_source_checkpoint_generation < + previous_cursor.source_checkpoint_generation + or ( + p_source_checkpoint_generation = + previous_cursor.source_checkpoint_generation + and (normalized_target, p_provider_cursor) <= ( + previous_cursor.block_number::bigint, + previous_cursor.provider_cursor + ) + ) + or normalized_target < previous_cursor.block_number + or ( + normalized_target = previous_cursor.block_number + and p_target_block_hash <> previous_cursor.block_hash + ) + or ( + p_hour_coverage_end is not null + and previous_cursor.hour_coverage_end is not null + and p_hour_coverage_end < previous_cursor.hour_coverage_end + ) + or ( + p_day_coverage_end is not null + and previous_cursor.day_coverage_end is not null + and p_day_coverage_end < previous_cursor.day_coverage_end + ) + then + raise exception using + errcode = '23514', message = 'market cursor did not advance canonically'; + end if; + end if; + if not exists ( + select 1 + from programmable_private.market_snapshots as snapshot + join programmable_private.market_snapshot_details as detail + on detail.market_snapshot_id = snapshot.market_snapshot_id + join programmable_private.market_snapshot_lineage_memberships as membership + on membership.chain_id = header.chain_id + and membership.release_id = header.release_id + and membership.model_id = header.model_id + and membership.source_group = header.source_group + and membership.projector_version = p_market_projector_version + and membership.pool_id = p_pool_id + and membership.reorg_generation = p_next_reorg_generation + and membership.market_snapshot_id = snapshot.market_snapshot_id + and programmable_private.market_fact_reconciliation_usable_v1( + membership.attached_reconciliation_id, p_reconciliation_id + ) + join programmable_private.global_eth_usd_snapshots as global_snapshot + on global_snapshot.global_market_snapshot_id = + detail.global_market_snapshot_id + where snapshot.chain_id = header.chain_id + and snapshot.pool_id = p_pool_id + and snapshot.block_number = normalized_target + and snapshot.block_hash = p_target_block_hash + and programmable_private.market_fact_reconciliation_usable_v1( + snapshot.reconciliation_id, p_reconciliation_id + ) + and global_snapshot.block_evidence_id = snapshot.block_evidence_id + and global_snapshot.block_number = snapshot.block_number + and global_snapshot.block_hash = snapshot.block_hash + and global_snapshot.chain_id = header.chain_id + and global_snapshot.release_id = header.release_id + and global_snapshot.model_id = header.model_id + and global_snapshot.source_group = header.source_group + and global_snapshot.epoch_id = header.epoch_id + and global_snapshot.pointer_generation = + header.captured_pointer_generation + ) then + raise exception using + errcode = '23514', message = 'market cursor target lineage is incomplete'; + end if; + if exists ( + select 1 + from programmable_private.market_snapshots as snapshot + join programmable_private.market_snapshot_details as detail + on detail.market_snapshot_id = snapshot.market_snapshot_id + where snapshot.chain_id = header.chain_id + and snapshot.pool_id = p_pool_id + and snapshot.reconciliation_id = p_reconciliation_id + and not exists ( + select 1 + from programmable_private.market_snapshot_lineage_memberships as membership + where membership.chain_id = header.chain_id + and membership.release_id = header.release_id + and membership.model_id = header.model_id + and membership.source_group = header.source_group + and membership.projector_version = p_market_projector_version + and membership.pool_id = p_pool_id + and membership.reorg_generation = p_next_reorg_generation + and membership.market_snapshot_id = snapshot.market_snapshot_id + and membership.attached_reconciliation_id = p_reconciliation_id + ) + ) or exists ( + select 1 + from programmable_private.market_candles as candle + join programmable_private.market_candle_details as detail + on detail.market_candle_id = candle.market_candle_id + where candle.chain_id = header.chain_id + and candle.pool_id = p_pool_id + and candle.reconciliation_id = p_reconciliation_id + and not exists ( + select 1 + from programmable_private.market_candle_lineage_memberships as membership + where membership.chain_id = header.chain_id + and membership.release_id = header.release_id + and membership.model_id = header.model_id + and membership.source_group = header.source_group + and membership.projector_version = p_market_projector_version + and membership.pool_id = p_pool_id + and membership.reorg_generation = p_next_reorg_generation + and membership.market_candle_id = candle.market_candle_id + and membership.attached_reconciliation_id = p_reconciliation_id + ) + ) then + raise exception using + errcode = '23514', message = 'market page lineage is incomplete'; + end if; + if exists ( + select 1 + from ( + select distinct on (occurrence.block_number, occurrence.block_hash) + occurrence.occurrence_id, + occurrence.block_number, + occurrence.block_hash + from programmable_private.chain_event_occurrences as occurrence + join programmable_private.chain_event_occurrence_materializations as materialization + on materialization.occurrence_id = occurrence.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = header.captured_pointer_generation + and programmable_private.is_market_fee_event_v1( + header.model_id, materialization.event_type + ) + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = occurrence.occurrence_id + and canonical.logical_event_id = occurrence.logical_event_id + and canonical.block_hash = occurrence.block_hash + where occurrence.chain_id = header.chain_id + and occurrence.block_number > coverage_from_exclusive + and occurrence.block_number <= normalized_target + and pg_catalog.lower(materialization.decoded_payload ->> 'poolId') = + '0x' || pg_catalog.encode(p_pool_id, 'hex') + order by occurrence.block_number, occurrence.block_hash, + occurrence.transaction_index desc, + occurrence.block_global_log_index desc, + occurrence.occurrence_id desc + ) as required_close + where not exists ( + select 1 + from programmable_private.market_block_closes as close_fact + join programmable_private.reconciliation_records as close_reconciliation + on close_reconciliation.reconciliation_id = + close_fact.reconciliation_id + and close_reconciliation.mismatch_count = 0 + join programmable_private.run_headers as close_run + on close_run.run_id = close_reconciliation.run_id + and close_run.run_kind = 'reconciliation' + left join programmable_private.run_lifecycle_outcomes as close_outcome + on close_outcome.run_id = close_run.run_id + where close_fact.chain_id = header.chain_id + and close_fact.release_id = header.release_id + and close_fact.model_id = header.model_id + and close_fact.source_group = header.source_group + and close_fact.epoch_id = header.epoch_id + and close_fact.pointer_generation = header.captured_pointer_generation + and close_fact.pool_id = p_pool_id + and close_fact.last_source_occurrence_id = required_close.occurrence_id + and close_fact.block_number = required_close.block_number + and close_fact.block_hash = required_close.block_hash + and ( + close_fact.reconciliation_id = p_reconciliation_id + or close_outcome.status = 'succeeded' + ) + ) + ) then + raise exception using + errcode = '23514', message = 'market cursor coverage contains a close gap'; + end if; + created_audit_id := programmable_private.append_mutation_audit( + case when is_rewind then 'market_cursor.rewind' + else 'market_cursor.advance' end, + p_page_commitment, header.run_id, p_advanced_at + ); + insert into programmable_private.market_projector_cursor_history ( + market_cursor_id, chain_id, release_id, model_id, source_group, + projector_version, pool_id, epoch_id, pointer_generation, + cursor_generation, reorg_generation, source_checkpoint_id, + source_checkpoint_generation, source_reorg_generation, + block_evidence_id, block_number, block_hash, provider_cursor, + hour_coverage_end, day_coverage_end, page_commitment, + reconciliation_id, advanced_at, audit_id + ) values ( + p_market_cursor_id, header.chain_id, header.release_id, + header.model_id, header.source_group, + p_market_projector_version::programmable_private.projector_identifier, + p_pool_id::programmable_private.bytes32_value, + header.epoch_id, header.captured_pointer_generation, + p_next_cursor_generation, p_next_reorg_generation, + p_source_checkpoint_id, p_source_checkpoint_generation, + p_source_reorg_generation, p_target_block_evidence_id, + normalized_target::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_provider_cursor, p_hour_coverage_end, p_day_coverage_end, + p_page_commitment::programmable_private.bytes32_value, + p_reconciliation_id, p_advanced_at, created_audit_id + ); + if p_expected_cursor_generation = 0 then + insert into programmable_private.market_projector_cursor_current ( + chain_id, release_id, model_id, source_group, projector_version, + pool_id, market_cursor_id, cursor_generation, reorg_generation, + changed_at, changed_by_audit_id + ) values ( + header.chain_id, header.release_id, header.model_id, + header.source_group, + p_market_projector_version::programmable_private.projector_identifier, + p_pool_id::programmable_private.bytes32_value, + p_market_cursor_id, p_next_cursor_generation, + p_next_reorg_generation, p_advanced_at, created_audit_id + ) on conflict ( + chain_id, release_id, model_id, source_group, projector_version, pool_id + ) do nothing; + else + update programmable_private.market_projector_cursor_current + set market_cursor_id = p_market_cursor_id, + cursor_generation = p_next_cursor_generation, + reorg_generation = p_next_reorg_generation, + changed_at = p_advanced_at, + changed_by_audit_id = created_audit_id + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_market_projector_version + and pool_id = p_pool_id + and cursor_generation = p_expected_cursor_generation + and reorg_generation = p_expected_reorg_generation; + end if; + if not found then + raise exception using + errcode = '40001', message = 'market cursor CAS lost'; + end if; + return p_market_cursor_id; +end +$function$; + +-- V1 required the candle and its closing block to be written by one +-- reconciliation. That prevents finalizing a candle after its closing block +-- was safely projected by an earlier run. V2 binds the exact current-epoch +-- close instead, while requiring it to be the last close in the period. +create function programmable_private.append_market_candle_details_v2( + p_market_candle_id uuid, + p_reconciliation_id uuid, + p_projector_version text, + p_reorg_generation bigint, + p_closing_market_block_close_id uuid, + p_fees_usd numeric, + p_transaction_count bigint, + p_detail_commitment bytea, + p_recorded_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + candle programmable_private.market_candles%rowtype; + close_fact programmable_private.market_block_closes%rowtype; + context record; + existing programmable_private.market_candle_details%rowtype; + created_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + select * into candle from programmable_private.market_candles + where market_candle_id = p_market_candle_id; + select * into context from programmable_private.market_reconciliation_context_v1( + p_reconciliation_id, candle.source_block_evidence_id, + candle.source_block_hash + ); + select candidate.* into close_fact + from programmable_private.market_block_closes as candidate + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = candidate.last_source_occurrence_id + and canonical.logical_event_id = candidate.last_source_logical_event_id + and canonical.block_hash = candidate.last_source_occurrence_block_hash + join programmable_private.reconciliation_records as close_reconciliation + on close_reconciliation.reconciliation_id = candidate.reconciliation_id + and close_reconciliation.mismatch_count = 0 + join programmable_private.run_headers as close_run + on close_run.run_id = close_reconciliation.run_id + and close_run.run_kind = 'reconciliation' + left join programmable_private.run_lifecycle_outcomes as close_outcome + on close_outcome.run_id = close_run.run_id + where candidate.market_block_close_id = p_closing_market_block_close_id + and candidate.chain_id = candle.chain_id + and candidate.release_id = context.release_id + and candidate.model_id = context.model_id + and candidate.source_group = context.source_group + and candidate.epoch_id = context.epoch_id + and candidate.pointer_generation = context.pointer_generation + and candidate.pool_id = candle.pool_id + and candidate.block_timestamp >= candle.period_start + and candidate.block_timestamp < candle.period_end + and ( + candidate.reconciliation_id = p_reconciliation_id + or close_outcome.status = 'succeeded' + ); + if candle.market_candle_id is null or context.run_id is null + or close_fact.market_block_close_id is null + or not programmable_private.market_fact_reconciliation_usable_v1( + candle.reconciliation_id, p_reconciliation_id + ) + or p_fees_usd < 0 + or p_fees_usd::text in ('NaN', 'Infinity', '-Infinity') + or p_transaction_count < 0 + or pg_catalog.octet_length(p_detail_commitment) <> 32 + or exists ( + select 1 + from programmable_private.market_block_closes as later_close + join programmable_private.chain_event_current_canonical as later_canonical + on later_canonical.occurrence_id = + later_close.last_source_occurrence_id + and later_canonical.logical_event_id = + later_close.last_source_logical_event_id + and later_canonical.block_hash = + later_close.last_source_occurrence_block_hash + join programmable_private.reconciliation_records as later_reconciliation + on later_reconciliation.reconciliation_id = + later_close.reconciliation_id + and later_reconciliation.mismatch_count = 0 + join programmable_private.run_headers as later_run + on later_run.run_id = later_reconciliation.run_id + and later_run.run_kind = 'reconciliation' + left join programmable_private.run_lifecycle_outcomes as later_outcome + on later_outcome.run_id = later_run.run_id + where later_close.chain_id = candle.chain_id + and later_close.release_id = context.release_id + and later_close.model_id = context.model_id + and later_close.source_group = context.source_group + and later_close.epoch_id = context.epoch_id + and later_close.pointer_generation = context.pointer_generation + and later_close.pool_id = candle.pool_id + and later_close.block_timestamp >= candle.period_start + and later_close.block_timestamp < candle.period_end + and ( + later_close.reconciliation_id = p_reconciliation_id + or later_outcome.status = 'succeeded' + ) + and ( + later_close.block_number, + later_close.last_block_global_log_index, + later_close.market_block_close_id + ) > ( + close_fact.block_number, + close_fact.last_block_global_log_index, + close_fact.market_block_close_id + ) + ) + then + raise exception using + errcode = '23514', message = 'invalid exact candle close detail'; + end if; + select * into existing from programmable_private.market_candle_details + where market_candle_id = p_market_candle_id; + if found then + if existing.closing_market_block_close_id <> + p_closing_market_block_close_id + or existing.fees_usd <> p_fees_usd + or existing.transaction_count <> p_transaction_count + or existing.detail_commitment <> p_detail_commitment + then + raise exception using + errcode = '23505', message = 'market candle detail replay conflict'; + end if; + else + created_audit_id := programmable_private.append_mutation_audit( + 'market_candle_detail_v2.append', p_detail_commitment, + context.run_id, p_recorded_at + ); + insert into programmable_private.market_candle_details ( + market_candle_id, closing_market_block_close_id, + close_sqrt_price_x96, close_liquidity, close_tick, + close_token0_price, close_token1_price, close_tvl_usd, + fees_usd, transaction_count, global_market_snapshot_id, + detail_commitment, audit_id + ) values ( + p_market_candle_id, close_fact.market_block_close_id, + close_fact.sqrt_price_x96, close_fact.liquidity, close_fact.tick, + close_fact.token0_price, close_fact.token1_price, close_fact.tvl_usd, + p_fees_usd, p_transaction_count, + close_fact.global_market_snapshot_id, + p_detail_commitment::programmable_private.bytes32_value, + created_audit_id + ); + end if; + perform programmable_private.attach_market_candle_lineage_v1( + p_reconciliation_id, p_projector_version, p_reorg_generation, + p_market_candle_id, p_detail_commitment, p_recorded_at + ); + return p_market_candle_id; +end +$function$; + +revoke all on programmable_private.market_projector_cursor_history, + programmable_private.market_projector_cursor_current, + programmable_private.market_snapshot_lineage_memberships, + programmable_private.market_candle_lineage_memberships, + programmable_private.market_projector_runtime_lease_current, + programmable_private.market_projector_runtime_lease_history +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_projector_runtime; + +revoke all on function + programmable_private.try_acquire_market_projector_runtime_lease_v1( + text,bytea,timestamptz,timestamptz,bytea + ), + programmable_private.assert_market_projector_runtime_lease_v1( + text,bigint,bytea + ), + programmable_private.release_market_projector_runtime_lease_v1( + text,bigint,bytea,timestamptz,bytea + ), + programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea), + programmable_private.list_market_projector_pools_v1( + bigint,text,text,text,text,text,integer + ), + programmable_private.resolve_market_block_evidence_v1( + uuid,numeric,bytea,uuid,uuid + ), + programmable_private.resolve_market_close_anchor_v1( + uuid,bytea,numeric,bytea + ), + programmable_private.get_market_block_evidence_context_v1(uuid,uuid), + programmable_private.get_market_global_snapshot_v1(uuid,uuid), + programmable_private.list_market_close_anchors_v1( + bigint,text,text,text,text,bytea,numeric,numeric,integer,numeric + ), + programmable_private.resolve_market_candle_close_v1( + uuid,bytea,timestamptz,timestamptz + ), + programmable_private.append_market_snapshot_v2( + uuid,uuid,uuid,uuid,bytea,numeric,bytea,numeric,numeric,numeric, + numeric,numeric,numeric,timestamptz,bytea + ), + programmable_private.append_market_snapshot_details_v2( + uuid,uuid,text,bigint,uuid,integer,numeric,numeric,numeric,numeric, + numeric,bigint,bytea,timestamptz + ), + programmable_private.append_market_block_close_v2( + uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric, + numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid, + bytea,bytea,timestamptz + ), + programmable_private.get_market_projector_cursor_v1( + bigint,text,text,text,text,bytea + ), + programmable_private.advance_market_projector_cursor_v1( + uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint, + bigint,uuid,numeric,bytea,text,timestamptz,timestamptz,bytea,timestamptz + ), + programmable_private.append_market_candle_v2( + uuid,uuid,uuid,uuid,bytea,text,timestamptz,timestamptz,numeric, + numeric,numeric,numeric,numeric,numeric,numeric,bytea,bytea + ), + programmable_private.append_market_candle_details_v2( + uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_projector_runtime; + +-- The wider V1 append capabilities accept earlier price observations and are +-- not part of the exact-block market projector contract. +revoke execute on function + programmable_private.append_market_snapshot_details_v1( + uuid,uuid,integer,numeric,numeric,numeric,numeric,numeric, + bigint,bytea,timestamptz + ), + programmable_private.append_market_block_close_v1( + uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric, + numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid, + bytea,bytea,timestamptz + ), + programmable_private.append_market_candle_details_v1( + uuid,uuid,bytea,timestamptz + ) +from programmable_reconciler; + +revoke all on function + programmable_private.is_market_fee_event_v1(text,text), + programmable_private.market_fact_reconciliation_usable_v1(uuid,uuid), + programmable_private.attach_market_snapshot_lineage_v1( + uuid,text,bigint,uuid,bytea,timestamptz + ), + programmable_private.attach_market_candle_lineage_v1( + uuid,text,bigint,uuid,bytea,timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_projector_runtime; + +grant execute on function + programmable_private.try_acquire_market_projector_runtime_lease_v1( + text,bytea,timestamptz,timestamptz,bytea + ), + programmable_private.assert_market_projector_runtime_lease_v1( + text,bigint,bytea + ), + programmable_private.release_market_projector_runtime_lease_v1( + text,bigint,bytea,timestamptz,bytea + ), + programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea), + programmable_private.list_market_projector_pools_v1( + bigint,text,text,text,text,text,integer + ), + programmable_private.resolve_market_block_evidence_v1( + uuid,numeric,bytea,uuid,uuid + ), + programmable_private.resolve_market_close_anchor_v1( + uuid,bytea,numeric,bytea + ), + programmable_private.get_market_block_evidence_context_v1(uuid,uuid), + programmable_private.get_market_global_snapshot_v1(uuid,uuid), + programmable_private.list_market_close_anchors_v1( + bigint,text,text,text,text,bytea,numeric,numeric,integer,numeric + ), + programmable_private.resolve_market_candle_close_v1( + uuid,bytea,timestamptz,timestamptz + ), + programmable_private.append_market_snapshot_v2( + uuid,uuid,uuid,uuid,bytea,numeric,bytea,numeric,numeric,numeric, + numeric,numeric,numeric,timestamptz,bytea + ), + programmable_private.append_market_snapshot_details_v2( + uuid,uuid,text,bigint,uuid,integer,numeric,numeric,numeric,numeric, + numeric,bigint,bytea,timestamptz + ), + programmable_private.append_market_block_close_v2( + uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric, + numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid, + bytea,bytea,timestamptz + ), + programmable_private.get_market_projector_cursor_v1( + bigint,text,text,text,text,bytea + ), + programmable_private.advance_market_projector_cursor_v1( + uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint, + bigint,uuid,numeric,bytea,text,timestamptz,timestamptz,bytea,timestamptz + ), + programmable_private.append_market_candle_v2( + uuid,uuid,uuid,uuid,bytea,text,timestamptz,timestamptz,numeric, + numeric,numeric,numeric,numeric,numeric,numeric,bytea,bytea + ), + programmable_private.append_market_candle_details_v2( + uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamptz + ) +to programmable_reconciler; + +reset role; diff --git a/supabase/migrations/20260731224000_projector_provider_evidence_binding.sql b/supabase/migrations/20260731224000_projector_provider_evidence_binding.sql new file mode 100644 index 00000000..656d0722 --- /dev/null +++ b/supabase/migrations/20260731224000_projector_provider_evidence_binding.sql @@ -0,0 +1,7336 @@ +-- Promotion-bound provider evidence and atomic reward block groups. + +set role programmable_migrator; + +insert into programmable_private.fingerprint_encoding_versions ( + fingerprint_domain, encoding_version, domain_prefix, write_enabled, + definition_commitment, allowlisted_at +) values ( + 'evidence', 3, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763300', + 'hex' + ), + true, + pg_catalog.decode( + '3234e87ac53489e1cfefafa865b053e9723945930d060265c0e8084669a1e955', + 'hex' + ), + '2026-08-01T00:00:00Z' +); + +insert into programmable_private.provider_evidence_encoding_subtypes ( + evidence_subtype, encoding_version, subtype_tag, frame_prefix, + definition_commitment +) values + ( + 'projection_execution', 3, 6, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330006', + 'hex' + ), + pg_catalog.decode( + '4a101e2e339f883474c6d939016a1189ebd0bbfc3bd6df0a2fba37c5bd5ecf3a', + 'hex' + ) + ), + ( + 'reward_snapshot', 3, 7, + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330007', + 'hex' + ), + pg_catalog.decode( + '886a97852a33023bb6edd87bfb79e0acf5d5ededf8008d42cd78cd43ea071a95', + 'hex' + ) + ); + +create table programmable_private.projection_provider_execution_evidence ( + execution_evidence_id uuid primary key, + run_id uuid not null unique + references programmable_private.run_headers(run_id) + on delete restrict, + safe_head_observation_id uuid not null, + epoch_id uuid not null, + chain_id programmable_private.chain_id_value not null, + pointer_generation bigint not null check (pointer_generation > 0), + configured_provider_deployment_ids uuid[] not null, + envio_provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_a_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_a_vendor programmable_private.source_identifier not null, + provider_b_vendor programmable_private.source_identifier not null, + provider_a_identity programmable_private.source_identifier not null, + provider_b_identity programmable_private.source_identifier not null, + provider_a_endpoint_url_commitment + programmable_private.bytes32_value not null, + provider_b_endpoint_url_commitment + programmable_private.bytes32_value not null, + provider_a_endpoint_origin_commitment + programmable_private.bytes32_value not null, + provider_b_endpoint_origin_commitment + programmable_private.bytes32_value not null, + provider_a_call_count smallint not null + check (provider_a_call_count between 1 and 128), + provider_b_call_count smallint not null + check (provider_b_call_count between 1 and 128), + candidate_batch_size smallint not null + check (candidate_batch_size between 0 and 4096), + hard_deadline_ms integer not null + check (hard_deadline_ms between 10 and 75000), + maximum_calls_per_provider smallint not null + check (maximum_calls_per_provider between 1 and 128), + elapsed_ms integer not null check (elapsed_ms between 0 and 75000), + execution_trace jsonb not null, + execution_trace_preimage bytea not null, + execution_trace_commitment programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version = 3), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key ( + safe_head_observation_id, epoch_id, chain_id, pointer_generation + ) references programmable_private.safe_head_observations( + observation_id, epoch_id, chain_id, pointer_generation + ) on delete restrict, + unique (execution_evidence_id, run_id), + unique (epoch_id, content_fingerprint), + check ( + configured_provider_deployment_ids = array[ + envio_provider_deployment_id, provider_a_id, provider_b_id + ]::uuid[] + ), + check (provider_a_id <> provider_b_id), + check ( + provider_a_vendor = 'alchemy' + and provider_b_vendor = 'quicknode' + ), + check ( + pg_catalog.octet_length(canonical_preimage) >= 35 + and pg_catalog.substring(canonical_preimage, 1, 35) = + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330006', + 'hex' + ) + ) +); + +create table programmable_private.reward_snapshot_provider_evidence ( + reward_snapshot_evidence_id uuid primary key, + run_id uuid not null, + execution_evidence_id uuid not null, + safe_head_observation_id uuid not null, + target_block_evidence_id uuid not null, + epoch_id uuid not null, + chain_id programmable_private.chain_id_value not null, + pointer_generation bigint not null check (pointer_generation > 0), + vault programmable_private.eth_address not null, + model_id programmable_private.model_identifier not null, + reward_model programmable_private.model_identifier not null, + target_block_number programmable_private.block_number_value not null, + target_block_hash programmable_private.bytes32_value not null, + provider_a_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_a_snapshot_commitment + programmable_private.bytes32_value not null, + provider_b_snapshot_commitment + programmable_private.bytes32_value not null, + provider_a_call_count integer not null + check (provider_a_call_count between 1 and 11008), + provider_b_call_count integer not null + check (provider_b_call_count between 1 and 11008), + verification_accounts programmable_private.eth_address[] not null, + verification_account_chunk_end_offsets integer[] not null, + provider_a_verification_chunk_commitments + programmable_private.bytes32_value[] not null, + provider_b_verification_chunk_commitments + programmable_private.bytes32_value[] not null, + provider_a_verification_chunk_call_counts smallint[] not null, + provider_b_verification_chunk_call_counts smallint[] not null, + folded_snapshot_preimage bytea not null, + folded_snapshot_commitment programmable_private.bytes32_value not null, + execution_trace jsonb not null, + execution_trace_preimage bytea not null, + execution_trace_commitment programmable_private.bytes32_value not null, + encoding_version smallint not null check (encoding_version = 3), + canonical_preimage bytea not null, + content_fingerprint programmable_private.bytes32_value not null, + verified_at timestamptz not null, + foreign key (execution_evidence_id, run_id) + references programmable_private.projection_provider_execution_evidence( + execution_evidence_id, run_id + ) on delete restrict, + foreign key ( + target_block_evidence_id, safe_head_observation_id, + epoch_id, chain_id, pointer_generation + ) references programmable_private.dual_rpc_block_evidence( + block_evidence_id, observation_id, epoch_id, chain_id, + pointer_generation + ) on delete restrict, + unique (run_id, vault), + unique (reward_snapshot_evidence_id, run_id, execution_evidence_id), + unique (epoch_id, content_fingerprint), + check (provider_a_id <> provider_b_id), + check ( + provider_a_snapshot_commitment = provider_b_snapshot_commitment + ), + check (provider_a_call_count = provider_b_call_count), + check (pg_catalog.cardinality(verification_accounts) between 1 and 4096), + check ( + pg_catalog.cardinality(verification_account_chunk_end_offsets) + between 1 and 86 + and pg_catalog.cardinality(verification_account_chunk_end_offsets) = + ( + (pg_catalog.cardinality(verification_accounts) + 47) / 48 + ) + and verification_account_chunk_end_offsets[ + pg_catalog.cardinality(verification_account_chunk_end_offsets) + ] = pg_catalog.cardinality(verification_accounts) + ), + check ( + pg_catalog.cardinality(verification_account_chunk_end_offsets) + = pg_catalog.cardinality( + provider_a_verification_chunk_commitments + ) + and pg_catalog.cardinality(verification_account_chunk_end_offsets) + = pg_catalog.cardinality( + provider_b_verification_chunk_commitments + ) + and pg_catalog.cardinality(verification_account_chunk_end_offsets) + = pg_catalog.cardinality( + provider_a_verification_chunk_call_counts + ) + and pg_catalog.cardinality(verification_account_chunk_end_offsets) + = pg_catalog.cardinality( + provider_b_verification_chunk_call_counts + ) + ), + check ( + provider_a_verification_chunk_commitments = + provider_b_verification_chunk_commitments + and provider_a_verification_chunk_call_counts = + provider_b_verification_chunk_call_counts + ), + check ( + pg_catalog.octet_length(canonical_preimage) >= 35 + and pg_catalog.substring(canonical_preimage, 1, 35) = + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330007', + 'hex' + ) + ) +); + +create table programmable_private.projection_publication_provider_bindings ( + provider_binding_id uuid primary key, + publication_id uuid not null unique + references programmable_private.projection_publications(publication_id) + on delete restrict, + run_id uuid not null unique, + promotion_mode programmable_private.source_identifier not null, + execution_evidence_id uuid not null, + reward_snapshot_evidence_ids uuid[] not null, + provider_binding_commitment programmable_private.bytes32_value not null, + bound_at timestamptz not null, + foreign key (execution_evidence_id, run_id) + references programmable_private.projection_provider_execution_evidence( + execution_evidence_id, run_id + ) on delete restrict, + check (promotion_mode = 'exact_incremental'), + check ( + provider_binding_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ) +); + +create table programmable_private.projection_publication_reward_evidence ( + provider_binding_id uuid not null + references programmable_private.projection_publication_provider_bindings( + provider_binding_id + ) on delete restrict, + evidence_ordinal smallint not null check (evidence_ordinal >= 1), + reward_snapshot_evidence_id uuid not null, + run_id uuid not null, + execution_evidence_id uuid not null, + vault programmable_private.eth_address not null, + primary key (provider_binding_id, evidence_ordinal), + unique (provider_binding_id, reward_snapshot_evidence_id), + unique (provider_binding_id, vault), + foreign key ( + reward_snapshot_evidence_id, run_id, execution_evidence_id + ) references programmable_private.reward_snapshot_provider_evidence( + reward_snapshot_evidence_id, run_id, execution_evidence_id + ) on delete restrict +); + +create table programmable_private.provisional_dynamic_parent_pages ( + provisional_page_id uuid primary key, + staging_run_id uuid not null, + chain_id programmable_private.chain_id_value not null, + release_id programmable_private.release_identifier not null, + model_id programmable_private.model_identifier not null, + source_group programmable_private.source_identifier not null, + projector_version programmable_private.projector_identifier not null, + release_epoch_id uuid not null, + release_pointer_generation bigint not null + check (release_pointer_generation > 0), + ingestion_epoch_id uuid not null, + ingestion_pointer_generation bigint not null + check (ingestion_pointer_generation > 0), + reorg_generation bigint not null check (reorg_generation >= 0), + expected_cursor_generation bigint not null + check (expected_cursor_generation >= 0), + expected_cursor_block_hash programmable_private.bytes32_value not null, + envio_provider_deployment_id uuid not null + references programmable_private.provider_deployments( + provider_deployment_id + ) on delete restrict, + stream_id programmable_private.source_identifier not null, + safe_head_observation_id uuid not null, + target_block_evidence_id uuid not null, + provider_a_id uuid not null + references programmable_private.provider_deployments( + provider_deployment_id + ) on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments( + provider_deployment_id + ) on delete restrict, + filter_commitment programmable_private.bytes32_value not null, + provider_a_parent_commitments + programmable_private.bytes32_value[] not null, + provider_b_parent_commitments + programmable_private.bytes32_value[] not null, + execution_trace jsonb not null, + execution_trace_preimage bytea not null, + execution_trace_commitment programmable_private.bytes32_value not null, + coverage_commitment programmable_private.bytes32_value not null, + snapshot_block_number programmable_private.block_number_value not null, + snapshot_block_hash programmable_private.bytes32_value not null, + parent_candidate_ids + programmable_private.envio_candidate_identifier[] not null, + parent_candidate_commitments + programmable_private.bytes32_value[] not null, + parent_candidates jsonb not null, + parent_set_commitment programmable_private.bytes32_value not null, + staged_at timestamptz not null, + foreign key ( + staging_run_id, ingestion_epoch_id, ingestion_pointer_generation + ) + references programmable_private.run_headers( + run_id, epoch_id, captured_pointer_generation + ) on delete restrict, + foreign key ( + release_epoch_id, chain_id, release_id, model_id, source_group + ) references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + foreign key ( + target_block_evidence_id, safe_head_observation_id, + ingestion_epoch_id, chain_id, ingestion_pointer_generation + ) references programmable_private.dual_rpc_block_evidence( + block_evidence_id, observation_id, epoch_id, chain_id, + pointer_generation + ) on delete restrict, + unique ( + chain_id, release_id, model_id, source_group, projector_version, + release_epoch_id, release_pointer_generation, reorg_generation, + snapshot_block_number, snapshot_block_hash + ), + unique ( + release_epoch_id, release_pointer_generation, parent_set_commitment + ), + check (provider_a_id <> provider_b_id), + check ( + provider_a_parent_commitments = provider_b_parent_commitments + and provider_a_parent_commitments = parent_candidate_commitments + and pg_catalog.cardinality(provider_a_parent_commitments) + between 1 and 32 + and programmable_private.valid_topics( + provider_a_parent_commitments + ) + ), + check ( + execution_trace_commitment = + pg_catalog.sha256(execution_trace_preimage) + and coverage_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ), + check ( + pg_catalog.cardinality(parent_candidate_ids) between 1 and 32 + and pg_catalog.cardinality(parent_candidate_ids) = + pg_catalog.cardinality(parent_candidate_commitments) + and pg_catalog.cardinality(parent_candidate_ids) = + pg_catalog.jsonb_array_length(parent_candidates) + ), + check ( + parent_set_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ) +); + +create table programmable_private.provisional_dynamic_source_lineages ( + provisional_lineage_id uuid primary key, + provisional_page_id uuid not null + references programmable_private.provisional_dynamic_parent_pages( + provisional_page_id + ) on delete restrict, + lineage_ordinal smallint not null check (lineage_ordinal >= 1), + dynamic_source_attestation_id uuid not null unique, + dynamic_source_template_id uuid not null + references programmable_private.release_dynamic_source_templates( + dynamic_source_template_id + ) on delete restrict, + runtime_code_evidence_id uuid not null unique + references programmable_private.dual_rpc_runtime_code_evidence( + runtime_code_evidence_id + ) on delete restrict, + parent_candidate_id + programmable_private.envio_candidate_identifier not null, + parent_candidate_commitment programmable_private.bytes32_value not null, + deployed_source_address programmable_private.eth_address not null, + unique (provisional_page_id, lineage_ordinal), + unique (provisional_page_id, parent_candidate_id), + unique (provisional_page_id, deployed_source_address), + check ( + parent_candidate_commitment <> + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + ) +); + +create table programmable_private.provisional_dynamic_parent_consumptions ( + provisional_page_id uuid primary key + references programmable_private.provisional_dynamic_parent_pages( + provisional_page_id + ) on delete restrict, + final_run_id uuid not null, + publication_id uuid not null + references programmable_private.projection_publications(publication_id) + on delete restrict, + final_execution_evidence_id uuid not null, + final_target_block_evidence_id uuid not null, + consumed_at timestamptz not null, + foreign key (final_execution_evidence_id, final_run_id) + references programmable_private.projection_provider_execution_evidence( + execution_evidence_id, run_id + ) on delete restrict, + unique (final_run_id, provisional_page_id) +); + +alter table programmable_private.projection_provider_execution_evidence + enable row level security; +alter table programmable_private.projection_provider_execution_evidence + force row level security; +alter table programmable_private.reward_snapshot_provider_evidence + enable row level security; +alter table programmable_private.reward_snapshot_provider_evidence + force row level security; +alter table programmable_private.projection_publication_provider_bindings + enable row level security; +alter table programmable_private.projection_publication_provider_bindings + force row level security; +alter table programmable_private.projection_publication_reward_evidence + enable row level security; +alter table programmable_private.projection_publication_reward_evidence + force row level security; +alter table programmable_private.provisional_dynamic_parent_pages + enable row level security; +alter table programmable_private.provisional_dynamic_parent_pages + force row level security; +alter table programmable_private.provisional_dynamic_source_lineages + enable row level security; +alter table programmable_private.provisional_dynamic_source_lineages + force row level security; +alter table programmable_private.provisional_dynamic_parent_consumptions + enable row level security; +alter table programmable_private.provisional_dynamic_parent_consumptions + force row level security; + +create policy projection_provider_execution_evidence_migrator_all +on programmable_private.projection_provider_execution_evidence +for all to programmable_migrator using (true) with check (true); + +create policy reward_snapshot_provider_evidence_migrator_all +on programmable_private.reward_snapshot_provider_evidence +for all to programmable_migrator using (true) with check (true); + +create policy projection_publication_provider_bindings_migrator_all +on programmable_private.projection_publication_provider_bindings +for all to programmable_migrator using (true) with check (true); + +create policy projection_publication_reward_evidence_migrator_all +on programmable_private.projection_publication_reward_evidence +for all to programmable_migrator using (true) with check (true); + +create policy provisional_dynamic_parent_pages_migrator_all +on programmable_private.provisional_dynamic_parent_pages +for all to programmable_migrator using (true) with check (true); + +create policy provisional_dynamic_source_lineages_migrator_all +on programmable_private.provisional_dynamic_source_lineages +for all to programmable_migrator using (true) with check (true); + +create policy provisional_dynamic_parent_consumptions_migrator_all +on programmable_private.provisional_dynamic_parent_consumptions +for all to programmable_migrator using (true) with check (true); + +create trigger projection_provider_execution_evidence_immutable +before update or delete +on programmable_private.projection_provider_execution_evidence +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger reward_snapshot_provider_evidence_immutable +before update or delete +on programmable_private.reward_snapshot_provider_evidence +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger projection_publication_provider_bindings_immutable +before update or delete +on programmable_private.projection_publication_provider_bindings +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger projection_publication_reward_evidence_immutable +before update or delete +on programmable_private.projection_publication_reward_evidence +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger provisional_dynamic_parent_pages_immutable +before update or delete +on programmable_private.provisional_dynamic_parent_pages +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger provisional_dynamic_source_lineages_immutable +before update or delete +on programmable_private.provisional_dynamic_source_lineages +for each row execute function programmable_private.reject_immutable_mutation(); + +create trigger provisional_dynamic_parent_consumptions_immutable +before update or delete +on programmable_private.provisional_dynamic_parent_consumptions +for each row execute function programmable_private.reject_immutable_mutation(); + +-- The trace commitment is a frozen structural binary encoding. JSON is only +-- the transport envelope; key order and serializer whitespace never enter the +-- commitment domain. +create function programmable_private.projection_execution_trace_preimage_v1( + p_execution_trace jsonb +) +returns bytea +language plpgsql +immutable +security invoker +set search_path = '' +as $function$ +declare + expected_top_keys text[] := array[ + 'calls', 'candidateBatchSize', 'completedAtMs', 'elapsedMs', + 'hardDeadlineMs', 'maxCallsPerProvider', 'providerCallCounts', + 'startedAtMs' + ]; + expected_call_keys text[] := array[ + 'attempt', 'durationMs', 'operation', 'outcome', + 'providerEndpointCommitment', 'providerIdentity', + 'providerOriginCommitment', 'providerVendorGroup', + 'startedOffsetMs' + ]; + actual_keys text[]; + call_item jsonb; + call_bytes bytea := ''::bytea; + identity_bytes bytea; + vendor_bytes bytea; + endpoint_bytes bytea; + origin_bytes bytea; + operation_tag integer; + outcome_tag integer; + started_at numeric; + completed_at numeric; + candidate_batch_size numeric; + hard_deadline numeric; + maximum_calls numeric; + elapsed numeric; + call_count_a numeric; + call_count_b numeric; + attempt_number numeric; + started_offset numeric; + duration numeric; +begin + if p_execution_trace is null + or pg_catalog.jsonb_typeof(p_execution_trace) <> 'object' + then + raise exception using + errcode = '22023', message = 'invalid execution trace encoding'; + end if; + select pg_catalog.array_agg(key order by key) into actual_keys + from pg_catalog.jsonb_object_keys(p_execution_trace) as key; + if actual_keys is distinct from expected_top_keys + or p_execution_trace ->> 'startedAtMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'completedAtMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'candidateBatchSize' !~ '^[0-9]+$' + or p_execution_trace ->> 'hardDeadlineMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'maxCallsPerProvider' !~ '^[0-9]+$' + or p_execution_trace ->> 'elapsedMs' !~ '^[0-9]+$' + or pg_catalog.jsonb_typeof( + p_execution_trace -> 'providerCallCounts' + ) <> 'array' + or pg_catalog.jsonb_array_length( + p_execution_trace -> 'providerCallCounts' + ) <> 2 + or p_execution_trace #>> '{providerCallCounts,0}' !~ '^[0-9]+$' + or p_execution_trace #>> '{providerCallCounts,1}' !~ '^[0-9]+$' + or pg_catalog.jsonb_typeof(p_execution_trace -> 'calls') <> 'array' + then + raise exception using + errcode = '22023', message = 'execution trace encoding shape changed'; + end if; + + started_at := (p_execution_trace ->> 'startedAtMs')::numeric; + completed_at := (p_execution_trace ->> 'completedAtMs')::numeric; + candidate_batch_size := + (p_execution_trace ->> 'candidateBatchSize')::numeric; + hard_deadline := (p_execution_trace ->> 'hardDeadlineMs')::numeric; + maximum_calls := + (p_execution_trace ->> 'maxCallsPerProvider')::numeric; + elapsed := (p_execution_trace ->> 'elapsedMs')::numeric; + call_count_a := + (p_execution_trace #>> '{providerCallCounts,0}')::numeric; + call_count_b := + (p_execution_trace #>> '{providerCallCounts,1}')::numeric; + if started_at not between 0 and 9223372036854775807 + or completed_at not between 0 and 9223372036854775807 + or candidate_batch_size not between 0 and 4096 + or hard_deadline not between 10 and 75000 + or maximum_calls not between 1 and 128 + or elapsed not between 0 and 75000 + or call_count_a not between 0 and 11008 + or call_count_b not between 0 and 11008 + or pg_catalog.trunc(started_at) <> started_at + or pg_catalog.trunc(completed_at) <> completed_at + or pg_catalog.trunc(candidate_batch_size) <> candidate_batch_size + or pg_catalog.trunc(hard_deadline) <> hard_deadline + or pg_catalog.trunc(maximum_calls) <> maximum_calls + or pg_catalog.trunc(elapsed) <> elapsed + or pg_catalog.trunc(call_count_a) <> call_count_a + or pg_catalog.trunc(call_count_b) <> call_count_b + or pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') < 1 + or pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') > 256 + then + raise exception using + errcode = '22023', message = 'execution trace encoding is out of bounds'; + end if; + + for call_item in + select value + from pg_catalog.jsonb_array_elements(p_execution_trace -> 'calls') + with ordinality as calls(value, ordinality) + order by ordinality + loop + if pg_catalog.jsonb_typeof(call_item) <> 'object' then + raise exception using + errcode = '22023', message = 'execution trace call is invalid'; + end if; + select pg_catalog.array_agg(key order by key) into actual_keys + from pg_catalog.jsonb_object_keys(call_item) as key; + if actual_keys is distinct from expected_call_keys + or call_item ->> 'attempt' !~ '^[0-9]+$' + or call_item ->> 'startedOffsetMs' !~ '^[0-9]+$' + or call_item ->> 'durationMs' !~ '^[0-9]+$' + or call_item ->> 'providerEndpointCommitment' + !~ '^0x[0-9a-f]{64}$' + or call_item ->> 'providerOriginCommitment' + !~ '^0x[0-9a-f]{64}$' + or coalesce(call_item ->> 'providerIdentity', '') = '' + or coalesce(call_item ->> 'providerVendorGroup', '') = '' + then + raise exception using + errcode = '22023', message = 'execution trace call shape changed'; + end if; + identity_bytes := pg_catalog.convert_to( + call_item ->> 'providerIdentity', 'UTF8' + ); + vendor_bytes := pg_catalog.convert_to( + call_item ->> 'providerVendorGroup', 'UTF8' + ); + endpoint_bytes := pg_catalog.decode( + pg_catalog.substring( + call_item ->> 'providerEndpointCommitment', 3 + ), 'hex' + ); + origin_bytes := pg_catalog.decode( + pg_catalog.substring( + call_item ->> 'providerOriginCommitment', 3 + ), 'hex' + ); + operation_tag := case call_item ->> 'operation' + when 'getChainId' then 1 + when 'getBlockNumber' then 2 + when 'getBlock' then 3 + when 'getTransactionReceipt' then 4 + when 'getBytecode' then 5 + when 'readRewardSnapshot' then 6 + else 0 + end; + outcome_tag := case call_item ->> 'outcome' + when 'success' then 1 + when 'error' then 2 + else 0 + end; + attempt_number := (call_item ->> 'attempt')::numeric; + started_offset := (call_item ->> 'startedOffsetMs')::numeric; + duration := (call_item ->> 'durationMs')::numeric; + if pg_catalog.octet_length(identity_bytes) not between 1 and 512 + or pg_catalog.octet_length(vendor_bytes) not between 1 and 512 + or operation_tag = 0 + or outcome_tag = 0 + or attempt_number not between 1 and 3 + or started_offset not between 0 and 75000 + or duration not between 0 and 75000 + or pg_catalog.trunc(attempt_number) <> attempt_number + or pg_catalog.trunc(started_offset) <> started_offset + or pg_catalog.trunc(duration) <> duration + then + raise exception using + errcode = '22023', message = 'execution trace call is out of bounds'; + end if; + call_bytes := call_bytes + || pg_catalog.int4send(pg_catalog.octet_length(identity_bytes)) + || identity_bytes + || pg_catalog.int4send(pg_catalog.octet_length(vendor_bytes)) + || vendor_bytes + || endpoint_bytes + || origin_bytes + || pg_catalog.decode(pg_catalog.lpad( + pg_catalog.to_hex(operation_tag), 2, '0' + ), 'hex') + || pg_catalog.int4send(attempt_number::integer) + || pg_catalog.int4send(started_offset::integer) + || pg_catalog.int4send(duration::integer) + || pg_catalog.decode(pg_catalog.lpad( + pg_catalog.to_hex(outcome_tag), 2, '0' + ), 'hex'); + end loop; + + return pg_catalog.decode( + '70726f6772616d6d61626c653a70726f6a656374696f6e2d657865637574696f6e2d74726163653a763100', + 'hex' + ) + || pg_catalog.int8send(started_at::bigint) + || pg_catalog.int8send(completed_at::bigint) + || pg_catalog.int4send(candidate_batch_size::integer) + || pg_catalog.int4send(hard_deadline::integer) + || pg_catalog.int4send(maximum_calls::integer) + || pg_catalog.int4send(elapsed::integer) + || pg_catalog.int4send(call_count_a::integer) + || pg_catalog.int4send(call_count_b::integer) + || pg_catalog.int4send( + pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') + ) + || call_bytes; +end +$function$; + +create function programmable_private.projection_execution_trace_commitment_v1( + p_execution_trace jsonb +) +returns bytea +language sql +immutable +security invoker +set search_path = '' +as $function$ + select pg_catalog.sha256( + programmable_private.projection_execution_trace_preimage_v1( + p_execution_trace + ) + ) +$function$; + +create function programmable_private.projection_execution_evidence_preimage_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_run_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_provider_a_identity text, + p_provider_b_identity text, + p_provider_a_vendor_group text, + p_provider_b_vendor_group text, + p_provider_a_endpoint_commitment bytea, + p_provider_b_endpoint_commitment bytea, + p_provider_a_origin_commitment bytea, + p_provider_b_origin_commitment bytea, + p_provider_a_call_count integer, + p_provider_b_call_count integer, + p_candidate_batch_size integer, + p_hard_deadline_ms integer, + p_maximum_calls_per_provider integer, + p_elapsed_ms integer, + p_execution_trace_commitment bytea +) +returns bytea +language sql +immutable +security invoker +set search_path = '' +as $function$ + select + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330006', + 'hex' + ) + || pg_catalog.int8send(p_chain_id) + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_release_id, 'UTF8')) + ) + || pg_catalog.convert_to(p_release_id, 'UTF8') + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_model_id, 'UTF8')) + ) + || pg_catalog.convert_to(p_model_id, 'UTF8') + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_source_group, 'UTF8')) + ) + || pg_catalog.convert_to(p_source_group, 'UTF8') + || pg_catalog.uuid_send(p_epoch_id) + || pg_catalog.int8send(p_pointer_generation) + || pg_catalog.uuid_send(p_run_id) + || pg_catalog.uuid_send(p_provider_a_id) + || pg_catalog.uuid_send(p_provider_b_id) + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_provider_a_identity, 'UTF8')) + ) + || pg_catalog.convert_to(p_provider_a_identity, 'UTF8') + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_provider_b_identity, 'UTF8')) + ) + || pg_catalog.convert_to(p_provider_b_identity, 'UTF8') + || pg_catalog.int4send( + pg_catalog.octet_length( + pg_catalog.convert_to(p_provider_a_vendor_group, 'UTF8') + ) + ) + || pg_catalog.convert_to(p_provider_a_vendor_group, 'UTF8') + || pg_catalog.int4send( + pg_catalog.octet_length( + pg_catalog.convert_to(p_provider_b_vendor_group, 'UTF8') + ) + ) + || pg_catalog.convert_to(p_provider_b_vendor_group, 'UTF8') + || p_provider_a_endpoint_commitment + || p_provider_b_endpoint_commitment + || p_provider_a_origin_commitment + || p_provider_b_origin_commitment + || pg_catalog.int4send(p_provider_a_call_count) + || pg_catalog.int4send(p_provider_b_call_count) + || pg_catalog.int4send(p_candidate_batch_size) + || pg_catalog.int4send(p_hard_deadline_ms) + || pg_catalog.int4send(p_maximum_calls_per_provider) + || pg_catalog.int4send(p_elapsed_ms) + || p_execution_trace_commitment +$function$; + +create function programmable_private.reward_snapshot_evidence_preimage_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_run_id uuid, + p_execution_evidence_id uuid, + p_target_block_evidence_id uuid, + p_vault bytea, + p_reward_model text, + p_target_block_number bigint, + p_target_block_hash bytea, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_provider_a_snapshot_commitment bytea, + p_provider_b_snapshot_commitment bytea, + p_provider_a_call_count integer, + p_provider_b_call_count integer, + p_verification_accounts bytea[], + p_verification_account_chunk_end_offsets integer[], + p_provider_a_verification_chunk_commitments bytea[], + p_provider_b_verification_chunk_commitments bytea[], + p_provider_a_verification_chunk_call_counts integer[], + p_provider_b_verification_chunk_call_counts integer[], + p_folded_snapshot_commitment bytea, + p_execution_trace_commitment bytea +) +returns bytea +language sql +immutable +security invoker +set search_path = '' +as $function$ + select + pg_catalog.decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330007', + 'hex' + ) + || pg_catalog.int8send(p_chain_id) + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_release_id, 'UTF8')) + ) + || pg_catalog.convert_to(p_release_id, 'UTF8') + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_model_id, 'UTF8')) + ) + || pg_catalog.convert_to(p_model_id, 'UTF8') + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_source_group, 'UTF8')) + ) + || pg_catalog.convert_to(p_source_group, 'UTF8') + || pg_catalog.uuid_send(p_epoch_id) + || pg_catalog.int8send(p_pointer_generation) + || pg_catalog.uuid_send(p_run_id) + || pg_catalog.uuid_send(p_execution_evidence_id) + || pg_catalog.uuid_send(p_target_block_evidence_id) + || p_vault + || pg_catalog.int4send( + pg_catalog.octet_length(pg_catalog.convert_to(p_reward_model, 'UTF8')) + ) + || pg_catalog.convert_to(p_reward_model, 'UTF8') + || pg_catalog.int8send(p_target_block_number) + || p_target_block_hash + || pg_catalog.uuid_send(p_provider_a_id) + || pg_catalog.uuid_send(p_provider_b_id) + || p_provider_a_snapshot_commitment + || p_provider_b_snapshot_commitment + || pg_catalog.int4send(p_provider_a_call_count) + || pg_catalog.int4send(p_provider_b_call_count) + || pg_catalog.int4send(pg_catalog.cardinality(p_verification_accounts)) + || ( + select coalesce( + pg_catalog.string_agg( + account, ''::bytea order by account_ordinal + ), + ''::bytea + ) + from pg_catalog.unnest(p_verification_accounts) + with ordinality as accounts(account, account_ordinal) + ) + || pg_catalog.int4send(pg_catalog.cardinality( + p_verification_account_chunk_end_offsets + )) + || ( + select coalesce(pg_catalog.string_agg( + pg_catalog.int4send(chunk_end_offset), ''::bytea + order by chunk_ordinal + ), ''::bytea) + from pg_catalog.unnest(p_verification_account_chunk_end_offsets) + with ordinality as chunks(chunk_end_offset, chunk_ordinal) + ) + || pg_catalog.int4send(pg_catalog.cardinality( + p_provider_a_verification_chunk_commitments + )) + || ( + select coalesce(pg_catalog.string_agg( + commitment, ''::bytea order by chunk_ordinal + ), ''::bytea) + from pg_catalog.unnest(p_provider_a_verification_chunk_commitments) + with ordinality as chunks(commitment, chunk_ordinal) + ) + || pg_catalog.int4send(pg_catalog.cardinality( + p_provider_b_verification_chunk_commitments + )) + || ( + select coalesce(pg_catalog.string_agg( + commitment, ''::bytea order by chunk_ordinal + ), ''::bytea) + from pg_catalog.unnest(p_provider_b_verification_chunk_commitments) + with ordinality as chunks(commitment, chunk_ordinal) + ) + || pg_catalog.int4send(pg_catalog.cardinality( + p_provider_a_verification_chunk_call_counts + )) + || ( + select coalesce(pg_catalog.string_agg( + pg_catalog.int4send(call_count), ''::bytea + order by chunk_ordinal + ), ''::bytea) + from pg_catalog.unnest(p_provider_a_verification_chunk_call_counts) + with ordinality as chunks(call_count, chunk_ordinal) + ) + || pg_catalog.int4send(pg_catalog.cardinality( + p_provider_b_verification_chunk_call_counts + )) + || ( + select coalesce(pg_catalog.string_agg( + pg_catalog.int4send(call_count), ''::bytea + order by chunk_ordinal + ), ''::bytea) + from pg_catalog.unnest(p_provider_b_verification_chunk_call_counts) + with ordinality as chunks(call_count, chunk_ordinal) + ) + || p_folded_snapshot_commitment + || p_execution_trace_commitment +$function$; + +create function programmable_private.assert_reward_verification_chunk_manifest_v1( + p_verification_accounts bytea[], + p_verification_account_chunk_end_offsets integer[], + p_provider_a_verification_chunk_commitments bytea[], + p_provider_b_verification_chunk_commitments bytea[], + p_provider_a_verification_chunk_call_counts integer[], + p_provider_b_verification_chunk_call_counts integer[], + p_provider_a_call_count integer, + p_provider_b_call_count integer +) +returns void +language plpgsql +immutable +security invoker +set search_path = '' +as $function$ +declare + account_count integer; + chunk_count integer; + chunk_index integer; + chunk_call_total integer := 0; + ordered_verification_accounts bytea[]; +begin + account_count := coalesce( + pg_catalog.cardinality(p_verification_accounts), 0 + ); + chunk_count := coalesce( + pg_catalog.cardinality(p_verification_account_chunk_end_offsets), 0 + ); + if account_count not between 1 and 4096 + or p_provider_a_call_count not between 1 and 11008 + or p_provider_b_call_count is distinct from p_provider_a_call_count + or chunk_count not between 1 and 86 + or chunk_count <> ((account_count + 47) / 48) + or coalesce(pg_catalog.cardinality( + p_provider_a_verification_chunk_commitments + ), -1) <> chunk_count + or coalesce(pg_catalog.cardinality( + p_provider_b_verification_chunk_commitments + ), -1) <> chunk_count + or coalesce(pg_catalog.cardinality( + p_provider_a_verification_chunk_call_counts + ), -1) <> chunk_count + or coalesce(pg_catalog.cardinality( + p_provider_b_verification_chunk_call_counts + ), -1) <> chunk_count + or exists ( + select 1 + from pg_catalog.unnest(p_verification_accounts) as account + where account is null or pg_catalog.octet_length(account) <> 20 + ) + then + raise exception using + errcode = '22023', + message = 'invalid reward verification chunk manifest'; + end if; + + select pg_catalog.array_agg(account order by account) + into ordered_verification_accounts + from pg_catalog.unnest(p_verification_accounts) as account; + if p_verification_accounts is distinct from ordered_verification_accounts + or account_count <> ( + select pg_catalog.count(distinct account) + from pg_catalog.unnest(p_verification_accounts) as account + ) + then + raise exception using + errcode = '22023', + message = 'reward verification accounts are not canonical'; + end if; + + for chunk_index in 1..chunk_count loop + if p_verification_account_chunk_end_offsets[chunk_index] + is distinct from least(chunk_index * 48, account_count) + or pg_catalog.octet_length( + p_provider_a_verification_chunk_commitments[chunk_index] + ) <> 32 + or p_provider_a_verification_chunk_commitments[chunk_index] = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_provider_b_verification_chunk_commitments[chunk_index] + is distinct from + p_provider_a_verification_chunk_commitments[chunk_index] + or p_provider_a_verification_chunk_call_counts[chunk_index] is null + or p_provider_a_verification_chunk_call_counts[chunk_index] + not between 1 and 128 + or p_provider_b_verification_chunk_call_counts[chunk_index] + is distinct from + p_provider_a_verification_chunk_call_counts[chunk_index] + then + raise exception using + errcode = '23514', + message = 'reward verification chunk manifest changed'; + end if; + chunk_call_total := chunk_call_total + + p_provider_a_verification_chunk_call_counts[chunk_index]; + end loop; + + if p_verification_account_chunk_end_offsets[chunk_count] + is distinct from account_count + or chunk_call_total <> p_provider_a_call_count + then + raise exception using + errcode = '23514', + message = 'reward verification chunks do not exactly cover reads'; + end if; +end +$function$; + +create function programmable_private.reward_snapshot_folded_preimage_v1( + p_run_id uuid, + p_vault bytea +) +returns bytea +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + staged_vault programmable_private.reward_vault_projections%rowtype; + release_bytes bytea; + model_bytes bytea; + snapshot_kind_bytes bytea; + vault_row bytea; + allocation_rows bytea := ''::bytea; + balance_rows bytea := ''::bytea; + claim_rows bytea := ''::bytea; + payout_rows bytea := ''::bytea; + allocation_count integer; + balance_count integer; + claim_count integer; + payout_count integer; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_run_id is null or pg_catalog.octet_length(p_vault) <> 20 then + raise exception using + errcode = '22023', message = 'invalid folded reward snapshot identity'; + end if; + select staged.* into staged_vault + from programmable_private.reward_vault_projections as staged + where staged.projection_run_id = p_run_id + and staged.vault = p_vault + and staged.snapshot_kind in ('initial_seed', 'exact_current'); + if not found then + raise exception using + errcode = '23503', message = 'exact staged reward snapshot is missing'; + end if; + release_bytes := pg_catalog.convert_to(staged_vault.release_id, 'UTF8'); + model_bytes := pg_catalog.convert_to(staged_vault.model_id, 'UTF8'); + snapshot_kind_bytes := pg_catalog.convert_to( + staged_vault.snapshot_kind, 'UTF8' + ); + vault_row := pg_catalog.decode('01', 'hex') + || pg_catalog.uuid_send(staged_vault.reward_vault_projection_id) + || pg_catalog.uuid_send(staged_vault.launch_projection_id) + || pg_catalog.int8send(staged_vault.chain_id::bigint) + || pg_catalog.int4send(pg_catalog.octet_length(release_bytes)) + || release_bytes + || pg_catalog.int4send(pg_catalog.octet_length(model_bytes)) + || model_bytes + || pg_catalog.uuid_send(staged_vault.epoch_id) + || pg_catalog.int8send(staged_vault.pointer_generation) + || staged_vault.vault + || staged_vault.pool_id + || case when staged_vault.quote_asset is null + then pg_catalog.decode('00', 'hex') + else pg_catalog.decode('01', 'hex') || staged_vault.quote_asset end + || staged_vault.configuration_hash + || pg_catalog.uuid_send(staged_vault.current_allocation_fact_id) + || pg_catalog.uuid_send(staged_vault.last_source_logical_event_id) + || pg_catalog.uuid_send(staged_vault.last_source_occurrence_id) + || staged_vault.last_source_occurrence_block_hash + || pg_catalog.uuid_send(staged_vault.projection_run_id) + || pg_catalog.int8send(staged_vault.promoted_block_number::bigint) + || staged_vault.promoted_block_hash + || pg_catalog.int4send(pg_catalog.octet_length(snapshot_kind_bytes)) + || snapshot_kind_bytes + || pg_catalog.int8send(staged_vault.configuration_epoch) + || staged_vault.active_configuration_hash + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to( + staged_vault.total_creator_fees_received::text, 'UTF8' + ) + )) + || pg_catalog.convert_to( + staged_vault.total_creator_fees_received::text, 'UTF8' + ) + || case staged_vault.snapshot_kind + when 'initial_seed' then pg_catalog.decode('00', 'hex') + else pg_catalog.uuid_send( + staged_vault.baseline_reward_vault_projection_id + ) + || pg_catalog.uuid_send(staged_vault.baseline_checkpoint_id) + || pg_catalog.int8send(staged_vault.baseline_checkpoint_generation) + || pg_catalog.int8send(staged_vault.baseline_reorg_generation) + end; + + select pg_catalog.count(*)::integer, + coalesce(pg_catalog.string_agg( + pg_catalog.decode('02', 'hex') + || pg_catalog.uuid_send(allocation.reward_allocation_projection_id) + || pg_catalog.uuid_send(allocation.reward_vault_projection_id) + || pg_catalog.uuid_send(allocation.allocation_fact_id) + || pg_catalog.int8send(allocation.chain_id::bigint) + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(allocation.release_id, 'UTF8') + )) + || pg_catalog.convert_to(allocation.release_id, 'UTF8') + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(allocation.model_id, 'UTF8') + )) + || pg_catalog.convert_to(allocation.model_id, 'UTF8') + || pg_catalog.uuid_send(allocation.epoch_id) + || pg_catalog.int8send(allocation.pointer_generation) + || pg_catalog.int8send(allocation.configuration_epoch) + || pg_catalog.int4send(allocation.allocation_index) + || allocation.beneficiary + || allocation.payout_address + || pg_catalog.int4send(allocation.share_bps::integer) + || pg_catalog.int8send(allocation.effective_from_block::bigint) + || case when allocation.effective_to_block is null + then pg_catalog.decode('00', 'hex') + else pg_catalog.decode('01', 'hex') + || pg_catalog.int8send(allocation.effective_to_block) end + || pg_catalog.uuid_send(allocation.last_source_logical_event_id) + || pg_catalog.uuid_send(allocation.last_source_occurrence_id) + || allocation.last_source_occurrence_block_hash + || pg_catalog.uuid_send(allocation.projection_run_id) + || pg_catalog.int8send(allocation.promoted_block_number::bigint) + || allocation.promoted_block_hash, + ''::bytea order by allocation.configuration_epoch, + allocation.allocation_index, + allocation.reward_allocation_projection_id + ), ''::bytea) + into allocation_count, allocation_rows + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id; + + select pg_catalog.count(*)::integer, + coalesce(pg_catalog.string_agg( + pg_catalog.decode('03', 'hex') + || pg_catalog.uuid_send(balance.account_reward_balance_id) + || pg_catalog.int8send(balance.chain_id::bigint) + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(balance.release_id, 'UTF8') + )) + || pg_catalog.convert_to(balance.release_id, 'UTF8') + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(balance.model_id, 'UTF8') + )) + || pg_catalog.convert_to(balance.model_id, 'UTF8') + || pg_catalog.uuid_send(balance.epoch_id) + || pg_catalog.int8send(balance.pointer_generation) + || balance.account + || balance.vault + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(balance.claimable_accrued::text, 'UTF8') + )) + || pg_catalog.convert_to(balance.claimable_accrued::text, 'UTF8') + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(balance.claimed_total::text, 'UTF8') + )) + || pg_catalog.convert_to(balance.claimed_total::text, 'UTF8') + || pg_catalog.uuid_send(balance.last_source_logical_event_id) + || pg_catalog.uuid_send(balance.last_source_occurrence_id) + || balance.last_source_occurrence_block_hash + || pg_catalog.uuid_send(balance.projection_run_id) + || pg_catalog.int8send(balance.promoted_block_number::bigint) + || balance.promoted_block_hash + || case when balance.payout_address is null + then pg_catalog.decode('00', 'hex') + else pg_catalog.decode('01', 'hex') || balance.payout_address end, + ''::bytea order by balance.account, balance.account_reward_balance_id + ), ''::bytea) + into balance_count, balance_rows + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault; + + select pg_catalog.count(*)::integer, + coalesce(pg_catalog.string_agg( + pg_catalog.decode('04', 'hex') + || pg_catalog.uuid_send(claim.claim_projection_id) + || pg_catalog.int8send(claim.chain_id::bigint) + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(claim.release_id, 'UTF8') + )) + || pg_catalog.convert_to(claim.release_id, 'UTF8') + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(claim.model_id, 'UTF8') + )) + || pg_catalog.convert_to(claim.model_id, 'UTF8') + || pg_catalog.uuid_send(claim.epoch_id) + || pg_catalog.int8send(claim.pointer_generation) + || claim.vault + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(claim.claimant_kind, 'UTF8') + )) + || pg_catalog.convert_to(claim.claimant_kind, 'UTF8') + || claim.beneficiary + || claim.recipient + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(claim.amount::text, 'UTF8') + )) + || pg_catalog.convert_to(claim.amount::text, 'UTF8') + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(claim.beneficiary_total_claimed::text, 'UTF8') + )) + || pg_catalog.convert_to( + claim.beneficiary_total_claimed::text, 'UTF8' + ) + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(claim.vault_total_received::text, 'UTF8') + )) + || pg_catalog.convert_to(claim.vault_total_received::text, 'UTF8') + || pg_catalog.uuid_send(claim.source_occurrence_id) + || pg_catalog.uuid_send(claim.source_logical_event_id) + || claim.source_occurrence_block_hash + || pg_catalog.uuid_send(claim.projection_run_id) + || pg_catalog.int8send(claim.promoted_block_number::bigint) + || claim.promoted_block_hash, + ''::bytea order by source.block_number, + source.block_global_log_index, source.transaction_index, + source.receipt_log_ordinal, claim.source_occurrence_id + ), ''::bytea) + into claim_count, claim_rows + from programmable_private.claim_projections as claim + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = claim.source_occurrence_id + where claim.projection_run_id = p_run_id + and claim.vault = p_vault; + + select pg_catalog.count(*)::integer, + coalesce(pg_catalog.string_agg( + pg_catalog.decode('05', 'hex') + || pg_catalog.uuid_send(payout.payout_change_projection_id) + || pg_catalog.int8send(payout.chain_id::bigint) + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(payout.release_id, 'UTF8') + )) + || pg_catalog.convert_to(payout.release_id, 'UTF8') + || pg_catalog.int4send(pg_catalog.octet_length( + pg_catalog.convert_to(payout.model_id, 'UTF8') + )) + || pg_catalog.convert_to(payout.model_id, 'UTF8') + || pg_catalog.uuid_send(payout.epoch_id) + || pg_catalog.int8send(payout.pointer_generation) + || payout.vault + || payout.beneficiary + || payout.previous_payout_address + || payout.new_payout_address + || case when payout.configuration_epoch is null + then pg_catalog.decode('00', 'hex') + else pg_catalog.decode('01', 'hex') + || pg_catalog.int8send(payout.configuration_epoch) end + || pg_catalog.uuid_send(payout.source_occurrence_id) + || pg_catalog.uuid_send(payout.source_logical_event_id) + || payout.source_occurrence_block_hash + || pg_catalog.uuid_send(payout.projection_run_id) + || pg_catalog.int8send(payout.promoted_block_number::bigint) + || payout.promoted_block_hash, + ''::bytea order by source.block_number, + source.block_global_log_index, source.transaction_index, + source.receipt_log_ordinal, payout.source_occurrence_id + ), ''::bytea) + into payout_count, payout_rows + from programmable_private.payout_change_projections as payout + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = payout.source_occurrence_id + where payout.projection_run_id = p_run_id + and payout.vault = p_vault; + + return pg_catalog.decode( + '70726f6772616d6d61626c653a7265776172642d736e617073686f742d666f6c643a763100', + 'hex' + ) + || pg_catalog.uuid_send(p_run_id) + || p_vault + || vault_row + || pg_catalog.int4send(allocation_count) || allocation_rows + || pg_catalog.int4send(balance_count) || balance_rows + || pg_catalog.int4send(claim_count) || claim_rows + || pg_catalog.int4send(payout_count) || payout_rows; +end +$function$; + +create function programmable_private.reward_snapshot_folded_commitment_v1( + p_run_id uuid, + p_vault bytea +) +returns bytea +language sql +stable +security definer +set search_path = '' +as $function$ + select pg_catalog.sha256( + programmable_private.reward_snapshot_folded_preimage_v1( + p_run_id, p_vault + ) + ) +$function$; + +create function programmable_private.get_staged_reward_folded_commitment_v1( + p_run_id uuid, + p_vault bytea +) +returns bytea +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection'; + if header.run_id is null + or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'projection run is not open'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation + ); + return programmable_private.reward_snapshot_folded_commitment_v1( + p_run_id, p_vault + ); +end +$function$; + +comment on function + programmable_private.get_staged_reward_folded_commitment_v1(uuid, bytea) +is + 'Returns the structural commitment of one exact staged reward snapshot for the open current projection run. It does not expose private rows.'; + +create function programmable_private.validate_projection_execution_trace_v1( + p_execution_trace jsonb, + p_provider_a_id uuid, + p_provider_b_id uuid +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + expected_top_keys text[] := array[ + 'calls', 'candidateBatchSize', 'completedAtMs', 'elapsedMs', + 'hardDeadlineMs', 'maxCallsPerProvider', 'providerCallCounts', + 'startedAtMs' + ]; + expected_call_keys text[] := array[ + 'attempt', 'durationMs', 'operation', 'outcome', + 'providerEndpointCommitment', 'providerIdentity', + 'providerOriginCommitment', 'providerVendorGroup', + 'startedOffsetMs' + ]; + actual_keys text[]; + actual_call_keys text[]; + provider_ids uuid[] := array[p_provider_a_id, p_provider_b_id]; + provider_id uuid; + deployment programmable_private.provider_deployments%rowtype; + metadata programmable_private.rpc_provider_deployment_metadata%rowtype; + call_item jsonb; + call_ordinal bigint; + provider_index integer; + expected_identity text; + expected_endpoint text; + expected_origin text; + started_at numeric; + completed_at numeric; + elapsed numeric; + hard_deadline numeric; + maximum_calls numeric; + candidate_batch_size numeric; + call_count_a numeric; + call_count_b numeric; + call_attempt numeric; + call_started numeric; + call_duration numeric; + counted_calls bigint; + successful_calls bigint; +begin + if p_execution_trace is null + or pg_catalog.jsonb_typeof(p_execution_trace) <> 'object' + or pg_catalog.octet_length(p_execution_trace::text) > 65536 + then + raise exception using + errcode = '22023', message = 'invalid projection execution trace'; + end if; + select pg_catalog.array_agg(key order by key) into actual_keys + from pg_catalog.jsonb_object_keys(p_execution_trace) as key; + if actual_keys is distinct from expected_top_keys + or p_execution_trace ->> 'startedAtMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'completedAtMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'candidateBatchSize' !~ '^[0-9]+$' + or p_execution_trace ->> 'hardDeadlineMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'maxCallsPerProvider' !~ '^[0-9]+$' + or p_execution_trace ->> 'elapsedMs' !~ '^[0-9]+$' + or pg_catalog.jsonb_typeof( + p_execution_trace -> 'providerCallCounts' + ) <> 'array' + or pg_catalog.jsonb_array_length( + p_execution_trace -> 'providerCallCounts' + ) <> 2 + or pg_catalog.jsonb_typeof(p_execution_trace -> 'calls') <> 'array' + then + raise exception using + errcode = '22023', message = 'projection execution trace shape changed'; + end if; + + started_at := (p_execution_trace ->> 'startedAtMs')::numeric; + completed_at := (p_execution_trace ->> 'completedAtMs')::numeric; + candidate_batch_size := + (p_execution_trace ->> 'candidateBatchSize')::numeric; + hard_deadline := (p_execution_trace ->> 'hardDeadlineMs')::numeric; + maximum_calls := (p_execution_trace ->> 'maxCallsPerProvider')::numeric; + elapsed := (p_execution_trace ->> 'elapsedMs')::numeric; + if p_execution_trace #>> '{providerCallCounts,0}' !~ '^[0-9]+$' + or p_execution_trace #>> '{providerCallCounts,1}' !~ '^[0-9]+$' + then + raise exception using + errcode = '22023', message = 'invalid provider call counts'; + end if; + call_count_a := + (p_execution_trace #>> '{providerCallCounts,0}')::numeric; + call_count_b := + (p_execution_trace #>> '{providerCallCounts,1}')::numeric; + if started_at < 1 + or completed_at < started_at + or completed_at - started_at <> elapsed + or elapsed > hard_deadline + or candidate_batch_size < 0 + or candidate_batch_size > 4096 + or hard_deadline < 10 + or hard_deadline > 75000 + or maximum_calls < 1 + or maximum_calls > 128 + or maximum_calls <> pg_catalog.trunc(maximum_calls) + or call_count_a < 1 + or call_count_b < 1 + or call_count_a > maximum_calls + or call_count_b > maximum_calls + or call_count_a <> pg_catalog.trunc(call_count_a) + or call_count_b <> pg_catalog.trunc(call_count_b) + or pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') < 2 + or pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') > 256 + or pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') <> + call_count_a + call_count_b + then + raise exception using + errcode = '22023', message = 'projection execution trace is out of bounds'; + end if; + + for call_item, call_ordinal in + select value, ordinality + from pg_catalog.jsonb_array_elements(p_execution_trace -> 'calls') + with ordinality as calls(value, ordinality) + loop + if pg_catalog.jsonb_typeof(call_item) <> 'object' then + raise exception using + errcode = '22023', message = 'projection call trace is not an object'; + end if; + select pg_catalog.array_agg(key order by key) into actual_call_keys + from pg_catalog.jsonb_object_keys(call_item) as key; + if actual_call_keys is distinct from expected_call_keys + or call_item ->> 'attempt' !~ '^[0-9]+$' + or call_item ->> 'startedOffsetMs' !~ '^[0-9]+$' + or call_item ->> 'durationMs' !~ '^[0-9]+$' + or call_item ->> 'operation' not in ( + 'getChainId', 'getBlockNumber', 'getBlock', + 'getTransactionReceipt', 'getBytecode', 'readRewardSnapshot' + ) + or call_item ->> 'outcome' not in ('success', 'error') + then + raise exception using + errcode = '22023', message = 'projection call trace shape changed'; + end if; + call_attempt := (call_item ->> 'attempt')::numeric; + call_started := (call_item ->> 'startedOffsetMs')::numeric; + call_duration := (call_item ->> 'durationMs')::numeric; + if call_attempt not between 1 and 3 + or call_attempt <> pg_catalog.trunc(call_attempt) + or call_started > elapsed + or call_duration > elapsed + or call_started + call_duration > elapsed + then + raise exception using + errcode = '22023', message = 'projection call trace is out of bounds'; + end if; + + provider_index := case + when call_ordinal <= call_count_a then 1 else 2 + end; + provider_id := provider_ids[provider_index]; + select * into deployment + from programmable_private.provider_deployments + where provider_deployment_id = provider_id + and provider_type = 'rpc_provider'; + select * into metadata + from programmable_private.rpc_provider_deployment_metadata + where provider_deployment_id = provider_id + and chain_id = 1 + and vendor_order = provider_index; + if deployment.provider_deployment_id is null + or metadata.provider_deployment_id is null + then + raise exception using + errcode = '23503', message = 'projection trace provider is not registered'; + end if; + expected_identity := metadata.vendor || '-mainnet-' || + pg_catalog.substring( + pg_catalog.encode(deployment.deployment_commitment, 'hex'), 1, 32 + ); + expected_endpoint := '0x' || pg_catalog.encode( + metadata.endpoint_url_commitment, 'hex' + ); + expected_origin := '0x' || pg_catalog.encode( + metadata.endpoint_origin_commitment, 'hex' + ); + if call_item ->> 'providerIdentity' <> expected_identity + or call_item ->> 'providerVendorGroup' <> metadata.vendor + or call_item ->> 'providerEndpointCommitment' <> expected_endpoint + or call_item ->> 'providerOriginCommitment' <> expected_origin + then + raise exception using + errcode = '23514', message = 'projection trace provider was substituted'; + end if; + end loop; + + for provider_index in 1..2 loop + select pg_catalog.count(*), + pg_catalog.count(*) filter (where value ->> 'outcome' = 'success') + into counted_calls, successful_calls + from pg_catalog.jsonb_array_elements(p_execution_trace -> 'calls') + with ordinality as calls(value, ordinality) + where ( + provider_index = 1 and ordinality <= call_count_a + ) or ( + provider_index = 2 and ordinality > call_count_a + ); + if ( + provider_index = 1 and counted_calls <> call_count_a + ) + or ( + provider_index = 2 and counted_calls <> call_count_b + ) + or successful_calls < 1 + then + raise exception using + errcode = '23514', + message = 'projection trace lacks successful provider evidence'; + end if; + end loop; +end +$function$; + +create function programmable_private.validate_reward_snapshot_execution_trace_v1( + p_execution_trace jsonb, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_provider_a_call_count integer, + p_provider_b_call_count integer +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + expected_top_keys text[] := array[ + 'calls', 'candidateBatchSize', 'completedAtMs', 'elapsedMs', + 'hardDeadlineMs', 'maxCallsPerProvider', 'providerCallCounts', + 'startedAtMs' + ]; + expected_call_keys text[] := array[ + 'attempt', 'durationMs', 'operation', 'outcome', + 'providerEndpointCommitment', 'providerIdentity', + 'providerOriginCommitment', 'providerVendorGroup', + 'startedOffsetMs' + ]; + actual_keys text[]; + actual_call_keys text[]; + provider_ids uuid[] := array[p_provider_a_id, p_provider_b_id]; + provider_id uuid; + deployment programmable_private.provider_deployments%rowtype; + metadata programmable_private.rpc_provider_deployment_metadata%rowtype; + call_item jsonb; + call_ordinal bigint; + chunk_count integer; + provider_index integer; + expected_identity text; + expected_endpoint text; + expected_origin text; + started_at numeric; + completed_at numeric; + elapsed numeric; + hard_deadline numeric; + maximum_calls numeric; + call_started numeric; + call_duration numeric; +begin + if p_execution_trace is null + or pg_catalog.jsonb_typeof(p_execution_trace) <> 'object' + or pg_catalog.octet_length(p_execution_trace::text) > 262144 + then + raise exception using + errcode = '22023', message = 'invalid reward execution trace'; + end if; + select pg_catalog.array_agg(key order by key) into actual_keys + from pg_catalog.jsonb_object_keys(p_execution_trace) as key; + if actual_keys is distinct from expected_top_keys + or p_execution_trace ->> 'startedAtMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'completedAtMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'candidateBatchSize' !~ '^[0-9]+$' + or p_execution_trace ->> 'hardDeadlineMs' !~ '^[0-9]+$' + or p_execution_trace ->> 'maxCallsPerProvider' !~ '^[0-9]+$' + or p_execution_trace ->> 'elapsedMs' !~ '^[0-9]+$' + or pg_catalog.jsonb_typeof( + p_execution_trace -> 'providerCallCounts' + ) <> 'array' + or pg_catalog.jsonb_array_length( + p_execution_trace -> 'providerCallCounts' + ) <> 2 + or pg_catalog.jsonb_typeof(p_execution_trace -> 'calls') <> 'array' + or pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') < 2 + or pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') > 172 + or pg_catalog.mod( + pg_catalog.jsonb_array_length(p_execution_trace -> 'calls'), 2 + ) <> 0 + or p_execution_trace #>> '{providerCallCounts,0}' !~ '^[0-9]+$' + or p_execution_trace #>> '{providerCallCounts,1}' !~ '^[0-9]+$' + then + raise exception using + errcode = '22023', message = 'reward execution trace shape changed'; + end if; + + started_at := (p_execution_trace ->> 'startedAtMs')::numeric; + completed_at := (p_execution_trace ->> 'completedAtMs')::numeric; + elapsed := (p_execution_trace ->> 'elapsedMs')::numeric; + hard_deadline := (p_execution_trace ->> 'hardDeadlineMs')::numeric; + maximum_calls := + (p_execution_trace ->> 'maxCallsPerProvider')::numeric; + chunk_count := pg_catalog.jsonb_array_length( + p_execution_trace -> 'calls' + ) / 2; + if started_at < 1 + or completed_at < started_at + or completed_at - started_at <> elapsed + or elapsed > hard_deadline + or (p_execution_trace ->> 'candidateBatchSize')::numeric <> 0 + or hard_deadline < 10 + or hard_deadline > 75000 + or maximum_calls < 1 + or maximum_calls > 128 + or maximum_calls <> pg_catalog.trunc(maximum_calls) + or p_provider_a_call_count not between 1 and 11008 + or p_provider_b_call_count not between 1 and 11008 + or p_provider_b_call_count <> p_provider_a_call_count + or (p_execution_trace #>> '{providerCallCounts,0}')::numeric <> + p_provider_a_call_count + or (p_execution_trace #>> '{providerCallCounts,1}')::numeric <> + p_provider_b_call_count + then + raise exception using + errcode = '22023', message = 'reward execution trace is out of bounds'; + end if; + + for call_item, call_ordinal in + select value, ordinality + from pg_catalog.jsonb_array_elements(p_execution_trace -> 'calls') + with ordinality as calls(value, ordinality) + loop + if pg_catalog.jsonb_typeof(call_item) <> 'object' then + raise exception using + errcode = '22023', message = 'reward trace call is not an object'; + end if; + select pg_catalog.array_agg(key order by key) into actual_call_keys + from pg_catalog.jsonb_object_keys(call_item) as key; + if actual_call_keys is distinct from expected_call_keys + or call_item ->> 'attempt' <> '1' + or call_item ->> 'startedOffsetMs' !~ '^[0-9]+$' + or call_item ->> 'durationMs' !~ '^[0-9]+$' + or call_item ->> 'operation' <> 'readRewardSnapshot' + or call_item ->> 'outcome' <> 'success' + then + raise exception using + errcode = '22023', message = 'reward trace call shape changed'; + end if; + call_started := (call_item ->> 'startedOffsetMs')::numeric; + call_duration := (call_item ->> 'durationMs')::numeric; + if call_started > elapsed + or call_duration > elapsed + or call_started + call_duration > elapsed + then + raise exception using + errcode = '22023', message = 'reward trace call is out of bounds'; + end if; + + provider_index := case + when call_ordinal <= chunk_count then 1 + else 2 + end; + provider_id := provider_ids[provider_index]; + select * into deployment + from programmable_private.provider_deployments + where provider_deployment_id = provider_id + and provider_type = 'rpc_provider'; + select * into metadata + from programmable_private.rpc_provider_deployment_metadata + where provider_deployment_id = provider_id + and chain_id = 1 + and vendor_order = provider_index; + if deployment.provider_deployment_id is null + or metadata.provider_deployment_id is null + then + raise exception using + errcode = '23503', message = 'reward trace provider is not registered'; + end if; + expected_identity := metadata.vendor || '-mainnet-' || + pg_catalog.substring( + pg_catalog.encode(deployment.deployment_commitment, 'hex'), 1, 32 + ); + expected_endpoint := '0x' || pg_catalog.encode( + metadata.endpoint_url_commitment, 'hex' + ); + expected_origin := '0x' || pg_catalog.encode( + metadata.endpoint_origin_commitment, 'hex' + ); + if call_item ->> 'providerIdentity' <> expected_identity + or call_item ->> 'providerVendorGroup' <> metadata.vendor + or call_item ->> 'providerEndpointCommitment' <> expected_endpoint + or call_item ->> 'providerOriginCommitment' <> expected_origin + then + raise exception using + errcode = '23514', message = 'reward trace provider was substituted'; + end if; + end loop; +end +$function$; + +create function programmable_private.assert_projection_provider_evidence_v1( + p_promotion_mode text, + p_run_id uuid, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_target_block_number bigint, + p_target_block_hash bytea, + p_execution_evidence_id uuid, + p_reward_snapshot_evidence_ids uuid[] +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + execution + programmable_private.projection_provider_execution_evidence%rowtype; + expected_reward_ids uuid[]; +begin + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection'; + if not found then + raise exception using + errcode = '23503', message = 'projection run is missing'; + end if; + select * into execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + and run_id = p_run_id + and safe_head_observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and pointer_generation = header.captured_pointer_generation; + if not found then + raise exception using + errcode = '23514', + message = 'promotion provider execution evidence was substituted'; + end if; + if execution.execution_trace_preimage <> + programmable_private.projection_execution_trace_preimage_v1( + execution.execution_trace + ) + or execution.execution_trace_commitment <> + pg_catalog.sha256(execution.execution_trace_preimage) + then + raise exception using + errcode = '23514', + message = 'promotion execution trace evidence changed'; + end if; + if not exists ( + select 1 + from programmable_private.dual_rpc_block_evidence as evidence + where evidence.block_evidence_id = p_target_block_evidence_id + and evidence.observation_id = p_safe_head_observation_id + and evidence.epoch_id = header.epoch_id + and evidence.chain_id = header.chain_id + and evidence.pointer_generation = + header.captured_pointer_generation + and evidence.block_number = p_target_block_number + and evidence.agreed_block_hash = p_target_block_hash + ) then + raise exception using + errcode = '23514', message = 'promotion target evidence was substituted'; + end if; + + if p_promotion_mode <> 'exact_incremental' then + raise exception using + errcode = '22023', message = 'unknown projection promotion mode'; + end if; + + select pg_catalog.array_agg( + evidence.reward_snapshot_evidence_id order by evidence.vault + ) into expected_reward_ids + from programmable_private.reward_snapshot_provider_evidence as evidence + join programmable_private.reward_vault_projections as vault + on vault.projection_run_id = evidence.run_id + and vault.vault = evidence.vault + and vault.model_id = evidence.model_id + left join programmable_private.launch_projections as launch + on launch.launch_projection_id = vault.launch_projection_id + and ( + vault.snapshot_kind = 'exact_current' + or ( + vault.snapshot_kind = 'initial_seed' + and launch.projection_run_id = vault.projection_run_id + and ( + exists ( + select 1 + from programmable_private.creator_fee_checkpoint_facts as fact + where fact.verification_run_id = evidence.run_id + and fact.vault = evidence.vault + ) + or exists ( + select 1 + from programmable_private.reward_configuration_activation_facts + as fact + where fact.verification_run_id = evidence.run_id + and fact.vault = evidence.vault + ) + or exists ( + select 1 + from programmable_private.claim_projections as claim + where claim.projection_run_id = evidence.run_id + and claim.vault = evidence.vault + ) + or exists ( + select 1 + from programmable_private.payout_change_projections as payout + where payout.projection_run_id = evidence.run_id + and payout.vault = evidence.vault + ) + ) + ) + ) + and vault.promoted_block_number = evidence.target_block_number + and vault.promoted_block_hash = evidence.target_block_hash + where evidence.run_id = p_run_id + and evidence.execution_evidence_id = p_execution_evidence_id + and evidence.safe_head_observation_id = p_safe_head_observation_id + and evidence.target_block_evidence_id = p_target_block_evidence_id + and evidence.target_block_number = p_target_block_number + and evidence.target_block_hash = p_target_block_hash + and evidence.execution_trace_preimage = + programmable_private.projection_execution_trace_preimage_v1( + evidence.execution_trace + ) + and evidence.execution_trace_commitment = + pg_catalog.sha256(evidence.execution_trace_preimage) + and evidence.folded_snapshot_preimage = + programmable_private.reward_snapshot_folded_preimage_v1( + evidence.run_id, evidence.vault + ) + and evidence.folded_snapshot_commitment = + pg_catalog.sha256(evidence.folded_snapshot_preimage); + if p_reward_snapshot_evidence_ids is distinct from + coalesce(expected_reward_ids, array[]::uuid[]) + or coalesce(pg_catalog.cardinality(expected_reward_ids), 0) <> + ( + select pg_catalog.count(*) + from programmable_private.reward_vault_projections as staged + where staged.projection_run_id = p_run_id + and ( + staged.snapshot_kind = 'exact_current' + or ( + staged.snapshot_kind = 'initial_seed' + and ( + exists ( + select 1 + from programmable_private.creator_fee_checkpoint_facts as fact + where fact.verification_run_id = p_run_id + and fact.vault = staged.vault + ) + or exists ( + select 1 + from programmable_private.reward_configuration_activation_facts + as fact + where fact.verification_run_id = p_run_id + and fact.vault = staged.vault + ) + or exists ( + select 1 + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + and claim.vault = staged.vault + ) + or exists ( + select 1 + from programmable_private.payout_change_projections as payout + where payout.projection_run_id = p_run_id + and payout.vault = staged.vault + ) + ) + ) + ) + ) + or exists ( + select 1 + from pg_catalog.unnest(p_reward_snapshot_evidence_ids) as item + where item is null + ) + or pg_catalog.cardinality(p_reward_snapshot_evidence_ids) <> + ( + select pg_catalog.count(distinct item) + from pg_catalog.unnest(p_reward_snapshot_evidence_ids) as item + ) + then + raise exception using + errcode = '23514', + message = 'reward provider evidence does not exactly cover staged vaults'; + end if; +end +$function$; + +create function programmable_private.projection_provider_binding_preimage_v1( + p_publication_id uuid, + p_run_id uuid, + p_promotion_mode text, + p_execution_evidence_id uuid, + p_reward_snapshot_evidence_ids uuid[], + p_bound_at timestamptz +) +returns bytea +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + mode_bytes bytea; + execution_fingerprint bytea; + reward_pairs bytea; + reward_count integer; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_publication_id is null + or p_run_id is null + or p_promotion_mode <> 'exact_incremental' + or p_execution_evidence_id is null + or p_reward_snapshot_evidence_ids is null + or p_bound_at is null + or pg_catalog.date_part('epoch', p_bound_at) < 0 + then + raise exception using + errcode = '22023', message = 'invalid provider binding preimage'; + end if; + select content_fingerprint into execution_fingerprint + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + and run_id = p_run_id; + select pg_catalog.count(*)::integer, + coalesce(pg_catalog.string_agg( + pg_catalog.uuid_send(evidence.reward_snapshot_evidence_id) + || evidence.content_fingerprint, + ''::bytea order by requested.evidence_ordinal + ), ''::bytea) + into reward_count, reward_pairs + from pg_catalog.unnest(p_reward_snapshot_evidence_ids) + with ordinality as requested(evidence_id, evidence_ordinal) + join programmable_private.reward_snapshot_provider_evidence as evidence + on evidence.reward_snapshot_evidence_id = requested.evidence_id + and evidence.run_id = p_run_id + and evidence.execution_evidence_id = p_execution_evidence_id; + if execution_fingerprint is null + or reward_count <> pg_catalog.cardinality( + p_reward_snapshot_evidence_ids + ) + then + raise exception using + errcode = '23503', message = 'provider binding evidence is incomplete'; + end if; + mode_bytes := pg_catalog.convert_to(p_promotion_mode, 'UTF8'); + return pg_catalog.decode( + '70726f6772616d6d61626c653a70726f6a656374696f6e2d70726f76696465722d62696e64696e673a763100', + 'hex' + ) + || pg_catalog.uuid_send(p_publication_id) + || pg_catalog.uuid_send(p_run_id) + || pg_catalog.int4send(pg_catalog.octet_length(mode_bytes)) + || mode_bytes + || pg_catalog.uuid_send(p_execution_evidence_id) + || execution_fingerprint + || pg_catalog.int4send(reward_count) + || reward_pairs + || pg_catalog.int8send( + pg_catalog.floor( + pg_catalog.date_part('epoch', p_bound_at) * 1000 + )::bigint + ); +end +$function$; + +create function programmable_private.projection_provider_binding_commitment_v1( + p_publication_id uuid, + p_run_id uuid, + p_promotion_mode text, + p_execution_evidence_id uuid, + p_reward_snapshot_evidence_ids uuid[], + p_bound_at timestamptz +) +returns bytea +language sql +stable +security definer +set search_path = '' +as $function$ + select pg_catalog.sha256( + programmable_private.projection_provider_binding_preimage_v1( + p_publication_id, p_run_id, p_promotion_mode, + p_execution_evidence_id, p_reward_snapshot_evidence_ids, + p_bound_at + ) + ) +$function$; + +create function programmable_private.bind_projection_publication_provider_evidence_v1( + p_provider_binding_id uuid, + p_publication_id uuid, + p_run_id uuid, + p_promotion_mode text, + p_execution_evidence_id uuid, + p_reward_snapshot_evidence_ids uuid[], + p_provider_binding_commitment bytea, + p_bound_at timestamptz +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + publication programmable_private.projection_publications%rowtype; + execution + programmable_private.projection_provider_execution_evidence%rowtype; + existing + programmable_private.projection_publication_provider_bindings%rowtype; + evidence_record record; + ordinal integer := 0; + expected_binding_commitment bytea; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_provider_binding_id is null + or p_publication_id is null + or p_run_id is null + or p_execution_evidence_id is null + or p_promotion_mode <> 'exact_incremental' + or p_reward_snapshot_evidence_ids is null + or pg_catalog.octet_length(p_provider_binding_commitment) <> 32 + or p_provider_binding_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + then + raise exception using + errcode = '22023', message = 'invalid publication provider binding'; + end if; + select * into publication + from programmable_private.projection_publications + where publication_id = p_publication_id + and run_id = p_run_id + and published_at = p_bound_at; + select * into execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + and run_id = p_run_id; + if publication.publication_id is null + or execution.execution_evidence_id is null + then + raise exception using + errcode = '23503', message = 'publication provider evidence is missing'; + end if; + expected_binding_commitment := + programmable_private.projection_provider_binding_commitment_v1( + p_publication_id, p_run_id, p_promotion_mode, + p_execution_evidence_id, p_reward_snapshot_evidence_ids, + p_bound_at + ); + if p_provider_binding_commitment <> expected_binding_commitment then + raise exception using + errcode = '23514', + message = 'publication provider binding commitment changed'; + end if; + perform programmable_private.assert_projection_provider_evidence_v1( + p_promotion_mode, p_run_id, + execution.safe_head_observation_id, + ( + select checkpoint.target_block_evidence_id + from programmable_private.projector_checkpoints as checkpoint + where checkpoint.checkpoint_id = publication.checkpoint_id + ), + publication.target_block_number, + publication.target_block_hash, + p_execution_evidence_id, p_reward_snapshot_evidence_ids + ); + + select * into existing + from programmable_private.projection_publication_provider_bindings + where provider_binding_id = p_provider_binding_id + or publication_id = p_publication_id + or run_id = p_run_id; + if found then + if existing.provider_binding_id <> p_provider_binding_id + or existing.publication_id <> p_publication_id + or existing.run_id <> p_run_id + or existing.promotion_mode <> p_promotion_mode + or existing.execution_evidence_id <> p_execution_evidence_id + or existing.reward_snapshot_evidence_ids <> + p_reward_snapshot_evidence_ids + or existing.provider_binding_commitment <> + p_provider_binding_commitment + then + raise exception using + errcode = '23505', + message = 'publication provider binding replay changed content'; + end if; + return existing.provider_binding_id; + end if; + + insert into programmable_private.projection_publication_provider_bindings ( + provider_binding_id, publication_id, run_id, promotion_mode, + execution_evidence_id, reward_snapshot_evidence_ids, + provider_binding_commitment, bound_at + ) values ( + p_provider_binding_id, p_publication_id, p_run_id, + p_promotion_mode::programmable_private.source_identifier, + p_execution_evidence_id, p_reward_snapshot_evidence_ids, + p_provider_binding_commitment::programmable_private.bytes32_value, + p_bound_at + ); + for evidence_record in + select evidence.* + from pg_catalog.unnest(p_reward_snapshot_evidence_ids) + with ordinality as requested(evidence_id, evidence_ordinal) + join programmable_private.reward_snapshot_provider_evidence as evidence + on evidence.reward_snapshot_evidence_id = requested.evidence_id + and evidence.run_id = p_run_id + and evidence.execution_evidence_id = p_execution_evidence_id + order by requested.evidence_ordinal + loop + ordinal := ordinal + 1; + insert into programmable_private.projection_publication_reward_evidence ( + provider_binding_id, evidence_ordinal, + reward_snapshot_evidence_id, run_id, + execution_evidence_id, vault + ) values ( + p_provider_binding_id, ordinal, + evidence_record.reward_snapshot_evidence_id, + p_run_id, p_execution_evidence_id, evidence_record.vault + ); + end loop; + if ordinal <> pg_catalog.cardinality(p_reward_snapshot_evidence_ids) then + raise exception using + errcode = '23514', + message = 'publication reward evidence binding is incomplete'; + end if; + perform programmable_private.append_mutation_audit( + 'projection.provider_evidence.bind', + p_provider_binding_commitment, p_run_id, p_bound_at + ); + return p_provider_binding_id; +end +$function$; + +create function programmable_private.append_projection_provider_execution_evidence_v1( + p_execution_evidence_id uuid, + p_run_id uuid, + p_safe_head_observation_id uuid, + p_configured_provider_deployment_ids uuid[], + p_execution_trace jsonb, + p_execution_trace_commitment bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + observation programmable_private.safe_head_observations%rowtype; + existing + programmable_private.projection_provider_execution_evidence%rowtype; + envio_provider programmable_private.provider_deployments%rowtype; + provider_a programmable_private.rpc_provider_deployment_metadata%rowtype; + provider_b programmable_private.rpc_provider_deployment_metadata%rowtype; + expected_preimage bytea; + execution_trace_preimage bytea; + provider_a_identity text; + provider_b_identity text; + provider_a_call_count integer; + provider_b_call_count integer; + candidate_batch_size integer; + hard_deadline_ms integer; + maximum_calls_per_provider integer; + elapsed_ms integer; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'projection_execution', p_encoding_version, + p_canonical_preimage, p_content_fingerprint + ); + if p_execution_evidence_id is null + or p_run_id is null + or p_safe_head_observation_id is null + or coalesce( + pg_catalog.cardinality(p_configured_provider_deployment_ids), 0 + ) <> 3 + or exists ( + select 1 + from pg_catalog.unnest(p_configured_provider_deployment_ids) as item + where item is null + ) + or pg_catalog.cardinality(p_configured_provider_deployment_ids) <> + ( + select pg_catalog.count(distinct item) + from pg_catalog.unnest( + p_configured_provider_deployment_ids + ) as item + ) + or pg_catalog.octet_length(p_execution_trace_commitment) <> 32 + or pg_catalog.octet_length(p_content_fingerprint) <> 32 + or p_execution_trace_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_content_fingerprint = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + then + raise exception using + errcode = '22023', + message = 'invalid projection provider execution evidence'; + end if; + + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection' + for share; + if not found + or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'projection run is not open'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation + ); + + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and pointer_generation = header.captured_pointer_generation; + if not found + or p_configured_provider_deployment_ids[2] <> + observation.provider_a_id + or p_configured_provider_deployment_ids[3] <> + observation.provider_b_id + then + raise exception using + errcode = '23514', + message = 'execution evidence does not match its safe-head providers'; + end if; + + select * into envio_provider + from programmable_private.provider_deployments + where provider_deployment_id = + p_configured_provider_deployment_ids[1] + and provider_type = 'envio_deployment'; + select * into provider_a + from programmable_private.rpc_provider_deployment_metadata + where provider_deployment_id = observation.provider_a_id + and chain_id = header.chain_id + and vendor = 'alchemy' + and vendor_order = 1; + select * into provider_b + from programmable_private.rpc_provider_deployment_metadata + where provider_deployment_id = observation.provider_b_id + and chain_id = header.chain_id + and vendor = 'quicknode' + and vendor_order = 2; + if envio_provider.provider_deployment_id is null + or provider_a.provider_deployment_id is null + or provider_b.provider_deployment_id is null + then + raise exception using + errcode = '23503', + message = 'configured projection provider set is not registered'; + end if; + + perform programmable_private.validate_projection_execution_trace_v1( + p_execution_trace, observation.provider_a_id, observation.provider_b_id + ); + execution_trace_preimage := + programmable_private.projection_execution_trace_preimage_v1( + p_execution_trace + ); + if p_execution_trace_commitment <> + pg_catalog.sha256(execution_trace_preimage) + then + raise exception using + errcode = '23514', + message = 'projection execution trace commitment changed'; + end if; + provider_a_identity := provider_a.vendor || '-mainnet-' || + pg_catalog.substring( + pg_catalog.encode( + ( + select deployment_commitment + from programmable_private.provider_deployments + where provider_deployment_id = observation.provider_a_id + ), + 'hex' + ), + 1, 32 + ); + provider_b_identity := provider_b.vendor || '-mainnet-' || + pg_catalog.substring( + pg_catalog.encode( + ( + select deployment_commitment + from programmable_private.provider_deployments + where provider_deployment_id = observation.provider_b_id + ), + 'hex' + ), + 1, 32 + ); + provider_a_call_count := + (p_execution_trace #>> '{providerCallCounts,0}')::integer; + provider_b_call_count := + (p_execution_trace #>> '{providerCallCounts,1}')::integer; + candidate_batch_size := + (p_execution_trace ->> 'candidateBatchSize')::integer; + hard_deadline_ms := + (p_execution_trace ->> 'hardDeadlineMs')::integer; + maximum_calls_per_provider := + (p_execution_trace ->> 'maxCallsPerProvider')::integer; + elapsed_ms := (p_execution_trace ->> 'elapsedMs')::integer; + expected_preimage := + programmable_private.projection_execution_evidence_preimage_v1( + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, p_run_id, + observation.provider_a_id, observation.provider_b_id, + provider_a_identity, provider_b_identity, + provider_a.vendor, provider_b.vendor, + provider_a.endpoint_url_commitment, + provider_b.endpoint_url_commitment, + provider_a.endpoint_origin_commitment, + provider_b.endpoint_origin_commitment, + provider_a_call_count, provider_b_call_count, + candidate_batch_size, hard_deadline_ms, + maximum_calls_per_provider, elapsed_ms, + p_execution_trace_commitment + ); + if p_canonical_preimage <> expected_preimage then + raise exception using + errcode = '23514', + message = 'projection execution evidence codec mismatch'; + end if; + + select * into existing + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + or run_id = p_run_id; + if found then + if existing.execution_evidence_id <> p_execution_evidence_id + or existing.run_id <> p_run_id + or existing.safe_head_observation_id <> + p_safe_head_observation_id + or existing.configured_provider_deployment_ids <> + p_configured_provider_deployment_ids + or existing.execution_trace <> p_execution_trace + or existing.execution_trace_preimage <> execution_trace_preimage + or existing.execution_trace_commitment <> + p_execution_trace_commitment + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + then + raise exception using + errcode = '23505', + message = 'projection execution evidence replay changed content'; + end if; + return existing.execution_evidence_id; + end if; + + insert into programmable_private.projection_provider_execution_evidence ( + execution_evidence_id, run_id, safe_head_observation_id, + epoch_id, chain_id, pointer_generation, + configured_provider_deployment_ids, + envio_provider_deployment_id, provider_a_id, provider_b_id, + provider_a_vendor, provider_b_vendor, + provider_a_identity, provider_b_identity, + provider_a_endpoint_url_commitment, + provider_b_endpoint_url_commitment, + provider_a_endpoint_origin_commitment, + provider_b_endpoint_origin_commitment, + provider_a_call_count, provider_b_call_count, + candidate_batch_size, hard_deadline_ms, + maximum_calls_per_provider, elapsed_ms, + execution_trace, execution_trace_preimage, + execution_trace_commitment, encoding_version, + canonical_preimage, content_fingerprint, verified_at + ) values ( + p_execution_evidence_id, p_run_id, p_safe_head_observation_id, + header.epoch_id, header.chain_id, + header.captured_pointer_generation, + p_configured_provider_deployment_ids, + p_configured_provider_deployment_ids[1], + observation.provider_a_id, observation.provider_b_id, + provider_a.vendor, provider_b.vendor, + provider_a_identity, provider_b_identity, + provider_a.endpoint_url_commitment, + provider_b.endpoint_url_commitment, + provider_a.endpoint_origin_commitment, + provider_b.endpoint_origin_commitment, + provider_a_call_count, provider_b_call_count, + candidate_batch_size, hard_deadline_ms, + maximum_calls_per_provider, elapsed_ms, + p_execution_trace, execution_trace_preimage, + p_execution_trace_commitment, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint, p_verified_at + ); + perform programmable_private.append_mutation_audit( + 'projection.provider_execution.append', + p_content_fingerprint, p_run_id, p_verified_at + ); + return p_execution_evidence_id; +end +$function$; + +create function programmable_private.append_reward_snapshot_provider_evidence_v1( + p_reward_snapshot_evidence_id uuid, + p_run_id uuid, + p_execution_evidence_id uuid, + p_target_block_evidence_id uuid, + p_vault bytea, + p_model_id text, + p_reward_model text, + p_target_block_number numeric, + p_target_block_hash bytea, + p_provider_a_snapshot_commitment bytea, + p_provider_b_snapshot_commitment bytea, + p_provider_a_call_count integer, + p_provider_b_call_count integer, + p_verification_accounts bytea[], + p_verification_account_chunk_end_offsets integer[], + p_provider_a_verification_chunk_commitments bytea[], + p_provider_b_verification_chunk_commitments bytea[], + p_provider_a_verification_chunk_call_counts integer[], + p_provider_b_verification_chunk_call_counts integer[], + p_folded_snapshot_commitment bytea, + p_execution_trace jsonb, + p_execution_trace_commitment bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + execution + programmable_private.projection_provider_execution_evidence%rowtype; + block_evidence programmable_private.dual_rpc_block_evidence%rowtype; + staged_vault programmable_private.reward_vault_projections%rowtype; + existing programmable_private.reward_snapshot_provider_evidence%rowtype; + expected_preimage bytea; + target_block bigint; + expected_reward_model text; + ordered_verification_accounts bytea[]; + expected_verification_accounts bytea[]; + folded_snapshot_preimage bytea; + execution_trace_preimage bytea; + chunk_count integer; + chunk_index integer; + chunk_call_total integer := 0; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_provider_evidence_encoding( + 'reward_snapshot', p_encoding_version, + p_canonical_preimage, p_content_fingerprint + ); + if p_reward_snapshot_evidence_id is null + or p_run_id is null + or p_execution_evidence_id is null + or p_target_block_evidence_id is null + or pg_catalog.octet_length(p_vault) <> 20 + or p_model_id is null + or p_reward_model is null + or p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or pg_catalog.octet_length(p_provider_a_snapshot_commitment) <> 32 + or pg_catalog.octet_length(p_provider_b_snapshot_commitment) <> 32 + or p_provider_a_snapshot_commitment <> + p_provider_b_snapshot_commitment + or p_provider_a_snapshot_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_provider_a_call_count not between 1 and 11008 + or p_provider_b_call_count is distinct from p_provider_a_call_count + or coalesce(pg_catalog.cardinality(p_verification_accounts), 0) + not between 1 and 4096 + or exists ( + select 1 + from pg_catalog.unnest(p_verification_accounts) as account + where account is null or pg_catalog.octet_length(account) <> 20 + ) + or coalesce(pg_catalog.cardinality( + p_verification_account_chunk_end_offsets + ), 0) not between 1 and 86 + or coalesce(pg_catalog.cardinality( + p_provider_a_verification_chunk_commitments + ), -1) <> pg_catalog.cardinality( + p_verification_account_chunk_end_offsets + ) + or coalesce(pg_catalog.cardinality( + p_provider_b_verification_chunk_commitments + ), -1) <> pg_catalog.cardinality( + p_verification_account_chunk_end_offsets + ) + or coalesce(pg_catalog.cardinality( + p_provider_a_verification_chunk_call_counts + ), -1) <> pg_catalog.cardinality( + p_verification_account_chunk_end_offsets + ) + or coalesce(pg_catalog.cardinality( + p_provider_b_verification_chunk_call_counts + ), -1) <> pg_catalog.cardinality( + p_verification_account_chunk_end_offsets + ) + or pg_catalog.octet_length(p_folded_snapshot_commitment) <> 32 + or p_folded_snapshot_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or pg_catalog.octet_length(p_execution_trace_commitment) <> 32 + or p_execution_trace_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or pg_catalog.octet_length(p_content_fingerprint) <> 32 + or p_content_fingerprint = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + then + raise exception using + errcode = '22023', message = 'invalid reward snapshot evidence'; + end if; + chunk_count := pg_catalog.cardinality( + p_verification_account_chunk_end_offsets + ); + if pg_catalog.jsonb_array_length(p_execution_trace -> 'calls') <> + chunk_count * 2 + then + raise exception using + errcode = '23514', + message = 'reward execution trace does not exactly cover chunks'; + end if; + perform programmable_private.assert_reward_verification_chunk_manifest_v1( + p_verification_accounts, + p_verification_account_chunk_end_offsets, + p_provider_a_verification_chunk_commitments, + p_provider_b_verification_chunk_commitments, + p_provider_a_verification_chunk_call_counts, + p_provider_b_verification_chunk_call_counts, + p_provider_a_call_count, + p_provider_b_call_count + ); + for chunk_index in 1..chunk_count loop + if p_verification_account_chunk_end_offsets[chunk_index] < 1 + or p_verification_account_chunk_end_offsets[chunk_index] <> + least( + chunk_index * 48, + pg_catalog.cardinality(p_verification_accounts) + ) + or pg_catalog.octet_length( + p_provider_a_verification_chunk_commitments[chunk_index] + ) <> 32 + or p_provider_a_verification_chunk_commitments[chunk_index] = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_provider_b_verification_chunk_commitments[chunk_index] + is distinct from + p_provider_a_verification_chunk_commitments[chunk_index] + or p_provider_a_verification_chunk_call_counts[chunk_index] is null + or p_provider_a_verification_chunk_call_counts[chunk_index] + not between 1 and 128 + or p_provider_b_verification_chunk_call_counts[chunk_index] + is distinct from + p_provider_a_verification_chunk_call_counts[chunk_index] + then + raise exception using + errcode = '23514', + message = 'reward verification chunk manifest changed'; + end if; + chunk_call_total := chunk_call_total + + p_provider_a_verification_chunk_call_counts[chunk_index]; + end loop; + if p_verification_account_chunk_end_offsets[chunk_count] <> + pg_catalog.cardinality(p_verification_accounts) + or chunk_call_total <> p_provider_a_call_count + then + raise exception using + errcode = '23514', + message = 'reward verification chunks do not exactly cover reads'; + end if; + select pg_catalog.array_agg(account order by account) + into ordered_verification_accounts + from pg_catalog.unnest(p_verification_accounts) as account; + if p_verification_accounts is distinct from ordered_verification_accounts + or pg_catalog.cardinality(p_verification_accounts) <> + ( + select pg_catalog.count(distinct account) + from pg_catalog.unnest(p_verification_accounts) as account + ) + then + raise exception using + errcode = '22023', + message = 'reward verification accounts are not canonical'; + end if; + target_block := p_target_block_number::bigint; + + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection' + for share; + if not found + or header.model_id <> p_model_id + or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'reward snapshot run is not open'; + end if; + expected_reward_model := case + when header.release_id = 'classic-v3' then 'classic-v3' + when header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) then 'stock-paired' + else null + end; + if p_reward_model is distinct from expected_reward_model then + raise exception using + errcode = '23514', message = 'reward snapshot model changed'; + end if; + select * into execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + and run_id = p_run_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and pointer_generation = header.captured_pointer_generation; + if not found then + raise exception using + errcode = '23503', message = 'projection execution evidence is missing'; + end if; + perform programmable_private.validate_reward_snapshot_execution_trace_v1( + p_execution_trace, execution.provider_a_id, execution.provider_b_id, + p_provider_a_call_count, p_provider_b_call_count + ); + execution_trace_preimage := + programmable_private.projection_execution_trace_preimage_v1( + p_execution_trace + ); + if p_execution_trace_commitment <> + pg_catalog.sha256(execution_trace_preimage) + then + raise exception using + errcode = '23514', + message = 'reward execution trace commitment changed'; + end if; + select * into block_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_target_block_evidence_id + and observation_id = execution.safe_head_observation_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and pointer_generation = header.captured_pointer_generation + and block_number = target_block + and agreed_block_hash = p_target_block_hash; + if not found then + raise exception using + errcode = '23514', + message = 'reward snapshot target is not dual-RPC evidence'; + end if; + + select * into staged_vault + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + and vault = p_vault + and model_id = p_model_id + and ( + snapshot_kind = 'exact_current' + or ( + snapshot_kind = 'initial_seed' + and exists ( + select 1 + from programmable_private.launch_projections as launch + where launch.projection_run_id = p_run_id + and launch.launch_projection_id = + reward_vault_projections.launch_projection_id + and launch.reward_vault = p_vault + and ( + exists ( + select 1 + from programmable_private.creator_fee_checkpoint_facts as fact + where fact.verification_run_id = p_run_id + and fact.vault = p_vault + ) + or exists ( + select 1 + from programmable_private.reward_configuration_activation_facts + as fact + where fact.verification_run_id = p_run_id + and fact.vault = p_vault + ) + or exists ( + select 1 + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + and claim.vault = p_vault + ) + or exists ( + select 1 + from programmable_private.payout_change_projections as payout + where payout.projection_run_id = p_run_id + and payout.vault = p_vault + ) + ) + ) + ) + ) + and promoted_block_number = target_block + and promoted_block_hash = p_target_block_hash; + if not found + or staged_vault.epoch_id <> header.epoch_id + or staged_vault.pointer_generation <> + header.captured_pointer_generation + then + raise exception using + errcode = '23514', + message = 'reward snapshot evidence has no exact staged snapshot'; + end if; + select pg_catalog.array_agg(required.account order by required.account) + into expected_verification_accounts + from ( + select allocation.beneficiary::bytea as account + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + and allocation.effective_to_block is null + union + select staged.account::bytea + from programmable_private.account_reward_balances as staged + left join programmable_private.current_account_reward_balances_v1 + as baseline + on baseline.chain_id = staged.chain_id + and baseline.release_id = staged.release_id + and baseline.model_id = staged.model_id + and baseline.epoch_id = staged.epoch_id + and baseline.pointer_generation = staged.pointer_generation + and baseline.vault = staged.vault + and baseline.account = staged.account + where staged.projection_run_id = p_run_id + and staged.vault = p_vault + and ( + baseline.account_reward_balance_id is null + or baseline.payout_address is distinct from staged.payout_address + or baseline.claimable_accrued is distinct from + staged.claimable_accrued + or baseline.claimed_total is distinct from staged.claimed_total + ) + ) as required; + if p_verification_accounts is distinct from + expected_verification_accounts + then + raise exception using + errcode = '23514', + message = 'reward verification account coverage changed'; + end if; + folded_snapshot_preimage := + programmable_private.reward_snapshot_folded_preimage_v1( + p_run_id, p_vault + ); + if p_folded_snapshot_commitment <> + pg_catalog.sha256(folded_snapshot_preimage) + then + raise exception using + errcode = '23514', + message = 'folded reward snapshot commitment changed'; + end if; + expected_preimage := + programmable_private.reward_snapshot_evidence_preimage_v1( + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, + p_run_id, p_execution_evidence_id, p_target_block_evidence_id, + p_vault, p_reward_model, target_block, p_target_block_hash, + execution.provider_a_id, execution.provider_b_id, + p_provider_a_snapshot_commitment, + p_provider_b_snapshot_commitment, + p_provider_a_call_count, p_provider_b_call_count, + p_verification_accounts, + p_verification_account_chunk_end_offsets, + p_provider_a_verification_chunk_commitments, + p_provider_b_verification_chunk_commitments, + p_provider_a_verification_chunk_call_counts, + p_provider_b_verification_chunk_call_counts, + p_folded_snapshot_commitment, + p_execution_trace_commitment + ); + if p_canonical_preimage <> expected_preimage then + raise exception using + errcode = '23514', message = 'reward snapshot evidence codec mismatch'; + end if; + + select * into existing + from programmable_private.reward_snapshot_provider_evidence + where reward_snapshot_evidence_id = p_reward_snapshot_evidence_id + or (run_id = p_run_id and vault = p_vault); + if found then + if existing.reward_snapshot_evidence_id <> + p_reward_snapshot_evidence_id + or existing.run_id <> p_run_id + or existing.execution_evidence_id <> p_execution_evidence_id + or existing.target_block_evidence_id <> + p_target_block_evidence_id + or existing.vault <> p_vault + or existing.model_id <> p_model_id + or existing.reward_model <> p_reward_model + or existing.target_block_number <> target_block + or existing.target_block_hash <> p_target_block_hash + or existing.provider_a_id <> execution.provider_a_id + or existing.provider_b_id <> execution.provider_b_id + or existing.provider_a_snapshot_commitment <> + p_provider_a_snapshot_commitment + or existing.provider_b_snapshot_commitment <> + p_provider_b_snapshot_commitment + or existing.provider_a_call_count <> p_provider_a_call_count + or existing.provider_b_call_count <> p_provider_b_call_count + or existing.verification_accounts <> p_verification_accounts + or existing.verification_account_chunk_end_offsets <> + p_verification_account_chunk_end_offsets + or existing.provider_a_verification_chunk_commitments <> + p_provider_a_verification_chunk_commitments + or existing.provider_b_verification_chunk_commitments <> + p_provider_b_verification_chunk_commitments + or existing.provider_a_verification_chunk_call_counts <> + p_provider_a_verification_chunk_call_counts::smallint[] + or existing.provider_b_verification_chunk_call_counts <> + p_provider_b_verification_chunk_call_counts::smallint[] + or existing.folded_snapshot_preimage <> + folded_snapshot_preimage + or existing.folded_snapshot_commitment <> + p_folded_snapshot_commitment + or existing.execution_trace <> p_execution_trace + or existing.execution_trace_preimage <> execution_trace_preimage + or existing.execution_trace_commitment <> + p_execution_trace_commitment + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + then + raise exception using + errcode = '23505', + message = 'reward snapshot evidence replay changed content'; + end if; + return existing.reward_snapshot_evidence_id; + end if; + + insert into programmable_private.reward_snapshot_provider_evidence ( + reward_snapshot_evidence_id, run_id, execution_evidence_id, + safe_head_observation_id, target_block_evidence_id, + epoch_id, chain_id, pointer_generation, vault, model_id, reward_model, + target_block_number, target_block_hash, provider_a_id, provider_b_id, + provider_a_snapshot_commitment, provider_b_snapshot_commitment, + provider_a_call_count, provider_b_call_count, + verification_accounts, verification_account_chunk_end_offsets, + provider_a_verification_chunk_commitments, + provider_b_verification_chunk_commitments, + provider_a_verification_chunk_call_counts, + provider_b_verification_chunk_call_counts, + folded_snapshot_preimage, + folded_snapshot_commitment, execution_trace, + execution_trace_preimage, execution_trace_commitment, + encoding_version, canonical_preimage, content_fingerprint, verified_at + ) values ( + p_reward_snapshot_evidence_id, p_run_id, p_execution_evidence_id, + execution.safe_head_observation_id, p_target_block_evidence_id, + header.epoch_id, header.chain_id, + header.captured_pointer_generation, + p_vault::programmable_private.eth_address, + p_model_id::programmable_private.model_identifier, + p_reward_model::programmable_private.model_identifier, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + execution.provider_a_id, execution.provider_b_id, + p_provider_a_snapshot_commitment::programmable_private.bytes32_value, + p_provider_b_snapshot_commitment::programmable_private.bytes32_value, + p_provider_a_call_count, + p_provider_b_call_count, + p_verification_accounts::programmable_private.eth_address[], + p_verification_account_chunk_end_offsets, + p_provider_a_verification_chunk_commitments:: + programmable_private.bytes32_value[], + p_provider_b_verification_chunk_commitments:: + programmable_private.bytes32_value[], + p_provider_a_verification_chunk_call_counts::smallint[], + p_provider_b_verification_chunk_call_counts::smallint[], + folded_snapshot_preimage, + p_folded_snapshot_commitment::programmable_private.bytes32_value, + p_execution_trace, execution_trace_preimage, + p_execution_trace_commitment::programmable_private.bytes32_value, + p_encoding_version, p_canonical_preimage, + p_content_fingerprint, p_verified_at + ); + perform programmable_private.append_mutation_audit( + 'projection.reward_snapshot_evidence.append', + p_content_fingerprint, p_run_id, p_verified_at + ); + return p_reward_snapshot_evidence_id; +end +$function$; + +create function programmable_private.stage_verified_dynamic_parents_v2( + p_provisional_page_id uuid, + p_run_id uuid, + p_release_id text, + p_model_id text, + p_source_group text, + p_projector_version text, + p_release_epoch_id uuid, + p_release_pointer_generation bigint, + p_reorg_generation bigint, + p_expected_cursor_generation bigint, + p_expected_cursor_block_hash bytea, + p_envio_provider_deployment_id uuid, + p_stream_id text, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_snapshot_block_number numeric, + p_snapshot_block_hash bytea, + p_filter_commitment bytea, + p_provider_a_parent_commitments bytea[], + p_provider_b_parent_commitments bytea[], + p_execution_trace jsonb, + p_execution_trace_commitment bytea, + p_parent_candidates jsonb, + p_provisional_dynamic_sources jsonb, + p_staged_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + ingestion_header programmable_private.run_headers%rowtype; + current_cursor + programmable_private.envio_ingestion_cursor_current%rowtype; + current_checkpoint + programmable_private.projector_checkpoint_current%rowtype; + existing programmable_private.provisional_dynamic_parent_pages%rowtype; + template programmable_private.release_dynamic_source_templates%rowtype; + runtime programmable_private.dual_rpc_runtime_code_evidence%rowtype; + parent_candidate_ids text[]; + parent_candidate_commitments bytea[]; + parent_set_commitment bytea; + execution_trace_preimage bytea; + coverage_commitment bytea; + candidate_item jsonb; + candidate_ordinal bigint; + candidate_keys text[]; + expected_candidate_keys text[] := array[ + 'blockGlobalLogIndex', 'blockHash', 'blockNumber', 'candidateId', + 'contentCommitment', 'contractName', 'decodedPayload', 'eventSignature', + 'eventType', 'payloadHash', 'sourceAddress', 'transactionHash', + 'transactionIndex' + ]; + source_item jsonb; + source_ordinal bigint; + source_keys text[]; + expected_source_keys text[] := array[ + 'dynamicSourceAttestationId', 'parentCandidateId', + 'provisionalLineageId', 'runtimeCodeEvidenceId', 'templateId' + ]; + deployed_source_address bytea; + stored_dynamic_sources jsonb; + prior_log_index bigint := -1; + snapshot_block bigint; + audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_provisional_page_id is null + or p_run_id is null + or p_release_id is null + or p_model_id is null + or p_source_group is null + or p_projector_version is null + or p_release_epoch_id is null + or p_release_pointer_generation < 1 + or p_reorg_generation < 0 + or p_expected_cursor_generation < 0 + or pg_catalog.octet_length(p_expected_cursor_block_hash) <> 32 + or p_envio_provider_deployment_id is null + or p_stream_id is null + or p_provider_a_id is null + or p_provider_b_id is null + or p_provider_a_id = p_provider_b_id + or p_safe_head_observation_id is null + or p_target_block_evidence_id is null + or p_snapshot_block_number <> pg_catalog.trunc(p_snapshot_block_number) + or p_snapshot_block_number < 0 + or p_snapshot_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_snapshot_block_hash) <> 32 + or pg_catalog.octet_length(p_filter_commitment) <> 32 + or p_provider_a_parent_commitments is null + or p_provider_b_parent_commitments is null + or p_provider_a_parent_commitments <> + p_provider_b_parent_commitments + or not programmable_private.valid_topics( + p_provider_a_parent_commitments + ) + or p_execution_trace is null + or pg_catalog.octet_length(p_execution_trace_commitment) <> 32 + or p_parent_candidates is null + or pg_catalog.jsonb_typeof(p_parent_candidates) <> 'array' + or p_provisional_dynamic_sources is null + or pg_catalog.jsonb_typeof(p_provisional_dynamic_sources) <> 'array' + or p_staged_at is null + then + raise exception using + errcode = '22023', message = 'invalid provisional dynamic-source page'; + end if; + if pg_catalog.jsonb_array_length(p_parent_candidates) not between 1 and 32 + or coalesce(pg_catalog.cardinality( + p_provider_a_parent_commitments + ), 0) <> pg_catalog.jsonb_array_length(p_parent_candidates) + or pg_catalog.octet_length(p_parent_candidates::text) > 262144 + or pg_catalog.jsonb_array_length(p_provisional_dynamic_sources) <> + pg_catalog.jsonb_array_length(p_parent_candidates) + or pg_catalog.octet_length(p_provisional_dynamic_sources::text) > 65536 + then + raise exception using + errcode = '22023', message = 'invalid provisional dynamic-source page'; + end if; + snapshot_block := p_snapshot_block_number::bigint; + + select * into ingestion_header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'ingestion' + for share; + if not found + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'provisional ingestion run is not open'; + end if; + perform programmable_private.assert_current_epoch( + ingestion_header.chain_id, p_release_id, p_model_id, p_source_group, + p_release_epoch_id, p_release_pointer_generation + ); + if p_stream_id <> ingestion_header.source_group then + raise exception using + errcode = '23514', message = 'provisional ingestion stream changed'; + end if; + + if not exists ( + select 1 + from programmable_private.dual_rpc_block_evidence as block_evidence + join programmable_private.safe_head_observations as observation + on observation.observation_id = + block_evidence.observation_id + and observation.epoch_id = block_evidence.epoch_id + and observation.pointer_generation = + block_evidence.pointer_generation + where block_evidence.block_evidence_id = p_target_block_evidence_id + and block_evidence.observation_id = p_safe_head_observation_id + and block_evidence.epoch_id = ingestion_header.epoch_id + and block_evidence.chain_id = ingestion_header.chain_id + and block_evidence.pointer_generation = + ingestion_header.captured_pointer_generation + and block_evidence.block_number = snapshot_block + and block_evidence.agreed_block_hash = p_snapshot_block_hash + and observation.provider_a_id = p_provider_a_id + and observation.provider_b_id = p_provider_b_id + ) + or not exists ( + select 1 + from programmable_private.provider_deployments as provider + where provider.provider_deployment_id = + p_envio_provider_deployment_id + and provider.provider_type = 'envio_deployment' + ) + then + raise exception using + errcode = '23514', + message = 'provisional parent provider evidence changed'; + end if; + + execution_trace_preimage := + programmable_private.projection_execution_trace_preimage_v1( + p_execution_trace + ); + if p_execution_trace_commitment <> + pg_catalog.sha256(execution_trace_preimage) + or (p_execution_trace ->> 'candidateBatchSize')::numeric <> + pg_catalog.jsonb_array_length(p_parent_candidates) + then + raise exception using + errcode = '23514', + message = 'provisional parent execution trace changed'; + end if; + perform programmable_private.validate_projection_execution_trace_v1( + p_execution_trace, p_provider_a_id, p_provider_b_id + ); + + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = ingestion_header.chain_id + and provider_deployment_id = p_envio_provider_deployment_id + and stream_id = p_stream_id; + if not found + or current_cursor.generation <> p_expected_cursor_generation + or current_cursor.block_hash <> p_expected_cursor_block_hash + then + raise exception using + errcode = '40001', message = 'provisional parent cursor changed'; + end if; + select * into current_checkpoint + from programmable_private.projector_checkpoint_current + where chain_id = ingestion_header.chain_id + and release_id = p_release_id + and model_id = p_model_id + and source_group = p_source_group + and projector_version = p_projector_version; + if coalesce(current_checkpoint.reorg_generation, 0) <> + p_reorg_generation + then + raise exception using + errcode = '40001', message = 'provisional parent reorg generation changed'; + end if; + + for candidate_item, candidate_ordinal in + select value, ordinality + from pg_catalog.jsonb_array_elements(p_parent_candidates) + with ordinality as candidates(value, ordinality) + loop + if pg_catalog.jsonb_typeof(candidate_item) <> 'object' then + raise exception using + errcode = '23514', message = 'invalid provisional parent candidate'; + end if; + select pg_catalog.array_agg(key order by key) into candidate_keys + from pg_catalog.jsonb_object_keys(candidate_item) as key; + if candidate_keys is distinct from expected_candidate_keys + or candidate_item ->> 'candidateId' + !~ '^1:0x[0-9a-f]{64}:0x[0-9a-f]{64}:(0|[1-9][0-9]*)$' + or candidate_item ->> 'blockNumber' !~ '^(0|[1-9][0-9]*)$' + or candidate_item ->> 'blockHash' !~ '^0x[0-9a-f]{64}$' + or candidate_item ->> 'transactionHash' !~ '^0x[0-9a-f]{64}$' + or candidate_item ->> 'transactionIndex' !~ '^(0|[1-9][0-9]*)$' + or candidate_item ->> 'blockGlobalLogIndex' + !~ '^(0|[1-9][0-9]*)$' + or candidate_item ->> 'sourceAddress' !~ '^0x[0-9a-f]{40}$' + or candidate_item ->> 'eventSignature' !~ '^0x[0-9a-f]{64}$' + or candidate_item ->> 'payloadHash' !~ '^0x[0-9a-f]{64}$' + or candidate_item ->> 'contentCommitment' !~ '^0x[0-9a-f]{64}$' + or pg_catalog.jsonb_typeof(candidate_item -> 'decodedPayload') + <> 'object' + or candidate_item ->> 'contractName' not in ( + 'ClassicV3RewardVaultFactory', 'StockV1RewardVaultFactory', + 'StockV2V3RewardVaultFactory' + ) + or candidate_item ->> 'eventType' not in ( + 'ClassicRewardVaultDeployed', + 'QuoteAssetFeeSplitVaultDeployed' + ) + or (candidate_item ->> 'blockNumber')::numeric <> snapshot_block + or candidate_item ->> 'blockHash' <> + '0x' || pg_catalog.encode(p_snapshot_block_hash, 'hex') + or (candidate_item ->> 'blockGlobalLogIndex')::numeric <= + prior_log_index + or candidate_item ->> 'candidateId' <> + '1:' || (candidate_item ->> 'blockHash') || ':' || + (candidate_item ->> 'transactionHash') || ':' || + (candidate_item ->> 'blockGlobalLogIndex') + or pg_catalog.decode( + pg_catalog.substring(candidate_item ->> 'contentCommitment', 3), + 'hex' + ) <> p_provider_a_parent_commitments[candidate_ordinal::integer] + then + raise exception using + errcode = '23514', message = 'invalid provisional parent candidate'; + end if; + parent_candidate_ids := pg_catalog.array_append( + parent_candidate_ids, candidate_item ->> 'candidateId' + ); + parent_candidate_commitments := pg_catalog.array_append( + parent_candidate_commitments, + pg_catalog.decode( + pg_catalog.substring(candidate_item ->> 'contentCommitment', 3), + 'hex' + ) + ); + prior_log_index := + (candidate_item ->> 'blockGlobalLogIndex')::bigint; + end loop; + + for source_item, source_ordinal in + select value, ordinality + from pg_catalog.jsonb_array_elements(p_provisional_dynamic_sources) + with ordinality as sources(value, ordinality) + loop + if pg_catalog.jsonb_typeof(source_item) <> 'object' then + raise exception using + errcode = '22023', message = 'invalid provisional child lineage'; + end if; + select pg_catalog.array_agg(key order by key) into source_keys + from pg_catalog.jsonb_object_keys(source_item) as key; + if source_keys is distinct from expected_source_keys + or source_item ->> 'provisionalLineageId' !~ + '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + or source_item ->> 'dynamicSourceAttestationId' !~ + '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + or source_item ->> 'runtimeCodeEvidenceId' !~ + '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + or source_item ->> 'templateId' !~ + '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + or source_item ->> 'parentCandidateId' is distinct from + parent_candidate_ids[source_ordinal::integer] + then + raise exception using + errcode = '22023', message = 'invalid provisional child lineage'; + end if; + select value into candidate_item + from pg_catalog.jsonb_array_elements(p_parent_candidates) + with ordinality as candidates(value, ordinality) + where ordinality = source_ordinal; + select template_row.* + into template + from programmable_private.release_dynamic_source_templates + as template_row + join programmable_private.release_source_bindings as binding + on binding.binding_id = + template_row.parent_factory_release_binding_id + and binding.epoch_id = template_row.epoch_id + where template_row.dynamic_source_template_id = + (source_item ->> 'templateId')::uuid + and template_row.epoch_id = p_release_epoch_id + and binding.source_name = candidate_item ->> 'contractName' + and binding.source_address = pg_catalog.decode( + pg_catalog.substring(candidate_item ->> 'sourceAddress', 3), 'hex' + ) + and binding.binding_commitment = + template_row.parent_factory_binding_commitment + and template_row.factory_event_type = candidate_item ->> 'eventType'; + if template.dynamic_source_template_id is null then + raise exception using + errcode = '23514', message = 'provisional parent template changed'; + end if; + if candidate_item -> 'decodedPayload' ->> + template.deployed_address_field !~ '^0x[0-9a-f]{40}$' + or candidate_item -> 'decodedPayload' ->> + (template.immutable_binding_spec ->> 'factoryConfigurationField') + !~ '^0x[0-9a-f]{64}$' + then + raise exception using + errcode = '23514', message = 'provisional parent payload changed'; + end if; + deployed_source_address := pg_catalog.decode( + pg_catalog.substring( + candidate_item -> 'decodedPayload' ->> + template.deployed_address_field, + 3 + ), 'hex' + ); + select * into runtime + from programmable_private.dual_rpc_runtime_code_evidence + where runtime_code_evidence_id = + (source_item ->> 'runtimeCodeEvidenceId')::uuid + and verification_run_id = p_run_id + and chain_id = ingestion_header.chain_id + and epoch_id = ingestion_header.epoch_id + and pointer_generation = ingestion_header.captured_pointer_generation + and source_address = deployed_source_address + and deployment_block_evidence_id = p_target_block_evidence_id + and deployment_block_number = snapshot_block + and deployment_block_hash = p_snapshot_block_hash + and provider_a_id = p_provider_a_id + and provider_b_id = p_provider_b_id + and agreed_runtime_code_length = template.runtime_code_length + and agreed_normalized_runtime_code_hash = + template.normalized_runtime_code_hash + and immutable_references_commitment = + template.immutable_references_commitment + and reconstructed_runtime_code = runtime_code_a + and reconstructed_runtime_code_hash = agreed_runtime_code_hash; + if runtime.runtime_code_evidence_id is null + or ( + template.expected_instance_runtime_code_hash is not null + and runtime.agreed_runtime_code_hash <> + template.expected_instance_runtime_code_hash + ) + or not programmable_private.immutable_values_match_binding_spec( + template.immutable_binding_spec, + candidate_item -> 'decodedPayload', + deployed_source_address, + runtime.immutable_values + ) + then + raise exception using + errcode = '23514', + message = 'provisional child runtime is not template-attested'; + end if; + end loop; + + coverage_commitment := pg_catalog.sha256( + pg_catalog.convert_to( + pg_catalog.jsonb_build_object( + 'chainId', ingestion_header.chain_id::text, + 'runId', p_run_id::text, + 'ingestionEpochId', ingestion_header.epoch_id::text, + 'ingestionPointerGeneration', + ingestion_header.captured_pointer_generation::text, + 'expectedCursorGeneration', p_expected_cursor_generation::text, + 'expectedCursorBlockHash', + '0x' || pg_catalog.encode(p_expected_cursor_block_hash, 'hex'), + 'envioProviderDeploymentId', + p_envio_provider_deployment_id::text, + 'streamId', p_stream_id, + 'safeHeadObservationId', p_safe_head_observation_id::text, + 'targetBlockEvidenceId', p_target_block_evidence_id::text, + 'snapshotBlockNumber', snapshot_block::text, + 'snapshotBlockHash', + '0x' || pg_catalog.encode(p_snapshot_block_hash, 'hex'), + 'providerAId', p_provider_a_id::text, + 'providerBId', p_provider_b_id::text, + 'filterCommitment', + '0x' || pg_catalog.encode(p_filter_commitment, 'hex'), + 'parentCommitments', to_jsonb(p_provider_a_parent_commitments), + 'executionTraceCommitment', + '0x' || pg_catalog.encode( + p_execution_trace_commitment, 'hex' + ) + )::text, + 'UTF8' + ) + ); + parent_set_commitment := pg_catalog.sha256( + pg_catalog.convert_to( + 'programmable:provisional-dynamic-parent-page:v2', 'UTF8' + ) || coverage_commitment || coalesce(( + select pg_catalog.string_agg(commitment, ''::bytea order by ordinal) + from pg_catalog.unnest(parent_candidate_commitments) + with ordinality as commitments(commitment, ordinal) + ), ''::bytea) || pg_catalog.convert_to( + p_provisional_dynamic_sources::text, 'UTF8' + ) + ); + select * into existing + from programmable_private.provisional_dynamic_parent_pages + where chain_id = ingestion_header.chain_id + and release_id = p_release_id + and model_id = p_model_id + and source_group = p_source_group + and projector_version = p_projector_version + and release_epoch_id = p_release_epoch_id + and release_pointer_generation = p_release_pointer_generation + and reorg_generation = p_reorg_generation + and snapshot_block_number = snapshot_block + and snapshot_block_hash = p_snapshot_block_hash; + if found then + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'provisionalLineageId', lineage.provisional_lineage_id::text, + 'dynamicSourceAttestationId', + lineage.dynamic_source_attestation_id::text, + 'runtimeCodeEvidenceId', lineage.runtime_code_evidence_id::text, + 'templateId', lineage.dynamic_source_template_id::text, + 'parentCandidateId', lineage.parent_candidate_id::text + ) order by lineage.lineage_ordinal + ) into stored_dynamic_sources + from programmable_private.provisional_dynamic_source_lineages as lineage + where lineage.provisional_page_id = existing.provisional_page_id; + if existing.provisional_page_id <> p_provisional_page_id + or existing.staging_run_id <> p_run_id + or existing.ingestion_epoch_id <> ingestion_header.epoch_id + or existing.ingestion_pointer_generation <> + ingestion_header.captured_pointer_generation + or existing.expected_cursor_generation <> + p_expected_cursor_generation + or existing.expected_cursor_block_hash <> + p_expected_cursor_block_hash + or existing.safe_head_observation_id <> + p_safe_head_observation_id + or existing.target_block_evidence_id <> + p_target_block_evidence_id + or existing.envio_provider_deployment_id <> + p_envio_provider_deployment_id + or existing.stream_id <> p_stream_id + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.filter_commitment <> p_filter_commitment + or existing.provider_a_parent_commitments <> + p_provider_a_parent_commitments:: + programmable_private.bytes32_value[] + or existing.provider_b_parent_commitments <> + p_provider_b_parent_commitments:: + programmable_private.bytes32_value[] + or existing.execution_trace <> p_execution_trace + or existing.execution_trace_preimage <> + execution_trace_preimage + or existing.execution_trace_commitment <> + p_execution_trace_commitment + or existing.coverage_commitment <> coverage_commitment + or existing.parent_candidate_ids <> parent_candidate_ids + or existing.parent_candidate_commitments <> + parent_candidate_commitments + or existing.parent_candidates <> p_parent_candidates + or existing.parent_set_commitment <> parent_set_commitment + or stored_dynamic_sources is distinct from + p_provisional_dynamic_sources + then + raise exception using + errcode = '23505', + message = 'provisional dynamic-source replay changed content'; + end if; + return existing.provisional_page_id; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'projection.dynamic_parent.provisional_stage_v2', + parent_set_commitment, p_run_id, p_staged_at + ); + insert into programmable_private.provisional_dynamic_parent_pages ( + provisional_page_id, staging_run_id, chain_id, release_id, model_id, + source_group, projector_version, release_epoch_id, + release_pointer_generation, ingestion_epoch_id, + ingestion_pointer_generation, reorg_generation, + expected_cursor_generation, expected_cursor_block_hash, + envio_provider_deployment_id, stream_id, + safe_head_observation_id, target_block_evidence_id, + provider_a_id, provider_b_id, filter_commitment, + provider_a_parent_commitments, provider_b_parent_commitments, + execution_trace, execution_trace_preimage, + execution_trace_commitment, coverage_commitment, + snapshot_block_number, snapshot_block_hash, + parent_candidate_ids, parent_candidate_commitments, + parent_candidates, parent_set_commitment, staged_at + ) values ( + p_provisional_page_id, p_run_id, ingestion_header.chain_id, + p_release_id::programmable_private.release_identifier, + p_model_id::programmable_private.model_identifier, + p_source_group::programmable_private.source_identifier, + p_projector_version::programmable_private.projector_identifier, + p_release_epoch_id, p_release_pointer_generation, + ingestion_header.epoch_id, + ingestion_header.captured_pointer_generation, + p_reorg_generation, p_expected_cursor_generation, + p_expected_cursor_block_hash, p_envio_provider_deployment_id, + p_stream_id::programmable_private.source_identifier, + p_safe_head_observation_id, p_target_block_evidence_id, + p_provider_a_id, p_provider_b_id, p_filter_commitment, + p_provider_a_parent_commitments:: + programmable_private.bytes32_value[], + p_provider_b_parent_commitments:: + programmable_private.bytes32_value[], + p_execution_trace, execution_trace_preimage, + p_execution_trace_commitment, coverage_commitment, + snapshot_block::programmable_private.block_number_value, + p_snapshot_block_hash::programmable_private.bytes32_value, + parent_candidate_ids::programmable_private.envio_candidate_identifier[], + parent_candidate_commitments::programmable_private.bytes32_value[], + p_parent_candidates, parent_set_commitment, p_staged_at + ); + for source_item, source_ordinal in + select value, ordinality + from pg_catalog.jsonb_array_elements(p_provisional_dynamic_sources) + with ordinality as sources(value, ordinality) + loop + select value into candidate_item + from pg_catalog.jsonb_array_elements(p_parent_candidates) + with ordinality as candidates(value, ordinality) + where ordinality = source_ordinal; + select * into template + from programmable_private.release_dynamic_source_templates + where dynamic_source_template_id = + (source_item ->> 'templateId')::uuid; + deployed_source_address := pg_catalog.decode( + pg_catalog.substring( + candidate_item -> 'decodedPayload' ->> + template.deployed_address_field, + 3 + ), 'hex' + ); + insert into programmable_private.provisional_dynamic_source_lineages ( + provisional_lineage_id, provisional_page_id, lineage_ordinal, + dynamic_source_attestation_id, dynamic_source_template_id, + runtime_code_evidence_id, parent_candidate_id, + parent_candidate_commitment, deployed_source_address + ) values ( + (source_item ->> 'provisionalLineageId')::uuid, + p_provisional_page_id, source_ordinal::smallint, + (source_item ->> 'dynamicSourceAttestationId')::uuid, + (source_item ->> 'templateId')::uuid, + (source_item ->> 'runtimeCodeEvidenceId')::uuid, + (source_item ->> 'parentCandidateId'):: + programmable_private.envio_candidate_identifier, + parent_candidate_commitments[source_ordinal::integer]:: + programmable_private.bytes32_value, + deployed_source_address::programmable_private.eth_address + ); + end loop; + return p_provisional_page_id; +end +$function$; + +create function programmable_private.get_current_provisional_dynamic_sources_v1( + p_projector_version text +) +returns table ( + provisional_page_id uuid, + provisional_lineage_id uuid, + release_epoch_id uuid, + release_pointer_generation bigint, + ingestion_epoch_id uuid, + ingestion_pointer_generation bigint, + reorg_generation bigint, + snapshot_block_number bigint, + snapshot_block_hash bytea, + expected_cursor_generation bigint, + expected_cursor_block_hash bytea, + envio_provider_deployment_id uuid, + rpc_provider_a_id uuid, + rpc_provider_b_id uuid, + provisional_coverage_commitment bytea, + runtime_code_evidence_id uuid, + dynamic_source_template_id uuid, + dynamic_source_attestation_id uuid, + deployed_source_address bytea, + contract_name text, + model text, + release_version text, + factory_address bytea, + factory_contract_name text, + factory_candidate_id text, + factory_block_number bigint, + factory_block_hash bytea, + factory_block_global_log_index bigint, + parent_candidate_commitment bytea, + expected_exact_runtime_code_hash bytea, + expected_normalized_runtime_code_hash bytea, + expected_immutable_references_commitment bytea, + expected_runtime_byte_length bigint, + immutable_references jsonb, + staged_at timestamptz +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_projector_version is null then + raise exception using + errcode = '22023', message = 'invalid projector version'; + end if; + return query + select page.provisional_page_id, + lineage.provisional_lineage_id, + page.release_epoch_id, + page.release_pointer_generation, + page.ingestion_epoch_id, + page.ingestion_pointer_generation, + page.reorg_generation, + page.snapshot_block_number::bigint, + page.snapshot_block_hash::bytea, + page.expected_cursor_generation, + page.expected_cursor_block_hash::bytea, + page.envio_provider_deployment_id, + page.provider_a_id, + page.provider_b_id, + page.coverage_commitment::bytea, + runtime.runtime_code_evidence_id, + template.dynamic_source_template_id, + lineage.dynamic_source_attestation_id, + lineage.deployed_source_address::bytea, + case page.release_id + when 'classic-v3' then 'ClassicV3RewardVault' + when 'stock-paired-v1' then 'StockV1RewardVault' + when 'stock-paired-v2' then 'StockV2V3RewardVault' + when 'stock-paired-v3' then 'StockV2V3RewardVault' + end, + page.model_id::text, + page.release_id::text, + factory_binding.source_address::bytea, + factory_binding.source_name::text, + lineage.parent_candidate_id::text, + (parent.item ->> 'blockNumber')::bigint, + pg_catalog.decode( + pg_catalog.substring(parent.item ->> 'blockHash', 3), 'hex' + ), + (parent.item ->> 'blockGlobalLogIndex')::bigint, + lineage.parent_candidate_commitment::bytea, + runtime.agreed_runtime_code_hash::bytea, + runtime.agreed_normalized_runtime_code_hash::bytea, + runtime.immutable_references_commitment::bytea, + runtime.agreed_runtime_code_length, + references_json.items, + page.staged_at + from programmable_private.provisional_dynamic_parent_pages as page + join programmable_private.provisional_dynamic_source_lineages as lineage + on lineage.provisional_page_id = page.provisional_page_id + join programmable_private.release_epoch_current as current_release + on current_release.chain_id = page.chain_id + and current_release.release_id = page.release_id + and current_release.model_id = page.model_id + and current_release.source_group = page.source_group + and current_release.epoch_id = page.release_epoch_id + and current_release.pointer_generation = page.release_pointer_generation + join programmable_private.run_headers as ingestion_header + on ingestion_header.run_id = page.staging_run_id + and ingestion_header.epoch_id = page.ingestion_epoch_id + and ingestion_header.captured_pointer_generation = + page.ingestion_pointer_generation + join programmable_private.release_epoch_current as current_ingestion + on current_ingestion.chain_id = ingestion_header.chain_id + and current_ingestion.release_id = ingestion_header.release_id + and current_ingestion.model_id = ingestion_header.model_id + and current_ingestion.source_group = ingestion_header.source_group + and current_ingestion.epoch_id = page.ingestion_epoch_id + and current_ingestion.pointer_generation = + page.ingestion_pointer_generation + join programmable_private.envio_ingestion_cursor_current as cursor + on cursor.chain_id = page.chain_id + and cursor.provider_deployment_id = + page.envio_provider_deployment_id + and cursor.stream_id = page.stream_id + and cursor.generation = page.expected_cursor_generation + and cursor.block_hash = page.expected_cursor_block_hash + join programmable_private.release_dynamic_source_templates as template + on template.dynamic_source_template_id = + lineage.dynamic_source_template_id + and template.epoch_id = page.release_epoch_id + join programmable_private.release_source_bindings as factory_binding + on factory_binding.binding_id = + template.parent_factory_release_binding_id + and factory_binding.epoch_id = template.epoch_id + join programmable_private.dual_rpc_runtime_code_evidence as runtime + on runtime.runtime_code_evidence_id = lineage.runtime_code_evidence_id + and runtime.verification_run_id = page.staging_run_id + and runtime.source_address = lineage.deployed_source_address + and runtime.deployment_block_number = page.snapshot_block_number + and runtime.deployment_block_hash = page.snapshot_block_hash + cross join lateral ( + select value as item + from pg_catalog.jsonb_array_elements(page.parent_candidates) + where value ->> 'candidateId' = lineage.parent_candidate_id::text + ) as parent + cross join lateral ( + select coalesce( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'start', (binding ->> 'offset')::integer, + 'length', (binding ->> 'length')::integer + ) order by (binding ->> 'ordinal')::integer + ), + '[]'::jsonb + ) as items + from pg_catalog.jsonb_array_elements( + template.immutable_binding_spec -> 'bindings' + ) as binding + ) as references_json + left join programmable_private.projector_checkpoint_current as checkpoint + on checkpoint.chain_id = page.chain_id + and checkpoint.release_id = page.release_id + and checkpoint.model_id = page.model_id + and checkpoint.source_group = page.source_group + and checkpoint.projector_version = page.projector_version + where page.projector_version = p_projector_version + and coalesce(checkpoint.reorg_generation, 0) = page.reorg_generation + and not exists ( + select 1 + from programmable_private.provisional_dynamic_parent_consumptions + as consumed + where consumed.provisional_page_id = page.provisional_page_id + ) + order by page.snapshot_block_number, page.provisional_page_id, + lineage.lineage_ordinal; +end +$function$; + +create function programmable_private.consume_matching_provisional_sources_v1( + p_final_run_id uuid, + p_publication_id uuid, + p_final_execution_evidence_id uuid, + p_final_target_block_evidence_id uuid, + p_occurrence_ids uuid[], + p_consumed_at timestamptz +) +returns integer +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + publication programmable_private.projection_publications%rowtype; + final_execution + programmable_private.projection_provider_execution_evidence%rowtype; + page programmable_private.provisional_dynamic_parent_pages%rowtype; + lineage programmable_private.provisional_dynamic_source_lineages%rowtype; + consumed_count integer := 0; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_final_run_id + and run_kind = 'projection'; + select * into publication + from programmable_private.projection_publications + where publication_id = p_publication_id + and run_id = p_final_run_id + and published_at = p_consumed_at; + select * into final_execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_final_execution_evidence_id + and run_id = p_final_run_id; + if header.run_id is null + or publication.publication_id is null + or final_execution.execution_evidence_id is null + or not exists ( + select 1 + from programmable_private.dual_rpc_block_evidence as evidence + where evidence.block_evidence_id = p_final_target_block_evidence_id + and evidence.observation_id = + final_execution.safe_head_observation_id + and evidence.epoch_id = header.epoch_id + and evidence.chain_id = header.chain_id + and evidence.pointer_generation = + header.captured_pointer_generation + and evidence.block_number = publication.target_block_number + and evidence.agreed_block_hash = publication.target_block_hash + ) + then + raise exception using + errcode = '23514', + message = 'final provisional-source evidence changed'; + end if; + + for page in + select staged.* + from programmable_private.provisional_dynamic_parent_pages as staged + left join programmable_private.provisional_dynamic_parent_consumptions + as consumed + on consumed.provisional_page_id = staged.provisional_page_id + where staged.chain_id = header.chain_id + and staged.release_id = header.release_id + and staged.model_id = header.model_id + and staged.source_group = header.source_group + and staged.release_epoch_id = header.epoch_id + and staged.release_pointer_generation = + header.captured_pointer_generation + and staged.snapshot_block_number = publication.target_block_number + and staged.snapshot_block_hash = publication.target_block_hash + and consumed.provisional_page_id is null + loop + if page.reorg_generation <> coalesce(( + select checkpoint.reorg_generation + from programmable_private.projector_checkpoint_current as checkpoint + where checkpoint.chain_id = header.chain_id + and checkpoint.release_id = header.release_id + and checkpoint.model_id = header.model_id + and checkpoint.source_group = header.source_group + and checkpoint.projector_version = page.projector_version + ), 0) + or page.envio_provider_deployment_id <> + final_execution.envio_provider_deployment_id + or page.provider_a_id <> final_execution.provider_a_id + or page.provider_b_id <> final_execution.provider_b_id + then + raise exception using + errcode = '23514', + message = 'provisional source provider context changed'; + end if; + + for lineage in + select staged_lineage.* + from programmable_private.provisional_dynamic_source_lineages + as staged_lineage + where staged_lineage.provisional_page_id = page.provisional_page_id + order by staged_lineage.lineage_ordinal + loop + if not exists ( + select 1 + from programmable_private.dynamic_source_attestations as attestation + join programmable_private.dual_rpc_runtime_code_evidence + as final_runtime + on final_runtime.runtime_code_evidence_id = + attestation.runtime_code_evidence_id + join programmable_private.dual_rpc_runtime_code_evidence + as staged_runtime + on staged_runtime.runtime_code_evidence_id = + lineage.runtime_code_evidence_id + join programmable_private.chain_event_occurrences as parent + on parent.occurrence_id = + attestation.parent_factory_occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = parent.occurrence_id + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + where attestation.dynamic_source_attestation_id = + lineage.dynamic_source_attestation_id + and attestation.verification_run_id = p_final_run_id + and attestation.chain_id = header.chain_id + and attestation.release_id = header.release_id + and attestation.model_id = header.model_id + and attestation.source_group = header.source_group + and attestation.epoch_id = header.epoch_id + and attestation.pointer_generation = + header.captured_pointer_generation + and attestation.dynamic_source_template_id = + lineage.dynamic_source_template_id + and attestation.deployed_source_address = + lineage.deployed_source_address + and parent.occurrence_id = any(p_occurrence_ids) + and parent.block_number = page.snapshot_block_number + and parent.block_hash = page.snapshot_block_hash + and parent.content_commitment = + lineage.parent_candidate_commitment + and coalesce( + materialization.first_seen_neutral_candidate_id::text, + materialization.first_seen_envio_candidate_id::text + ) = lineage.parent_candidate_id::text + and final_runtime.verification_run_id = p_final_run_id + and final_runtime.deployment_block_evidence_id = + p_final_target_block_evidence_id + and final_runtime.provider_a_id = final_execution.provider_a_id + and final_runtime.provider_b_id = final_execution.provider_b_id + and final_runtime.source_address = + staged_runtime.source_address + and final_runtime.deployment_block_number = + staged_runtime.deployment_block_number + and final_runtime.deployment_block_hash = + staged_runtime.deployment_block_hash + and final_runtime.runtime_code_a = staged_runtime.runtime_code_a + and final_runtime.runtime_code_b = staged_runtime.runtime_code_b + and final_runtime.agreed_runtime_code_hash = + staged_runtime.agreed_runtime_code_hash + and final_runtime.agreed_runtime_code_length = + staged_runtime.agreed_runtime_code_length + and final_runtime.agreed_normalized_runtime_code_hash = + staged_runtime.agreed_normalized_runtime_code_hash + and final_runtime.immutable_references_commitment = + staged_runtime.immutable_references_commitment + and final_runtime.immutable_values = + staged_runtime.immutable_values + and final_runtime.immutable_values_commitment = + staged_runtime.immutable_values_commitment + and final_runtime.reconstructed_runtime_code = + staged_runtime.reconstructed_runtime_code + and final_runtime.reconstructed_runtime_code_hash = + staged_runtime.reconstructed_runtime_code_hash + ) then + raise exception using + errcode = '23514', + message = 'final block omitted a provisional child attestation'; + end if; + end loop; + insert into programmable_private.provisional_dynamic_parent_consumptions ( + provisional_page_id, final_run_id, publication_id, + final_execution_evidence_id, final_target_block_evidence_id, + consumed_at + ) values ( + page.provisional_page_id, p_final_run_id, p_publication_id, + p_final_execution_evidence_id, p_final_target_block_evidence_id, + p_consumed_at + ); + consumed_count := consumed_count + 1; + end loop; + return consumed_count; +end +$function$; + +create function programmable_private.assert_classic_reward_block_fold_v1( + p_run_id uuid, + p_vault bytea, + p_occurrence_ids uuid[] +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + staged_vault programmable_private.reward_vault_projections%rowtype; + baseline_vault programmable_private.reward_vault_projections%rowtype; + block_event record; + activation programmable_private.reward_configuration_activation_facts%rowtype; + allocation_accounts bytea[]; + allocation_shares numeric[]; + balance_accounts bytea[]; + balance_claimable numeric[]; + balance_claimed numeric[]; + configuration_epoch bigint; + active_configuration_hash bytea; + received numeric; + event_amount numeric; + event_total numeric; + event_account bytea; + previous_account bytea; + next_account bytea; + beneficiary_total numeric; + allocation_position integer; + balance_position integer; + idx integer; + non_last_total numeric; + allocation_credit numeric; +begin + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection' + and release_id = 'classic-v3'; + select * into staged_vault + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + and vault = p_vault + and snapshot_kind = 'exact_current'; + if header.run_id is null or staged_vault.reward_vault_projection_id is null + then + raise exception using + errcode = '23503', message = 'Classic reward fold state is missing'; + end if; + select * into baseline_vault + from programmable_private.current_reward_vault_projections_v1 + where reward_vault_projection_id = + staged_vault.baseline_reward_vault_projection_id; + if baseline_vault.reward_vault_projection_id is null then + raise exception using + errcode = '23503', message = 'Classic reward fold baseline is missing'; + end if; + select + pg_catalog.array_agg( + allocation.payout_address::bytea order by allocation.allocation_index + ), + pg_catalog.array_agg( + allocation.share_bps::numeric order by allocation.allocation_index + ) + into allocation_accounts, allocation_shares + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + baseline_vault.reward_vault_projection_id + and allocation.projection_run_id = baseline_vault.projection_run_id + and allocation.effective_to_block is null; + select + pg_catalog.array_agg(balance.account::bytea order by balance.account), + pg_catalog.array_agg( + balance.claimable_accrued::numeric order by balance.account + ), + pg_catalog.array_agg(balance.claimed_total::numeric order by balance.account) + into balance_accounts, balance_claimable, balance_claimed + from programmable_private.current_account_reward_balances_v1 as balance + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = p_vault; + if allocation_accounts is null or balance_accounts is null then + raise exception using + errcode = '23514', message = 'Classic reward fold baseline is incomplete'; + end if; + configuration_epoch := baseline_vault.configuration_epoch; + active_configuration_hash := baseline_vault.active_configuration_hash; + received := baseline_vault.total_creator_fees_received; + + for block_event in + select source.*, materialization.event_type, + materialization.decoded_payload + from pg_catalog.unnest(p_occurrence_ids) + with ordinality as requested(occurrence_id, ordinal) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + where source.source_address = p_vault + order by requested.ordinal + loop + if block_event.event_type = 'CreatorFeesCheckpointed' then + event_amount := + (block_event.decoded_payload ->> 'amount')::numeric; + event_total := + (block_event.decoded_payload ->> 'totalCreatorFeesReceived')::numeric; + if programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'poolId', 32 + ) is distinct from staged_vault.pool_id + or (block_event.decoded_payload ->> 'configurationEpoch')::numeric + is distinct from configuration_epoch::numeric + or event_amount <= 0 + or event_total <> received + event_amount + then + raise exception using + errcode = '23514', + message = 'Classic checkpoint does not match ordered state'; + end if; + non_last_total := 0; + for idx in 1..pg_catalog.cardinality(allocation_accounts) loop + if idx < pg_catalog.cardinality(allocation_accounts) then + allocation_credit := pg_catalog.div( + event_amount * allocation_shares[idx], 10000 + ); + non_last_total := non_last_total + allocation_credit; + else + allocation_credit := event_amount - non_last_total; + end if; + balance_position := pg_catalog.array_position( + balance_accounts, allocation_accounts[idx] + ); + if balance_position is null then + balance_accounts := pg_catalog.array_append( + balance_accounts, allocation_accounts[idx] + ); + balance_claimable := pg_catalog.array_append( + balance_claimable, 0::numeric + ); + balance_claimed := pg_catalog.array_append( + balance_claimed, 0::numeric + ); + balance_position := pg_catalog.cardinality(balance_accounts); + end if; + balance_claimable[balance_position] := + balance_claimable[balance_position] + allocation_credit; + end loop; + received := event_total; + elsif block_event.event_type = 'BeneficiaryFeesClaimed' then + event_account := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'beneficiary', 20 + ); + event_amount := (block_event.decoded_payload ->> 'amount')::numeric; + beneficiary_total := ( + block_event.decoded_payload ->> 'beneficiaryTotalClaimed' + )::numeric; + event_total := ( + block_event.decoded_payload ->> 'vaultTotalReceived' + )::numeric; + balance_position := pg_catalog.array_position( + balance_accounts, event_account + ); + if balance_position is null + or event_total <> received + or event_amount <= 0 + or balance_claimable[balance_position] <> event_amount + or beneficiary_total <> + balance_claimed[balance_position] + event_amount + then + raise exception using + errcode = '23514', + message = 'Classic claim does not match ordered state'; + end if; + balance_claimable[balance_position] := 0; + balance_claimed[balance_position] := beneficiary_total; + elsif block_event.event_type = 'PayoutWalletChanged' then + allocation_position := + (block_event.decoded_payload ->> 'allocationIndex')::integer + 1; + if allocation_position not between 1 and + pg_catalog.cardinality(allocation_accounts) + then + raise exception using + errcode = '23514', message = 'Classic payout index is invalid'; + end if; + previous_account := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'previousPayoutWallet', 20 + ); + next_account := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'newPayoutWallet', 20 + ); + if programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'poolId', 32 + ) is distinct from staged_vault.pool_id + or previous_account <> allocation_accounts[allocation_position] + or next_account = previous_account + or (block_event.decoded_payload ->> 'shareBps')::numeric <> + allocation_shares[allocation_position] + or (block_event.decoded_payload ->> 'configurationEpoch')::numeric + <> configuration_epoch + 1 + or (block_event.decoded_payload ->> + 'effectiveTotalCreatorFeesReceived')::numeric <> received + then + raise exception using + errcode = '23514', + message = 'Classic payout change does not match ordered state'; + end if; + allocation_accounts[allocation_position] := next_account; + balance_position := pg_catalog.array_position( + balance_accounts, next_account + ); + if balance_position is null then + balance_accounts := pg_catalog.array_append( + balance_accounts, next_account + ); + balance_claimable := pg_catalog.array_append( + balance_claimable, 0::numeric + ); + balance_claimed := pg_catalog.array_append( + balance_claimed, 0::numeric + ); + end if; + configuration_epoch := configuration_epoch + 1; + active_configuration_hash := + programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'activeConfigurationHash', 32 + ); + elsif block_event.event_type = 'CtoRewardConfigurationActivated' then + select * into activation + from programmable_private.reward_configuration_activation_facts + where source_occurrence_id = block_event.occurrence_id + and verification_run_id = p_run_id + and vault = p_vault + and pool_id = staged_vault.pool_id; + if activation.reward_configuration_activation_fact_id is null + or activation.configuration_epoch <> configuration_epoch + 1 + or activation.previous_configuration_hash <> + active_configuration_hash + or activation.effective_total_creator_fees_received <> received + then + raise exception using + errcode = '23514', + message = 'Classic CTO change does not match ordered state'; + end if; + allocation_accounts := activation.ordered_beneficiaries::bytea[]; + allocation_shares := activation.ordered_shares_bps::numeric[]; + for idx in 1..pg_catalog.cardinality(allocation_accounts) loop + if pg_catalog.array_position( + balance_accounts, allocation_accounts[idx] + ) is null then + balance_accounts := pg_catalog.array_append( + balance_accounts, allocation_accounts[idx] + ); + balance_claimable := pg_catalog.array_append( + balance_claimable, 0::numeric + ); + balance_claimed := pg_catalog.array_append( + balance_claimed, 0::numeric + ); + end if; + end loop; + configuration_epoch := activation.configuration_epoch; + active_configuration_hash := activation.new_configuration_hash; + else + raise exception using + errcode = '23514', message = 'Classic reward event is unsupported'; + end if; + end loop; + + if staged_vault.configuration_epoch <> configuration_epoch + or staged_vault.active_configuration_hash <> + active_configuration_hash + or staged_vault.total_creator_fees_received <> received + or ( + select pg_catalog.count(*) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + ) <> pg_catalog.cardinality(allocation_accounts) + or exists ( + select 1 + from pg_catalog.generate_series( + 1, pg_catalog.cardinality(allocation_accounts) + ) as position + where not exists ( + select 1 + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + and allocation.allocation_index = position - 1 + and allocation.beneficiary = allocation_accounts[position] + and allocation.payout_address = allocation_accounts[position] + and allocation.share_bps = allocation_shares[position] + and allocation.configuration_epoch = configuration_epoch + ) + ) + or ( + select pg_catalog.count(*) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + ) <> pg_catalog.cardinality(balance_accounts) + or exists ( + select 1 + from pg_catalog.generate_series( + 1, pg_catalog.cardinality(balance_accounts) + ) as position + where not exists ( + select 1 + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + and balance.account = balance_accounts[position] + and balance.payout_address = balance_accounts[position] + and balance.claimable_accrued = balance_claimable[position] + and balance.claimed_total = balance_claimed[position] + ) + ) + then + raise exception using + errcode = '23514', + message = 'Classic staged snapshot differs from ordered block fold'; + end if; +end +$function$; + +create function programmable_private.assert_stock_reward_block_fold_v1( + p_run_id uuid, + p_vault bytea, + p_occurrence_ids uuid[] +) +returns void +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + staged_vault programmable_private.reward_vault_projections%rowtype; + baseline_vault programmable_private.reward_vault_projections%rowtype; + block_event record; + allocation_accounts bytea[]; + allocation_payouts bytea[]; + allocation_shares numeric[]; + balance_accounts bytea[]; + balance_payouts bytea[]; + balance_claimable numeric[]; + balance_claimed numeric[]; + event_account bytea; + previous_account bytea; + next_account bytea; + event_recipient bytea; + event_amount numeric; + event_beneficiary_total numeric; + event_vault_total numeric; + allocation_position integer; + balance_position integer; + idx integer; + non_last_total numeric := 0; + entitlement numeric; +begin + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection' + and release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ); + select * into staged_vault + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + and vault = p_vault + and snapshot_kind = 'exact_current'; + if header.run_id is null or staged_vault.reward_vault_projection_id is null + then + raise exception using + errcode = '23503', message = 'Stock reward fold state is missing'; + end if; + select * into baseline_vault + from programmable_private.current_reward_vault_projections_v1 + where reward_vault_projection_id = + staged_vault.baseline_reward_vault_projection_id; + if baseline_vault.reward_vault_projection_id is null then + raise exception using + errcode = '23503', message = 'Stock reward fold baseline is missing'; + end if; + select + pg_catalog.array_agg( + allocation.beneficiary::bytea order by allocation.allocation_index + ), + pg_catalog.array_agg( + allocation.payout_address::bytea order by allocation.allocation_index + ), + pg_catalog.array_agg( + allocation.share_bps::numeric order by allocation.allocation_index + ) + into allocation_accounts, allocation_payouts, allocation_shares + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + baseline_vault.reward_vault_projection_id + and allocation.projection_run_id = baseline_vault.projection_run_id + and allocation.effective_to_block is null; + select + pg_catalog.array_agg(balance.account::bytea order by balance.account), + pg_catalog.array_agg( + balance.payout_address::bytea order by balance.account + ), + pg_catalog.array_agg( + balance.claimable_accrued::numeric order by balance.account + ), + pg_catalog.array_agg(balance.claimed_total::numeric order by balance.account) + into balance_accounts, balance_payouts, balance_claimable, balance_claimed + from programmable_private.current_account_reward_balances_v1 as balance + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = header.captured_pointer_generation + and balance.vault = p_vault; + if allocation_accounts is null or balance_accounts is null then + raise exception using + errcode = '23514', message = 'Stock reward fold baseline is incomplete'; + end if; + + for block_event in + select source.*, materialization.event_type, + materialization.decoded_payload + from pg_catalog.unnest(p_occurrence_ids) + with ordinality as requested(occurrence_id, ordinal) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + where source.source_address = p_vault + order by requested.ordinal + loop + if block_event.event_type = 'PayoutAddressUpdated' then + event_account := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'beneficiary', 20 + ); + previous_account := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'previousPayoutAddress', 20 + ); + next_account := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'newPayoutAddress', 20 + ); + allocation_position := pg_catalog.array_position( + allocation_accounts, event_account + ); + balance_position := pg_catalog.array_position( + balance_accounts, event_account + ); + if allocation_position is null + or balance_position is null + or previous_account <> allocation_payouts[allocation_position] + or previous_account <> balance_payouts[balance_position] + or next_account = previous_account + then + raise exception using + errcode = '23514', + message = 'Stock payout change does not match ordered state'; + end if; + allocation_payouts[allocation_position] := next_account; + balance_payouts[balance_position] := next_account; + elsif block_event.event_type = 'BeneficiaryFeesClaimed' then + event_account := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'beneficiary', 20 + ); + event_recipient := programmable_private.json_hex_bytes_v1( + block_event.decoded_payload, 'payoutAddress', 20 + ); + event_amount := (block_event.decoded_payload ->> 'amount')::numeric; + event_beneficiary_total := ( + block_event.decoded_payload ->> 'beneficiaryTotalClaimed' + )::numeric; + event_vault_total := ( + block_event.decoded_payload ->> 'vaultTotalReceived' + )::numeric; + balance_position := pg_catalog.array_position( + balance_accounts, event_account + ); + if balance_position is null + or event_recipient <> balance_payouts[balance_position] + or event_amount <= 0 + or event_beneficiary_total <> + balance_claimed[balance_position] + event_amount + or event_vault_total > staged_vault.total_creator_fees_received + then + raise exception using + errcode = '23514', + message = 'Stock claim does not match ordered state'; + end if; + balance_claimed[balance_position] := event_beneficiary_total; + else + raise exception using + errcode = '23514', message = 'Stock reward event is unsupported'; + end if; + end loop; + + if staged_vault.configuration_epoch <> + baseline_vault.configuration_epoch + or staged_vault.active_configuration_hash <> + baseline_vault.active_configuration_hash + or staged_vault.total_creator_fees_received < + baseline_vault.total_creator_fees_received + or ( + select pg_catalog.count(*) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + ) <> pg_catalog.cardinality(allocation_accounts) + or exists ( + select 1 + from pg_catalog.generate_series( + 1, pg_catalog.cardinality(allocation_accounts) + ) as position + where not exists ( + select 1 + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + and allocation.allocation_index = position - 1 + and allocation.beneficiary = allocation_accounts[position] + and allocation.payout_address = allocation_payouts[position] + and allocation.share_bps = allocation_shares[position] + and allocation.configuration_epoch = + staged_vault.configuration_epoch + ) + ) + then + raise exception using + errcode = '23514', + message = 'Stock staged allocation differs from ordered block fold'; + end if; + + non_last_total := 0; + for idx in 1..pg_catalog.cardinality(allocation_accounts) loop + if idx < pg_catalog.cardinality(allocation_accounts) then + entitlement := pg_catalog.div( + staged_vault.total_creator_fees_received * allocation_shares[idx], + 10000 + ); + non_last_total := non_last_total + entitlement; + else + entitlement := staged_vault.total_creator_fees_received + - non_last_total; + end if; + balance_position := pg_catalog.array_position( + balance_accounts, allocation_accounts[idx] + ); + if balance_position is null + or entitlement < balance_claimed[balance_position] + then + raise exception using + errcode = '23514', message = 'Stock entitlement is incomplete'; + end if; + balance_claimable[balance_position] := + entitlement - balance_claimed[balance_position]; + end loop; + if ( + select pg_catalog.count(*) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + ) <> pg_catalog.cardinality(balance_accounts) + or exists ( + select 1 + from pg_catalog.generate_series( + 1, pg_catalog.cardinality(balance_accounts) + ) as position + where not exists ( + select 1 + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = p_vault + and balance.account = balance_accounts[position] + and balance.payout_address = balance_payouts[position] + and balance.claimable_accrued = balance_claimable[position] + and balance.claimed_total = balance_claimed[position] + ) + ) + then + raise exception using + errcode = '23514', + message = 'Stock staged balances differ from ordered block fold'; + end if; +end +$function$; + +create function programmable_private.stage_current_reward_snapshot_v2( + p_run_id uuid, + p_vault bytea, + p_pool_id bytea, + p_initial_allocation_fact_id uuid, + p_configuration_epoch bigint, + p_active_configuration_hash bytea, + p_total_creator_fees_received numeric, + p_allocation_indices integer[], + p_beneficiaries bytea[], + p_payout_addresses bytea[], + p_shares_bps numeric[], + p_balance_accounts bytea[], + p_balance_payout_addresses bytea[], + p_claimable_accrued numeric[], + p_claimed_totals numeric[], + p_snapshot_source_occurrence_id uuid, + p_occurrence_ids uuid[], + p_promoted_block_number numeric, + p_promoted_block_hash bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + scope record; + baseline record; + existing_vault programmable_private.reward_vault_projections%rowtype; + returned_id uuid; + allocation_id uuid; + balance_id uuid; + allocation_count integer; + balance_count integer; + total_share_bps numeric := 0; + total_balance_value numeric := 0; + normalized_total numeric; + idx integer; + prior_idx integer; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection' + for share; + if not found then + raise exception using + errcode = '23503', message = 'invalid reward snapshot run'; + end if; + if coalesce(pg_catalog.cardinality(p_occurrence_ids), 0) <= 1 then + return programmable_private.stage_current_reward_snapshot_v1( + p_run_id, p_vault, p_pool_id, p_initial_allocation_fact_id, + p_configuration_epoch, p_active_configuration_hash, + p_total_creator_fees_received, p_allocation_indices, + p_beneficiaries, p_payout_addresses, p_shares_bps, + p_balance_accounts, p_balance_payout_addresses, + p_claimable_accrued, p_claimed_totals, + p_snapshot_source_occurrence_id, p_promoted_block_number, + p_promoted_block_hash, p_verified_at + ); + end if; + if header.release_id not in ( + 'classic-v3', + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) then + raise exception using + errcode = '55000', message = 'grouped reward release is unsupported'; + end if; + + select * into scope + from programmable_private.projection_stage_context( + p_run_id, p_snapshot_source_occurrence_id, + p_promoted_block_number, p_promoted_block_hash + ); + normalized_total := programmable_private.validate_uint256( + p_total_creator_fees_received + ); + allocation_count := coalesce( + pg_catalog.cardinality(p_allocation_indices), 0 + ); + balance_count := coalesce(pg_catalog.cardinality(p_balance_accounts), 0); + if pg_catalog.octet_length(p_vault) <> 20 + or pg_catalog.octet_length(p_pool_id) <> 32 + or pg_catalog.octet_length(p_active_configuration_hash) <> 32 + or p_configuration_epoch is null + or p_configuration_epoch <= 0 + or coalesce(pg_catalog.cardinality(p_occurrence_ids), 0) + not between 2 and 4096 + or not (p_snapshot_source_occurrence_id = any(p_occurrence_ids)) + or p_snapshot_source_occurrence_id is distinct from ( + select requested.occurrence_id + from pg_catalog.unnest(p_occurrence_ids) + with ordinality as requested(occurrence_id, ordinal) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + where source.source_address = p_vault + order by requested.ordinal desc + limit 1 + ) + or allocation_count not between 1 and ( + case when header.release_id = 'classic-v3' then 5 else 8 end + ) + or pg_catalog.cardinality(p_beneficiaries) <> allocation_count + or pg_catalog.cardinality(p_payout_addresses) <> allocation_count + or pg_catalog.cardinality(p_shares_bps) <> allocation_count + or balance_count not between 1 and 65535 + or pg_catalog.cardinality(p_balance_payout_addresses) <> balance_count + or pg_catalog.cardinality(p_claimable_accrued) <> balance_count + or pg_catalog.cardinality(p_claimed_totals) <> balance_count + then + raise exception using + errcode = '22023', message = 'invalid grouped reward snapshot'; + end if; + for idx in 1..allocation_count loop + if p_allocation_indices[idx] <> idx - 1 + or pg_catalog.octet_length(p_beneficiaries[idx]) <> 20 + or pg_catalog.octet_length(p_payout_addresses[idx]) <> 20 + or ( + header.release_id = 'classic-v3' + and p_beneficiaries[idx] <> p_payout_addresses[idx] + ) + or p_shares_bps[idx] is null + or p_shares_bps[idx] <> pg_catalog.trunc(p_shares_bps[idx]) + or p_shares_bps[idx] not between 1 and 10000 + then + raise exception using + errcode = '22023', message = 'invalid grouped reward allocation'; + end if; + if header.release_id <> 'classic-v3' and idx > 1 then + for prior_idx in 1..idx - 1 loop + if p_beneficiaries[prior_idx] = p_beneficiaries[idx] then + raise exception using + errcode = '22023', + message = 'grouped Stock beneficiaries must remain unique'; + end if; + end loop; + end if; + total_share_bps := total_share_bps + p_shares_bps[idx]; + end loop; + if total_share_bps <> 10000 then + raise exception using + errcode = '22023', message = 'grouped reward shares do not conserve'; + end if; + for idx in 1..balance_count loop + if pg_catalog.octet_length(p_balance_accounts[idx]) <> 20 + or pg_catalog.octet_length( + p_balance_payout_addresses[idx] + ) <> 20 + or ( + header.release_id = 'classic-v3' + and p_balance_accounts[idx] <> + p_balance_payout_addresses[idx] + ) + or ( + idx > 1 and + p_balance_accounts[idx - 1] >= p_balance_accounts[idx] + ) + then + raise exception using + errcode = '22023', message = 'invalid grouped reward balance'; + end if; + total_balance_value := total_balance_value + + programmable_private.validate_uint256(p_claimable_accrued[idx]) + + programmable_private.validate_uint256(p_claimed_totals[idx]); + end loop; + if total_balance_value <> normalized_total then + raise exception using + errcode = '23514', message = 'grouped reward balances do not conserve'; + end if; + + select + vault.*, + entity.checkpoint_id as current_checkpoint_id, + checkpoint.checkpoint_generation as current_checkpoint_generation, + checkpoint.reorg_generation as current_reorg_generation + into baseline + from programmable_private.current_reward_vault_projections_v1 as vault + join programmable_private.projection_entity_current as entity + on entity.entity_kind = 'reward_vault' + and entity.projection_row_id = vault.reward_vault_projection_id + and entity.projection_run_id = vault.projection_run_id + and entity.chain_id = vault.chain_id + and entity.release_id = vault.release_id + and entity.model_id = vault.model_id + and entity.source_group = header.source_group + join programmable_private.projector_checkpoints as baseline_checkpoint + on baseline_checkpoint.checkpoint_id = entity.checkpoint_id + and baseline_checkpoint.epoch_id = header.epoch_id + and baseline_checkpoint.pointer_generation = + header.captured_pointer_generation + join programmable_private.projector_checkpoint_current as current_pointer + on current_pointer.chain_id = baseline_checkpoint.chain_id + and current_pointer.release_id = baseline_checkpoint.release_id + and current_pointer.model_id = baseline_checkpoint.model_id + and current_pointer.source_group = baseline_checkpoint.source_group + and current_pointer.projector_version = + baseline_checkpoint.projector_version + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_pointer.checkpoint_id + and checkpoint.chain_id = baseline_checkpoint.chain_id + and checkpoint.release_id = baseline_checkpoint.release_id + and checkpoint.model_id = baseline_checkpoint.model_id + and checkpoint.source_group = baseline_checkpoint.source_group + and checkpoint.projector_version = baseline_checkpoint.projector_version + and checkpoint.epoch_id = header.epoch_id + and checkpoint.pointer_generation = header.captured_pointer_generation + and checkpoint.checkpoint_generation = + current_pointer.checkpoint_generation + and checkpoint.reorg_generation = current_pointer.reorg_generation + where vault.chain_id = header.chain_id + and vault.release_id = header.release_id + and vault.model_id = header.model_id + and vault.epoch_id = header.epoch_id + and vault.pointer_generation = header.captured_pointer_generation + and vault.vault = p_vault + and vault.pool_id = p_pool_id; + if baseline.reward_vault_projection_id is null + or baseline.snapshot_kind not in ('initial_seed', 'exact_current') + or baseline.current_allocation_fact_id <> + p_initial_allocation_fact_id + or normalized_total < baseline.total_creator_fees_received + then + raise exception using + errcode = '23514', message = 'grouped reward baseline changed'; + end if; + + select * into existing_vault + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + and vault = p_vault; + if found then + if existing_vault.pool_id <> p_pool_id + or existing_vault.configuration_epoch <> p_configuration_epoch + or existing_vault.active_configuration_hash <> + p_active_configuration_hash + or existing_vault.total_creator_fees_received <> normalized_total + or existing_vault.last_source_occurrence_id <> + p_snapshot_source_occurrence_id + then + raise exception using + errcode = '23505', + message = 'grouped reward snapshot replay changed content'; + end if; + if header.release_id = 'classic-v3' then + perform programmable_private.assert_classic_reward_block_fold_v1( + p_run_id, p_vault, p_occurrence_ids + ); + else + perform programmable_private.assert_stock_reward_block_fold_v1( + p_run_id, p_vault, p_occurrence_ids + ); + end if; + return existing_vault.reward_vault_projection_id; + end if; + + returned_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_vault_projections ( + reward_vault_projection_id, launch_projection_id, chain_id, release_id, + model_id, epoch_id, pointer_generation, vault, pool_id, quote_asset, + configuration_hash, current_allocation_fact_id, + last_source_logical_event_id, last_source_occurrence_id, + last_source_occurrence_block_hash, projection_run_id, + promoted_block_number, promoted_block_hash, verified_at, + snapshot_kind, configuration_epoch, active_configuration_hash, + total_creator_fees_received, baseline_reward_vault_projection_id, + baseline_checkpoint_id, baseline_checkpoint_generation, + baseline_reorg_generation + ) values ( + returned_id, baseline.launch_projection_id, header.chain_id, + header.release_id, header.model_id, header.epoch_id, + header.captured_pointer_generation, + p_vault::programmable_private.eth_address, + p_pool_id::programmable_private.bytes32_value, baseline.quote_asset, + baseline.configuration_hash, p_initial_allocation_fact_id, + scope.source_logical_event_id, p_snapshot_source_occurrence_id, + scope.source_occurrence_block_hash, p_run_id, + scope.promoted_block_number, scope.promoted_block_hash, p_verified_at, + 'exact_current', p_configuration_epoch, + p_active_configuration_hash::programmable_private.bytes32_value, + normalized_total::programmable_private.uint256_value, + baseline.reward_vault_projection_id, baseline.current_checkpoint_id, + baseline.current_checkpoint_generation, baseline.current_reorg_generation + ); + for idx in 1..allocation_count loop + allocation_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_allocation_projections ( + reward_allocation_projection_id, reward_vault_projection_id, + allocation_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, configuration_epoch, allocation_index, + beneficiary, payout_address, share_bps, effective_from_block, + effective_to_block, last_source_logical_event_id, + last_source_occurrence_id, last_source_occurrence_block_hash, + projection_run_id, promoted_block_number, promoted_block_hash, + verified_at + ) values ( + allocation_id, returned_id, p_initial_allocation_fact_id, + header.chain_id, header.release_id, header.model_id, header.epoch_id, + header.captured_pointer_generation, p_configuration_epoch, + p_allocation_indices[idx], + p_beneficiaries[idx]::programmable_private.eth_address, + p_payout_addresses[idx]::programmable_private.eth_address, + p_shares_bps[idx]::programmable_private.basis_points, + scope.promoted_block_number, null, scope.source_logical_event_id, + p_snapshot_source_occurrence_id, scope.source_occurrence_block_hash, + p_run_id, scope.promoted_block_number, scope.promoted_block_hash, + p_verified_at + ); + end loop; + for idx in 1..balance_count loop + balance_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.account_reward_balances ( + account_reward_balance_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, account, vault, payout_address, + claimable_accrued, claimed_total, last_source_logical_event_id, + last_source_occurrence_id, last_source_occurrence_block_hash, + projection_run_id, promoted_block_number, promoted_block_hash, + verified_at + ) values ( + balance_id, header.chain_id, header.release_id, header.model_id, + header.epoch_id, header.captured_pointer_generation, + p_balance_accounts[idx]::programmable_private.eth_address, + p_vault::programmable_private.eth_address, + p_balance_payout_addresses[idx]::programmable_private.eth_address, + p_claimable_accrued[idx]::programmable_private.uint256_value, + p_claimed_totals[idx]::programmable_private.uint256_value, + scope.source_logical_event_id, p_snapshot_source_occurrence_id, + scope.source_occurrence_block_hash, p_run_id, + scope.promoted_block_number, scope.promoted_block_hash, p_verified_at + ); + end loop; + if header.release_id = 'classic-v3' then + perform programmable_private.assert_classic_reward_block_fold_v1( + p_run_id, p_vault, p_occurrence_ids + ); + else + perform programmable_private.assert_stock_reward_block_fold_v1( + p_run_id, p_vault, p_occurrence_ids + ); + end if; + perform programmable_private.append_mutation_audit( + 'reward_snapshot_group.stage', p_active_configuration_hash, + p_run_id, p_verified_at + ); + return returned_id; +end +$function$; + +create function programmable_private.promote_reward_block_group_v1( + p_publication_id uuid, + p_checkpoint_id uuid, + p_outcome_id uuid, + p_run_id uuid, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_expected_checkpoint_generation bigint, + p_next_checkpoint_generation bigint, + p_reorg_generation bigint, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea, + p_cursor_block_global_log_index numeric, + p_cursor_candidate_id text, + p_occurrence_ids uuid[], + p_allocation_fact_ids uuid[], + p_allocation_evidence_ids uuid[], + p_candidate_disposition_ids uuid[], + p_route_keys text[], + p_result_commitment bytea, + p_execution_evidence_id uuid, + p_reward_snapshot_evidence_ids uuid[], + p_provider_binding_id uuid, + p_provider_binding_commitment bytea, + p_published_at timestamptz +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + observation programmable_private.safe_head_observations%rowtype; + target_evidence programmable_private.dual_rpc_block_evidence%rowtype; + execution + programmable_private.projection_provider_execution_evidence%rowtype; + current_checkpoint + programmable_private.projector_checkpoint_current%rowtype; + previous_checkpoint programmable_private.projector_checkpoints%rowtype; + staged_vault programmable_private.reward_vault_projections%rowtype; + baseline_vault programmable_private.reward_vault_projections%rowtype; + group_event record; + selected_route_key text; + target_block bigint; + cursor_log_index bigint; + audit_id uuid; + status_id uuid; + route_history_id uuid; + ordered_occurrence_ids uuid[]; + complete_group_occurrence_ids uuid[]; + ordered_fact_ids uuid[]; + ordered_disposition_ids uuid[]; + required_disposition_ids uuid[]; + ordered_route_keys text[]; + ordered_projection_rows text[]; + projection_row_count bigint; + vault_count bigint; + allocation_count bigint; + balance_count bigint; + claim_count bigint; + claim_event_count bigint; + payout_count bigint; + payout_event_count bigint; + allocation_row_count bigint; + balance_row_count bigint; + terminal_event_count bigint; + baseline_total numeric; + checkpoint_amount_total numeric; + checkpoint_terminal_total numeric; + vault_terminal_occurrence_id uuid; + total_share_bps numeric; + total_balance_value numeric; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection' + for update; + if not found then + raise exception using + errcode = '23503', message = 'invalid projection run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation + ); + if header.release_id not in ( + 'classic-v3', + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'reward block run is not promotable'; + end if; + if not exists ( + select 1 + from programmable_private.projector_lease_current as lease + where lease.chain_id = header.chain_id + and lease.release_id = header.release_id + and lease.model_id = header.model_id + and lease.source_group = header.source_group + and lease.projector_version = p_projector_version + and lease.epoch_id = header.epoch_id + and lease.pointer_generation = header.captured_pointer_generation + and lease.lease_generation = p_lease_generation + and lease.lease_token_hash = p_lease_token_hash + and lease.expires_at >= p_published_at + ) then + raise exception using + errcode = '40001', message = 'stale projector lease'; + end if; + + select pg_catalog.count(*) into vault_count + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id; + if p_publication_id is null + or p_checkpoint_id is null + or p_outcome_id is null + or p_provider_binding_id is null + or p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or p_cursor_block_global_log_index < 0 + or p_cursor_block_global_log_index <> + pg_catalog.trunc(p_cursor_block_global_log_index) + or p_cursor_block_global_log_index > 4294967295 + or p_cursor_candidate_id is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + or pg_catalog.octet_length(p_provider_binding_commitment) <> 32 + or p_provider_binding_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_next_checkpoint_generation <> + p_expected_checkpoint_generation + 1 + or vault_count not between 1 and 4096 + or coalesce(pg_catalog.cardinality(p_occurrence_ids), 0) + not between 1 and 4096 + or pg_catalog.cardinality(p_allocation_fact_ids) <> vault_count + or pg_catalog.cardinality(p_allocation_evidence_ids) <> vault_count + or pg_catalog.cardinality(p_reward_snapshot_evidence_ids) <> + vault_count + or coalesce(pg_catalog.cardinality(p_route_keys), 0) + not between 1 and 32 + then + raise exception using + errcode = '22023', message = 'invalid reward block promotion request'; + end if; + + select pg_catalog.array_agg(item order by item) into ordered_fact_ids + from ( + select distinct item + from pg_catalog.unnest(p_allocation_fact_ids) as item + ) as unique_items; + select pg_catalog.array_agg(item order by item) + into ordered_disposition_ids + from ( + select distinct item + from pg_catalog.unnest(p_candidate_disposition_ids) as item + ) as unique_items; + select pg_catalog.array_agg(item order by item) into ordered_route_keys + from ( + select distinct item + from pg_catalog.unnest(p_route_keys) as item + ) as unique_items; + if p_allocation_fact_ids is distinct from ordered_fact_ids + or p_candidate_disposition_ids is distinct from + coalesce(ordered_disposition_ids, array[]::uuid[]) + or p_route_keys is distinct from ordered_route_keys + or exists ( + select 1 + from pg_catalog.unnest(p_occurrence_ids) as item + where item is null + ) + or pg_catalog.cardinality(p_occurrence_ids) <> + ( + select pg_catalog.count(distinct item) + from pg_catalog.unnest(p_occurrence_ids) as item + ) + or exists ( + select 1 + from pg_catalog.unnest(p_allocation_fact_ids) as item + where item is null + ) + or exists ( + select 1 + from pg_catalog.unnest(p_allocation_evidence_ids) as item + where item is null + ) + or pg_catalog.cardinality(p_allocation_evidence_ids) <> + ( + select pg_catalog.count(distinct item) + from pg_catalog.unnest(p_allocation_evidence_ids) as item + ) + or exists ( + select 1 + from pg_catalog.unnest(p_candidate_disposition_ids) as item + where item is null + ) + or exists ( + select 1 + from pg_catalog.unnest(p_route_keys) as item + where item is null + ) + then + raise exception using + errcode = '22023', + message = 'reward block arrays are not canonical'; + end if; + + target_block := p_target_block_number::bigint; + cursor_log_index := p_cursor_block_global_log_index::bigint; + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and pointer_generation = header.captured_pointer_generation; + if not found or target_block > observation.safe_block_number then + raise exception using + errcode = '23514', message = 'target is outside accepted safe head'; + end if; + select * into target_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_target_block_evidence_id + and observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and pointer_generation = header.captured_pointer_generation; + if not found + or target_evidence.block_number <> target_block + or target_evidence.agreed_block_hash <> p_target_block_hash + then + raise exception using + errcode = '23514', message = 'target block evidence changed'; + end if; + perform programmable_private.assert_projection_provider_evidence_v1( + 'exact_incremental', p_run_id, p_safe_head_observation_id, + p_target_block_evidence_id, target_block, p_target_block_hash, + p_execution_evidence_id, p_reward_snapshot_evidence_ids + ); + select * into execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + and run_id = p_run_id; + + select * into current_checkpoint + from programmable_private.projector_checkpoint_current + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + for update; + if not found + or current_checkpoint.checkpoint_generation <> + p_expected_checkpoint_generation + or current_checkpoint.reorg_generation <> p_reorg_generation + or p_expected_checkpoint_generation = 0 + then + raise exception using + errcode = '40001', message = 'reward block checkpoint CAS lost'; + end if; + select * into previous_checkpoint + from programmable_private.projector_checkpoints + where checkpoint_id = current_checkpoint.checkpoint_id; + if not found + or previous_checkpoint.epoch_id <> header.epoch_id + or previous_checkpoint.pointer_generation <> + header.captured_pointer_generation + or ( + target_block, cursor_log_index, p_cursor_candidate_id + ) <= ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + then + raise exception using + errcode = '23514', message = 'reward block cursor did not advance'; + end if; + if not exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = p_cursor_candidate_id + and candidate.chain_id = header.chain_id + and candidate.provider_deployment_id = + execution.envio_provider_deployment_id + and candidate.block_number = target_block + and candidate.block_hash = p_target_block_hash + and candidate.block_global_log_index = cursor_log_index + ) then + raise exception using + errcode = '23514', + message = 'reward block cursor or Envio deployment changed'; + end if; + if exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + where candidate.chain_id = header.chain_id + and candidate.provider_deployment_id <> + execution.envio_provider_deployment_id + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id) + ) then + raise exception using + errcode = '23514', message = 'Envio provider was substituted in range'; + end if; + if exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + where candidate.chain_id = header.chain_id + and candidate.block_number = target_block + and ( + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > (cursor_log_index, p_cursor_candidate_id) + ) then + raise exception using + errcode = '23514', message = 'reward block cursor is not block-terminal'; + end if; + if exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + left join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + where candidate.chain_id = header.chain_id + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id) + and coalesce(status.status::text, 'pending') not in ( + 'resolved', 'ignored', 'quarantined' + ) + ) then + raise exception using + errcode = '23514', + message = 'reward block cursor cannot pass a pending candidate'; + end if; + select pg_catalog.array_agg(status.decision_id order by status.decision_id) + into required_disposition_ids + from programmable_private.envio_candidate_inbox as candidate + join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + and status.status in ('resolved', 'ignored', 'quarantined') + where candidate.chain_id = header.chain_id + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id); + if p_candidate_disposition_ids is distinct from + coalesce(required_disposition_ids, array[]::uuid[]) + or execution.candidate_batch_size <> + coalesce(pg_catalog.cardinality(required_disposition_ids), 0) + then + raise exception using + errcode = '23514', + message = 'candidate disposition manifest or execution count changed'; + end if; + + if exists ( + select 1 from programmable_private.launch_projections + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.pool_projections + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.pool_fee_configurations + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.fee_accrual_facts + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.pool_fee_totals + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.initial_buy_custody_projections + where projection_run_id = p_run_id + ) + or exists ( + select 1 from programmable_private.initial_buy_vesting_projections + where projection_run_id = p_run_id + ) + then + raise exception using + errcode = '23514', + message = 'reward block contains another projection mode'; + end if; + + select pg_catalog.array_agg( + source.occurrence_id + order by source.block_number, source.block_global_log_index, + source.transaction_index, source.receipt_log_ordinal, + source.occurrence_id + ) into ordered_occurrence_ids + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation; + if ordered_occurrence_ids is distinct from p_occurrence_ids + or pg_catalog.cardinality(ordered_occurrence_ids) <> + pg_catalog.cardinality(p_occurrence_ids) + then + raise exception using + errcode = '23514', + message = 'reward block occurrences are incomplete or misordered'; + end if; + + select pg_catalog.array_agg( + source.occurrence_id + order by source.block_number, source.block_global_log_index, + source.transaction_index, source.receipt_log_ordinal, + source.occurrence_id + ) into complete_group_occurrence_ids + from programmable_private.chain_event_occurrences as source + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + left join programmable_private.release_source_bindings as binding + on binding.binding_id = materialization.release_binding_id + left join programmable_private.dynamic_source_attestations as dynamic_source + on dynamic_source.dynamic_source_attestation_id = + materialization.dynamic_source_attestation_id + where source.chain_id = header.chain_id + and coalesce( + binding.source_role::text, + dynamic_source.deployed_source_role::text + ) = 'reward_vault' + and ( + source.block_number::bigint, + source.block_global_log_index::bigint + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint + ) + and ( + source.block_number::bigint, + source.block_global_log_index::bigint + ) <= (target_block, cursor_log_index); + if complete_group_occurrence_ids is distinct from p_occurrence_ids then + raise exception using + errcode = '23514', + message = 'reward block group omits a reward-vault occurrence'; + end if; + + for group_event in + select + source.*, + materialization.event_type as materialized_event_type, + materialization.block_evidence_id as materialized_block_evidence_id, + coalesce( + binding.source_role::text, + dynamic_source.deployed_source_role::text + ) as materialized_source_role + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.chain_id = header.chain_id + and materialization.release_id = header.release_id + and materialization.model_id = header.model_id + and materialization.source_group = header.source_group + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + left join programmable_private.release_source_bindings as binding + on binding.binding_id = materialization.release_binding_id + left join programmable_private.dynamic_source_attestations as dynamic_source + on dynamic_source.dynamic_source_attestation_id = + materialization.dynamic_source_attestation_id + order by source.block_number, source.block_global_log_index, + source.transaction_index, source.receipt_log_ordinal, + source.occurrence_id + loop + if group_event.block_number <> target_block + or group_event.block_hash <> p_target_block_hash + or group_event.materialized_source_role <> 'reward_vault' + or not exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and vault.vault = group_event.source_address + ) + or not exists ( + select 1 + from programmable_private.dual_rpc_block_evidence as evidence + where evidence.block_evidence_id = + group_event.materialized_block_evidence_id + and evidence.observation_id = p_safe_head_observation_id + and evidence.epoch_id = header.epoch_id + and evidence.pointer_generation = + header.captured_pointer_generation + and evidence.block_number = group_event.block_number + and evidence.agreed_block_hash = group_event.block_hash + ) + or ( + header.release_id = 'classic-v3' + and group_event.materialized_event_type not in ( + 'CreatorFeesCheckpointed', 'BeneficiaryFeesClaimed', + 'PayoutWalletChanged', 'CtoRewardConfigurationActivated' + ) + ) + or ( + header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and group_event.materialized_event_type not in ( + 'BeneficiaryFeesClaimed', 'PayoutAddressUpdated' + ) + ) + or exists ( + select 1 + from programmable_private.chain_event_current_canonical as current + where current.logical_event_id = group_event.logical_event_id + and current.occurrence_id <> group_event.occurrence_id + ) + then + raise exception using + errcode = '23514', + message = 'reward block occurrence lacks exact canonical evidence'; + end if; + end loop; + + if exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and ( + vault.snapshot_kind <> 'exact_current' + or vault.chain_id <> header.chain_id + or vault.release_id <> header.release_id + or vault.model_id <> header.model_id + or vault.epoch_id <> header.epoch_id + or vault.pointer_generation <> + header.captured_pointer_generation + or vault.promoted_block_number <> target_block + or vault.promoted_block_hash <> p_target_block_hash + or vault.baseline_checkpoint_id <> + current_checkpoint.checkpoint_id + or vault.baseline_checkpoint_generation <> + current_checkpoint.checkpoint_generation + or vault.baseline_reorg_generation <> + current_checkpoint.reorg_generation + or vault.configuration_epoch is null + or vault.active_configuration_hash is null + or vault.total_creator_fees_received is null + or not (vault.last_source_occurrence_id = any(p_occurrence_ids)) + ) + ) + or ( + select pg_catalog.count(distinct vault) + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + ) <> vault_count + or exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and vault.last_source_occurrence_id is distinct from ( + select source.occurrence_id + from pg_catalog.unnest(p_occurrence_ids) + with ordinality as requested(occurrence_id, ordinal) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + where source.source_address = vault.vault + order by requested.ordinal desc + limit 1 + ) + ) + or exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and not (vault.current_allocation_fact_id = + any(p_allocation_fact_ids)) + ) + then + raise exception using + errcode = '23514', + message = 'reward block vault set is not checkpoint-exact'; + end if; + + for staged_vault in + select * + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + order by vault + loop + select * into baseline_vault + from programmable_private.current_reward_vault_projections_v1 as baseline + where baseline.reward_vault_projection_id = + staged_vault.baseline_reward_vault_projection_id + and baseline.chain_id = header.chain_id + and baseline.release_id = header.release_id + and baseline.model_id = header.model_id + and baseline.epoch_id = header.epoch_id + and baseline.pointer_generation = + header.captured_pointer_generation + and baseline.vault = staged_vault.vault + and baseline.pool_id = staged_vault.pool_id; + if not found + or baseline_vault.launch_projection_id <> + staged_vault.launch_projection_id + or baseline_vault.current_allocation_fact_id <> + staged_vault.current_allocation_fact_id + or not programmable_private.has_current_verified_reward_seed( + baseline_vault.projection_run_id, baseline_vault.vault + ) + or not exists ( + select 1 + from programmable_private.current_launch_projections_v1 as launch + where launch.launch_projection_id = + baseline_vault.launch_projection_id + and launch.chain_id = header.chain_id + and launch.release_id = header.release_id + and launch.model_id = header.model_id + and launch.epoch_id = header.epoch_id + and launch.pointer_generation = + header.captured_pointer_generation + and launch.reward_vault = baseline_vault.vault + and launch.pool_id = baseline_vault.pool_id + and launch.is_complete + ) + then + raise exception using + errcode = '23514', + message = 'reward block baseline is stale or incomplete'; + end if; + if not exists ( + select 1 + from pg_catalog.generate_subscripts( + p_allocation_fact_ids, 1 + ) as position(index) + join programmable_private.reward_allocation_current_verified + as seed + on seed.allocation_fact_id = p_allocation_fact_ids[position.index] + and seed.allocation_evidence_id = + p_allocation_evidence_ids[position.index] + and seed.vault = staged_vault.vault + where seed.allocation_fact_id = + staged_vault.current_allocation_fact_id + ) then + raise exception using + errcode = '23514', + message = 'reward block initial seed is not current verified'; + end if; + + vault_terminal_occurrence_id := staged_vault.last_source_occurrence_id; + select pg_catalog.count(*), + coalesce(pg_catalog.sum(allocation.share_bps), 0) + into allocation_count, total_share_bps + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + and allocation.allocation_fact_id = + staged_vault.current_allocation_fact_id + and allocation.chain_id = header.chain_id + and allocation.release_id = header.release_id + and allocation.model_id = header.model_id + and allocation.epoch_id = header.epoch_id + and allocation.pointer_generation = + header.captured_pointer_generation + and allocation.promoted_block_number = target_block + and allocation.promoted_block_hash = p_target_block_hash + and allocation.last_source_occurrence_id = + vault_terminal_occurrence_id + and allocation.configuration_epoch = + staged_vault.configuration_epoch + and allocation.effective_to_block is null; + if allocation_count < 1 + or allocation_count > ( + case when header.release_id = 'classic-v3' then 5 else 8 end + ) + or total_share_bps <> 10000 + or ( + select pg_catalog.count(distinct allocation.allocation_index) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + ) <> allocation_count + or ( + select pg_catalog.min(allocation.allocation_index) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + ) <> 0 + or ( + select pg_catalog.max(allocation.allocation_index) + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + ) <> allocation_count - 1 + then + raise exception using + errcode = '23514', + message = 'reward block allocation set is incomplete'; + end if; + + select pg_catalog.count(*), + coalesce(pg_catalog.sum( + balance.claimable_accrued + balance.claimed_total + ), 0) + into balance_count, total_balance_value + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = + header.captured_pointer_generation + and balance.vault = staged_vault.vault + and balance.promoted_block_number = target_block + and balance.promoted_block_hash = p_target_block_hash + and balance.last_source_occurrence_id = + vault_terminal_occurrence_id; + if balance_count < 1 + or total_balance_value <> + staged_vault.total_creator_fees_received + or ( + select pg_catalog.count(distinct balance.account) + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and balance.vault = staged_vault.vault + ) <> balance_count + or exists ( + select 1 + from programmable_private.current_account_reward_balances_v1 + as prior_balance + where prior_balance.chain_id = header.chain_id + and prior_balance.release_id = header.release_id + and prior_balance.model_id = header.model_id + and prior_balance.epoch_id = header.epoch_id + and prior_balance.pointer_generation = + header.captured_pointer_generation + and prior_balance.vault = staged_vault.vault + and not exists ( + select 1 + from programmable_private.account_reward_balances + as next_balance + where next_balance.projection_run_id = p_run_id + and next_balance.vault = staged_vault.vault + and next_balance.account = prior_balance.account + and next_balance.claimed_total >= + prior_balance.claimed_total + and next_balance.claimable_accrued + + next_balance.claimed_total >= + prior_balance.claimable_accrued + + prior_balance.claimed_total + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward block balance set is incomplete or nonmonotonic'; + end if; + + select coalesce( + baseline_vault.total_creator_fees_received, + ( + select pg_catalog.sum( + balance.claimable_accrued + balance.claimed_total + ) + from programmable_private.current_account_reward_balances_v1 + as balance + where balance.chain_id = header.chain_id + and balance.release_id = header.release_id + and balance.model_id = header.model_id + and balance.epoch_id = header.epoch_id + and balance.pointer_generation = + header.captured_pointer_generation + and balance.vault = staged_vault.vault + ), + 0 + ) into baseline_total; + if header.release_id = 'classic-v3' then + select + pg_catalog.count(*), + coalesce(pg_catalog.sum( + (materialization.decoded_payload ->> 'amount')::numeric + ), 0), + ( + pg_catalog.array_agg( + (materialization.decoded_payload ->> + 'totalCreatorFeesReceived')::numeric + order by source.block_global_log_index desc, + source.occurrence_id desc + ) + )[1] + into terminal_event_count, checkpoint_amount_total, + checkpoint_terminal_total + from programmable_private.chain_event_occurrence_materializations + as materialization + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = materialization.occurrence_id + where materialization.occurrence_id = any(p_occurrence_ids) + and source.source_address = staged_vault.vault + and materialization.event_type = 'CreatorFeesCheckpointed'; + if checkpoint_amount_total < 0 + or staged_vault.total_creator_fees_received < baseline_total + or ( + terminal_event_count = 0 + and staged_vault.total_creator_fees_received <> baseline_total + ) + or ( + terminal_event_count > 0 + and ( + baseline_total + checkpoint_amount_total <> + staged_vault.total_creator_fees_received + or checkpoint_terminal_total <> + staged_vault.total_creator_fees_received + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward block checkpoint totals do not reconcile'; + end if; + + perform programmable_private.assert_classic_reward_block_fold_v1( + p_run_id, staged_vault.vault, p_occurrence_ids + ); + elsif header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and exists ( + with active_allocations as ( + select + allocation.allocation_index, + allocation.beneficiary::bytea as account, + allocation.share_bps::numeric as share_bps, + pg_catalog.max(allocation.allocation_index) over () as last_index + from programmable_private.reward_allocation_projections + as allocation + where allocation.projection_run_id = p_run_id + and allocation.reward_vault_projection_id = + staged_vault.reward_vault_projection_id + and allocation.effective_to_block is null + ), non_last_total as ( + select coalesce(pg_catalog.sum( + case when allocation_index < last_index then + pg_catalog.div( + staged_vault.total_creator_fees_received * share_bps, 10000 + ) + else 0 end + ), 0) as amount + from active_allocations + ), entitlements as ( + select allocation.account, + case + when allocation.allocation_index = allocation.last_index + then staged_vault.total_creator_fees_received + - non_last_total.amount + else pg_catalog.div( + staged_vault.total_creator_fees_received + * allocation.share_bps, 10000 + ) + end as amount + from active_allocations as allocation + cross join non_last_total + ), block_claims as ( + select claim.beneficiary::bytea as account, + pg_catalog.sum(claim.amount)::numeric as amount + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + and claim.vault = staged_vault.vault + group by claim.beneficiary + ) + select 1 + from programmable_private.account_reward_balances as next_balance + left join entitlements as entitlement + on entitlement.account = next_balance.account + left join programmable_private.current_account_reward_balances_v1 + as prior_balance + on prior_balance.chain_id = header.chain_id + and prior_balance.release_id = header.release_id + and prior_balance.model_id = header.model_id + and prior_balance.epoch_id = header.epoch_id + and prior_balance.pointer_generation = + header.captured_pointer_generation + and prior_balance.vault = staged_vault.vault + and prior_balance.account = next_balance.account + left join block_claims as block_claim + on block_claim.account = next_balance.account + where next_balance.projection_run_id = p_run_id + and next_balance.vault = staged_vault.vault + and ( + entitlement.account is null + or next_balance.claimed_total <> + coalesce(prior_balance.claimed_total, 0) + + coalesce(block_claim.amount, 0) + or next_balance.claimable_accrued <> + entitlement.amount + - coalesce(prior_balance.claimed_total, 0) + - coalesce(block_claim.amount, 0) + ) + ) then + raise exception using + errcode = '23514', + message = 'Stock-Paired rewards do not match the exact block state'; + end if; + end loop; + + if exists ( + select 1 + from programmable_private.reward_allocation_projections as allocation + where allocation.projection_run_id = p_run_id + and not exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and vault.reward_vault_projection_id = + allocation.reward_vault_projection_id + ) + ) + or exists ( + select 1 + from programmable_private.account_reward_balances as balance + where balance.projection_run_id = p_run_id + and not exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and vault.vault = balance.vault + ) + ) + or exists ( + select 1 + from programmable_private.claim_projections as claim + where claim.projection_run_id = p_run_id + and not exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and vault.vault = claim.vault + ) + ) + or exists ( + select 1 + from programmable_private.payout_change_projections as payout + where payout.projection_run_id = p_run_id + and not exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.projection_run_id = p_run_id + and vault.vault = payout.vault + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward block contains cross-vault staged rows'; + end if; + + select pg_catalog.count(*) into claim_count + from programmable_private.claim_projections + where projection_run_id = p_run_id; + select pg_catalog.count(*) into claim_event_count + from programmable_private.chain_event_occurrence_materializations + as materialization + where materialization.occurrence_id = any(p_occurrence_ids) + and materialization.event_type = 'BeneficiaryFeesClaimed'; + if claim_count <> claim_event_count + or exists ( + select 1 + from programmable_private.claim_projections as claim + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = claim.source_occurrence_id + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = materialization.occurrence_id + join programmable_private.reward_vault_projections as vault + on vault.projection_run_id = p_run_id + and vault.vault = claim.vault + where claim.projection_run_id = p_run_id + and ( + claim.chain_id <> header.chain_id + or claim.release_id <> header.release_id + or claim.model_id <> header.model_id + or claim.epoch_id <> header.epoch_id + or claim.pointer_generation <> + header.captured_pointer_generation + or claim.claimant_kind <> 'beneficiary' + or claim.amount <= 0 + or claim.vault_total_received > + vault.total_creator_fees_received + or claim.promoted_block_number <> target_block + or claim.promoted_block_hash <> p_target_block_hash + or not (claim.source_occurrence_id = any(p_occurrence_ids)) + or source.source_address <> claim.vault + or materialization.event_type <> 'BeneficiaryFeesClaimed' + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'beneficiary', 20 + ) is distinct from claim.beneficiary + or (materialization.decoded_payload ->> 'amount')::numeric + is distinct from claim.amount + or ( + materialization.decoded_payload ->> + 'beneficiaryTotalClaimed' + )::numeric is distinct from claim.beneficiary_total_claimed + or (materialization.decoded_payload ->> + 'vaultTotalReceived')::numeric + is distinct from claim.vault_total_received + or ( + header.release_id = 'classic-v3' + and claim.recipient <> claim.beneficiary + ) + or ( + header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and ( + programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'payoutAddress', 20 + ) is distinct from claim.recipient + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'quoteAsset', 20 + ) is distinct from vault.quote_asset + ) + ) + ) + ) + or exists ( + with ordered_claims as ( + select + claim.claim_projection_id, + claim.vault, + claim.beneficiary, + claim.beneficiary_total_claimed, + coalesce(prior.claimed_total, 0) + pg_catalog.sum(claim.amount) + over ( + partition by claim.vault, claim.beneficiary + order by source.block_number, + source.block_global_log_index, + source.transaction_index, + source.receipt_log_ordinal, + claim.source_occurrence_id + rows between unbounded preceding and current row + ) as expected_total_claimed + from programmable_private.claim_projections as claim + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = claim.source_occurrence_id + left join programmable_private.current_account_reward_balances_v1 + as prior + on prior.chain_id = header.chain_id + and prior.release_id = header.release_id + and prior.model_id = header.model_id + and prior.epoch_id = header.epoch_id + and prior.pointer_generation = header.captured_pointer_generation + and prior.vault = claim.vault + and prior.account = claim.beneficiary + where claim.projection_run_id = p_run_id + ) + select 1 + from ordered_claims + where beneficiary_total_claimed <> expected_total_claimed + ) + then + raise exception using + errcode = '23514', + message = 'reward block claims do not reconcile'; + end if; + + select pg_catalog.count(*) into payout_count + from programmable_private.payout_change_projections + where projection_run_id = p_run_id; + select pg_catalog.count(*) into payout_event_count + from programmable_private.chain_event_occurrence_materializations + as materialization + where materialization.occurrence_id = any(p_occurrence_ids) + and materialization.event_type in ( + 'PayoutWalletChanged', 'PayoutAddressUpdated' + ); + if payout_count <> payout_event_count + or exists ( + select 1 + from programmable_private.payout_change_projections as payout + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = payout.source_occurrence_id + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = payout.source_occurrence_id + where payout.projection_run_id = p_run_id + and ( + payout.chain_id <> header.chain_id + or payout.release_id <> header.release_id + or payout.model_id <> header.model_id + or payout.epoch_id <> header.epoch_id + or payout.pointer_generation <> + header.captured_pointer_generation + or payout.promoted_block_number <> target_block + or payout.promoted_block_hash <> p_target_block_hash + or not (payout.source_occurrence_id = any(p_occurrence_ids)) + or payout.source_logical_event_id <> source.logical_event_id + or payout.source_occurrence_block_hash <> source.block_hash + or source.source_address <> payout.vault + or ( + header.release_id = 'classic-v3' + and ( + materialization.event_type <> 'PayoutWalletChanged' + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, + 'previousPayoutWallet', 20 + ) is distinct from payout.previous_payout_address + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, + 'newPayoutWallet', 20 + ) is distinct from payout.new_payout_address + or (materialization.decoded_payload ->> + 'configurationEpoch')::numeric is distinct from + payout.configuration_epoch::numeric + ) + ) + or ( + header.release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) + and ( + materialization.event_type <> 'PayoutAddressUpdated' + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, 'beneficiary', 20 + ) is distinct from payout.beneficiary + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, + 'previousPayoutAddress', 20 + ) is distinct from payout.previous_payout_address + or programmable_private.json_hex_bytes_v1( + materialization.decoded_payload, + 'newPayoutAddress', 20 + ) is distinct from payout.new_payout_address + or payout.configuration_epoch is not null + ) + ) + ) + ) + then + raise exception using + errcode = '23514', + message = 'reward block payout changes do not reconcile'; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'projection.promote.reward_block_group', + p_result_commitment, p_run_id, p_published_at + ); + select pg_catalog.array_agg( + pg_catalog.format('%s:%s', staged.row_kind, staged.row_id) + order by staged.row_kind, staged.row_id + ), pg_catalog.count(*) + into ordered_projection_rows, projection_row_count + from ( + select 'reward_vault'::text as row_kind, + reward_vault_projection_id as row_id + from programmable_private.reward_vault_projections + where projection_run_id = p_run_id + union all + select 'reward_allocation', reward_allocation_projection_id + from programmable_private.reward_allocation_projections + where projection_run_id = p_run_id + union all + select 'account_reward_balance', account_reward_balance_id + from programmable_private.account_reward_balances + where projection_run_id = p_run_id + union all + select 'claim', claim_projection_id + from programmable_private.claim_projections + where projection_run_id = p_run_id + union all + select 'payout_change', payout_change_projection_id + from programmable_private.payout_change_projections + where projection_run_id = p_run_id + ) as staged; + select pg_catalog.count(*) into allocation_row_count + from programmable_private.reward_allocation_projections + where projection_run_id = p_run_id; + select pg_catalog.count(*) into balance_row_count + from programmable_private.account_reward_balances + where projection_run_id = p_run_id; + if projection_row_count <> + vault_count + allocation_row_count + balance_row_count + + claim_count + payout_count + then + raise exception using + errcode = '23514', message = 'reward block manifest is incomplete'; + end if; + insert into programmable_private.projection_fold_manifests ( + run_id, epoch_id, pointer_generation, target_block_number, + target_block_hash, ordered_occurrence_ids, + ordered_allocation_fact_ids, ordered_allocation_evidence_ids, + ordered_candidate_disposition_ids, ordered_route_keys, + cursor_block_global_log_index, cursor_candidate_id, + ordered_projection_rows, projection_row_count, + result_commitment, created_at, audit_id + ) values ( + p_run_id, header.epoch_id, header.captured_pointer_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_occurrence_ids, p_allocation_fact_ids, p_allocation_evidence_ids, + p_candidate_disposition_ids, p_route_keys, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + ordered_projection_rows, projection_row_count, + p_result_commitment::programmable_private.bytes32_value, + p_published_at, audit_id + ); + + for group_event in + select source.*, materialization.block_evidence_id + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id + join programmable_private.chain_event_occurrence_materializations + as materialization + on materialization.occurrence_id = source.occurrence_id + and materialization.epoch_id = header.epoch_id + and materialization.pointer_generation = + header.captured_pointer_generation + order by source.block_number, source.block_global_log_index, + source.transaction_index, source.receipt_log_ordinal, + source.occurrence_id + loop + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.chain_event_occurrence_status_history ( + status_history_id, occurrence_id, logical_event_id, block_hash, + status, safe_head_observation_id, block_evidence_id, + decision_run_id, decision_commitment, decided_at, audit_id + ) values ( + status_id, group_event.occurrence_id, group_event.logical_event_id, + group_event.block_hash, 'canonical', p_safe_head_observation_id, + group_event.block_evidence_id, p_run_id, + p_result_commitment, p_published_at, audit_id + ); + insert into programmable_private.chain_event_current_canonical ( + logical_event_id, occurrence_id, block_hash, status_history_id, + selected_by_run_id, selected_at + ) values ( + group_event.logical_event_id, group_event.occurrence_id, + group_event.block_hash, status_id, p_run_id, p_published_at + ) + on conflict (logical_event_id) do update + set status_history_id = excluded.status_history_id, + selected_by_run_id = excluded.selected_by_run_id, + selected_at = excluded.selected_at + where programmable_private.chain_event_current_canonical.occurrence_id + = excluded.occurrence_id; + if not found then + raise exception using + errcode = '23505', message = 'canonical pointer conflict'; + end if; + end loop; + + insert into programmable_private.run_lifecycle_outcomes ( + outcome_id, run_id, status, result_commitment, caller_role, + finished_at, audit_id + ) values ( + p_outcome_id, p_run_id, 'succeeded', + p_result_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_published_at, audit_id + ); + insert into programmable_private.projector_checkpoints ( + checkpoint_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, lease_generation, + checkpoint_generation, reorg_generation, block_number, block_hash, + cursor_block_global_log_index, cursor_candidate_id, + safe_head_observation_id, target_block_evidence_id, run_id, + terminal_outcome_id, created_at + ) values ( + p_checkpoint_id, header.chain_id, header.release_id, header.model_id, + header.source_group, + p_projector_version::programmable_private.projector_identifier, + header.epoch_id, header.captured_pointer_generation, + p_lease_generation, p_next_checkpoint_generation, + p_reorg_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + p_safe_head_observation_id, p_target_block_evidence_id, + p_run_id, p_outcome_id, p_published_at + ); + update programmable_private.projector_checkpoint_current + set checkpoint_id = p_checkpoint_id, + checkpoint_generation = p_next_checkpoint_generation, + reorg_generation = p_reorg_generation, + changed_at = p_published_at + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + and checkpoint_generation = p_expected_checkpoint_generation + and reorg_generation = p_reorg_generation; + if not found then + raise exception using + errcode = '40001', message = 'checkpoint CAS lost'; + end if; + insert into programmable_private.projection_publications ( + publication_id, run_id, epoch_id, pointer_generation, checkpoint_id, + terminal_outcome_id, target_block_number, target_block_hash, + published_at, audit_id + ) values ( + p_publication_id, p_run_id, header.epoch_id, + header.captured_pointer_generation, p_checkpoint_id, p_outcome_id, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_published_at, audit_id + ); + foreach selected_route_key in array p_route_keys loop + route_history_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, + model_id, source_group, epoch_id, pointer_generation, status, + route_mode, checkpoint_id, reason_commitment, changed_by_run_id, + changed_at, audit_id + ) values ( + route_history_id, + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, 'eligible', 'indexed', + p_checkpoint_id, + p_result_commitment::programmable_private.bytes32_value, + p_run_id, p_published_at, audit_id + ); + insert into programmable_private.route_eligibility_current ( + route_key, chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, status, route_mode, checkpoint_id, history_id, + changed_at + ) values ( + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, 'eligible', 'indexed', + p_checkpoint_id, route_history_id, p_published_at + ) + on conflict ( + route_key, chain_id, release_id, model_id, source_group + ) do update + set epoch_id = excluded.epoch_id, + pointer_generation = excluded.pointer_generation, + status = excluded.status, + route_mode = excluded.route_mode, + checkpoint_id = excluded.checkpoint_id, + history_id = excluded.history_id, + changed_at = excluded.changed_at + where programmable_private.route_eligibility_current + .pointer_generation <= excluded.pointer_generation; + if not found then + raise exception using + errcode = '40001', message = 'stale route eligibility generation'; + end if; + end loop; + perform programmable_private.bind_projection_publication_provider_evidence_v1( + p_provider_binding_id, p_publication_id, p_run_id, + 'exact_incremental', p_execution_evidence_id, + p_reward_snapshot_evidence_ids, p_provider_binding_commitment, + p_published_at + ); + return p_publication_id; +end +$function$; + +create function programmable_private.promote_projection_cursor_only_v1( + p_publication_id uuid, + p_checkpoint_id uuid, + p_outcome_id uuid, + p_run_id uuid, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_expected_checkpoint_generation bigint, + p_next_checkpoint_generation bigint, + p_reorg_generation bigint, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea, + p_cursor_block_global_log_index numeric, + p_cursor_candidate_id text, + p_occurrence_ids uuid[], + p_allocation_fact_ids uuid[], + p_allocation_evidence_ids uuid[], + p_candidate_disposition_ids uuid[], + p_route_keys text[], + p_result_commitment bytea, + p_execution_evidence_id uuid, + p_reward_snapshot_evidence_ids uuid[], + p_provider_binding_id uuid, + p_provider_binding_commitment bytea, + p_published_at timestamptz +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + observation programmable_private.safe_head_observations%rowtype; + target_evidence programmable_private.dual_rpc_block_evidence%rowtype; + execution + programmable_private.projection_provider_execution_evidence%rowtype; + current_checkpoint + programmable_private.projector_checkpoint_current%rowtype; + previous_checkpoint programmable_private.projector_checkpoints%rowtype; + target_block bigint; + cursor_log_index bigint; + required_disposition_ids uuid[]; + ordered_disposition_ids uuid[]; + ordered_route_keys text[]; + ordered_projection_rows text[]; + audit_id uuid; + route_history_id uuid; + selected_route_key text; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind = 'projection' + for update; + if not found + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'cursor-only projection run is not open'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation + ); + if not exists ( + select 1 + from programmable_private.projector_lease_current as lease + where lease.chain_id = header.chain_id + and lease.release_id = header.release_id + and lease.model_id = header.model_id + and lease.source_group = header.source_group + and lease.projector_version = p_projector_version + and lease.epoch_id = header.epoch_id + and lease.pointer_generation = header.captured_pointer_generation + and lease.lease_generation = p_lease_generation + and lease.lease_token_hash = p_lease_token_hash + and lease.expires_at >= p_published_at + ) then + raise exception using + errcode = '40001', message = 'stale projector lease'; + end if; + if p_publication_id is null + or p_checkpoint_id is null + or p_outcome_id is null + or p_provider_binding_id is null + or p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number > 9223372036854775807 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or p_cursor_block_global_log_index < 0 + or p_cursor_block_global_log_index <> + pg_catalog.trunc(p_cursor_block_global_log_index) + or p_cursor_block_global_log_index > 4294967295 + or p_cursor_candidate_id is null + or pg_catalog.octet_length(p_result_commitment) <> 32 + or pg_catalog.octet_length(p_provider_binding_commitment) <> 32 + or p_provider_binding_commitment = + pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex') + or p_next_checkpoint_generation <> + p_expected_checkpoint_generation + 1 + or coalesce(pg_catalog.cardinality(p_occurrence_ids), 0) <> 0 + or coalesce(pg_catalog.cardinality(p_allocation_fact_ids), 0) <> 0 + or coalesce(pg_catalog.cardinality(p_allocation_evidence_ids), 0) <> 0 + or coalesce(pg_catalog.cardinality(p_reward_snapshot_evidence_ids), 0) <> 0 + or coalesce(pg_catalog.cardinality(p_candidate_disposition_ids), 0) + not between 1 and 4096 + or coalesce(pg_catalog.cardinality(p_route_keys), 0) + not between 1 and 32 + then + raise exception using + errcode = '22023', message = 'invalid cursor-only promotion request'; + end if; + select pg_catalog.array_agg(item order by item) + into ordered_disposition_ids + from ( + select distinct item + from pg_catalog.unnest(p_candidate_disposition_ids) as item + ) as unique_items; + select pg_catalog.array_agg(item order by item) + into ordered_route_keys + from ( + select distinct item + from pg_catalog.unnest(p_route_keys) as item + ) as unique_items; + if p_candidate_disposition_ids is distinct from ordered_disposition_ids + or p_route_keys is distinct from ordered_route_keys + or exists ( + select 1 from pg_catalog.unnest(p_candidate_disposition_ids) as item + where item is null + ) + or exists ( + select 1 from pg_catalog.unnest(p_route_keys) as item + where item is null + ) + then + raise exception using + errcode = '22023', + message = 'cursor-only manifests are not canonical'; + end if; + if exists ( + select 1 + from ( + select projection_run_id from programmable_private.launch_projections + union all + select projection_run_id from programmable_private.pool_projections + union all + select projection_run_id + from programmable_private.pool_fee_configurations + union all + select projection_run_id from programmable_private.fee_accrual_facts + union all + select projection_run_id from programmable_private.pool_fee_totals + union all + select projection_run_id + from programmable_private.reward_vault_projections + union all + select projection_run_id + from programmable_private.reward_allocation_projections + union all + select projection_run_id + from programmable_private.account_reward_balances + union all + select projection_run_id from programmable_private.claim_projections + union all + select projection_run_id + from programmable_private.payout_change_projections + union all + select projection_run_id + from programmable_private.initial_buy_custody_projections + union all + select projection_run_id + from programmable_private.initial_buy_vesting_projections + ) as staged + where staged.projection_run_id = p_run_id + ) then + raise exception using + errcode = '23514', + message = 'cursor-only promotion contains staged projection rows'; + end if; + + target_block := p_target_block_number::bigint; + cursor_log_index := p_cursor_block_global_log_index::bigint; + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and pointer_generation = header.captured_pointer_generation; + if not found or target_block > observation.safe_block_number then + raise exception using + errcode = '23514', message = 'target is outside accepted safe head'; + end if; + select * into target_evidence + from programmable_private.dual_rpc_block_evidence + where block_evidence_id = p_target_block_evidence_id + and observation_id = p_safe_head_observation_id + and epoch_id = header.epoch_id + and chain_id = header.chain_id + and pointer_generation = header.captured_pointer_generation + and block_number = target_block + and agreed_block_hash = p_target_block_hash; + if not found then + raise exception using + errcode = '23514', message = 'target block evidence changed'; + end if; + select * into execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + and run_id = p_run_id; + if not found + or execution.candidate_batch_size <> + pg_catalog.cardinality(p_candidate_disposition_ids) + then + raise exception using + errcode = '23514', message = 'cursor-only execution evidence changed'; + end if; + + select * into current_checkpoint + from programmable_private.projector_checkpoint_current + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + for update; + if found then + if current_checkpoint.checkpoint_generation <> + p_expected_checkpoint_generation + or current_checkpoint.reorg_generation <> p_reorg_generation + then + raise exception using + errcode = '40001', message = 'cursor-only checkpoint CAS lost'; + end if; + select * into previous_checkpoint + from programmable_private.projector_checkpoints + where checkpoint_id = current_checkpoint.checkpoint_id; + if not found + or previous_checkpoint.epoch_id <> header.epoch_id + or previous_checkpoint.pointer_generation <> + header.captured_pointer_generation + or (target_block, cursor_log_index, p_cursor_candidate_id) <= ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + then + raise exception using + errcode = '23514', message = 'cursor-only cursor did not advance'; + end if; + elsif p_expected_checkpoint_generation <> 0 + or p_reorg_generation <> 0 + then + raise exception using + errcode = '40001', message = 'cursor-only checkpoint CAS lost'; + end if; + if not exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = p_cursor_candidate_id + and candidate.chain_id = header.chain_id + and candidate.provider_deployment_id = + execution.envio_provider_deployment_id + and candidate.block_number = target_block + and candidate.block_hash = p_target_block_hash + and candidate.block_global_log_index = cursor_log_index + ) or exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + where candidate.chain_id = header.chain_id + and candidate.provider_deployment_id = + execution.envio_provider_deployment_id + and candidate.block_number = target_block + and ( + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > (cursor_log_index, p_cursor_candidate_id) + ) then + raise exception using + errcode = '23514', + message = 'cursor-only cursor is not the verified block terminal'; + end if; + if exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + left join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + where candidate.chain_id = header.chain_id + and candidate.provider_deployment_id = + execution.envio_provider_deployment_id + and ( + previous_checkpoint.checkpoint_id is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id) + and coalesce(status.status::text, 'pending') not in ( + 'resolved', 'ignored', 'quarantined' + ) + ) then + raise exception using + errcode = '23514', + message = 'cursor-only cursor cannot pass a pending candidate'; + end if; + select pg_catalog.array_agg(status.decision_id order by status.decision_id) + into required_disposition_ids + from programmable_private.envio_candidate_inbox as candidate + join programmable_private.envio_candidate_status_current as status + on status.candidate_id = candidate.candidate_id + and status.epoch_id = header.epoch_id + and status.pointer_generation = header.captured_pointer_generation + and status.status in ('resolved', 'ignored', 'quarantined') + where candidate.chain_id = header.chain_id + and candidate.provider_deployment_id = + execution.envio_provider_deployment_id + and ( + previous_checkpoint.checkpoint_id is null + or ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + previous_checkpoint.block_number::bigint, + previous_checkpoint.cursor_block_global_log_index::bigint, + previous_checkpoint.cursor_candidate_id::text + ) + ) + and ( + candidate.block_number::bigint, + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) <= (target_block, cursor_log_index, p_cursor_candidate_id); + if p_candidate_disposition_ids is distinct from + coalesce(required_disposition_ids, array[]::uuid[]) + then + raise exception using + errcode = '23514', + message = 'cursor-only disposition manifest is incomplete'; + end if; + + audit_id := programmable_private.append_mutation_audit( + 'projection.promote.cursor_only', p_result_commitment, + p_run_id, p_published_at + ); + select pg_catalog.array_agg( + 'candidate_disposition:' || item::text order by item + ) into ordered_projection_rows + from pg_catalog.unnest(p_candidate_disposition_ids) as item; + insert into programmable_private.projection_fold_manifests ( + run_id, epoch_id, pointer_generation, target_block_number, + target_block_hash, ordered_occurrence_ids, + ordered_allocation_fact_ids, ordered_allocation_evidence_ids, + ordered_candidate_disposition_ids, ordered_route_keys, + cursor_block_global_log_index, cursor_candidate_id, + ordered_projection_rows, projection_row_count, + result_commitment, created_at, audit_id + ) values ( + p_run_id, header.epoch_id, header.captured_pointer_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + array[]::uuid[], array[]::uuid[], array[]::uuid[], + p_candidate_disposition_ids, p_route_keys, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + ordered_projection_rows, + pg_catalog.cardinality(ordered_projection_rows), + p_result_commitment::programmable_private.bytes32_value, + p_published_at, audit_id + ); + insert into programmable_private.run_lifecycle_outcomes ( + outcome_id, run_id, status, result_commitment, caller_role, + finished_at, audit_id + ) values ( + p_outcome_id, p_run_id, 'succeeded', + p_result_commitment::programmable_private.bytes32_value, + programmable_private.caller_role_name(), p_published_at, audit_id + ); + insert into programmable_private.projector_checkpoints ( + checkpoint_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, lease_generation, + checkpoint_generation, reorg_generation, block_number, block_hash, + cursor_block_global_log_index, cursor_candidate_id, + safe_head_observation_id, target_block_evidence_id, run_id, + terminal_outcome_id, created_at + ) values ( + p_checkpoint_id, header.chain_id, header.release_id, header.model_id, + header.source_group, + p_projector_version::programmable_private.projector_identifier, + header.epoch_id, header.captured_pointer_generation, + p_lease_generation, p_next_checkpoint_generation, + p_reorg_generation, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + cursor_log_index::programmable_private.block_log_index_value, + p_cursor_candidate_id::programmable_private.envio_candidate_identifier, + p_safe_head_observation_id, p_target_block_evidence_id, + p_run_id, p_outcome_id, p_published_at + ); + if current_checkpoint.checkpoint_id is null then + insert into programmable_private.projector_checkpoint_current ( + chain_id, release_id, model_id, source_group, projector_version, + checkpoint_id, checkpoint_generation, reorg_generation, changed_at + ) values ( + header.chain_id, header.release_id, header.model_id, + header.source_group, + p_projector_version::programmable_private.projector_identifier, + p_checkpoint_id, p_next_checkpoint_generation, + p_reorg_generation, p_published_at + ); + else + update programmable_private.projector_checkpoint_current + set checkpoint_id = p_checkpoint_id, + checkpoint_generation = p_next_checkpoint_generation, + reorg_generation = p_reorg_generation, + changed_at = p_published_at + where chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and projector_version = p_projector_version + and checkpoint_generation = p_expected_checkpoint_generation + and reorg_generation = p_reorg_generation; + if not found then + raise exception using + errcode = '40001', message = 'cursor-only checkpoint CAS lost'; + end if; + end if; + insert into programmable_private.projection_publications ( + publication_id, run_id, epoch_id, pointer_generation, checkpoint_id, + terminal_outcome_id, target_block_number, target_block_hash, + published_at, audit_id + ) values ( + p_publication_id, p_run_id, header.epoch_id, + header.captured_pointer_generation, p_checkpoint_id, p_outcome_id, + target_block::programmable_private.block_number_value, + p_target_block_hash::programmable_private.bytes32_value, + p_published_at, audit_id + ); + foreach selected_route_key in array p_route_keys loop + route_history_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, + model_id, source_group, epoch_id, pointer_generation, status, + route_mode, checkpoint_id, reason_commitment, changed_by_run_id, + changed_at, audit_id + ) values ( + route_history_id, + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, 'eligible', 'indexed', + p_checkpoint_id, + p_result_commitment::programmable_private.bytes32_value, + p_run_id, p_published_at, audit_id + ); + insert into programmable_private.route_eligibility_current ( + route_key, chain_id, release_id, model_id, source_group, epoch_id, + pointer_generation, status, route_mode, checkpoint_id, history_id, + changed_at + ) values ( + selected_route_key::programmable_private.source_identifier, + header.chain_id, header.release_id, header.model_id, + header.source_group, header.epoch_id, + header.captured_pointer_generation, 'eligible', 'indexed', + p_checkpoint_id, route_history_id, p_published_at + ) + on conflict ( + route_key, chain_id, release_id, model_id, source_group + ) do update + set epoch_id = excluded.epoch_id, + pointer_generation = excluded.pointer_generation, + status = excluded.status, + route_mode = excluded.route_mode, + checkpoint_id = excluded.checkpoint_id, + history_id = excluded.history_id, + changed_at = excluded.changed_at + where programmable_private.route_eligibility_current + .pointer_generation <= excluded.pointer_generation; + if not found then + raise exception using + errcode = '40001', message = 'stale route eligibility generation'; + end if; + end loop; + perform programmable_private.bind_projection_publication_provider_evidence_v1( + p_provider_binding_id, p_publication_id, p_run_id, + 'exact_incremental', p_execution_evidence_id, + array[]::uuid[], p_provider_binding_commitment, + p_published_at + ); + return p_publication_id; +end +$function$; + +create function programmable_private.promote_projection_run_v3( + p_promotion_mode text, + p_publication_id uuid, + p_checkpoint_id uuid, + p_outcome_id uuid, + p_run_id uuid, + p_projector_version text, + p_lease_generation bigint, + p_lease_token_hash bytea, + p_expected_checkpoint_generation bigint, + p_next_checkpoint_generation bigint, + p_reorg_generation bigint, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea, + p_cursor_block_global_log_index numeric, + p_cursor_candidate_id text, + p_occurrence_ids uuid[], + p_allocation_fact_ids uuid[], + p_allocation_evidence_ids uuid[], + p_candidate_disposition_ids uuid[], + p_route_keys text[], + p_result_commitment bytea, + p_execution_evidence_id uuid, + p_reward_snapshot_evidence_ids uuid[], + p_provider_binding_id uuid, + p_provider_binding_commitment bytea, + p_published_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + publication_id uuid; + execution + programmable_private.projection_provider_execution_evidence%rowtype; + has_launch_rows boolean; + has_reward_evidence boolean; + chain_ordered_occurrence_ids uuid[]; + internal_occurrence_ids uuid[]; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_promotion_mode <> 'exact_incremental' + or p_target_block_number is null + or p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or p_target_block_number > 9223372036854775807 + or p_cursor_block_global_log_index is null + or p_cursor_block_global_log_index < 0 + or p_cursor_block_global_log_index <> + pg_catalog.trunc(p_cursor_block_global_log_index) + or p_cursor_block_global_log_index > 4294967295 + or p_cursor_candidate_id is null + then + raise exception using + errcode = '22023', message = 'invalid promotion target block'; + end if; + perform programmable_private.assert_projection_provider_evidence_v1( + p_promotion_mode, p_run_id, p_safe_head_observation_id, + p_target_block_evidence_id, p_target_block_number::bigint, + p_target_block_hash, p_execution_evidence_id, + p_reward_snapshot_evidence_ids + ); + select * into execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_execution_evidence_id + and run_id = p_run_id; + if execution.candidate_batch_size <> coalesce( + pg_catalog.cardinality(p_candidate_disposition_ids), 0 + ) + then + raise exception using + errcode = '23514', + message = 'promotion candidate count changed'; + end if; + if not exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = p_cursor_candidate_id + and candidate.chain_id = execution.chain_id + and candidate.provider_deployment_id = + execution.envio_provider_deployment_id + and candidate.block_number = p_target_block_number::bigint + and candidate.block_hash = p_target_block_hash + and candidate.block_global_log_index = + p_cursor_block_global_log_index::bigint + ) + or exists ( + select 1 + from programmable_private.envio_candidate_inbox as candidate + where candidate.chain_id = execution.chain_id + and candidate.provider_deployment_id = + execution.envio_provider_deployment_id + and candidate.block_number = p_target_block_number::bigint + and ( + candidate.block_global_log_index::bigint, + candidate.candidate_id::text + ) > ( + p_cursor_block_global_log_index::bigint, + p_cursor_candidate_id + ) + ) + then + raise exception using + errcode = '23514', + message = + 'promotion cursor is not the exact configured-provider block terminal'; + end if; + + has_launch_rows := exists ( + select 1 + from programmable_private.launch_projections + where projection_run_id = p_run_id + ); + has_reward_evidence := coalesce( + pg_catalog.cardinality(p_reward_snapshot_evidence_ids), 0 + ) > 0; + if has_reward_evidence then + select pg_catalog.array_agg( + source.occurrence_id + order by source.block_number, source.block_global_log_index, + source.transaction_index, source.receipt_log_ordinal, + source.occurrence_id + ) into chain_ordered_occurrence_ids + from pg_catalog.unnest(p_occurrence_ids) as requested(occurrence_id) + join programmable_private.chain_event_occurrences as source + on source.occurrence_id = requested.occurrence_id; + if p_occurrence_ids is distinct from chain_ordered_occurrence_ids then + raise exception using + errcode = '23514', + message = 'reward-bearing promotion events are not in chain order'; + end if; + end if; + + if has_reward_evidence and not has_launch_rows then + return programmable_private.promote_reward_block_group_v1( + p_publication_id, p_checkpoint_id, p_outcome_id, p_run_id, + p_projector_version, p_lease_generation, p_lease_token_hash, + p_expected_checkpoint_generation, p_next_checkpoint_generation, + p_reorg_generation, p_safe_head_observation_id, + p_target_block_evidence_id, p_target_block_number, + p_target_block_hash, p_cursor_block_global_log_index, + p_cursor_candidate_id, p_occurrence_ids, p_allocation_fact_ids, + p_allocation_evidence_ids, p_candidate_disposition_ids, + p_route_keys, p_result_commitment, p_execution_evidence_id, + p_reward_snapshot_evidence_ids, p_provider_binding_id, + p_provider_binding_commitment, p_published_at + ); + end if; + + if not has_launch_rows then + return programmable_private.promote_projection_cursor_only_v1( + p_publication_id, p_checkpoint_id, p_outcome_id, p_run_id, + p_projector_version, p_lease_generation, p_lease_token_hash, + p_expected_checkpoint_generation, p_next_checkpoint_generation, + p_reorg_generation, p_safe_head_observation_id, + p_target_block_evidence_id, p_target_block_number, + p_target_block_hash, p_cursor_block_global_log_index, + p_cursor_candidate_id, p_occurrence_ids, p_allocation_fact_ids, + p_allocation_evidence_ids, p_candidate_disposition_ids, + p_route_keys, p_result_commitment, p_execution_evidence_id, + p_reward_snapshot_evidence_ids, p_provider_binding_id, + p_provider_binding_commitment, p_published_at + ); + end if; + + select pg_catalog.array_agg(item order by item) + into internal_occurrence_ids + from ( + select distinct item + from pg_catalog.unnest(p_occurrence_ids) as item + ) as unique_items; + publication_id := programmable_private.promote_projection_run_v2( + 'full_launch', p_publication_id, p_checkpoint_id, + p_outcome_id, p_run_id, p_projector_version, + p_lease_generation, p_lease_token_hash, + p_expected_checkpoint_generation, p_next_checkpoint_generation, + p_reorg_generation, p_safe_head_observation_id, + p_target_block_evidence_id, p_target_block_number, + p_target_block_hash, p_cursor_block_global_log_index, + p_cursor_candidate_id, + coalesce(internal_occurrence_ids, array[]::uuid[]), + p_allocation_fact_ids, p_allocation_evidence_ids, + p_candidate_disposition_ids, p_route_keys, + p_result_commitment, p_published_at + ); + perform programmable_private.bind_projection_publication_provider_evidence_v1( + p_provider_binding_id, publication_id, p_run_id, + 'exact_incremental', p_execution_evidence_id, + p_reward_snapshot_evidence_ids, p_provider_binding_commitment, + p_published_at + ); + perform programmable_private.consume_matching_provisional_sources_v1( + p_run_id, publication_id, p_execution_evidence_id, + p_target_block_evidence_id, p_occurrence_ids, p_published_at + ); + return publication_id; +end +$function$; + +comment on function programmable_private.promote_projection_run_v3( + text, uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, uuid, + uuid[], uuid, bytea, timestamptz +) is + 'The only projector promotion entrypoint. It atomically binds immutable configured-provider evidence and supports one complete multi-transaction, multi-vault reward block group.'; + +revoke all on table + programmable_private.projection_provider_execution_evidence, + programmable_private.reward_snapshot_provider_evidence, + programmable_private.projection_publication_provider_bindings, + programmable_private.projection_publication_reward_evidence, + programmable_private.provisional_dynamic_parent_pages, + programmable_private.provisional_dynamic_source_lineages, + programmable_private.provisional_dynamic_parent_consumptions +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +revoke all on function + programmable_private.projection_execution_trace_preimage_v1(jsonb), + programmable_private.projection_execution_trace_commitment_v1(jsonb), + programmable_private.reward_snapshot_folded_preimage_v1(uuid, bytea), + programmable_private.reward_snapshot_folded_commitment_v1(uuid, bytea), + programmable_private.get_staged_reward_folded_commitment_v1(uuid, bytea), + programmable_private.projection_provider_binding_preimage_v1( + uuid, uuid, text, uuid, uuid[], timestamptz + ), + programmable_private.projection_provider_binding_commitment_v1( + uuid, uuid, text, uuid, uuid[], timestamptz + ), + programmable_private.projection_execution_evidence_preimage_v1( + bigint, text, text, text, uuid, bigint, uuid, uuid, uuid, + text, text, text, text, bytea, bytea, bytea, bytea, + integer, integer, integer, integer, integer, integer, bytea + ), + programmable_private.reward_snapshot_evidence_preimage_v1( + bigint, text, text, text, uuid, bigint, uuid, uuid, uuid, + bytea, text, bigint, bytea, uuid, uuid, bytea, bytea, + integer, integer, bytea[], integer[], bytea[], bytea[], + integer[], integer[], bytea, bytea + ), + programmable_private.assert_reward_verification_chunk_manifest_v1( + bytea[], integer[], bytea[], bytea[], integer[], integer[], + integer, integer + ), + programmable_private.validate_projection_execution_trace_v1( + jsonb, uuid, uuid + ), + programmable_private.validate_reward_snapshot_execution_trace_v1( + jsonb, uuid, uuid, integer, integer + ), + programmable_private.assert_classic_reward_block_fold_v1( + uuid, bytea, uuid[] + ), + programmable_private.assert_stock_reward_block_fold_v1( + uuid, bytea, uuid[] + ), + programmable_private.stage_current_reward_snapshot_v2( + uuid, bytea, bytea, uuid, bigint, bytea, numeric, + integer[], bytea[], bytea[], numeric[], bytea[], bytea[], + numeric[], numeric[], uuid, uuid[], numeric, bytea, timestamptz + ), + programmable_private.assert_projection_provider_evidence_v1( + text, uuid, uuid, uuid, bigint, bytea, uuid, uuid[] + ), + programmable_private.bind_projection_publication_provider_evidence_v1( + uuid, uuid, uuid, text, uuid, uuid[], bytea, timestamptz + ), + programmable_private.promote_reward_block_group_v1( + uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, uuid, + uuid[], uuid, bytea, timestamptz + ), + programmable_private.promote_projection_cursor_only_v1( + uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, uuid, + uuid[], uuid, bytea, timestamptz + ), + programmable_private.append_projection_provider_execution_evidence_v1( + uuid, uuid, uuid, uuid[], jsonb, bytea, smallint, + bytea, bytea, timestamptz + ), + programmable_private.append_reward_snapshot_provider_evidence_v1( + uuid, uuid, uuid, uuid, bytea, text, text, numeric, bytea, + bytea, bytea, integer, integer, bytea[], integer[], bytea[], + bytea[], integer[], integer[], bytea, jsonb, bytea, smallint, + bytea, bytea, timestamptz + ), + programmable_private.stage_verified_dynamic_parents_v2( + uuid, uuid, text, text, text, text, uuid, bigint, bigint, bigint, + bytea, uuid, text, uuid, uuid, uuid, uuid, numeric, bytea, bytea, + bytea[], bytea[], jsonb, bytea, jsonb, jsonb, timestamptz + ), + programmable_private.get_current_provisional_dynamic_sources_v1(text), + programmable_private.consume_matching_provisional_sources_v1( + uuid, uuid, uuid, uuid, uuid[], timestamptz + ), + programmable_private.promote_projection_run_v3( + text, uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, uuid, + uuid[], uuid, bytea, timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +grant execute on function + programmable_private.projection_execution_evidence_preimage_v1( + bigint, text, text, text, uuid, bigint, uuid, uuid, uuid, + text, text, text, text, bytea, bytea, bytea, bytea, + integer, integer, integer, integer, integer, integer, bytea + ), + programmable_private.reward_snapshot_evidence_preimage_v1( + bigint, text, text, text, uuid, bigint, uuid, uuid, uuid, + bytea, text, bigint, bytea, uuid, uuid, bytea, bytea, + integer, integer, bytea[], integer[], bytea[], bytea[], + integer[], integer[], bytea, bytea + ), + programmable_private.append_projection_provider_execution_evidence_v1( + uuid, uuid, uuid, uuid[], jsonb, bytea, smallint, + bytea, bytea, timestamptz + ), + programmable_private.append_reward_snapshot_provider_evidence_v1( + uuid, uuid, uuid, uuid, bytea, text, text, numeric, bytea, + bytea, bytea, integer, integer, bytea[], integer[], bytea[], + bytea[], integer[], integer[], bytea, jsonb, bytea, smallint, + bytea, bytea, timestamptz + ), + programmable_private.get_staged_reward_folded_commitment_v1(uuid, bytea), + programmable_private.stage_current_reward_snapshot_v2( + uuid, bytea, bytea, uuid, bigint, bytea, numeric, + integer[], bytea[], bytea[], numeric[], bytea[], bytea[], + numeric[], numeric[], uuid, uuid[], numeric, bytea, timestamptz + ), + programmable_private.stage_verified_dynamic_parents_v2( + uuid, uuid, text, text, text, text, uuid, bigint, bigint, bigint, + bytea, uuid, text, uuid, uuid, uuid, uuid, numeric, bytea, bytea, + bytea[], bytea[], jsonb, bytea, jsonb, jsonb, timestamptz + ), + programmable_private.get_current_provisional_dynamic_sources_v1(text), + programmable_private.promote_projection_run_v3( + text, uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, uuid, + uuid[], uuid, bytea, timestamptz + ) +to programmable_projector; + +-- Older promotion functions cannot bypass provider-evidence binding. +revoke execute on function programmable_private.promote_projection_run( + uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, timestamptz +) from programmable_projector; +revoke execute on function programmable_private.promote_projection_run_v2( + text, uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, timestamptz +) from programmable_projector; + +reset role; diff --git a/supabase/migrations/20260731225000_reconciler_route_corpus.sql b/supabase/migrations/20260731225000_reconciler_route_corpus.sql new file mode 100644 index 00000000..f2dd83d7 --- /dev/null +++ b/supabase/migrations/20260731225000_reconciler_route_corpus.sql @@ -0,0 +1,1092 @@ +-- Bounded, exact-checkpoint route corpus for independent pre-parity reads. +-- +-- This function deliberately reads projection state before route parity exists. +-- It exposes only fields which can be reconstructed from the supported +-- release contracts and their canonical logs at the supplied checkpoint. Public +-- route views are not used because they are themselves parity-gated. + +reset role; +set role programmable_migrator; + +create function programmable_private.build_classic_v3_reconciler_reward_v1( + p_vault_address bytea, + p_pool_id bytea, + p_token_address bytea, + p_token_name text, + p_token_symbol text, + p_launch_transaction_hash bytea, + p_buy_swap_fee_bps integer, + p_sell_swap_fee_bps integer, + p_launcher_fee_bps integer, + p_configuration_hash bytea, + p_active_configuration_hash bytea, + p_configuration_epoch bigint, + p_total_creator_fees_received numeric, + p_total_creator_fees_claimed numeric, + p_pending_creator_fees numeric, + p_allocations jsonb, + p_entitlements jsonb, + p_events jsonb +) +returns jsonb +language sql +immutable +security invoker +set search_path = '' +as $function$ + with normalized_allocations as ( + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'allocationIndex', allocation.item -> 'allocationIndex', + 'payoutAddress', allocation.item -> 'payoutAddress', + 'shareBps', allocation.item -> 'shareBps' + ) order by allocation.ordinality + ), '[]'::jsonb) as value + from pg_catalog.jsonb_array_elements(p_allocations) with ordinality + as allocation(item, ordinality) + ), normalized_entitlements as ( + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'account', entitlement.item -> 'account', + 'claimableWei', entitlement.item -> 'claimableWei', + 'claimedWei', entitlement.item -> 'claimedWei' + ) order by entitlement.ordinality + ), '[]'::jsonb) as value + from pg_catalog.jsonb_array_elements(p_entitlements) with ordinality + as entitlement(item, ordinality) + ) + select pg_catalog.jsonb_build_object( + 'releaseVersion', 'classic-v3', + 'modelId', 'classic', + 'vaultAddress', '0x' || pg_catalog.encode(p_vault_address, 'hex'), + 'poolId', '0x' || pg_catalog.encode(p_pool_id, 'hex'), + 'tokenAddress', '0x' || pg_catalog.encode(p_token_address, 'hex'), + 'tokenName', p_token_name, + 'tokenSymbol', p_token_symbol, + 'launchTransactionHash', + '0x' || pg_catalog.encode(p_launch_transaction_hash, 'hex'), + 'buySwapFeeBps', p_buy_swap_fee_bps, + 'sellSwapFeeBps', p_sell_swap_fee_bps, + 'launcherFeeBps', p_launcher_fee_bps, + 'configurationHash', + '0x' || pg_catalog.encode(p_configuration_hash, 'hex'), + 'activeConfigurationHash', + '0x' || pg_catalog.encode(p_active_configuration_hash, 'hex'), + 'configurationEpoch', p_configuration_epoch::text, + 'totalCreatorFeesReceivedWei', p_total_creator_fees_received::text, + 'totalCreatorFeesClaimedWei', p_total_creator_fees_claimed::text, + 'pendingCreatorFeesWei', p_pending_creator_fees::text, + 'allocations', normalized_allocations.value, + 'entitlements', normalized_entitlements.value, + 'events', p_events + ) + from normalized_allocations cross join normalized_entitlements +$function$; + +comment on function programmable_private.build_classic_v3_reconciler_reward_v1( + bytea, bytea, bytea, text, text, bytea, integer, integer, integer, + bytea, bytea, bigint, numeric, numeric, numeric, jsonb, jsonb, jsonb +) is + 'Builds the exact Classic V3 reward DTO shared by indexed and runtime reconciliation.'; + +revoke all on function + programmable_private.build_classic_v3_reconciler_reward_v1( + bytea, bytea, bytea, text, text, bytea, integer, integer, integer, + bytea, bytea, bigint, numeric, numeric, numeric, jsonb, jsonb, jsonb + ) from public, anon, authenticated, service_role, programmable_projector, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +grant execute on function + programmable_private.build_classic_v3_reconciler_reward_v1( + bytea, bytea, bytea, text, text, bytea, integer, integer, integer, + bytea, bytea, bigint, numeric, numeric, numeric, jsonb, jsonb, jsonb + ) to programmable_reconciler; + +create function programmable_private.assemble_reconciler_routes_v1( + p_tokens jsonb, + p_charts jsonb, + p_profiles jsonb, + p_rewards jsonb, + p_launches jsonb +) +returns table ( + route_key text, + compared_count bigint, + dto jsonb +) +language plpgsql +immutable +security invoker +set search_path = '' +as $function$ +declare + route_contract_version constant text := 'programmable-route-corpus-v1'; + launch_count bigint; + profile_token_count bigint; + reward_count bigint; + lookup_count bigint; + release_id text; + model_id text; + expected_route_keys text[]; +begin + if p_tokens is null + or pg_catalog.jsonb_typeof(p_tokens) <> 'array' + or p_charts is null + or pg_catalog.jsonb_typeof(p_charts) <> 'array' + or p_profiles is null + or pg_catalog.jsonb_typeof(p_profiles) <> 'array' + or p_rewards is null + or pg_catalog.jsonb_typeof(p_rewards) <> 'array' + or p_launches is null + or pg_catalog.jsonb_typeof(p_launches) <> 'array' + then + raise exception using + errcode = '22023', + message = 'invalid reconciler route parts'; + end if; + + launch_count := pg_catalog.jsonb_array_length(p_tokens); + reward_count := pg_catalog.jsonb_array_length(p_rewards); + lookup_count := pg_catalog.jsonb_array_length(p_launches); + if launch_count > 0 then + release_id := p_tokens -> 0 ->> 'releaseVersion'; + model_id := p_tokens -> 0 ->> 'modelId'; + end if; + expected_route_keys := case + when release_id = 'classic-v2' and model_id = 'classic' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile' + ]::text[] + when release_id = 'classic-v3' and model_id = 'classic' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile', + 'classic-v3-profile', 'launch-lookup' + ]::text[] + when release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and model_id = 'stock-paired' then + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile', + 'launch-lookup' + ]::text[] + else null + end; + select coalesce(pg_catalog.sum(pg_catalog.jsonb_array_length(profile -> 'tokens')), 0) + into profile_token_count + from pg_catalog.jsonb_array_elements(p_profiles) as profile + where pg_catalog.jsonb_typeof(profile -> 'tokens') = 'array'; + + if launch_count < 1 + or launch_count <> pg_catalog.jsonb_array_length(p_charts) + or launch_count <> profile_token_count + or expected_route_keys is null + or exists ( + select 1 + from pg_catalog.jsonb_array_elements(p_tokens) as token + where token ->> 'releaseVersion' is distinct from release_id + or token ->> 'modelId' is distinct from model_id + ) + or (release_id = 'classic-v3' and reward_count <> launch_count) + or (release_id <> 'classic-v3' and reward_count <> 0) + or (release_id = 'classic-v2' and lookup_count <> 0) + or (release_id <> 'classic-v2' and lookup_count <> launch_count) + then + raise exception using + errcode = '22023', + message = 'reconciler route cardinality mismatch'; + end if; + + return query + select route.route_key, route.compared_count, route.dto + from (values + ('explore-list'::text, launch_count, pg_catalog.jsonb_build_object( + 'contractVersion', route_contract_version, 'tokens', p_tokens + )), + ('explore-token'::text, launch_count, pg_catalog.jsonb_build_object( + 'contractVersion', route_contract_version, 'tokens', p_tokens + )), + ('explore-chart'::text, launch_count, pg_catalog.jsonb_build_object( + 'contractVersion', route_contract_version, 'charts', p_charts + )), + ('creator-profile'::text, launch_count, pg_catalog.jsonb_build_object( + 'contractVersion', route_contract_version, 'profiles', p_profiles + )), + ('classic-v3-profile'::text, reward_count, pg_catalog.jsonb_build_object( + 'contractVersion', route_contract_version, 'rewards', p_rewards + )), + ('launch-lookup'::text, lookup_count, pg_catalog.jsonb_build_object( + 'contractVersion', route_contract_version, 'launches', p_launches + )) + ) as route(route_key, compared_count, dto) + where route.route_key = any(expected_route_keys) + order by pg_catalog.array_position(expected_route_keys, route.route_key); +end +$function$; + +comment on function programmable_private.assemble_reconciler_routes_v1( + jsonb, jsonb, jsonb, jsonb, jsonb +) is + 'Assembles the immutable applicable-route corpus contract from validated canonical release parts.'; + +revoke all on function programmable_private.assemble_reconciler_routes_v1( + jsonb, jsonb, jsonb, jsonb, jsonb +) from public, anon, authenticated, service_role, programmable_projector, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +grant execute on function programmable_private.assemble_reconciler_routes_v1( + jsonb, jsonb, jsonb, jsonb, jsonb +) to programmable_reconciler; + +create function programmable_private.get_reconciler_route_corpus_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_checkpoint_id uuid, + p_checkpoint_block_number numeric, + p_checkpoint_block_hash bytea, + p_maximum_entity_count integer default 10000 +) +returns table ( + route_key text, + compared_count bigint, + dto jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + contract_row record; + launch_count bigint; + projected_launch_count bigint; + vault_count bigint; + launch_rows jsonb; + chart_rows jsonb; + creator_rows jsonb; + reward_rows jsonb; + lookup_rows jsonb; +begin + perform programmable_private.assert_caller('programmable_reconciler'); + + if not ( + p_release_id = 'classic-v2' and p_model_id = 'classic' + or p_release_id = 'classic-v3' and p_model_id = 'classic' + or p_release_id in ( + 'stock-paired-v1', 'stock-paired-v2', 'stock-paired-v3' + ) and p_model_id = 'stock-paired' + ) then + raise exception using + errcode = '0A000', + message = 'reconciler route corpus release is not supported'; + end if; + + -- Reuse the already-reviewed exact checkpoint, manifest, route coverage and + -- entity-bound validation. This call neither reads nor manufactures parity. + select * into strict contract_row + from programmable_private.get_reconciler_preparity_contract_v1( + p_chain_id, p_release_id, p_model_id, p_source_group, + p_epoch_id, p_pointer_generation, p_checkpoint_id, + p_checkpoint_block_number, p_checkpoint_block_hash, + p_maximum_entity_count + ); + + with launches as materialized ( + select + launch.chain_id, + launch.release_id, + launch.model_id, + launch.token, + launch.creator, + launch.launch_transaction_hash, + launch.pool_id, + launch.reward_vault, + launch.launch_hash, + launch.token_name, + launch.token_symbol, + launch.total_supply, + run.source_group, + launch.epoch_id, + launch.pointer_generation, + launch.projection_run_id, + source_occurrence.block_timestamp as launch_block_timestamp, + source_occurrence.transaction_index::bigint + as launch_transaction_index, + source_occurrence.receipt_log_ordinal::bigint + as launch_receipt_log_ordinal, + pool.currency0, + pool.currency1, + pool.hook, + pool.pool_key_fee, + pool.tick_spacing, + case + when pool.currency0 = launch.token then pool.currency1 + when pool.currency1 = launch.token then pool.currency0 + else null + end as quote_asset, + fee.buy_swap_fee_bps, + fee.sell_swap_fee_bps, + fee.buy_creator_fee_bps, + fee.sell_creator_fee_bps, + fee.creator_fee_bps, + fee.launcher_fee_bps, + fee.transfer_tax_bps, + fee.lp_fee_pips, + launch.promoted_block_number, + launch.promoted_block_hash, + liquidity.position_recipient, + liquidity.position_token_id, + liquidity.token_liquidity_amount, + liquidity.locked_token_dust, + liquidity.initial_sqrt_price_x96, + liquidity.initial_tick, + liquidity.tick_lower, + liquidity.tick_upper, + coalesce( + case + when pool.currency0 = launch.token then pool.currency1 + when pool.currency1 = launch.token then pool.currency0 + else null + end, + pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + ) as normalized_quote_asset, + source_occurrence.block_number as launch_source_block_number, + source_occurrence.block_global_log_index::bigint + as launch_source_block_global_log_index, + market.block_number as market_block_number, + market.block_hash as market_block_hash, + market.last_transaction_hash as market_last_transaction_hash, + market.last_transaction_index as market_last_transaction_index, + market.last_block_global_log_index as market_last_log_index, + market.sqrt_price_x96 as market_sqrt_price_x96, + market.liquidity as market_liquidity, + market.tick as market_tick, + fee_total.gross_total as gross_total, + fee_total.creator_fee_total as creator_fee_total, + fee_total.launcher_fee_total as launcher_fee_total + from programmable_private.projection_entity_current as current_launch + join programmable_private.launch_projections as launch + on launch.launch_projection_id = current_launch.projection_row_id + and launch.projection_run_id = current_launch.projection_run_id + and launch.chain_id = current_launch.chain_id + and launch.release_id = current_launch.release_id + and launch.model_id = current_launch.model_id + and launch.promoted_block_number = current_launch.promoted_block_number + and launch.promoted_block_hash = current_launch.promoted_block_hash + join programmable_private.run_headers as run + on run.run_id = launch.projection_run_id + and run.run_kind = 'projection' + and run.chain_id = launch.chain_id + and run.release_id = launch.release_id + and run.model_id = launch.model_id + and run.source_group = current_launch.source_group + and run.epoch_id = launch.epoch_id + and run.captured_pointer_generation = launch.pointer_generation + join programmable_private.projection_publications as publication + on publication.publication_id = current_launch.publication_id + and publication.run_id = launch.projection_run_id + and publication.epoch_id = launch.epoch_id + and publication.pointer_generation = launch.pointer_generation + and publication.checkpoint_id = current_launch.checkpoint_id + and publication.target_block_number = launch.promoted_block_number + and publication.target_block_hash = launch.promoted_block_hash + join programmable_private.release_epoch_current as current_epoch + on current_epoch.chain_id = launch.chain_id + and current_epoch.release_id = launch.release_id + and current_epoch.model_id = launch.model_id + and current_epoch.source_group = run.source_group + and current_epoch.epoch_id = launch.epoch_id + and current_epoch.generation = launch.pointer_generation + join programmable_private.chain_event_current_canonical + as launch_canonical + on launch_canonical.logical_event_id = + launch.last_source_logical_event_id + and launch_canonical.occurrence_id = launch.last_source_occurrence_id + and launch_canonical.block_hash = + launch.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as source_occurrence + on source_occurrence.occurrence_id = + launch.last_source_occurrence_id + and source_occurrence.logical_event_id = + launch.last_source_logical_event_id + and source_occurrence.block_hash = + launch.last_source_occurrence_block_hash + and source_occurrence.chain_id = launch.chain_id + and source_occurrence.release_id = launch.release_id + and source_occurrence.model_id = launch.model_id + and source_occurrence.source_group = run.source_group + and source_occurrence.epoch_id = launch.epoch_id + and source_occurrence.pointer_generation = launch.pointer_generation + join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + and pool.chain_id = launch.chain_id + and pool.release_id = launch.release_id + and pool.model_id = launch.model_id + and pool.epoch_id = launch.epoch_id + and pool.pointer_generation = launch.pointer_generation + and pool.pool_id = launch.pool_id + and pool.promoted_block_number = launch.promoted_block_number + and pool.promoted_block_hash = launch.promoted_block_hash + and (pool.currency0 = launch.token or pool.currency1 = launch.token) + join programmable_private.chain_event_current_canonical + as pool_canonical + on pool_canonical.logical_event_id = pool.last_source_logical_event_id + and pool_canonical.occurrence_id = pool.last_source_occurrence_id + and pool_canonical.block_hash = pool.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as pool_source_occurrence + on pool_source_occurrence.occurrence_id = pool.last_source_occurrence_id + and pool_source_occurrence.logical_event_id = + pool.last_source_logical_event_id + and pool_source_occurrence.block_hash = + pool.last_source_occurrence_block_hash + and pool_source_occurrence.chain_id = launch.chain_id + and pool_source_occurrence.release_id = launch.release_id + and pool_source_occurrence.model_id = launch.model_id + and pool_source_occurrence.source_group = run.source_group + and pool_source_occurrence.epoch_id = launch.epoch_id + and pool_source_occurrence.pointer_generation = + launch.pointer_generation + join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = pool.projection_run_id + and fee.chain_id = pool.chain_id + and fee.release_id = pool.release_id + and fee.model_id = pool.model_id + and fee.epoch_id = pool.epoch_id + and fee.pointer_generation = pool.pointer_generation + and fee.promoted_block_number = pool.promoted_block_number + and fee.promoted_block_hash = pool.promoted_block_hash + join programmable_private.chain_event_current_canonical as fee_canonical + on fee_canonical.logical_event_id = + fee.disclosure_source_logical_event_id + and fee_canonical.occurrence_id = fee.disclosure_source_occurrence_id + and fee_canonical.block_hash = + fee.disclosure_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as fee_source_occurrence + on fee_source_occurrence.occurrence_id = + fee.disclosure_source_occurrence_id + and fee_source_occurrence.logical_event_id = + fee.disclosure_source_logical_event_id + and fee_source_occurrence.block_hash = + fee.disclosure_source_occurrence_block_hash + and fee_source_occurrence.chain_id = launch.chain_id + and fee_source_occurrence.release_id = launch.release_id + and fee_source_occurrence.model_id = launch.model_id + and fee_source_occurrence.source_group = run.source_group + and fee_source_occurrence.epoch_id = launch.epoch_id + and fee_source_occurrence.pointer_generation = launch.pointer_generation + join programmable_private.launch_position_liquidity_facts as liquidity + on liquidity.launch_projection_id = launch.launch_projection_id + and liquidity.projection_run_id = launch.projection_run_id + and liquidity.chain_id = launch.chain_id + and liquidity.release_id = launch.release_id + and liquidity.model_id = launch.model_id + and liquidity.source_group = run.source_group + and liquidity.epoch_id = launch.epoch_id + and liquidity.pointer_generation = launch.pointer_generation + and liquidity.token = launch.token + and liquidity.pool_id = launch.pool_id + join programmable_private.chain_event_current_canonical + as liquidity_canonical + on liquidity_canonical.logical_event_id = + liquidity.source_logical_event_id + and liquidity_canonical.occurrence_id = liquidity.source_occurrence_id + and liquidity_canonical.block_hash = + liquidity.source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as liquidity_source_occurrence + on liquidity_source_occurrence.occurrence_id = + liquidity.source_occurrence_id + and liquidity_source_occurrence.logical_event_id = + liquidity.source_logical_event_id + and liquidity_source_occurrence.block_hash = + liquidity.source_occurrence_block_hash + and liquidity_source_occurrence.chain_id = launch.chain_id + and liquidity_source_occurrence.release_id = launch.release_id + and liquidity_source_occurrence.model_id = launch.model_id + and liquidity_source_occurrence.source_group = run.source_group + and liquidity_source_occurrence.epoch_id = launch.epoch_id + and liquidity_source_occurrence.pointer_generation = + launch.pointer_generation + -- Market closes are prior independently reconciled evidence. Read the + -- private base fact rather than the parity-gated public view, but bind the + -- complete release/epoch/pointer scope so another release sharing a pool + -- id can never satisfy this corpus. + join lateral ( + select close_fact.* + from programmable_private.market_block_closes as close_fact + join programmable_private.reconciliation_records as reconciliation + on reconciliation.reconciliation_id = close_fact.reconciliation_id + and reconciliation.mismatch_count = 0 + join programmable_private.run_headers as reconciliation_run + on reconciliation_run.run_id = reconciliation.run_id + and reconciliation_run.run_kind = 'reconciliation' + and reconciliation_run.chain_id = close_fact.chain_id + and reconciliation_run.release_id = close_fact.release_id + and reconciliation_run.model_id = close_fact.model_id + and reconciliation_run.source_group = close_fact.source_group + and reconciliation_run.epoch_id = close_fact.epoch_id + and reconciliation_run.captured_pointer_generation = + close_fact.pointer_generation + join programmable_private.run_lifecycle_outcomes + as reconciliation_outcome + on reconciliation_outcome.run_id = reconciliation_run.run_id + and reconciliation_outcome.status = 'succeeded' + join programmable_private.release_epoch_current as market_epoch + on market_epoch.chain_id = close_fact.chain_id + and market_epoch.release_id = close_fact.release_id + and market_epoch.model_id = close_fact.model_id + and market_epoch.source_group = close_fact.source_group + and market_epoch.epoch_id = close_fact.epoch_id + and market_epoch.generation = close_fact.pointer_generation + join programmable_private.chain_event_current_canonical + as market_canonical + on market_canonical.occurrence_id = + close_fact.last_source_occurrence_id + and market_canonical.logical_event_id = + close_fact.last_source_logical_event_id + and market_canonical.block_hash = + close_fact.last_source_occurrence_block_hash + join programmable_private.chain_event_materialized_occurrences_v1 + as market_source_occurrence + on market_source_occurrence.occurrence_id = + close_fact.last_source_occurrence_id + and market_source_occurrence.logical_event_id = + close_fact.last_source_logical_event_id + and market_source_occurrence.block_hash = + close_fact.last_source_occurrence_block_hash + and market_source_occurrence.chain_id = close_fact.chain_id + and market_source_occurrence.release_id = close_fact.release_id + and market_source_occurrence.model_id = close_fact.model_id + and market_source_occurrence.source_group = close_fact.source_group + and market_source_occurrence.epoch_id = close_fact.epoch_id + and market_source_occurrence.pointer_generation = + close_fact.pointer_generation + where close_fact.chain_id = launch.chain_id + and close_fact.release_id = launch.release_id + and close_fact.model_id = launch.model_id + and close_fact.source_group = run.source_group + and close_fact.epoch_id = launch.epoch_id + and close_fact.pointer_generation = launch.pointer_generation + and close_fact.pool_id = launch.pool_id + and close_fact.block_number <= p_checkpoint_block_number::bigint + order by close_fact.block_number desc, + close_fact.last_block_global_log_index desc, + close_fact.market_block_close_id + limit 1 + ) as market on true + join programmable_private.current_pool_fee_totals_v1 as fee_total + on fee_total.chain_id = launch.chain_id + and fee_total.release_id = launch.release_id + and fee_total.model_id = launch.model_id + and fee_total.epoch_id = launch.epoch_id + and fee_total.pointer_generation = launch.pointer_generation + and fee_total.pool_id = launch.pool_id + and ( + fee_total.quote_asset is not distinct from case + when pool.currency0 = launch.token then pool.currency1 + when pool.currency1 = launch.token then pool.currency0 + else null + end + or ( + fee_total.quote_asset is null + and case + when pool.currency0 = launch.token then pool.currency1 + when pool.currency1 = launch.token then pool.currency0 + else null + end = pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + ) + ) + where current_launch.entity_kind = 'launch' + and current_launch.chain_id = p_chain_id + and current_launch.release_id = p_release_id + and current_launch.model_id = p_model_id + and current_launch.source_group = p_source_group + and launch.chain_id = p_chain_id + and launch.release_id = p_release_id + and launch.model_id = p_model_id + and run.source_group = p_source_group + and launch.epoch_id = p_epoch_id + and launch.pointer_generation = p_pointer_generation + and launch.promoted_block_number <= p_checkpoint_block_number::bigint + and launch.is_complete + and ( + launch.reward_vault is null + or programmable_private.has_current_verified_reward_seed( + run.run_id, + launch.reward_vault + ) + ) + ), normalized as materialized ( + select + launch.*, + pg_catalog.jsonb_build_object( + 'releaseVersion', launch.release_id, + 'modelId', launch.model_id, + 'tokenAddress', '0x' || pg_catalog.encode(launch.token, 'hex'), + 'creatorAddress', '0x' || pg_catalog.encode(launch.creator, 'hex'), + 'launchTransactionHash', '0x' || pg_catalog.encode( + launch.launch_transaction_hash, 'hex' + ), + 'launchBlockNumber', launch.launch_source_block_number::text, + 'launchTransactionIndex', launch.launch_transaction_index, + 'launchLogIndex', launch.launch_receipt_log_ordinal, + 'launchedAt', pg_catalog.to_char( + launch.launch_block_timestamp at time zone 'UTC', + 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"' + ), + 'poolId', '0x' || pg_catalog.encode(launch.pool_id, 'hex'), + 'hookAddress', '0x' || pg_catalog.encode(launch.hook, 'hex'), + 'rewardVaultAddress', case when launch.reward_vault is null then null + else '0x' || pg_catalog.encode(launch.reward_vault, 'hex') end, + 'positionRecipient', '0x' || pg_catalog.encode( + launch.position_recipient, 'hex' + ), + 'positionTokenId', launch.position_token_id::text, + 'launchHash', '0x' || pg_catalog.encode(launch.launch_hash, 'hex'), + 'name', launch.token_name, + 'symbol', launch.token_symbol, + 'decimals', 18, + 'totalSupplyRaw', launch.total_supply::text, + 'quoteAssetAddress', '0x' || pg_catalog.encode( + launch.normalized_quote_asset, 'hex' + ), + 'fees', pg_catalog.jsonb_build_object( + 'buySwapFeeBps', launch.buy_swap_fee_bps, + 'sellSwapFeeBps', launch.sell_swap_fee_bps, + 'buyCreatorFeeBps', launch.buy_creator_fee_bps, + 'sellCreatorFeeBps', launch.sell_creator_fee_bps, + 'launcherFeeBps', launch.launcher_fee_bps, + 'transferTaxBps', launch.transfer_tax_bps, + 'lpFeePips', launch.lp_fee_pips + ), + 'liquidity', pg_catalog.jsonb_build_object( + 'tokenLiquidityAmountRaw', launch.token_liquidity_amount::text, + 'lockedTokenDustRaw', launch.locked_token_dust::text, + 'initialTick', launch.initial_tick, + 'tickLower', launch.tick_lower, + 'tickUpper', launch.tick_upper + ) + ) as token_json, + pg_catalog.jsonb_build_object( + 'releaseVersion', launch.release_id, + 'modelId', launch.model_id, + 'tokenAddress', '0x' || pg_catalog.encode(launch.token, 'hex'), + 'poolId', '0x' || pg_catalog.encode(launch.pool_id, 'hex'), + 'quoteAssetAddress', '0x' || pg_catalog.encode( + launch.normalized_quote_asset, 'hex' + ), + 'state', pg_catalog.jsonb_build_object( + 'blockNumber', launch.market_block_number::text, + 'blockHash', '0x' || pg_catalog.encode( + launch.market_block_hash, 'hex' + ), + 'transactionHash', '0x' || pg_catalog.encode( + launch.market_last_transaction_hash, 'hex' + ), + 'transactionIndex', launch.market_last_transaction_index, + 'logIndex', launch.market_last_log_index, + 'sqrtPriceX96', launch.market_sqrt_price_x96::text, + 'liquidity', launch.market_liquidity::text, + 'tick', launch.market_tick, + 'lpFeePips', launch.lp_fee_pips + ), + 'volume', pg_catalog.jsonb_build_object( + 'quoteAssetAddress', '0x' || pg_catalog.encode( + launch.normalized_quote_asset, 'hex' + ), + 'grossQuoteRaw', launch.gross_total::text, + 'creatorFeeQuoteRaw', launch.creator_fee_total::text, + 'launcherFeeQuoteRaw', launch.launcher_fee_total::text + ) + ) as chart_json + from launches as launch + ), ordered_tokens as ( + select * from normalized + order by launch_source_block_number, launch_transaction_index, + launch_receipt_log_ordinal, launch_transaction_hash, token + ) + select + pg_catalog.count(*), + coalesce(pg_catalog.jsonb_agg(token_json), '[]'::jsonb), + coalesce(pg_catalog.jsonb_agg(chart_json), '[]'::jsonb) + into launch_count, launch_rows, chart_rows + from ordered_tokens; + + -- The pre-parity contract already captured the immutable current-entity + -- manifest for this exact checkpoint. Count launch pointers from that + -- contract so any missing canonical/publication/base-fact join fails closed + -- instead of silently shrinking every DTO route together. + select pg_catalog.count(*) into projected_launch_count + from pg_catalog.jsonb_array_elements( + contract_row.current_entities + ) as entity + where entity ->> 'entityKind' = 'launch'; + + if launch_count = 0 + or launch_count <> projected_launch_count + or launch_count > p_maximum_entity_count + then + raise exception using + errcode = '54000', + message = 'reconciler route corpus launch cardinality is invalid'; + end if; + + with token_rows as ( + select token, ordinal + from pg_catalog.jsonb_array_elements(launch_rows) with ordinality + as launch_token(token, ordinal) + ), creators as ( + select + token ->> 'creatorAddress' as account, + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'releaseVersion', token ->> 'releaseVersion', + 'modelId', token ->> 'modelId', + 'tokenAddress', token ->> 'tokenAddress', + 'launchTransactionHash', token ->> 'launchTransactionHash' + ) order by ordinal + ) as tokens + from token_rows + group by token ->> 'creatorAddress' + ) + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'account', account, + 'tokens', tokens + ) order by account + ), '[]'::jsonb) + into creator_rows + from creators; + + with vaults as ( + select + vault.reward_vault_projection_id, + vault.vault, + vault.pool_id, + launch.token, + launch.token_name, + launch.token_symbol, + launch.launch_transaction_hash, + fee.buy_swap_fee_bps, + fee.sell_swap_fee_bps, + fee.launcher_fee_bps, + vault.configuration_hash, + coalesce( + vault.active_configuration_hash, + vault.configuration_hash + ) as active_configuration_hash, + coalesce( + vault.configuration_epoch, + ( + select pg_catalog.max(allocation.configuration_epoch) + from programmable_private.reward_allocation_projections + as allocation + where allocation.reward_vault_projection_id = + vault.reward_vault_projection_id + and allocation.allocation_fact_id = + vault.current_allocation_fact_id + and allocation.projection_run_id = vault.projection_run_id + ) + ) as configuration_epoch, + coalesce( + vault.total_creator_fees_received, + entitlement_state.total_entitlement_value, + 0 + ) as total_creator_fees_received, + entitlement_state.total_creator_fees_claimed, + fee_total.creator_fee_total - coalesce( + vault.total_creator_fees_received, + entitlement_state.total_entitlement_value, + 0 + ) as pending_creator_fees, + coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'allocationIndex', allocation.allocation_index, + 'payoutAddress', '0x' || pg_catalog.encode( + allocation.payout_address, 'hex' + ), + 'shareBps', allocation.share_bps + ) order by allocation.allocation_index + ) + from programmable_private.reward_allocation_projections as allocation + where allocation.reward_vault_projection_id = + vault.reward_vault_projection_id + and allocation.allocation_fact_id = vault.current_allocation_fact_id + and allocation.projection_run_id = vault.projection_run_id + and allocation.effective_from_block <= + p_checkpoint_block_number::bigint + and ( + allocation.effective_to_block is null + or allocation.effective_to_block >= + p_checkpoint_block_number::bigint + ) + ), '[]'::jsonb) as allocations, + entitlement_state.entitlements, + coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'blockNumber', event.block_number::text, + 'blockHash', '0x' || pg_catalog.encode(event.block_hash, 'hex'), + 'transactionHash', + '0x' || pg_catalog.encode(event.transaction_hash, 'hex'), + 'transactionIndex', event.transaction_index::integer, + 'logIndex', event.block_global_log_index::integer, + 'kind', case event.event_type + when 'CreatorFeesCheckpointed' then 'checkpoint' + when 'BeneficiaryFeesClaimed' then 'claim' + when 'PayoutWalletChanged' then 'payout-change' + when 'CtoRewardConfigurationActivated' then 'cto-activation' + end + ) || case event.event_type + when 'CreatorFeesCheckpointed' then + pg_catalog.jsonb_build_object( + 'configurationEpoch', + (event.decoded_payload ->> 'configurationEpoch')::numeric::text, + 'amountWei', + (event.decoded_payload ->> 'amount')::numeric::text, + 'totalCreatorFeesReceivedWei', + (event.decoded_payload ->> + 'totalCreatorFeesReceived')::numeric::text + ) + when 'BeneficiaryFeesClaimed' then + pg_catalog.jsonb_build_object( + 'beneficiary', pg_catalog.lower( + event.decoded_payload ->> 'beneficiary' + ), + 'amountWei', + (event.decoded_payload ->> 'amount')::numeric::text, + 'beneficiaryTotalClaimedWei', + (event.decoded_payload ->> + 'beneficiaryTotalClaimed')::numeric::text, + 'vaultTotalReceivedWei', + (event.decoded_payload ->> + 'vaultTotalReceived')::numeric::text + ) + when 'PayoutWalletChanged' then + pg_catalog.jsonb_build_object( + 'allocationIndex', + (event.decoded_payload ->> 'allocationIndex')::numeric::text, + 'previousPayoutWallet', pg_catalog.lower( + event.decoded_payload ->> 'previousPayoutWallet' + ), + 'newPayoutWallet', pg_catalog.lower( + event.decoded_payload ->> 'newPayoutWallet' + ), + 'shareBps', + (event.decoded_payload ->> 'shareBps')::integer, + 'configurationEpoch', + (event.decoded_payload ->> 'configurationEpoch')::numeric::text, + 'activeConfigurationHash', pg_catalog.lower( + event.decoded_payload ->> 'activeConfigurationHash' + ), + 'effectiveTotalCreatorFeesReceivedWei', + (event.decoded_payload ->> + 'effectiveTotalCreatorFeesReceived')::numeric::text + ) + when 'CtoRewardConfigurationActivated' then + pg_catalog.jsonb_build_object( + 'approvalReference', pg_catalog.lower( + event.decoded_payload ->> 'approvalReference' + ), + 'configurationEpoch', + (event.decoded_payload ->> 'configurationEpoch')::numeric::text, + 'previousConfigurationHash', pg_catalog.lower( + event.decoded_payload ->> 'previousConfigurationHash' + ), + 'newConfigurationHash', pg_catalog.lower( + event.decoded_payload ->> 'newConfigurationHash' + ), + 'allocations', coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'beneficiary', pg_catalog.lower( + beneficiary.item #>> '{}' + ), + 'shareBps', (share.item #>> '{}')::integer + ) order by beneficiary.ordinality + ) + from pg_catalog.jsonb_array_elements( + event.decoded_payload -> 'beneficiaries' + ) with ordinality as beneficiary(item, ordinality) + join pg_catalog.jsonb_array_elements( + event.decoded_payload -> 'sharesBps' + ) with ordinality as share(item, ordinality) + on share.ordinality = beneficiary.ordinality + ), '[]'::jsonb), + 'effectiveTotalCreatorFeesReceivedWei', + (event.decoded_payload ->> + 'effectiveTotalCreatorFeesReceived')::numeric::text + ) + else '{}'::jsonb + end + order by event.block_number, event.transaction_index, + event.block_global_log_index, event.transaction_hash + ) + from programmable_private.chain_event_materialized_occurrences_v1 + as event + join programmable_private.chain_event_current_canonical as canonical + on canonical.occurrence_id = event.occurrence_id + and canonical.logical_event_id = event.logical_event_id + and canonical.block_hash = event.block_hash + where event.chain_id = vault.chain_id + and event.release_id = vault.release_id + and event.model_id = vault.model_id + and event.source_group = p_source_group + and event.epoch_id = vault.epoch_id + and event.pointer_generation = vault.pointer_generation + and event.source_address = vault.vault + and event.block_number <= p_checkpoint_block_number::bigint + and event.event_type in ( + 'CreatorFeesCheckpointed', 'BeneficiaryFeesClaimed', + 'PayoutWalletChanged', 'CtoRewardConfigurationActivated' + ) + ), '[]'::jsonb) as events + from programmable_private.current_reward_vault_projections_v1 as vault + join programmable_private.current_launch_projections_v1 as launch + on launch.launch_projection_id = vault.launch_projection_id + join programmable_private.pool_projections as pool + on pool.launch_projection_id = launch.launch_projection_id + and pool.projection_run_id = launch.projection_run_id + join programmable_private.pool_fee_configurations as fee + on fee.pool_projection_id = pool.pool_projection_id + and fee.projection_run_id = pool.projection_run_id + join programmable_private.current_pool_fee_totals_v1 as fee_total + on fee_total.chain_id = vault.chain_id + and fee_total.release_id = vault.release_id + and fee_total.model_id = vault.model_id + and fee_total.epoch_id = vault.epoch_id + and fee_total.pointer_generation = vault.pointer_generation + and fee_total.pool_id = vault.pool_id + and ( + fee_total.quote_asset is null + or fee_total.quote_asset = + pg_catalog.decode(pg_catalog.repeat('00', 20), 'hex') + ) + and fee_total.promoted_block_number <= p_checkpoint_block_number::bigint + cross join lateral ( + select + coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'account', '0x' || pg_catalog.encode(balance.account, 'hex'), + 'claimableWei', balance.claimable_accrued::text, + 'claimedWei', balance.claimed_total::text + ) order by balance.account + ), '[]'::jsonb) as entitlements, + coalesce(pg_catalog.sum(balance.claimed_total), 0) + as total_creator_fees_claimed, + coalesce(pg_catalog.sum( + balance.claimable_accrued + balance.claimed_total + ), 0) as total_entitlement_value + from programmable_private.current_account_reward_balances_v1 as balance + where balance.chain_id = vault.chain_id + and balance.release_id = vault.release_id + and balance.model_id = vault.model_id + and balance.epoch_id = vault.epoch_id + and balance.pointer_generation = vault.pointer_generation + and balance.vault = vault.vault + and balance.promoted_block_number <= p_checkpoint_block_number::bigint + ) as entitlement_state + where vault.chain_id = p_chain_id + and p_release_id = 'classic-v3' + and vault.release_id = p_release_id + and vault.model_id = p_model_id + and vault.epoch_id = p_epoch_id + and vault.pointer_generation = p_pointer_generation + and vault.promoted_block_number <= p_checkpoint_block_number::bigint + ), ordered_vaults as ( + select *, pg_catalog.jsonb_array_length(allocations) as allocation_count + from vaults + order by vault + ) + select + pg_catalog.count(*), + coalesce(pg_catalog.jsonb_agg( + programmable_private.build_classic_v3_reconciler_reward_v1( + vault, + pool_id, + token, + token_name, + token_symbol, + launch_transaction_hash, + buy_swap_fee_bps, + sell_swap_fee_bps, + launcher_fee_bps, + configuration_hash, + active_configuration_hash, + configuration_epoch, + total_creator_fees_received, + total_creator_fees_claimed, + pending_creator_fees, + allocations, + entitlements, + events + ) order by vault + ), '[]'::jsonb) + into vault_count, reward_rows + from ordered_vaults; + + if (p_release_id = 'classic-v3' and vault_count <> launch_count) + or (p_release_id <> 'classic-v3' and vault_count <> 0) + or exists ( + select 1 from pg_catalog.jsonb_array_elements(reward_rows) as reward + where pg_catalog.jsonb_array_length(reward -> 'allocations') = 0 + or pg_catalog.jsonb_array_length(reward -> 'entitlements') = 0 + ) then + raise exception using + errcode = '55000', + message = 'reconciler route corpus reward coverage is incomplete'; + end if; + + select coalesce(pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'releaseVersion', launch.token ->> 'releaseVersion', + 'modelId', launch.token ->> 'modelId', + 'account', launch.token ->> 'creatorAddress', + 'launchTransactionHash', launch.token ->> 'launchTransactionHash', + 'tokenAddress', launch.token ->> 'tokenAddress' + ) order by launch.token ->> 'creatorAddress', + launch.token ->> 'launchTransactionHash', + launch.token ->> 'tokenAddress' + ), '[]'::jsonb) + into lookup_rows + from pg_catalog.jsonb_array_elements(launch_rows) as launch(token) + where p_release_id <> 'classic-v2'; + + return query + select route.route_key, route.compared_count, route.dto + from programmable_private.assemble_reconciler_routes_v1( + launch_rows, chart_rows, creator_rows, reward_rows, lookup_rows + ) as route; +end +$function$; + +comment on function programmable_private.get_reconciler_route_corpus_v1( + bigint, text, text, text, uuid, bigint, uuid, numeric, bytea, integer +) is + 'Returns the bounded deterministic applicable-route corpus for one supported release at one exact current checkpoint without reading public or parity-gated route views.'; + +revoke all on function programmable_private.get_reconciler_route_corpus_v1( + bigint, text, text, text, uuid, bigint, uuid, numeric, bytea, integer +) from public; +revoke all on function programmable_private.get_reconciler_route_corpus_v1( + bigint, text, text, text, uuid, bigint, uuid, numeric, bytea, integer +) from anon, authenticated, service_role, programmable_projector, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; +grant execute on function programmable_private.get_reconciler_route_corpus_v1( + bigint, text, text, text, uuid, bigint, uuid, numeric, bytea, integer +) to programmable_reconciler; + +reset role; diff --git a/supabase/migrations/20260801024013_projector_atomic_block_liveness.sql b/supabase/migrations/20260801024013_projector_atomic_block_liveness.sql new file mode 100644 index 00000000..b190f4dc --- /dev/null +++ b/supabase/migrations/20260801024013_projector_atomic_block_liveness.sql @@ -0,0 +1,1363 @@ +-- A relevant block is the projector atomicity boundary. Keep one exact page +-- intact through the already-reviewed 4,096-candidate atomic-group ceiling so +-- blocks above the former UI-sized page limit cannot wedge ingestion. + +set role programmable_migrator; + +alter table programmable_private.dual_rpc_log_coverage_evidence + drop constraint dual_rpc_log_coverage_exact_page_shape_check, + add constraint dual_rpc_log_coverage_exact_page_shape_check check ( + pg_catalog.cardinality(ordered_log_commitments_a) between 0 and 4096 + and programmable_private.valid_topics(ordered_log_commitments_a) + and ordered_log_commitments_a = ordered_log_commitments_b + and ordered_log_commitments_a = ordered_inbox_commitments + and ( + ( + pg_catalog.cardinality(ordered_log_commitments_a) = 0 + and final_block_global_log_index = 4294967295 + and final_candidate_id = 'empty-page' + ) + or + ( + pg_catalog.cardinality(ordered_log_commitments_a) between 1 and 4096 + and final_candidate_id <> 'empty-page' + ) + ) + ); + +do $migration$ +declare + commitments_constraint name; + candidates_constraint name; +begin + select constraint_row.conname into strict commitments_constraint + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.provisional_dynamic_parent_pages'::regclass + and constraint_row.contype = 'c' + and pg_catalog.pg_get_constraintdef(constraint_row.oid) like + '%cardinality(provider_a_parent_commitments)%'; + + select constraint_row.conname into strict candidates_constraint + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.provisional_dynamic_parent_pages'::regclass + and constraint_row.contype = 'c' + and pg_catalog.pg_get_constraintdef(constraint_row.oid) like + '%cardinality(parent_candidate_ids)%'; + + execute pg_catalog.format( + 'alter table programmable_private.provisional_dynamic_parent_pages drop constraint %I', + commitments_constraint + ); + execute pg_catalog.format( + 'alter table programmable_private.provisional_dynamic_parent_pages drop constraint %I', + candidates_constraint + ); +exception + when no_data_found or too_many_rows then + raise exception using + errcode = '55000', + message = 'unexpected provisional dynamic-parent constraint shape'; +end +$migration$; + +alter table programmable_private.provisional_dynamic_parent_pages + add constraint provisional_dynamic_parent_commitments_count_check check ( + provider_a_parent_commitments = provider_b_parent_commitments + and provider_a_parent_commitments = parent_candidate_commitments + and pg_catalog.cardinality(provider_a_parent_commitments) + between 1 and 4096 + and programmable_private.valid_topics(provider_a_parent_commitments) + ), + add constraint provisional_dynamic_parent_candidates_count_check check ( + pg_catalog.cardinality(parent_candidate_ids) between 1 and 4096 + and pg_catalog.cardinality(parent_candidate_ids) = + pg_catalog.cardinality(parent_candidate_commitments) + and pg_catalog.cardinality(parent_candidate_ids) = + pg_catalog.jsonb_array_length(parent_candidates) + ); + +do $migration$ +declare + function_name text; + function_definition text; + old_fragment text; + new_fragment text; + expected_replacements integer; + actual_replacements integer; +begin + for function_name, old_fragment, new_fragment, expected_replacements in + values + ( + 'append_dual_rpc_log_coverage_evidence', + 'between 0 and 2000', + 'between 0 and 4096', + 1 + ), + ( + 'commit_envio_ingestion_page_v1', + 'not between 0 and 2000', + 'not between 0 and 4096', + 1 + ) + loop + select pg_catalog.pg_get_functiondef(procedure_row.oid) + into strict function_definition + from pg_catalog.pg_proc as procedure_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = procedure_row.pronamespace + where namespace_row.nspname = 'programmable_private' + and procedure_row.proname = function_name; + + actual_replacements := + (pg_catalog.length(function_definition) - pg_catalog.length( + pg_catalog.replace(function_definition, old_fragment, '') + )) / pg_catalog.length(old_fragment); + if actual_replacements <> expected_replacements then + raise exception using + errcode = '55000', + message = 'unexpected ingestion function shape for ' || function_name; + end if; + execute pg_catalog.replace( + function_definition, + old_fragment, + new_fragment + ); + end loop; +exception + when no_data_found or too_many_rows then + raise exception using + errcode = '55000', + message = 'unexpected ingestion function overload set'; +end +$migration$; + +do $migration$ +declare + function_definition text; + replacement_count integer; +begin + select pg_catalog.pg_get_functiondef(procedure_row.oid) + into strict function_definition + from pg_catalog.pg_proc as procedure_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = procedure_row.pronamespace + where namespace_row.nspname = 'programmable_private' + and procedure_row.proname = 'stage_verified_dynamic_parents_v2'; + + replacement_count := + (pg_catalog.length(function_definition) - pg_catalog.length( + pg_catalog.replace(function_definition, 'not between 1 and 32', '') + )) / pg_catalog.length('not between 1 and 32'); + if replacement_count <> 1 then + raise exception using + errcode = '55000', + message = 'unexpected provisional staging cardinality shape'; + end if; + function_definition := pg_catalog.replace( + function_definition, + 'not between 1 and 32', + 'not between 1 and 4096' + ); + + replacement_count := + (pg_catalog.length(function_definition) - pg_catalog.length( + pg_catalog.replace(function_definition, '> 262144', '') + )) / pg_catalog.length('> 262144'); + if replacement_count <> 1 then + raise exception using + errcode = '55000', + message = 'unexpected provisional parent payload shape'; + end if; + function_definition := pg_catalog.replace( + function_definition, + '> 262144', + '> 33554432' + ); + + replacement_count := + (pg_catalog.length(function_definition) - pg_catalog.length( + pg_catalog.replace(function_definition, '> 65536', '') + )) / pg_catalog.length('> 65536'); + if replacement_count <> 1 then + raise exception using + errcode = '55000', + message = 'unexpected provisional source payload shape'; + end if; + function_definition := pg_catalog.replace( + function_definition, + '> 65536', + '> 8388608' + ); + + execute function_definition; +exception + when no_data_found or too_many_rows then + raise exception using + errcode = '55000', + message = 'unexpected provisional staging overload set'; +end +$migration$; + +-- Reorg recovery is one process-fenced database transition. A release may +-- have no candidate-backed checkpoint below the common ancestor (including +-- its first published block). Such a release receives an explicit neutral +-- checkpoint at the proven ancestor and replays from the beginning; it never +-- invents an Envio candidate for the genesis boundary. +alter table programmable_private.projector_checkpoints + alter column cursor_block_global_log_index drop not null, + alter column cursor_candidate_id drop not null, + alter column safe_head_observation_id drop not null, + alter column target_block_evidence_id drop not null, + add column is_neutral boolean not null default false, + add constraint projector_checkpoint_recovery_shape_check check ( + ( + not is_neutral + and cursor_block_global_log_index is not null + and cursor_candidate_id is not null + and safe_head_observation_id is not null + and target_block_evidence_id is not null + ) + or + ( + is_neutral + and cursor_block_global_log_index is null + and cursor_candidate_id is null + and safe_head_observation_id is null + and target_block_evidence_id is null + ) + ); + +create table programmable_private.projector_reorg_recovery_history ( + recovery_id uuid primary key, + expected_reorg_generation bigint not null check ( + expected_reorg_generation >= 0 + ), + next_reorg_generation bigint not null check ( + next_reorg_generation = expected_reorg_generation + 1 + ), + expected_cursor_generation bigint not null check ( + expected_cursor_generation > 0 + ), + next_cursor_generation bigint not null check ( + next_cursor_generation = expected_cursor_generation + 1 + ), + target_history_generation bigint not null check ( + target_history_generation >= 0 + and target_history_generation < expected_cursor_generation + ), + target_block_number programmable_private.block_number_value not null, + target_block_hash programmable_private.bytes32_value not null, + target_block_global_log_index bigint, + target_candidate_id text, + genesis_point_id uuid references + programmable_private.envio_ingestion_cursor_genesis_points( + genesis_point_id + ) on delete restrict, + verification_run_id uuid not null + references programmable_private.run_headers(run_id) on delete restrict, + safe_head_observation_id uuid not null + references programmable_private.safe_head_observations(observation_id) + on delete restrict, + target_block_evidence_id uuid not null + references programmable_private.dual_rpc_block_evidence(block_evidence_id) + on delete restrict, + runtime_lease_generation bigint not null check ( + runtime_lease_generation > 0 + ), + reason_commitment programmable_private.bytes32_value not null, + recovered_at timestamptz not null, + audit_id uuid not null + references programmable_private.mutation_audits(audit_id) + on delete restrict, + check ( + ( + target_history_generation = 0 + and genesis_point_id is not null + and target_block_global_log_index is null + and target_candidate_id is null + ) + or + ( + target_history_generation > 0 + and genesis_point_id is null + and ( + ( + target_block_global_log_index is null + and target_candidate_id is null + ) + or + ( + target_block_global_log_index between 0 and 4294967295 + and target_candidate_id is not null + ) + ) + ) + ), + unique (next_reorg_generation), + unique (verification_run_id) +); + +create table programmable_private.projector_reorg_current ( + singleton_key text primary key check ( + singleton_key = 'canonical-projector-reorg-v1' + ), + reorg_generation bigint not null check (reorg_generation >= 0), + recovery_id uuid unique references + programmable_private.projector_reorg_recovery_history(recovery_id) + on delete restrict, + changed_at timestamptz, + check ( + (reorg_generation = 0 and recovery_id is null and changed_at is null) + or + (reorg_generation > 0 and recovery_id is not null and changed_at is not null) + ) +); + +insert into programmable_private.projector_reorg_current ( + singleton_key, reorg_generation +) values ('canonical-projector-reorg-v1', 0); + +alter table programmable_private.projector_reorg_recovery_history + enable row level security; +alter table programmable_private.projector_reorg_recovery_history + force row level security; +create policy projector_reorg_recovery_history_migrator_all + on programmable_private.projector_reorg_recovery_history + for all to programmable_migrator using (true) with check (true); +alter table programmable_private.projector_reorg_current + enable row level security; +alter table programmable_private.projector_reorg_current + force row level security; +create policy projector_reorg_current_migrator_all + on programmable_private.projector_reorg_current + for all to programmable_migrator using (true) with check (true); + +create trigger projector_reorg_recovery_history_immutable +before update or delete +on programmable_private.projector_reorg_recovery_history +for each row execute function programmable_private.reject_immutable_mutation(); + +create function programmable_private.get_projector_reorg_targets_v1( + p_provider_deployment_id uuid, + p_stream_id text, + p_maximum_depth integer default 128 +) +returns table ( + target_kind text, + history_generation bigint, + genesis_point_id uuid, + block_number bigint, + block_hash bytea, + block_global_log_index bigint, + candidate_id text, + current_reorg_generation bigint +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + current_reorg bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_maximum_depth not between 1 and 128 then + raise exception using errcode = '22023', message = 'invalid reorg depth'; + end if; + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current as cursor + where cursor.chain_id = 1 + and cursor.provider_deployment_id = p_provider_deployment_id + and cursor.stream_id = p_stream_id; + if not found or current_cursor.generation < 1 then + raise exception using errcode = '40001', message = 'reorg cursor is stale'; + end if; + select pointer.reorg_generation into strict current_reorg + from programmable_private.projector_reorg_current as pointer + where pointer.singleton_key = 'canonical-projector-reorg-v1'; + + return query + with history_targets as ( + select 'history'::text as target_kind, + history.generation as history_generation, + null::uuid as genesis_point_id, + history.block_number::bigint as block_number, + history.block_hash::bytea as block_hash, + history.block_global_log_index::bigint as block_global_log_index, + history.candidate_id::text as candidate_id, + current_reorg as current_reorg_generation, + 0 as target_order, + history.generation as generation_order + from programmable_private.envio_ingestion_cursor_history as history + where history.chain_id = 1 + and history.provider_deployment_id = p_provider_deployment_id + and history.stream_id = p_stream_id + and history.generation < current_cursor.generation + order by history.generation desc + limit pg_catalog.greatest(p_maximum_depth - 1, 0) + ), genesis_target as ( + select 'genesis'::text, + 0::bigint, + genesis.genesis_point_id, + genesis.anchor_block_number::bigint, + genesis.anchor_block_hash::bytea, + null::bigint, + null::text, + current_reorg, + 1, + 0::bigint + from programmable_private.envio_ingestion_cursor_genesis_points as genesis + where genesis.chain_id = 1 + and genesis.provider_deployment_id = p_provider_deployment_id + and genesis.stream_id = p_stream_id + ) + select target.target_kind, target.history_generation, + target.genesis_point_id, target.block_number, target.block_hash, + target.block_global_log_index, target.candidate_id, + target.current_reorg_generation + from ( + select * from history_targets + union all + select * from genesis_target + ) as target + order by target.target_order, target.generation_order desc + limit p_maximum_depth; +end +$function$; + +create function programmable_private.get_projector_reorg_generation_v1() +returns bigint +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + current_generation bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + select pointer.reorg_generation into strict current_generation + from programmable_private.projector_reorg_current as pointer + where pointer.singleton_key = 'canonical-projector-reorg-v1'; + return current_generation; +end +$function$; + +-- The normal projector promotion path treats a neutral recovery checkpoint as +-- the beginning of the release stream. This is the only semantic change to +-- the reviewed function and is guarded by an exact replacement count. +do $migration$ +declare + function_definition text; + replacement_count integer; +begin + select pg_catalog.pg_get_functiondef(procedure_row.oid) + into strict function_definition + from pg_catalog.pg_proc as procedure_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = procedure_row.pronamespace + where namespace_row.nspname = 'programmable_private' + and procedure_row.proname = 'promote_projection_run'; + replacement_count := + (pg_catalog.length(function_definition) - pg_catalog.length( + pg_catalog.replace( + function_definition, + 'previous_checkpoint.checkpoint_id is null', + '' + ) + )) / pg_catalog.length('previous_checkpoint.checkpoint_id is null'); + if replacement_count <> 2 then + raise exception using + errcode = '55000', + message = 'unexpected projection promotion checkpoint shape'; + end if; + execute pg_catalog.replace( + function_definition, + 'previous_checkpoint.checkpoint_id is null', + 'previous_checkpoint.checkpoint_id is null or previous_checkpoint.is_neutral' + ); +end +$migration$; + +-- A reorg boundary is an exact chain placement, not only a block height. At +-- the target height, rows from another block hash are always invalid. For a +-- history target on the same block, rows after the target log are invalid too. +-- A genesis target has no log boundary, so only its exact block hash survives. +create function programmable_private.projector_reorg_invalidates_placement_v1( + p_block_number bigint, + p_block_hash bytea, + p_block_global_log_index bigint, + p_target_block_number bigint, + p_target_block_hash bytea, + p_target_block_global_log_index bigint +) +returns boolean +language sql +immutable +security invoker +set search_path = '' +as $function$ + select + p_block_number > p_target_block_number + or ( + p_block_number = p_target_block_number + and ( + p_block_hash <> p_target_block_hash + or ( + p_target_block_global_log_index is not null + and p_block_global_log_index > p_target_block_global_log_index + ) + ) + ) +$function$; + +-- Projection rows inherit their exact replay boundary from the checkpoint that +-- published their run. This preserves legacy mid-block checkpoints: a row +-- published later in the same block is rebuilt even when its promoted block +-- number and hash match the selected ancestor. +create function programmable_private.projector_reorg_invalidates_projection_run_v1( + p_projection_run_id uuid, + p_promoted_block_number bigint, + p_promoted_block_hash bytea, + p_ancestor_block_number bigint, + p_ancestor_block_hash bytea, + p_ancestor_block_global_log_index bigint +) +returns boolean +language plpgsql +stable +security invoker +set search_path = '' +as $function$ +declare + publication record; +begin + if p_ancestor_block_number is null + and p_ancestor_block_hash is null + and p_ancestor_block_global_log_index is null + then + return true; + end if; + if p_projection_run_id is null + or p_promoted_block_number is null + or p_promoted_block_hash is null + or p_ancestor_block_number is null + or p_ancestor_block_hash is null + or p_ancestor_block_global_log_index is null + then + return true; + end if; + + select + projection_publication.target_block_number, + projection_publication.target_block_hash, + checkpoint.block_number as checkpoint_block_number, + checkpoint.block_hash as checkpoint_block_hash, + checkpoint.cursor_block_global_log_index, + checkpoint.is_neutral + into publication + from programmable_private.projection_publications + as projection_publication + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = projection_publication.checkpoint_id + where projection_publication.run_id = p_projection_run_id; + + if not found + or publication.is_neutral + or publication.cursor_block_global_log_index is null + or publication.target_block_number <> p_promoted_block_number + or publication.target_block_hash <> p_promoted_block_hash + or publication.checkpoint_block_number <> + publication.target_block_number + or publication.checkpoint_block_hash <> + publication.target_block_hash + then + return true; + end if; + + return programmable_private.projector_reorg_invalidates_placement_v1( + publication.checkpoint_block_number, + publication.checkpoint_block_hash, + publication.cursor_block_global_log_index, + p_ancestor_block_number, + p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); +end +$function$; + +-- Rebuildable projection rows are removed in strict foreign-key order. The +-- reward-vault snapshot chain is self-referential, so leaves are removed +-- before their baselines. A surviving child of an invalid baseline fails the +-- recovery closed instead of silently preserving inconsistent state. +create function programmable_private.delete_projector_projection_replay_scope_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_ancestor_block_number bigint, + p_ancestor_block_hash bytea, + p_ancestor_block_global_log_index bigint +) +returns bigint +language plpgsql +volatile +security invoker +set search_path = '' +as $function$ +declare + affected_rows bigint; + deleted_rows bigint := 0; +begin + if p_chain_id is null or p_chain_id <> 1 + or p_release_id is null + or pg_catalog.octet_length(p_release_id) not between 1 and 128 + or p_model_id is null + or pg_catalog.octet_length(p_model_id) not between 1 and 128 + or (p_ancestor_block_number is null) <> + (p_ancestor_block_hash is null) + or (p_ancestor_block_number is null) <> + (p_ancestor_block_global_log_index is null) + or ( + p_ancestor_block_number is not null + and ( + p_ancestor_block_number < 0 + or pg_catalog.octet_length(p_ancestor_block_hash) <> 32 + or p_ancestor_block_global_log_index < 0 + or p_ancestor_block_global_log_index > 4294967295 + ) + ) + then + raise exception using + errcode = '22023', message = 'invalid projection replay scope'; + end if; + + delete from programmable_private.initial_buy_vesting_projections + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.initial_buy_custody_projections + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.account_reward_balances + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.payout_change_projections + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.claim_projections + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.reward_allocation_projections + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + loop + delete from programmable_private.reward_vault_projections as vault + where vault.chain_id = p_chain_id + and vault.release_id = p_release_id + and vault.model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + vault.projection_run_id, + vault.promoted_block_number, vault.promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ) + and not exists ( + select 1 + from programmable_private.reward_vault_projections as child + where child.baseline_reward_vault_projection_id = + vault.reward_vault_projection_id + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + exit when affected_rows = 0; + end loop; + if exists ( + select 1 + from programmable_private.reward_vault_projections as vault + where vault.chain_id = p_chain_id + and vault.release_id = p_release_id + and vault.model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + vault.projection_run_id, + vault.promoted_block_number, vault.promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ) + ) then + raise exception using + errcode = '23503', + message = 'invalid reward snapshot retains a dependent child'; + end if; + + delete from programmable_private.pool_fee_totals + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.fee_accrual_facts + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.pool_fee_configurations + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.pool_projections + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.launch_position_liquidity_facts as position + using programmable_private.launch_projections as launch + where position.launch_projection_id = launch.launch_projection_id + and launch.chain_id = p_chain_id + and launch.release_id = p_release_id + and launch.model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + position.projection_run_id, + launch.promoted_block_number, launch.promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.launch_projection_occurrence_roles as role + using programmable_private.launch_projections as launch + where role.launch_projection_id = launch.launch_projection_id + and launch.chain_id = p_chain_id + and launch.release_id = p_release_id + and launch.model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + role.projection_run_id, + launch.promoted_block_number, launch.promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.launch_projection_conditions as condition + using programmable_private.launch_projections as launch + where condition.launch_projection_id = launch.launch_projection_id + and launch.chain_id = p_chain_id + and launch.release_id = p_release_id + and launch.model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + condition.projection_run_id, + launch.promoted_block_number, launch.promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + delete from programmable_private.launch_projections + where chain_id = p_chain_id and release_id = p_release_id + and model_id = p_model_id + and programmable_private.projector_reorg_invalidates_projection_run_v1( + projection_run_id, promoted_block_number, promoted_block_hash, + p_ancestor_block_number, p_ancestor_block_hash, + p_ancestor_block_global_log_index + ); + get diagnostics affected_rows = row_count; + deleted_rows := deleted_rows + affected_rows; + + return deleted_rows; +end +$function$; + +create function programmable_private.recover_projector_reorg_v1( + p_recovery_id uuid, + p_run_id uuid, + p_outcome_id uuid, + p_safe_head_observation_id uuid, + p_target_block_evidence_id uuid, + p_provider_deployment_id uuid, + p_stream_id text, + p_expected_cursor_generation bigint, + p_next_cursor_generation bigint, + p_target_history_generation bigint, + p_expected_reorg_generation bigint, + p_next_reorg_generation bigint, + p_target_block_number numeric, + p_target_block_hash bytea, + p_target_block_global_log_index numeric, + p_target_candidate_id text, + p_genesis_point_id uuid, + p_runtime_holder_id text, + p_runtime_lease_generation bigint, + p_runtime_lease_token_hash bytea, + p_reason_commitment bytea, + p_recovered_at timestamptz default pg_catalog.clock_timestamp() +) +returns table ( + cursor_generation bigint, + reorg_generation bigint, + release_checkpoint_count bigint +) +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + current_reorg programmable_private.projector_reorg_current%rowtype; + current_cursor programmable_private.envio_ingestion_cursor_current%rowtype; + target_evidence programmable_private.dual_rpc_block_evidence%rowtype; + release_epoch record; + current_pointer programmable_private.projector_checkpoint_current%rowtype; + current_checkpoint programmable_private.projector_checkpoints%rowtype; + ancestor programmable_private.projector_checkpoints%rowtype; + new_checkpoint_id uuid; + next_checkpoint_generation bigint; + next_release_reorg_generation bigint; + created_audit_id uuid; + route_record record; + route_history_id uuid; + occurrence_record record; + status_id uuid; + seed_record record; + release_count bigint := 0; +begin + perform programmable_private.assert_caller('programmable_projector'); + if not programmable_private.assert_projector_runtime_lease_v1( + p_runtime_holder_id, + p_runtime_lease_generation, + p_runtime_lease_token_hash + ) then + raise exception using errcode = '40001', message = 'stale runtime lease'; + end if; + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'rewind' + and chain_id = 1 and release_id = 'envio-control' + and model_id = 'envio-control' and source_group = 'canonical-events' + and epoch_id = '70000000-0000-0000-0000-000000000002' + and captured_pointer_generation = 1; + if not found or not exists ( + select 1 from programmable_private.run_lifecycle_outcomes as outcome + where outcome.outcome_id = p_outcome_id + and outcome.run_id = p_run_id + and outcome.status = 'succeeded' + ) then + raise exception using errcode = '55000', message = 'invalid reorg run'; + end if; + if p_next_cursor_generation <> p_expected_cursor_generation + 1 + or p_target_history_generation < 0 + or p_target_history_generation >= p_expected_cursor_generation + or p_next_reorg_generation <> p_expected_reorg_generation + 1 + or p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + or pg_catalog.octet_length(p_reason_commitment) <> 32 + then + raise exception using errcode = '22023', message = 'invalid reorg recovery'; + end if; + if ( + p_target_history_generation = 0 + and ( + p_genesis_point_id is null + or p_target_block_global_log_index is not null + or p_target_candidate_id is not null + ) + ) or ( + p_target_history_generation > 0 + and ( + p_genesis_point_id is not null + or ( + (p_target_block_global_log_index is null) + <> (p_target_candidate_id is null) + ) + ) + ) then + raise exception using errcode = '22023', message = 'invalid reorg target'; + end if; + + select * into current_reorg + from programmable_private.projector_reorg_current as pointer + where pointer.singleton_key = 'canonical-projector-reorg-v1' + for update; + if current_reorg.reorg_generation <> p_expected_reorg_generation then + raise exception using errcode = '40001', message = 'reorg generation CAS lost'; + end if; + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current as cursor + where cursor.chain_id = 1 + and cursor.provider_deployment_id = p_provider_deployment_id + and cursor.stream_id = p_stream_id + for update; + if not found + or current_cursor.generation <> p_expected_cursor_generation + then + raise exception using errcode = '40001', message = 'cursor generation CAS lost'; + end if; + select * into target_evidence + from programmable_private.dual_rpc_block_evidence as evidence + where evidence.block_evidence_id = p_target_block_evidence_id + and evidence.observation_id = p_safe_head_observation_id + and evidence.verification_run_id = p_run_id + and evidence.epoch_id = header.epoch_id + and evidence.pointer_generation = header.captured_pointer_generation; + if not found + or target_evidence.block_number <> p_target_block_number::bigint + or target_evidence.agreed_block_hash <> p_target_block_hash + then + raise exception using errcode = '23514', message = 'reorg target lacks evidence'; + end if; + + cursor_generation := programmable_private.rewind_envio_ingestion_cursor_v1( + p_run_id, + p_provider_deployment_id, + p_stream_id, + p_expected_cursor_generation, + p_next_cursor_generation, + p_target_history_generation, + p_reason_commitment, + p_recovered_at + ); + select * into current_cursor + from programmable_private.envio_ingestion_cursor_current as cursor + where cursor.chain_id = 1 + and cursor.provider_deployment_id = p_provider_deployment_id + and cursor.stream_id = p_stream_id; + if current_cursor.generation <> p_next_cursor_generation + or current_cursor.block_number <> p_target_block_number::bigint + or current_cursor.block_hash <> p_target_block_hash + or current_cursor.block_global_log_index is distinct from + p_target_block_global_log_index::bigint + or current_cursor.candidate_id is distinct from p_target_candidate_id + or current_cursor.genesis_point_id is distinct from p_genesis_point_id + then + raise exception using errcode = '40001', message = 'rewind target changed'; + end if; + + created_audit_id := programmable_private.append_mutation_audit( + 'projector_reorg.recover', p_reason_commitment, p_run_id, p_recovered_at + ); + + -- Canonical selections above the common ancestor are invalidated once for + -- every release. Historical facts remain immutable and replayable. + for occurrence_record in + select selected.*, occurrence.block_number, + materialization.block_evidence_id + from programmable_private.chain_event_current_canonical as selected + join programmable_private.chain_event_occurrences as occurrence + on occurrence.occurrence_id = selected.occurrence_id + join lateral ( + select scoped.block_evidence_id + from programmable_private.chain_event_occurrence_materializations as scoped + where scoped.occurrence_id = occurrence.occurrence_id + order by scoped.pointer_generation desc, scoped.verified_at desc + limit 1 + ) as materialization on true + where occurrence.chain_id = 1 + and programmable_private.projector_reorg_invalidates_placement_v1( + occurrence.block_number, + occurrence.block_hash, + occurrence.block_global_log_index, + p_target_block_number::bigint, + p_target_block_hash, + p_target_block_global_log_index::bigint + ) + for update of selected + loop + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.chain_event_occurrence_status_history ( + status_history_id, occurrence_id, logical_event_id, block_hash, status, + safe_head_observation_id, block_evidence_id, decision_run_id, + decision_commitment, decided_at, audit_id + ) values ( + status_id, occurrence_record.occurrence_id, + occurrence_record.logical_event_id, occurrence_record.block_hash, + 'orphaned', p_safe_head_observation_id, + occurrence_record.block_evidence_id, p_run_id, + p_reason_commitment, p_recovered_at, created_audit_id + ); + delete from programmable_private.chain_event_current_canonical + where logical_event_id = occurrence_record.logical_event_id + and occurrence_id = occurrence_record.occurrence_id; + end loop; + + for seed_record in + select seed.*, fact.creation_block_number, + occurrence.block_global_log_index + from programmable_private.reward_allocation_current_verified as seed + join programmable_private.reward_allocation_facts as fact + on fact.allocation_fact_id = seed.allocation_fact_id + join programmable_private.chain_event_occurrences as occurrence + on occurrence.occurrence_id = fact.factory_occurrence_id + where programmable_private.projector_reorg_invalidates_placement_v1( + fact.creation_block_number, + occurrence.block_hash, + occurrence.block_global_log_index, + p_target_block_number::bigint, + p_target_block_hash, + p_target_block_global_log_index::bigint + ) + for update of seed + loop + status_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.reward_allocation_status_history ( + seed_status_history_id, allocation_fact_id, allocation_evidence_id, + status, reason_commitment, decision_run_id, decided_at, audit_id + ) values ( + status_id, seed_record.allocation_fact_id, + seed_record.allocation_evidence_id, 'orphaned', + p_reason_commitment, p_run_id, p_recovered_at, created_audit_id + ); + delete from programmable_private.reward_allocation_current_verified + where allocation_fact_id = seed_record.allocation_fact_id; + end loop; + + for release_epoch in + select epoch.* + from programmable_private.release_epoch_current as epoch + where epoch.chain_id = 1 and ( + (epoch.release_id = 'classic-v2' and epoch.model_id = 'classic' + and epoch.source_group = 'core') + or (epoch.release_id = 'classic-v3' and epoch.model_id = 'classic' + and epoch.source_group = 'core') + or (epoch.release_id = 'stock-paired-v1' + and epoch.model_id = 'stock-paired' and epoch.source_group = 'core') + or (epoch.release_id = 'stock-paired-v2' + and epoch.model_id = 'stock-paired' and epoch.source_group = 'core') + or (epoch.release_id = 'stock-paired-v3' + and epoch.model_id = 'stock-paired' and epoch.source_group = 'core') + ) + order by epoch.release_id + for update + loop + current_pointer := null; + current_checkpoint := null; + ancestor := null; + select * into current_pointer + from programmable_private.projector_checkpoint_current as pointer + where pointer.chain_id = 1 + and pointer.release_id = release_epoch.release_id + and pointer.model_id = release_epoch.model_id + and pointer.source_group = release_epoch.source_group + and pointer.projector_version = 'projector-v1' + for update; + if found then + select * into strict current_checkpoint + from programmable_private.projector_checkpoints as checkpoint + where checkpoint.checkpoint_id = current_pointer.checkpoint_id; + select * into ancestor + from programmable_private.projector_checkpoints as checkpoint + where checkpoint.chain_id = 1 + and checkpoint.release_id = release_epoch.release_id + and checkpoint.model_id = release_epoch.model_id + and checkpoint.source_group = release_epoch.source_group + and checkpoint.projector_version = 'projector-v1' + and checkpoint.epoch_id = release_epoch.epoch_id + and checkpoint.pointer_generation = release_epoch.generation + and not checkpoint.is_neutral + and ( + checkpoint.block_number < p_target_block_number::bigint + or ( + checkpoint.block_number = p_target_block_number::bigint + and checkpoint.block_hash = p_target_block_hash + and ( + p_target_block_global_log_index is null + or checkpoint.cursor_block_global_log_index <= + p_target_block_global_log_index::bigint + ) + ) + ) + order by checkpoint.block_number desc, + checkpoint.cursor_block_global_log_index desc, + checkpoint.checkpoint_generation desc + limit 1; + next_checkpoint_generation := + current_pointer.checkpoint_generation + 1; + next_release_reorg_generation := current_pointer.reorg_generation + 1; + else + next_checkpoint_generation := 1; + next_release_reorg_generation := 1; + end if; + + new_checkpoint_id := pg_catalog.gen_random_uuid(); + if ancestor.checkpoint_id is null then + insert into programmable_private.projector_checkpoints ( + checkpoint_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, lease_generation, + checkpoint_generation, reorg_generation, block_number, block_hash, + cursor_block_global_log_index, cursor_candidate_id, + safe_head_observation_id, target_block_evidence_id, run_id, + terminal_outcome_id, created_at, is_neutral + ) values ( + new_checkpoint_id, 1, release_epoch.release_id, + release_epoch.model_id, release_epoch.source_group, 'projector-v1', + release_epoch.epoch_id, release_epoch.generation, + pg_catalog.greatest(coalesce(current_checkpoint.lease_generation, 0), 1), + next_checkpoint_generation, next_release_reorg_generation, + p_target_block_number::bigint, p_target_block_hash, + null, null, null, null, p_run_id, p_outcome_id, + p_recovered_at, true + ); + else + insert into programmable_private.projector_checkpoints ( + checkpoint_id, chain_id, release_id, model_id, source_group, + projector_version, epoch_id, pointer_generation, lease_generation, + checkpoint_generation, reorg_generation, block_number, block_hash, + cursor_block_global_log_index, cursor_candidate_id, + safe_head_observation_id, target_block_evidence_id, run_id, + terminal_outcome_id, created_at, is_neutral + ) values ( + new_checkpoint_id, 1, release_epoch.release_id, + release_epoch.model_id, release_epoch.source_group, 'projector-v1', + release_epoch.epoch_id, release_epoch.generation, + pg_catalog.greatest(coalesce(current_checkpoint.lease_generation, 0), 1), + next_checkpoint_generation, next_release_reorg_generation, + ancestor.block_number, ancestor.block_hash, + ancestor.cursor_block_global_log_index, ancestor.cursor_candidate_id, + ancestor.safe_head_observation_id, ancestor.target_block_evidence_id, + p_run_id, p_outcome_id, p_recovered_at, false + ); + end if; + + if current_pointer.checkpoint_id is null then + insert into programmable_private.projector_checkpoint_current ( + chain_id, release_id, model_id, source_group, projector_version, + checkpoint_id, checkpoint_generation, reorg_generation, changed_at + ) values ( + 1, release_epoch.release_id, release_epoch.model_id, + release_epoch.source_group, 'projector-v1', new_checkpoint_id, + next_checkpoint_generation, next_release_reorg_generation, + p_recovered_at + ) on conflict do nothing; + else + update programmable_private.projector_checkpoint_current as pointer + set checkpoint_id = new_checkpoint_id, + checkpoint_generation = next_checkpoint_generation, + reorg_generation = next_release_reorg_generation, + changed_at = p_recovered_at + where pointer.chain_id = 1 + and pointer.release_id = release_epoch.release_id + and pointer.model_id = release_epoch.model_id + and pointer.source_group = release_epoch.source_group + and pointer.projector_version = 'projector-v1' + and pointer.checkpoint_id = current_pointer.checkpoint_id + and pointer.checkpoint_generation = current_pointer.checkpoint_generation + and pointer.reorg_generation = current_pointer.reorg_generation; + end if; + if not found then + raise exception using errcode = '40001', message = 'release checkpoint CAS lost'; + end if; + + for route_record in + select * from programmable_private.route_eligibility_current as route + where route.chain_id = 1 + and route.release_id = release_epoch.release_id + and route.model_id = release_epoch.model_id + and route.source_group = release_epoch.source_group + for update + loop + route_history_id := pg_catalog.gen_random_uuid(); + insert into programmable_private.route_eligibility_history ( + route_eligibility_history_id, route_key, chain_id, release_id, + model_id, source_group, epoch_id, pointer_generation, status, + route_mode, checkpoint_id, reason_commitment, changed_by_run_id, + changed_at, audit_id + ) values ( + route_history_id, route_record.route_key, 1, + release_epoch.release_id, release_epoch.model_id, + release_epoch.source_group, release_epoch.epoch_id, + release_epoch.generation, 'ineligible', 'rpc', new_checkpoint_id, + p_reason_commitment, p_run_id, p_recovered_at, created_audit_id + ); + update programmable_private.route_eligibility_current as route + set epoch_id = release_epoch.epoch_id, + pointer_generation = release_epoch.generation, + status = 'ineligible', route_mode = 'rpc', + checkpoint_id = new_checkpoint_id, history_id = route_history_id, + changed_at = p_recovered_at + where route.route_key = route_record.route_key + and route.chain_id = 1 + and route.release_id = release_epoch.release_id + and route.model_id = release_epoch.model_id + and route.source_group = release_epoch.source_group; + end loop; + + delete from programmable_private.projection_entity_current as entity + where entity.chain_id = 1 + and entity.release_id = release_epoch.release_id + and entity.model_id = release_epoch.model_id + and entity.source_group = release_epoch.source_group + and ( + ancestor.checkpoint_id is null + or exists ( + select 1 + from programmable_private.projector_checkpoints as entity_checkpoint + where entity_checkpoint.checkpoint_id = entity.checkpoint_id + and ( + entity_checkpoint.block_number > ancestor.block_number + or ( + entity_checkpoint.block_number = ancestor.block_number + and ( + entity_checkpoint.block_hash <> ancestor.block_hash + or entity_checkpoint.cursor_block_global_log_index > + ancestor.cursor_block_global_log_index + ) + ) + ) + ) + ); + perform programmable_private.delete_projector_projection_replay_scope_v1( + 1, + release_epoch.release_id, + release_epoch.model_id, + case when ancestor.checkpoint_id is null + then null else ancestor.block_number end, + case when ancestor.checkpoint_id is null + then null else ancestor.block_hash end, + case when ancestor.checkpoint_id is null + then null else ancestor.cursor_block_global_log_index end + ); + release_count := release_count + 1; + end loop; + + insert into programmable_private.projector_reorg_recovery_history ( + recovery_id, expected_reorg_generation, next_reorg_generation, + expected_cursor_generation, next_cursor_generation, + target_history_generation, target_block_number, target_block_hash, + target_block_global_log_index, target_candidate_id, genesis_point_id, + verification_run_id, safe_head_observation_id, + target_block_evidence_id, runtime_lease_generation, + reason_commitment, recovered_at, audit_id + ) values ( + p_recovery_id, p_expected_reorg_generation, p_next_reorg_generation, + p_expected_cursor_generation, p_next_cursor_generation, + p_target_history_generation, p_target_block_number::bigint, + p_target_block_hash, p_target_block_global_log_index::bigint, + p_target_candidate_id, p_genesis_point_id, p_run_id, + p_safe_head_observation_id, p_target_block_evidence_id, + p_runtime_lease_generation, p_reason_commitment, + p_recovered_at, created_audit_id + ); + update programmable_private.projector_reorg_current as pointer + set reorg_generation = p_next_reorg_generation, + recovery_id = p_recovery_id, + changed_at = p_recovered_at + where pointer.singleton_key = 'canonical-projector-reorg-v1' + and pointer.reorg_generation = p_expected_reorg_generation; + if not found then + raise exception using errcode = '40001', message = 'reorg generation CAS lost'; + end if; + reorg_generation := p_next_reorg_generation; + release_checkpoint_count := release_count; + return next; +end +$function$; + +revoke all on function programmable_private.get_projector_reorg_targets_v1( + uuid, text, integer +) from public, anon, authenticated, service_role; +grant execute on function programmable_private.get_projector_reorg_targets_v1( + uuid, text, integer +) to programmable_projector; +revoke all on function programmable_private.get_projector_reorg_generation_v1() + from public, anon, authenticated, service_role; +grant execute on function programmable_private.get_projector_reorg_generation_v1() + to programmable_projector; +revoke all on function + programmable_private.projector_reorg_invalidates_placement_v1( + bigint, bytea, bigint, bigint, bytea, bigint + ) from public, anon, authenticated, service_role; +revoke all on function + programmable_private.projector_reorg_invalidates_projection_run_v1( + uuid, bigint, bytea, bigint, bytea, bigint + ) from public, anon, authenticated, service_role; +revoke all on function + programmable_private.delete_projector_projection_replay_scope_v1( + bigint, text, text, bigint, bytea, bigint + ) from public, anon, authenticated, service_role; +revoke all on function programmable_private.recover_projector_reorg_v1( + uuid, uuid, uuid, uuid, uuid, uuid, text, bigint, bigint, bigint, + bigint, bigint, numeric, bytea, numeric, text, uuid, text, bigint, + bytea, bytea, timestamptz +) from public, anon, authenticated, service_role; +grant execute on function programmable_private.recover_projector_reorg_v1( + uuid, uuid, uuid, uuid, uuid, uuid, text, bigint, bigint, bigint, + bigint, bigint, numeric, bytea, numeric, text, uuid, text, bigint, + bytea, bytea, timestamptz +) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801042040_classic_v3_dynamic_activation_reward_seed.sql b/supabase/migrations/20260801042040_classic_v3_dynamic_activation_reward_seed.sql new file mode 100644 index 00000000..23adc567 --- /dev/null +++ b/supabase/migrations/20260801042040_classic_v3_dynamic_activation_reward_seed.sql @@ -0,0 +1,1922 @@ +-- Classic V3 dynamic activation and first reward seed staging. +-- +-- Runtime/model proofs are persisted by a non-cursor-advancing ingestion run. +-- The first allocation seed is materialized only inside the normal projection +-- transaction, after its exact occurrences exist. Canonical selection remains +-- exclusively owned by promote_projection_run_v3. + +set role programmable_migrator; + +create table programmable_private.provisional_dynamic_parent_receipt_ordinals ( + provisional_page_id uuid not null + references programmable_private.provisional_dynamic_parent_pages( + provisional_page_id + ) on delete restrict, + parent_candidate_id + programmable_private.envio_candidate_identifier not null, + receipt_log_ordinal + programmable_private.receipt_log_ordinal_value not null, + staging_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + staged_at timestamptz not null, + primary key (provisional_page_id, parent_candidate_id), + unique (provisional_page_id, receipt_log_ordinal) +); + +create table programmable_private.dynamic_source_activation_staging ( + activation_id uuid primary key, + staging_run_id uuid not null + references programmable_private.run_headers(run_id) + on delete restrict, + chain_id programmable_private.chain_id_value not null check (chain_id = 1), + release_id programmable_private.release_identifier not null + check (release_id = 'classic-v3'), + model_id programmable_private.model_identifier not null + check (model_id = 'classic'), + source_group programmable_private.source_identifier not null + check (source_group = 'core'), + projector_version programmable_private.projector_identifier not null, + release_epoch_id uuid not null, + release_pointer_generation bigint not null + check (release_pointer_generation > 0), + reorg_generation bigint not null check (reorg_generation >= 0), + expected_cursor_generation bigint not null + check (expected_cursor_generation >= 0), + expected_cursor_block_hash + programmable_private.bytes32_value not null, + envio_provider_deployment_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_a_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_b_id uuid not null + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + provider_a_identity programmable_private.source_identifier not null, + provider_b_identity programmable_private.source_identifier not null, + provider_a_vendor programmable_private.source_identifier not null, + provider_b_vendor programmable_private.source_identifier not null, + provider_a_endpoint_url_commitment + programmable_private.bytes32_value not null, + provider_b_endpoint_url_commitment + programmable_private.bytes32_value not null, + provider_a_endpoint_origin_commitment + programmable_private.bytes32_value not null, + provider_b_endpoint_origin_commitment + programmable_private.bytes32_value not null, + safe_head_observation_id uuid not null + references programmable_private.safe_head_observations(observation_id) + on delete restrict, + activation_block_evidence_id uuid not null + references programmable_private.dual_rpc_block_evidence(block_evidence_id) + on delete restrict, + provisional_page_id uuid not null + references programmable_private.provisional_dynamic_parent_pages( + provisional_page_id + ) on delete restrict, + provisional_lineage_id uuid not null + references programmable_private.provisional_dynamic_source_lineages( + provisional_lineage_id + ) on delete restrict, + dynamic_source_attestation_id uuid not null, + runtime_code_evidence_id uuid not null + references programmable_private.dual_rpc_runtime_code_evidence( + runtime_code_evidence_id + ) on delete restrict, + dynamic_source_template_id uuid not null + references programmable_private.release_dynamic_source_templates( + dynamic_source_template_id + ) on delete restrict, + parent_candidate_id + programmable_private.envio_candidate_identifier not null, + parent_occurrence_id uuid not null, + parent_block_number programmable_private.block_number_value not null, + parent_block_hash programmable_private.bytes32_value not null, + parent_block_global_log_index + programmable_private.block_log_index_value not null, + parent_receipt_log_ordinal + programmable_private.receipt_log_ordinal_value not null, + parent_transaction_hash programmable_private.bytes32_value not null, + parent_transaction_index + programmable_private.transaction_index_value not null, + parent_source_address programmable_private.eth_address not null, + parent_payload_hash programmable_private.bytes32_value not null, + parent_raw_log_commitment programmable_private.bytes32_value not null, + launch_candidate_id + programmable_private.envio_candidate_identifier not null, + launch_occurrence_id uuid not null, + launch_block_number programmable_private.block_number_value not null, + launch_block_hash programmable_private.bytes32_value not null, + launch_block_global_log_index + programmable_private.block_log_index_value not null, + launch_receipt_log_ordinal + programmable_private.receipt_log_ordinal_value not null, + launch_transaction_hash programmable_private.bytes32_value not null, + hook_candidate_id + programmable_private.envio_candidate_identifier not null, + hook_occurrence_id uuid not null, + hook_receipt_log_ordinal + programmable_private.receipt_log_ordinal_value not null, + source_address programmable_private.eth_address not null, + pool_id programmable_private.bytes32_value not null, + cto_authority programmable_private.eth_address not null, + ordered_beneficiaries bytea[] not null, + ordered_shares_bps integer[] not null, + allocation_hash programmable_private.bytes32_value not null, + configuration_hash programmable_private.bytes32_value not null, + active_configuration_hash programmable_private.bytes32_value not null, + artifact_creation_code_commitment + programmable_private.bytes32_value not null, + deployed_artifact_creation_code_commitment + programmable_private.bytes32_value not null, + constructor_arguments_commitment + programmable_private.bytes32_value not null, + local_init_code_hash programmable_private.bytes32_value not null, + create2_salt programmable_private.bytes32_value not null, + predict_result_hash programmable_private.bytes32_value not null, + activation_payload jsonb not null, + activation_commitment programmable_private.bytes32_value not null, + staged_at timestamptz not null, + foreign key ( + release_epoch_id, chain_id, release_id, model_id, source_group + ) references programmable_private.release_epochs( + epoch_id, chain_id, release_id, model_id, source_group + ) on delete restrict, + check (provider_a_id <> provider_b_id), + check (provider_a_vendor = 'alchemy'), + check (provider_b_vendor = 'quicknode'), + check ( + launch_block_number = parent_block_number + and launch_block_hash = parent_block_hash + and launch_transaction_hash = parent_transaction_hash + and launch_block_global_log_index > parent_block_global_log_index + ), + check ( + programmable_private.valid_beneficiary_set( + ordered_beneficiaries, ordered_shares_bps, 5 + ) + ), + check (pg_catalog.octet_length(activation_payload::text) <= 262144), + unique ( + release_epoch_id, release_pointer_generation, + source_address, launch_candidate_id + ), + unique (release_epoch_id, activation_commitment) +); + +create table programmable_private.dynamic_source_activation_model_evidence ( + activation_id uuid not null + references programmable_private.dynamic_source_activation_staging( + activation_id + ) on delete restrict, + evidence_ordinal smallint not null check (evidence_ordinal between 1 and 3), + evidence_kind programmable_private.source_identifier not null, + payload jsonb not null, + evidence_commitment programmable_private.bytes32_value not null, + primary key (activation_id, evidence_ordinal), + unique (activation_id, evidence_kind), + unique (activation_id, evidence_commitment), + check (evidence_kind in ( + 'classic-v3-runtime-activation-v1', + 'classic-v3-initial-reward-configuration-v1', + 'classic-v3-launch-reward-conservation-v1' + )), + check (pg_catalog.octet_length(payload::text) <= 262144) +); + +create table programmable_private.dynamic_source_activation_consumptions ( + activation_id uuid primary key + references programmable_private.dynamic_source_activation_staging( + activation_id + ) on delete restrict, + final_run_id uuid not null, + publication_id uuid not null + references programmable_private.projection_publications(publication_id) + on delete restrict, + final_execution_evidence_id uuid not null, + allocation_fact_id uuid not null + references programmable_private.reward_allocation_facts(allocation_fact_id) + on delete restrict, + allocation_evidence_id uuid not null + references programmable_private.reward_allocation_evidence( + allocation_evidence_id + ) on delete restrict, + consumed_at timestamptz not null, + unique (final_run_id, activation_id), + foreign key (final_execution_evidence_id, final_run_id) + references programmable_private.projection_provider_execution_evidence( + execution_evidence_id, run_id + ) on delete restrict +); + +alter table programmable_private.provisional_dynamic_parent_receipt_ordinals + enable row level security; +alter table programmable_private.provisional_dynamic_parent_receipt_ordinals + force row level security; +alter table programmable_private.dynamic_source_activation_staging + enable row level security; +alter table programmable_private.dynamic_source_activation_staging + force row level security; +alter table programmable_private.dynamic_source_activation_model_evidence + enable row level security; +alter table programmable_private.dynamic_source_activation_model_evidence + force row level security; +alter table programmable_private.dynamic_source_activation_consumptions + enable row level security; +alter table programmable_private.dynamic_source_activation_consumptions + force row level security; + +create policy provisional_dynamic_parent_receipt_ordinals_migrator_all +on programmable_private.provisional_dynamic_parent_receipt_ordinals +for all to programmable_migrator using (true) with check (true); +create policy dynamic_source_activation_staging_migrator_all +on programmable_private.dynamic_source_activation_staging +for all to programmable_migrator using (true) with check (true); +create policy dynamic_source_activation_model_evidence_migrator_all +on programmable_private.dynamic_source_activation_model_evidence +for all to programmable_migrator using (true) with check (true); +create policy dynamic_source_activation_consumptions_migrator_all +on programmable_private.dynamic_source_activation_consumptions +for all to programmable_migrator using (true) with check (true); + +create trigger provisional_dynamic_parent_receipt_ordinals_immutable +before update or delete +on programmable_private.provisional_dynamic_parent_receipt_ordinals +for each row execute function programmable_private.reject_immutable_mutation(); +create trigger dynamic_source_activation_staging_immutable +before update or delete +on programmable_private.dynamic_source_activation_staging +for each row execute function programmable_private.reject_immutable_mutation(); +create trigger dynamic_source_activation_model_evidence_immutable +before update or delete +on programmable_private.dynamic_source_activation_model_evidence +for each row execute function programmable_private.reject_immutable_mutation(); +create trigger dynamic_source_activation_consumptions_immutable +before update or delete +on programmable_private.dynamic_source_activation_consumptions +for each row execute function programmable_private.reject_immutable_mutation(); + +create function programmable_private.stage_provisional_parent_receipt_ordinals_v1( + p_provisional_page_id uuid, + p_run_id uuid, + p_candidate_ids text[], + p_receipt_log_ordinals numeric[], + p_staged_at timestamptz +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + page programmable_private.provisional_dynamic_parent_pages%rowtype; + item record; + normalized_ordinal bigint; + existing programmable_private.provisional_dynamic_parent_receipt_ordinals%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into page + from programmable_private.provisional_dynamic_parent_pages + where provisional_page_id = p_provisional_page_id + and staging_run_id = p_run_id; + if not found + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + or p_staged_at is null + or coalesce(pg_catalog.cardinality(p_candidate_ids), 0) < 1 + or pg_catalog.cardinality(p_candidate_ids) + <> pg_catalog.cardinality(p_receipt_log_ordinals) + or p_candidate_ids is distinct from page.parent_candidate_ids::text[] + then + raise exception using + errcode = '23514', message = 'invalid provisional receipt ordinals'; + end if; + for item in + select candidate_id, receipt_ordinal + from pg_catalog.unnest(p_candidate_ids, p_receipt_log_ordinals) + as requested(candidate_id, receipt_ordinal) + loop + if item.receipt_ordinal <> pg_catalog.trunc(item.receipt_ordinal) + or item.receipt_ordinal < 0 + or item.receipt_ordinal > 4294967295 + then + raise exception using + errcode = '22023', message = 'invalid provisional receipt ordinal'; + end if; + normalized_ordinal := item.receipt_ordinal::bigint; + select * into existing + from programmable_private.provisional_dynamic_parent_receipt_ordinals + where provisional_page_id = p_provisional_page_id + and parent_candidate_id = item.candidate_id; + if found then + if existing.receipt_log_ordinal <> normalized_ordinal + or existing.staging_run_id <> p_run_id + then + raise exception using + errcode = '23505', message = 'provisional receipt replay changed'; + end if; + else + insert into programmable_private.provisional_dynamic_parent_receipt_ordinals ( + provisional_page_id, parent_candidate_id, receipt_log_ordinal, + staging_run_id, staged_at + ) values ( + p_provisional_page_id, + item.candidate_id::programmable_private.envio_candidate_identifier, + normalized_ordinal, p_run_id, p_staged_at + ); + end if; + end loop; + return p_provisional_page_id; +end +$function$; + +create function programmable_private.resolve_pending_dynamic_source_activations_v1( + p_projector_version text, + p_expected_cursor_generation bigint, + p_expected_cursor_block_hash bytea, + p_expected_reorg_generation bigint +) +returns table ( + provisional_page_id uuid, + provisional_lineage_id uuid, + dynamic_source_attestation_id uuid, + runtime_code_evidence_id uuid, + dynamic_source_template_id uuid, + parent_candidate_id text, + parent_receipt_log_ordinal bigint, + parent_candidate_commitment bytea, + safe_head_observation_id uuid, + target_block_evidence_id uuid, + source_address bytea, + release_epoch_id uuid, + release_pointer_generation bigint, + reorg_generation bigint, + envio_provider_deployment_id uuid, + provider_a_id uuid, + provider_b_id uuid, + provider_a_identity text, + provider_b_identity text, + provider_a_vendor text, + provider_b_vendor text, + provider_a_endpoint_url_commitment bytea, + provider_b_endpoint_url_commitment bytea, + provider_a_endpoint_origin_commitment bytea, + provider_b_endpoint_origin_commitment bytea, + manifest_artifact_creation_code_commitment bytea, + deployed_artifact_creation_code_commitment bytea +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + current_reorg bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_projector_version is null + or p_expected_cursor_generation < 0 + or pg_catalog.octet_length(p_expected_cursor_block_hash) <> 32 + or p_expected_reorg_generation < 0 + then + raise exception using + errcode = '22023', message = 'invalid activation resolution fence'; + end if; + select programmable_private.get_projector_reorg_generation_v1() + into current_reorg; + if current_reorg <> p_expected_reorg_generation then + raise exception using + errcode = '40001', message = 'activation reorg generation changed'; + end if; + + return query + select source.provisional_page_id, + source.provisional_lineage_id, + source.dynamic_source_attestation_id, + source.runtime_code_evidence_id, + source.dynamic_source_template_id, + source.factory_candidate_id, + receipt.receipt_log_ordinal::bigint, + source.parent_candidate_commitment, + page.safe_head_observation_id, + page.target_block_evidence_id, + source.deployed_source_address, + source.release_epoch_id, + source.release_pointer_generation, + source.reorg_generation, + source.envio_provider_deployment_id, + source.rpc_provider_a_id, + source.rpc_provider_b_id, + provider_a.redacted_identity::text, + provider_b.redacted_identity::text, + metadata_a.vendor::text, + metadata_b.vendor::text, + metadata_a.endpoint_url_commitment::bytea, + metadata_b.endpoint_url_commitment::bytea, + metadata_a.endpoint_origin_commitment::bytea, + metadata_b.endpoint_origin_commitment::bytea, + factory_binding.artifact_creation_code_commitment::bytea, + template.deployed_artifact_creation_code_commitment::bytea + from programmable_private.get_current_provisional_dynamic_sources_v1( + p_projector_version + ) as source + join programmable_private.provisional_dynamic_parent_pages as page + on page.provisional_page_id = source.provisional_page_id + and page.expected_cursor_generation = p_expected_cursor_generation + and page.expected_cursor_block_hash = p_expected_cursor_block_hash + and page.reorg_generation = p_expected_reorg_generation + join programmable_private.provisional_dynamic_parent_receipt_ordinals + as receipt + on receipt.provisional_page_id = source.provisional_page_id + and receipt.parent_candidate_id = source.factory_candidate_id + join programmable_private.provider_deployments as provider_a + on provider_a.provider_deployment_id = source.rpc_provider_a_id + and provider_a.provider_type = 'rpc_provider' + join programmable_private.provider_deployments as provider_b + on provider_b.provider_deployment_id = source.rpc_provider_b_id + and provider_b.provider_type = 'rpc_provider' + and provider_b.provider_deployment_id <> provider_a.provider_deployment_id + join programmable_private.rpc_provider_deployment_metadata as metadata_a + on metadata_a.provider_deployment_id = provider_a.provider_deployment_id + and metadata_a.chain_id = 1 + and metadata_a.vendor = 'alchemy' + and metadata_a.vendor_order = 1 + join programmable_private.rpc_provider_deployment_metadata as metadata_b + on metadata_b.provider_deployment_id = provider_b.provider_deployment_id + and metadata_b.chain_id = 1 + and metadata_b.vendor = 'quicknode' + and metadata_b.vendor_order = 2 + join programmable_private.release_dynamic_source_templates as template + on template.dynamic_source_template_id = + source.dynamic_source_template_id + and template.epoch_id = source.release_epoch_id + join programmable_private.release_source_bindings as factory_binding + on factory_binding.binding_id = + template.parent_factory_release_binding_id + and factory_binding.epoch_id = source.release_epoch_id + and factory_binding.source_role = 'vault_factory' + where source.release_epoch_id = ( + select current_epoch.epoch_id + from programmable_private.release_epoch_current as current_epoch + where current_epoch.chain_id = 1 + and current_epoch.release_id = 'classic-v3' + and current_epoch.model_id = 'classic' + and current_epoch.source_group = 'core' + ) + and source.deployed_source_address is not null + and not exists ( + select 1 + from programmable_private.dynamic_source_activation_staging as staged + where staged.release_epoch_id = source.release_epoch_id + and staged.release_pointer_generation = + source.release_pointer_generation + and staged.source_address = source.deployed_source_address + and staged.reorg_generation = p_expected_reorg_generation + and staged.parent_candidate_id = source.factory_candidate_id + and staged.parent_block_hash = source.factory_block_hash + ) + order by source.factory_block_number, + source.factory_block_global_log_index, + source.provisional_lineage_id; +end +$function$; + +create function programmable_private.stage_verified_dynamic_source_activations_v1( + p_run_id uuid, + p_projector_version text, + p_release_epoch_id uuid, + p_release_pointer_generation bigint, + p_reorg_generation bigint, + p_expected_cursor_generation bigint, + p_expected_cursor_block_hash bytea, + p_envio_provider_deployment_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_safe_head_observation_id uuid, + p_activation_block_evidence_id uuid, + p_activations jsonb, + p_model_evidence jsonb, + p_staged_at timestamptz +) +returns integer +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + cursor programmable_private.envio_ingestion_cursor_current%rowtype; + provider_a record; + provider_b record; + block_evidence programmable_private.dual_rpc_block_evidence%rowtype; + item jsonb; + evidence_item jsonb; + evidence_count integer; + evidence_kinds text[]; + allocation_accounts bytea[]; + allocation_shares integer[]; + existing programmable_private.dynamic_source_activation_staging%rowtype; + inserted_count integer := 0; +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_run_id is null + or p_projector_version is null + or p_release_epoch_id is null + or p_release_pointer_generation < 1 + or p_reorg_generation < 0 + or p_expected_cursor_generation < 0 + or pg_catalog.octet_length(p_expected_cursor_block_hash) <> 32 + or p_envio_provider_deployment_id is null + or p_provider_a_id is null + or p_provider_b_id is null + or p_provider_a_id = p_provider_b_id + or p_safe_head_observation_id is null + or p_activation_block_evidence_id is null + or p_activations is null + or pg_catalog.jsonb_typeof(p_activations) <> 'array' + or pg_catalog.jsonb_array_length(p_activations) not between 1 and 32 + or pg_catalog.octet_length(p_activations::text) > 1048576 + or p_model_evidence is null + or pg_catalog.jsonb_typeof(p_model_evidence) <> 'array' + or pg_catalog.jsonb_array_length(p_model_evidence) + <> pg_catalog.jsonb_array_length(p_activations) * 3 + or pg_catalog.octet_length(p_model_evidence::text) > 2097152 + or p_staged_at is null + then + raise exception using + errcode = '22023', message = 'invalid dynamic activation stage'; + end if; + + select * into header + from programmable_private.run_headers + where run_id = p_run_id and run_kind = 'ingestion' + for share; + if not found + or header.chain_id <> 1 + or header.release_id <> 'envio-control' + or header.model_id <> 'envio-control' + or header.source_group <> 'canonical-events' + or exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) + then + raise exception using + errcode = '55000', message = 'dynamic activation run is not open'; + end if; + perform programmable_private.assert_current_epoch( + 1, 'classic-v3', 'classic', 'core', + p_release_epoch_id, p_release_pointer_generation + ); + if programmable_private.get_projector_reorg_generation_v1() + <> p_reorg_generation + then + raise exception using + errcode = '40001', message = 'dynamic activation reorg changed'; + end if; + select * into cursor + from programmable_private.envio_ingestion_cursor_current + where chain_id = 1 + and provider_deployment_id = p_envio_provider_deployment_id + and stream_id = 'canonical-events'; + if not found + or cursor.generation <> p_expected_cursor_generation + or cursor.block_hash <> p_expected_cursor_block_hash + then + raise exception using + errcode = '40001', message = 'dynamic activation cursor changed'; + end if; + + select deployment.redacted_identity::text as identity, + metadata.vendor::text as vendor, + metadata.endpoint_url_commitment, + metadata.endpoint_origin_commitment + into provider_a + from programmable_private.provider_deployments as deployment + join programmable_private.rpc_provider_deployment_metadata as metadata + on metadata.provider_deployment_id = deployment.provider_deployment_id + and metadata.chain_id = 1 + and metadata.vendor = 'alchemy' + and metadata.vendor_order = 1 + where deployment.provider_deployment_id = p_provider_a_id + and deployment.provider_type = 'rpc_provider'; + select deployment.redacted_identity::text as identity, + metadata.vendor::text as vendor, + metadata.endpoint_url_commitment, + metadata.endpoint_origin_commitment + into provider_b + from programmable_private.provider_deployments as deployment + join programmable_private.rpc_provider_deployment_metadata as metadata + on metadata.provider_deployment_id = deployment.provider_deployment_id + and metadata.chain_id = 1 + and metadata.vendor = 'quicknode' + and metadata.vendor_order = 2 + where deployment.provider_deployment_id = p_provider_b_id + and deployment.provider_type = 'rpc_provider'; + if provider_a.identity is null + or provider_b.identity is null + or provider_a.identity = provider_b.identity + or provider_a.endpoint_url_commitment = + provider_b.endpoint_url_commitment + or provider_a.endpoint_origin_commitment = + provider_b.endpoint_origin_commitment + then + raise exception using + errcode = '23514', message = 'dynamic activation providers are not independent'; + end if; + select block.* into block_evidence + from programmable_private.dual_rpc_block_evidence as block + join programmable_private.safe_head_observations as observation + on observation.observation_id = block.observation_id + and observation.epoch_id = block.epoch_id + and observation.chain_id = block.chain_id + and observation.pointer_generation = block.pointer_generation + where block.block_evidence_id = p_activation_block_evidence_id + and block.observation_id = p_safe_head_observation_id + and block.verification_run_id = p_run_id + and block.epoch_id = header.epoch_id + and block.pointer_generation = header.captured_pointer_generation + and block.chain_id = 1 + and block.provider_a_block_hash = block.provider_b_block_hash + and observation.verification_run_id = p_run_id + and observation.provider_a_id = p_provider_a_id + and observation.provider_b_id = p_provider_b_id + and observation.reported_chain_id_a = 1 + and observation.reported_chain_id_b = 1; + if not found then + raise exception using + errcode = '23514', message = 'dynamic activation block evidence changed'; + end if; + + for item in + select value from pg_catalog.jsonb_array_elements(p_activations) + loop + if pg_catalog.jsonb_typeof(item) <> 'object' + or (item ->> 'activationId') is null + or (item ->> 'provisionalPageId') is null + or (item ->> 'provisionalLineageId') is null + or (item ->> 'sourceAddress') is null + or (item ->> 'activationCommitment') is null + or pg_catalog.lower(item ->> 'launchBlockHash') <> + '0x' || pg_catalog.encode(block_evidence.agreed_block_hash, 'hex') + or (item ->> 'launchBlockNumber')::bigint <> + block_evidence.block_number + then + raise exception using + errcode = '23514', message = 'invalid dynamic activation payload'; + end if; + if not exists ( + select 1 + from programmable_private.provisional_dynamic_parent_pages as page + join programmable_private.provisional_dynamic_source_lineages as lineage + on lineage.provisional_page_id = page.provisional_page_id + join programmable_private.release_dynamic_source_templates as template + on template.dynamic_source_template_id = + lineage.dynamic_source_template_id + and template.epoch_id = page.release_epoch_id + join programmable_private.release_source_bindings as factory_binding + on factory_binding.binding_id = + template.parent_factory_release_binding_id + and factory_binding.epoch_id = page.release_epoch_id + and factory_binding.source_role = 'vault_factory' + join programmable_private.provisional_dynamic_parent_receipt_ordinals + as receipt + on receipt.provisional_page_id = page.provisional_page_id + and receipt.parent_candidate_id = lineage.parent_candidate_id + where page.provisional_page_id = + (item ->> 'provisionalPageId')::uuid + and lineage.provisional_lineage_id = + (item ->> 'provisionalLineageId')::uuid + and lineage.dynamic_source_attestation_id = + (item ->> 'dynamicSourceAttestationId')::uuid + and lineage.runtime_code_evidence_id = + (item ->> 'runtimeCodeEvidenceId')::uuid + and lineage.dynamic_source_template_id = + (item ->> 'dynamicSourceTemplateId')::uuid + and lineage.parent_candidate_id::text = + item ->> 'parentCandidateId' + and receipt.receipt_log_ordinal = + (item ->> 'parentReceiptLogOrdinal')::bigint + and lineage.deployed_source_address = pg_catalog.decode( + pg_catalog.substring(item ->> 'sourceAddress', 3), 'hex' + ) + and page.release_epoch_id = p_release_epoch_id + and page.release_pointer_generation = p_release_pointer_generation + and page.reorg_generation = p_reorg_generation + and page.expected_cursor_generation = p_expected_cursor_generation + and page.expected_cursor_block_hash = p_expected_cursor_block_hash + and page.envio_provider_deployment_id = + p_envio_provider_deployment_id + and page.provider_a_id = p_provider_a_id + and page.provider_b_id = p_provider_b_id + and factory_binding.artifact_creation_code_commitment = + pg_catalog.decode( + pg_catalog.substring( + item ->> 'artifactCreationCodeCommitment', 3 + ), 'hex' + ) + and template.deployed_artifact_creation_code_commitment = + pg_catalog.decode( + pg_catalog.substring( + item ->> 'deployedArtifactCreationCodeCommitment', 3 + ), 'hex' + ) + and page.snapshot_block_number = + (item ->> 'parentBlockNumber')::bigint + and page.snapshot_block_hash = pg_catalog.decode( + pg_catalog.substring(item ->> 'parentBlockHash', 3), 'hex' + ) + ) then + raise exception using + errcode = '23514', message = 'dynamic activation parent changed'; + end if; + + select pg_catalog.count(*), + pg_catalog.array_agg( + evidence.value ->> 'evidenceKind' + order by evidence.value ->> 'evidenceKind' + ) + into evidence_count, evidence_kinds + from pg_catalog.jsonb_array_elements(p_model_evidence) as evidence(value) + where evidence.value ->> 'activationId' = item ->> 'activationId'; + if evidence_count <> 3 + or evidence_kinds is distinct from array[ + 'classic-v3-initial-reward-configuration-v1', + 'classic-v3-launch-reward-conservation-v1', + 'classic-v3-runtime-activation-v1' + ]::text[] + then + raise exception using + errcode = '23514', message = 'activation requires exactly three evidences'; + end if; + + -- Every persisted proof must retain the exact ordered provider tuple. A + -- length check is not provenance: provider B must be independently bound + -- through identity, vendor, endpoint and origin commitments. + if not exists ( + select 1 + from pg_catalog.jsonb_array_elements(p_model_evidence) as evidence(value) + where evidence.value ->> 'activationId' = item ->> 'activationId' + and evidence.value ->> 'evidenceKind' = + 'classic-v3-runtime-activation-v1' + and evidence.value -> 'payload' -> 'canonicalDeployment' + -> 'providerIdentities' = pg_catalog.jsonb_build_array( + provider_a.identity, provider_b.identity + ) + and evidence.value -> 'payload' -> 'canonicalDeployment' + -> 'providerVendorGroups' = pg_catalog.jsonb_build_array( + provider_a.vendor, provider_b.vendor + ) + and evidence.value -> 'payload' -> 'canonicalDeployment' + -> 'providerEndpointCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_url_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_url_commitment, 'hex' + ) + ) + and evidence.value -> 'payload' -> 'canonicalDeployment' + -> 'providerOriginCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_origin_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_origin_commitment, 'hex' + ) + ) + and evidence.value -> 'payload' -> 'runtimeObservation' + -> 'providerIdentities' = pg_catalog.jsonb_build_array( + provider_a.identity, provider_b.identity + ) + and evidence.value -> 'payload' -> 'runtimeObservation' + -> 'providerVendorGroups' = pg_catalog.jsonb_build_array( + provider_a.vendor, provider_b.vendor + ) + and evidence.value -> 'payload' -> 'runtimeObservation' + -> 'providerEndpointCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_url_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_url_commitment, 'hex' + ) + ) + and evidence.value -> 'payload' -> 'runtimeObservation' + -> 'providerOriginCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_origin_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_origin_commitment, 'hex' + ) + ) + and evidence.value -> 'payload' -> 'runtimeObservation' + -> 'providerCallCounts' = '[1,1]'::jsonb + ) then + raise exception using + errcode = '23514', message = 'activation runtime provider tuple changed'; + end if; + + -- Bind both provider branches and the full exact-block factory proof. + if not exists ( + select 1 + from pg_catalog.jsonb_array_elements(p_model_evidence) as evidence(value) + where evidence.value ->> 'activationId' = item ->> 'activationId' + and evidence.value ->> 'evidenceKind' = + 'classic-v3-initial-reward-configuration-v1' + and evidence.value -> 'payload' ->> 'constructorArgumentsCommitment' = + item ->> 'constructorArgumentsCommitment' + and evidence.value -> 'payload' ->> 'factoryConfigurationHash' = + item ->> 'configurationHash' + and evidence.value -> 'payload' ->> 'initialActiveConfigurationHash' = + item ->> 'activeConfigurationHash' + and evidence.value -> 'payload' ->> 'salt' = item ->> 'create2Salt' + and evidence.value -> 'payload' ->> 'locallyPredictedVault' = + item ->> 'sourceAddress' + and evidence.value -> 'payload' ->> 'ctoAuthority' = + item ->> 'ctoAuthority' + and evidence.value -> 'payload' -> 'providerCtoAuthorities' = + pg_catalog.jsonb_build_array( + item ->> 'ctoAuthority', item ->> 'ctoAuthority' + ) + and evidence.value -> 'payload' -> 'factoryProviderCallCounts' + = '[4,4]'::jsonb + and pg_catalog.jsonb_array_length( + evidence.value -> 'payload' -> 'providerFactoryConfigurationHashes' + ) = 2 + and pg_catalog.jsonb_array_length( + evidence.value -> 'payload' -> 'providerInitCodeHashes' + ) = 2 + and pg_catalog.jsonb_array_length( + evidence.value -> 'payload' -> 'providerPredictedVaults' + ) = 2 + and pg_catalog.jsonb_array_length( + evidence.value -> 'payload' -> 'factoryProviderSnapshotCommitments' + ) = 2 + and evidence.value -> 'payload' -> 'providerFactoryConfigurationHashes' + ->> 0 = item ->> 'configurationHash' + and evidence.value -> 'payload' -> 'providerFactoryConfigurationHashes' + ->> 1 = item ->> 'configurationHash' + and evidence.value -> 'payload' -> 'providerInitCodeHashes' + ->> 0 = item ->> 'localInitCodeHash' + and evidence.value -> 'payload' -> 'providerInitCodeHashes' + ->> 1 = item ->> 'localInitCodeHash' + and evidence.value -> 'payload' -> 'providerPredictedVaults' + ->> 0 = item ->> 'sourceAddress' + and evidence.value -> 'payload' -> 'providerPredictedVaults' + ->> 1 = item ->> 'sourceAddress' + and evidence.value -> 'payload' -> 'endConfigurationSnapshot' + -> 'providerIdentities' = pg_catalog.jsonb_build_array( + provider_a.identity, provider_b.identity + ) + and evidence.value -> 'payload' -> 'endConfigurationSnapshot' + -> 'providerVendorGroups' = pg_catalog.jsonb_build_array( + provider_a.vendor, provider_b.vendor + ) + and evidence.value -> 'payload' -> 'endConfigurationSnapshot' + -> 'providerEndpointCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_url_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_url_commitment, 'hex' + ) + ) + and evidence.value -> 'payload' -> 'endConfigurationSnapshot' + -> 'providerOriginCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_origin_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_origin_commitment, 'hex' + ) + ) + ) then + raise exception using + errcode = '23514', message = 'activation factory evidence changed'; + end if; + + if not exists ( + select 1 + from pg_catalog.jsonb_array_elements(p_model_evidence) as evidence(value) + where evidence.value ->> 'activationId' = item ->> 'activationId' + and evidence.value ->> 'evidenceKind' = + 'classic-v3-launch-reward-conservation-v1' + and evidence.value -> 'payload' -> 'rewardEvidence' + -> 'providerIdentities' = pg_catalog.jsonb_build_array( + provider_a.identity, provider_b.identity + ) + and evidence.value -> 'payload' -> 'rewardEvidence' + -> 'providerVendorGroups' = pg_catalog.jsonb_build_array( + provider_a.vendor, provider_b.vendor + ) + and evidence.value -> 'payload' -> 'rewardEvidence' + -> 'providerEndpointCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_url_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_url_commitment, 'hex' + ) + ) + and evidence.value -> 'payload' -> 'rewardEvidence' + -> 'providerOriginCommitments' = pg_catalog.jsonb_build_array( + '0x' || pg_catalog.encode( + provider_a.endpoint_origin_commitment, 'hex' + ), + '0x' || pg_catalog.encode( + provider_b.endpoint_origin_commitment, 'hex' + ) + ) + ) then + raise exception using + errcode = '23514', message = 'activation conservation provider tuple changed'; + end if; + + select pg_catalog.array_agg( + pg_catalog.decode( + pg_catalog.substring(allocation.value ->> 'beneficiary', 3), 'hex' + ) order by (allocation.value ->> 'allocationIndex')::integer + ), pg_catalog.array_agg( + (allocation.value ->> 'shareBps')::integer + order by (allocation.value ->> 'allocationIndex')::integer + ) + into allocation_accounts, allocation_shares + from pg_catalog.jsonb_array_elements(item -> 'allocations') + as allocation(value); + if not programmable_private.valid_beneficiary_set( + allocation_accounts, allocation_shares, 5 + ) then + raise exception using + errcode = '23514', message = 'invalid activation allocation'; + end if; + + select * into existing + from programmable_private.dynamic_source_activation_staging + where activation_id = (item ->> 'activationId')::uuid; + if found then + if existing.activation_commitment <> pg_catalog.decode( + pg_catalog.substring(item ->> 'activationCommitment', 3), 'hex' + ) + or existing.activation_payload <> item + then + raise exception using + errcode = '23505', message = 'activation replay changed immutable content'; + end if; + inserted_count := inserted_count + 1; + continue; + end if; + + insert into programmable_private.dynamic_source_activation_staging ( + activation_id, staging_run_id, chain_id, release_id, model_id, + source_group, projector_version, release_epoch_id, + release_pointer_generation, reorg_generation, + expected_cursor_generation, expected_cursor_block_hash, + envio_provider_deployment_id, provider_a_id, provider_b_id, + provider_a_identity, provider_b_identity, + provider_a_vendor, provider_b_vendor, + provider_a_endpoint_url_commitment, + provider_b_endpoint_url_commitment, + provider_a_endpoint_origin_commitment, + provider_b_endpoint_origin_commitment, + safe_head_observation_id, activation_block_evidence_id, + provisional_page_id, provisional_lineage_id, + dynamic_source_attestation_id, runtime_code_evidence_id, + dynamic_source_template_id, parent_candidate_id, + parent_occurrence_id, parent_block_number, parent_block_hash, + parent_block_global_log_index, parent_receipt_log_ordinal, + parent_transaction_hash, parent_transaction_index, + parent_source_address, parent_payload_hash, + parent_raw_log_commitment, launch_candidate_id, + launch_occurrence_id, launch_block_number, launch_block_hash, + launch_block_global_log_index, launch_receipt_log_ordinal, + launch_transaction_hash, hook_candidate_id, hook_occurrence_id, + hook_receipt_log_ordinal, source_address, pool_id, cto_authority, + ordered_beneficiaries, ordered_shares_bps, + allocation_hash, configuration_hash, active_configuration_hash, + artifact_creation_code_commitment, + deployed_artifact_creation_code_commitment, + constructor_arguments_commitment, local_init_code_hash, + create2_salt, predict_result_hash, activation_payload, + activation_commitment, staged_at + ) values ( + (item ->> 'activationId')::uuid, p_run_id, 1, 'classic-v3', + 'classic', 'core', p_projector_version, p_release_epoch_id, + p_release_pointer_generation, p_reorg_generation, + p_expected_cursor_generation, p_expected_cursor_block_hash, + p_envio_provider_deployment_id, p_provider_a_id, p_provider_b_id, + provider_a.identity, provider_b.identity, + provider_a.vendor, provider_b.vendor, + provider_a.endpoint_url_commitment, + provider_b.endpoint_url_commitment, + provider_a.endpoint_origin_commitment, + provider_b.endpoint_origin_commitment, + p_safe_head_observation_id, p_activation_block_evidence_id, + (item ->> 'provisionalPageId')::uuid, + (item ->> 'provisionalLineageId')::uuid, + (item ->> 'dynamicSourceAttestationId')::uuid, + (item ->> 'runtimeCodeEvidenceId')::uuid, + (item ->> 'dynamicSourceTemplateId')::uuid, + (item ->> 'parentCandidateId')::programmable_private.envio_candidate_identifier, + (item ->> 'parentOccurrenceId')::uuid, + (item ->> 'parentBlockNumber')::bigint, + pg_catalog.decode(pg_catalog.substring(item ->> 'parentBlockHash', 3), 'hex'), + (item ->> 'parentBlockGlobalLogIndex')::bigint, + (item ->> 'parentReceiptLogOrdinal')::bigint, + pg_catalog.decode(pg_catalog.substring(item ->> 'parentTransactionHash', 3), 'hex'), + (item ->> 'parentTransactionIndex')::bigint, + pg_catalog.decode(pg_catalog.substring(item ->> 'parentSourceAddress', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'parentPayloadHash', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'parentRawLogCommitment', 3), 'hex'), + (item ->> 'launchCandidateId')::programmable_private.envio_candidate_identifier, + (item ->> 'launchOccurrenceId')::uuid, + (item ->> 'launchBlockNumber')::bigint, + pg_catalog.decode(pg_catalog.substring(item ->> 'launchBlockHash', 3), 'hex'), + (item ->> 'launchBlockGlobalLogIndex')::bigint, + (item ->> 'launchReceiptLogOrdinal')::bigint, + pg_catalog.decode(pg_catalog.substring(item ->> 'launchTransactionHash', 3), 'hex'), + (item ->> 'hookCandidateId')::programmable_private.envio_candidate_identifier, + (item ->> 'hookOccurrenceId')::uuid, + (item ->> 'hookReceiptLogOrdinal')::bigint, + pg_catalog.decode(pg_catalog.substring(item ->> 'sourceAddress', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'poolId', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'ctoAuthority', 3), 'hex'), + allocation_accounts, allocation_shares, + pg_catalog.decode(pg_catalog.substring(item ->> 'allocationHash', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'configurationHash', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'activeConfigurationHash', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'artifactCreationCodeCommitment', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'deployedArtifactCreationCodeCommitment', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'constructorArgumentsCommitment', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'localInitCodeHash', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'create2Salt', 3), 'hex'), + pg_catalog.decode(pg_catalog.substring(item ->> 'predictResultHash', 3), 'hex'), + item, + pg_catalog.decode(pg_catalog.substring(item ->> 'activationCommitment', 3), 'hex'), + p_staged_at + ); + + for evidence_item in + select value + from pg_catalog.jsonb_array_elements(p_model_evidence) + where value ->> 'activationId' = item ->> 'activationId' + order by value ->> 'evidenceKind' + loop + insert into programmable_private.dynamic_source_activation_model_evidence ( + activation_id, evidence_ordinal, evidence_kind, + payload, evidence_commitment + ) values ( + (item ->> 'activationId')::uuid, + case evidence_item ->> 'evidenceKind' + when 'classic-v3-initial-reward-configuration-v1' then 1 + when 'classic-v3-launch-reward-conservation-v1' then 2 + else 3 + end, + (evidence_item ->> 'evidenceKind')::programmable_private.source_identifier, + evidence_item -> 'payload', + pg_catalog.decode( + pg_catalog.substring(evidence_item ->> 'evidenceCommitment', 3), + 'hex' + ) + ); + end loop; + inserted_count := inserted_count + 1; + end loop; + return inserted_count; +end +$function$; + +create function programmable_private.get_dynamic_activation_seed_requests_v1( + p_projection_run_id uuid, + p_target_block_number numeric, + p_target_block_hash bytea +) +returns table ( + activation_id uuid, + vault bytea, + ordered_beneficiaries bytea[], + ordered_shares_bps integer[], + allocation_hash bytea, + configuration_hash bytea, + active_configuration_hash bytea, + artifact_creation_code_commitment bytea, + constructor_arguments_commitment bytea, + local_init_code_hash bytea, + create2_salt bytea, + predict_result_hash bytea, + factory_occurrence_id uuid, + factory_transaction_hash bytea, + factory_receipt_log_ordinal bigint, + factory_block_hash bytea, + creation_block_number bigint, + creation_transaction_index bigint, + required_occurrences jsonb +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_open_projection_run_v1( + p_projection_run_id + ); + select * into header + from programmable_private.run_headers + where run_id = p_projection_run_id and run_kind = 'projection'; + if header.run_id is null + or header.chain_id <> 1 + or header.release_id <> 'classic-v3' + or header.model_id <> 'classic' + or header.source_group <> 'core' + or p_target_block_number <> pg_catalog.trunc(p_target_block_number) + or p_target_block_number < 0 + or pg_catalog.octet_length(p_target_block_hash) <> 32 + then + raise exception using + errcode = '22023', message = 'invalid activation seed request'; + end if; + + return query + select staged.activation_id, + staged.source_address::bytea, + staged.ordered_beneficiaries, + staged.ordered_shares_bps, + staged.allocation_hash::bytea, + staged.configuration_hash::bytea, + staged.active_configuration_hash::bytea, + staged.artifact_creation_code_commitment::bytea, + staged.constructor_arguments_commitment::bytea, + staged.local_init_code_hash::bytea, + staged.create2_salt::bytea, + staged.predict_result_hash::bytea, + factory.occurrence_id, + factory.transaction_hash::bytea, + factory.receipt_log_ordinal::bigint, + factory.block_hash::bytea, + factory.block_number::bigint, + factory.transaction_index::bigint, + pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object( + 'role', 'launcher', + 'occurrenceId', launcher.occurrence_id, + 'transactionHash', '0x' || pg_catalog.encode( + launcher.transaction_hash, 'hex' + ), + 'receiptLogOrdinal', launcher.receipt_log_ordinal::text, + 'blockHash', '0x' || pg_catalog.encode(launcher.block_hash, 'hex'), + 'contentFingerprint', '0x' || pg_catalog.encode( + launcher.content_fingerprint, 'hex' + ), + 'releaseBindingId', launcher.release_binding_id, + 'releaseBindingCommitment', '0x' || pg_catalog.encode( + launcher_binding.binding_commitment, 'hex' + ) + ), + pg_catalog.jsonb_build_object( + 'role', 'vault_factory', + 'occurrenceId', factory.occurrence_id, + 'transactionHash', '0x' || pg_catalog.encode( + factory.transaction_hash, 'hex' + ), + 'receiptLogOrdinal', factory.receipt_log_ordinal::text, + 'blockHash', '0x' || pg_catalog.encode(factory.block_hash, 'hex'), + 'contentFingerprint', '0x' || pg_catalog.encode( + factory.content_fingerprint, 'hex' + ), + 'releaseBindingId', factory.release_binding_id, + 'releaseBindingCommitment', '0x' || pg_catalog.encode( + factory_binding.binding_commitment, 'hex' + ) + ), + pg_catalog.jsonb_build_object( + 'role', 'hook', + 'occurrenceId', hook.occurrence_id, + 'transactionHash', '0x' || pg_catalog.encode( + hook.transaction_hash, 'hex' + ), + 'receiptLogOrdinal', hook.receipt_log_ordinal::text, + 'blockHash', '0x' || pg_catalog.encode(hook.block_hash, 'hex'), + 'contentFingerprint', '0x' || pg_catalog.encode( + hook.content_fingerprint, 'hex' + ), + 'releaseBindingId', hook.release_binding_id, + 'releaseBindingCommitment', '0x' || pg_catalog.encode( + hook_binding.binding_commitment, 'hex' + ) + ) + ) + from programmable_private.dynamic_source_activation_staging as staged + join programmable_private.chain_event_materialized_occurrences_v1 + as launcher + on launcher.occurrence_id = staged.launch_occurrence_id + and coalesce( + launcher.first_seen_neutral_candidate_id::text, + launcher.first_seen_envio_candidate_id::text + ) = staged.launch_candidate_id::text + and launcher.verification_run_id = p_projection_run_id + and launcher.epoch_id = header.epoch_id + and launcher.pointer_generation = header.captured_pointer_generation + and launcher.block_number = staged.launch_block_number + and launcher.block_hash = staged.launch_block_hash + join programmable_private.release_source_bindings as launcher_binding + on launcher_binding.binding_id = launcher.release_binding_id + and launcher_binding.epoch_id = header.epoch_id + and launcher_binding.source_role = 'launcher' + join programmable_private.chain_event_materialized_occurrences_v1 + as factory + on factory.occurrence_id = staged.parent_occurrence_id + and coalesce( + factory.first_seen_neutral_candidate_id::text, + factory.first_seen_envio_candidate_id::text + ) = staged.parent_candidate_id::text + and factory.verification_run_id = p_projection_run_id + and factory.epoch_id = header.epoch_id + and factory.pointer_generation = header.captured_pointer_generation + and factory.block_number = staged.parent_block_number + and factory.block_hash = staged.parent_block_hash + join programmable_private.release_source_bindings as factory_binding + on factory_binding.binding_id = factory.release_binding_id + and factory_binding.epoch_id = header.epoch_id + and factory_binding.source_role = 'vault_factory' + and factory_binding.artifact_creation_code_commitment = + staged.artifact_creation_code_commitment + join programmable_private.chain_event_materialized_occurrences_v1 + as hook + on hook.occurrence_id = staged.hook_occurrence_id + and coalesce( + hook.first_seen_neutral_candidate_id::text, + hook.first_seen_envio_candidate_id::text + ) = staged.hook_candidate_id::text + and hook.verification_run_id = p_projection_run_id + and hook.epoch_id = header.epoch_id + and hook.pointer_generation = header.captured_pointer_generation + and hook.block_number = staged.launch_block_number + and hook.block_hash = staged.launch_block_hash + join programmable_private.release_source_bindings as hook_binding + on hook_binding.binding_id = hook.release_binding_id + and hook_binding.epoch_id = header.epoch_id + and hook_binding.source_role = 'hook' + where staged.release_epoch_id = header.epoch_id + and staged.release_pointer_generation = + header.captured_pointer_generation + and staged.reorg_generation = + programmable_private.get_projector_reorg_generation_v1() + and staged.launch_block_number = p_target_block_number::bigint + and staged.launch_block_hash = p_target_block_hash + and staged.parent_block_hash = p_target_block_hash + and launcher.transaction_hash = staged.launch_transaction_hash + and launcher.receipt_log_ordinal = staged.launch_receipt_log_ordinal + and factory.transaction_hash = staged.parent_transaction_hash + and factory.receipt_log_ordinal = staged.parent_receipt_log_ordinal + and hook.receipt_log_ordinal = staged.hook_receipt_log_ordinal + and not exists ( + select 1 + from programmable_private.dynamic_source_activation_consumptions + as consumed + where consumed.activation_id = staged.activation_id + ) + order by staged.activation_id; +end +$function$; + +create function programmable_private.materialize_dynamic_activation_seed_v1( + p_projection_run_id uuid, + p_activation_id uuid, + p_allocation_fact_id uuid, + p_allocation_evidence_id uuid, + p_required_occurrence_ids uuid[], + p_required_occurrence_roles text[], + p_allocation_canonical_preimage bytea, + p_allocation_content_fingerprint bytea, + p_evidence_canonical_preimage bytea, + p_evidence_content_fingerprint bytea, + p_verified_at timestamptz +) +returns table ( + allocation_fact_id uuid, + allocation_evidence_id uuid +) +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + staged programmable_private.dynamic_source_activation_staging%rowtype; + factory programmable_private.chain_event_materialized_occurrences_v1%rowtype; + factory_binding programmable_private.release_source_bindings%rowtype; + required_id uuid; + required_role text; + required_ordinal integer := 0; + required_materialization programmable_private.chain_event_materialized_occurrences_v1%rowtype; + required_binding programmable_private.release_source_bindings%rowtype; + existing_fact programmable_private.reward_allocation_facts%rowtype; + existing_evidence programmable_private.reward_allocation_evidence%rowtype; + evidence_audit_id uuid; +begin + perform programmable_private.assert_caller('programmable_projector'); + perform programmable_private.assert_open_projection_run_v1( + p_projection_run_id + ); + perform programmable_private.assert_fingerprint_encoding( + 'allocation', 1, p_allocation_canonical_preimage, + p_allocation_content_fingerprint + ); + perform programmable_private.assert_fingerprint_encoding( + 'evidence', 1, p_evidence_canonical_preimage, + p_evidence_content_fingerprint + ); + select * into header + from programmable_private.run_headers + where run_id = p_projection_run_id and run_kind = 'projection'; + select * into staged + from programmable_private.dynamic_source_activation_staging + where activation_id = p_activation_id; + if header.run_id is null + or staged.activation_id is null + or header.chain_id <> staged.chain_id + or header.release_id <> staged.release_id + or header.model_id <> staged.model_id + or header.source_group <> staged.source_group + or header.epoch_id <> staged.release_epoch_id + or header.captured_pointer_generation <> + staged.release_pointer_generation + or staged.reorg_generation <> + programmable_private.get_projector_reorg_generation_v1() + or p_required_occurrence_roles is distinct from + array['launcher', 'vault_factory', 'hook']::text[] + or pg_catalog.cardinality(p_required_occurrence_ids) <> 3 + or p_verified_at is null + then + raise exception using + errcode = '23514', message = 'activation seed context changed'; + end if; + + select occurrence.* into factory + from programmable_private.chain_event_materialized_occurrences_v1 + as occurrence + where occurrence.occurrence_id = staged.parent_occurrence_id + and occurrence.verification_run_id = p_projection_run_id + and occurrence.epoch_id = header.epoch_id + and occurrence.pointer_generation = header.captured_pointer_generation + and occurrence.block_number = staged.parent_block_number + and occurrence.block_hash = staged.parent_block_hash + and occurrence.transaction_hash = staged.parent_transaction_hash + and occurrence.receipt_log_ordinal = staged.parent_receipt_log_ordinal; + select binding.* into factory_binding + from programmable_private.release_source_bindings as binding + where binding.binding_id = factory.release_binding_id + and binding.epoch_id = header.epoch_id + and binding.source_role = 'vault_factory' + and binding.artifact_creation_code_commitment = + staged.artifact_creation_code_commitment; + if factory.occurrence_id is null or factory_binding.binding_id is null then + raise exception using + errcode = '23514', message = 'activation factory materialization changed'; + end if; + + select * into existing_fact + from programmable_private.reward_allocation_facts + where allocation_fact_id = p_allocation_fact_id; + if found then + if existing_fact.verification_run_id <> p_projection_run_id + or existing_fact.factory_occurrence_id <> factory.occurrence_id + or existing_fact.vault <> staged.source_address + or existing_fact.content_fingerprint <> + p_allocation_content_fingerprint + or existing_fact.canonical_preimage <> + p_allocation_canonical_preimage + then + raise exception using + errcode = '23505', message = 'activation seed fact replay changed'; + end if; + else + insert into programmable_private.reward_allocation_facts ( + allocation_fact_id, chain_id, release_id, model_id, epoch_id, + pointer_generation, vault, factory_occurrence_id, + factory_release_binding_id, factory_release_binding_commitment, + factory_logical_event_id, factory_occurrence_block_hash, + creation_block_number, creation_transaction_index, + ordered_beneficiaries, ordered_shares_bps, allocation_hash, + configuration_hash, active_configuration_hash, + manifest_artifact_creation_code_commitment, encoding_version, + canonical_preimage, content_fingerprint, verification_run_id, + created_at + ) values ( + p_allocation_fact_id, header.chain_id, header.release_id, + header.model_id, header.epoch_id, + header.captured_pointer_generation, staged.source_address, + factory.occurrence_id, factory_binding.binding_id, + factory_binding.binding_commitment, factory.logical_event_id, + factory.block_hash, factory.block_number, factory.transaction_index, + staged.ordered_beneficiaries, staged.ordered_shares_bps, + staged.allocation_hash, staged.configuration_hash, + staged.active_configuration_hash, + staged.artifact_creation_code_commitment, 1, + p_allocation_canonical_preimage, p_allocation_content_fingerprint, + p_projection_run_id, p_verified_at + ); + perform programmable_private.append_mutation_audit( + 'dynamic_activation.reward_allocation_fact.append', + p_allocation_content_fingerprint, + p_projection_run_id, p_verified_at + ); + + for required_id, required_role in + select ids.id, roles.role + from pg_catalog.unnest(p_required_occurrence_ids) + with ordinality as ids(id, ordinal) + join pg_catalog.unnest(p_required_occurrence_roles) + with ordinality as roles(role, ordinal) using (ordinal) + order by ids.ordinal + loop + select occurrence.* into required_materialization + from programmable_private.chain_event_materialized_occurrences_v1 + as occurrence + where occurrence.occurrence_id = required_id + and occurrence.verification_run_id = p_projection_run_id + and occurrence.epoch_id = header.epoch_id + and occurrence.pointer_generation = + header.captured_pointer_generation + and occurrence.block_hash = staged.launch_block_hash; + select binding.* into required_binding + from programmable_private.release_source_bindings as binding + where binding.binding_id = required_materialization.release_binding_id + and binding.epoch_id = header.epoch_id + and binding.source_role = required_role; + if required_materialization.occurrence_id is null + or required_binding.binding_id is null + or required_id is distinct from (case required_role + when 'launcher' then staged.launch_occurrence_id + when 'vault_factory' then staged.parent_occurrence_id + else staged.hook_occurrence_id + end) + then + raise exception using + errcode = '23514', message = 'activation required occurrence changed'; + end if; + insert into programmable_private.reward_allocation_required_occurrences ( + allocation_fact_id, occurrence_ordinal, occurrence_role, + occurrence_id, release_binding_id, release_binding_commitment + ) values ( + p_allocation_fact_id, required_ordinal, + required_role::programmable_private.source_identifier, + required_id, required_binding.binding_id, + required_binding.binding_commitment + ); + required_ordinal := required_ordinal + 1; + end loop; + end if; + + select * into existing_evidence + from programmable_private.reward_allocation_evidence + where allocation_evidence_id = p_allocation_evidence_id; + if found then + if existing_evidence.allocation_fact_id <> p_allocation_fact_id + or existing_evidence.verification_run_id <> p_projection_run_id + or existing_evidence.content_fingerprint <> + p_evidence_content_fingerprint + or existing_evidence.canonical_preimage <> + p_evidence_canonical_preimage + then + raise exception using + errcode = '23505', message = 'activation seed evidence replay changed'; + end if; + else + evidence_audit_id := programmable_private.append_mutation_audit( + 'dynamic_activation.reward_allocation_evidence.append', + p_evidence_content_fingerprint, + p_projection_run_id, p_verified_at + ); + insert into programmable_private.reward_allocation_evidence ( + allocation_evidence_id, allocation_fact_id, factory_occurrence_id, + vault, recovery_method, evidence_version, + recovery_release_binding_id, + recovery_release_binding_commitment, top_level_destination, + method_selector, transaction_input_hash, + recomputed_allocation_hash, recomputed_configuration_hash, + recomputed_active_configuration_hash, is_recomputation_attested, + constructor_arguments_commitment, local_init_code_hash, + create2_salt, local_create2_address, + historical_enrichment_status, getter_block_hash, + getter_result_hash_a, getter_result_hash_b, + predict_result_hash_a, predict_result_hash_b, + predicted_vault_a, predicted_vault_b, + selected_rpc_result_hash_a, selected_rpc_result_hash_b, + selected_rpc_transaction_receipt_hash_a, + selected_rpc_transaction_receipt_hash_b, + encoding_version, canonical_preimage, content_fingerprint, + verification_run_id, verified_at, audit_id + ) values ( + p_allocation_evidence_id, p_allocation_fact_id, + factory.occurrence_id, staged.source_address, + 'historical_getters', 'classic-v3-activation-v1', + factory_binding.binding_id, factory_binding.binding_commitment, + null, null, null, staged.allocation_hash, + staged.configuration_hash, staged.active_configuration_hash, true, + staged.constructor_arguments_commitment, + staged.local_init_code_hash, staged.create2_salt, + staged.source_address, 'matched', factory.block_hash, + staged.active_configuration_hash, + staged.active_configuration_hash, + staged.predict_result_hash, staged.predict_result_hash, + staged.source_address, staged.source_address, + staged.configuration_hash, staged.configuration_hash, + null, null, 1, p_evidence_canonical_preimage, + p_evidence_content_fingerprint, p_projection_run_id, + p_verified_at, evidence_audit_id + ); + end if; + return query select p_allocation_fact_id, p_allocation_evidence_id; +end +$function$; + +create function programmable_private.consume_matching_dynamic_activations_v1( + p_final_run_id uuid, + p_publication_id uuid, + p_final_execution_evidence_id uuid, + p_consumed_at timestamptz +) +returns integer +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + publication programmable_private.projection_publications%rowtype; + execution + programmable_private.projection_provider_execution_evidence%rowtype; + staged programmable_private.dynamic_source_activation_staging%rowtype; + fact programmable_private.reward_allocation_facts%rowtype; + selected_evidence_id uuid; + consumed_count integer := 0; + expected_count integer := 0; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header + from programmable_private.run_headers + where run_id = p_final_run_id and run_kind = 'projection'; + select * into publication + from programmable_private.projection_publications + where publication_id = p_publication_id + and run_id = p_final_run_id + and published_at = p_consumed_at; + select * into execution + from programmable_private.projection_provider_execution_evidence + where execution_evidence_id = p_final_execution_evidence_id + and run_id = p_final_run_id; + if header.run_id is null + or publication.publication_id is null + or execution.execution_evidence_id is null + then + raise exception using + errcode = '23514', message = 'activation final evidence changed'; + end if; + + -- Count every immutable activation that targets this exact publication. + -- Starting from staged activations, rather than materialized reward facts, + -- makes a missing seed a hard promotion failure instead of an accidental + -- expected_count = 0 success. + select pg_catalog.count(*)::integer into expected_count + from programmable_private.dynamic_source_activation_staging as activation + where activation.release_epoch_id = header.epoch_id + and activation.release_pointer_generation = + header.captured_pointer_generation + and activation.reorg_generation = + programmable_private.get_projector_reorg_generation_v1() + and activation.launch_block_number = publication.target_block_number + and activation.launch_block_hash = publication.target_block_hash + and activation.parent_block_number = publication.target_block_number + and activation.parent_block_hash = publication.target_block_hash; + + for staged in + select activation.* + from programmable_private.dynamic_source_activation_staging + as activation + left join programmable_private.dynamic_source_activation_consumptions + as consumed + on consumed.activation_id = activation.activation_id + where activation.release_epoch_id = header.epoch_id + and activation.release_pointer_generation = + header.captured_pointer_generation + and activation.reorg_generation = + programmable_private.get_projector_reorg_generation_v1() + and activation.launch_block_number = publication.target_block_number + and activation.launch_block_hash = publication.target_block_hash + and activation.parent_block_number = publication.target_block_number + and activation.parent_block_hash = publication.target_block_hash + and consumed.activation_id is null + order by activation.activation_id + loop + select * into fact + from programmable_private.reward_allocation_facts + where verification_run_id = p_final_run_id + and factory_occurrence_id = staged.parent_occurrence_id + and vault = staged.source_address + and allocation_hash = staged.allocation_hash + and configuration_hash = staged.configuration_hash; + select current_seed.allocation_evidence_id into selected_evidence_id + from programmable_private.reward_allocation_current_verified + as current_seed + where current_seed.allocation_fact_id = fact.allocation_fact_id; + if staged.reorg_generation <> + programmable_private.get_projector_reorg_generation_v1() + or staged.envio_provider_deployment_id <> + execution.envio_provider_deployment_id + or staged.provider_a_id <> execution.provider_a_id + or staged.provider_b_id <> execution.provider_b_id + or staged.provider_a_identity <> execution.provider_a_identity + or staged.provider_b_identity <> execution.provider_b_identity + or staged.provider_a_vendor <> execution.provider_a_vendor + or staged.provider_b_vendor <> execution.provider_b_vendor + or staged.provider_a_endpoint_url_commitment <> + execution.provider_a_endpoint_url_commitment + or staged.provider_b_endpoint_url_commitment <> + execution.provider_b_endpoint_url_commitment + or staged.provider_a_endpoint_origin_commitment <> + execution.provider_a_endpoint_origin_commitment + or staged.provider_b_endpoint_origin_commitment <> + execution.provider_b_endpoint_origin_commitment + or fact.allocation_fact_id is null + or selected_evidence_id is null + or not exists ( + select 1 + from programmable_private.reward_allocation_evidence as evidence + where evidence.verification_run_id = p_final_run_id + and evidence.allocation_fact_id = fact.allocation_fact_id + and evidence.allocation_evidence_id = selected_evidence_id + and evidence.is_recomputation_attested + ) + or exists ( + select 1 + from programmable_private.reward_allocation_required_occurrences + as required + join programmable_private.chain_event_occurrences as occurrence + on occurrence.occurrence_id = required.occurrence_id + left join programmable_private.chain_event_current_canonical + as canonical + on canonical.occurrence_id = occurrence.occurrence_id + and canonical.logical_event_id = occurrence.logical_event_id + and canonical.block_hash = occurrence.block_hash + where required.allocation_fact_id = fact.allocation_fact_id + and ( + canonical.occurrence_id is null + or occurrence.block_hash <> staged.launch_block_hash + ) + ) + or ( + select pg_catalog.count(*) + from programmable_private.dynamic_source_activation_model_evidence + as model_evidence + where model_evidence.activation_id = staged.activation_id + ) <> 3 + then + raise exception using + errcode = '23514', message = 'dynamic activation is not promotion eligible'; + end if; + insert into programmable_private.dynamic_source_activation_consumptions ( + activation_id, final_run_id, publication_id, + final_execution_evidence_id, allocation_fact_id, + allocation_evidence_id, consumed_at + ) values ( + staged.activation_id, p_final_run_id, p_publication_id, + p_final_execution_evidence_id, fact.allocation_fact_id, + selected_evidence_id, p_consumed_at + ); + consumed_count := consumed_count + 1; + end loop; + + select pg_catalog.count(*)::integer into consumed_count + from programmable_private.dynamic_source_activation_consumptions + as consumed + join programmable_private.dynamic_source_activation_staging as activation + on activation.activation_id = consumed.activation_id + join programmable_private.reward_allocation_facts as selected_fact + on selected_fact.allocation_fact_id = consumed.allocation_fact_id + and selected_fact.verification_run_id = p_final_run_id + and selected_fact.factory_occurrence_id = activation.parent_occurrence_id + and selected_fact.vault = activation.source_address + and selected_fact.allocation_hash = activation.allocation_hash + and selected_fact.configuration_hash = activation.configuration_hash + join programmable_private.reward_allocation_current_verified as current_seed + on current_seed.allocation_fact_id = selected_fact.allocation_fact_id + where consumed.final_run_id = p_final_run_id + and consumed.publication_id = p_publication_id + and consumed.final_execution_evidence_id = + p_final_execution_evidence_id + and activation.release_epoch_id = header.epoch_id + and activation.release_pointer_generation = + header.captured_pointer_generation + and activation.reorg_generation = + programmable_private.get_projector_reorg_generation_v1() + and activation.launch_block_number = publication.target_block_number + and activation.launch_block_hash = publication.target_block_hash + and activation.parent_block_number = publication.target_block_number + and activation.parent_block_hash = publication.target_block_hash; + if consumed_count <> expected_count then + raise exception using + errcode = '23514', + message = 'dynamic activation consumption is incomplete'; + end if; + return consumed_count; +end +$function$; + +-- A noncanonical seed is eligible only in the exact projection run that +-- materialized its occurrences. Canonical seeds remain reusable. This closes +-- the same-height replacement-fork gap for aborted pre-promotion runs. +do $seed_selector_hardening$ +declare + function_definition text; + hardened_definition text; + factory_needle text := + E'canonical.logical_event_id is null\n or ('; + factory_replacement text := + E'(\n canonical.logical_event_id is null\n' + || E' and factory_occurrence.verification_run_id = header.run_id\n' + || E' )\n or ('; + required_needle text := + E'required_canonical.logical_event_id is null\n or ('; + required_replacement text := + E'(\n required_canonical.logical_event_id is null\n' + || E' and required_occurrence.verification_run_id = header.run_id\n' + || E' )\n or ('; +begin + select pg_catalog.pg_get_functiondef(procedure.oid) + into function_definition + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_projector_verified_reward_seed_v1' + and pg_catalog.pg_get_function_identity_arguments(procedure.oid) + = 'p_projection_run_id uuid, p_vault bytea'; + if function_definition is null then + raise exception 'reward seed selector is unavailable'; + end if; + if pg_catalog.length(function_definition) + - pg_catalog.length(pg_catalog.replace( + function_definition, factory_needle, '' + )) <> pg_catalog.length(factory_needle) + or pg_catalog.length(function_definition) + - pg_catalog.length(pg_catalog.replace( + function_definition, required_needle, '' + )) <> pg_catalog.length(required_needle) + then + raise exception 'reward seed selector hardening is not exact'; + end if; + hardened_definition := pg_catalog.replace( + function_definition, factory_needle, factory_replacement + ); + hardened_definition := pg_catalog.replace( + hardened_definition, required_needle, required_replacement + ); + if hardened_definition = function_definition + or pg_catalog.length(hardened_definition) + - pg_catalog.length(pg_catalog.replace( + hardened_definition, factory_replacement, '' + )) <> pg_catalog.length(factory_replacement) + or pg_catalog.length(hardened_definition) + - pg_catalog.length(pg_catalog.replace( + hardened_definition, required_replacement, '' + )) <> pg_catalog.length(required_replacement) + or pg_catalog.strpos(hardened_definition, factory_needle) <> 0 + or pg_catalog.strpos(hardened_definition, required_needle) <> 0 + then + raise exception 'reward seed selector hardening did not match'; + end if; + execute hardened_definition; +end +$seed_selector_hardening$; + +-- Preserve the existing promotion implementation and append consumption only +-- after promote_projection_run_v2 has selected every required occurrence and +-- reward seed as canonical. +do $promotion_extension$ +declare + function_definition text; + extended_definition text; + needle text := + E' perform programmable_private.consume_matching_provisional_sources_v1(\n' + || E' p_run_id, publication_id, p_execution_evidence_id,\n' + || E' p_target_block_evidence_id, p_occurrence_ids, p_published_at\n' + || E' );\n' + || ' return publication_id;'; + replacement text := + E' perform programmable_private.consume_matching_provisional_sources_v1(\n' + || E' p_run_id, publication_id, p_execution_evidence_id,\n' + || E' p_target_block_evidence_id, p_occurrence_ids, p_published_at\n' + || E' );\n' + || E' perform programmable_private.consume_matching_dynamic_activations_v1(\n' + || E' p_run_id, publication_id, p_execution_evidence_id, p_published_at\n' + || E' );\n' + || ' return publication_id;'; +begin + select pg_catalog.pg_get_functiondef(procedure.oid) + into function_definition + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'promote_projection_run_v3' + and pg_catalog.pg_get_function_identity_arguments(procedure.oid) = + 'p_promotion_mode text, p_publication_id uuid, p_checkpoint_id uuid, p_outcome_id uuid, p_run_id uuid, p_projector_version text, p_lease_generation bigint, p_lease_token_hash bytea, p_expected_checkpoint_generation bigint, p_next_checkpoint_generation bigint, p_reorg_generation bigint, p_safe_head_observation_id uuid, p_target_block_evidence_id uuid, p_target_block_number numeric, p_target_block_hash bytea, p_cursor_block_global_log_index numeric, p_cursor_candidate_id text, p_occurrence_ids uuid[], p_allocation_fact_ids uuid[], p_allocation_evidence_ids uuid[], p_candidate_disposition_ids uuid[], p_route_keys text[], p_result_commitment bytea, p_execution_evidence_id uuid, p_reward_snapshot_evidence_ids uuid[], p_provider_binding_id uuid, p_provider_binding_commitment bytea, p_published_at timestamp with time zone'; + if function_definition is null then + raise exception 'projection promotion v3 is unavailable'; + end if; + if pg_catalog.length(function_definition) + - pg_catalog.length(pg_catalog.replace( + function_definition, needle, '' + )) <> pg_catalog.length(needle) + then + raise exception 'projection promotion extension is not exact'; + end if; + extended_definition := pg_catalog.replace( + function_definition, needle, replacement + ); + if extended_definition = function_definition + or pg_catalog.length(extended_definition) + - pg_catalog.length(pg_catalog.replace( + extended_definition, replacement, '' + )) <> pg_catalog.length(replacement) + or pg_catalog.strpos(extended_definition, needle) <> 0 + then + raise exception 'projection promotion extension did not match'; + end if; + execute extended_definition; +end +$promotion_extension$; + +revoke all on table + programmable_private.provisional_dynamic_parent_receipt_ordinals, + programmable_private.dynamic_source_activation_staging, + programmable_private.dynamic_source_activation_model_evidence, + programmable_private.dynamic_source_activation_consumptions +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +revoke all on function + programmable_private.stage_provisional_parent_receipt_ordinals_v1( + uuid, uuid, text[], numeric[], timestamptz + ), + programmable_private.resolve_pending_dynamic_source_activations_v1( + text, bigint, bytea, bigint + ), + programmable_private.stage_verified_dynamic_source_activations_v1( + uuid, text, uuid, bigint, bigint, bigint, bytea, uuid, uuid, uuid, + uuid, uuid, jsonb, jsonb, timestamptz + ), + programmable_private.get_dynamic_activation_seed_requests_v1( + uuid, numeric, bytea + ), + programmable_private.materialize_dynamic_activation_seed_v1( + uuid, uuid, uuid, uuid, uuid[], text[], bytea, bytea, bytea, bytea, + timestamptz + ), + programmable_private.consume_matching_dynamic_activations_v1( + uuid, uuid, uuid, timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance; + +grant execute on function + programmable_private.stage_provisional_parent_receipt_ordinals_v1( + uuid, uuid, text[], numeric[], timestamptz + ), + programmable_private.resolve_pending_dynamic_source_activations_v1( + text, bigint, bytea, bigint + ), + programmable_private.stage_verified_dynamic_source_activations_v1( + uuid, text, uuid, bigint, bigint, bigint, bytea, uuid, uuid, uuid, + uuid, uuid, jsonb, jsonb, timestamptz + ), + programmable_private.get_dynamic_activation_seed_requests_v1( + uuid, numeric, bytea + ), + programmable_private.materialize_dynamic_activation_seed_v1( + uuid, uuid, uuid, uuid, uuid[], text[], bytea, bytea, bytea, bytea, + timestamptz + ) +to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801090000_bootstrap_dynamic_evidence_and_launch_requirements.sql b/supabase/migrations/20260801090000_bootstrap_dynamic_evidence_and_launch_requirements.sql new file mode 100644 index 00000000..a3902874 --- /dev/null +++ b/supabase/migrations/20260801090000_bootstrap_dynamic_evidence_and_launch_requirements.sql @@ -0,0 +1,563 @@ +-- Separate projection-writer authorization from launch-completeness evidence, +-- and permit runtime-observed immutable values whose meaning is authenticated +-- later by the reward-allocation evidence path. Deferred values never make a +-- launch publishable by themselves. + +do $bootstrap_operator$ +begin + if not exists ( + select 1 from pg_catalog.pg_roles where rolname = 'programmable_operator' + ) then + create role programmable_operator + nologin nosuperuser nocreatedb nocreaterole noinherit + noreplication nobypassrls; + end if; +end +$bootstrap_operator$; + +alter role programmable_operator + nologin nocreatedb nocreaterole noinherit; + +do $posture$ +begin + if exists ( + select 1 + from pg_catalog.pg_roles + where rolname = 'programmable_operator' + and (rolsuper or rolreplication or rolbypassrls) + ) then + raise exception 'programmable operator role posture is privileged'; + end if; +end +$posture$; + +grant programmable_operator to postgres with inherit false, set true; + +set role programmable_migrator; + +create table programmable_private.candidate_database_control ( + singleton boolean primary key default true check (singleton), + database_mode programmable_private.source_identifier not null + check (database_mode = 'candidate-only'), + envio_provider_deployment_id uuid not null unique + references programmable_private.provider_deployments(provider_deployment_id) + on delete restrict, + envio_deployment_commitment programmable_private.bytes32_value not null, + envio_schema_commitment programmable_private.bytes32_value not null, + initialization_input_commitment programmable_private.bytes32_value not null, + initialized_at timestamptz not null, + promotion_attestation_commitment programmable_private.bytes32_value, + promotion_baseline_commitment programmable_private.bytes32_value, + promotion_parity_commitment programmable_private.bytes32_value, + promotion_input_commitment programmable_private.bytes32_value, + promoted_at timestamptz, + check ( + ( + promoted_at is null + and promotion_attestation_commitment is null + and promotion_baseline_commitment is null + and promotion_parity_commitment is null + and promotion_input_commitment is null + ) + or ( + promoted_at is not null + and promotion_attestation_commitment is not null + and promotion_baseline_commitment is not null + and promotion_parity_commitment is not null + and promotion_input_commitment is not null + ) + ) +); + +alter table programmable_private.candidate_database_control + enable row level security; +alter table programmable_private.candidate_database_control + force row level security; +create policy candidate_database_control_migrator_all + on programmable_private.candidate_database_control + for all to programmable_migrator using (true) with check (true); + +create function programmable_private.initialize_candidate_database( + p_envio_provider_deployment_id uuid, + p_envio_deployment_commitment bytea, + p_envio_schema_commitment bytea, + p_input_commitment bytea, + p_initialized_at timestamptz +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + provider programmable_private.provider_deployments%rowtype; + existing programmable_private.candidate_database_control%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into provider + from programmable_private.provider_deployments + where provider_deployment_id = p_envio_provider_deployment_id; + if provider.provider_deployment_id is null + or provider.provider_type <> 'envio_deployment' + or provider.redacted_identity <> 'envio:production-7f24e63' + or provider.deployment_commitment <> p_envio_deployment_commitment + or provider.schema_commitment <> p_envio_schema_commitment + or pg_catalog.octet_length(p_input_commitment) <> 32 + then + raise exception using errcode = '23514', message = 'candidate database provider evidence mismatch'; + end if; + select * into existing + from programmable_private.candidate_database_control + where singleton; + if found then + if existing.envio_provider_deployment_id <> p_envio_provider_deployment_id + or existing.envio_deployment_commitment <> p_envio_deployment_commitment + or existing.envio_schema_commitment <> p_envio_schema_commitment + or existing.initialization_input_commitment <> p_input_commitment + or existing.initialized_at <> p_initialized_at + then + raise exception using errcode = '23505', message = 'candidate database initialization replay conflict'; + end if; + return false; + end if; + insert into programmable_private.candidate_database_control ( + singleton, database_mode, envio_provider_deployment_id, + envio_deployment_commitment, envio_schema_commitment, + initialization_input_commitment, initialized_at + ) values ( + true, 'candidate-only', p_envio_provider_deployment_id, + p_envio_deployment_commitment, p_envio_schema_commitment, + p_input_commitment, p_initialized_at + ); + return true; +end +$function$; + +create function programmable_private.attest_candidate_database_promotion( + p_expected_envio_provider_deployment_id uuid, + p_baseline_commitment bytea, + p_parity_commitment bytea, + p_promotion_attestation_commitment bytea, + p_input_commitment bytea, + p_promoted_at timestamptz +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + existing programmable_private.candidate_database_control%rowtype; + zero_bytes bytea := pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex'); +begin + perform programmable_private.assert_caller('programmable_operator'); + select * into existing + from programmable_private.candidate_database_control + where singleton + for update; + if not found + or existing.envio_provider_deployment_id <> + p_expected_envio_provider_deployment_id + or pg_catalog.octet_length(p_baseline_commitment) <> 32 + or pg_catalog.octet_length(p_parity_commitment) <> 32 + or pg_catalog.octet_length(p_promotion_attestation_commitment) <> 32 + or pg_catalog.octet_length(p_input_commitment) <> 32 + or p_baseline_commitment = zero_bytes + or p_parity_commitment = zero_bytes + or p_promotion_attestation_commitment = zero_bytes + or p_input_commitment = zero_bytes + then + raise exception using errcode = '23514', message = 'candidate promotion evidence is incomplete'; + end if; + if existing.promoted_at is not null then + if existing.promotion_baseline_commitment <> p_baseline_commitment + or existing.promotion_parity_commitment <> p_parity_commitment + or existing.promotion_attestation_commitment <> + p_promotion_attestation_commitment + or existing.promotion_input_commitment <> p_input_commitment + or existing.promoted_at <> p_promoted_at + then + raise exception using errcode = '23505', message = 'candidate promotion replay conflict'; + end if; + return false; + end if; + update programmable_private.candidate_database_control + set promotion_baseline_commitment = p_baseline_commitment, + promotion_parity_commitment = p_parity_commitment, + promotion_attestation_commitment = p_promotion_attestation_commitment, + promotion_input_commitment = p_input_commitment, + promoted_at = p_promoted_at + where singleton and promoted_at is null; + return true; +end +$function$; + +create function programmable_private.enforce_candidate_database_promotion() +returns trigger +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +begin + if exists ( + select 1 + from programmable_private.candidate_database_control + where singleton + ) and not exists ( + select 1 + from programmable_private.candidate_database_control + where singleton and promoted_at is not null + ) then + raise exception using errcode = '55000', message = 'candidate database has not been promoted'; + end if; + return new; +end +$function$; + +create trigger projection_publication_candidate_promotion_gate +before insert on programmable_private.projection_publications +for each row execute function + programmable_private.enforce_candidate_database_promotion(); + +create or replace function programmable_private.valid_immutable_binding_spec( + p_spec jsonb +) +returns boolean +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +declare + binding jsonb; + binding_count integer; + ordinal integer := 0; + binding_offset integer; + binding_length integer; + previous_end integer := 0; + source_kind text; + encoding_kind text; + field_name text; + constant_value text; + evidence_role text; + configuration_field text; + deferred_configuration_count integer := 0; + deferred_beneficiary_count integer := 0; +begin + if pg_catalog.jsonb_typeof(p_spec) <> 'object' + or pg_catalog.octet_length(p_spec::text) > 65536 + or pg_catalog.jsonb_typeof(p_spec -> 'bindings') <> 'array' + or not (p_spec ? 'factoryConfigurationField') + or pg_catalog.jsonb_typeof(p_spec -> 'factoryConfigurationField') + not in ('string', 'null') + then + return false; + end if; + configuration_field := p_spec ->> 'factoryConfigurationField'; + if configuration_field is not null + and configuration_field !~ '^[A-Za-z][A-Za-z0-9_]{0,63}$' + then + return false; + end if; + binding_count := pg_catalog.jsonb_array_length(p_spec -> 'bindings'); + if binding_count < 1 or binding_count > 64 then + return false; + end if; + for binding in + select value from pg_catalog.jsonb_array_elements(p_spec -> 'bindings') + loop + if pg_catalog.jsonb_typeof(binding) <> 'object' + or coalesce(binding ->> 'ordinal', '') !~ '^(0|[1-9][0-9]*)$' + or coalesce(binding ->> 'offset', '') !~ '^(0|[1-9][0-9]*)$' + or coalesce(binding ->> 'length', '') !~ '^[1-9][0-9]*$' + then + return false; + end if; + if (binding ->> 'ordinal')::integer <> ordinal then + return false; + end if; + binding_offset := (binding ->> 'offset')::integer; + binding_length := (binding ->> 'length')::integer; + source_kind := binding ->> 'source'; + encoding_kind := binding ->> 'encoding'; + field_name := binding ->> 'field'; + constant_value := binding ->> 'value'; + evidence_role := binding ->> 'evidenceRole'; + if binding_length > 32 + or binding_offset < previous_end + or source_kind not in ( + 'factory_event', 'constant', 'deployed_address', + 'deferred_allocation_evidence' + ) + or encoding_kind not in ('address', 'bytes') + or (encoding_kind = 'address' and binding_length not in (20, 32)) + or ( + source_kind = 'factory_event' + and ( + field_name is null + or field_name !~ '^[A-Za-z][A-Za-z0-9_]{0,63}$' + or constant_value is not null + or evidence_role is not null + ) + ) + or ( + source_kind = 'constant' + and ( + field_name is not null + or constant_value is null + or constant_value !~ '^0x([0-9a-f][0-9a-f])+$' + or pg_catalog.length(constant_value) <> 2 + (2 * binding_length) + or evidence_role is not null + ) + ) + or ( + source_kind = 'deployed_address' + and ( + field_name is not null or constant_value is not null + or evidence_role is not null or encoding_kind <> 'address' + ) + ) + or ( + source_kind = 'deferred_allocation_evidence' + and ( + field_name is not null + or constant_value is not null + or encoding_kind <> 'bytes' + or binding_length <> 32 + or evidence_role not in ('configuration_hash', 'beneficiary_count') + ) + ) + then + return false; + end if; + if source_kind = 'deferred_allocation_evidence' + and evidence_role = 'configuration_hash' + then + deferred_configuration_count := deferred_configuration_count + 1; + elsif source_kind = 'deferred_allocation_evidence' + and evidence_role = 'beneficiary_count' + then + deferred_beneficiary_count := deferred_beneficiary_count + 1; + end if; + previous_end := binding_offset + binding_length; + ordinal := ordinal + 1; + end loop; + if configuration_field is null then + return deferred_configuration_count = 1 + and deferred_beneficiary_count >= 1; + end if; + return deferred_configuration_count = 0; +exception when others then + return false; +end +$function$; + +create or replace function programmable_private.immutable_values_match_binding_spec( + p_spec jsonb, + p_factory_payload jsonb, + p_deployed_address bytea, + p_values bytea[] +) +returns boolean +language plpgsql +immutable +strict +security invoker +set search_path = '' +as $function$ +declare + binding jsonb; + ordinal integer := 1; + binding_length integer; + source_kind text; + encoding_kind text; + source_value text; + expected_value bytea; +begin + if not programmable_private.valid_immutable_binding_spec(p_spec) + or pg_catalog.octet_length(p_deployed_address) <> 20 + or pg_catalog.cardinality(p_values) + <> pg_catalog.jsonb_array_length(p_spec -> 'bindings') + or exists ( + select 1 from pg_catalog.unnest(p_values) as value + where value is null + ) + then + return false; + end if; + for binding in + select value from pg_catalog.jsonb_array_elements(p_spec -> 'bindings') + loop + binding_length := (binding ->> 'length')::integer; + source_kind := binding ->> 'source'; + encoding_kind := binding ->> 'encoding'; + if pg_catalog.octet_length(p_values[ordinal]) <> binding_length then + return false; + end if; + if source_kind = 'deferred_allocation_evidence' then + -- The immutable is observed and committed during runtime attestation. + -- Its semantic meaning is authenticated by the separate verified + -- reward-allocation path before publication. + expected_value := p_values[ordinal]; + elsif source_kind = 'constant' then + expected_value := pg_catalog.decode( + pg_catalog.substring(binding ->> 'value', 3), 'hex' + ); + elsif source_kind = 'deployed_address' then + expected_value := case + when binding_length = 20 then p_deployed_address + else pg_catalog.decode(pg_catalog.repeat('00', 12), 'hex') + || p_deployed_address + end; + else + source_value := p_factory_payload ->> (binding ->> 'field'); + if encoding_kind = 'address' then + if source_value is null or source_value !~ '^0x[0-9a-f]{40}$' then + return false; + end if; + expected_value := case + when binding_length = 20 then pg_catalog.decode( + pg_catalog.substring(source_value, 3), 'hex' + ) + else pg_catalog.decode(pg_catalog.repeat('00', 12), 'hex') + || pg_catalog.decode(pg_catalog.substring(source_value, 3), 'hex') + end; + else + if source_value is null + or source_value !~ '^0x([0-9a-f][0-9a-f])+$' + or pg_catalog.length(source_value) <> 2 + (2 * binding_length) + then + return false; + end if; + expected_value := pg_catalog.decode( + pg_catalog.substring(source_value, 3), 'hex' + ); + end if; + end if; + if p_values[ordinal] <> expected_value then + return false; + end if; + ordinal := ordinal + 1; + end loop; + return true; +exception when others then + return false; +end +$function$; + +create or replace function programmable_private.stage_launch_occurrence_role( + p_launch_projection_id uuid, + p_occurrence_role text, + p_occurrence_id uuid, + p_staged_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + launch programmable_private.launch_projections%rowtype; + materialization programmable_private.chain_event_occurrence_materializations%rowtype; + actual_role text; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into launch from programmable_private.launch_projections + where launch_projection_id = p_launch_projection_id; + if not found then + raise exception using errcode = '23503', message = 'unknown staged launch'; + end if; + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_occurrence_id + and chain_id = launch.chain_id + and release_id = launch.release_id + and model_id = launch.model_id + and epoch_id = launch.epoch_id + and pointer_generation = launch.pointer_generation; + if materialization.materialization_id is null then + raise exception using errcode = '23503', message = 'launch requirement materialization is outside the launch scope'; + end if; + perform programmable_private.projection_stage_context( + launch.projection_run_id, p_occurrence_id, + launch.promoted_block_number, launch.promoted_block_hash + ); + select coalesce(binding.source_role, dynamic_source.deployed_source_role) + into actual_role + from programmable_private.chain_event_occurrence_materializations as selected + left join programmable_private.release_source_bindings as binding + on binding.binding_id = selected.release_binding_id + left join programmable_private.dynamic_source_attestations as dynamic_source + on dynamic_source.dynamic_source_attestation_id = + selected.dynamic_source_attestation_id + where selected.materialization_id = materialization.materialization_id; + if actual_role <> p_occurrence_role + or not exists ( + select 1 + from programmable_private.release_launch_completeness_requirements + where epoch_id = launch.epoch_id + and occurrence_role = p_occurrence_role + and event_type = materialization.event_type + ) + then + raise exception using errcode = '23514', message = 'occurrence does not satisfy a launch requirement'; + end if; + insert into programmable_private.launch_projection_occurrence_roles ( + launch_projection_id, occurrence_role, occurrence_id, + projection_run_id, staged_at + ) values ( + p_launch_projection_id, + p_occurrence_role::programmable_private.source_identifier, + p_occurrence_id, launch.projection_run_id, p_staged_at + ) on conflict (launch_projection_id, occurrence_role) do update + set occurrence_id = excluded.occurrence_id, + staged_at = excluded.staged_at + where programmable_private.launch_projection_occurrence_roles.occurrence_id + = excluded.occurrence_id; + if not found then + raise exception using errcode = '23505', message = 'launch occurrence role replay conflict'; + end if; + return p_launch_projection_id; +end +$function$; + +revoke all on function programmable_private.stage_launch_occurrence_role( + uuid, text, uuid, timestamptz +) from public; +revoke all on function programmable_private.enforce_candidate_database_promotion() + from public, anon, authenticated, service_role; + +-- The promotion operator is deliberately narrower than every runtime role: +-- schema lookup plus one function, with no direct relation or sequence access. +revoke all on schema programmable_private from programmable_operator; +revoke all on all tables in schema programmable_private + from programmable_operator; +revoke all on all sequences in schema programmable_private + from programmable_operator; +revoke all on all functions in schema programmable_private + from programmable_operator; +grant usage on schema programmable_private to programmable_operator; + +grant execute on function programmable_private.stage_launch_occurrence_role( + uuid, text, uuid, timestamptz +) to programmable_projector; + +revoke all on function programmable_private.initialize_candidate_database( + uuid, bytea, bytea, bytea, timestamptz +) from public; +grant execute on function programmable_private.initialize_candidate_database( + uuid, bytea, bytea, bytea, timestamptz +) to programmable_projector; +revoke all on function programmable_private.attest_candidate_database_promotion( + uuid, bytea, bytea, bytea, bytea, timestamptz +) from public; +grant execute on function programmable_private.attest_candidate_database_promotion( + uuid, bytea, bytea, bytea, bytea, timestamptz +) to programmable_operator; + +reset role; diff --git a/supabase/migrations/20260801091000_candidate_projector_unpromoted_gate.sql b/supabase/migrations/20260801091000_candidate_projector_unpromoted_gate.sql new file mode 100644 index 00000000..61f9e65f --- /dev/null +++ b/supabase/migrations/20260801091000_candidate_projector_unpromoted_gate.sql @@ -0,0 +1,102 @@ +-- Candidate backfill must stop once the isolated database has been promoted. +-- This read-only gate binds the projector to the exact reviewed candidate and +-- rejects missing, mixed, or promoted database control state. + +set role programmable_migrator; + +-- The operator role is created by the bootstrap migration after the original +-- schema grant closure. USAGE is required to invoke its single promotion +-- attestation function; no table privilege is added. +grant usage on schema programmable_private to programmable_operator; + +create function programmable_private.verify_candidate_database_unpromoted_v1( + p_envio_provider_deployment_id uuid, + p_envio_deployment_commitment bytea, + p_envio_schema_commitment bytea, + p_initialization_input_commitment bytea, + p_initialized_at timestamptz +) +returns boolean +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + control programmable_private.candidate_database_control%rowtype; + provider programmable_private.provider_deployments%rowtype; + envio_provider_count bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + + select * into control + from programmable_private.candidate_database_control + where singleton; + + select * into provider + from programmable_private.provider_deployments + where provider_deployment_id = p_envio_provider_deployment_id; + + select pg_catalog.count(*) into envio_provider_count + from programmable_private.provider_deployments + where provider_type = 'envio_deployment'; + + if control.singleton is null + or control.database_mode is distinct from 'candidate-only' + or control.envio_provider_deployment_id is distinct from + p_envio_provider_deployment_id + or control.envio_deployment_commitment is distinct from + p_envio_deployment_commitment + or control.envio_schema_commitment is distinct from + p_envio_schema_commitment + or control.initialization_input_commitment is distinct from + p_initialization_input_commitment + or control.initialized_at is distinct from p_initialized_at + or control.promotion_attestation_commitment is not null + or control.promotion_baseline_commitment is not null + or control.promotion_parity_commitment is not null + or control.promotion_input_commitment is not null + or control.promoted_at is not null + or provider.provider_deployment_id is null + or provider.provider_type is distinct from 'envio_deployment' + or provider.redacted_identity is distinct from + 'envio:production-7f24e63' + or provider.deployment_commitment is distinct from + p_envio_deployment_commitment + or provider.schema_commitment is distinct from + p_envio_schema_commitment + or provider.created_at is distinct from p_initialized_at + or envio_provider_count is distinct from 1 + then + raise exception using + errcode = '55000', + message = 'candidate database is not in the exact unpromoted state'; + end if; + + return true; +end +$function$; + +comment on function + programmable_private.verify_candidate_database_unpromoted_v1( + uuid, bytea, bytea, bytea, timestamptz + ) is + 'Read-only candidate projector gate. It accepts only the exact isolated and unpromoted Envio candidate database.'; + +revoke all on function + programmable_private.verify_candidate_database_unpromoted_v1( + uuid, bytea, bytea, bytea, timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_operator; + +grant execute on function + programmable_private.verify_candidate_database_unpromoted_v1( + uuid, bytea, bytea, bytea, timestamptz + ) +to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801092000_verify_candidate_database_promoted.sql b/supabase/migrations/20260801092000_verify_candidate_database_promoted.sql new file mode 100644 index 00000000..a6527b21 --- /dev/null +++ b/supabase/migrations/20260801092000_verify_candidate_database_promoted.sql @@ -0,0 +1,127 @@ +-- Canonical release mode may consume the promoted Envio candidate only when +-- the database records the exact reviewed bootstrap and promotion evidence. +-- This verifier is read-only and intentionally unavailable to every role +-- except the projection writer capability. + +set role programmable_migrator; + +create function programmable_private.verify_candidate_database_promoted_v1( + p_envio_provider_deployment_id uuid, + p_envio_deployment_commitment bytea, + p_envio_schema_commitment bytea, + p_initialization_input_commitment bytea, + p_initialized_at timestamptz, + p_promotion_baseline_commitment bytea, + p_promotion_parity_commitment bytea, + p_promotion_attestation_commitment bytea, + p_promotion_input_commitment bytea, + p_promoted_at timestamptz +) +returns boolean +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + control programmable_private.candidate_database_control%rowtype; + provider programmable_private.provider_deployments%rowtype; + envio_provider_count bigint; + zero_bytes bytea := pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex'); +begin + perform programmable_private.assert_caller('programmable_projector'); + + select * into control + from programmable_private.candidate_database_control + where singleton; + + select * into provider + from programmable_private.provider_deployments + where provider_deployment_id = p_envio_provider_deployment_id; + + select pg_catalog.count(*) into envio_provider_count + from programmable_private.provider_deployments + where provider_type = 'envio_deployment'; + + if pg_catalog.octet_length(p_envio_deployment_commitment) is distinct from 32 + or pg_catalog.octet_length(p_envio_schema_commitment) is distinct from 32 + or pg_catalog.octet_length(p_initialization_input_commitment) is distinct from 32 + or pg_catalog.octet_length(p_promotion_baseline_commitment) is distinct from 32 + or pg_catalog.octet_length(p_promotion_parity_commitment) is distinct from 32 + or pg_catalog.octet_length(p_promotion_attestation_commitment) is distinct from 32 + or pg_catalog.octet_length(p_promotion_input_commitment) is distinct from 32 + or p_envio_deployment_commitment = zero_bytes + or p_envio_schema_commitment = zero_bytes + or p_initialization_input_commitment = zero_bytes + or p_promotion_baseline_commitment = zero_bytes + or p_promotion_parity_commitment = zero_bytes + or p_promotion_attestation_commitment = zero_bytes + or p_promotion_input_commitment = zero_bytes + or p_initialized_at is null + or p_promoted_at is null + or control.singleton is null + or control.database_mode is distinct from 'candidate-only' + or control.envio_provider_deployment_id is distinct from + p_envio_provider_deployment_id + or control.envio_deployment_commitment is distinct from + p_envio_deployment_commitment + or control.envio_schema_commitment is distinct from + p_envio_schema_commitment + or control.initialization_input_commitment is distinct from + p_initialization_input_commitment + or control.initialized_at is distinct from p_initialized_at + or control.promotion_baseline_commitment is distinct from + p_promotion_baseline_commitment + or control.promotion_parity_commitment is distinct from + p_promotion_parity_commitment + or control.promotion_attestation_commitment is distinct from + p_promotion_attestation_commitment + or control.promotion_input_commitment is distinct from + p_promotion_input_commitment + or control.promoted_at is distinct from p_promoted_at + or provider.provider_deployment_id is null + or provider.provider_type is distinct from 'envio_deployment' + or provider.redacted_identity is distinct from + 'envio:production-7f24e63' + or provider.deployment_commitment is distinct from + p_envio_deployment_commitment + or provider.schema_commitment is distinct from + p_envio_schema_commitment + or provider.created_at is distinct from p_initialized_at + or envio_provider_count is distinct from 1 + then + raise exception using + errcode = '55000', + message = 'candidate database is not in the exact promoted state'; + end if; + + return true; +end +$function$; + +comment on function + programmable_private.verify_candidate_database_promoted_v1( + uuid, bytea, bytea, bytea, timestamptz, + bytea, bytea, bytea, bytea, timestamptz + ) is + 'Read-only canonical projector gate. It accepts only the exact isolated and promoted Envio candidate database.'; + +revoke all on function + programmable_private.verify_candidate_database_promoted_v1( + uuid, bytea, bytea, bytea, timestamptz, + bytea, bytea, bytea, bytea, timestamptz + ) +from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_operator; + +grant execute on function + programmable_private.verify_candidate_database_promoted_v1( + uuid, bytea, bytea, bytea, timestamptz, + bytea, bytea, bytea, bytea, timestamptz + ) +to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801093000_bind_candidate_promotion_to_product.sql b/supabase/migrations/20260801093000_bind_candidate_promotion_to_product.sql new file mode 100644 index 00000000..de345d65 --- /dev/null +++ b/supabase/migrations/20260801093000_bind_candidate_promotion_to_product.sql @@ -0,0 +1,305 @@ +-- Bind candidate promotion to the immutable product artifact that was staged +-- and reviewed before cutover. Runtime release mode supplies only its platform +-- Git commit and deployment ID; private promotion evidence stays in the DB. + +set role programmable_migrator; + +alter table programmable_private.candidate_database_control + add column product_commit text, + add column staged_deployment_id text; + +alter table programmable_private.candidate_database_control + add constraint candidate_database_control_product_binding + check ( + ( + promoted_at is null + and product_commit is null + and staged_deployment_id is null + ) + or ( + promoted_at is not null + and product_commit ~ '^[0-9a-f]{40}$' + and product_commit <> pg_catalog.repeat('0', 40) + and staged_deployment_id ~ '^dpl_[A-Za-z0-9]{20,128}$' + ) + ) not valid; + +alter table programmable_private.candidate_database_control + validate constraint candidate_database_control_product_binding; + +-- Keep the old signature fail-closed so an older operator cannot produce a +-- promoted row without binding it to one immutable Vercel artifact. +create or replace function programmable_private.attest_candidate_database_promotion( + p_expected_envio_provider_deployment_id uuid, + p_baseline_commitment bytea, + p_parity_commitment bytea, + p_promotion_attestation_commitment bytea, + p_input_commitment bytea, + p_promoted_at timestamptz +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_operator'); + raise exception using + errcode = '55000', + message = 'product-bound candidate promotion is required'; +end +$function$; + +revoke all on function programmable_private.attest_candidate_database_promotion( + uuid, bytea, bytea, bytea, bytea, timestamptz +) +from public, anon, authenticated, service_role, + programmable_projector, programmable_projector_runtime, + programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_operator; + +create function programmable_private.attest_candidate_database_promotion( + p_expected_envio_provider_deployment_id uuid, + p_baseline_commitment bytea, + p_parity_commitment bytea, + p_promotion_attestation_commitment bytea, + p_input_commitment bytea, + p_product_commit text, + p_staged_deployment_id text, + p_promoted_at timestamptz +) +returns boolean +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + existing programmable_private.candidate_database_control%rowtype; + zero_bytes bytea := pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex'); + affected bigint; +begin + perform programmable_private.assert_caller('programmable_operator'); + + select * into existing + from programmable_private.candidate_database_control + where singleton + for update; + + if not found + or existing.envio_provider_deployment_id is distinct from + p_expected_envio_provider_deployment_id + or pg_catalog.octet_length(p_baseline_commitment) is distinct from 32 + or pg_catalog.octet_length(p_parity_commitment) is distinct from 32 + or pg_catalog.octet_length(p_promotion_attestation_commitment) is distinct from 32 + or pg_catalog.octet_length(p_input_commitment) is distinct from 32 + or p_baseline_commitment = zero_bytes + or p_parity_commitment = zero_bytes + or p_promotion_attestation_commitment = zero_bytes + or p_input_commitment = zero_bytes + or p_product_commit is null + or p_product_commit !~ '^[0-9a-f]{40}$' + or p_product_commit = pg_catalog.repeat('0', 40) + or p_staged_deployment_id is null + or p_staged_deployment_id !~ '^dpl_[A-Za-z0-9]{20,128}$' + or p_promoted_at is null + or p_promoted_at <= existing.initialized_at + then + raise exception using + errcode = '23514', + message = 'candidate product-bound promotion evidence is incomplete'; + end if; + + if existing.promoted_at is not null then + if existing.promotion_baseline_commitment is distinct from + p_baseline_commitment + or existing.promotion_parity_commitment is distinct from + p_parity_commitment + or existing.promotion_attestation_commitment is distinct from + p_promotion_attestation_commitment + or existing.promotion_input_commitment is distinct from + p_input_commitment + or existing.product_commit is distinct from p_product_commit + or existing.staged_deployment_id is distinct from + p_staged_deployment_id + or existing.promoted_at is distinct from p_promoted_at + then + raise exception using + errcode = '23505', + message = 'candidate product-bound promotion replay conflict'; + end if; + return false; + end if; + + update programmable_private.candidate_database_control + set promotion_baseline_commitment = p_baseline_commitment, + promotion_parity_commitment = p_parity_commitment, + promotion_attestation_commitment = + p_promotion_attestation_commitment, + promotion_input_commitment = p_input_commitment, + product_commit = p_product_commit, + staged_deployment_id = p_staged_deployment_id, + promoted_at = p_promoted_at + where singleton + and promoted_at is null + and product_commit is null + and staged_deployment_id is null; + + get diagnostics affected = row_count; + if affected <> 1 then + raise exception using + errcode = '40001', + message = 'candidate product-bound promotion CAS lost'; + end if; + return true; +end +$function$; + +comment on function programmable_private.attest_candidate_database_promotion( + uuid, bytea, bytea, bytea, bytea, text, text, timestamptz +) is + 'Atomically binds private promotion evidence to one immutable product commit and staged Vercel deployment.'; + +revoke all on function programmable_private.attest_candidate_database_promotion( + uuid, bytea, bytea, bytea, bytea, text, text, timestamptz +) +from public, anon, authenticated, service_role, + programmable_projector, programmable_projector_runtime, + programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance; + +grant execute on function programmable_private.attest_candidate_database_promotion( + uuid, bytea, bytea, bytea, bytea, text, text, timestamptz +) +to programmable_operator; + +-- Replace the post-deployment env-based gate with a product-bound verifier. +revoke all on function programmable_private.verify_candidate_database_promoted_v1( + uuid, bytea, bytea, bytea, timestamptz, + bytea, bytea, bytea, bytea, timestamptz +) +from public, anon, authenticated, service_role, + programmable_projector, programmable_projector_runtime, + programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_operator; + +create function programmable_private.verify_candidate_database_promoted_v2( + p_envio_provider_deployment_id uuid, + p_envio_deployment_commitment bytea, + p_envio_schema_commitment bytea, + p_initialization_input_commitment bytea, + p_initialized_at timestamptz, + p_product_commit text, + p_staged_deployment_id text +) +returns boolean +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + control programmable_private.candidate_database_control%rowtype; + provider programmable_private.provider_deployments%rowtype; + envio_provider_count bigint; + zero_bytes bytea := pg_catalog.decode(pg_catalog.repeat('00', 32), 'hex'); +begin + perform programmable_private.assert_caller('programmable_projector'); + + select * into control + from programmable_private.candidate_database_control + where singleton; + + select * into provider + from programmable_private.provider_deployments + where provider_deployment_id = p_envio_provider_deployment_id; + + select pg_catalog.count(*) into envio_provider_count + from programmable_private.provider_deployments + where provider_type = 'envio_deployment'; + + if pg_catalog.octet_length(p_envio_deployment_commitment) is distinct from 32 + or pg_catalog.octet_length(p_envio_schema_commitment) is distinct from 32 + or pg_catalog.octet_length(p_initialization_input_commitment) is distinct from 32 + or p_envio_deployment_commitment = zero_bytes + or p_envio_schema_commitment = zero_bytes + or p_initialization_input_commitment = zero_bytes + or p_initialized_at is null + or p_product_commit is null + or p_product_commit !~ '^[0-9a-f]{40}$' + or p_product_commit = pg_catalog.repeat('0', 40) + or p_staged_deployment_id is null + or p_staged_deployment_id !~ '^dpl_[A-Za-z0-9]{20,128}$' + or control.singleton is null + or control.database_mode is distinct from 'candidate-only' + or control.envio_provider_deployment_id is distinct from + p_envio_provider_deployment_id + or control.envio_deployment_commitment is distinct from + p_envio_deployment_commitment + or control.envio_schema_commitment is distinct from + p_envio_schema_commitment + or control.initialization_input_commitment is distinct from + p_initialization_input_commitment + or control.initialized_at is distinct from p_initialized_at + or pg_catalog.octet_length(control.promotion_baseline_commitment) + is distinct from 32 + or pg_catalog.octet_length(control.promotion_parity_commitment) + is distinct from 32 + or pg_catalog.octet_length(control.promotion_attestation_commitment) + is distinct from 32 + or pg_catalog.octet_length(control.promotion_input_commitment) + is distinct from 32 + or control.promotion_baseline_commitment = zero_bytes + or control.promotion_parity_commitment = zero_bytes + or control.promotion_attestation_commitment = zero_bytes + or control.promotion_input_commitment = zero_bytes + or control.product_commit is distinct from p_product_commit + or control.staged_deployment_id is distinct from + p_staged_deployment_id + or control.promoted_at is null + or provider.provider_deployment_id is null + or provider.provider_type is distinct from 'envio_deployment' + or provider.redacted_identity is distinct from + 'envio:production-7f24e63' + or provider.deployment_commitment is distinct from + p_envio_deployment_commitment + or provider.schema_commitment is distinct from + p_envio_schema_commitment + or provider.created_at is distinct from p_initialized_at + or envio_provider_count is distinct from 1 + then + raise exception using + errcode = '55000', + message = 'candidate database is not bound to this promoted product'; + end if; + + return true; +end +$function$; + +comment on function programmable_private.verify_candidate_database_promoted_v2( + uuid, bytea, bytea, bytea, timestamptz, text, text +) is + 'Read-only release gate binding complete private promotion evidence to the executing immutable product artifact.'; + +revoke all on function programmable_private.verify_candidate_database_promoted_v2( + uuid, bytea, bytea, bytea, timestamptz, text, text +) +from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_operator; + +grant execute on function programmable_private.verify_candidate_database_promoted_v2( + uuid, bytea, bytea, bytea, timestamptz, text, text +) +to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801094000_hosted_bootstrap_projector_membership.sql b/supabase/migrations/20260801094000_hosted_bootstrap_projector_membership.sql new file mode 100644 index 00000000..823eb41d --- /dev/null +++ b/supabase/migrations/20260801094000_hosted_bootstrap_projector_membership.sql @@ -0,0 +1,18 @@ +-- Supabase's project postgres role is intentionally not a superuser. Give the +-- reviewed bootstrap operator only the SET capability required to execute the +-- projector-owned bootstrap functions. Runtime logins remain separate. + +grant programmable_projector to postgres with inherit false, set true; + +do $bootstrap_projector_membership$ +begin + if not pg_catalog.pg_has_role( + 'postgres', + 'programmable_projector', + 'set' + ) then + raise exception 'bootstrap connection cannot set programmable_projector'; + end if; +end +$bootstrap_projector_membership$; + diff --git a/supabase/migrations/20260801100439_projector_genesis_cursor_reader.sql b/supabase/migrations/20260801100439_projector_genesis_cursor_reader.sql new file mode 100644 index 00000000..eb201563 --- /dev/null +++ b/supabase/migrations/20260801100439_projector_genesis_cursor_reader.sql @@ -0,0 +1,80 @@ +-- The source projector registers a dual-RPC-attested predecessor block before +-- its first page. Until generation one exists, expose that immutable genesis +-- point as the generation-zero cursor. A completely uninitialized scope keeps +-- the all-NULL sentinel so the runtime cannot start without that evidence. +set role programmable_migrator; + +create or replace function programmable_private.get_envio_ingestion_cursor_v1( + p_chain_id bigint, + p_provider_deployment_id uuid, + p_stream_id text +) +returns table ( + generation bigint, + block_number bigint, + block_hash bytea, + block_global_log_index bigint, + candidate_id text +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_chain_id <> 1 + or p_stream_id is null + or pg_catalog.octet_length(p_stream_id) not between 1 and 128 + or p_stream_id !~ '^[A-Za-z0-9][A-Za-z0-9._:/-]*$' + or not exists ( + select 1 + from programmable_private.provider_deployments + where provider_deployment_id = p_provider_deployment_id + and provider_type = 'envio_deployment' + ) + then + raise exception using errcode = '22023', message = 'invalid Envio cursor scope'; + end if; + + return query + select current_cursor.generation, + current_cursor.block_number::bigint, + current_cursor.block_hash::bytea, + current_cursor.block_global_log_index::bigint, + current_cursor.candidate_id::text + from programmable_private.envio_ingestion_cursor_current as current_cursor + where current_cursor.chain_id = p_chain_id + and current_cursor.provider_deployment_id = p_provider_deployment_id + and current_cursor.stream_id = p_stream_id; + if found then + return; + end if; + + return query + select 0::bigint, + genesis.anchor_block_number::bigint, + genesis.anchor_block_hash::bytea, + null::bigint, + null::text + from programmable_private.envio_ingestion_cursor_genesis_points as genesis + where genesis.chain_id = p_chain_id + and genesis.provider_deployment_id = p_provider_deployment_id + and genesis.stream_id = p_stream_id; + if found then + return; + end if; + + return query + select 0::bigint, null::bigint, null::bytea, null::bigint, null::text; +end +$function$; + +revoke all on function programmable_private.get_envio_ingestion_cursor_v1( + bigint, uuid, text +) from public; +grant execute on function programmable_private.get_envio_ingestion_cursor_v1( + bigint, uuid, text +) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801104022_projector_current_generation_columns.sql b/supabase/migrations/20260801104022_projector_current_generation_columns.sql new file mode 100644 index 00000000..9bb74bbb --- /dev/null +++ b/supabase/migrations/20260801104022_projector_current_generation_columns.sql @@ -0,0 +1,65 @@ +-- release_epoch_current exposes its CAS counter as generation. The provisional +-- lineage reader was created against a nonexistent pointer_generation column, +-- so the first real projector plan failed before reading any candidates. +set role programmable_migrator; + +do $migration$ +declare + function_definition text; + corrected_definition text; +begin + select pg_catalog.pg_get_functiondef( + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure + ) into strict function_definition; + + if pg_catalog.strpos( + function_definition, + 'current_release.pointer_generation = page.release_pointer_generation' + ) = 0 + or pg_catalog.strpos( + function_definition, + 'current_ingestion.pointer_generation =' + ) = 0 + then + raise exception using + errcode = '55000', + message = 'unexpected provisional source generation predicates'; + end if; + + corrected_definition := pg_catalog.replace( + pg_catalog.replace( + function_definition, + 'current_release.pointer_generation = page.release_pointer_generation', + 'current_release.generation = page.release_pointer_generation' + ), + 'current_ingestion.pointer_generation =', + 'current_ingestion.generation =' + ); + + if corrected_definition = function_definition + or pg_catalog.strpos( + corrected_definition, + 'current_release.pointer_generation' + ) > 0 + or pg_catalog.strpos( + corrected_definition, + 'current_ingestion.pointer_generation' + ) > 0 + then + raise exception using + errcode = '55000', + message = 'provisional source generation repair was incomplete'; + end if; + + execute corrected_definition; +end +$migration$; + +revoke all on function programmable_private.get_current_provisional_dynamic_sources_v1( + text +) from public; +grant execute on function programmable_private.get_current_provisional_dynamic_sources_v1( + text +) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801125441_reuse_safe_head_observations.sql b/supabase/migrations/20260801125441_reuse_safe_head_observations.sql new file mode 100644 index 00000000..fdf6e3d6 --- /dev/null +++ b/supabase/migrations/20260801125441_reuse_safe_head_observations.sql @@ -0,0 +1,131 @@ +-- Multiple exact Envio pages can be verified beneath one unchanged dual-RPC +-- safe head. Preserve one immutable safe-head row per content fingerprint and +-- let later projector runs reuse it after an exact field-by-field comparison. + +set role programmable_migrator; + +create function programmable_private.append_or_reuse_safe_head_observation_v1( + p_observation_id uuid, + p_run_id uuid, + p_provider_a_id uuid, + p_provider_b_id uuid, + p_reported_chain_id_a bigint, + p_reported_chain_id_b bigint, + p_head_a numeric, + p_head_b numeric, + p_finality_depth bigint, + p_safe_block_number numeric, + p_safe_block_hash_a bytea, + p_safe_block_hash_b bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_observed_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + duplicate_constraint text; + header programmable_private.run_headers%rowtype; + existing programmable_private.safe_head_observations%rowtype; +begin + perform programmable_private.assert_caller('programmable_projector'); + + begin + return programmable_private.append_safe_head_observation( + p_observation_id, + p_run_id, + p_provider_a_id, + p_provider_b_id, + p_reported_chain_id_a, + p_reported_chain_id_b, + p_head_a, + p_head_b, + p_finality_depth, + p_safe_block_number, + p_safe_block_hash_a, + p_safe_block_hash_b, + p_encoding_version, + p_canonical_preimage, + p_content_fingerprint, + p_observed_at + ); + exception + when unique_violation then + get stacked diagnostics duplicate_constraint = constraint_name; + if duplicate_constraint <> + 'safe_head_observations_epoch_id_content_fingerprint_key' + then + raise; + end if; + end; + + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection', 'rewind') + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid projector run'; + end if; + + select * into existing + from programmable_private.safe_head_observations + where epoch_id = header.epoch_id + and content_fingerprint = + p_content_fingerprint::programmable_private.bytes32_value + for key share; + if not found then + raise exception using + errcode = '23505', + message = 'safe-head fingerprint conflict has no reusable observation'; + end if; + + if existing.chain_id <> header.chain_id + or existing.release_id <> header.release_id + or existing.model_id <> header.model_id + or existing.source_group <> header.source_group + or existing.pointer_generation <> header.captured_pointer_generation + or existing.provider_a_id <> p_provider_a_id + or existing.provider_b_id <> p_provider_b_id + or existing.reported_chain_id_a <> p_reported_chain_id_a + or existing.reported_chain_id_b <> p_reported_chain_id_b + or existing.head_a <> p_head_a + or existing.head_b <> p_head_b + or existing.finality_depth <> p_finality_depth + or existing.safe_block_number <> p_safe_block_number + or existing.safe_block_hash_a <> p_safe_block_hash_a + or existing.safe_block_hash_b <> p_safe_block_hash_b + or existing.agreed_safe_block_hash <> p_safe_block_hash_a + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + then + raise exception using + errcode = '23505', + message = 'safe-head fingerprint replay conflicts with stored evidence'; + end if; + + return existing.observation_id; +end +$function$; + +revoke all on function programmable_private.append_or_reuse_safe_head_observation_v1( + uuid, uuid, uuid, uuid, bigint, bigint, numeric, numeric, bigint, numeric, + bytea, bytea, smallint, bytea, bytea, timestamptz +) from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_operator; + +grant execute on function programmable_private.append_or_reuse_safe_head_observation_v1( + uuid, uuid, uuid, uuid, bigint, bigint, numeric, numeric, bigint, numeric, + bytea, bytea, smallint, bytea, bytea, timestamptz +) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801144403_accept_uuid_v8_dynamic_source_lineage.sql b/supabase/migrations/20260801144403_accept_uuid_v8_dynamic_source_lineage.sql new file mode 100644 index 00000000..fb9b6a91 --- /dev/null +++ b/supabase/migrations/20260801144403_accept_uuid_v8_dynamic_source_lineage.sql @@ -0,0 +1,440 @@ +-- Projector-owned deterministic identifiers use RFC 9562 UUID version 8. +-- The provisional-lineage writer previously admitted only versions 1-5 even +-- though every identifier is otherwise a valid PostgreSQL UUID. Keep the +-- existing audited function body and widen only its four version checks. +set role programmable_migrator; + +do $migration$ +declare + function_signature constant text := + 'programmable_private.stage_verified_dynamic_parents_v2(' || + 'uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,' || + 'uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],' || + 'jsonb,bytea,jsonb,jsonb,timestamp with time zone)'; + function_oid regprocedure; + original_definition text; + updated_definition text; +begin + function_oid := pg_catalog.to_regprocedure(function_signature); + if function_oid is null then + raise exception using + errcode = '42704', + message = 'dynamic parent staging function is missing'; + end if; + + original_definition := pg_catalog.pg_get_functiondef(function_oid); + updated_definition := pg_catalog.replace( + original_definition, + '[1-5][0-9a-f]{3}', + '[1-58][0-9a-f]{3}' + ); + if updated_definition = original_definition then + raise exception using + errcode = '55000', + message = 'dynamic parent UUID validation shape changed'; + end if; + + execute updated_definition; +end +$migration$; + +-- A verified activation can be staged one ingestion generation before its +-- release projection materializes the permanent attestation. Expose only the +-- activation boundary attached to a still-current provisional lineage so the +-- next ingestion page can authenticate reward-vault events after the launch +-- log without trusting an address alone. +create function programmable_private.get_current_provisional_activation_boundaries_v1( + p_projector_version text +) +returns table ( + provisional_lineage_id uuid, + dynamic_source_attestation_id uuid, + deployed_source_address bytea, + activation_candidate_id text, + activation_occurrence_id uuid, + activation_block_number bigint, + activation_block_hash bytea, + activation_block_global_log_index bigint +) +language plpgsql +stable +security definer +set search_path = '' +as $function$ +begin + perform programmable_private.assert_caller('programmable_projector'); + if p_projector_version is null then + raise exception using + errcode = '22023', message = 'invalid projector version'; + end if; + + return query + select current_source.provisional_lineage_id, + activation.dynamic_source_attestation_id, + activation.source_address::bytea, + activation.launch_candidate_id::text, + activation.launch_occurrence_id, + activation.launch_block_number::bigint, + activation.launch_block_hash::bytea, + activation.launch_block_global_log_index::bigint + from programmable_private.get_current_provisional_dynamic_sources_v1( + p_projector_version + ) as current_source + join programmable_private.dynamic_source_activation_staging as activation + on activation.provisional_lineage_id = + current_source.provisional_lineage_id + and activation.provisional_page_id = current_source.provisional_page_id + and activation.dynamic_source_attestation_id = + current_source.dynamic_source_attestation_id + and activation.dynamic_source_template_id = + current_source.dynamic_source_template_id + and activation.runtime_code_evidence_id = + current_source.runtime_code_evidence_id + and activation.source_address = + current_source.deployed_source_address + and activation.projector_version = p_projector_version + and activation.reorg_generation = current_source.reorg_generation + where not exists ( + select 1 + from programmable_private.dynamic_source_activation_consumptions + as consumed + where consumed.activation_id = activation.activation_id + ) + order by current_source.provisional_lineage_id; +end +$function$; + +revoke all on function + programmable_private.get_current_provisional_activation_boundaries_v1(text) +from public, anon, authenticated, service_role, + programmable_projector, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_operator; + +grant execute on function + programmable_private.get_current_provisional_activation_boundaries_v1(text) +to programmable_projector; + +-- Supabase restores private tables under the database owner. Keep this +-- SECURITY DEFINER reader owned by the same role so it can read the two +-- private staging tables without granting table access to the projector. +reset role; + +do $migration$ +declare + table_owner name; +begin + select pg_catalog.pg_get_userbyid(table_class.relowner) + into table_owner + from pg_catalog.pg_class as table_class + join pg_catalog.pg_namespace as table_namespace + on table_namespace.oid = table_class.relnamespace + where table_namespace.nspname = 'programmable_private' + and table_class.relname = 'dynamic_source_activation_staging' + and table_class.relkind in ('r', 'p'); + + if table_owner is null then + raise exception using + errcode = '42704', + message = 'dynamic source activation staging table is missing'; + end if; + + execute pg_catalog.format( + 'alter function programmable_private.' || + 'get_current_provisional_activation_boundaries_v1(text) owner to %I', + table_owner + ); +end +$migration$; + +set role programmable_migrator; + +comment on function programmable_private.stage_verified_dynamic_parents_v2( + uuid, uuid, text, text, text, text, uuid, bigint, bigint, bigint, bytea, + uuid, text, uuid, uuid, uuid, uuid, numeric, bytea, bytea, bytea[], bytea[], + jsonb, bytea, jsonb, jsonb, timestamptz +) is + 'Stages dual-RPC verified dynamic parents; accepts deterministic RFC 9562 UUIDv8 lineage identifiers.'; + +-- Multi-array unnest is FROM-clause syntax, not a two-argument function in +-- pg_catalog. Removing the schema qualification keeps the arrays zipped by +-- ordinal while remaining inside the function's empty search_path. +do $migration$ +declare + function_signature constant text := + 'programmable_private.stage_provisional_parent_receipt_ordinals_v1(' || + 'uuid,uuid,text[],numeric[],timestamp with time zone)'; + function_oid regprocedure; + original_definition text; + updated_definition text; +begin + function_oid := pg_catalog.to_regprocedure(function_signature); + if function_oid is null then + raise exception using + errcode = '42704', + message = 'provisional receipt ordinal function is missing'; + end if; + + original_definition := pg_catalog.pg_get_functiondef(function_oid); + updated_definition := pg_catalog.replace( + original_definition, + 'pg_catalog.unnest(p_candidate_ids, p_receipt_log_ordinals)', + 'unnest(p_candidate_ids, p_receipt_log_ordinals)' + ); + if updated_definition = original_definition then + raise exception using + errcode = '55000', + message = 'provisional receipt ordinal function shape changed'; + end if; + + execute updated_definition; +end +$migration$; + +-- Keep a staged lineage readable after the raw-ingestion cursor advances past +-- its factory page. The activation itself is already dual-RPC verified and is +-- invalidated by any projector reorg generation change; requiring the old +-- cursor generation made the lineage disappear immediately after commit. +do $migration$ +declare + function_signature constant text := + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'; + function_oid regprocedure; + original_definition text; + updated_definition text; + needle constant text := + ' and cursor.generation = page.expected_cursor_generation' || + pg_catalog.chr(10) || + ' and cursor.block_hash = page.expected_cursor_block_hash'; + replacement constant text := + ' and (' || pg_catalog.chr(10) || + ' (cursor.generation = page.expected_cursor_generation' || + pg_catalog.chr(10) || + ' and cursor.block_hash = page.expected_cursor_block_hash)' || + pg_catalog.chr(10) || + ' or (' || pg_catalog.chr(10) || + ' cursor.generation > page.expected_cursor_generation' || + pg_catalog.chr(10) || + ' and (' || pg_catalog.chr(10) || + ' cursor.block_number < page.snapshot_block_number' || + pg_catalog.chr(10) || + ' or exists (' || pg_catalog.chr(10) || + ' select 1' || pg_catalog.chr(10) || + ' from programmable_private.dynamic_source_activation_staging' || + pg_catalog.chr(10) || + ' as activation' || pg_catalog.chr(10) || + ' where activation.provisional_page_id =' || + pg_catalog.chr(10) || + ' page.provisional_page_id' || pg_catalog.chr(10) || + ' and activation.provisional_lineage_id =' || + pg_catalog.chr(10) || + ' lineage.provisional_lineage_id' || pg_catalog.chr(10) || + ' and activation.reorg_generation = page.reorg_generation' || + pg_catalog.chr(10) || + ' and (' || pg_catalog.chr(10) || + ' cursor.block_number > activation.launch_block_number' || + pg_catalog.chr(10) || + ' or (' || pg_catalog.chr(10) || + ' cursor.block_number = activation.launch_block_number' || + pg_catalog.chr(10) || + ' and cursor.block_hash = activation.launch_block_hash' || + pg_catalog.chr(10) || + ' and cursor.block_global_log_index >=' || + pg_catalog.chr(10) || + ' activation.launch_block_global_log_index' || + pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' and not exists (' || pg_catalog.chr(10) || + ' select 1' || pg_catalog.chr(10) || + ' from programmable_private.' || + 'dynamic_source_activation_consumptions as consumed_activation' || + pg_catalog.chr(10) || + ' where consumed_activation.activation_id =' || + pg_catalog.chr(10) || + ' activation.activation_id' || pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' )'; +begin + function_oid := pg_catalog.to_regprocedure(function_signature); + if function_oid is null then + raise exception using + errcode = '42704', + message = 'current provisional source reader is missing'; + end if; + + original_definition := pg_catalog.pg_get_functiondef(function_oid); + updated_definition := pg_catalog.replace( + original_definition, needle, replacement + ); + if updated_definition = original_definition then + raise exception using + errcode = '55000', + message = 'current provisional source cursor shape changed'; + end if; + + execute updated_definition; +end +$migration$; + +-- Resolution is fenced by the current ingestion cursor, while an individual +-- provisional page may have been verified under an earlier cursor generation +-- for a still-future block. +do $migration$ +declare + function_signature constant text := + 'programmable_private.resolve_pending_dynamic_source_activations_v1(' || + 'text,bigint,bytea,bigint)'; + function_oid regprocedure; + original_definition text; + updated_definition text; + needle constant text := + ' join programmable_private.provisional_dynamic_parent_pages as page' || + pg_catalog.chr(10) || + ' on page.provisional_page_id = source.provisional_page_id' || + pg_catalog.chr(10) || + ' and page.expected_cursor_generation = p_expected_cursor_generation' || + pg_catalog.chr(10) || + ' and page.expected_cursor_block_hash = p_expected_cursor_block_hash' || + pg_catalog.chr(10) || + ' and page.reorg_generation = p_expected_reorg_generation'; + replacement constant text := + ' join programmable_private.provisional_dynamic_parent_pages as page' || + pg_catalog.chr(10) || + ' on page.provisional_page_id = source.provisional_page_id' || + pg_catalog.chr(10) || + ' and page.reorg_generation = p_expected_reorg_generation' || + pg_catalog.chr(10) || + ' join programmable_private.envio_ingestion_cursor_current as cursor' || + pg_catalog.chr(10) || + ' on cursor.chain_id = page.chain_id' || pg_catalog.chr(10) || + ' and cursor.provider_deployment_id =' || pg_catalog.chr(10) || + ' page.envio_provider_deployment_id' || pg_catalog.chr(10) || + ' and cursor.stream_id = page.stream_id' || pg_catalog.chr(10) || + ' and cursor.generation = p_expected_cursor_generation' || + pg_catalog.chr(10) || + ' and cursor.block_hash = p_expected_cursor_block_hash'; +begin + function_oid := pg_catalog.to_regprocedure(function_signature); + if function_oid is null then + raise exception using + errcode = '42704', + message = 'pending dynamic activation resolver is missing'; + end if; + original_definition := pg_catalog.pg_get_functiondef(function_oid); + updated_definition := pg_catalog.replace( + original_definition, needle, replacement + ); + if updated_definition = original_definition then + raise exception using + errcode = '55000', + message = 'pending dynamic activation resolver shape changed'; + end if; + execute updated_definition; +end +$migration$; + +-- The activation writer independently rechecks the current cursor and only +-- admits an older page fence while its verified parent block is still ahead. +do $migration$ +declare + function_signature constant text := + 'programmable_private.stage_verified_dynamic_source_activations_v1(' || + 'uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,' || + 'jsonb,jsonb,timestamp with time zone)'; + function_oid regprocedure; + original_definition text; + updated_definition text; + needle constant text := + ' and page.reorg_generation = p_reorg_generation' || + pg_catalog.chr(10) || + ' and page.expected_cursor_generation = p_expected_cursor_generation' || + pg_catalog.chr(10) || + ' and page.expected_cursor_block_hash = p_expected_cursor_block_hash'; + replacement constant text := + ' and page.reorg_generation = p_reorg_generation' || + pg_catalog.chr(10) || + ' and (' || pg_catalog.chr(10) || + ' (' || pg_catalog.chr(10) || + ' page.expected_cursor_generation =' || pg_catalog.chr(10) || + ' p_expected_cursor_generation' || pg_catalog.chr(10) || + ' and page.expected_cursor_block_hash =' || + pg_catalog.chr(10) || + ' p_expected_cursor_block_hash' || pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' or (' || pg_catalog.chr(10) || + ' page.expected_cursor_generation <' || pg_catalog.chr(10) || + ' p_expected_cursor_generation' || pg_catalog.chr(10) || + ' and cursor.block_number < page.snapshot_block_number' || + pg_catalog.chr(10) || + ' )' || pg_catalog.chr(10) || + ' )'; +begin + function_oid := pg_catalog.to_regprocedure(function_signature); + if function_oid is null then + raise exception using + errcode = '42704', + message = 'dynamic activation staging function is missing'; + end if; + original_definition := pg_catalog.pg_get_functiondef(function_oid); + updated_definition := pg_catalog.replace( + original_definition, needle, replacement + ); + if updated_definition = original_definition then + raise exception using + errcode = '55000', + message = 'dynamic activation page fence shape changed'; + end if; + execute updated_definition; +end +$migration$; + +-- Activation evidence uses the same endpoint-bound provider identity as every +-- projection trace. Static database labels remain control-plane identifiers, +-- not persisted proof identities. +do $migration$ +declare + function_signature constant text := + 'programmable_private.stage_verified_dynamic_source_activations_v1(' || + 'uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,' || + 'jsonb,jsonb,timestamp with time zone)'; + function_oid regprocedure; + original_definition text; + updated_definition text; + needle constant text := + 'select deployment.redacted_identity::text as identity,'; + replacement constant text := + 'select metadata.vendor || ''-mainnet-'' ||' || + pg_catalog.chr(10) || + ' pg_catalog.substring(' || pg_catalog.chr(10) || + ' pg_catalog.encode(deployment.deployment_commitment, ''hex''),' || + pg_catalog.chr(10) || + ' 1, 32' || pg_catalog.chr(10) || + ' ) as identity,'; +begin + function_oid := pg_catalog.to_regprocedure(function_signature); + if function_oid is null then + raise exception using + errcode = '42704', + message = 'dynamic activation staging function is missing'; + end if; + original_definition := pg_catalog.pg_get_functiondef(function_oid); + updated_definition := pg_catalog.replace( + original_definition, needle, replacement + ); + if updated_definition = original_definition + or pg_catalog.strpos(updated_definition, needle) > 0 + then + raise exception using + errcode = '55000', + message = 'dynamic activation provider identity shape changed'; + end if; + execute updated_definition; +end +$migration$; + +reset role; diff --git a/supabase/migrations/20260801155212_reuse_dual_rpc_block_evidence.sql b/supabase/migrations/20260801155212_reuse_dual_rpc_block_evidence.sql new file mode 100644 index 00000000..17bbf5db --- /dev/null +++ b/supabase/migrations/20260801155212_reuse_dual_rpc_block_evidence.sql @@ -0,0 +1,264 @@ +-- A projector retry can legitimately verify the same block beneath a reused +-- safe-head observation. Keep one immutable evidence row per fingerprint and +-- return it only after an exact comparison of every canonical field. + +set role programmable_migrator; + +create function programmable_private.append_or_reuse_dual_rpc_block_evidence_v1( + p_block_evidence_id uuid, + p_observation_id uuid, + p_run_id uuid, + p_block_number numeric, + p_provider_a_block_hash bytea, + p_provider_b_block_hash bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + duplicate_constraint text; + header programmable_private.run_headers%rowtype; + observation programmable_private.safe_head_observations%rowtype; + existing programmable_private.dual_rpc_block_evidence%rowtype; + normalized_block bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + + begin + return programmable_private.append_dual_rpc_block_evidence( + p_block_evidence_id, + p_observation_id, + p_run_id, + p_block_number, + p_provider_a_block_hash, + p_provider_b_block_hash, + p_encoding_version, + p_canonical_preimage, + p_content_fingerprint, + p_verified_at + ); + exception + when unique_violation then + get stacked diagnostics duplicate_constraint = constraint_name; + if duplicate_constraint <> + 'dual_rpc_block_evidence_epoch_id_content_fingerprint_key' + then + raise; + end if; + end; + + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection', 'rewind') + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid projector run'; + end if; + + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_observation_id + for key share; + if not found + or observation.epoch_id <> header.epoch_id + or observation.chain_id <> header.chain_id + or observation.pointer_generation <> header.captured_pointer_generation + then + raise exception using + errcode = '23503', + message = 'run and observation scope differ'; + end if; + + if p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 + or p_block_number > 9223372036854775807 + then + raise exception using errcode = '22023', message = 'invalid block number'; + end if; + normalized_block := p_block_number::bigint; + + select * into existing + from programmable_private.dual_rpc_block_evidence + where epoch_id = header.epoch_id + and content_fingerprint = + p_content_fingerprint::programmable_private.bytes32_value + for key share; + if not found then + raise exception using + errcode = '23505', + message = 'block-evidence fingerprint conflict has no reusable row'; + end if; + + if existing.observation_id <> p_observation_id + or existing.epoch_id <> header.epoch_id + or existing.chain_id <> header.chain_id + or existing.pointer_generation <> header.captured_pointer_generation + or existing.block_number <> normalized_block + or existing.provider_a_block_hash <> p_provider_a_block_hash + or existing.provider_b_block_hash <> p_provider_b_block_hash + or existing.agreed_block_hash <> p_provider_a_block_hash + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + then + raise exception using + errcode = '23505', + message = 'block-evidence fingerprint replay conflicts with stored evidence'; + end if; + + return existing.block_evidence_id; +end +$function$; + +revoke all on function programmable_private.append_or_reuse_dual_rpc_block_evidence_v1( + uuid, uuid, uuid, numeric, bytea, bytea, smallint, bytea, bytea, timestamptz +) from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_operator; + +grant execute on function programmable_private.append_or_reuse_dual_rpc_block_evidence_v1( + uuid, uuid, uuid, numeric, bytea, bytea, smallint, bytea, bytea, timestamptz +) to programmable_projector; + +reset role; + +do $migration$ +declare + table_owner name; +begin + select pg_catalog.pg_get_userbyid(table_class.relowner) + into table_owner + from pg_catalog.pg_class as table_class + join pg_catalog.pg_namespace as table_namespace + on table_namespace.oid = table_class.relnamespace + where table_namespace.nspname = 'programmable_private' + and table_class.relname = 'dual_rpc_block_evidence' + and table_class.relkind in ('r', 'p'); + + if table_owner is null then + raise exception using + errcode = '42704', + message = 'dual-RPC block evidence table is missing'; + end if; + + execute pg_catalog.format( + 'alter function programmable_private.' || + 'append_or_reuse_dual_rpc_block_evidence_v1(' || + 'uuid,uuid,uuid,numeric,bytea,bytea,smallint,bytea,bytea,timestamptz) ' || + 'owner to %I', + table_owner + ); +end +$migration$; + +-- Evidence rows record the run that first persisted them. Consumers still +-- bind every reusable row to the current epoch, pointer, observation, +-- providers, block number, and hash; requiring the creator run as well would +-- make an exact immutable replay unusable. + +set role programmable_migrator; + +do $migration$ +declare + source text; + rewritten text; +begin + source := pg_catalog.pg_get_functiondef( + 'programmable_private.append_dual_rpc_log_coverage_evidence(uuid,uuid,uuid,text,bigint,bigint,numeric,numeric,bytea,numeric,text,uuid,uuid,uuid,uuid,bytea,bytea[],bytea[],bytea,smallint,bytea,bytea,bytea,timestamp with time zone)'::regprocedure + ); + rewritten := pg_catalog.replace( + source, + ' on canonical_block.verification_run_id = p_run_id + and canonical_block.observation_id = p_safe_head_observation_id', + ' on canonical_block.observation_id = p_safe_head_observation_id' + ); + if rewritten = source then + raise exception 'log-coverage block replay fence source changed'; + end if; + execute rewritten; + + source := pg_catalog.pg_get_functiondef( + 'programmable_private.commit_envio_ingestion_page_v1(uuid,uuid,uuid,uuid,text,bigint,bigint,numeric,programmable_private.envio_candidate_page_item_v1[],uuid,uuid,uuid,uuid,bytea,bytea[],bytea[],bytea,bytea,smallint,bytea,bytea,bytea,timestamp with time zone)'::regprocedure + ); + rewritten := pg_catalog.replace( + source, + ' and verification_run_id = p_run_id + and chain_id = 1;', + ' and chain_id = 1;' + ); + rewritten := pg_catalog.replace( + rewritten, + 'empty Envio page lacks same-run final block evidence', + 'empty Envio page lacks final block evidence' + ); + if rewritten = source then + raise exception 'empty-page block replay fence source changed'; + end if; + execute rewritten; + + source := pg_catalog.pg_get_functiondef( + 'programmable_private.recover_projector_reorg_v1(uuid,uuid,uuid,uuid,uuid,uuid,text,bigint,bigint,bigint,bigint,bigint,numeric,bytea,numeric,text,uuid,text,bigint,bytea,bytea,timestamp with time zone)'::regprocedure + ); + rewritten := pg_catalog.replace( + source, + ' and evidence.verification_run_id = p_run_id + and evidence.epoch_id = header.epoch_id', + ' and evidence.epoch_id = header.epoch_id' + ); + if rewritten = source then + raise exception 'reorg block replay fence source changed'; + end if; + execute rewritten; + + source := pg_catalog.pg_get_functiondef( + 'programmable_private.register_envio_ingestion_genesis_v1(uuid,uuid,uuid,text,uuid,bytea,timestamp with time zone)'::regprocedure + ); + rewritten := pg_catalog.replace( + source, + ' and pointer_generation = header.captured_pointer_generation + and verification_run_id = p_run_id;', + ' and pointer_generation = header.captured_pointer_generation;' + ); + rewritten := pg_catalog.replace( + rewritten, + 'genesis anchor lacks same-run dual-RPC evidence', + 'genesis anchor lacks dual-RPC evidence' + ); + if rewritten = source then + raise exception 'genesis block replay fence source changed'; + end if; + execute rewritten; + + source := pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_source_activations_v1(uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,jsonb,jsonb,timestamp with time zone)'::regprocedure + ); + rewritten := pg_catalog.replace( + source, + ' and block.verification_run_id = p_run_id + and block.epoch_id = header.epoch_id', + ' and block.epoch_id = header.epoch_id' + ); + rewritten := pg_catalog.replace( + rewritten, + ' and observation.verification_run_id = p_run_id + and observation.provider_a_id = p_provider_a_id', + ' and observation.provider_a_id = p_provider_a_id' + ); + if rewritten = source then + raise exception 'dynamic activation block replay fence source changed'; + end if; + execute rewritten; +end +$migration$; + +reset role; diff --git a/supabase/migrations/20260801204500_reuse_dual_rpc_block_evidence_constraint.sql b/supabase/migrations/20260801204500_reuse_dual_rpc_block_evidence_constraint.sql new file mode 100644 index 00000000..a77d3e59 --- /dev/null +++ b/supabase/migrations/20260801204500_reuse_dual_rpc_block_evidence_constraint.sql @@ -0,0 +1,134 @@ +-- PostgreSQL may report either immutable uniqueness fence first when a retry +-- reuses the same observation, block and content fingerprint. Both conflicts +-- enter the same exact-field comparison before an existing row is returned. + +set role programmable_migrator; + +create or replace function programmable_private.append_or_reuse_dual_rpc_block_evidence_v1( + p_block_evidence_id uuid, + p_observation_id uuid, + p_run_id uuid, + p_block_number numeric, + p_provider_a_block_hash bytea, + p_provider_b_block_hash bytea, + p_encoding_version smallint, + p_canonical_preimage bytea, + p_content_fingerprint bytea, + p_verified_at timestamptz default pg_catalog.clock_timestamp() +) +returns uuid +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +declare + duplicate_constraint text; + header programmable_private.run_headers%rowtype; + observation programmable_private.safe_head_observations%rowtype; + existing programmable_private.dual_rpc_block_evidence%rowtype; + normalized_block bigint; +begin + perform programmable_private.assert_caller('programmable_projector'); + + begin + return programmable_private.append_dual_rpc_block_evidence( + p_block_evidence_id, + p_observation_id, + p_run_id, + p_block_number, + p_provider_a_block_hash, + p_provider_b_block_hash, + p_encoding_version, + p_canonical_preimage, + p_content_fingerprint, + p_verified_at + ); + exception + when unique_violation then + get stacked diagnostics duplicate_constraint = constraint_name; + if duplicate_constraint not in ( + 'dual_rpc_block_evidence_epoch_id_content_fingerprint_key', + 'dual_rpc_block_evidence_observation_id_block_number_key' + ) then + raise; + end if; + end; + + select * into header + from programmable_private.run_headers + where run_id = p_run_id + and run_kind in ('ingestion', 'projection', 'rewind') + for share; + if not found then + raise exception using errcode = '23503', message = 'invalid projector run'; + end if; + + select * into observation + from programmable_private.safe_head_observations + where observation_id = p_observation_id + for key share; + if not found + or observation.epoch_id <> header.epoch_id + or observation.chain_id <> header.chain_id + or observation.pointer_generation <> header.captured_pointer_generation + then + raise exception using + errcode = '23503', + message = 'run and observation scope differ'; + end if; + + if p_block_number <> pg_catalog.trunc(p_block_number) + or p_block_number < 0 + or p_block_number > 9223372036854775807 + then + raise exception using errcode = '22023', message = 'invalid block number'; + end if; + normalized_block := p_block_number::bigint; + + select * into existing + from programmable_private.dual_rpc_block_evidence + where epoch_id = header.epoch_id + and content_fingerprint = + p_content_fingerprint::programmable_private.bytes32_value + for key share; + if not found then + raise exception using + errcode = '23505', + message = 'block-evidence fingerprint conflict has no reusable row'; + end if; + + if existing.observation_id <> p_observation_id + or existing.epoch_id <> header.epoch_id + or existing.chain_id <> header.chain_id + or existing.pointer_generation <> header.captured_pointer_generation + or existing.block_number <> normalized_block + or existing.provider_a_block_hash <> p_provider_a_block_hash + or existing.provider_b_block_hash <> p_provider_b_block_hash + or existing.agreed_block_hash <> p_provider_a_block_hash + or existing.encoding_version <> p_encoding_version + or existing.canonical_preimage <> p_canonical_preimage + or existing.content_fingerprint <> p_content_fingerprint + then + raise exception using + errcode = '23505', + message = 'block-evidence fingerprint replay conflicts with stored evidence'; + end if; + + return existing.block_evidence_id; +end +$function$; + +revoke all on function programmable_private.append_or_reuse_dual_rpc_block_evidence_v1( + uuid, uuid, uuid, numeric, bytea, bytea, smallint, bytea, bytea, timestamptz +) from public, anon, authenticated, service_role, + programmable_projector_runtime, programmable_reconciler, + programmable_api_reader, programmable_profile_binder, + programmable_profile_recovery, programmable_profile_writer, + programmable_maintenance, programmable_operator; + +grant execute on function programmable_private.append_or_reuse_dual_rpc_block_evidence_v1( + uuid, uuid, uuid, numeric, bytea, bytea, smallint, bytea, bytea, timestamptz +) to programmable_projector; + +reset role; diff --git a/supabase/migrations/20260801210000_allow_reused_receipt_ordinals_across_transactions.sql b/supabase/migrations/20260801210000_allow_reused_receipt_ordinals_across_transactions.sql new file mode 100644 index 00000000..0494321e --- /dev/null +++ b/supabase/migrations/20260801210000_allow_reused_receipt_ordinals_across_transactions.sql @@ -0,0 +1,48 @@ +-- A receipt log ordinal is scoped to one transaction receipt, not to an +-- entire projected block page. Different launch transactions can therefore +-- legitimately have the same ordinal. The parent candidate identifier already +-- commits to the transaction hash and remains the immutable row identity. + +set role programmable_migrator; + +do $migration$ +declare + matching_constraints integer; + receipt_ordinal_constraint text; +begin + select pg_catalog.count(*)::integer, + pg_catalog.min(constraint_record.conname::text) + into matching_constraints, receipt_ordinal_constraint + from pg_catalog.pg_constraint as constraint_record + join pg_catalog.pg_class as table_record + on table_record.oid = constraint_record.conrelid + join pg_catalog.pg_namespace as namespace_record + on namespace_record.oid = table_record.relnamespace + where namespace_record.nspname = 'programmable_private' + and table_record.relname = + 'provisional_dynamic_parent_receipt_ordinals' + and constraint_record.contype = 'u' + and pg_catalog.pg_get_constraintdef(constraint_record.oid) = + 'UNIQUE (provisional_page_id, receipt_log_ordinal)'; + + if matching_constraints <> 1 or receipt_ordinal_constraint is null then + raise exception using + errcode = '55000', + message = 'provisional receipt ordinal constraint shape changed'; + end if; + + execute pg_catalog.format( + 'alter table programmable_private.' || + 'provisional_dynamic_parent_receipt_ordinals ' || + 'drop constraint %I', + receipt_ordinal_constraint + ); +end +$migration$; + +comment on column + programmable_private.provisional_dynamic_parent_receipt_ordinals. + receipt_log_ordinal is + 'Zero-based log ordinal within the parent transaction receipt; values may repeat across different parent candidates.'; + +reset role; diff --git a/supabase/migrations/20260802030000_projection_writer_event_authorization.sql b/supabase/migrations/20260802030000_projection_writer_event_authorization.sql new file mode 100644 index 00000000..384dbf55 --- /dev/null +++ b/supabase/migrations/20260802030000_projection_writer_event_authorization.sql @@ -0,0 +1,134 @@ +-- Bind internal projection writers to the semantic event rule that authorizes +-- their source occurrence. Writer table names and event semantic names are +-- deliberately separate domains. + +set role programmable_migrator; + +create or replace function programmable_private.assert_projection_event_allowed( + p_run_id uuid, + p_occurrence_id uuid, + p_projection_kind text +) +returns void +language plpgsql +stable +security definer +set search_path = '' +as $function$ +declare + header programmable_private.run_headers%rowtype; + occurrence programmable_private.chain_event_occurrences%rowtype; + materialization programmable_private.chain_event_occurrence_materializations%rowtype; + resolved_source_role text; +begin + perform programmable_private.assert_caller('programmable_projector'); + select * into header from programmable_private.run_headers + where run_id = p_run_id and run_kind in ('ingestion', 'projection'); + if not found then + raise exception using errcode = '23503', message = 'invalid event-writer run'; + end if; + perform programmable_private.assert_current_epoch( + header.chain_id, header.release_id, header.model_id, header.source_group, + header.epoch_id, header.captured_pointer_generation + ); + if exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = p_run_id + ) then + raise exception using errcode = '55000', message = 'run is terminal'; + end if; + select * into occurrence from programmable_private.chain_event_occurrences + where occurrence_id = p_occurrence_id; + if not found or occurrence.chain_id <> header.chain_id then + raise exception using errcode = '23503', message = 'projection event scope mismatch'; + end if; + select * into materialization + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = p_occurrence_id + and chain_id = header.chain_id + and release_id = header.release_id + and model_id = header.model_id + and source_group = header.source_group + and epoch_id = header.epoch_id + and pointer_generation = header.captured_pointer_generation; + if not found then + raise exception using errcode = '23503', message = 'projection event scope mismatch'; + end if; + select coalesce(binding.source_role, dynamic_source.deployed_source_role) + into resolved_source_role + from programmable_private.chain_event_occurrence_materializations as selected + left join programmable_private.release_source_bindings as binding + on binding.binding_id = selected.release_binding_id + left join programmable_private.dynamic_source_attestations as dynamic_source + on dynamic_source.dynamic_source_attestation_id = + selected.dynamic_source_attestation_id + where selected.materialization_id = materialization.materialization_id; + + if resolved_source_role is null then + raise exception using errcode = '23514', message = 'event/source role is outside the release writer allowlist'; + end if; + + if p_projection_kind = 'launch_requirement' then + if not exists ( + select 1 + from programmable_private.release_launch_completeness_requirements as requirement + where requirement.epoch_id = header.epoch_id + and requirement.occurrence_role = resolved_source_role + and requirement.event_type = materialization.event_type + ) then + raise exception using errcode = '23514', message = 'event/source role is outside the release writer allowlist'; + end if; + return; + end if; + + if not exists ( + select 1 + from programmable_private.release_projection_event_rules as rule + where rule.epoch_id = header.epoch_id + and rule.source_role = resolved_source_role + and rule.event_type = materialization.event_type + and ( + rule.projection_kind = p_projection_kind + or (p_projection_kind = 'pool' and rule.projection_kind = 'pool-registration') + or (p_projection_kind = 'pool_fee_configuration' and rule.projection_kind = 'fee-disclosure') + or (p_projection_kind = 'fee_accrual' and rule.projection_kind = 'fee-accrual') + or (p_projection_kind = 'pool_fee_total' and rule.projection_kind = 'fee-accrual') + or (p_projection_kind = 'reward_vault' and rule.projection_kind = 'reward-vault-deployment') + or (p_projection_kind = 'reward_allocation' and rule.projection_kind = 'reward-vault-deployment') + or (p_projection_kind = 'claim' and rule.projection_kind = 'beneficiary-claim') + or (p_projection_kind = 'payout_change' and rule.projection_kind = 'payout-change') + or ( + p_projection_kind = 'account_reward_balance' + and rule.projection_kind in ( + 'reward-vault-deployment', 'creator-fee-checkpoint', + 'beneficiary-claim', 'payout-change', + 'reward-configuration-activation' + ) + ) + or (p_projection_kind = 'initial_buy_custody' and rule.projection_kind = 'initial-buy-custody') + or (p_projection_kind = 'initial_buy_vesting' and rule.projection_kind = 'vesting-wallet-deployment') + ) + ) then + raise exception using errcode = '23514', message = 'event/source role is outside the release writer allowlist'; + end if; +end +$function$; + +comment on function programmable_private.assert_projection_event_allowed( + uuid, uuid, text +) is + 'Authorizes a projection writer against the exact semantic event rule or launch completeness requirement for its source occurrence.'; + +revoke all on function programmable_private.assert_projection_event_allowed( + uuid, uuid, text +) from public, anon, authenticated, service_role, + programmable_reconciler, programmable_api_reader, + programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer, programmable_maintenance, + programmable_operator; + +grant execute on function programmable_private.assert_projection_event_allowed( + uuid, uuid, text +) to programmable_projector; + +reset role; diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 00000000..1fe1a9b4 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,2 @@ +-- Local-only deterministic fixtures belong in pgTAP setup transactions. +-- Production data and credentials must never be added to this file. diff --git a/supabase/tests/codec/.gitignore b/supabase/tests/codec/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/supabase/tests/codec/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/supabase/tests/codec/.node-version b/supabase/tests/codec/.node-version new file mode 100644 index 00000000..d845d9d8 --- /dev/null +++ b/supabase/tests/codec/.node-version @@ -0,0 +1 @@ +24.14.0 diff --git a/supabase/tests/codec/canonical-fingerprint-v1.json b/supabase/tests/codec/canonical-fingerprint-v1.json new file mode 100644 index 00000000..164fec7a --- /dev/null +++ b/supabase/tests/codec/canonical-fingerprint-v1.json @@ -0,0 +1,577 @@ +{ + "format": "programmable-canonical-fingerprint-fixture", + "encoding_version": 1, + "hash": "ethereum-keccak-256", + "integer_encoding": "unsigned fixed-width big-endian", + "variable_encoding": "uint32 big-endian byte length followed by exact bytes", + "nullable_encoding": "0x00 for null; 0x01 followed by ordinary encoding for present", + "array_encoding": "uint32 big-endian count followed by elements in original order", + "json_encoding": "RFC 8785 JCS; uint256 values are canonical decimal strings", + "field_schemas": { + "occurrence": [ + [ + "chain_id", + "u64" + ], + [ + "transaction_hash", + "bytes32" + ], + [ + "receipt_log_ordinal", + "u32" + ], + [ + "block_number", + "u64" + ], + [ + "block_hash", + "bytes32" + ], + [ + "transaction_index", + "u32" + ], + [ + "block_global_log_index", + "u32" + ], + [ + "source_address", + "bytes20" + ], + [ + "event_signature", + "bytes32" + ], + [ + "ordered_topics", + "array" + ], + [ + "raw_data", + "varbytes" + ], + [ + "decoded_payload", + "varutf8" + ], + [ + "payload_hash", + "bytes32" + ], + [ + "decoder_version", + "varutf8" + ], + [ + "abi_event_set_commitment", + "bytes32" + ], + [ + "release_id", + "varutf8" + ], + [ + "model_id", + "varutf8" + ], + [ + "envio_candidate_id", + "varutf8" + ], + [ + "provider_cursor", + "varutf8" + ], + [ + "block_timestamp_unix", + "u64" + ] + ], + "allocation": [ + [ + "chain_id", + "u64" + ], + [ + "release_id", + "varutf8" + ], + [ + "model_id", + "varutf8" + ], + [ + "vault", + "bytes20" + ], + [ + "factory_transaction_hash", + "bytes32" + ], + [ + "factory_receipt_log_ordinal", + "u32" + ], + [ + "factory_block_hash", + "bytes32" + ], + [ + "creation_block_number", + "u64" + ], + [ + "creation_transaction_index", + "u32" + ], + [ + "ordered_beneficiaries", + "array" + ], + [ + "ordered_shares_bps", + "array" + ], + [ + "allocation_hash", + "bytes32" + ], + [ + "configuration_hash", + "bytes32" + ], + [ + "active_configuration_hash", + "nullable" + ], + [ + "artifact_creation_code_commitment", + "bytes32" + ], + [ + "required_occurrences", + "array" + ] + ], + "evidence": [ + [ + "allocation_fingerprint", + "bytes32" + ], + [ + "recovery_method", + "varutf8" + ], + [ + "evidence_version", + "varutf8" + ], + [ + "top_level_destination", + "nullable" + ], + [ + "method_selector", + "nullable" + ], + [ + "transaction_input_hash", + "nullable" + ], + [ + "constructor_arguments_commitment", + "bytes32" + ], + [ + "local_init_code_hash", + "bytes32" + ], + [ + "create2_salt", + "bytes32" + ], + [ + "local_create2_address", + "bytes20" + ], + [ + "historical_enrichment_status", + "varutf8" + ], + [ + "getter_block_hash", + "nullable" + ], + [ + "getter_result_hash_a", + "nullable" + ], + [ + "getter_result_hash_b", + "nullable" + ], + [ + "predict_result_hash_a", + "nullable" + ], + [ + "predict_result_hash_b", + "nullable" + ], + [ + "predicted_vault_a", + "nullable" + ], + [ + "predicted_vault_b", + "nullable" + ], + [ + "selected_rpc_result_hash_a", + "bytes32" + ], + [ + "selected_rpc_result_hash_b", + "bytes32" + ], + [ + "selected_rpc_transaction_receipt_hash_a", + "nullable" + ], + [ + "selected_rpc_transaction_receipt_hash_b", + "nullable" + ], + [ + "extra_note", + "nullable" + ], + [ + "required_occurrence_fingerprints", + "array" + ] + ] + }, + "sentinel_vectors": [ + { + "name": "evidence_null_marker", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a76310000", + "expected_keccak256": "0x945cecbc3714b84a54cebb706446ed17b531a8e53703a90774a51797989f2f63" + }, + { + "name": "evidence_present_empty", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a7631000100000000", + "expected_keccak256": "0x080646fe0f81fe8dc11056eec4e4690a9351a30b435968db0571673a18efb934" + }, + { + "name": "allocation_order_ab", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000002111111111111111111111111111111111111111122222222222222222222222222222222222222220000000217700fa0", + "expected_keccak256": "0xe9937a36b49028672572c67a28640c69473ebb5dfafed3eb04e33867e177243c" + }, + { + "name": "allocation_order_ba", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a7631000000000222222222222222222222222222222222222222221111111111111111111111111111111111111111000000020fa01770", + "expected_keccak256": "0xf5a3b18d2b3d8c9ae9a520109fd0f4dac92ff19f1ef55fca8aa91fce98259059" + }, + { + "name": "occurrence_two_topics_indexed_only", + "expected_preimage_hex": "0x70726f6772616d6d61626c653a6f6363757272656e63653a76310000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000", + "expected_keccak256": "0x262dc3ea7b1dac050cfca3aa67f6881de42706ea35349b88b7d14e860d64ff74" + } + ], + "vectors": [ + { + "name": "occurrence_all_fields_v1", + "domain": "occurrence", + "input": { + "chain_id": "1", + "transaction_hash": "0x1111111111111111111111111111111111111111111111111111111111111111", + "receipt_log_ordinal": "0", + "block_number": "25639596", + "block_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "transaction_index": "3", + "block_global_log_index": "7", + "source_address": "0x3333333333333333333333333333333333333333", + "event_signature": "0x4444444444444444444444444444444444444444444444444444444444444444", + "ordered_topics": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "raw_data": "0x010203", + "decoded_payload": { + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "creator": "0x3333333333333333333333333333333333333333", + "flags": [ + true, + false + ], + "nested": { + "b": "two", + "a": "one" + } + }, + "payload_hash": "0x5555555555555555555555555555555555555555555555555555555555555555", + "decoder_version": "projector-v1.0.0", + "abi_event_set_commitment": "0x6666666666666666666666666666666666666666666666666666666666666666", + "release_id": "classic-v3", + "model_id": "classic-v3", + "envio_candidate_id": "1:22:11:7", + "provider_cursor": "25639596:7", + "block_timestamp_unix": "1785463200" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a6f6363757272656e63653a76310000000000000000011111111111111111111111111111111111111111111111111111111111111111000000000000000001873aac222222222222222222222222222222222222222222222222222222222222222200000003000000073333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444400000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000003010203000000c67b22616d6f756e74223a22313135373932303839323337333136313935343233353730393835303038363837393037383533323639393834363635363430353634303339343537353834303037393133313239363339393335222c2263726561746f72223a22307833333333333333333333333333333333333333333333333333333333333333333333333333333333222c22666c616773223a5b747275652c66616c73655d2c226e6573746564223a7b2261223a226f6e65222c2262223a2274776f227d7d55555555555555555555555555555555555555555555555555555555555555550000001070726f6a6563746f722d76312e302e3066666666666666666666666666666666666666666666666666666666666666660000000a636c61737369632d76330000000a636c61737369632d763300000009313a32323a31313a370000000a32353633393539363a37000000006a6c01a0", + "expected_keccak256": "0x6fe25eb0a62ea86736aa134ada719976b6166844b98d83b56d478ae409956955" + }, + { + "name": "occurrence_indexed_only_empty_data_v1", + "domain": "occurrence", + "input": { + "chain_id": "1", + "transaction_hash": "0x1212121212121212121212121212121212121212121212121212121212121212", + "receipt_log_ordinal": "0", + "block_number": "25639596", + "block_hash": "0x2323232323232323232323232323232323232323232323232323232323232323", + "transaction_index": "3", + "block_global_log_index": "7", + "source_address": "0x3333333333333333333333333333333333333333", + "event_signature": "0x4444444444444444444444444444444444444444444444444444444444444444", + "ordered_topics": [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "raw_data": "0x", + "decoded_payload": { + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "creator": "0x3333333333333333333333333333333333333333", + "flags": [ + true, + false + ], + "nested": { + "b": "two", + "a": "one" + } + }, + "payload_hash": "0x5656565656565656565656565656565656565656565656565656565656565656", + "decoder_version": "projector-v1.0.0", + "abi_event_set_commitment": "0x6666666666666666666666666666666666666666666666666666666666666666", + "release_id": "classic-v3", + "model_id": "classic-v3", + "envio_candidate_id": "1:23:12:7", + "provider_cursor": "25639596:7", + "block_timestamp_unix": "1785463200" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a6f6363757272656e63653a76310000000000000000011212121212121212121212121212121212121212121212121212121212121212000000000000000001873aac232323232323232323232323232323232323232323232323232323232323232300000003000000073333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444400000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000c67b22616d6f756e74223a22313135373932303839323337333136313935343233353730393835303038363837393037383533323639393834363635363430353634303339343537353834303037393133313239363339393335222c2263726561746f72223a22307833333333333333333333333333333333333333333333333333333333333333333333333333333333222c22666c616773223a5b747275652c66616c73655d2c226e6573746564223a7b2261223a226f6e65222c2262223a2274776f227d7d56565656565656565656565656565656565656565656565656565656565656560000001070726f6a6563746f722d76312e302e3066666666666666666666666666666666666666666666666666666666666666660000000a636c61737369632d76330000000a636c61737369632d763300000009313a32333a31323a370000000a32353633393539363a37000000006a6c01a0", + "expected_keccak256": "0xef8ea5dee7777948f98130615b8f4ab9fd1d316e243b6c3a745c88043ef5d2d8" + }, + { + "name": "allocation_order_ab_v1", + "domain": "allocation", + "input": { + "chain_id": "1", + "release_id": "classic-v3", + "model_id": "classic-v3", + "vault": "0x7777777777777777777777777777777777777777", + "factory_transaction_hash": "0x8888888888888888888888888888888888888888888888888888888888888888", + "factory_receipt_log_ordinal": "2", + "factory_block_hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "creation_block_number": "25639600", + "creation_transaction_index": "4", + "ordered_beneficiaries": [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222" + ], + "ordered_shares_bps": [ + "6000", + "4000" + ], + "allocation_hash": "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "configuration_hash": "0xa2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2", + "active_configuration_hash": "0xa3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "artifact_creation_code_commitment": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "required_occurrences": [ + { + "transaction_hash": "0xa5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + "receipt_log_ordinal": "1", + "block_hash": "0xa6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6", + "role": "launcher" + }, + { + "transaction_hash": "0xa7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7", + "receipt_log_ordinal": "1", + "block_hash": "0xa8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8", + "role": "factory" + }, + { + "transaction_hash": "0xa9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9", + "receipt_log_ordinal": "1", + "block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "role": "hook" + } + ] + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000000000000010000000a636c61737369632d76330000000a636c61737369632d7633777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000299999999999999999999999999999999999999999999999999999999999999990000000001873ab00000000400000002111111111111111111111111111111111111111122222222222222222222222222222222222222220000000217700fa0a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a201a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a400000003a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a500000001a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6000000086c61756e63686572a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a700000001a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a800000007666163746f7279a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a900000001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000004686f6f6b", + "expected_keccak256": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0" + }, + { + "name": "allocation_order_ba_v1", + "domain": "allocation", + "input": { + "chain_id": "1", + "release_id": "classic-v3", + "model_id": "classic-v3", + "vault": "0x7777777777777777777777777777777777777777", + "factory_transaction_hash": "0x8888888888888888888888888888888888888888888888888888888888888888", + "factory_receipt_log_ordinal": "2", + "factory_block_hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "creation_block_number": "25639600", + "creation_transaction_index": "4", + "ordered_beneficiaries": [ + "0x2222222222222222222222222222222222222222", + "0x1111111111111111111111111111111111111111" + ], + "ordered_shares_bps": [ + "4000", + "6000" + ], + "allocation_hash": "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "configuration_hash": "0xa2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2", + "active_configuration_hash": "0xa3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "artifact_creation_code_commitment": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "required_occurrences": [ + { + "transaction_hash": "0xa5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + "receipt_log_ordinal": "1", + "block_hash": "0xa6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6", + "role": "launcher" + }, + { + "transaction_hash": "0xa7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7", + "receipt_log_ordinal": "1", + "block_hash": "0xa8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8", + "role": "factory" + }, + { + "transaction_hash": "0xa9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9", + "receipt_log_ordinal": "1", + "block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "role": "hook" + } + ] + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000000000000010000000a636c61737369632d76330000000a636c61737369632d7633777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000299999999999999999999999999999999999999999999999999999999999999990000000001873ab0000000040000000222222222222222222222222222222222222222221111111111111111111111111111111111111111000000020fa01770a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a201a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a400000003a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a500000001a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6000000086c61756e63686572a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a700000001a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a800000007666163746f7279a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a900000001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000004686f6f6b", + "expected_keccak256": "0x1578cf70b13f4382f5b68923f4034573bcaf233970306c835db13c28c746887e" + }, + { + "name": "evidence_all_fields_v1", + "domain": "evidence", + "input": { + "allocation_fingerprint": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0", + "recovery_method": "launcher_calldata", + "evidence_version": "seed-verifier-v1.0.1", + "top_level_destination": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "method_selector": "0xbf388406", + "transaction_input_hash": "0xb3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3", + "constructor_arguments_commitment": "0xc0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "local_init_code_hash": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "create2_salt": "0xc2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2", + "local_create2_address": "0x7777777777777777777777777777777777777777", + "historical_enrichment_status": "matched", + "getter_block_hash": "0x9999999999999999999999999999999999999999999999999999999999999999", + "getter_result_hash_a": "0xb5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5", + "getter_result_hash_b": "0xb5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5", + "predict_result_hash_a": "0xb6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6", + "predict_result_hash_b": "0xb6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6", + "selected_rpc_result_hash_a": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_result_hash_b": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_transaction_receipt_hash_a": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "selected_rpc_transaction_receipt_hash_b": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "extra_note": "complete", + "required_occurrence_fingerprints": [ + "0x5252525252525252525252525252525252525252525252525252525252525252", + "0x5454545454545454545454545454545454545454545454545454545454545454", + "0x5656565656565656565656565656565656565656565656565656565656565656" + ], + "predicted_vault_a": "0x7777777777777777777777777777777777777777", + "predicted_vault_b": "0x7777777777777777777777777777777777777777" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3101b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c27777777777777777777777777777777777777777000000076d61746368656401999999999999999999999999999999999999999999999999999999999999999901b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b501b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b501b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b601b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6017777777777777777777777777777777777777777017777777777777777777777777777777777777777b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b80100000008636f6d706c65746500000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656", + "expected_keccak256": "0xd5eefc2d52e51f7e1b058450f49d5d153f56e1f646233699aa92e5edc15e335e" + }, + { + "name": "evidence_null_optional_v1", + "domain": "evidence", + "input": { + "allocation_fingerprint": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0", + "recovery_method": "launcher_calldata", + "evidence_version": "seed-verifier-v1.0.0", + "top_level_destination": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "method_selector": "0xbf388406", + "transaction_input_hash": "0xb3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3", + "constructor_arguments_commitment": "0xc0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "local_init_code_hash": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "create2_salt": "0xc2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2", + "local_create2_address": "0x7777777777777777777777777777777777777777", + "historical_enrichment_status": "unavailable", + "getter_block_hash": null, + "getter_result_hash_a": null, + "getter_result_hash_b": null, + "predict_result_hash_a": null, + "predict_result_hash_b": null, + "selected_rpc_result_hash_a": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_result_hash_b": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_transaction_receipt_hash_a": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "selected_rpc_transaction_receipt_hash_b": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "extra_note": null, + "required_occurrence_fingerprints": [ + "0x5252525252525252525252525252525252525252525252525252525252525252", + "0x5454545454545454545454545454545454545454545454545454545454545454", + "0x5656565656565656565656565656565656565656565656565656565656565656" + ], + "predicted_vault_a": null, + "predicted_vault_b": null + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3001b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c277777777777777777777777777777777777777770000000b756e617661696c61626c6500000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b80000000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656", + "expected_keccak256": "0x27996b52c56c657c4a8ef3894a50dceb3348f299afc64adcfc0a202f5713b914" + }, + { + "name": "evidence_present_empty_v1", + "domain": "evidence", + "input": { + "allocation_fingerprint": "0x760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0", + "recovery_method": "launcher_calldata", + "evidence_version": "seed-verifier-v1.0.0", + "top_level_destination": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "method_selector": "0xbf388406", + "transaction_input_hash": "0xb3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3", + "constructor_arguments_commitment": "0xc0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "local_init_code_hash": "0xa4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4", + "create2_salt": "0xc2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2", + "local_create2_address": "0x7777777777777777777777777777777777777777", + "historical_enrichment_status": "unavailable", + "getter_block_hash": null, + "getter_result_hash_a": null, + "getter_result_hash_b": null, + "predict_result_hash_a": null, + "predict_result_hash_b": null, + "selected_rpc_result_hash_a": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_result_hash_b": "0xb7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", + "selected_rpc_transaction_receipt_hash_a": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "selected_rpc_transaction_receipt_hash_b": "0xb8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", + "extra_note": "", + "required_occurrence_fingerprints": [ + "0x5252525252525252525252525252525252525252525252525252525252525252", + "0x5454545454545454545454545454545454545454545454545454545454545454", + "0x5656565656565656565656565656565656565656565656565656565656565656" + ], + "predicted_vault_a": null, + "predicted_vault_b": null + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3001b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c277777777777777777777777777777777777777770000000b756e617661696c61626c6500000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8010000000000000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656", + "expected_keccak256": "0xbd4b5c41c44f9d85419a04918edb205b72c0a49a794cf43664c9509e803579fa" + } + ] +} diff --git a/supabase/tests/codec/package-lock.json b/supabase/tests/codec/package-lock.json new file mode 100644 index 00000000..76c33d6f --- /dev/null +++ b/supabase/tests/codec/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "@programmable/canonical-fingerprint-reference", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@programmable/canonical-fingerprint-reference", + "version": "1.0.0", + "dependencies": { + "@noble/hashes": "2.2.0", + "canonicalize": "3.0.0" + }, + "engines": { + "node": "24.14.0" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/canonicalize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-3.0.0.tgz", + "integrity": "sha512-yYLfHyDMIXRyRqsKBRLX023riFLpXY2YOfdtqKXZRZy9qsfOJ9U+4F9YZL7MEzL5+ziN2x2nlBvY/Voi3EBljA==", + "license": "Apache-2.0", + "bin": { + "canonicalize": "bin/canonicalize.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/supabase/tests/codec/package.json b/supabase/tests/codec/package.json new file mode 100644 index 00000000..523b46a8 --- /dev/null +++ b/supabase/tests/codec/package.json @@ -0,0 +1,17 @@ +{ + "name": "@programmable/canonical-fingerprint-reference", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": "24.14.0" + }, + "packageManager": "npm@11.16.0", + "scripts": { + "test": "node verify-reference-canonical-fingerprint-v1.mjs && node verify-production-canonical-fingerprint-v1.ts && node verify-reference-provider-evidence-v2.mjs && node verify-production-provider-evidence-v2.ts" + }, + "dependencies": { + "@noble/hashes": "2.2.0", + "canonicalize": "3.0.0" + } +} diff --git a/supabase/tests/codec/provider-evidence-v2.json b/supabase/tests/codec/provider-evidence-v2.json new file mode 100644 index 00000000..e214ea67 --- /dev/null +++ b/supabase/tests/codec/provider-evidence-v2.json @@ -0,0 +1,262 @@ +{ + "format": "programmable-provider-evidence-fixture", + "encoding_version": 2, + "hash": "ethereum-keccak-256", + "domain_prefix_utf8": "programmable:provider-evidence:v2\\u0000", + "definition_prefix_utf8": "programmable:provider-evidence-definition:v2\\u0000", + "domain_definition_prefix_utf8": "programmable:provider-evidence-domain:v2\\u0000", + "integer_encoding": "unsigned fixed-width big-endian", + "uuid_encoding": "16 RFC 4122 bytes after removing hyphens", + "variable_encoding": "uint32 big-endian byte length followed by exact bytes", + "nullable_encoding": "0x00 for null; 0x01 followed by ordinary encoding for present", + "array_encoding": "uint32 big-endian count followed by elements in supplied order", + "empty_log_coverage_encoding": { + "ordered_log_commitments": "empty array", + "final_block_global_log_index": "4294967295", + "final_candidate_id": "empty-page", + "cursor_storage": "persist final block/hash with null log index and candidate id" + }, + "subtype_tags": { + "safe_head": 1, + "block": 2, + "runtime_code": 3, + "dynamic_attestation": 4, + "log_coverage": 5 + }, + "field_schemas": { + "safe_head": [ + ["chain_id", "u64"], ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], ["reported_chain_id_a", "u64"], + ["reported_chain_id_b", "u64"], ["head_a", "u64"], + ["head_b", "u64"], ["finality_depth", "u32"], + ["safe_block_number", "u64"], ["safe_block_hash_a", "bytes32"], + ["safe_block_hash_b", "bytes32"] + ], + "block": [ + ["chain_id", "u64"], ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], ["observation_id", "uuid16"], + ["block_number", "u64"], ["provider_a_block_hash", "bytes32"], + ["provider_b_block_hash", "bytes32"] + ], + "runtime_code": [ + ["chain_id", "u64"], ["release_id", "varutf8"], + ["model_id", "varutf8"], ["source_group", "varutf8"], + ["epoch_id", "uuid16"], ["pointer_generation", "u64"], + ["source_address", "bytes20"], + ["deployment_block_evidence_id", "uuid16"], + ["deployment_block_number", "u64"], + ["deployment_block_hash", "bytes32"], ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], ["runtime_code_hash_a", "bytes32"], + ["runtime_code_hash_b", "bytes32"], ["runtime_code_a", "varbytes"], + ["runtime_code_b", "varbytes"], + ["normalized_runtime_code_hash_a", "bytes32"], + ["normalized_runtime_code_hash_b", "bytes32"], + ["immutable_references_commitment", "bytes32"], + ["immutable_values", "array"], + ["immutable_values_commitment", "bytes32"], + ["reconstructed_runtime_code", "varbytes"], + ["reconstructed_runtime_code_hash", "bytes32"] + ], + "dynamic_attestation": [ + ["chain_id", "u64"], ["release_id", "varutf8"], + ["model_id", "varutf8"], ["source_group", "varutf8"], + ["epoch_id", "uuid16"], ["pointer_generation", "u64"], + ["runtime_code_evidence_id", "uuid16"], + ["dynamic_source_template_id", "uuid16"], + ["parent_factory_occurrence_id", "uuid16"], + ["parent_factory_release_binding_id", "uuid16"], + ["parent_factory_binding_commitment", "bytes32"], + ["deployed_source_address", "bytes20"], + ["deployed_source_role", "varutf8"], + ["deployment_block_number", "u64"], + ["deployed_artifact_creation_code_commitment", "bytes32"], + ["expected_immutable_values_commitment", "bytes32"], + ["factory_configuration_commitment", "bytes32"], + ["constructor_arguments_commitment", "bytes32"], + ["local_init_code_hash", "bytes32"], ["runtime_code_hash", "bytes32"], + ["abi_event_set_commitment", "bytes32"] + ], + "log_coverage": [ + ["chain_id", "u64"], ["epoch_id", "uuid16"], + ["pointer_generation", "u64"], + ["provider_deployment_id", "uuid16"], ["stream_id", "varutf8"], + ["expected_cursor_generation", "u64"], + ["next_cursor_generation", "u64"], + ["previous_block_number", "u64"], + ["previous_block_global_log_index", "optional"], + ["previous_candidate_id", "optional"], + ["from_block_number", "u64"], ["to_block_number", "u64"], + ["final_block_hash", "bytes32"], + ["final_block_global_log_index", "u32"], + ["final_candidate_id", "varutf8"], + ["safe_head_observation_id", "uuid16"], + ["final_block_evidence_id", "uuid16"], ["provider_a_id", "uuid16"], + ["provider_b_id", "uuid16"], ["filter_commitment", "bytes32"], + ["ordered_log_commitments", "array"], + ["page_commitment", "bytes32"] + ] + }, + "expected_definition_commitments": { + "safe_head": "0x3a26ae9c9220347568e33b5850ac6f605d120e6443f64e9e8b8742ea8a016f52", + "block": "0x83948b75a3c05b9d257749f754f09a1b02e658496ba562f36e07bc15be3d7bec", + "runtime_code": "0x4c191e91130097832a91025e85c2ff3be2705af0e3ea9abc396f09e7cd9dbbc5", + "dynamic_attestation": "0x206e1f89ad459e55e0591de13eb40856dd94ff62923d76034eba5776706e6de9", + "log_coverage": "0x4ab7460cb321503613935191917c46872c9e3c9a681b2d4b349b6187f4dc0aec" + }, + "expected_domain_definition_commitment": "0x45b8e9d1bf3ffc2e70b7fd612ec2346aef5e74ae08348b699eb68ce0afbc9483", + "vectors": [ + { + "name": "safe_head_full_v2", + "subtype": "safe_head", + "input": { + "chain_id": "1", "epoch_id": "11111111-2222-4333-8444-555555555555", + "pointer_generation": "7", "provider_a_id": "aaaaaaaa-0000-4000-8000-000000000001", + "provider_b_id": "bbbbbbbb-0000-4000-8000-000000000002", + "reported_chain_id_a": "1", "reported_chain_id_b": "1", + "head_a": "9007199254741000", "head_b": "9007199254741001", + "finality_depth": "12", "safe_block_number": "9007199254740988", + "safe_block_hash_a": "0x1111111111111111111111111111111111111111111111111111111111111111", + "safe_block_hash_b": "0x1111111111111111111111111111111111111111111111111111111111111111" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200010000000000000001111111112222433384445555555555550000000000000007aaaaaaaa000040008000000000000001bbbbbbbb00004000800000000000000200000000000000010000000000000001002000000000000800200000000000090000000c001ffffffffffffc11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "expected_keccak256": "0xd1032dcaf7fb2640de21d21a57aee32193d12c4c4d4d52ad2ab1f9d0d42d91be" + }, + { + "name": "block_full_v2", "subtype": "block", + "input": { + "chain_id": "1", "epoch_id": "11111111-2222-4333-8444-555555555555", + "pointer_generation": "7", "observation_id": "cccccccc-0000-4000-8000-000000000003", + "block_number": "9007199254740988", + "provider_a_block_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "provider_b_block_hash": "0x2222222222222222222222222222222222222222222222222222222222222222" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200020000000000000001111111112222433384445555555555550000000000000007cccccccc000040008000000000000003001ffffffffffffc22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + "expected_keccak256": "0xe9654af5bbe8f31ae085253869d46622ad138d579c82b613e30e1eb8ada00e10" + }, + { + "name": "runtime_code_byte_complete_v2", "subtype": "runtime_code", + "input": { + "chain_id": "1", "release_id": "classic-v3", "model_id": "classic-v3", + "source_group": "core", "epoch_id": "11111111-2222-4333-8444-555555555555", + "pointer_generation": "7", "source_address": "0x3333333333333333333333333333333333333333", + "deployment_block_evidence_id": "dddddddd-0000-4000-8000-000000000004", + "deployment_block_number": "25639600", + "deployment_block_hash": "0x4444444444444444444444444444444444444444444444444444444444444444", + "provider_a_id": "aaaaaaaa-0000-4000-8000-000000000001", + "provider_b_id": "bbbbbbbb-0000-4000-8000-000000000002", + "runtime_code_hash_a": "0x5555555555555555555555555555555555555555555555555555555555555555", + "runtime_code_hash_b": "0x5555555555555555555555555555555555555555555555555555555555555555", + "runtime_code_a": "0x6001600055", "runtime_code_b": "0x6001600055", + "normalized_runtime_code_hash_a": "0x6666666666666666666666666666666666666666666666666666666666666666", + "normalized_runtime_code_hash_b": "0x6666666666666666666666666666666666666666666666666666666666666666", + "immutable_references_commitment": "0x7777777777777777777777777777777777777777777777777777777777777777", + "immutable_values": ["0x3333333333333333333333333333333333333333", "0x0001"], + "immutable_values_commitment": "0x8888888888888888888888888888888888888888888888888888888888888888", + "reconstructed_runtime_code": "0x6001600055", + "reconstructed_runtime_code_hash": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000300000000000000010000000a636c61737369632d76330000000a636c61737369632d763300000004636f72651111111122224333844455555555555500000000000000073333333333333333333333333333333333333333dddddddd0000400080000000000000040000000001873ab04444444444444444444444444444444444444444444444444444444444444444aaaaaaaa000040008000000000000001bbbbbbbb000040008000000000000002555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555550000000560016000550000000560016000556666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666677777777777777777777777777777777777777777777777777777777777777770000000200000014333333333333333333333333333333333333333300000002000188888888888888888888888888888888888888888888888888888888888888880000000560016000555555555555555555555555555555555555555555555555555555555555555555", + "expected_keccak256": "0xc3de153bafae8972485e86778076dbe187a4814a30f53e69a283cbeb9819660b" + }, + { + "name": "dynamic_attestation_full_v2", "subtype": "dynamic_attestation", + "input": { + "chain_id": "1", "release_id": "classic-v3", "model_id": "classic-v3", + "source_group": "core", "epoch_id": "11111111-2222-4333-8444-555555555555", + "pointer_generation": "7", "runtime_code_evidence_id": "eeeeeeee-0000-4000-8000-000000000005", + "dynamic_source_template_id": "ffffffff-0000-4000-8000-000000000006", + "parent_factory_occurrence_id": "12121212-0000-4000-8000-000000000007", + "parent_factory_release_binding_id": "13131313-0000-4000-8000-000000000008", + "parent_factory_binding_commitment": "0x9999999999999999999999999999999999999999999999999999999999999999", + "deployed_source_address": "0x3333333333333333333333333333333333333333", + "deployed_source_role": "reward_vault", "deployment_block_number": "25639600", + "deployed_artifact_creation_code_commitment": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_immutable_values_commitment": "0x8888888888888888888888888888888888888888888888888888888888888888", + "factory_configuration_commitment": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "constructor_arguments_commitment": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "local_init_code_hash": "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "runtime_code_hash": "0x5555555555555555555555555555555555555555555555555555555555555555", + "abi_event_set_commitment": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000400000000000000010000000a636c61737369632d76330000000a636c61737369632d763300000004636f7265111111112222433384445555555555550000000000000007eeeeeeee000040008000000000000005ffffffff0000400080000000000000061212121200004000800000000000000713131313000040008000000000000008999999999999999999999999999999999999999999999999999999999999999933333333333333333333333333333333333333330000000c7265776172645f7661756c740000000001873ab0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa8888888888888888888888888888888888888888888888888888888888888888bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd5555555555555555555555555555555555555555555555555555555555555555eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "expected_keccak256": "0x7797d11f8db9f299d00ec2e882e3203cf5b669fb578d1020f757a3156c239cf7" + }, + { + "name": "log_coverage_genesis_v2", "subtype": "log_coverage", + "input": { + "chain_id": "1", "epoch_id": "14141414-0000-4000-8000-000000000009", + "pointer_generation": "1", "provider_deployment_id": "15151515-0000-4000-8000-000000000010", + "stream_id": "canonical-events", "expected_cursor_generation": "0", + "next_cursor_generation": "1", "previous_block_number": "25639599", + "previous_block_global_log_index": null, "previous_candidate_id": null, + "from_block_number": "25639600", "to_block_number": "25639600", + "final_block_hash": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "final_block_global_log_index": "4294967295", + "final_candidate_id": "1:0xffff:0xdddd:4294967295", + "safe_head_observation_id": "16161616-0000-4000-8000-000000000011", + "final_block_evidence_id": "17171717-0000-4000-8000-000000000012", + "provider_a_id": "aaaaaaaa-0000-4000-8000-000000000001", + "provider_b_id": "bbbbbbbb-0000-4000-8000-000000000002", + "filter_commitment": "0x0101010101010101010101010101010101010101010101010101010101010101", + "ordered_log_commitments": [ + "0x0202020202020202020202020202020202020202020202020202020202020202", + "0x0303030303030303030303030303030303030303030303030303030303030303" + ], + "page_commitment": "0x0404040404040404040404040404040404040404040404040404040404040404" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200050000000000000001141414140000400080000000000000090000000000000001151515150000400080000000000000100000001063616e6f6e6963616c2d6576656e7473000000000000000000000000000000010000000001873aaf00000000000001873ab00000000001873ab0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000001a313a3078666666663a3078646464643a343239343936373239351616161600004000800000000000001117171717000040008000000000000012aaaaaaaa000040008000000000000001bbbbbbbb000040008000000000000002010101010101010101010101010101010101010101010101010101010101010100000002020202020202020202020202020202020202020202020202020202020202020203030303030303030303030303030303030303030303030303030303030303030404040404040404040404040404040404040404040404040404040404040404", + "expected_keccak256": "0x9819358434f5b8ac5a2d31e6a151387ca54f66b914f7840a56151a9755257c3c" + }, + { + "name": "log_coverage_continuation_v2", "subtype": "log_coverage", + "input": { + "chain_id": "1", "epoch_id": "14141414-0000-4000-8000-000000000009", + "pointer_generation": "1", "provider_deployment_id": "15151515-0000-4000-8000-000000000010", + "stream_id": "canonical-events", "expected_cursor_generation": "1", + "next_cursor_generation": "2", "previous_block_number": "25639600", + "previous_block_global_log_index": "4294967294", + "previous_candidate_id": "1:0xffff:0xcccc:4294967294", + "from_block_number": "25639600", "to_block_number": "25639600", + "final_block_hash": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "final_block_global_log_index": "4294967295", + "final_candidate_id": "1:0xffff:0xdddd:4294967295", + "safe_head_observation_id": "16161616-0000-4000-8000-000000000011", + "final_block_evidence_id": "17171717-0000-4000-8000-000000000012", + "provider_a_id": "aaaaaaaa-0000-4000-8000-000000000001", + "provider_b_id": "bbbbbbbb-0000-4000-8000-000000000002", + "filter_commitment": "0x0101010101010101010101010101010101010101010101010101010101010101", + "ordered_log_commitments": [ + "0x0303030303030303030303030303030303030303030303030303030303030303" + ], + "page_commitment": "0x0505050505050505050505050505050505050505050505050505050505050505" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200050000000000000001141414140000400080000000000000090000000000000001151515150000400080000000000000100000001063616e6f6e6963616c2d6576656e7473000000000000000100000000000000020000000001873ab001fffffffe010000001a313a3078666666663a3078636363633a343239343936373239340000000001873ab00000000001873ab0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000001a313a3078666666663a3078646464643a343239343936373239351616161600004000800000000000001117171717000040008000000000000012aaaaaaaa000040008000000000000001bbbbbbbb00004000800000000000000201010101010101010101010101010101010101010101010101010101010101010000000103030303030303030303030303030303030303030303030303030303030303030505050505050505050505050505050505050505050505050505050505050505", + "expected_keccak256": "0x5b8be97de0c921fc2b7a54698a49d82556f68c32509ee8be2f164da64869f355" + }, + { + "name": "log_coverage_empty_page_v2", "subtype": "log_coverage", + "input": { + "chain_id": "1", "epoch_id": "14141414-0000-4000-8000-000000000009", + "pointer_generation": "1", "provider_deployment_id": "15151515-0000-4000-8000-000000000010", + "stream_id": "canonical-events", "expected_cursor_generation": "2", + "next_cursor_generation": "3", "previous_block_number": "25639600", + "previous_block_global_log_index": "4294967295", + "previous_candidate_id": "1:0xffff:0xdddd:4294967295", + "from_block_number": "25639600", "to_block_number": "25639601", + "final_block_hash": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "final_block_global_log_index": "4294967295", + "final_candidate_id": "empty-page", + "safe_head_observation_id": "16161616-0000-4000-8000-000000000011", + "final_block_evidence_id": "18181818-0000-4000-8000-000000000013", + "provider_a_id": "aaaaaaaa-0000-4000-8000-000000000001", + "provider_b_id": "bbbbbbbb-0000-4000-8000-000000000002", + "filter_commitment": "0x0101010101010101010101010101010101010101010101010101010101010101", + "ordered_log_commitments": [], + "page_commitment": "0x0606060606060606060606060606060606060606060606060606060606060606" + }, + "expected_preimage_hex": "0x70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200050000000000000001141414140000400080000000000000090000000000000001151515150000400080000000000000100000001063616e6f6e6963616c2d6576656e7473000000000000000200000000000000030000000001873ab001ffffffff010000001a313a3078666666663a3078646464643a343239343936373239350000000001873ab00000000001873ab1eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffff0000000a656d7074792d706167651616161600004000800000000000001118181818000040008000000000000013aaaaaaaa000040008000000000000001bbbbbbbb0000400080000000000000020101010101010101010101010101010101010101010101010101010101010101000000000606060606060606060606060606060606060606060606060606060606060606", + "expected_keccak256": "0xd1193e46fdc9e62809a99e5e06f28135e515289e9efc38cea1e29a2cee89703e" + } + ] +} diff --git a/supabase/tests/codec/verify-production-canonical-fingerprint-v1.ts b/supabase/tests/codec/verify-production-canonical-fingerprint-v1.ts new file mode 100644 index 00000000..eb32d91b --- /dev/null +++ b/supabase/tests/codec/verify-production-canonical-fingerprint-v1.ts @@ -0,0 +1,391 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { keccak256 } from "viem"; + +type JsonValue = + | null + | boolean + | number + | string + | JsonValue[] + | { [key: string]: JsonValue }; + +type UnsignedInput = string | number | bigint; + +type OccurrenceInput = { + chain_id: UnsignedInput; + transaction_hash: string; + receipt_log_ordinal: UnsignedInput; + block_number: UnsignedInput; + block_hash: string; + transaction_index: UnsignedInput; + block_global_log_index: UnsignedInput; + source_address: string; + event_signature: string; + ordered_topics: string[]; + raw_data: string; + decoded_payload: JsonValue; + payload_hash: string; + decoder_version: string; + abi_event_set_commitment: string; + release_id: string; + model_id: string; + envio_candidate_id: string; + provider_cursor: string; + block_timestamp_unix: UnsignedInput; +}; + +type OccurrenceReferenceInput = { + transaction_hash: string; + receipt_log_ordinal: UnsignedInput; + block_hash: string; + role: string; +}; + +type AllocationInput = { + chain_id: UnsignedInput; + release_id: string; + model_id: string; + vault: string; + factory_transaction_hash: string; + factory_receipt_log_ordinal: UnsignedInput; + factory_block_hash: string; + creation_block_number: UnsignedInput; + creation_transaction_index: UnsignedInput; + ordered_beneficiaries: string[]; + ordered_shares_bps: UnsignedInput[]; + allocation_hash: string; + configuration_hash: string; + active_configuration_hash: string | null; + artifact_creation_code_commitment: string; + required_occurrences: OccurrenceReferenceInput[]; +}; + +type EvidenceInput = { + allocation_fingerprint: string; + recovery_method: string; + evidence_version: string; + top_level_destination: string | null; + method_selector: string | null; + transaction_input_hash: string | null; + constructor_arguments_commitment: string; + local_init_code_hash: string; + create2_salt: string; + local_create2_address: string; + historical_enrichment_status: string; + getter_block_hash: string | null; + getter_result_hash_a: string | null; + getter_result_hash_b: string | null; + predict_result_hash_a: string | null; + predict_result_hash_b: string | null; + predicted_vault_a: string | null; + predicted_vault_b: string | null; + selected_rpc_result_hash_a: string; + selected_rpc_result_hash_b: string; + selected_rpc_transaction_receipt_hash_a: string | null; + selected_rpc_transaction_receipt_hash_b: string | null; + extra_note: string | null; + required_occurrence_fingerprints: string[]; +}; + +type FixtureVector = { + name: string; + domain: "occurrence" | "allocation" | "evidence"; + input: OccurrenceInput | AllocationInput | EvidenceInput; + expected_preimage_hex: `0x${string}`; + expected_keccak256: `0x${string}`; +}; + +function concatenate(...chunks: Uint8Array[]): Uint8Array { + const output = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.length, 0)); + let cursor = 0; + for (const chunk of chunks) { + output.set(chunk, cursor); + cursor += chunk.length; + } + return output; +} + +export function decodeCanonicalHex(value: string, byteLength?: number): Uint8Array { + if (!value.startsWith("0x")) throw new Error("canonical hex input requires 0x"); + const digits = value.slice(2); + if (digits.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(digits)) { + throw new Error("canonical hex input must be even-length hexadecimal"); + } + if (byteLength !== undefined && digits.length !== byteLength * 2) { + throw new Error(`canonical hex input must be exactly ${byteLength} bytes`); + } + return Uint8Array.from(Buffer.from(digits, "hex")); +} + +function encodeUnsigned(value: string | number | bigint, width: number): Uint8Array { + let integer = BigInt(value); + if (integer < 0n || integer >= 1n << BigInt(width * 8)) { + throw new Error(`unsigned integer exceeds ${width * 8} bits`); + } + const encoded = new Uint8Array(width); + for (let index = width - 1; index >= 0; index -= 1) { + encoded[index] = Number(integer & 0xffn); + integer >>= 8n; + } + return encoded; +} + +function frameBytes(value: Uint8Array): Uint8Array { + return concatenate(encodeUnsigned(value.length, 4), value); +} + +function frameString(value: string): Uint8Array { + return frameBytes(new TextEncoder().encode(value)); +} + +function frameNullable( + value: T | null, + encodePresent: (present: T) => Uint8Array, +): Uint8Array { + return value === null + ? Uint8Array.of(0) + : concatenate(Uint8Array.of(1), encodePresent(value)); +} + +function frameArray(values: T[], encodeElement: (value: T) => Uint8Array): Uint8Array { + return concatenate(encodeUnsigned(values.length, 4), ...values.map(encodeElement)); +} + +export function canonicalizeJson(value: JsonValue): string { + if (value === null || typeof value === "boolean" || typeof value === "string") { + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value) || !Number.isSafeInteger(value)) { + throw new Error("JCS numbers must be finite safe integers; uint256 values are strings"); + } + return JSON.stringify(value); + } + if (Array.isArray(value)) return `[${value.map(canonicalizeJson).join(",")}]`; + const members = Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalizeJson(value[key])}`); + return `{${members.join(",")}}`; +} + +const domainPrefix = { + occurrence: new TextEncoder().encode("programmable:occurrence:v1\0"), + allocation: new TextEncoder().encode("programmable:allocation:v1\0"), + evidence: new TextEncoder().encode("programmable:evidence:v1\0"), +} as const; + +function productionOccurrencePreimage(input: OccurrenceInput): Uint8Array { + return concatenate( + domainPrefix.occurrence, + encodeUnsigned(input.chain_id, 8), + decodeCanonicalHex(input.transaction_hash, 32), + encodeUnsigned(input.receipt_log_ordinal, 4), + encodeUnsigned(input.block_number, 8), + decodeCanonicalHex(input.block_hash, 32), + encodeUnsigned(input.transaction_index, 4), + encodeUnsigned(input.block_global_log_index, 4), + decodeCanonicalHex(input.source_address, 20), + decodeCanonicalHex(input.event_signature, 32), + frameArray(input.ordered_topics, (topic) => decodeCanonicalHex(topic, 32)), + frameBytes(decodeCanonicalHex(input.raw_data)), + frameString(canonicalizeJson(input.decoded_payload)), + decodeCanonicalHex(input.payload_hash, 32), + frameString(input.decoder_version), + decodeCanonicalHex(input.abi_event_set_commitment, 32), + frameString(input.release_id), + frameString(input.model_id), + frameString(input.envio_candidate_id), + frameString(input.provider_cursor), + encodeUnsigned(input.block_timestamp_unix, 8), + ); +} + +function productionOccurrenceReference(reference: OccurrenceReferenceInput): Uint8Array { + return concatenate( + decodeCanonicalHex(reference.transaction_hash, 32), + encodeUnsigned(reference.receipt_log_ordinal, 4), + decodeCanonicalHex(reference.block_hash, 32), + frameString(reference.role), + ); +} + +function productionAllocationPreimage(input: AllocationInput): Uint8Array { + return concatenate( + domainPrefix.allocation, + encodeUnsigned(input.chain_id, 8), + frameString(input.release_id), + frameString(input.model_id), + decodeCanonicalHex(input.vault, 20), + decodeCanonicalHex(input.factory_transaction_hash, 32), + encodeUnsigned(input.factory_receipt_log_ordinal, 4), + decodeCanonicalHex(input.factory_block_hash, 32), + encodeUnsigned(input.creation_block_number, 8), + encodeUnsigned(input.creation_transaction_index, 4), + frameArray(input.ordered_beneficiaries, (address) => decodeCanonicalHex(address, 20)), + frameArray(input.ordered_shares_bps, (share) => encodeUnsigned(share, 2)), + decodeCanonicalHex(input.allocation_hash, 32), + decodeCanonicalHex(input.configuration_hash, 32), + frameNullable(input.active_configuration_hash, (hash) => decodeCanonicalHex(hash, 32)), + decodeCanonicalHex(input.artifact_creation_code_commitment, 32), + frameArray(input.required_occurrences, productionOccurrenceReference), + ); +} + +function productionEvidencePreimage(input: EvidenceInput): Uint8Array { + const nullableHash = (value: string | null) => + frameNullable(value, (hash) => decodeCanonicalHex(hash, 32)); + return concatenate( + domainPrefix.evidence, + decodeCanonicalHex(input.allocation_fingerprint, 32), + frameString(input.recovery_method), + frameString(input.evidence_version), + frameNullable(input.top_level_destination, (address) => decodeCanonicalHex(address, 20)), + frameNullable(input.method_selector, (selector) => decodeCanonicalHex(selector, 4)), + nullableHash(input.transaction_input_hash), + decodeCanonicalHex(input.constructor_arguments_commitment, 32), + decodeCanonicalHex(input.local_init_code_hash, 32), + decodeCanonicalHex(input.create2_salt, 32), + decodeCanonicalHex(input.local_create2_address, 20), + frameString(input.historical_enrichment_status), + nullableHash(input.getter_block_hash), + nullableHash(input.getter_result_hash_a), + nullableHash(input.getter_result_hash_b), + nullableHash(input.predict_result_hash_a), + nullableHash(input.predict_result_hash_b), + frameNullable(input.predicted_vault_a, (address) => decodeCanonicalHex(address, 20)), + frameNullable(input.predicted_vault_b, (address) => decodeCanonicalHex(address, 20)), + decodeCanonicalHex(input.selected_rpc_result_hash_a, 32), + decodeCanonicalHex(input.selected_rpc_result_hash_b, 32), + nullableHash(input.selected_rpc_transaction_receipt_hash_a), + nullableHash(input.selected_rpc_transaction_receipt_hash_b), + frameNullable(input.extra_note, frameString), + frameArray( + input.required_occurrence_fingerprints, + (fingerprint) => decodeCanonicalHex(fingerprint, 32), + ), + ); +} + +export function productionCanonicalPreimage( + domain: FixtureVector["domain"], + input: FixtureVector["input"], +): Uint8Array { + if (domain === "occurrence") { + return productionOccurrencePreimage(input as OccurrenceInput); + } + if (domain === "allocation") { + return productionAllocationPreimage(input as AllocationInput); + } + return productionEvidencePreimage(input as EvidenceInput); +} + +function bytesToHex(value: Uint8Array): `0x${string}` { + return `0x${Buffer.from(value).toString("hex")}`; +} + +function expectEqual(actual: string, expected: string, label: string): void { + if (actual !== expected) { + throw new Error(`${label}\nexpected ${expected}\nactual ${actual}`); + } +} + +function expectRejected(label: string, operation: () => unknown): void { + let rejected = false; + try { + operation(); + } catch { + rejected = true; + } + if (!rejected) throw new Error(`${label} was accepted`); +} + +function verifyFixture(): void { + const fixture = JSON.parse( + readFileSync(new URL("./canonical-fingerprint-v1.json", import.meta.url), "utf8"), + ) as { + sentinel_vectors: Array<{ + name: string; + expected_preimage_hex: `0x${string}`; + expected_keccak256: `0x${string}`; + }>; + vectors: FixtureVector[]; + }; + for (const sentinel of fixture.sentinel_vectors) { + expectEqual( + keccak256(sentinel.expected_preimage_hex), + sentinel.expected_keccak256, + `${sentinel.name} Keccak-256`, + ); + } + for (const vector of fixture.vectors) { + const preimage = productionCanonicalPreimage(vector.domain, vector.input); + const preimageHex = bytesToHex(preimage); + expectEqual(preimageHex, vector.expected_preimage_hex, `${vector.name} preimage`); + expectEqual(keccak256(preimageHex), vector.expected_keccak256, `${vector.name} digest`); + } + + expectRejected("missing 0x prefix", () => decodeCanonicalHex("00", 1)); + expectRejected("odd-length hex", () => decodeCanonicalHex("0x0")); + expectRejected("invalid hex digit", () => decodeCanonicalHex("0x0g")); + expectRejected("under-width address", () => decodeCanonicalHex(`0x${"11".repeat(19)}`, 20)); + expectRejected("over-width hash", () => decodeCanonicalHex(`0x${"22".repeat(33)}`, 32)); + expectRejected("under-width selector", () => decodeCanonicalHex("0x010203", 4)); + expectEqual(bytesToHex(decodeCanonicalHex("0x0001", 2)), "0x0001", "leading zero"); + + const occurrence = fixture.vectors.find((vector) => + vector.name === "occurrence_all_fields_v1")!; + const occurrenceInput = occurrence.input as OccurrenceInput; + const mixedCase = structuredClone(occurrenceInput); + mixedCase.source_address = mixedCase.source_address.toUpperCase().replace("0X", "0x"); + expectEqual( + bytesToHex(productionCanonicalPreimage("occurrence", mixedCase)), + occurrence.expected_preimage_hex, + "mixed-case hex normalization", + ); + const reorderedTopics = structuredClone(occurrenceInput); + reorderedTopics.ordered_topics.reverse(); + if ( + bytesToHex(productionCanonicalPreimage("occurrence", reorderedTopics)) + === occurrence.expected_preimage_hex + ) { + throw new Error("sorting topics unexpectedly preserved the fixed preimage"); + } + const orderAB = fixture.vectors.find((vector) => vector.name === "allocation_order_ab_v1")!; + const orderBA = fixture.vectors.find((vector) => vector.name === "allocation_order_ba_v1")!; + if (orderAB.expected_preimage_hex === orderBA.expected_preimage_hex) { + throw new Error("allocation ordering is not committed"); + } + const nullOptional = fixture.vectors.find((vector) => + vector.name === "evidence_null_optional_v1")!; + const presentEmpty = fixture.vectors.find((vector) => + vector.name === "evidence_present_empty_v1")!; + if (nullOptional.expected_preimage_hex === presentEmpty.expected_preimage_hex) { + throw new Error("null and present-empty evidence are not distinct"); + } + const evidence = fixture.vectors.find((vector) => + vector.name === "evidence_all_fields_v1")!; + const changedConstructor = structuredClone(evidence.input as EvidenceInput); + changedConstructor.constructor_arguments_commitment = `0x${"c1".repeat(32)}`; + if ( + bytesToHex(productionCanonicalPreimage("evidence", changedConstructor)) + === evidence.expected_preimage_hex + ) { + throw new Error("constructor arguments are not committed independently"); + } + const changedSalt = structuredClone(evidence.input as EvidenceInput); + changedSalt.create2_salt = `0x${"c3".repeat(32)}`; + if ( + bytesToHex(productionCanonicalPreimage("evidence", changedSalt)) + === evidence.expected_preimage_hex + ) { + throw new Error("CREATE2 salt is not committed independently"); + } + console.log(`production canonical fingerprint v1: ${fixture.vectors.length} vectors passed`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + verifyFixture(); +} diff --git a/supabase/tests/codec/verify-production-provider-evidence-v2.ts b/supabase/tests/codec/verify-production-provider-evidence-v2.ts new file mode 100644 index 00000000..47bbd823 --- /dev/null +++ b/supabase/tests/codec/verify-production-provider-evidence-v2.ts @@ -0,0 +1,186 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { keccak256, type Hex } from "viem"; + +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; +type Fixture = { + subtype_tags: Record; + field_schemas: Record; + expected_definition_commitments: Record; + expected_domain_definition_commitment: Hex; + vectors: Array<{ + name: string; + subtype: string; + input: Record; + expected_preimage_hex: Hex; + expected_keccak256: Hex; + }>; +}; + +const utf8 = (value: string) => Buffer.from(value, "utf8"); +const BASE_PREFIX = utf8("programmable:provider-evidence:v2\0"); +const DEFINITION_PREFIX = utf8("programmable:provider-evidence-definition:v2\0"); +const DOMAIN_DEFINITION_PREFIX = utf8("programmable:provider-evidence-domain:v2\0"); + +function fixedUnsigned(value: unknown, width: 4 | 8): Buffer { + const parsed = BigInt(value as string | number | bigint); + if (parsed < 0n || parsed >= (1n << BigInt(width * 8))) { + throw new Error(`unsigned integer exceeds ${width * 8} bits`); + } + const result = Buffer.alloc(width); + if (width === 4) result.writeUInt32BE(Number(parsed)); + else result.writeBigUInt64BE(parsed); + return result; +} + +function exactHex(value: unknown, width?: number): Buffer { + if (typeof value !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(value)) { + throw new Error("invalid canonical hex"); + } + const result = Buffer.from(value.slice(2), "hex"); + if (width !== undefined && result.length !== width) throw new Error(`expected ${width} bytes`); + return result; +} + +function exactUuid(value: unknown): Buffer { + if (typeof value !== "string" || + !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value)) { + throw new Error("invalid UUID"); + } + return Buffer.from(value.replaceAll("-", ""), "hex"); +} + +function framed(value: Uint8Array): Buffer { + return Buffer.concat([fixedUnsigned(value.length, 4), value]); +} + +function encodeType(type: string, value: unknown): Buffer { + if (type === "u32") return fixedUnsigned(value, 4); + if (type === "u64") return fixedUnsigned(value, 8); + if (type === "uuid16") return exactUuid(value); + if (type === "varutf8") { + if (typeof value !== "string") throw new Error("expected text"); + return framed(utf8(value)); + } + if (type === "varbytes") return framed(exactHex(value)); + const fixedBytes = /^bytes(\d+)$/.exec(type); + if (fixedBytes) return exactHex(value, Number(fixedBytes[1])); + const optional = /^optional<(.+)>$/.exec(type); + if (optional) { + return value === null + ? Buffer.from([0]) + : Buffer.concat([Buffer.from([1]), encodeType(optional[1], value)]); + } + const arrayType = /^array<(.+)>$/.exec(type); + if (arrayType) { + if (!Array.isArray(value)) throw new Error("expected array"); + return Buffer.concat([ + fixedUnsigned(value.length, 4), + ...value.map((item) => encodeType(arrayType[1], item)), + ]); + } + throw new Error(`unknown provider evidence field type ${type}`); +} + +export function productionProviderEvidencePreimage( + subtype: string, + input: Record, + fixture: Pick, +): Buffer { + const tag = fixture.subtype_tags[subtype]; + const schema = fixture.field_schemas[subtype]; + if (!Number.isInteger(tag) || tag < 1 || tag > 255 || !schema) { + throw new Error(`unknown provider evidence subtype ${subtype}`); + } + return Buffer.concat([ + BASE_PREFIX, + Buffer.from([tag]), + ...schema.map(([name, type]) => encodeType(type, input[name])), + ]); +} + +function canonicalJson(value: Json): string { + if (value === null || typeof value === "boolean" || typeof value === "string") { + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value) || !Number.isSafeInteger(value)) { + throw new Error("definition JSON requires finite safe integers"); + } + return JSON.stringify(value); + } + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + return `{${Object.keys(value).sort().map( + (key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`, + ).join(",")}}`; +} + +function definitionPreimage(tag: number, schema: [string, string][]): Buffer { + return Buffer.concat([ + DEFINITION_PREFIX, + Buffer.from([tag]), + framed(utf8(canonicalJson(schema))), + ]); +} + +function domainDefinitionPreimage(commitments: readonly Hex[]): Buffer { + return Buffer.concat([ + DOMAIN_DEFINITION_PREFIX, + fixedUnsigned(commitments.length, 4), + ...commitments.map((value) => exactHex(value, 32)), + ]); +} + +const asHex = (value: Uint8Array): Hex => `0x${Buffer.from(value).toString("hex")}`; +const digest = (value: Uint8Array): Hex => keccak256(asHex(value)); + +function mutate(subtype: string, input: Record): Record { + const changed = structuredClone(input); + if (subtype === "safe_head") changed.head_b = (BigInt(changed.head_b as string) + 1n).toString(); + if (subtype === "block") { + changed.block_number = (BigInt(changed.block_number as string) - 1n).toString(); + } + if (subtype === "runtime_code") changed.runtime_code_b = "0x6002600055"; + if (subtype === "dynamic_attestation") changed.deployed_source_role = "vesting_wallet"; + if (subtype === "log_coverage") { + changed.page_commitment = `0x${"ff".repeat(32)}`; + } + return changed; +} + +function verify(): void { + const fixture = JSON.parse( + readFileSync(new URL("./provider-evidence-v2.json", import.meta.url), "utf8"), + ) as Fixture; + const commitments: Hex[] = []; + for (const [subtype, schema] of Object.entries(fixture.field_schemas)) { + const actual = digest(definitionPreimage(fixture.subtype_tags[subtype], schema)); + if (actual !== fixture.expected_definition_commitments[subtype]) { + throw new Error(`${subtype} definition commitment mismatch`); + } + commitments.push(actual); + } + if (digest(domainDefinitionPreimage(commitments)) !== fixture.expected_domain_definition_commitment) { + throw new Error("provider evidence domain definition commitment mismatch"); + } + for (const vector of fixture.vectors) { + const preimage = productionProviderEvidencePreimage(vector.subtype, vector.input, fixture); + if (asHex(preimage) !== vector.expected_preimage_hex) { + throw new Error(`${vector.name} preimage mismatch`); + } + if (digest(preimage) !== vector.expected_keccak256) { + throw new Error(`${vector.name} digest mismatch`); + } + const mutation = productionProviderEvidencePreimage( + vector.subtype, mutate(vector.subtype, vector.input), fixture, + ); + if (digest(mutation) === vector.expected_keccak256) { + throw new Error(`${vector.name} one-field mutation preserved digest`); + } + } + console.log(`production provider evidence v2: ${fixture.vectors.length} vectors passed`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) verify(); diff --git a/supabase/tests/codec/verify-reference-canonical-fingerprint-v1.mjs b/supabase/tests/codec/verify-reference-canonical-fingerprint-v1.mjs new file mode 100644 index 00000000..ea18a97d --- /dev/null +++ b/supabase/tests/codec/verify-reference-canonical-fingerprint-v1.mjs @@ -0,0 +1,276 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import canonicalize from "canonicalize"; +import { keccak_256 } from "@noble/hashes/sha3.js"; + +export function referenceKeccak256(bytes) { + return keccak_256(bytes); +} + +function join(parts) { + const length = parts.reduce((sum, part) => sum + part.length, 0); + const result = new Uint8Array(length); + let offset = 0; + for (const part of parts) { + result.set(part, offset); + offset += part.length; + } + return result; +} + +function parseHex(input, expectedBytes) { + if (typeof input !== "string" || !input.startsWith("0x")) { + throw new Error("hex values require a 0x prefix"); + } + const body = input.slice(2); + if (body.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(body)) { + throw new Error("hex values must contain an even number of hexadecimal digits"); + } + if (expectedBytes !== undefined && body.length !== expectedBytes * 2) { + throw new Error(`expected ${expectedBytes} bytes`); + } + return Uint8Array.from(Buffer.from(body, "hex")); +} + +function unsigned(value, width) { + const parsed = BigInt(value); + if (parsed < 0n || parsed >= (1n << BigInt(width * 8))) { + throw new Error(`unsigned integer does not fit ${width} bytes`); + } + const output = new Uint8Array(width); + let remaining = parsed; + for (let index = width - 1; index >= 0; index -= 1) { + output[index] = Number(remaining & 0xffn); + remaining >>= 8n; + } + return output; +} + +function variableBytes(bytes) { + return join([unsigned(bytes.length, 4), bytes]); +} + +function variableText(value) { + return variableBytes(new TextEncoder().encode(value)); +} + +function optional(value, encoder) { + return value === null ? Uint8Array.of(0) : join([Uint8Array.of(1), encoder(value)]); +} + +function orderedArray(values, encoder) { + return join([unsigned(values.length, 4), ...values.map(encoder)]); +} + +function canonicalJson(value) { + if (typeof value === "number" && (!Number.isFinite(value) || !Number.isSafeInteger(value))) { + throw new Error("JCS numbers must be finite safe integers; uint256 uses strings"); + } + if (Array.isArray(value)) value.forEach(canonicalJson); + if (value !== null && typeof value === "object") { + Object.values(value).forEach(canonicalJson); + } + const encoded = canonicalize(value); + if (encoded === undefined) throw new Error("unsupported JCS value"); + return encoded; +} + +const PREFIXES = { + occurrence: new TextEncoder().encode("programmable:occurrence:v1\0"), + allocation: new TextEncoder().encode("programmable:allocation:v1\0"), + evidence: new TextEncoder().encode("programmable:evidence:v1\0"), +}; + +function encodeOccurrence(input) { + return join([ + PREFIXES.occurrence, + unsigned(input.chain_id, 8), + parseHex(input.transaction_hash, 32), + unsigned(input.receipt_log_ordinal, 4), + unsigned(input.block_number, 8), + parseHex(input.block_hash, 32), + unsigned(input.transaction_index, 4), + unsigned(input.block_global_log_index, 4), + parseHex(input.source_address, 20), + parseHex(input.event_signature, 32), + orderedArray(input.ordered_topics, (topic) => parseHex(topic, 32)), + variableBytes(parseHex(input.raw_data)), + variableText(canonicalJson(input.decoded_payload)), + parseHex(input.payload_hash, 32), + variableText(input.decoder_version), + parseHex(input.abi_event_set_commitment, 32), + variableText(input.release_id), + variableText(input.model_id), + variableText(input.envio_candidate_id), + variableText(input.provider_cursor), + unsigned(input.block_timestamp_unix, 8), + ]); +} + +function encodeOccurrenceReference(reference) { + return join([ + parseHex(reference.transaction_hash, 32), + unsigned(reference.receipt_log_ordinal, 4), + parseHex(reference.block_hash, 32), + variableText(reference.role), + ]); +} + +function encodeAllocation(input) { + return join([ + PREFIXES.allocation, + unsigned(input.chain_id, 8), + variableText(input.release_id), + variableText(input.model_id), + parseHex(input.vault, 20), + parseHex(input.factory_transaction_hash, 32), + unsigned(input.factory_receipt_log_ordinal, 4), + parseHex(input.factory_block_hash, 32), + unsigned(input.creation_block_number, 8), + unsigned(input.creation_transaction_index, 4), + orderedArray(input.ordered_beneficiaries, (address) => parseHex(address, 20)), + orderedArray(input.ordered_shares_bps, (share) => unsigned(share, 2)), + parseHex(input.allocation_hash, 32), + parseHex(input.configuration_hash, 32), + optional(input.active_configuration_hash, (hash) => parseHex(hash, 32)), + parseHex(input.artifact_creation_code_commitment, 32), + orderedArray(input.required_occurrences, encodeOccurrenceReference), + ]); +} + +function encodeEvidence(input) { + return join([ + PREFIXES.evidence, + parseHex(input.allocation_fingerprint, 32), + variableText(input.recovery_method), + variableText(input.evidence_version), + optional(input.top_level_destination, (value) => parseHex(value, 20)), + optional(input.method_selector, (value) => parseHex(value, 4)), + optional(input.transaction_input_hash, (value) => parseHex(value, 32)), + parseHex(input.constructor_arguments_commitment, 32), + parseHex(input.local_init_code_hash, 32), + parseHex(input.create2_salt, 32), + parseHex(input.local_create2_address, 20), + variableText(input.historical_enrichment_status), + optional(input.getter_block_hash, (value) => parseHex(value, 32)), + optional(input.getter_result_hash_a, (value) => parseHex(value, 32)), + optional(input.getter_result_hash_b, (value) => parseHex(value, 32)), + optional(input.predict_result_hash_a, (value) => parseHex(value, 32)), + optional(input.predict_result_hash_b, (value) => parseHex(value, 32)), + optional(input.predicted_vault_a, (value) => parseHex(value, 20)), + optional(input.predicted_vault_b, (value) => parseHex(value, 20)), + parseHex(input.selected_rpc_result_hash_a, 32), + parseHex(input.selected_rpc_result_hash_b, 32), + optional( + input.selected_rpc_transaction_receipt_hash_a, + (value) => parseHex(value, 32), + ), + optional( + input.selected_rpc_transaction_receipt_hash_b, + (value) => parseHex(value, 32), + ), + optional(input.extra_note, (value) => variableText(value)), + orderedArray( + input.required_occurrence_fingerprints, + (value) => parseHex(value, 32), + ), + ]); +} + +export function referenceEncode(domain, input) { + if (domain === "occurrence") return encodeOccurrence(input); + if (domain === "allocation") return encodeAllocation(input); + if (domain === "evidence") return encodeEvidence(input); + throw new Error(`unknown domain ${domain}`); +} + +function toHex(bytes) { + return `0x${Buffer.from(bytes).toString("hex")}`; +} + +function assertEqual(actual, expected, label) { + if (actual !== expected) { + throw new Error(`${label}\nexpected ${expected}\nactual ${actual}`); + } +} + +function runFixture() { + const fixturePath = new URL("./canonical-fingerprint-v1.json", import.meta.url); + const fixture = JSON.parse(readFileSync(fixturePath, "utf8")); + for (const sentinel of fixture.sentinel_vectors) { + const preimage = parseHex(sentinel.expected_preimage_hex); + assertEqual( + toHex(referenceKeccak256(preimage)), + sentinel.expected_keccak256, + `${sentinel.name} digest`, + ); + } + for (const vector of fixture.vectors) { + const preimage = referenceEncode(vector.domain, vector.input); + assertEqual(toHex(preimage), vector.expected_preimage_hex, `${vector.name} preimage`); + assertEqual( + toHex(referenceKeccak256(preimage)), + vector.expected_keccak256, + `${vector.name} digest`, + ); + } + + const mixed = structuredClone( + fixture.vectors.find((vector) => vector.name === "occurrence_all_fields_v1"), + ); + mixed.input.transaction_hash = mixed.input.transaction_hash.toUpperCase().replace("0X", "0x"); + assertEqual( + toHex(referenceEncode(mixed.domain, mixed.input)), + fixture.vectors.find((vector) => vector.name === "occurrence_all_fields_v1") + .expected_preimage_hex, + "mixed-case input normalization", + ); + for (const malformed of ["11", "0x1", "0xzz"]) { + let rejected = false; + try { + parseHex(malformed, 1); + } catch { + rejected = true; + } + if (!rejected) throw new Error(`malformed hex was accepted: ${malformed}`); + } + if (parseHex("0x0001", 2)[0] !== 0) throw new Error("leading zero was not preserved"); + + const allocationAB = fixture.vectors.find( + (vector) => vector.name === "allocation_order_ab_v1", + ); + const allocationBA = fixture.vectors.find( + (vector) => vector.name === "allocation_order_ba_v1", + ); + if (allocationAB.expected_preimage_hex === allocationBA.expected_preimage_hex) { + throw new Error("allocation array reversal did not change the preimage"); + } + const occurrence = fixture.vectors.find( + (vector) => vector.name === "occurrence_all_fields_v1", + ); + const sortedTopics = structuredClone(occurrence.input); + sortedTopics.ordered_topics.reverse(); + if (toHex(referenceEncode("occurrence", sortedTopics)) === occurrence.expected_preimage_hex) { + throw new Error("topic-order mutation matched the fixed vector"); + } + const evidence = fixture.vectors.find( + (vector) => vector.name === "evidence_all_fields_v1", + ); + const changedConstructor = structuredClone(evidence.input); + changedConstructor.constructor_arguments_commitment = `0x${"c1".repeat(32)}`; + if (toHex(referenceEncode("evidence", changedConstructor)) === evidence.expected_preimage_hex) { + throw new Error("constructor arguments are not committed independently"); + } + const changedSalt = structuredClone(evidence.input); + changedSalt.create2_salt = `0x${"c3".repeat(32)}`; + if (toHex(referenceEncode("evidence", changedSalt)) === evidence.expected_preimage_hex) { + throw new Error("CREATE2 salt is not committed independently"); + } + console.log(`reference canonical fingerprint v1: ${fixture.vectors.length} vectors passed`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + runFixture(); +} diff --git a/supabase/tests/codec/verify-reference-provider-evidence-v2.mjs b/supabase/tests/codec/verify-reference-provider-evidence-v2.mjs new file mode 100644 index 00000000..84af0b19 --- /dev/null +++ b/supabase/tests/codec/verify-reference-provider-evidence-v2.mjs @@ -0,0 +1,223 @@ +#!/usr/bin/env node + +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import canonicalize from "canonicalize"; +import { keccak_256 } from "@noble/hashes/sha3.js"; + +const encoder = new TextEncoder(); +const BASE_PREFIX = encoder.encode("programmable:provider-evidence:v2\0"); +const DEFINITION_PREFIX = encoder.encode( + "programmable:provider-evidence-definition:v2\0", +); +const DOMAIN_DEFINITION_PREFIX = encoder.encode( + "programmable:provider-evidence-domain:v2\0", +); + +function join(parts) { + const output = new Uint8Array(parts.reduce((sum, value) => sum + value.length, 0)); + let offset = 0; + for (const part of parts) { + output.set(part, offset); + offset += part.length; + } + return output; +} + +function hex(value, width) { + if (typeof value !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(value)) { + throw new Error("invalid canonical hex"); + } + const result = Uint8Array.from(Buffer.from(value.slice(2), "hex")); + if (width !== undefined && result.length !== width) throw new Error(`expected ${width} bytes`); + return result; +} + +function uuid(value) { + if (typeof value !== "string" || + !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value)) { + throw new Error("invalid UUID"); + } + return Uint8Array.from(Buffer.from(value.replaceAll("-", ""), "hex")); +} + +function unsigned(value, width) { + let remaining = BigInt(value); + if (remaining < 0n || remaining >= (1n << BigInt(width * 8))) { + throw new Error(`unsigned integer exceeds ${width * 8} bits`); + } + const result = new Uint8Array(width); + for (let index = width - 1; index >= 0; index -= 1) { + result[index] = Number(remaining & 255n); + remaining >>= 8n; + } + return result; +} + +const u32 = (value) => unsigned(value, 4); +const u64 = (value) => unsigned(value, 8); +const variable = (value) => join([u32(value.length), value]); +const text = (value) => variable(encoder.encode(value)); +const optional = (value, encode) => value === null + ? Uint8Array.of(0) + : join([Uint8Array.of(1), encode(value)]); +const array = (values, encode) => join([u32(values.length), ...values.map(encode)]); +const scope = (input) => [u64(input.chain_id), uuid(input.epoch_id), u64(input.pointer_generation)]; + +const TAGS = Object.freeze({ + safe_head: 1, block: 2, runtime_code: 3, dynamic_attestation: 4, log_coverage: 5, +}); + +function safeHead(input) { + return [ + ...scope(input), uuid(input.provider_a_id), uuid(input.provider_b_id), + u64(input.reported_chain_id_a), u64(input.reported_chain_id_b), + u64(input.head_a), u64(input.head_b), u32(input.finality_depth), + u64(input.safe_block_number), hex(input.safe_block_hash_a, 32), + hex(input.safe_block_hash_b, 32), + ]; +} + +function block(input) { + return [ + ...scope(input), uuid(input.observation_id), u64(input.block_number), + hex(input.provider_a_block_hash, 32), hex(input.provider_b_block_hash, 32), + ]; +} + +function runtimeCode(input) { + return [ + u64(input.chain_id), text(input.release_id), text(input.model_id), + text(input.source_group), uuid(input.epoch_id), u64(input.pointer_generation), + hex(input.source_address, 20), uuid(input.deployment_block_evidence_id), + u64(input.deployment_block_number), hex(input.deployment_block_hash, 32), + uuid(input.provider_a_id), uuid(input.provider_b_id), + hex(input.runtime_code_hash_a, 32), hex(input.runtime_code_hash_b, 32), + variable(hex(input.runtime_code_a)), variable(hex(input.runtime_code_b)), + hex(input.normalized_runtime_code_hash_a, 32), + hex(input.normalized_runtime_code_hash_b, 32), + hex(input.immutable_references_commitment, 32), + array(input.immutable_values, (value) => variable(hex(value))), + hex(input.immutable_values_commitment, 32), + variable(hex(input.reconstructed_runtime_code)), + hex(input.reconstructed_runtime_code_hash, 32), + ]; +} + +function dynamicAttestation(input) { + return [ + u64(input.chain_id), text(input.release_id), text(input.model_id), + text(input.source_group), uuid(input.epoch_id), u64(input.pointer_generation), + uuid(input.runtime_code_evidence_id), uuid(input.dynamic_source_template_id), + uuid(input.parent_factory_occurrence_id), + uuid(input.parent_factory_release_binding_id), + hex(input.parent_factory_binding_commitment, 32), + hex(input.deployed_source_address, 20), text(input.deployed_source_role), + u64(input.deployment_block_number), + hex(input.deployed_artifact_creation_code_commitment, 32), + hex(input.expected_immutable_values_commitment, 32), + hex(input.factory_configuration_commitment, 32), + hex(input.constructor_arguments_commitment, 32), + hex(input.local_init_code_hash, 32), hex(input.runtime_code_hash, 32), + hex(input.abi_event_set_commitment, 32), + ]; +} + +function logCoverage(input) { + return [ + ...scope(input), uuid(input.provider_deployment_id), text(input.stream_id), + u64(input.expected_cursor_generation), u64(input.next_cursor_generation), + u64(input.previous_block_number), + optional(input.previous_block_global_log_index, u32), + optional(input.previous_candidate_id, text), + u64(input.from_block_number), u64(input.to_block_number), + hex(input.final_block_hash, 32), u32(input.final_block_global_log_index), + text(input.final_candidate_id), uuid(input.safe_head_observation_id), + uuid(input.final_block_evidence_id), uuid(input.provider_a_id), + uuid(input.provider_b_id), hex(input.filter_commitment, 32), + array(input.ordered_log_commitments, (value) => hex(value, 32)), + hex(input.page_commitment, 32), + ]; +} + +export function referenceProviderEvidencePreimage(subtype, input) { + const encode = { + safe_head: safeHead, + block, + runtime_code: runtimeCode, + dynamic_attestation: dynamicAttestation, + log_coverage: logCoverage, + }[subtype]; + if (!encode) throw new Error(`unknown provider evidence subtype ${subtype}`); + return join([BASE_PREFIX, Uint8Array.of(TAGS[subtype]), ...encode(input)]); +} + +export function referenceDefinitionPreimage(subtype, schema) { + return join([ + DEFINITION_PREFIX, Uint8Array.of(TAGS[subtype]), + variable(encoder.encode(canonicalize(schema))), + ]); +} + +export function referenceDomainDefinitionPreimage(commitments) { + return join([ + DOMAIN_DEFINITION_PREFIX, + array(commitments, (commitment) => hex(commitment, 32)), + ]); +} + +const toHex = (value) => `0x${Buffer.from(value).toString("hex")}`; +const digest = (value) => toHex(keccak_256(value)); + +function mutate(subtype, input) { + const changed = structuredClone(input); + if (subtype === "safe_head") changed.head_b = (BigInt(changed.head_b) + 1n).toString(); + if (subtype === "block") changed.block_number = (BigInt(changed.block_number) - 1n).toString(); + if (subtype === "runtime_code") changed.runtime_code_b = "0x6002600055"; + if (subtype === "dynamic_attestation") changed.deployed_source_role = "vesting_wallet"; + if (subtype === "log_coverage") { + changed.page_commitment = `0x${"ff".repeat(32)}`; + } + return changed; +} + +function verify() { + const fixture = JSON.parse(readFileSync(new URL("./provider-evidence-v2.json", import.meta.url))); + const definitions = []; + for (const [subtype, schema] of Object.entries(fixture.field_schemas)) { + const commitment = digest(referenceDefinitionPreimage(subtype, schema)); + if (commitment !== fixture.expected_definition_commitments[subtype]) { + throw new Error(`${subtype} definition commitment mismatch`); + } + definitions.push(commitment); + } + const domainCommitment = digest(referenceDomainDefinitionPreimage(definitions)); + if (domainCommitment !== fixture.expected_domain_definition_commitment) { + throw new Error("provider evidence domain definition commitment mismatch"); + } + for (const vector of fixture.vectors) { + const preimage = referenceProviderEvidencePreimage(vector.subtype, vector.input); + if (toHex(preimage) !== vector.expected_preimage_hex) { + throw new Error(`${vector.name} preimage mismatch`); + } + if (digest(preimage) !== vector.expected_keccak256) { + throw new Error(`${vector.name} digest mismatch`); + } + if (digest(referenceProviderEvidencePreimage( + vector.subtype, mutate(vector.subtype, vector.input), + )) === vector.expected_keccak256) { + throw new Error(`${vector.name} one-field mutation preserved digest`); + } + } + const genesis = fixture.vectors.find((item) => item.name === "log_coverage_genesis_v2"); + const continuation = fixture.vectors.find((item) => item.name === "log_coverage_continuation_v2"); + if (genesis.expected_preimage_hex === continuation.expected_preimage_hex) { + throw new Error("nullable genesis and present cursor encodings collided"); + } + console.log(`reference provider evidence v2: ${fixture.vectors.length} vectors passed`); +} + +export const referenceProviderEvidenceHex = toHex; +export const referenceProviderEvidenceDigest = digest; + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) verify(); diff --git a/supabase/tests/concurrency/profile_claim_sessions.sql b/supabase/tests/concurrency/profile_claim_sessions.sql new file mode 100644 index 00000000..8a7b0427 --- /dev/null +++ b/supabase/tests/concurrency/profile_claim_sessions.sql @@ -0,0 +1,812 @@ +\if :setup +drop schema if exists programmable_concurrency_profile cascade; +create schema programmable_concurrency_profile; + +create table programmable_concurrency_profile.ready ( + phase text not null, + actor text not null, + primary key (phase, actor) +); + +create table programmable_concurrency_profile.results ( + phase text not null, + actor text not null, + outcome text not null, + detail text, + primary key (phase, actor) +); + +create function programmable_concurrency_profile.arrive( + p_phase text, + p_actor text +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_profile.ready (phase, actor) + values (p_phase, p_actor) + on conflict (phase, actor) do nothing +$function$; + +create function programmable_concurrency_profile.wait_for_peers( + p_phase text, + p_expected integer +) +returns void +language plpgsql +security definer +set search_path = '' +as $function$ +declare + attempt integer; +begin + for attempt in 1..200 loop + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.ready + where phase = p_phase + ) >= p_expected then + return; + end if; + perform pg_catalog.pg_sleep(0.05); + end loop; + raise exception 'timed out waiting for phase %', p_phase; +end +$function$; + +create function programmable_concurrency_profile.record_result( + p_phase text, + p_actor text, + p_outcome text, + p_detail text +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_profile.results ( + phase, actor, outcome, detail + ) + values (p_phase, p_actor, p_outcome, pg_catalog.left(p_detail, 512)) + on conflict (phase, actor) do update + set outcome = excluded.outcome, + detail = excluded.detail +$function$; + +grant usage on schema programmable_concurrency_profile + to programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer; +grant execute on all functions in schema programmable_concurrency_profile + to programmable_profile_binder, programmable_profile_recovery, + programmable_profile_writer; + +set role programmable_profile_recovery; +select programmable_private.define_profile_hash_version( + 1::smallint, 'hmac-sha256-v1', + decode(repeat('11', 32), 'hex'), decode(repeat('12', 32), 'hex'), + '2026-01-01T00:00:00Z' +); +select programmable_private.set_profile_hash_version_state( + '21000000-0000-0000-0000-000000000001', + 1::smallint, 'current', decode(repeat('13', 32), 'hex'), + '2026-01-01T00:00:01Z' +); +reset role; + +set role programmable_profile_binder; +select programmable_private.bind_profile_subject( + decode(repeat('33', 20), 'hex'), 1::smallint, + decode(repeat('33', 32), 'hex'), 'wallet_signature', + decode(repeat('34', 32), 'hex'), '2026-01-01T00:00:02Z' +); +select programmable_private.bind_profile_subject( + decode(repeat('44', 20), 'hex'), 1::smallint, + decode(repeat('44', 32), 'hex'), 'wallet_signature', + decode(repeat('45', 32), 'hex'), '2026-01-01T00:00:03Z' +); +select programmable_private.bind_profile_subject( + decode(repeat('55', 20), 'hex'), 1::smallint, + decode(repeat('55', 32), 'hex'), 'wallet_signature', + decode(repeat('56', 32), 'hex'), '2026-01-01T00:00:03Z' +); +select programmable_private.bind_profile_subject( + decode(repeat('66', 20), 'hex'), 1::smallint, + decode(repeat('66', 32), 'hex'), 'wallet_signature', + decode(repeat('67', 32), 'hex'), '2026-01-01T00:00:03Z' +); +select programmable_private.bind_profile_subject( + decode(repeat('77', 20), 'hex'), 1::smallint, + decode(repeat('77', 32), 'hex'), 'wallet_signature', + decode(repeat('78', 32), 'hex'), '2026-01-01T00:00:03Z' +); +reset role; + +set role programmable_profile_recovery; +select programmable_private.tombstone_profile_binding( + decode(repeat('33', 20), 'hex'), 1::smallint, + decode(repeat('33', 32), 'hex'), 1, + decode(repeat('35', 32), 'hex'), '2026-01-01T00:00:04Z' +); +reset role; +\endif + +\if :first_wallet_a +select programmable_concurrency_profile.arrive('first-wallet', 'a'); +select programmable_concurrency_profile.wait_for_peers('first-wallet', 2); +set role programmable_profile_binder; +do $session$ +declare + claimed_subject uuid; +begin + claimed_subject := programmable_private.bind_profile_subject( + decode(repeat('11', 20), 'hex'), 1::smallint, + decode(repeat('11', 32), 'hex'), 'wallet_signature', + decode(repeat('16', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'first-wallet', 'a', 'success', claimed_subject::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'first-wallet', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :first_wallet_b +select programmable_concurrency_profile.arrive('first-wallet', 'b'); +select programmable_concurrency_profile.wait_for_peers('first-wallet', 2); +set role programmable_profile_binder; +do $session$ +declare + claimed_subject uuid; +begin + claimed_subject := programmable_private.bind_profile_subject( + decode(repeat('11', 20), 'hex'), 1::smallint, + decode(repeat('12', 32), 'hex'), 'wallet_signature', + decode(repeat('17', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'first-wallet', 'b', 'success', claimed_subject::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'first-wallet', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :first_alias_a +select programmable_concurrency_profile.arrive('first-alias', 'a'); +select programmable_concurrency_profile.wait_for_peers('first-alias', 2); +set role programmable_profile_binder; +do $session$ +declare + claimed_subject uuid; +begin + claimed_subject := programmable_private.bind_profile_subject( + decode(repeat('12', 20), 'hex'), 1::smallint, + decode(repeat('13', 32), 'hex'), 'wallet_signature', + decode(repeat('18', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'first-alias', 'a', 'success', claimed_subject::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'first-alias', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :first_alias_b +select programmable_concurrency_profile.arrive('first-alias', 'b'); +select programmable_concurrency_profile.wait_for_peers('first-alias', 2); +set role programmable_profile_binder; +do $session$ +declare + claimed_subject uuid; +begin + claimed_subject := programmable_private.bind_profile_subject( + decode(repeat('13', 20), 'hex'), 1::smallint, + decode(repeat('13', 32), 'hex'), 'wallet_signature', + decode(repeat('19', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'first-alias', 'b', 'success', claimed_subject::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'first-alias', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :after_first +do $verify_first$ +begin + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-wallet' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-wallet' and outcome = '23505' + ) <> 1 then + raise exception 'same-wallet first-bind race did not have one winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-alias' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-alias' and outcome = '23505' + ) <> 1 then + raise exception 'same-alias first-bind race did not have one winner'; + end if; +end +$verify_first$; + +set role programmable_profile_recovery; +select programmable_private.define_profile_hash_version( + 2::smallint, 'hmac-sha256-v2', + decode(repeat('21', 32), 'hex'), decode(repeat('22', 32), 'hex'), + '2026-01-01T00:01:00Z' +); +select programmable_private.set_profile_hash_version_state( + '22000000-0000-0000-0000-000000000001', + 2::smallint, 'current', decode(repeat('23', 32), 'hex'), + '2026-01-01T00:01:01Z' +); +select programmable_private.rekey_profile_subject( + decode(repeat('44', 20), 'hex'), + 1::smallint, decode(repeat('44', 32), 'hex'), + 2::smallint, decode(repeat('45', 32), 'hex'), + 1, decode(repeat('46', 32), 'hex'), + '2026-01-01T00:01:02Z' +); +reset role; + +set role programmable_profile_binder; +select programmable_private.bind_profile_subject( + decode(repeat('88', 20), 'hex'), 2::smallint, + decode(repeat('88', 32), 'hex'), 'wallet_signature', + decode(repeat('89', 32), 'hex'), '2026-01-01T00:01:03Z' +); +reset role; + +set role programmable_profile_recovery; +select programmable_private.tombstone_profile_binding( + decode(repeat('88', 20), 'hex'), 2::smallint, + decode(repeat('88', 32), 'hex'), 1, + decode(repeat('8a', 32), 'hex'), '2026-01-01T00:01:04Z' +); +reset role; +\endif + +\if :ownership_a +select programmable_concurrency_profile.arrive('ownership', 'a'); +select programmable_concurrency_profile.wait_for_peers('ownership', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.rekey_profile_subject( + decode(repeat('55', 20), 'hex'), + 1::smallint, decode(repeat('55', 32), 'hex'), + 2::smallint, decode(repeat('56', 32), 'hex'), + 1, decode(repeat('24', 32), 'hex'), + pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'ownership', 'a', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'ownership', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :ownership_b +select programmable_concurrency_profile.arrive('ownership', 'b'); +select programmable_concurrency_profile.wait_for_peers('ownership', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.tombstone_profile_binding( + decode(repeat('55', 20), 'hex'), 1::smallint, + decode(repeat('55', 32), 'hex'), 1, + decode(repeat('25', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'ownership', 'b', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'ownership', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :recover_a +select programmable_concurrency_profile.arrive('recover', 'a'); +select programmable_concurrency_profile.wait_for_peers('recover', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.recover_profile_binding( + decode(repeat('33', 20), 'hex'), 1::smallint, + decode(repeat('33', 32), 'hex'), 2, + decode(repeat('36', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'recover', 'a', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'recover', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :recover_b +select programmable_concurrency_profile.arrive('recover', 'b'); +select programmable_concurrency_profile.wait_for_peers('recover', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.recover_profile_binding( + decode(repeat('33', 20), 'hex'), 1::smallint, + decode(repeat('33', 32), 'hex'), 2, + decode(repeat('37', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'recover', 'b', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'recover', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :rekey_a +select programmable_concurrency_profile.arrive('rekey', 'a'); +select programmable_concurrency_profile.wait_for_peers('rekey', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.rekey_profile_subject( + decode(repeat('66', 20), 'hex'), + 1::smallint, decode(repeat('66', 32), 'hex'), + 2::smallint, decode(repeat('67', 32), 'hex'), + 1, decode(repeat('69', 32), 'hex'), + pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'rekey', 'a', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'rekey', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :rekey_b +select programmable_concurrency_profile.arrive('rekey', 'b'); +select programmable_concurrency_profile.wait_for_peers('rekey', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.rekey_profile_subject( + decode(repeat('66', 20), 'hex'), + 1::smallint, decode(repeat('66', 32), 'hex'), + 2::smallint, decode(repeat('68', 32), 'hex'), + 1, decode(repeat('6a', 32), 'hex'), + pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'rekey', 'b', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'rekey', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :alias_claim_a +select programmable_concurrency_profile.arrive('alias-claim', 'a'); +select programmable_concurrency_profile.wait_for_peers('alias-claim', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.rekey_profile_subject( + decode(repeat('77', 20), 'hex'), + 1::smallint, decode(repeat('77', 32), 'hex'), + 2::smallint, decode(repeat('78', 32), 'hex'), + 1, decode(repeat('7a', 32), 'hex'), + pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'alias-claim', 'a', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'alias-claim', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :alias_claim_b +select programmable_concurrency_profile.arrive('alias-claim', 'b'); +select programmable_concurrency_profile.wait_for_peers('alias-claim', 2); +set role programmable_profile_binder; +do $session$ +declare + claimed_subject uuid; +begin + claimed_subject := programmable_private.bind_profile_subject( + decode(repeat('79', 20), 'hex'), 2::smallint, + decode(repeat('78', 32), 'hex'), 'wallet_signature', + decode(repeat('7b', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'alias-claim', 'b', 'success', claimed_subject::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'alias-claim', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :recover_mutate_a +select programmable_concurrency_profile.arrive('recover-mutate', 'a'); +select programmable_concurrency_profile.wait_for_peers('recover-mutate', 2); +set role programmable_profile_recovery; +do $session$ +declare + next_generation bigint; +begin + next_generation := programmable_private.recover_profile_binding( + decode(repeat('88', 20), 'hex'), 2::smallint, + decode(repeat('88', 32), 'hex'), 2, + decode(repeat('8b', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'recover-mutate', 'a', 'success', next_generation::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'recover-mutate', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :recover_mutate_b +select programmable_concurrency_profile.arrive('recover-mutate', 'b'); +select programmable_concurrency_profile.wait_for_peers('recover-mutate', 2); +set role programmable_profile_writer; +do $session$ +declare + next_revision bigint; +begin + next_revision := programmable_private.mutate_profile( + decode(repeat('88', 20), 'hex'), 2::smallint, + decode(repeat('88', 32), 'hex'), 2, 1, + 'RaceMutation', null, 'Must not persist', null, + decode(repeat('8c', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'recover-mutate', 'b', 'success', next_revision::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'recover-mutate', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :revision_a +select programmable_concurrency_profile.arrive('revision', 'a'); +select programmable_concurrency_profile.wait_for_peers('revision', 2); +set role programmable_profile_writer; +do $session$ +declare + next_revision bigint; +begin + next_revision := programmable_private.mutate_profile( + decode(repeat('44', 20), 'hex'), 2::smallint, + decode(repeat('45', 32), 'hex'), 2, 0, + 'RevAlpha', null, 'Revision A', null, + decode(repeat('47', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'revision', 'a', 'success', next_revision::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'revision', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :revision_b +select programmable_concurrency_profile.arrive('revision', 'b'); +select programmable_concurrency_profile.wait_for_peers('revision', 2); +set role programmable_profile_writer; +do $session$ +declare + next_revision bigint; +begin + next_revision := programmable_private.mutate_profile( + decode(repeat('44', 20), 'hex'), 2::smallint, + decode(repeat('45', 32), 'hex'), 2, 0, + 'RevBeta', null, 'Revision B', null, + decode(repeat('48', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_profile.record_result( + 'revision', 'b', 'success', next_revision::text + ); +exception when others then + perform programmable_concurrency_profile.record_result( + 'revision', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :verify +do $verify$ +begin + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-wallet' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-wallet' and outcome = '23505' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_owner_binding_current + where wallet = decode(repeat('11', 20), 'hex') + and generation = 1 + ) <> 1 then + raise exception 'same-wallet first bind did not have one stable winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-alias' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'first-alias' and outcome = '23505' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_subject_aliases + where hash_version = 1 + and keyed_subject_hash = decode(repeat('13', 32), 'hex') + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_owner_binding_current + where wallet in ( + decode(repeat('12', 20), 'hex'), + decode(repeat('13', 20), 'hex') + ) + ) <> 1 then + raise exception 'same-alias first bind did not have one stable winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_private.profile_owner_binding_current + where wallet = decode(repeat('55', 20), 'hex') + and generation = 2 + ) <> 1 or ( + select pg_catalog.count(distinct subject_id) + from programmable_private.profile_owner_binding_history + where wallet = decode(repeat('55', 20), 'hex') + ) <> 1 then + raise exception 'ownership race split or replaced the stable subject'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'ownership' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'ownership' and outcome = '40001' + ) <> 1 then + raise exception 'rekey/tombstone race did not have one CAS winner'; + end if; + if exists ( + select 1 + from programmable_private.profile_owner_binding_current as binding + left join programmable_private.profile_subject_current_alias as current_alias + on current_alias.subject_id = binding.subject_id + and current_alias.generation = binding.generation + where binding.wallet = decode(repeat('55', 20), 'hex') + and current_alias.subject_id is null + ) then + raise exception 'ownership race left a dangling subject alias'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'recover' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'recover' and outcome = '40001' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_owner_binding_current + where wallet = decode(repeat('33', 20), 'hex') + and generation = 3 and state = 'recovered' + ) <> 1 or ( + select pg_catalog.count(distinct subject_id) + from programmable_private.profile_owner_binding_history + where wallet = decode(repeat('33', 20), 'hex') + ) <> 1 then + raise exception 'recovery race reused or split the tombstoned subject'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'rekey' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'rekey' and outcome = '40001' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_owner_binding_current + where wallet = decode(repeat('66', 20), 'hex') + and generation = 2 and state = 'recovered' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_subject_aliases + where hash_version = 2 + and keyed_subject_hash in ( + decode(repeat('67', 32), 'hex'), + decode(repeat('68', 32), 'hex') + ) + ) <> 1 then + raise exception 'same-generation rekey race did not have one winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'alias-claim' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'alias-claim' and outcome in ('23505', '40001') + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_subject_aliases + where hash_version = 2 + and keyed_subject_hash = decode(repeat('78', 32), 'hex') + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_subject_aliases as alias + join programmable_private.profile_subject_alias_status_current as status + on status.alias_id = alias.alias_id + where alias.hash_version = 2 + and alias.keyed_subject_hash = decode(repeat('78', 32), 'hex') + and status.state = 'current' + ) <> 1 then + raise exception 'rekey versus alias claim did not preserve unique ownership'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'recover-mutate' and actor = 'a' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'recover-mutate' and actor = 'b' and outcome = '40001' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profile_owner_binding_current + where wallet = decode(repeat('88', 20), 'hex') + and generation = 3 and state = 'recovered' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profiles as profile + join programmable_private.profile_owner_binding_current as binding + on binding.subject_id = profile.subject_id + where binding.wallet = decode(repeat('88', 20), 'hex') + and profile.revision = 2 + and profile.deleted_at is null + and profile.username is null + ) <> 1 then + raise exception 'recovery versus mutation race admitted a stale mutation'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'revision' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_profile.results + where phase = 'revision' and outcome = '40001' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profiles as profile + join programmable_private.profile_owner_binding_current as binding + on binding.subject_id = profile.subject_id + where binding.wallet = decode(repeat('44', 20), 'hex') + and profile.revision = 1 + and profile.username in ('RevAlpha', 'RevBeta') + ) <> 1 then + raise exception 'profile revision CAS did not have one winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_private.profile_audit_records + where wallet = decode(repeat('11', 20), 'hex') + and action = 'profile.bind_first' + ) <> 1 then + raise exception 'idempotent first-bind duplicated audit history'; + end if; +end +$verify$; + +drop schema programmable_concurrency_profile cascade; +\endif diff --git a/supabase/tests/concurrency/projector_checkpoint_sessions.sql b/supabase/tests/concurrency/projector_checkpoint_sessions.sql new file mode 100644 index 00000000..c59d4b7c --- /dev/null +++ b/supabase/tests/concurrency/projector_checkpoint_sessions.sql @@ -0,0 +1,1256 @@ +\if :setup +drop schema if exists programmable_concurrency_projector cascade; +create schema programmable_concurrency_projector; + +create table programmable_concurrency_projector.ready ( + phase text not null, + actor text not null, + primary key (phase, actor) +); + +create table programmable_concurrency_projector.results ( + phase text not null, + actor text not null, + outcome text not null, + detail text, + primary key (phase, actor) +); + +create function programmable_concurrency_projector.arrive( + p_phase text, + p_actor text +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_projector.ready (phase, actor) + values (p_phase, p_actor) + on conflict (phase, actor) do nothing +$function$; + +create function programmable_concurrency_projector.wait_for_peers( + p_phase text, + p_expected integer +) +returns void +language plpgsql +security definer +set search_path = '' +as $function$ +declare + attempt integer; +begin + for attempt in 1..200 loop + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.ready + where phase = p_phase + ) >= p_expected then + return; + end if; + perform pg_catalog.pg_sleep(0.05); + end loop; + raise exception 'timed out waiting for phase %', p_phase; +end +$function$; + +create function programmable_concurrency_projector.record_result( + p_phase text, + p_actor text, + p_outcome text, + p_detail text +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_projector.results ( + phase, actor, outcome, detail + ) + values (p_phase, p_actor, p_outcome, pg_catalog.left(p_detail, 512)) + on conflict (phase, actor) do update + set outcome = excluded.outcome, + detail = excluded.detail +$function$; + +create function programmable_concurrency_projector.current_scope( + p_release_id text +) +returns table(epoch_id uuid, generation bigint) +language sql +stable +security definer +set search_path = '' +as $function$ + select current_epoch.epoch_id, current_epoch.generation + from programmable_private.release_epoch_current as current_epoch + where current_epoch.chain_id = 1 + and current_epoch.release_id = p_release_id + and current_epoch.model_id = 'classic-v3' + and current_epoch.source_group = 'core' +$function$; + +grant usage on schema programmable_concurrency_projector + to programmable_projector; +grant execute on all functions in schema programmable_concurrency_projector + to programmable_projector; + +set role programmable_projector; + +select programmable_private.create_release_epoch( + '10000000-1000-0000-0000-000000000001', + 1, 'race', 'classic-v3', 'core', 1, + decode(repeat('10', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('12', 32), 'hex'), '2026-01-01T00:00:00Z' +); +select programmable_private.create_release_epoch( + '10000000-1000-0000-0000-000000000002', + 1, 'race', 'classic-v3', 'core', 2, + decode(repeat('13', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('14', 32), 'hex'), '2026-01-01T00:00:01Z' +); +select programmable_private.create_release_epoch( + '10000000-1000-0000-0000-000000000003', + 1, 'race', 'classic-v3', 'core', 3, + decode(repeat('15', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('16', 32), 'hex'), '2026-01-01T00:00:02Z' +); +select programmable_private.create_release_epoch( + '10000000-1000-0000-0000-000000000004', + 1, 'race', 'classic-v3', 'core', 4, + decode(repeat('18', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('19', 32), 'hex'), '2026-01-01T00:00:02Z' +); +select programmable_private.append_release_source_binding( + '11000000-1000-0000-0000-000000000002', + '10000000-1000-0000-0000-000000000002', + 'checkpoint-source', 'launcher', 'ethereum_contract', + decode(repeat('aa', 20), 'hex'), null, + 0, decode(repeat('ab', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('a2', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + '2026-01-01T00:00:02Z' +); +select programmable_private.append_release_source_binding( + '11000000-1000-0000-0000-000000000003', + '10000000-1000-0000-0000-000000000003', + 'checkpoint-source', 'launcher', 'ethereum_contract', + decode(repeat('aa', 20), 'hex'), null, + 0, decode(repeat('ab', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('b3', 32), 'hex'), + '2026-01-01T00:00:02Z' +); +select programmable_private.append_release_source_binding( + '11000000-1000-0000-0000-000000000004', + '10000000-1000-0000-0000-000000000004', + 'checkpoint-source', 'launcher', 'ethereum_contract', + decode(repeat('aa', 20), 'hex'), null, + 0, decode(repeat('ab', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('a4', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + '2026-01-01T00:00:02Z' +); +select programmable_private.activate_release_epoch( + 1, 'race', 'classic-v3', 'core', + '10000000-1000-0000-0000-000000000001', + 0, 1, decode(repeat('17', 32), 'hex'), '2026-01-01T00:00:03Z' +); + +select programmable_private.create_release_epoch( + '10000000-2000-0000-0000-000000000001', + 1, 'diff-a', 'classic-v3', 'core', 1, + decode(repeat('20', 32), 'hex'), decode(repeat('21', 32), 'hex'), + decode(repeat('22', 32), 'hex'), '2026-01-01T00:00:04Z' +); +select programmable_private.activate_release_epoch( + 1, 'diff-a', 'classic-v3', 'core', + '10000000-2000-0000-0000-000000000001', + 0, 1, decode(repeat('23', 32), 'hex'), '2026-01-01T00:00:05Z' +); +select programmable_private.create_release_epoch( + '10000000-3000-0000-0000-000000000001', + 1, 'diff-b', 'classic-v3', 'core', 1, + decode(repeat('30', 32), 'hex'), decode(repeat('31', 32), 'hex'), + decode(repeat('32', 32), 'hex'), '2026-01-01T00:00:06Z' +); +select programmable_private.activate_release_epoch( + 1, 'diff-b', 'classic-v3', 'core', + '10000000-3000-0000-0000-000000000001', + 0, 1, decode(repeat('33', 32), 'hex'), '2026-01-01T00:00:07Z' +); + +select programmable_private.register_rpc_provider_deployment( + '12000000-0000-0000-0000-000000000001', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('a3', 32), 'hex'), + decode(repeat('91', 32), 'hex'), + decode(repeat('92', 32), 'hex'), decode(repeat('93', 32), 'hex'), + '2026-01-01T00:00:08Z' +); +select programmable_private.register_rpc_provider_deployment( + '12000000-0000-0000-0000-000000000002', + 1, 'quicknode', 'rpc-provider-v1', + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('b3', 32), 'hex'), + decode(repeat('94', 32), 'hex'), + decode(repeat('95', 32), 'hex'), decode(repeat('96', 32), 'hex'), + '2026-01-01T00:00:08Z' +); +select programmable_private.register_provider_deployment( + '12000000-0000-0000-0000-000000000003', 'envio_deployment', + 'concurrency-envio', decode(repeat('97', 32), 'hex'), + decode(repeat('98', 32), 'hex'), decode(repeat('99', 32), 'hex'), + '2026-01-01T00:00:08Z' +); + +reset role; +\endif + +\if :pointer_a +select programmable_concurrency_projector.arrive('pointer', 'a'); +select programmable_concurrency_projector.wait_for_peers('pointer', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.activate_release_epoch( + 1, 'race', 'classic-v3', 'core', + '10000000-1000-0000-0000-000000000002', + 1, 2, decode(repeat('41', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_projector.record_result( + 'pointer', 'a', 'success', 'epoch-2' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'pointer', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :pointer_b +select programmable_concurrency_projector.arrive('pointer', 'b'); +select programmable_concurrency_projector.wait_for_peers('pointer', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.activate_release_epoch( + 1, 'race', 'classic-v3', 'core', + '10000000-1000-0000-0000-000000000003', + 1, 2, decode(repeat('42', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_projector.record_result( + 'pointer', 'b', 'success', 'epoch-3' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'pointer', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :lease_a +select programmable_concurrency_projector.arrive('lease', 'a'); +select programmable_concurrency_projector.wait_for_peers('lease', 2); +set role programmable_projector; +do $session$ +declare + selected_epoch uuid; + selected_generation bigint; +begin + select scope.epoch_id, scope.generation + into selected_epoch, selected_generation + from programmable_concurrency_projector.current_scope('race') as scope; + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'projector-v1', + selected_epoch, selected_generation, 0, 1, + decode(repeat('51', 32), 'hex'), 'lease-a', + pg_catalog.clock_timestamp(), + pg_catalog.clock_timestamp() + interval '5 minutes', + decode(repeat('52', 32), 'hex') + ); + perform programmable_concurrency_projector.record_result( + 'lease', 'a', 'success', selected_epoch::text + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'lease', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :lease_b +select programmable_concurrency_projector.arrive('lease', 'b'); +select programmable_concurrency_projector.wait_for_peers('lease', 2); +set role programmable_projector; +do $session$ +declare + selected_epoch uuid; + selected_generation bigint; +begin + select scope.epoch_id, scope.generation + into selected_epoch, selected_generation + from programmable_concurrency_projector.current_scope('race') as scope; + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'projector-v1', + selected_epoch, selected_generation, 0, 1, + decode(repeat('53', 32), 'hex'), 'lease-b', + pg_catalog.clock_timestamp(), + pg_catalog.clock_timestamp() + interval '5 minutes', + decode(repeat('54', 32), 'hex') + ); + perform programmable_concurrency_projector.record_result( + 'lease', 'b', 'success', selected_epoch::text + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'lease', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :different_a +select programmable_concurrency_projector.arrive('different', 'a'); +select programmable_concurrency_projector.wait_for_peers('different', 2); +set role programmable_projector; +do $session$ +begin + perform pg_catalog.set_config('lock_timeout', '250ms', true); + perform programmable_private.acquire_projector_lease( + 1, 'diff-a', 'classic-v3', 'core', 'projector-v1', + '10000000-2000-0000-0000-000000000001', 1, 0, 1, + decode(repeat('61', 32), 'hex'), 'different-a', + pg_catalog.clock_timestamp(), + pg_catalog.clock_timestamp() + interval '5 minutes', + decode(repeat('62', 32), 'hex') + ); + perform programmable_concurrency_projector.record_result( + 'different', 'a', 'success', 'diff-a' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'different', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :different_b +select programmable_concurrency_projector.arrive('different', 'b'); +select programmable_concurrency_projector.wait_for_peers('different', 2); +set role programmable_projector; +do $session$ +begin + perform pg_catalog.set_config('lock_timeout', '250ms', true); + perform programmable_private.acquire_projector_lease( + 1, 'diff-b', 'classic-v3', 'core', 'projector-v1', + '10000000-3000-0000-0000-000000000001', 1, 0, 1, + decode(repeat('63', 32), 'hex'), 'different-b', + pg_catalog.clock_timestamp(), + pg_catalog.clock_timestamp() + interval '5 minutes', + decode(repeat('64', 32), 'hex') + ); + perform programmable_concurrency_projector.record_result( + 'different', 'b', 'success', 'diff-b' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'different', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :stale_a +select programmable_concurrency_projector.arrive('stale', 'a'); +select programmable_concurrency_projector.wait_for_peers('stale', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'stale-a', + '10000000-1000-0000-0000-000000000001', 1, 0, 1, + decode(repeat('71', 32), 'hex'), 'stale-a', + pg_catalog.clock_timestamp(), + pg_catalog.clock_timestamp() + interval '5 minutes', + decode(repeat('72', 32), 'hex') + ); + perform programmable_concurrency_projector.record_result( + 'stale', 'a', 'unexpected-success', null + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'stale', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :stale_b +select programmable_concurrency_projector.arrive('stale', 'b'); +select programmable_concurrency_projector.wait_for_peers('stale', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'stale-b', + '10000000-1000-0000-0000-000000000001', 1, 0, 1, + decode(repeat('73', 32), 'hex'), 'stale-b', + pg_catalog.clock_timestamp(), + pg_catalog.clock_timestamp() + interval '5 minutes', + decode(repeat('74', 32), 'hex') + ); + perform programmable_concurrency_projector.record_result( + 'stale', 'b', 'unexpected-success', null + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'stale', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :checkpoint_setup +set role programmable_projector; +do $checkpoint_setup$ +declare + selected_epoch uuid; + selected_generation bigint; + setup_at timestamptz := pg_catalog.clock_timestamp(); +begin + select scope.epoch_id, scope.generation + into selected_epoch, selected_generation + from programmable_concurrency_projector.current_scope('race') as scope; + + perform programmable_private.open_run( + '13000000-1000-0000-0000-000000000001', + 'ingestion', 1, 'race', 'classic-v3', 'core', + selected_epoch, selected_generation, 'checkpoint-fixture', + decode(repeat('c1', 32), 'hex'), setup_at + ); + perform programmable_private.append_safe_head_observation( + '13200000-1000-0000-0000-000000000001', + '13000000-1000-0000-0000-000000000001', + '12000000-0000-0000-0000-000000000001', + '12000000-0000-0000-0000-000000000002', + 1, 1, 112, 112, 12, 100, + decode(repeat('c0', 32), 'hex'), decode(repeat('c0', 32), 'hex'), + 2::smallint, + decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320001c1', + 'hex' + ), + decode(repeat('c1', 32), 'hex'), setup_at + ); + perform programmable_private.append_dual_rpc_block_evidence( + '13200000-1000-0000-0000-000000000002', + '13200000-1000-0000-0000-000000000001', + '13000000-1000-0000-0000-000000000001', + 100, decode(repeat('c0', 32), 'hex'), decode(repeat('c0', 32), 'hex'), + 2::smallint, + decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320002c2', + 'hex' + ), + decode(repeat('c2', 32), 'hex'), setup_at + ); + perform programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('c0', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 0), + '13000000-1000-0000-0000-000000000001', + 100, + decode(repeat('c0', 32), 'hex'), + decode(repeat('c4', 32), 'hex'), + 0, + 0, + decode(repeat('aa', 20), 'hex'), + decode(repeat('c5', 32), 'hex'), + 'launch-created', + array[decode(repeat('c5', 32), 'hex')], + decode('00', 'hex'), + '{"kind":"checkpoint-race"}'::jsonb, + decode(repeat('c6', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('c0', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 0), + '12000000-0000-0000-0000-000000000003', + decode(repeat('c7', 32), 'hex'), + setup_at +); + perform programmable_private.append_chain_event_occurrence( + '13100000-1000-0000-0000-000000000001', + '13100000-1000-0000-0000-000000000002', + '13000000-1000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('c0', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 0), 0, setup_at, 'checkpoint-decoder-v1', + decode(repeat('ab', 32), 'hex'), + '13200000-1000-0000-0000-000000000002', 1::smallint, + decode( + '70726f6772616d6d61626c653a6f6363757272656e63653a763100c3', + 'hex' + ), + decode(repeat('c3', 32), 'hex'), setup_at + ); + + perform programmable_private.open_run( + '13000000-1000-0000-0000-000000000002', + 'projection', 1, 'race', 'classic-v3', 'core', + selected_epoch, selected_generation, 'checkpoint-a', + decode(repeat('ca', 32), 'hex'), setup_at + ); + perform programmable_private.open_run( + '13000000-1000-0000-0000-000000000003', + 'projection', 1, 'race', 'classic-v3', 'core', + selected_epoch, selected_generation, 'checkpoint-b', + decode(repeat('cb', 32), 'hex'), setup_at + ); + perform programmable_private.open_run( + '13000000-1000-0000-0000-000000000004', + 'projection', 1, 'race', 'classic-v3', 'core', + selected_epoch, selected_generation, 'checkpoint-baseline', + decode(repeat('cc', 32), 'hex'), setup_at + ); + perform programmable_private.stage_launch_projection( + '13300000-1000-0000-0000-000000000001', + '13000000-1000-0000-0000-000000000002', + decode(repeat('a1', 20), 'hex'), decode(repeat('a2', 20), 'hex'), + decode(repeat('c4', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + null, decode(repeat('a4', 32), 'hex'), 'Checkpoint A', 'CPA', 1000000, + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_pool_projection( + '13300000-1000-0000-0000-000000000011', + '13300000-1000-0000-0000-000000000001', + '13000000-1000-0000-0000-000000000002', + decode(repeat('00', 20), 'hex'), decode(repeat('a1', 20), 'hex'), + 3000, 60, decode(repeat('aa', 20), 'hex'), + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_pool_fee_configuration( + '13300000-1000-0000-0000-000000000021', + '13300000-1000-0000-0000-000000000011', + '13000000-1000-0000-0000-000000000002', + 100, 100, 90, 10, 0, 0, + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_launch_projection( + '13300000-1000-0000-0000-000000000002', + '13000000-1000-0000-0000-000000000003', + decode(repeat('b1', 20), 'hex'), decode(repeat('b2', 20), 'hex'), + decode(repeat('c4', 32), 'hex'), decode(repeat('b3', 32), 'hex'), + null, decode(repeat('b4', 32), 'hex'), 'Checkpoint B', 'CPB', 1000000, + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_pool_projection( + '13300000-1000-0000-0000-000000000012', + '13300000-1000-0000-0000-000000000002', + '13000000-1000-0000-0000-000000000003', + decode(repeat('00', 20), 'hex'), decode(repeat('b1', 20), 'hex'), + 3000, 60, decode(repeat('aa', 20), 'hex'), + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_pool_fee_configuration( + '13300000-1000-0000-0000-000000000022', + '13300000-1000-0000-0000-000000000012', + '13000000-1000-0000-0000-000000000003', + 100, 100, 90, 10, 0, 0, + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_launch_projection( + '13300000-1000-0000-0000-000000000003', + '13000000-1000-0000-0000-000000000004', + decode(repeat('c1', 20), 'hex'), decode(repeat('c2', 20), 'hex'), + decode(repeat('c4', 32), 'hex'), decode(repeat('c3', 32), 'hex'), + null, decode(repeat('c4', 32), 'hex'), + 'Checkpoint Baseline', 'CP0', 1000000, + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_pool_projection( + '13300000-1000-0000-0000-000000000013', + '13300000-1000-0000-0000-000000000003', + '13000000-1000-0000-0000-000000000004', + decode(repeat('00', 20), 'hex'), decode(repeat('c1', 20), 'hex'), + 3000, 60, decode(repeat('aa', 20), 'hex'), + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.stage_pool_fee_configuration( + '13300000-1000-0000-0000-000000000023', + '13300000-1000-0000-0000-000000000013', + '13000000-1000-0000-0000-000000000004', + 100, 100, 90, 10, 0, 0, + '13100000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), setup_at + ); + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'checkpoint-v1', + selected_epoch, selected_generation, 0, 1, + decode(repeat('c7', 32), 'hex'), 'checkpoint-fixture', + setup_at, setup_at + interval '9 minutes', + decode(repeat('c8', 32), 'hex') + ); + perform programmable_private.promote_projection_run( + '13400000-1000-0000-0000-000000000021', + '13400000-1000-0000-0000-000000000022', + '13400000-1000-0000-0000-000000000023', + '13000000-1000-0000-0000-000000000004', + 'checkpoint-v1', 1, decode(repeat('c7', 32), 'hex'), + 0, 1, 0, + '13200000-1000-0000-0000-000000000001', + '13200000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), + array['13100000-1000-0000-0000-000000000002'::uuid], + array[]::uuid[], array[]::uuid[], array['checkpoint-race'], + decode(repeat('cc', 32), 'hex'), setup_at + ); +end +$checkpoint_setup$; +reset role; +\endif + +\if :checkpoint_a +select programmable_concurrency_projector.arrive('checkpoint', 'a'); +select programmable_concurrency_projector.wait_for_peers('checkpoint', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.promote_projection_run( + '13400000-1000-0000-0000-000000000001', + '13400000-1000-0000-0000-000000000002', + '13400000-1000-0000-0000-000000000003', + '13000000-1000-0000-0000-000000000002', + 'checkpoint-v1', 1, decode(repeat('c7', 32), 'hex'), + 1, 2, 0, + '13200000-1000-0000-0000-000000000001', + '13200000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), + array['13100000-1000-0000-0000-000000000002'::uuid], + array[]::uuid[], array[]::uuid[], array['checkpoint-race'], + decode(repeat('c9', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_projector.record_result( + 'checkpoint', 'a', 'success', 'generation-2' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'checkpoint', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :checkpoint_b +select programmable_concurrency_projector.arrive('checkpoint', 'b'); +select programmable_concurrency_projector.wait_for_peers('checkpoint', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.promote_projection_run( + '13400000-1000-0000-0000-000000000011', + '13400000-1000-0000-0000-000000000012', + '13400000-1000-0000-0000-000000000013', + '13000000-1000-0000-0000-000000000003', + 'checkpoint-v1', 1, decode(repeat('c7', 32), 'hex'), + 1, 2, 0, + '13200000-1000-0000-0000-000000000001', + '13200000-1000-0000-0000-000000000002', + 100, decode(repeat('c0', 32), 'hex'), + array['13100000-1000-0000-0000-000000000002'::uuid], + array[]::uuid[], array[]::uuid[], array['checkpoint-race'], + decode(repeat('ca', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_projector.record_result( + 'checkpoint', 'b', 'success', 'generation-2' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'checkpoint', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :reorg_setup +set role programmable_projector; +do $reorg_setup$ +declare + setup_at timestamptz := pg_catalog.clock_timestamp(); +begin + perform programmable_private.activate_release_epoch( + 1, 'race', 'classic-v3', 'core', + '10000000-1000-0000-0000-000000000004', + 2, 3, decode(repeat('d2', 32), 'hex'), setup_at + ); + perform programmable_private.open_run( + '13500000-1000-0000-0000-000000000001', + 'ingestion', 1, 'race', 'classic-v3', 'core', + '10000000-1000-0000-0000-000000000004', 3, + 'reorg-fixture', decode(repeat('d3', 32), 'hex'), setup_at + ); + perform programmable_private.append_safe_head_observation( + '13500000-1000-0000-0000-000000000002', + '13500000-1000-0000-0000-000000000001', + '12000000-0000-0000-0000-000000000001', + '12000000-0000-0000-0000-000000000002', + 1, 1, 112, 112, 12, 100, + decode(repeat('d0', 32), 'hex'), decode(repeat('d0', 32), 'hex'), + 2::smallint, + decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320001d1', + 'hex' + ), + decode(repeat('d1', 32), 'hex'), setup_at + ); + perform programmable_private.append_dual_rpc_block_evidence( + '13500000-1000-0000-0000-000000000003', + '13500000-1000-0000-0000-000000000002', + '13500000-1000-0000-0000-000000000001', + 90, decode(repeat('d9', 32), 'hex'), decode(repeat('d9', 32), 'hex'), + 2::smallint, + decode( + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320002d2', + 'hex' + ), + decode(repeat('d2', 32), 'hex'), setup_at + ); + perform programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('d9', 32), 'hex'), decode(repeat('d4', 32), 'hex'), 0), + '13500000-1000-0000-0000-000000000001', + 90, + decode(repeat('d9', 32), 'hex'), + decode(repeat('d4', 32), 'hex'), + 0, + 0, + decode(repeat('aa', 20), 'hex'), + decode(repeat('d5', 32), 'hex'), + 'launch-created', + array[decode(repeat('d5', 32), 'hex')], + decode('00', 'hex'), + '{"kind":"rollback-probe"}'::jsonb, + decode(repeat('d6', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('d9', 32), 'hex'), decode(repeat('d4', 32), 'hex'), 0), + '12000000-0000-0000-0000-000000000003', + decode(repeat('d7', 32), 'hex'), + setup_at +); + perform programmable_private.append_chain_event_occurrence( + '13600000-1000-0000-0000-000000000001', + '13600000-1000-0000-0000-000000000002', + '13500000-1000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('d9', 32), 'hex'), decode(repeat('d4', 32), 'hex'), 0), 0, setup_at, 'rollback-decoder-v1', + decode(repeat('ab', 32), 'hex'), + '13500000-1000-0000-0000-000000000003', 1::smallint, + decode( + '70726f6772616d6d61626c653a6f6363757272656e63653a763100d3', + 'hex' + ), + decode(repeat('d3', 32), 'hex'), setup_at + ); + perform programmable_private.open_run( + '13700000-1000-0000-0000-000000000001', + 'rewind', 1, 'race', 'classic-v3', 'core', + '10000000-1000-0000-0000-000000000004', 3, + 'reorg-a', decode(repeat('da', 32), 'hex'), setup_at + ); + perform programmable_private.open_run( + '13700000-1000-0000-0000-000000000002', + 'rewind', 1, 'race', 'classic-v3', 'core', + '10000000-1000-0000-0000-000000000004', 3, + 'reorg-b', decode(repeat('db', 32), 'hex'), setup_at + ); + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'checkpoint-v1', + '10000000-1000-0000-0000-000000000004', 3, 1, 2, + decode(repeat('d7', 32), 'hex'), 'reorg-fixture', + setup_at, setup_at + interval '9 minutes', + decode(repeat('d8', 32), 'hex') + ); + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'rollback-a', + '10000000-1000-0000-0000-000000000004', 3, 0, 1, + decode(repeat('e1', 32), 'hex'), 'rollback-a', + setup_at, setup_at + interval '9 minutes', + decode(repeat('e2', 32), 'hex') + ); + perform programmable_private.acquire_projector_lease( + 1, 'race', 'classic-v3', 'core', 'rollback-b', + '10000000-1000-0000-0000-000000000004', 3, 0, 1, + decode(repeat('e3', 32), 'hex'), 'rollback-b', + setup_at, setup_at + interval '9 minutes', + decode(repeat('e4', 32), 'hex') + ); +end +$reorg_setup$; +reset role; +\endif + +\if :reorg_a +select programmable_concurrency_projector.arrive('reorg', 'a'); +select programmable_concurrency_projector.wait_for_peers('reorg', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.rewind_projection_run( + '13800000-1000-0000-0000-000000000001', + '13800000-1000-0000-0000-000000000002', + '13700000-1000-0000-0000-000000000001', + 'checkpoint-v1', 2, decode(repeat('d7', 32), 'hex'), + 2, 3, 1, + '13500000-1000-0000-0000-000000000002', + '13500000-1000-0000-0000-000000000003', + 90, decode(repeat('d9', 32), 'hex'), + decode(repeat('dc', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_projector.record_result( + 'reorg', 'a', 'success', 'pointer-3/lease-2/checkpoint-3/reorg-1' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'reorg', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :reorg_b +select programmable_concurrency_projector.arrive('reorg', 'b'); +select programmable_concurrency_projector.wait_for_peers('reorg', 2); +set role programmable_projector; +do $session$ +begin + perform programmable_private.rewind_projection_run( + '13800000-1000-0000-0000-000000000011', + '13800000-1000-0000-0000-000000000012', + '13700000-1000-0000-0000-000000000002', + 'checkpoint-v1', 2, decode(repeat('d7', 32), 'hex'), + 2, 3, 1, + '13500000-1000-0000-0000-000000000002', + '13500000-1000-0000-0000-000000000003', + 90, decode(repeat('d9', 32), 'hex'), + decode(repeat('dd', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_projector.record_result( + 'reorg', 'b', 'success', 'pointer-3/lease-2/checkpoint-3/reorg-1' + ); +exception when others then + perform programmable_concurrency_projector.record_result( + 'reorg', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :rollback_a +select programmable_concurrency_projector.arrive('rollback', 'a'); +select programmable_concurrency_projector.wait_for_peers('rollback', 2); +set role programmable_projector; +do $session$ +declare + selected_epoch uuid; + selected_generation bigint; + failure_at timestamptz; +begin + select scope.epoch_id, scope.generation + into selected_epoch, selected_generation + from programmable_concurrency_projector.current_scope('race') as scope; + begin + failure_at := pg_catalog.clock_timestamp(); + perform programmable_private.open_run( + '18000000-1000-0000-0000-000000000001', + 'projection', 1, 'race', 'classic-v3', 'core', + selected_epoch, selected_generation, 'rollback-a', + decode(repeat('81', 32), 'hex'), failure_at + ); + perform programmable_private.stage_launch_projection( + '18000000-1000-0000-0000-000000000011', + '18000000-1000-0000-0000-000000000001', + decode(repeat('e1', 20), 'hex'), decode(repeat('e2', 20), 'hex'), + decode(repeat('d4', 32), 'hex'), decode(repeat('e3', 32), 'hex'), + null, decode(repeat('e4', 32), 'hex'), + 'Rollback A', 'RBA', 1000000, + '13600000-1000-0000-0000-000000000002', + 90, decode(repeat('d9', 32), 'hex'), failure_at + ); + perform programmable_private.stage_pool_projection( + '18000000-1000-0000-0000-000000000012', + '18000000-1000-0000-0000-000000000011', + '18000000-1000-0000-0000-000000000001', + decode(repeat('00', 20), 'hex'), decode(repeat('e1', 20), 'hex'), + 3000, 60, decode(repeat('aa', 20), 'hex'), + '13600000-1000-0000-0000-000000000002', + 90, decode(repeat('d9', 32), 'hex'), failure_at + ); + perform programmable_private.stage_pool_fee_configuration( + '18000000-1000-0000-0000-000000000016', + '18000000-1000-0000-0000-000000000012', + '18000000-1000-0000-0000-000000000001', + 100, 100, 90, 10, 0, 0, + '13600000-1000-0000-0000-000000000002', + 90, decode(repeat('d9', 32), 'hex'), failure_at + ); + perform programmable_private.promote_projection_run( + '18000000-1000-0000-0000-000000000013', + '18000000-1000-0000-0000-000000000014', + '18000000-1000-0000-0000-000000000015', + '18000000-1000-0000-0000-000000000001', + 'rollback-a', 1, decode(repeat('e1', 32), 'hex'), + 0, 1, 0, + '13500000-1000-0000-0000-000000000002', + '13500000-1000-0000-0000-000000000003', + 90, decode(repeat('d9', 32), 'hex'), + array['13600000-1000-0000-0000-000000000002'::uuid], + array[]::uuid[], array[]::uuid[], array['rollback-probe-a'], + decode(repeat('e5', 32), 'hex'), failure_at + ); + raise exception using + errcode = 'PZ001', + message = 'injected failure after projection and checkpoint writes'; + exception when sqlstate 'PZ001' then + perform programmable_concurrency_projector.record_result( + 'rollback', 'a', 'rolled-back', + 'injected failure after projection and checkpoint writes' + ); + end; +end +$session$; +reset role; +\endif + +\if :rollback_b +select programmable_concurrency_projector.arrive('rollback', 'b'); +select programmable_concurrency_projector.wait_for_peers('rollback', 2); +set role programmable_projector; +do $session$ +declare + selected_epoch uuid; + selected_generation bigint; + failure_at timestamptz; +begin + select scope.epoch_id, scope.generation + into selected_epoch, selected_generation + from programmable_concurrency_projector.current_scope('race') as scope; + begin + failure_at := pg_catalog.clock_timestamp(); + perform programmable_private.open_run( + '18000000-1000-0000-0000-000000000002', + 'projection', 1, 'race', 'classic-v3', 'core', + selected_epoch, selected_generation, 'rollback-b', + decode(repeat('82', 32), 'hex'), failure_at + ); + perform programmable_private.stage_launch_projection( + '18000000-1000-0000-0000-000000000021', + '18000000-1000-0000-0000-000000000002', + decode(repeat('f1', 20), 'hex'), decode(repeat('f2', 20), 'hex'), + decode(repeat('d4', 32), 'hex'), decode(repeat('f3', 32), 'hex'), + null, decode(repeat('f4', 32), 'hex'), + 'Rollback B', 'RBB', 1000000, + '13600000-1000-0000-0000-000000000002', + 90, decode(repeat('d9', 32), 'hex'), failure_at + ); + perform programmable_private.stage_pool_projection( + '18000000-1000-0000-0000-000000000022', + '18000000-1000-0000-0000-000000000021', + '18000000-1000-0000-0000-000000000002', + decode(repeat('00', 20), 'hex'), decode(repeat('f1', 20), 'hex'), + 3000, 60, decode(repeat('aa', 20), 'hex'), + '13600000-1000-0000-0000-000000000002', + 90, decode(repeat('d9', 32), 'hex'), failure_at + ); + perform programmable_private.stage_pool_fee_configuration( + '18000000-1000-0000-0000-000000000026', + '18000000-1000-0000-0000-000000000022', + '18000000-1000-0000-0000-000000000002', + 100, 100, 90, 10, 0, 0, + '13600000-1000-0000-0000-000000000002', + 90, decode(repeat('d9', 32), 'hex'), failure_at + ); + perform programmable_private.promote_projection_run( + '18000000-1000-0000-0000-000000000023', + '18000000-1000-0000-0000-000000000024', + '18000000-1000-0000-0000-000000000025', + '18000000-1000-0000-0000-000000000002', + 'rollback-b', 1, decode(repeat('e3', 32), 'hex'), + 0, 1, 0, + '13500000-1000-0000-0000-000000000002', + '13500000-1000-0000-0000-000000000003', + 90, decode(repeat('d9', 32), 'hex'), + array['13600000-1000-0000-0000-000000000002'::uuid], + array[]::uuid[], array[]::uuid[], array['rollback-probe-b'], + decode(repeat('f5', 32), 'hex'), failure_at + ); + raise exception using + errcode = 'PZ001', + message = 'injected failure after projection and checkpoint writes'; + exception when sqlstate 'PZ001' then + perform programmable_concurrency_projector.record_result( + 'rollback', 'b', 'rolled-back', + 'injected failure after projection and checkpoint writes' + ); + end; +end +$session$; +reset role; +\endif + +\if :verify +set role programmable_projector; +do $runtime_resume$ +declare + state record; +begin + select * into state + from programmable_private.get_projector_runtime_state_v1( + 1, 'race', 'classic-v3', 'core', 'checkpoint-v1', + array['rpc_provider', 'rpc_provider', 'envio_deployment']::text[], + array['rpc:1:alchemy', 'rpc:1:quicknode', 'concurrency-envio']::text[], + array[ + decode(repeat('91', 32), 'hex'), + decode(repeat('94', 32), 'hex'), + decode(repeat('97', 32), 'hex') + ], + array[ + decode(repeat('92', 32), 'hex'), + decode(repeat('95', 32), 'hex'), + decode(repeat('98', 32), 'hex') + ] + ); + if state.epoch_id <> + '10000000-1000-0000-0000-000000000004'::uuid + or state.pointer_generation <> 3 + or state.lease_generation <> 2 + or state.checkpoint_generation <> 3 + or state.reorg_generation <> 1 + or state.checkpoint_block_number <> 90 + then + raise exception 'stateless runtime resume returned stale CAS state'; + end if; +end +$runtime_resume$; +reset role; + +do $verify$ +begin + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'pointer' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'pointer' and outcome = '40001' + ) <> 1 then + raise exception 'same-scope release pointer CAS did not produce one winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_private.release_epoch_current + where chain_id = 1 and release_id = 'race' + and model_id = 'classic-v3' and source_group = 'core' + and epoch_id = '10000000-1000-0000-0000-000000000004' + and generation = 3 + ) <> 1 then + raise exception 'higher-generation release pointer invariant failed'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'lease' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'lease' and outcome = '40001' + ) <> 1 then + raise exception 'same-scope projector lease CAS did not produce one winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_private.projector_lease_current + where chain_id = 1 and release_id = 'race' + and model_id = 'classic-v3' and source_group = 'core' + and projector_version = 'projector-v1' + and epoch_id in ( + '10000000-1000-0000-0000-000000000002', + '10000000-1000-0000-0000-000000000003' + ) + and pointer_generation = 2 + and lease_generation = 1 + ) <> 1 then + raise exception 'lease winner is not fenced to the winning pointer'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'different' and outcome = 'success' + ) <> 2 then + raise exception 'independent exact scopes interfered'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'stale' and outcome = '40001' + ) <> 2 then + raise exception 'stale pre-generation workers were not fenced'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'checkpoint' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'checkpoint' and outcome = '40001' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.projector_checkpoints + where chain_id = 1 and release_id = 'race' + and model_id = 'classic-v3' and source_group = 'core' + and projector_version = 'checkpoint-v1' + and pointer_generation = 2 and lease_generation = 1 + and checkpoint_generation = 2 and reorg_generation = 0 + and block_number = 100 + ) <> 1 then + raise exception 'same-scope checkpoint CAS did not produce one winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'reorg' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'reorg' and outcome = '40001' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.projector_checkpoint_current as current_checkpoint + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current_checkpoint.checkpoint_id + where current_checkpoint.chain_id = 1 + and current_checkpoint.release_id = 'race' + and current_checkpoint.model_id = 'classic-v3' + and current_checkpoint.source_group = 'core' + and current_checkpoint.projector_version = 'checkpoint-v1' + and current_checkpoint.checkpoint_generation = 3 + and current_checkpoint.reorg_generation = 1 + and checkpoint.epoch_id = '10000000-1000-0000-0000-000000000004' + and checkpoint.pointer_generation = 3 + and checkpoint.lease_generation = 2 + and checkpoint.block_number = 90 + ) <> 1 then + raise exception 'higher-generation reorg CAS did not produce one fenced winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_private.route_eligibility_current + where route_key = 'checkpoint-race' + and chain_id = 1 and release_id = 'race' + and model_id = 'classic-v3' and source_group = 'core' + and epoch_id = '10000000-1000-0000-0000-000000000004' + and pointer_generation = 3 and status = 'ineligible' + ) <> 1 or exists ( + select 1 + from programmable_private.chain_event_current_canonical + where occurrence_id = '13100000-1000-0000-0000-000000000002' + ) then + raise exception 'reorg did not revoke the previous publication state'; + end if; + if ( + select pg_catalog.count(*) + from programmable_concurrency_projector.results + where phase = 'rollback' and outcome = 'rolled-back' + ) <> 2 then + raise exception 'failure injection did not execute in both sessions'; + end if; + if exists ( + select 1 + from programmable_private.run_headers + where run_id in ( + '18000000-1000-0000-0000-000000000001', + '18000000-1000-0000-0000-000000000002' + ) + ) or exists ( + select 1 + from programmable_private.launch_projections + where launch_projection_id in ( + '18000000-1000-0000-0000-000000000011', + '18000000-1000-0000-0000-000000000021' + ) + ) or exists ( + select 1 + from programmable_private.pool_projections + where pool_projection_id in ( + '18000000-1000-0000-0000-000000000012', + '18000000-1000-0000-0000-000000000022' + ) + ) or exists ( + select 1 + from programmable_private.projector_checkpoints + where checkpoint_id in ( + '18000000-1000-0000-0000-000000000014', + '18000000-1000-0000-0000-000000000024' + ) + ) or exists ( + select 1 + from programmable_private.projector_checkpoint_current + where chain_id = 1 and release_id = 'race' + and model_id = 'classic-v3' and source_group = 'core' + and projector_version in ('rollback-a', 'rollback-b') + ) or exists ( + select 1 + from programmable_private.projection_publications + where publication_id in ( + '18000000-1000-0000-0000-000000000013', + '18000000-1000-0000-0000-000000000023' + ) + ) or exists ( + select 1 + from programmable_private.run_lifecycle_outcomes + where outcome_id in ( + '18000000-1000-0000-0000-000000000015', + '18000000-1000-0000-0000-000000000025' + ) + ) or exists ( + select 1 + from programmable_private.route_eligibility_current + where chain_id = 1 and release_id = 'race' + and model_id = 'classic-v3' + and route_key in ('rollback-probe-a', 'rollback-probe-b') + ) or exists ( + select 1 + from programmable_private.chain_event_current_canonical + where occurrence_id = '13600000-1000-0000-0000-000000000002' + ) or exists ( + select 1 + from programmable_private.mutation_audits + where run_id in ( + '18000000-1000-0000-0000-000000000001', + '18000000-1000-0000-0000-000000000002' + ) + ) then + raise exception 'rolled-back projection/checkpoint transaction left state'; + end if; +end +$verify$; + +drop schema programmable_concurrency_projector cascade; +\endif diff --git a/supabase/tests/concurrency/release_probe_nonce_sessions.sql b/supabase/tests/concurrency/release_probe_nonce_sessions.sql new file mode 100644 index 00000000..bcc905f8 --- /dev/null +++ b/supabase/tests/concurrency/release_probe_nonce_sessions.sql @@ -0,0 +1,211 @@ +\if :setup +drop schema if exists programmable_concurrency_release_probe_nonce cascade; +create schema programmable_concurrency_release_probe_nonce; + +create table programmable_concurrency_release_probe_nonce.ready ( + phase text not null, + actor text not null, + primary key (phase, actor) +); + +create table programmable_concurrency_release_probe_nonce.results ( + phase text not null, + actor text not null, + consumed boolean not null, + primary key (phase, actor) +); + +create function programmable_concurrency_release_probe_nonce.arrive( + p_phase text, + p_actor text +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_release_probe_nonce.ready ( + phase, actor + ) values (p_phase, p_actor) + on conflict (phase, actor) do nothing +$function$; + +create function programmable_concurrency_release_probe_nonce.wait_for_peer( + p_phase text +) +returns void +language plpgsql +security definer +set search_path = '' +as $function$ +declare + attempt integer; +begin + for attempt in 1..200 loop + if ( + select pg_catalog.count(*) + from programmable_concurrency_release_probe_nonce.ready + where phase = p_phase + ) = 2 then + return; + end if; + perform pg_catalog.pg_sleep(0.05); + end loop; + raise exception 'timed out waiting for release-probe nonce peer'; +end +$function$; + +create function programmable_concurrency_release_probe_nonce.record_result( + p_phase text, + p_actor text, + p_consumed boolean +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_release_probe_nonce.results ( + phase, actor, consumed + ) values (p_phase, p_actor, p_consumed) + on conflict (phase, actor) do update + set consumed = excluded.consumed +$function$; + +grant usage on schema programmable_concurrency_release_probe_nonce + to programmable_release_probe_nonce; +grant execute on all functions in schema + programmable_concurrency_release_probe_nonce + to programmable_release_probe_nonce; + +set role programmable_migrator; +delete from programmable_release_probe_private.release_probe_nonce_consumptions_v1 +where nonce_digest in ( + decode(repeat('c1', 32), 'hex'), + decode(repeat('c2', 32), 'hex') +); +reset role; +\endif + +\if :same_nonce_a +set session authorization programmable_release_probe_nonce_login; +set role programmable_release_probe_nonce; +select programmable_concurrency_release_probe_nonce.arrive( + 'same-nonce', 'a' +); +select programmable_concurrency_release_probe_nonce.wait_for_peer( + 'same-nonce' +); +select programmable_concurrency_release_probe_nonce.record_result( + 'same-nonce', + 'a', + programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('c1', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ) +); +reset role; +reset session authorization; +\endif + +\if :same_nonce_b +set session authorization programmable_release_probe_nonce_login; +set role programmable_release_probe_nonce; +select programmable_concurrency_release_probe_nonce.arrive( + 'same-nonce', 'b' +); +select programmable_concurrency_release_probe_nonce.wait_for_peer( + 'same-nonce' +); +select programmable_concurrency_release_probe_nonce.record_result( + 'same-nonce', + 'b', + programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('c1', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ) +); +reset role; +reset session authorization; +\endif + +\if :different_route_a +set session authorization programmable_release_probe_nonce_login; +set role programmable_release_probe_nonce; +select programmable_concurrency_release_probe_nonce.arrive( + 'different-route', 'a' +); +select programmable_concurrency_release_probe_nonce.wait_for_peer( + 'different-route' +); +select programmable_concurrency_release_probe_nonce.record_result( + 'different-route', + 'a', + programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-token', decode(repeat('c2', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ) +); +reset role; +reset session authorization; +\endif + +\if :different_route_b +set session authorization programmable_release_probe_nonce_login; +set role programmable_release_probe_nonce; +select programmable_concurrency_release_probe_nonce.arrive( + 'different-route', 'b' +); +select programmable_concurrency_release_probe_nonce.wait_for_peer( + 'different-route' +); +select programmable_concurrency_release_probe_nonce.record_result( + 'different-route', + 'b', + programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-chart', decode(repeat('c2', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ) +); +reset role; +reset session authorization; +\endif + +\if :verify +do $verify$ +begin + if ( + select pg_catalog.count(*) + from programmable_concurrency_release_probe_nonce.results + where phase = 'same-nonce' and consumed + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_release_probe_nonce.results + where phase = 'same-nonce' and not consumed + ) <> 1 then + raise exception 'same-route nonce race did not have exactly one winner'; + end if; + + if ( + select pg_catalog.count(*) + from programmable_release_probe_private.release_probe_nonce_consumptions_v1 + where route_key = 'explore-list' + and nonce_digest = decode(repeat('c1', 32), 'hex') + ) <> 1 then + raise exception 'same-route nonce race persisted an invalid row count'; + end if; + + if ( + select pg_catalog.count(*) + from programmable_concurrency_release_probe_nonce.results + where phase = 'different-route' and consumed + ) <> 2 or exists ( + select 1 + from programmable_concurrency_release_probe_nonce.results + where phase = 'different-route' and not consumed + ) then + raise exception 'route-scoped nonce keys blocked independent routes'; + end if; +end +$verify$; +\endif diff --git a/supabase/tests/concurrency/run.sh b/supabase/tests/concurrency/run.sh new file mode 100755 index 00000000..5942defe --- /dev/null +++ b/supabase/tests/concurrency/run.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +psql_bin="${PSQL:-psql}" + +if ! command -v "$psql_bin" >/dev/null 2>&1; then + echo "psql is required for the two-session concurrency harness" >&2 + exit 127 +fi + +connection_args=() +if [[ -n "${PROGRAMMABLE_DATABASE_URL:-}" ]]; then + connection_args=("$PROGRAMMABLE_DATABASE_URL") +elif [[ -n "${DATABASE_URL:-}" ]]; then + connection_args=("$DATABASE_URL") +fi + +tmp_root="${TMPDIR:-/tmp}" +tmp_dir="$(mktemp -d "${tmp_root%/}/programmable-concurrency.XXXXXX")" + +cleanup() { + find "$tmp_dir" -type f -delete + rmdir "$tmp_dir" +} +trap cleanup EXIT + +run_phase() { + local file="$1" + local phase="$2" + shift 2 + local all_phases=("$@") + local variable_args=() + local name + for name in "${all_phases[@]}"; do + variable_args+=("-v" "${name}=0") + done + variable_args+=("-v" "${phase}=1") + "$psql_bin" "${connection_args[@]}" -X -v ON_ERROR_STOP=1 \ + "${variable_args[@]}" -f "$file" +} + +run_pair() { + local file="$1" + local phase_a="$2" + local phase_b="$3" + shift 3 + local all_phases=("$@") + local output_a="$tmp_dir/${phase_a}.out" + local output_b="$tmp_dir/${phase_b}.out" + local pid_a + local pid_b + local status=0 + + run_phase "$file" "$phase_a" "${all_phases[@]}" >"$output_a" 2>&1 & + pid_a=$! + run_phase "$file" "$phase_b" "${all_phases[@]}" >"$output_b" 2>&1 & + pid_b=$! + + wait "$pid_a" || status=1 + wait "$pid_b" || status=1 + cat "$output_a" + cat "$output_b" + if [[ "$status" -ne 0 ]]; then + echo "concurrent phase failed: $phase_a / $phase_b" >&2 + exit 1 + fi +} + +projector_file="$script_dir/projector_checkpoint_sessions.sql" +projector_phases=( + setup pointer_a pointer_b lease_a lease_b different_a different_b + stale_a stale_b checkpoint_setup checkpoint_a checkpoint_b + reorg_setup reorg_a reorg_b rollback_a rollback_b verify +) +run_phase "$projector_file" setup "${projector_phases[@]}" +run_pair "$projector_file" pointer_a pointer_b "${projector_phases[@]}" +run_pair "$projector_file" lease_a lease_b "${projector_phases[@]}" +run_pair "$projector_file" different_a different_b "${projector_phases[@]}" +run_pair "$projector_file" stale_a stale_b "${projector_phases[@]}" +run_phase "$projector_file" checkpoint_setup "${projector_phases[@]}" +run_pair "$projector_file" checkpoint_a checkpoint_b "${projector_phases[@]}" +run_phase "$projector_file" reorg_setup "${projector_phases[@]}" +run_pair "$projector_file" reorg_a reorg_b "${projector_phases[@]}" +run_pair "$projector_file" rollback_a rollback_b "${projector_phases[@]}" +run_phase "$projector_file" verify "${projector_phases[@]}" + +profile_file="$script_dir/profile_claim_sessions.sql" +profile_phases=( + setup first_wallet_a first_wallet_b first_alias_a first_alias_b after_first + ownership_a ownership_b recover_a recover_b rekey_a rekey_b + alias_claim_a alias_claim_b recover_mutate_a recover_mutate_b + revision_a revision_b verify +) +run_phase "$profile_file" setup "${profile_phases[@]}" +run_pair "$profile_file" first_wallet_a first_wallet_b "${profile_phases[@]}" +run_pair "$profile_file" first_alias_a first_alias_b "${profile_phases[@]}" +run_phase "$profile_file" after_first "${profile_phases[@]}" +run_pair "$profile_file" ownership_a ownership_b "${profile_phases[@]}" +run_pair "$profile_file" recover_a recover_b "${profile_phases[@]}" +run_pair "$profile_file" rekey_a rekey_b "${profile_phases[@]}" +run_pair "$profile_file" alias_claim_a alias_claim_b "${profile_phases[@]}" +run_pair "$profile_file" recover_mutate_a recover_mutate_b "${profile_phases[@]}" +run_pair "$profile_file" revision_a revision_b "${profile_phases[@]}" +run_phase "$profile_file" verify "${profile_phases[@]}" + +username_file="$script_dir/username_sessions.sql" +username_phases=(setup collision_a collision_b verify) +run_phase "$username_file" setup "${username_phases[@]}" +run_pair "$username_file" collision_a collision_b "${username_phases[@]}" +run_phase "$username_file" verify "${username_phases[@]}" + +release_probe_nonce_file="$script_dir/release_probe_nonce_sessions.sql" +release_probe_nonce_phases=( + setup same_nonce_a same_nonce_b different_route_a different_route_b verify +) +run_phase "$release_probe_nonce_file" setup "${release_probe_nonce_phases[@]}" +run_pair "$release_probe_nonce_file" \ + same_nonce_a same_nonce_b "${release_probe_nonce_phases[@]}" +run_pair "$release_probe_nonce_file" \ + different_route_a different_route_b "${release_probe_nonce_phases[@]}" +run_phase "$release_probe_nonce_file" verify "${release_probe_nonce_phases[@]}" + +echo "two-session concurrency harness passed" diff --git a/supabase/tests/concurrency/username_sessions.sql b/supabase/tests/concurrency/username_sessions.sql new file mode 100644 index 00000000..4242d00b --- /dev/null +++ b/supabase/tests/concurrency/username_sessions.sql @@ -0,0 +1,207 @@ +\if :setup +drop schema if exists programmable_concurrency_username cascade; +create schema programmable_concurrency_username; + +create table programmable_concurrency_username.ready ( + phase text not null, + actor text not null, + primary key (phase, actor) +); + +create table programmable_concurrency_username.results ( + phase text not null, + actor text not null, + outcome text not null, + detail text, + primary key (phase, actor) +); + +create function programmable_concurrency_username.arrive( + p_phase text, + p_actor text +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_username.ready (phase, actor) + values (p_phase, p_actor) + on conflict (phase, actor) do nothing +$function$; + +create function programmable_concurrency_username.wait_for_peers( + p_phase text, + p_expected integer +) +returns void +language plpgsql +security definer +set search_path = '' +as $function$ +declare + attempt integer; +begin + for attempt in 1..200 loop + if ( + select pg_catalog.count(*) + from programmable_concurrency_username.ready + where phase = p_phase + ) >= p_expected then + return; + end if; + perform pg_catalog.pg_sleep(0.05); + end loop; + raise exception 'timed out waiting for phase %', p_phase; +end +$function$; + +create function programmable_concurrency_username.record_result( + p_phase text, + p_actor text, + p_outcome text, + p_detail text +) +returns void +language sql +security definer +set search_path = '' +as $function$ + insert into programmable_concurrency_username.results ( + phase, actor, outcome, detail + ) + values (p_phase, p_actor, p_outcome, pg_catalog.left(p_detail, 512)) + on conflict (phase, actor) do update + set outcome = excluded.outcome, + detail = excluded.detail +$function$; + +grant usage on schema programmable_concurrency_username + to programmable_profile_writer; +grant execute on all functions in schema programmable_concurrency_username + to programmable_profile_writer; + +set role programmable_profile_recovery; +select programmable_private.define_profile_hash_version( + 10::smallint, 'hmac-sha256-v10', + decode(repeat('91', 32), 'hex'), decode(repeat('92', 32), 'hex'), + '2026-01-01T01:00:00Z' +); +select programmable_private.set_profile_hash_version_state( + '91000000-0000-0000-0000-000000000010', + 10::smallint, 'current', decode(repeat('93', 32), 'hex'), + '2026-01-01T01:00:01Z' +); +reset role; + +set role programmable_profile_binder; +select programmable_private.bind_profile_subject( + decode(repeat('aa', 20), 'hex'), 10::smallint, + decode(repeat('aa', 32), 'hex'), 'wallet_signature', + decode(repeat('a1', 32), 'hex'), '2026-01-01T01:00:02Z' +); +select programmable_private.bind_profile_subject( + decode(repeat('bb', 20), 'hex'), 10::smallint, + decode(repeat('bb', 32), 'hex'), 'wallet_signature', + decode(repeat('b1', 32), 'hex'), '2026-01-01T01:00:03Z' +); +reset role; +\endif + +\if :collision_a +select programmable_concurrency_username.arrive('collision', 'a'); +select programmable_concurrency_username.wait_for_peers('collision', 2); +set role programmable_profile_writer; +do $session$ +declare + next_revision bigint; +begin + next_revision := programmable_private.mutate_profile( + decode(repeat('aa', 20), 'hex'), 10::smallint, + decode(repeat('aa', 32), 'hex'), 1, 0, + 'CaseName', null, null, null, + decode(repeat('a2', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_username.record_result( + 'collision', 'a', 'success', next_revision::text + ); +exception when others then + perform programmable_concurrency_username.record_result( + 'collision', 'a', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :collision_b +select programmable_concurrency_username.arrive('collision', 'b'); +select programmable_concurrency_username.wait_for_peers('collision', 2); +set role programmable_profile_writer; +do $session$ +declare + next_revision bigint; +begin + next_revision := programmable_private.mutate_profile( + decode(repeat('bb', 20), 'hex'), 10::smallint, + decode(repeat('bb', 32), 'hex'), 1, 0, + 'casename', null, null, null, + decode(repeat('b2', 32), 'hex'), pg_catalog.clock_timestamp() + ); + perform programmable_concurrency_username.record_result( + 'collision', 'b', 'success', next_revision::text + ); +exception when others then + perform programmable_concurrency_username.record_result( + 'collision', 'b', sqlstate, sqlerrm + ); +end +$session$; +reset role; +\endif + +\if :verify +do $verify$ +begin + if ( + select pg_catalog.count(*) + from programmable_concurrency_username.results + where phase = 'collision' and outcome = 'success' + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_concurrency_username.results + where phase = 'collision' and outcome = '23505' + ) <> 1 then + raise exception 'case-insensitive username race did not have one winner'; + end if; + if ( + select pg_catalog.count(*) + from programmable_private.profiles + where username_key = 'casename' and revision = 1 + ) <> 1 or ( + select pg_catalog.count(*) + from programmable_private.profiles as profile + join programmable_private.profile_owner_binding_current as binding + on binding.subject_id = profile.subject_id + where binding.wallet in ( + decode(repeat('aa', 20), 'hex'), + decode(repeat('bb', 20), 'hex') + ) and profile.revision = 0 and profile.username is null + ) <> 1 then + raise exception 'username collision left split or partially mutated profiles'; + end if; + if ( + select pg_catalog.count(*) + from programmable_private.profile_audit_records + where wallet in ( + decode(repeat('aa', 20), 'hex'), + decode(repeat('bb', 20), 'hex') + ) and action = 'profile.mutate' + ) <> 1 then + raise exception 'losing username mutation left audit side effects'; + end if; +end +$verify$; + +drop schema programmable_concurrency_username cascade; +\endif diff --git a/supabase/tests/database/001_schema_constraints.test.sql b/supabase/tests/database/001_schema_constraints.test.sql new file mode 100644 index 00000000..2e9fe119 --- /dev/null +++ b/supabase/tests/database/001_schema_constraints.test.sql @@ -0,0 +1,221 @@ +begin; + +select plan(46); + +select has_schema('programmable_private', 'private schema exists'); +select has_domain('programmable_private', 'eth_address', 'eth_address domain exists'); +select has_domain('programmable_private', 'bytes32_value', 'bytes32 domain exists'); +select has_domain('programmable_private', 'uint256_value', 'uint256 domain exists'); +select has_domain('programmable_private', 'block_number_value', 'block-number domain exists'); + +select lives_ok( + $$select decode(repeat('00', 20), 'hex')::programmable_private.eth_address$$, + '20-byte address succeeds' +); +select throws_ok( + $$select decode(repeat('00', 19), 'hex')::programmable_private.eth_address$$, + '23514', + '19-byte address is rejected' +); +select throws_ok( + $$select decode(repeat('00', 21), 'hex')::programmable_private.eth_address$$, + '23514', + '21-byte address is rejected' +); +select lives_ok( + $$select decode(repeat('00', 32), 'hex')::programmable_private.bytes32_value$$, + '32-byte hash succeeds' +); +select throws_ok( + $$select decode(repeat('00', 31), 'hex')::programmable_private.bytes32_value$$, + '23514', + '31-byte hash is rejected' +); +select throws_ok( + $$select decode(repeat('00', 33), 'hex')::programmable_private.bytes32_value$$, + '23514', + '33-byte hash is rejected' +); +select lives_ok( + $$select decode('00000000', 'hex')::programmable_private.hex_selector$$, + '4-byte selector succeeds' +); +select throws_ok( + $$select decode('000000', 'hex')::programmable_private.hex_selector$$, + '23514', + 'short selector is rejected' +); +select throws_ok( + $$select decode('0000000000', 'hex')::programmable_private.hex_selector$$, + '23514', + 'long selector is rejected' +); + +select ok( + programmable_private.valid_profile_username('Alpha9'), + 'bounded alphanumeric profile username succeeds' +); +select ok( + not programmable_private.valid_profile_username('ab'), + 'profile username below three characters is rejected' +); +select ok( + not programmable_private.valid_profile_username('bad-name'), + 'profile username punctuation is rejected' +); + +select ok( + programmable_private.valid_beneficiary_set( + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000, 4000], + 5 + ), + 'valid ordered beneficiary allocation succeeds' +); +select ok( + not programmable_private.valid_beneficiary_set( + array[decode(repeat('11', 20), 'hex'), decode(repeat('11', 20), 'hex')], + array[6000, 4000], + 5 + ), + 'duplicate beneficiaries are rejected' +); +select ok( + not programmable_private.valid_beneficiary_set( + array[decode(repeat('00', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000, 4000], + 5 + ), + 'zero beneficiary address is rejected' +); +select ok( + not programmable_private.valid_beneficiary_set( + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[0, 10000], + 5 + ), + 'zero beneficiary share is rejected' +); +select ok( + not programmable_private.valid_beneficiary_set( + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000, 3999], + 5 + ), + 'shares must total exactly ten thousand basis points' +); + +select lives_ok( + $$select programmable_private.validate_uint256(0::numeric)$$, + 'uint256 zero succeeds' +); +select lives_ok( + $$select programmable_private.validate_uint256( + 115792089237316195423570985008687907853269984665640564039457584007913129639935::numeric + )$$, + 'uint256 maximum succeeds' +); +select throws_ok( + $$select programmable_private.validate_uint256( + 115792089237316195423570985008687907853269984665640564039457584007913129639936::numeric + )$$, + '22003', + '2^256 is rejected' +); +select throws_ok( + $$select programmable_private.validate_uint256(-1::numeric)$$, + '22003', + 'negative uint256 is rejected' +); +select throws_ok( + $$select programmable_private.validate_uint256(0.1::numeric)$$, + '22003', + '0.1 aborts before any rounded domain assignment' +); +select throws_ok( + $$select programmable_private.validate_uint256(1.5::numeric)$$, + '22003', + '1.5 aborts before any rounded domain assignment' +); +select throws_ok( + $$select programmable_private.validate_uint256(-0.1::numeric)$$, + '22003', + '-0.1 aborts before any rounded domain assignment' +); +select lives_ok( + $$select programmable_private.parse_uint256_decimal('1')$$, + 'canonical decimal succeeds' +); +select throws_ok( + $$select programmable_private.parse_uint256_decimal('01')$$, + '22P02', + 'leading-zero decimal is rejected' +); +select throws_ok( + $$select programmable_private.parse_uint256_decimal('1e2')$$, + '22P02', + 'exponent decimal is rejected' +); +select lives_ok( + $$select 9223372036854775807::programmable_private.block_number_value$$, + 'maximum block domain value succeeds' +); +select throws_ok( + $$select (-1)::programmable_private.block_number_value$$, + '23514', + 'negative block number is rejected' +); +select lives_ok( + $$select 2147483647::programmable_private.transaction_index_value$$, + 'signed-32-bit transaction-index boundary succeeds' +); +select lives_ok( + $$select 2147483648::programmable_private.transaction_index_value$$, + 'first unsigned-only transaction-index value succeeds' +); +select lives_ok( + $$select 4294967295::programmable_private.transaction_index_value$$, + 'explicit transaction-index ceiling succeeds' +); +select throws_ok( + $$select 4294967296::programmable_private.transaction_index_value$$, + '23514', + 'transaction index above explicit ceiling is rejected' +); +select lives_ok( + $$select 2147483647::programmable_private.block_log_index_value$$, + 'signed-32-bit block-global log-index boundary succeeds' +); +select lives_ok( + $$select 2147483648::programmable_private.block_log_index_value$$, + 'first unsigned-only block-global log-index value succeeds' +); +select lives_ok( + $$select 4294967295::programmable_private.block_log_index_value$$, + 'explicit block-global log-index ceiling succeeds' +); +select throws_ok( + $$select 4294967296::programmable_private.block_log_index_value$$, + '23514', + 'block-global log index above explicit ceiling is rejected' +); +select lives_ok( + $$select 2147483647::programmable_private.receipt_log_ordinal_value$$, + 'signed-32-bit receipt-local ordinal boundary succeeds' +); +select lives_ok( + $$select 2147483648::programmable_private.receipt_log_ordinal_value$$, + 'first unsigned-only receipt-local ordinal value succeeds' +); +select lives_ok( + $$select 4294967295::programmable_private.receipt_log_ordinal_value$$, + 'explicit receipt-local ordinal ceiling succeeds' +); +select throws_ok( + $$select 4294967296::programmable_private.receipt_log_ordinal_value$$, + '23514', + 'receipt ordinal above explicit ceiling is rejected' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/002_grants_and_profile_ownership.test.sql b/supabase/tests/database/002_grants_and_profile_ownership.test.sql new file mode 100644 index 00000000..25288c32 --- /dev/null +++ b/supabase/tests/database/002_grants_and_profile_ownership.test.sql @@ -0,0 +1,345 @@ +begin; + +select plan(22); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_roles + where rolname like 'programmable_%' and rolbypassrls + ), + 'no custom role has BYPASSRLS' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_roles + where rolname in ( + 'programmable_migrator', 'programmable_projector', + 'programmable_reconciler', 'programmable_api_reader', + 'programmable_profile_binder', 'programmable_profile_recovery', + 'programmable_profile_writer', 'programmable_maintenance' + ) + and rolcanlogin + ) + and not exists ( + select 1 + from pg_catalog.pg_roles + where rolname in ( + 'programmable_api_reader_login', 'programmable_projector_login', + 'programmable_reconciler_login' + ) + and ( + not rolcanlogin or rolinherit or rolsuper or rolcreatedb + or rolcreaterole or rolreplication or rolbypassrls + ) + ), + 'capability roles are NOLOGIN and gateway roles are unprivileged NOINHERIT logins' +); + +select is( + ( + select pg_catalog.array_agg( + member_role.rolname || '->' || granted_role.rolname + order by member_role.rolname + ) + from pg_catalog.pg_auth_members as membership + join pg_catalog.pg_roles as member_role on member_role.oid = membership.member + join pg_catalog.pg_roles as granted_role on granted_role.oid = membership.roleid + where member_role.rolname in ( + 'programmable_api_reader_login', 'programmable_projector_login', + 'programmable_reconciler_login' + ) + and not membership.admin_option + and not membership.inherit_option + and membership.set_option + ), + array[ + 'programmable_api_reader_login->programmable_api_reader', + 'programmable_projector_login->programmable_projector', + 'programmable_reconciler_login->programmable_reconciler' + ]::text[], + 'each gateway has exactly one SET-only non-admin capability membership' +); + +select ok( + not has_schema_privilege( + 'programmable_api_reader_login', 'programmable_private', 'USAGE' + ) + and not has_schema_privilege( + 'programmable_projector_login', 'programmable_private', 'USAGE' + ) + and not has_schema_privilege( + 'programmable_reconciler_login', 'programmable_private', 'USAGE' + ), + 'gateway sessions have no private-schema capability before explicit SET ROLE' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace on namespace.oid = class.relnamespace + join pg_catalog.pg_roles as owner_role on owner_role.oid = class.relowner + where namespace.nspname = 'programmable_private' + and class.relkind in ('r', 'p', 'v', 'S') + and owner_role.rolname <> 'programmable_migrator' + ), + 'migrator owns every private table, view and sequence' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_type as type + join pg_catalog.pg_namespace as namespace on namespace.oid = type.typnamespace + join pg_catalog.pg_roles as owner_role on owner_role.oid = type.typowner + where namespace.nspname = 'programmable_private' + and owner_role.rolname <> 'programmable_migrator' + ), + 'migrator owns every private type and domain' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as function + join pg_catalog.pg_namespace as namespace on namespace.oid = function.pronamespace + join pg_catalog.pg_roles as owner_role on owner_role.oid = function.proowner + where namespace.nspname = 'programmable_private' + and owner_role.rolname <> 'programmable_migrator' + ), + 'migrator owns every private function' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relkind in ('r', 'p') + and (not class.relrowsecurity or not class.relforcerowsecurity) + ), + 'every private base table enables and forces RLS' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_policy as policy + join pg_catalog.pg_class as class on class.oid = policy.polrelid + join pg_catalog.pg_namespace as namespace on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and ( + policy.polroles <> array[ + (select oid from pg_catalog.pg_roles where rolname = 'programmable_migrator') + ]::oid[] + or policy.polcmd <> '*' + ) + ), + 'RLS policies target only the migrator owner' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace on namespace.oid = class.relnamespace + cross join unnest(array[ + 'anon', 'authenticated', 'service_role', + 'programmable_api_reader', 'programmable_projector', + 'programmable_reconciler', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance' + ]) as checked_role(role_name) + where namespace.nspname = 'programmable_private' + and class.relkind in ('r', 'p') + and ( + has_table_privilege(checked_role.role_name, class.oid, 'SELECT') + or has_table_privilege(checked_role.role_name, class.oid, 'INSERT') + or has_table_privilege(checked_role.role_name, class.oid, 'UPDATE') + or has_table_privilege(checked_role.role_name, class.oid, 'DELETE') + or has_table_privilege(checked_role.role_name, class.oid, 'TRUNCATE') + ) + ), + 'browser and runtime roles have no base-table privileges' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace on namespace.oid = class.relnamespace + cross join unnest(array[ + 'anon', 'authenticated', 'service_role', + 'programmable_api_reader', 'programmable_projector', + 'programmable_reconciler', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance' + ]) as checked_role(role_name) + where namespace.nspname = 'programmable_private' + and class.relkind = 'S' + and ( + has_sequence_privilege(checked_role.role_name, class.oid, 'USAGE') + or has_sequence_privilege(checked_role.role_name, class.oid, 'SELECT') + or has_sequence_privilege(checked_role.role_name, class.oid, 'UPDATE') + ) + ), + 'runtime roles cannot use private sequences' +); + +select ok( + not has_schema_privilege('anon', 'programmable_private', 'USAGE') + and not has_schema_privilege('authenticated', 'programmable_private', 'USAGE') + and not has_schema_privilege('service_role', 'programmable_private', 'USAGE'), + 'Data API roles have no private-schema usage' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as function + join pg_catalog.pg_namespace as namespace on namespace.oid = function.pronamespace + where namespace.nspname = 'programmable_private' + and has_function_privilege('public', function.oid, 'EXECUTE') + ), + 'PUBLIC cannot execute any private function' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as function + join pg_catalog.pg_namespace as namespace on namespace.oid = function.pronamespace + where namespace.nspname = 'programmable_private' + and function.prosecdef + and not ('search_path=""' = any(function.proconfig)) + ), + 'every SECURITY DEFINER function fixes an empty search_path' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relkind = 'v' + and not ( + 'security_barrier=true' = any(class.reloptions) + and 'security_invoker=false' = any(class.reloptions) + ) + ), + 'every stable view is definer-mode and security-barrier' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_auth_members as membership + join pg_catalog.pg_roles as member_role on member_role.oid = membership.member + where member_role.rolname in ( + 'programmable_projector', 'programmable_reconciler', + 'programmable_api_reader', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance' + ) + ), + 'runtime capability roles inherit no roles' +); + +select ok( + has_table_privilege( + 'programmable_api_reader', + 'programmable_private.recent_launches_v1', + 'SELECT' + ) + and not has_table_privilege( + 'programmable_api_reader', + 'programmable_private.reconciliation_occurrence_summary_v1', + 'SELECT' + ), + 'API reader receives only named server views' +); + +select ok( + has_table_privilege( + 'programmable_reconciler', + 'programmable_private.reconciliation_occurrence_summary_v1', + 'SELECT' + ) + and not has_table_privilege( + 'programmable_reconciler', + 'programmable_private.recent_launches_v1', + 'SELECT' + ), + 'reconciler receives only named reconciliation views' +); + +select ok( + has_function_privilege( + 'programmable_profile_binder', + 'programmable_private.bind_profile_subject(bytea,smallint,bytea,text,bytea,timestamptz)', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_profile_binder', + 'programmable_private.mutate_profile(bytea,smallint,bytea,bigint,bigint,text,text,text,text,bytea,timestamptz)', + 'EXECUTE' + ), + 'first binder and ordinary writer are separate capabilities' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_default_acl as defaults + join pg_catalog.pg_roles as owner_role on owner_role.oid = defaults.defaclrole + left join pg_catalog.pg_namespace as namespace on namespace.oid = defaults.defaclnamespace + cross join lateral aclexplode(defaults.defaclacl) as acl + left join pg_catalog.pg_roles as grantee on grantee.oid = acl.grantee + where owner_role.rolname = 'programmable_migrator' + and namespace.nspname = 'programmable_private' + and ( + acl.grantee = 0 + or grantee.rolname in ( + 'anon', 'authenticated', 'service_role', + 'programmable_projector', 'programmable_reconciler', + 'programmable_api_reader', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance' + ) + ) + ), + 'migrator default ACLs grant no runtime or PUBLIC privilege' +); + +select ok( + not has_schema_privilege('service_role', 'programmable_private', 'CREATE') + and not has_schema_privilege('service_role', 'programmable_private', 'USAGE'), + 'managed service_role has zero private-schema grant' +); + +select ok( + has_function_privilege( + 'programmable_projector', + 'programmable_private.get_projector_runtime_state_v1(bigint,text,text,text,text,text[],text[],bytea[],bytea[])', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_api_reader', + 'programmable_private.get_projector_runtime_state_v1(bigint,text,text,text,text,text[],text[],bytea[],bytea[])', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_reconciler', + 'programmable_private.get_projector_runtime_state_v1(bigint,text,text,text,text,text[],text[],bytea[],bytea[])', + 'EXECUTE' + ), + 'only the projector can read exact scoped CAS and provider runtime state' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/003_ingestion_idempotency.test.sql b/supabase/tests/database/003_ingestion_idempotency.test.sql new file mode 100644 index 00000000..040ca099 --- /dev/null +++ b/supabase/tests/database/003_ingestion_idempotency.test.sql @@ -0,0 +1,500 @@ +begin; + +create function public.ingestion_test_occurrence_preimage() +returns bytea +language sql +stable +security definer +set search_path = '' +as $function$ + select occurrence.canonical_preimage + from programmable_private.chain_event_occurrences as occurrence + where occurrence.occurrence_id = '70000000-0000-0000-0000-000000000001' +$function$; + +set local role programmable_migrator; +insert into programmable_private.fingerprint_encoding_versions ( + fingerprint_domain, encoding_version, domain_prefix, write_enabled, + definition_commitment, allowlisted_at +) +values ( + 'occurrence', 2, + decode( + '70726f6772616d6d61626c653a6f6363757272656e63653a763200', + 'hex' + ), + true, decode(repeat('02', 32), 'hex'), '2026-07-31T06:00:00Z' +); +reset role; + +set local role programmable_projector; + +select programmable_private.create_release_epoch( + '10000000-0000-0000-0000-000000000001', + 1, 'classic-v3', 'classic-v3', 'core', 1, + decode(repeat('10', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + decode(repeat('12', 32), 'hex'), + '2026-07-31T06:00:00Z' +); +select programmable_private.append_release_source_binding( + '11000000-0000-0000-0000-000000000001', + '10000000-0000-0000-0000-000000000001', + 'fixture-launcher', 'launcher', 'ethereum_contract', + decode(repeat('33', 20), 'hex'), null, 25639596, + decode(repeat('66', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('14', 32), 'hex'), decode(repeat('15', 32), 'hex'), + '2026-07-31T06:00:00.500Z' +); +select programmable_private.activate_release_epoch( + 1, 'classic-v3', 'classic-v3', 'core', + '10000000-0000-0000-0000-000000000001', + 0, 1, decode(repeat('13', 32), 'hex'), + '2026-07-31T06:00:01Z' +); +select programmable_private.register_rpc_provider_deployment( + '20000000-0000-0000-0000-000000000001', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('a3', 32), 'hex'), + decode(repeat('21', 32), 'hex'), decode(repeat('22', 32), 'hex'), + decode(repeat('23', 32), 'hex'), '2026-07-31T06:00:02Z' +); +select programmable_private.register_rpc_provider_deployment( + '20000000-0000-0000-0000-000000000002', + 1, 'quicknode', 'rpc-provider-v1', + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('b3', 32), 'hex'), + decode(repeat('24', 32), 'hex'), decode(repeat('25', 32), 'hex'), + decode(repeat('26', 32), 'hex'), '2026-07-31T06:00:03Z' +); +select programmable_private.register_provider_deployment( + '20000000-0000-0000-0000-000000000003', + 'envio_deployment', 'envio-mainnet-v1', + decode(repeat('27', 32), 'hex'), decode(repeat('28', 32), 'hex'), + decode(repeat('29', 32), 'hex'), '2026-07-31T06:00:04Z' +); +select programmable_private.open_run( + '30000000-0000-0000-0000-000000000001', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + '10000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('31', 32), 'hex'), + '2026-07-31T06:01:00Z' +); +select programmable_private.append_safe_head_observation( + '40000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + '20000000-0000-0000-0000-000000000001', + '20000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000100', 'hex'), + decode(repeat('41', 32), 'hex'), + '2026-07-31T06:01:01Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '50000000-0000-0000-0000-000000000001', + '40000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + 25639596, + decode(repeat('22', 32), 'hex'), decode(repeat('22', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000201', 'hex'), + decode(repeat('51', 32), 'hex'), + '2026-07-31T06:01:02Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), + '30000000-0000-0000-0000-000000000001', + 25639596, + decode(repeat('22', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + 3, + 7, + decode(repeat('33', 20), 'hex'), + decode(repeat('44', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('aa', 32), 'hex'), decode(repeat('bb', 32), 'hex')], + decode('010203', 'hex'), + '{ + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "creator": "0x3333333333333333333333333333333333333333", + "flags": [true, false], + "nested": {"b": "two", "a": "one"} + }'::jsonb, + decode(repeat('55', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), + '20000000-0000-0000-0000-000000000003', + decode(repeat('66', 32), 'hex'), + '2026-07-31T06:01:03Z' +); +select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310000000000000000011111111111111111111111111111111111111111111111111111111111111111000000000000000001873aac222222222222222222222222222222222222222222222222222222222222222200000003000000073333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444400000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000003010203000000c67b22616d6f756e74223a22313135373932303839323337333136313935343233353730393835303038363837393037383533323639393834363635363430353634303339343537353834303037393133313239363339393335222c2263726561746f72223a22307833333333333333333333333333333333333333333333333333333333333333333333333333333333222c22666c616773223a5b747275652c66616c73655d2c226e6573746564223a7b2261223a226f6e65222c2262223a2274776f227d7d55555555555555555555555555555555555555555555555555555555555555550000001070726f6a6563746f722d76312e302e3066666666666666666666666666666666666666666666666666666666666666660000000a636c61737369632d76330000000a636c61737369632d763300000009313a32323a31313a370000000a32353633393539363a37000000006a6c01a0', 'hex'), + decode('6fe25eb0a62ea86736aa134ada719976b6166844b98d83b56d478ae409956955', 'hex'), + '2026-07-31T06:01:04Z' +); + +select plan(22); + +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('67', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 1::smallint, + public.ingestion_test_occurrence_preimage(), + decode('6fe25eb0a62ea86736aa134ada719976b6166844b98d83b56d478ae409956955', 'hex'), + '2026-07-31T06:01:04Z' + ) + $sql$, + '23514', + 'an occurrence with an ABI commitment outside its release manifest is rejected' +); + +select throws_ok( + $sql$ + select programmable_private.append_release_source_binding( + '11000000-0000-0000-0000-000000000002', + '10000000-0000-0000-0000-000000000001', + 'late-source', 'launcher', 'ethereum_contract', + decode(repeat('34', 20), 'hex'), null, 25639596, + decode(repeat('67', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('16', 32), 'hex'), decode(repeat('17', 32), 'hex'), + '2026-07-31T06:01:04.100Z' + ) + $sql$, + '55000', + 'an active release epoch cannot acquire another source binding' +); + +select throws_ok( + $sql$ + select programmable_private.append_release_source_binding( + '11000000-0000-0000-0000-000000000003', + '10000000-0000-0000-0000-000000000001', + 'wrong-artifact', 'launcher', 'ethereum_contract', + decode(repeat('35', 20), 'hex'), null, 25639596, + decode(repeat('68', 32), 'hex'), decode(repeat('19', 32), 'hex'), + decode(repeat('18', 32), 'hex'), decode(repeat('19', 32), 'hex'), + '2026-07-31T06:01:04.200Z' + ) + $sql$, + '23514', + 'a source binding cannot change its epoch artifact commitment' +); + +select is( + programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), + '30000000-0000-0000-0000-000000000001', + 25639596, + decode(repeat('22', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + 3, + 7, + decode(repeat('33', 20), 'hex'), + decode(repeat('44', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('aa', 32), 'hex'), decode(repeat('bb', 32), 'hex')], + decode('010203', 'hex'), + '{ + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "creator": "0x3333333333333333333333333333333333333333", + "flags": [true, false], + "nested": {"b": "two", "a": "one"} + }'::jsonb, + decode(repeat('55', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), + '20000000-0000-0000-0000-000000000003', + decode(repeat('66', 32), 'hex'), + '2026-07-31T06:01:03Z' +), + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), + 'exact candidate replay returns the existing identity' +); + +select is( + programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 1::smallint, + public.ingestion_test_occurrence_preimage(), + decode('6fe25eb0a62ea86736aa134ada719976b6166844b98d83b56d478ae409956955', 'hex'), + '2026-07-31T06:01:04Z' + ), + '70000000-0000-0000-0000-000000000001'::uuid, + 'exact occurrence replay returns the existing occurrence' +); + +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 1::smallint, + public.ingestion_test_occurrence_preimage() || decode('01', 'hex'), + decode('6fe25eb0a62ea86736aa134ada719976b6166844b98d83b56d478ae409956955', 'hex'), + '2026-07-31T06:01:04Z' + ) + $sql$, + '23505', + 'changed preimage with original digest is rejected' +); + +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 1::smallint, + public.ingestion_test_occurrence_preimage(), + decode(repeat('69', 32), 'hex'), '2026-07-31T06:01:04Z' + ) + $sql$, + '23505', + 'original preimage with changed digest is rejected' +); + +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 1::smallint, + public.ingestion_test_occurrence_preimage() || decode('02', 'hex'), + decode(repeat('6a', 32), 'hex'), '2026-07-31T06:01:04Z' + ) + $sql$, + '23505', + 'changed occurrence preimage and digest are rejected together' +); + +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 2::smallint, + decode( + '70726f6772616d6d61626c653a6f6363757272656e63653a76320000', + 'hex' + ), + decode(repeat('6b', 32), 'hex'), '2026-07-31T06:01:04Z' + ) + $sql$, + '23505', + 'a newly allowlisted version cannot rewrite an existing v1 logical key' +); + +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', + 3::smallint, + decode( + '70726f6772616d6d61626c653a6f6363757272656e63653a76330000', + 'hex' + ), + decode(repeat('6c', 32), 'hex'), '2026-07-31T06:01:04Z' + ) + $sql$, + '22023', + 'an unknown fingerprint encoding version is rejected before replay' +); + +select throws_ok( + $sql$ + select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('81', 32), 'hex'), decode(repeat('71', 32), 'hex'), 8), + '30000000-0000-0000-0000-000000000001', + 80.1, + decode(repeat('81', 32), 'hex'), + decode(repeat('71', 32), 'hex'), + 2, + 8, + decode(repeat('62', 20), 'hex'), + decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('63', 32), 'hex')], + decode('', 'hex'), + '{}'::jsonb, + decode(repeat('65', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('81', 32), 'hex'), decode(repeat('71', 32), 'hex'), 8), + '20000000-0000-0000-0000-000000000003', + decode(repeat('72', 32), 'hex'), + '2026-07-31T06:01:05Z' +) + $sql$, + '22023', + 'fractional block number aborts at function entry' +); + +reset role; + +select is( + ( + select count(*) + from programmable_private.release_source_bindings + where epoch_id = '10000000-0000-0000-0000-000000000001' + ), + 1::bigint, + 'failed binding calls leave the immutable source manifest unchanged' +); +select is( + ( + select count(*) + from programmable_private.fingerprint_encoding_versions + where fingerprint_domain = 'occurrence' and encoding_version = 2 + ), + 1::bigint, + 'new codec version is allowlisted without rewriting the stored v1 pair' +); +select is( + (select count(*) from programmable_private.envio_candidates), + 1::bigint, + 'candidate replay and failed input leave one fact' +); +select is( + (select count(*) from programmable_private.chain_event_identities), + 1::bigint, + 'logical identity is insert-once' +); +select is( + (select count(*) from programmable_private.chain_event_occurrences), + 1::bigint, + 'occurrence replay leaves one immutable placement' +); +select is( + (select count(*) from programmable_private.chain_event_occurrence_status_history), + 1::bigint, + 'exact replay does not duplicate observed status' +); +select is( + (select raw_data from programmable_private.chain_event_occurrences limit 1), + decode('010203', 'hex'), + 'raw event data is retained byte-for-byte' +); + +set local role programmable_projector; +select programmable_private.append_run_outcome( + '80000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('80', 32), 'hex'), + '2026-07-31T06:02:00Z' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + '40000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + '20000000-0000-0000-0000-000000000001', + '20000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000100', 'hex'), + decode(repeat('41', 32), 'hex'), '2026-07-31T06:02:01Z' + ) + $sql$, + '55000', + 'terminal ingestion runs reject safe-head evidence replays' +); +select throws_ok( + $sql$ + select programmable_private.append_dual_rpc_block_evidence( + '50000000-0000-0000-0000-000000000001', + '40000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', 25639596, + decode(repeat('22', 32), 'hex'), decode(repeat('22', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000201', 'hex'), + decode(repeat('51', 32), 'hex'), '2026-07-31T06:02:02Z' + ) + $sql$, + '55000', + 'terminal ingestion runs reject block-evidence replays' +); +select throws_ok( + $sql$ + select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), + '30000000-0000-0000-0000-000000000001', + 25639596, + decode(repeat('22', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + 3, + 7, + decode(repeat('33', 20), 'hex'), + decode(repeat('44', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('aa', 32), 'hex'), decode(repeat('bb', 32), 'hex')], + decode('010203', 'hex'), + '{"amount":"115792089237316195423570985008687907853269984665640564039457584007913129639935","creator":"0x3333333333333333333333333333333333333333","flags":[true,false],"nested":{"a":"one","b":"two"}}'::jsonb, + decode(repeat('55', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), + '20000000-0000-0000-0000-000000000003', + decode(repeat('66', 32), 'hex'), + '2026-07-31T06:02:03Z' +) + $sql$, + '55000', + 'terminal ingestion runs reject raw candidate replays' +); +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '60000000-0000-0000-0000-000000000001', + '70000000-0000-0000-0000-000000000001', + '30000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('22', 32), 'hex'), decode(repeat('11', 32), 'hex'), 7), 0, '2026-07-31T02:00:00Z', 'projector-v1.0.0', + decode(repeat('66', 32), 'hex'), + '50000000-0000-0000-0000-000000000001', 1::smallint, + public.ingestion_test_occurrence_preimage(), + decode('6fe25eb0a62ea86736aa134ada719976b6166844b98d83b56d478ae409956955', 'hex'), + '2026-07-31T06:02:04Z' + ) + $sql$, + '55000', + 'terminal ingestion runs reject occurrence replays' +); +reset role; + +select * from finish(); +rollback; diff --git a/supabase/tests/database/004_reorg_occurrences.test.sql b/supabase/tests/database/004_reorg_occurrences.test.sql new file mode 100644 index 00000000..67b8fbad --- /dev/null +++ b/supabase/tests/database/004_reorg_occurrences.test.sql @@ -0,0 +1,963 @@ +begin; + +-- Preserve regression coverage for the retired v1 promotion body without +-- restoring its production capability. The grant is rolled back with pgTAP. +set local role programmable_migrator; +grant execute on function programmable_private.promote_projection_run( + uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, timestamptz +) to programmable_projector; + +set local role programmable_projector; + +select programmable_private.create_release_epoch( + 'a1000000-0000-0000-0000-000000000001', + 1, 'classic-v3', 'classic-v3', 'core', 1, + decode(repeat('10', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + decode(repeat('12', 32), 'hex'), + '2026-07-31T05:00:00Z' +); +select programmable_private.append_release_source_binding( + 'a1100000-0000-0000-0000-000000000001', + 'a1000000-0000-0000-0000-000000000001', + 'reorg-launcher', 'launcher', 'ethereum_contract', + decode(repeat('62', 20), 'hex'), null, 70, + decode(repeat('68', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('14', 32), 'hex'), decode(repeat('15', 32), 'hex'), + '2026-07-31T05:00:00.500Z' +); +select programmable_private.append_release_projection_event_rule( + rule_id, 'a1000000-0000-0000-0000-000000000001', projection_kind, + 'launcher', 'MemeTokenLaunchedV2', commitment, '2026-07-31T05:00:00.600Z' +) +from (values + ('a1200000-0000-0000-0000-000000000001'::uuid, 'launch', decode(repeat('01', 32), 'hex')), + ('a1200000-0000-0000-0000-000000000002'::uuid, 'pool', decode(repeat('02', 32), 'hex')), + ('a1200000-0000-0000-0000-000000000003'::uuid, 'pool_fee_configuration', decode(repeat('03', 32), 'hex')), + ('a1200000-0000-0000-0000-000000000004'::uuid, 'launch_requirement', decode(repeat('04', 32), 'hex')) +) as rule(rule_id, projection_kind, commitment); +select programmable_private.append_release_launch_requirement( + 'a1300000-0000-0000-0000-000000000001', + 'a1000000-0000-0000-0000-000000000001', 0, + 'launcher', 'MemeTokenLaunchedV2', 'always', + decode(repeat('05', 32), 'hex'), '2026-07-31T05:00:00.700Z' +); +select programmable_private.activate_release_epoch( + 1, 'classic-v3', 'classic-v3', 'core', + 'a1000000-0000-0000-0000-000000000001', + 0, 1, decode(repeat('13', 32), 'hex'), + '2026-07-31T05:00:01Z' +); +select programmable_private.register_rpc_provider_deployment( + 'b1000000-0000-0000-0000-000000000001', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('a3', 32), 'hex'), + decode(repeat('21', 32), 'hex'), decode(repeat('22', 32), 'hex'), + decode(repeat('23', 32), 'hex'), '2026-07-31T05:00:02Z' +); +select programmable_private.register_rpc_provider_deployment( + 'b1000000-0000-0000-0000-000000000002', + 1, 'quicknode', 'rpc-provider-v1', + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('b3', 32), 'hex'), + decode(repeat('24', 32), 'hex'), decode(repeat('25', 32), 'hex'), + decode(repeat('26', 32), 'hex'), '2026-07-31T05:00:03Z' +); +select programmable_private.register_provider_deployment( + 'b1000000-0000-0000-0000-000000000003', + 'envio_deployment', 'reorg-envio', + decode(repeat('27', 32), 'hex'), decode(repeat('28', 32), 'hex'), + decode(repeat('29', 32), 'hex'), '2026-07-31T05:00:04Z' +); +select programmable_private.open_run( + '6c000000-0000-0000-0000-000000000001', + 'ingestion', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('30', 32), 'hex'), + '2026-07-31T05:00:05Z' +); +select programmable_private.open_run( + 'c1000000-0000-0000-0000-000000000001', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + 'a1000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('31', 32), 'hex'), + '2026-07-31T05:01:00Z' +); +select programmable_private.append_safe_head_observation( + 'd1000000-0000-0000-0000-000000000001', + 'c1000000-0000-0000-0000-000000000001', + 'b1000000-0000-0000-0000-000000000001', + 'b1000000-0000-0000-0000-000000000002', + 1, 1, 120, 120, 12, 108, + decode(repeat('08', 32), 'hex'), decode(repeat('08', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000110', 'hex'), + decode(repeat('41', 32), 'hex'), + '2026-07-31T05:01:01Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e1000000-0000-0000-0000-000000000070', + 'd1000000-0000-0000-0000-000000000001', + 'c1000000-0000-0000-0000-000000000001', + 70, decode(repeat('70', 32), 'hex'), decode(repeat('70', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000210', 'hex'), + decode(repeat('40', 32), 'hex'), '2026-07-31T05:01:01.500Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e1000000-0000-0000-0000-000000000080', + 'd1000000-0000-0000-0000-000000000001', + 'c1000000-0000-0000-0000-000000000001', + 80, decode(repeat('80', 32), 'hex'), decode(repeat('80', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000211', 'hex'), + decode(repeat('42', 32), 'hex'), '2026-07-31T05:01:02Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e1000000-0000-0000-0000-000000000081', + 'd1000000-0000-0000-0000-000000000001', + 'c1000000-0000-0000-0000-000000000001', + 81, decode(repeat('81', 32), 'hex'), decode(repeat('81', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000212', 'hex'), + decode(repeat('43', 32), 'hex'), '2026-07-31T05:01:03Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + 'c1000000-0000-0000-0000-000000000001', + 70, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), + 1, 1, decode(repeat('62', 20), 'hex'), decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', array[decode(repeat('63', 32), 'hex')], + decode('00', 'hex'), '{"amount":"0"}'::jsonb, + decode(repeat('64', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('65', 32), 'hex'), '2026-07-31T05:01:03.500Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7), + 'c1000000-0000-0000-0000-000000000001', + 80, + decode(repeat('80', 32), 'hex'), + decode(repeat('61', 32), 'hex'), + 2, + 7, + decode(repeat('62', 20), 'hex'), + decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('63', 32), 'hex'), decode(repeat('64', 32), 'hex')], + decode('010203', 'hex'), + '{"amount":"1"}'::jsonb, + decode(repeat('65', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('66', 32), 'hex'), + '2026-07-31T05:01:04Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19), + 'c1000000-0000-0000-0000-000000000001', + 81, + decode(repeat('81', 32), 'hex'), + decode(repeat('61', 32), 'hex'), + 3, + 19, + decode(repeat('62', 20), 'hex'), + decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('63', 32), 'hex'), decode(repeat('64', 32), 'hex')], + decode('010203', 'hex'), + '{"amount":"1"}'::jsonb, + decode(repeat('65', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('67', 32), 'hex'), + '2026-07-31T05:01:05Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + '6c000000-0000-0000-0000-000000000001', + 70, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), + 1, 1, decode(repeat('62', 20), 'hex'), decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', array[decode(repeat('63', 32), 'hex')], + decode('00', 'hex'), '{"amount":"0"}'::jsonb, + decode(repeat('64', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('65', 32), 'hex'), '2026-07-31T05:01:05.100Z', + 'canonical-events', 'ReorgLauncher' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7 + ), + '6c000000-0000-0000-0000-000000000001', + 80, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), + 2, 7, decode(repeat('62', 20), 'hex'), decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('63', 32), 'hex'), decode(repeat('64', 32), 'hex')], + decode('010203', 'hex'), '{"amount":"1"}'::jsonb, + decode(repeat('65', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7 + ), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('66', 32), 'hex'), '2026-07-31T05:01:05.200Z', + 'canonical-events', 'ReorgLauncher' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19 + ), + '6c000000-0000-0000-0000-000000000001', + 81, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), + 3, 19, decode(repeat('62', 20), 'hex'), decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('63', 32), 'hex'), decode(repeat('64', 32), 'hex')], + decode('010203', 'hex'), '{"amount":"1"}'::jsonb, + decode(repeat('65', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19 + ), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('67', 32), 'hex'), '2026-07-31T05:01:05.300Z', + 'canonical-events', 'ReorgLauncher' +); +select programmable_private.resolve_envio_candidate( + '6d000000-0000-0000-0000-000000000070', + 'c1000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + 'a1100000-0000-0000-0000-000000000001', null, + decode(repeat('68', 32), 'hex'), decode(repeat('70', 32), 'hex'), + '2026-07-31T05:01:05.400Z' +); +select programmable_private.resolve_envio_candidate( + '6d000000-0000-0000-0000-000000000080', + 'c1000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7 + ), + 'a1100000-0000-0000-0000-000000000001', null, + decode(repeat('68', 32), 'hex'), decode(repeat('71', 32), 'hex'), + '2026-07-31T05:01:05.500Z' +); +select programmable_private.resolve_envio_candidate( + '6d000000-0000-0000-0000-000000000081', + 'c1000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19 + ), + 'a1100000-0000-0000-0000-000000000001', null, + decode(repeat('68', 32), 'hex'), decode(repeat('72', 32), 'hex'), + '2026-07-31T05:01:05.600Z' +); +select programmable_private.append_chain_event_occurrence( + 'f0000000-0000-0000-0000-000000000001', + 'f0000000-0000-0000-0000-000000000070', + 'c1000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + 0, '2026-07-31T04:58:00Z', 'decoder-v1', + decode(repeat('68', 32), 'hex'), + 'e1000000-0000-0000-0000-000000000070', 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a7631000f', 'hex'), + decode(repeat('68', 32), 'hex'), '2026-07-31T05:01:05.700Z' +); +select programmable_private.append_chain_event_occurrence( + 'f1000000-0000-0000-0000-000000000001', + 'f1000000-0000-0000-0000-000000000080', + 'c1000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7), 0, '2026-07-31T04:59:00Z', 'decoder-v1', + decode(repeat('68', 32), 'hex'), + 'e1000000-0000-0000-0000-000000000080', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310010', 'hex'), + decode(repeat('69', 32), 'hex'), '2026-07-31T05:01:06Z' +); +select programmable_private.append_chain_event_occurrence( + 'f1000000-0000-0000-0000-000000000001', + 'f1000000-0000-0000-0000-000000000081', + 'c1000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19), 0, '2026-07-31T04:59:12Z', 'decoder-v1', + decode(repeat('68', 32), 'hex'), + 'e1000000-0000-0000-0000-000000000081', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310011', 'hex'), + decode(repeat('6a', 32), 'hex'), '2026-07-31T05:01:07Z' +); + +select programmable_private.acquire_projector_lease( + 1, 'classic-v3', 'classic-v3', 'core', 'projector-v1', + 'a1000000-0000-0000-0000-000000000001', 1, + 0, 1, decode(repeat('aa', 32), 'hex'), 'worker-a', + '2026-07-31T05:02:00Z', '2026-07-31T05:12:00Z', + decode(repeat('ab', 32), 'hex') +); +select programmable_private.open_run( + '70000000-0000-0000-0000-000000000070', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + 'a1000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('70', 32), 'hex'), + '2026-07-31T05:02:00.100Z' +); +select programmable_private.stage_launch_projection( + '84000000-0000-0000-0000-000000000070', + '70000000-0000-0000-0000-000000000070', + decode(repeat('a0', 20), 'hex'), decode(repeat('a2', 20), 'hex'), + decode(repeat('60', 32), 'hex'), decode(repeat('a0', 32), 'hex'), + null, decode(repeat('a5', 32), 'hex'), + 'Origin Token', 'ORG', 1000000, + 'f0000000-0000-0000-0000-000000000070', + 70, decode(repeat('70', 32), 'hex'), '2026-07-31T05:02:00.200Z' +); +select programmable_private.stage_pool_projection( + '84100000-0000-0000-0000-000000000070', + '84000000-0000-0000-0000-000000000070', + '70000000-0000-0000-0000-000000000070', + decode(repeat('00', 20), 'hex'), decode(repeat('a0', 20), 'hex'), + 3000, 60, decode(repeat('a4', 20), 'hex'), + 'f0000000-0000-0000-0000-000000000070', + 70, decode(repeat('70', 32), 'hex'), '2026-07-31T05:02:00.300Z' +); +select programmable_private.stage_pool_fee_configuration( + '84200000-0000-0000-0000-000000000070', + '84100000-0000-0000-0000-000000000070', + '70000000-0000-0000-0000-000000000070', + 100, 100, 90, 10, 0, 0, + 'f0000000-0000-0000-0000-000000000070', + 70, decode(repeat('70', 32), 'hex'), '2026-07-31T05:02:00.400Z' +); +select programmable_private.stage_launch_occurrence_role( + '84000000-0000-0000-0000-000000000070', 'launcher', + 'f0000000-0000-0000-0000-000000000070', '2026-07-31T05:02:00.500Z' +); +select programmable_private.stage_launch_projection_conditions( + '84000000-0000-0000-0000-000000000070', false, + '2026-07-31T05:02:00.600Z' +); +select programmable_private.promote_projection_run( + '81000000-0000-0000-0000-000000000070', + '82000000-0000-0000-0000-000000000070', + '83000000-0000-0000-0000-000000000070', + '70000000-0000-0000-0000-000000000070', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 0, 1, 0, + 'd1000000-0000-0000-0000-000000000001', + 'e1000000-0000-0000-0000-000000000070', + 70, decode(repeat('70', 32), 'hex'), 1, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + array['f0000000-0000-0000-0000-000000000070'::uuid], + array[]::uuid[], array[]::uuid[], + array['6d000000-0000-0000-0000-000000000070'::uuid], + array['explore-list']::text[], + decode(repeat('84', 32), 'hex'), '2026-07-31T05:02:00.700Z' +); +select programmable_private.open_run( + '71000000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + 'a1000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('71', 32), 'hex'), + '2026-07-31T05:02:01Z' +); +select programmable_private.stage_launch_projection( + '84000000-0000-0000-0000-000000000001', + '71000000-0000-0000-0000-000000000001', + decode(repeat('a1', 20), 'hex'), decode(repeat('a2', 20), 'hex'), + decode(repeat('61', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + null, decode(repeat('a5', 32), 'hex'), + 'Reorg Token', 'RGT', 1000000, + 'f1000000-0000-0000-0000-000000000080', + 80, decode(repeat('80', 32), 'hex'), '2026-07-31T05:02:02Z' +); +select programmable_private.stage_pool_projection( + '84100000-0000-0000-0000-000000000001', + '84000000-0000-0000-0000-000000000001', + '71000000-0000-0000-0000-000000000001', + decode(repeat('00', 20), 'hex'), decode(repeat('a1', 20), 'hex'), + 3000, 60, decode(repeat('a4', 20), 'hex'), + 'f1000000-0000-0000-0000-000000000080', + 80, decode(repeat('80', 32), 'hex'), '2026-07-31T05:02:02.100Z' +); +select programmable_private.stage_pool_fee_configuration( + '84200000-0000-0000-0000-000000000001', + '84100000-0000-0000-0000-000000000001', + '71000000-0000-0000-0000-000000000001', + 100, 100, 90, 10, 0, 0, + 'f1000000-0000-0000-0000-000000000080', + 80, decode(repeat('80', 32), 'hex'), '2026-07-31T05:02:02.200Z' +); +select programmable_private.stage_launch_occurrence_role( + '84000000-0000-0000-0000-000000000001', 'launcher', + 'f1000000-0000-0000-0000-000000000080', '2026-07-31T05:02:02.300Z' +); +select programmable_private.stage_launch_projection_conditions( + '84000000-0000-0000-0000-000000000001', false, + '2026-07-31T05:02:02.400Z' +); +select programmable_private.promote_projection_run( + '81000000-0000-0000-0000-000000000001', + '82000000-0000-0000-0000-000000000001', + '83000000-0000-0000-0000-000000000001', + '71000000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 1, 2, 0, + 'd1000000-0000-0000-0000-000000000001', + 'e1000000-0000-0000-0000-000000000080', + 80, decode(repeat('80', 32), 'hex'), 7, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7 + ), + array['f1000000-0000-0000-0000-000000000080'::uuid], + array[]::uuid[], array[]::uuid[], + array['6d000000-0000-0000-0000-000000000080'::uuid], + array['explore-list']::text[], + decode(repeat('85', 32), 'hex'), '2026-07-31T05:02:03Z' +); + +select programmable_private.open_run( + '72000000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + 'a1000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('72', 32), 'hex'), + '2026-07-31T05:03:00Z' +); +select programmable_private.stage_launch_projection( + '84000000-0000-0000-0000-000000000002', + '72000000-0000-0000-0000-000000000001', + decode(repeat('a1', 20), 'hex'), decode(repeat('a2', 20), 'hex'), + decode(repeat('61', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + null, decode(repeat('a5', 32), 'hex'), + 'Reorg Token', 'RGT', 1000000, + 'f1000000-0000-0000-0000-000000000081', + 81, decode(repeat('81', 32), 'hex'), '2026-07-31T05:03:01Z' +); +select programmable_private.stage_pool_projection( + '84100000-0000-0000-0000-000000000002', + '84000000-0000-0000-0000-000000000002', + '72000000-0000-0000-0000-000000000001', + decode(repeat('00', 20), 'hex'), decode(repeat('a1', 20), 'hex'), + 3000, 60, decode(repeat('a4', 20), 'hex'), + 'f1000000-0000-0000-0000-000000000081', + 81, decode(repeat('81', 32), 'hex'), '2026-07-31T05:03:01.100Z' +); +select programmable_private.stage_pool_fee_configuration( + '84200000-0000-0000-0000-000000000002', + '84100000-0000-0000-0000-000000000002', + '72000000-0000-0000-0000-000000000001', + 100, 100, 90, 10, 0, 0, + 'f1000000-0000-0000-0000-000000000081', + 81, decode(repeat('81', 32), 'hex'), '2026-07-31T05:03:01.200Z' +); +select programmable_private.stage_launch_occurrence_role( + '84000000-0000-0000-0000-000000000002', 'launcher', + 'f1000000-0000-0000-0000-000000000081', '2026-07-31T05:03:01.300Z' +); +select programmable_private.stage_launch_projection_conditions( + '84000000-0000-0000-0000-000000000002', false, + '2026-07-31T05:03:01.400Z' +); + +reset role; + +select plan(21); + +select is( + (select count(*) from programmable_private.chain_event_identities), + 2::bigint, + 'one origin identity plus one logical identity spanning both fork placements' +); +select is( + (select count(*) from programmable_private.chain_event_occurrences), + 3::bigint, + 'the origin and both fork occurrences are retained' +); +select is( + ( + select array_agg(block_global_log_index order by block_number)::text + from programmable_private.chain_event_occurrences + ), + '{1,7,19}', + 'block-global log index is retained but is not logical identity' +); +select is( + ( + select occurrence_id + from programmable_private.chain_event_current_canonical + where logical_event_id = 'f1000000-0000-0000-0000-000000000001' + ), + 'f1000000-0000-0000-0000-000000000080'::uuid, + 'first promotion selects exactly one current placement' +); +set local role programmable_projector; +select throws_ok( + $sql$ + select programmable_private.promote_projection_run( + '81000000-0000-0000-0000-000000000002', + '82000000-0000-0000-0000-000000000002', + '83000000-0000-0000-0000-000000000002', + '72000000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 2, 3, 0, + 'd1000000-0000-0000-0000-000000000001', + 'e1000000-0000-0000-0000-000000000081', + 81, decode(repeat('81', 32), 'hex'), 19, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19 + ), + array['f1000000-0000-0000-0000-000000000081'::uuid], + array[]::uuid[], array[]::uuid[], + array['6d000000-0000-0000-0000-000000000081'::uuid], + array['explore-list']::text[], + decode(repeat('86', 32), 'hex'), '2026-07-31T05:03:02Z' + ) + $sql$, + '23505', + 'a competing placement cannot replace current state without rewind' +); +reset role; +select is( + ( + select occurrence_id + from programmable_private.chain_event_current_canonical + where logical_event_id = 'f1000000-0000-0000-0000-000000000001' + ), + 'f1000000-0000-0000-0000-000000000080'::uuid, + 'failed competing promotion leaves the canonical pointer unchanged' +); +select is( + ( + select checkpoint_generation + from programmable_private.projector_checkpoint_current + where chain_id = 1 and release_id = 'classic-v3' + ), + 2::bigint, + 'failed competing promotion cannot advance the checkpoint' +); +select is( + ( + select count(*) + from programmable_private.run_lifecycle_outcomes + where run_id = '72000000-0000-0000-0000-000000000001' + ), + 0::bigint, + 'failed promotion rolls back its terminal outcome' +); + +set local role programmable_projector; +select programmable_private.create_release_epoch( + 'a2000000-0000-0000-0000-000000000001', + 1, 'classic-v3', 'classic-v3', 'core', 2, + decode(repeat('14', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + decode(repeat('15', 32), 'hex'), + '2026-07-31T05:04:00Z' +); +select programmable_private.append_release_source_binding( + 'a2100000-0000-0000-0000-000000000001', + 'a2000000-0000-0000-0000-000000000001', + 'reorg-launcher-v2', 'launcher', 'ethereum_contract', + decode(repeat('62', 20), 'hex'), null, 70, + decode(repeat('68', 32), 'hex'), decode(repeat('11', 32), 'hex'), + decode(repeat('17', 32), 'hex'), decode(repeat('18', 32), 'hex'), + '2026-07-31T05:04:00.500Z' +); +select programmable_private.append_release_projection_event_rule( + rule_id, 'a2000000-0000-0000-0000-000000000001', projection_kind, + 'launcher', 'MemeTokenLaunchedV2', commitment, '2026-07-31T05:04:00.600Z' +) +from (values + ('a2200000-0000-0000-0000-000000000001'::uuid, 'launch', decode(repeat('06', 32), 'hex')), + ('a2200000-0000-0000-0000-000000000002'::uuid, 'pool', decode(repeat('07', 32), 'hex')), + ('a2200000-0000-0000-0000-000000000003'::uuid, 'pool_fee_configuration', decode(repeat('08', 32), 'hex')), + ('a2200000-0000-0000-0000-000000000004'::uuid, 'launch_requirement', decode(repeat('09', 32), 'hex')) +) as rule(rule_id, projection_kind, commitment); +select programmable_private.append_release_launch_requirement( + 'a2300000-0000-0000-0000-000000000001', + 'a2000000-0000-0000-0000-000000000001', 0, + 'launcher', 'MemeTokenLaunchedV2', 'always', + decode(repeat('0a', 32), 'hex'), '2026-07-31T05:04:00.700Z' +); +select programmable_private.activate_release_epoch( + 1, 'classic-v3', 'classic-v3', 'core', + 'a2000000-0000-0000-0000-000000000001', + 1, 2, decode(repeat('16', 32), 'hex'), + '2026-07-31T05:04:01Z' +); +select programmable_private.open_run( + '73000000-0000-0000-0000-000000000001', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + 'a2000000-0000-0000-0000-000000000001', 2, + 'projector-v1', decode(repeat('73', 32), 'hex'), + '2026-07-31T05:04:02Z' +); +select programmable_private.append_safe_head_observation( + 'd2000000-0000-0000-0000-000000000001', + '73000000-0000-0000-0000-000000000001', + 'b1000000-0000-0000-0000-000000000001', + 'b1000000-0000-0000-0000-000000000002', + 1, 1, 120, 120, 12, 108, + decode(repeat('08', 32), 'hex'), decode(repeat('08', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000120', 'hex'), + decode(repeat('44', 32), 'hex'), '2026-07-31T05:04:03Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e2000000-0000-0000-0000-000000000070', + 'd2000000-0000-0000-0000-000000000001', + '73000000-0000-0000-0000-000000000001', + 70, decode(repeat('70', 32), 'hex'), decode(repeat('70', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000221', 'hex'), + decode(repeat('45', 32), 'hex'), '2026-07-31T05:04:04Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e2000000-0000-0000-0000-000000000082', + 'd2000000-0000-0000-0000-000000000001', + '73000000-0000-0000-0000-000000000001', + 82, decode(repeat('82', 32), 'hex'), decode(repeat('82', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000222', 'hex'), + decode(repeat('46', 32), 'hex'), '2026-07-31T05:04:05Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), 22), + '73000000-0000-0000-0000-000000000001', + 82, + decode(repeat('82', 32), 'hex'), + decode(repeat('61', 32), 'hex'), + 4, + 22, + decode(repeat('62', 20), 'hex'), + decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('63', 32), 'hex'), decode(repeat('64', 32), 'hex')], + decode('010203', 'hex'), + '{"amount":"1"}'::jsonb, + decode(repeat('65', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), 22), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('6b', 32), 'hex'), + '2026-07-31T05:04:06Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), 22 + ), + '6c000000-0000-0000-0000-000000000001', + 82, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), + 4, 22, decode(repeat('62', 20), 'hex'), decode(repeat('63', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('63', 32), 'hex'), decode(repeat('64', 32), 'hex')], + decode('010203', 'hex'), '{"amount":"1"}'::jsonb, + decode(repeat('65', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), 22 + ), + 'b1000000-0000-0000-0000-000000000003', + decode(repeat('6b', 32), 'hex'), '2026-07-31T05:04:06.100Z', + 'canonical-events', 'ReorgLauncher' +); +select programmable_private.resolve_envio_candidate( + '6e000000-0000-0000-0000-000000000080', + '73000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('80', 32), 'hex'), decode(repeat('61', 32), 'hex'), 7 + ), + 'a2100000-0000-0000-0000-000000000001', null, + decode(repeat('68', 32), 'hex'), decode(repeat('73', 32), 'hex'), + '2026-07-31T05:04:06.200Z' +); +select programmable_private.resolve_envio_candidate( + '6e000000-0000-0000-0000-000000000081', + '73000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19 + ), + 'a2100000-0000-0000-0000-000000000001', null, + decode(repeat('68', 32), 'hex'), decode(repeat('74', 32), 'hex'), + '2026-07-31T05:04:06.300Z' +); +select programmable_private.resolve_envio_candidate( + '6e000000-0000-0000-0000-000000000082', + '73000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), 22 + ), + 'a2100000-0000-0000-0000-000000000001', null, + decode(repeat('68', 32), 'hex'), decode(repeat('75', 32), 'hex'), + '2026-07-31T05:04:06.400Z' +); +select programmable_private.append_chain_event_occurrence( + 'f1000000-0000-0000-0000-000000000001', + 'f2000000-0000-0000-0000-000000000082', + '73000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), 22), 0, '2026-07-31T04:59:24Z', 'decoder-v1', + decode(repeat('68', 32), 'hex'), + 'e2000000-0000-0000-0000-000000000082', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310020', 'hex'), + decode(repeat('6c', 32), 'hex'), '2026-07-31T05:04:07Z' +); +select programmable_private.acquire_projector_lease( + 1, 'classic-v3', 'classic-v3', 'core', 'projector-v1', + 'a2000000-0000-0000-0000-000000000001', 2, + 1, 2, decode(repeat('bb', 32), 'hex'), 'worker-b', + '2026-07-31T05:05:00Z', '2026-07-31T05:15:00Z', + decode(repeat('bc', 32), 'hex') +); +select programmable_private.open_run( + '74000000-0000-0000-0000-000000000001', + 'rewind', 1, 'classic-v3', 'classic-v3', 'core', + 'a2000000-0000-0000-0000-000000000001', 2, + 'projector-v1', decode(repeat('74', 32), 'hex'), + '2026-07-31T05:05:01Z' +); +select programmable_private.rewind_projection_run( + '82000000-0000-0000-0000-000000000002', + '83000000-0000-0000-0000-000000000003', + '74000000-0000-0000-0000-000000000001', + 'projector-v1', 2, decode(repeat('bb', 32), 'hex'), + 2, 3, 1, + 'd2000000-0000-0000-0000-000000000001', + 'e2000000-0000-0000-0000-000000000070', + 70, decode(repeat('70', 32), 'hex'), 1, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('70', 32), 'hex'), decode(repeat('60', 32), 'hex'), 1 + ), + decode(repeat('87', 32), 'hex'), '2026-07-31T05:05:02Z' +); + +reset role; + +select is( + ( + select count(*) from programmable_private.chain_event_current_canonical + where logical_event_id = 'f1000000-0000-0000-0000-000000000001' + ), + 0::bigint, + 'higher-generation rewind removes the old canonical pointer' +); +select is( + ( + select count(*) + from programmable_private.chain_event_occurrence_status_history + where occurrence_id = 'f1000000-0000-0000-0000-000000000080' + and status = 'orphaned' + ), + 1::bigint, + 'rewind appends an orphan decision without deleting the occurrence' +); +select is( + (select count(*) from programmable_private.launch_projections), + 1::bigint, + 'rewind preserves its checkpoint baseline and removes rows above the target' +); +select is( + ( + select concat_ws( + '/', current.checkpoint_generation, current.reorg_generation, + checkpoint.block_number, + checkpoint.pointer_generation + ) + from programmable_private.projector_checkpoint_current as current + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current.checkpoint_id + where current.chain_id = 1 and current.release_id = 'classic-v3' + ), + '3/1/70/2', + 'rewind advances checkpoint and reorg generations under the new pointer' +); +select is( + ( + select status::text + from programmable_private.route_eligibility_current + where route_key = 'explore-list' + ), + 'ineligible', + 'rewind revokes route eligibility before later publication' +); +set local role programmable_projector; +select throws_ok( + $sql$ + select programmable_private.promote_projection_run( + '81000000-0000-0000-0000-000000000003', + '82000000-0000-0000-0000-000000000003', + '83000000-0000-0000-0000-000000000004', + '72000000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 2, 3, 1, + 'd1000000-0000-0000-0000-000000000001', + 'e1000000-0000-0000-0000-000000000081', + 81, decode(repeat('81', 32), 'hex'), 19, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('81', 32), 'hex'), decode(repeat('61', 32), 'hex'), 19 + ), + array['f1000000-0000-0000-0000-000000000081'::uuid], + array[]::uuid[], array[]::uuid[], + array['6d000000-0000-0000-0000-000000000081'::uuid], + array['explore-list']::text[], + decode(repeat('88', 32), 'hex'), '2026-07-31T05:05:03Z' + ) + $sql$, + '40001', + 'stale pre-reorg run cannot restore its projection or checkpoint' +); + +reset role; +set local role programmable_projector; +select programmable_private.open_run( + '75000000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + 'a2000000-0000-0000-0000-000000000001', 2, + 'projector-v1', decode(repeat('75', 32), 'hex'), + '2026-07-31T05:06:00Z' +); +select programmable_private.stage_launch_projection( + '84000000-0000-0000-0000-000000000003', + '75000000-0000-0000-0000-000000000001', + decode(repeat('a1', 20), 'hex'), decode(repeat('a2', 20), 'hex'), + decode(repeat('61', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + null, decode(repeat('a5', 32), 'hex'), + 'Reorg Token', 'RGT', 1000000, + 'f2000000-0000-0000-0000-000000000082', + 82, decode(repeat('82', 32), 'hex'), '2026-07-31T05:06:01Z' +); +select programmable_private.stage_pool_projection( + '84100000-0000-0000-0000-000000000003', + '84000000-0000-0000-0000-000000000003', + '75000000-0000-0000-0000-000000000001', + decode(repeat('00', 20), 'hex'), decode(repeat('a1', 20), 'hex'), + 3000, 60, decode(repeat('a4', 20), 'hex'), + 'f2000000-0000-0000-0000-000000000082', + 82, decode(repeat('82', 32), 'hex'), '2026-07-31T05:06:01.100Z' +); +select programmable_private.stage_pool_fee_configuration( + '84200000-0000-0000-0000-000000000003', + '84100000-0000-0000-0000-000000000003', + '75000000-0000-0000-0000-000000000001', + 100, 100, 90, 10, 0, 0, + 'f2000000-0000-0000-0000-000000000082', + 82, decode(repeat('82', 32), 'hex'), '2026-07-31T05:06:01.200Z' +); +select programmable_private.stage_launch_occurrence_role( + '84000000-0000-0000-0000-000000000003', 'launcher', + 'f2000000-0000-0000-0000-000000000082', '2026-07-31T05:06:01.300Z' +); +select programmable_private.stage_launch_projection_conditions( + '84000000-0000-0000-0000-000000000003', false, + '2026-07-31T05:06:01.400Z' +); +select programmable_private.promote_projection_run( + '81000000-0000-0000-0000-000000000004', + '82000000-0000-0000-0000-000000000004', + '83000000-0000-0000-0000-000000000005', + '75000000-0000-0000-0000-000000000001', + 'projector-v1', 2, decode(repeat('bb', 32), 'hex'), + 3, 4, 1, + 'd2000000-0000-0000-0000-000000000001', + 'e2000000-0000-0000-0000-000000000082', + 82, decode(repeat('82', 32), 'hex'), 22, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('82', 32), 'hex'), decode(repeat('61', 32), 'hex'), 22 + ), + array['f2000000-0000-0000-0000-000000000082'::uuid], + array[]::uuid[], array[]::uuid[], + array[ + '6e000000-0000-0000-0000-000000000080'::uuid, + '6e000000-0000-0000-0000-000000000081'::uuid, + '6e000000-0000-0000-0000-000000000082'::uuid + ], + array['explore-list']::text[], + decode(repeat('89', 32), 'hex'), '2026-07-31T05:06:02Z' +); + +reset role; + +select is( + ( + select occurrence_id + from programmable_private.chain_event_current_canonical + where logical_event_id = 'f1000000-0000-0000-0000-000000000001' + ), + 'f2000000-0000-0000-0000-000000000082'::uuid, + 'post-rewind promotion switches canonicality to the new occurrence' +); +select is( + (select count(*) from programmable_private.chain_event_occurrences), + 4::bigint, + 'all historical fork placements survive canonical switching' +); +select is( + ( + select count(*) + from programmable_private.chain_event_occurrence_status_history + where status = 'canonical' + ), + 3::bigint, + 'canonical choices are append-only history' +); +select is( + ( + select count(*) + from programmable_private.chain_event_occurrence_status_history + where status = 'orphaned' + ), + 1::bigint, + 'orphan history is retained after replacement promotion' +); +select is( + ( + select concat_ws( + '/', current.checkpoint_generation, current.reorg_generation, + checkpoint.block_number, + checkpoint.pointer_generation + ) + from programmable_private.projector_checkpoint_current as current + join programmable_private.projector_checkpoints as checkpoint + on checkpoint.checkpoint_id = current.checkpoint_id + where current.chain_id = 1 and current.release_id = 'classic-v3' + ), + '4/1/82/2', + 'post-reorg checkpoint remains fenced by higher pointer and reorg generations' +); +select is( + ( + select status::text + from programmable_private.route_eligibility_current + where route_key = 'explore-list' + ), + 'eligible', + 'only the new fenced publication restores route eligibility' +); +select is( + ( + select concat_ws( + '/', last_source_occurrence_id, pointer_generation, promoted_block_number + ) + from programmable_private.launch_projections + where launch_projection_id = '84000000-0000-0000-0000-000000000003' + ), + 'f2000000-0000-0000-0000-000000000082/2/82', + 'visible launch data is rebuilt from the replacement occurrence' +); + +reset role; + +select * from finish(); +rollback; diff --git a/supabase/tests/database/005_reward_seed_and_projection.test.sql b/supabase/tests/database/005_reward_seed_and_projection.test.sql new file mode 100644 index 00000000..017100b7 --- /dev/null +++ b/supabase/tests/database/005_reward_seed_and_projection.test.sql @@ -0,0 +1,5377 @@ +begin; +select plan(163); + +-- Preserve behavioral coverage for the retired v1/v2 promotion bodies while +-- production keeps both capabilities revoked. pgTAP rolls these grants back. +set local role programmable_migrator; +grant execute on function programmable_private.promote_projection_run( + uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, timestamptz +) to programmable_projector; +grant execute on function programmable_private.promote_projection_run_v2( + text, uuid, uuid, uuid, uuid, text, bigint, bytea, + bigint, bigint, bigint, uuid, uuid, numeric, bytea, numeric, + text, uuid[], uuid[], uuid[], uuid[], text[], bytea, timestamptz +) to programmable_projector; +reset role; + +-- Test-only definer readers let the restricted projector replay a previously +-- stored opaque pair without granting it base-table SELECT. The transaction +-- rollback removes these helpers. +create function public.reward_test_allocation_preimage() +returns bytea +language sql +stable +security definer +set search_path = '' +as $function$ + select fact.canonical_preimage + from programmable_private.reward_allocation_facts as fact + where fact.allocation_fact_id = '98000000-0000-0000-0000-000000000001' +$function$; + +create function public.reward_test_evidence_preimage() +returns bytea +language sql +stable +security definer +set search_path = '' +as $function$ + select evidence.canonical_preimage + from programmable_private.reward_allocation_evidence as evidence + where evidence.allocation_evidence_id = + '98100000-0000-0000-0000-000000000001' +$function$; + +create function public.reward_test_evidence_recovery_binding() +returns uuid +language sql +stable +security definer +set search_path = '' +as $function$ + select evidence.recovery_release_binding_id + from programmable_private.reward_allocation_evidence as evidence + where evidence.allocation_evidence_id = + '98100000-0000-0000-0000-000000000004' +$function$; + +create function public.reward_test_dynamic_occurrence_provenance() +returns boolean +language sql +stable +security definer +set search_path = '' +as $function$ + select occurrence.release_binding_id is null + and occurrence.dynamic_source_attestation_id = + '91210000-0000-0000-0000-000000000001'::uuid + and occurrence.first_seen_envio_candidate_id is null + and occurrence.first_seen_neutral_candidate_id = + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14) + and occurrence.candidate_resolution_id = + '91220000-0000-0000-0000-000000000001'::uuid + from programmable_private.chain_event_occurrences as occurrence + where occurrence.occurrence_id = + '91240000-0000-0000-0000-000000000001'::uuid +$function$; + +create function public.reward_test_orphaned_dynamic_resolution() +returns void +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +begin + delete from programmable_private.chain_event_current_canonical + where occurrence_id = '96100000-0000-0000-0000-000000000001'::uuid; + perform programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), null, + '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('d8', 32), 'hex'), + '2026-07-31T03:03:14Z' + ); +end +$function$; + +create function public.reward_test_shared_resolution_count() +returns bigint +language sql +stable +security definer +set search_path = '' +as $function$ + select pg_catalog.count(*) + from programmable_private.envio_candidate_resolutions + where candidate_id = programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), 15) +$function$; + +create function public.reward_test_private_call(p_sql text) +returns void +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +begin + execute p_sql; +end +$function$; + +create function public.reward_test_quarantine_then_rollback( + p_sql text, + p_mismatch_evidence_id uuid +) +returns void +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +begin + execute p_sql; + if not exists ( + select 1 + from programmable_private.reward_allocation_mismatch_evidence + where mismatch_evidence_id = p_mismatch_evidence_id + ) then + raise exception using + errcode = 'P0002', + message = 'contradictory evidence was not quarantined'; + end if; + raise exception using + errcode = 'P0001', + message = 'rollback expected quarantine fixture'; +end +$function$; + +create function public.reward_test_stale_reward_balance_reorg( + p_projection_run_id uuid, + p_vault bytea +) +returns void +language plpgsql +volatile +security definer +set search_path = '' +as $function$ +begin + update programmable_private.projector_checkpoint_current + set reorg_generation = reorg_generation + 1 + where chain_id = 1 + and release_id = 'classic-v3' + and model_id = 'classic-v3' + and source_group = 'core' + and projector_version = 'projector-v1'; + if not found then + raise exception using + errcode = 'P0002', message = 'checkpoint fixture is absent'; + end if; + perform balance.account + from programmable_private.get_projector_reward_balances_by_vault_v1( + p_projection_run_id, p_vault + ) as balance; + raise exception using + errcode = 'P0001', + message = 'stale reorg binding unexpectedly returned balances'; +end +$function$; + +set local role programmable_projector; + +select programmable_private.create_release_epoch( + '91000000-0000-0000-0000-000000000001', + 1, 'classic-v3', 'classic-v3', 'core', 1, + decode(repeat('91', 32), 'hex'), + decode(repeat('a4', 32), 'hex'), + decode(repeat('92', 32), 'hex'), + '2026-07-31T03:00:00Z' +); +select programmable_private.append_release_source_binding( + '91100000-0000-0000-0000-000000000001', + '91000000-0000-0000-0000-000000000001', + 'seed-launcher', 'launcher', 'ethereum_contract', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), 25639597, + decode(repeat('51', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + decode(repeat('14', 32), 'hex'), decode(repeat('15', 32), 'hex'), + '2026-07-31T03:00:00.100Z' +); +select programmable_private.append_release_source_binding( + '91100000-0000-0000-0000-000000000002', + '91000000-0000-0000-0000-000000000001', + 'seed-vesting-factory', 'vesting_factory', 'ethereum_contract', + decode(repeat('35', 20), 'hex'), null, 25639598, + decode(repeat('53', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + decode(repeat('16', 32), 'hex'), decode(repeat('17', 32), 'hex'), + '2026-07-31T03:00:00.200Z' +); +select programmable_private.append_release_source_binding( + '91100000-0000-0000-0000-000000000003', + '91000000-0000-0000-0000-000000000001', + 'seed-hook', 'hook', 'ethereum_contract', + decode(repeat('39', 20), 'hex'), null, 25639599, + decode(repeat('55', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + decode(repeat('18', 32), 'hex'), decode(repeat('19', 32), 'hex'), + '2026-07-31T03:00:00.300Z' +); +select programmable_private.append_release_source_binding( + '91100000-0000-0000-0000-000000000004', + '91000000-0000-0000-0000-000000000001', + 'seed-vault-factory', 'vault_factory', 'ethereum_contract', + decode(repeat('3d', 20), 'hex'), null, 25639600, + decode(repeat('57', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + decode(repeat('1a', 32), 'hex'), decode(repeat('1b', 32), 'hex'), + '2026-07-31T03:00:00.400Z' +); +select programmable_private.append_release_source_binding( + '91100000-0000-0000-0000-000000000005', + '91000000-0000-0000-0000-000000000001', + 'seed-coordinator', 'coordinator', 'ethereum_contract', + decode(repeat('3e', 20), 'hex'), decode('deadbeef', 'hex'), 25639597, + decode(repeat('59', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + decode(repeat('1c', 32), 'hex'), decode(repeat('1d', 32), 'hex'), + '2026-07-31T03:00:00.500Z' +); +select programmable_private.append_release_dynamic_source_template( + '91200000-0000-0000-0000-000000000001', + '91000000-0000-0000-0000-000000000001', + '91100000-0000-0000-0000-000000000004', + 'vault_factory', 'ClassicRewardVaultDeployed', 'vault', 'reward_vault', + decode(repeat('a4', 32), 'hex'), decode(repeat('d1', 32), 'hex'), + decode(repeat('df', 32), 'hex'), + '{"factoryConfigurationField":"configurationCommitment","bindings":[{"ordinal":"0","offset":"0","length":"20","source":"deployed_address","encoding":"address"}]}'::jsonb, + decode(repeat('de', 32), 'hex'), 6543, + decode(repeat('d2', 32), 'hex'), + decode(repeat('d3', 32), 'hex'), '2026-07-31T03:00:00.600Z' +); +select programmable_private.append_release_dynamic_source_template( + '91200000-0000-0000-0000-000000000002', + '91000000-0000-0000-0000-000000000001', + '91100000-0000-0000-0000-000000000002', + 'vesting_factory', 'ClassicInitialBuyVestingWalletDeployed', + 'wallet', 'vesting_wallet', + decode(repeat('a4', 32), 'hex'), decode(repeat('c1', 32), 'hex'), + decode(repeat('cf', 32), 'hex'), + '{"factoryConfigurationField":"configurationCommitment","bindings":[{"ordinal":"0","offset":"0","length":"20","source":"deployed_address","encoding":"address"}]}'::jsonb, + decode(repeat('ce', 32), 'hex'), 1234, + decode(repeat('c2', 32), 'hex'), + decode(repeat('c3', 32), 'hex'), '2026-07-31T03:00:00.700Z' +); +select programmable_private.append_release_projection_event_rule( + rule_id, '91000000-0000-0000-0000-000000000001', projection_kind, + source_role, event_type, commitment, '2026-07-31T03:00:00.800Z' +) +from (values + ('91400000-0000-0000-0000-000000000001'::uuid, 'launch', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('01', 32), 'hex')), + ('91400000-0000-0000-0000-000000000002'::uuid, 'pool', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('02', 32), 'hex')), + ('91400000-0000-0000-0000-000000000003'::uuid, 'pool_fee_configuration', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('03', 32), 'hex')), + ('91400000-0000-0000-0000-000000000004'::uuid, 'fee_accrual', 'launcher', 'MemeTokenLaunchedV2', decode(repeat('04', 32), 'hex')), + ('91400000-0000-0000-0000-000000000005'::uuid, 'pool_fee_total', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('05', 32), 'hex')), + ('91400000-0000-0000-0000-000000000006'::uuid, 'reward_vault', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('06', 32), 'hex')), + ('91400000-0000-0000-0000-000000000007'::uuid, 'reward_allocation', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('07', 32), 'hex')), + ('91400000-0000-0000-0000-000000000008'::uuid, 'claim', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('08', 32), 'hex')), + ('91400000-0000-0000-0000-000000000009'::uuid, 'payout_change', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('09', 32), 'hex')), + ('91400000-0000-0000-0000-000000000010'::uuid, 'account_reward_balance', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('0a', 32), 'hex')), + ('91400000-0000-0000-0000-000000000011'::uuid, 'initial_buy_custody', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('0b', 32), 'hex')), + ('91400000-0000-0000-0000-000000000012'::uuid, 'initial_buy_vesting', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('0c', 32), 'hex')), + ('91400000-0000-0000-0000-000000000013'::uuid, 'launch_requirement', 'vault_factory', 'ClassicRewardVaultDeployed', decode(repeat('0d', 32), 'hex')), + ('91400000-0000-0000-0000-000000000014'::uuid, 'launch_requirement', 'vesting_factory', 'ClassicInitialBuyVestingWalletDeployed', decode(repeat('0e', 32), 'hex')), + ('91400000-0000-0000-0000-000000000015'::uuid, 'launch_requirement', 'coordinator', 'StockPairedEthTokenLaunched', decode(repeat('0f', 32), 'hex')), + ('91400000-0000-0000-0000-000000000016'::uuid, 'creator_hook_claim', 'hook', 'CreatorFeesClaimed', decode(repeat('10', 32), 'hex')), + ('91400000-0000-0000-0000-000000000017'::uuid, 'launcher_hook_claim', 'hook', 'LauncherFeesClaimed', decode(repeat('20', 32), 'hex')), + ('91400000-0000-0000-0000-000000000018'::uuid, 'creator_fee_checkpoint', 'reward_vault', 'CreatorFeesCheckpointed', decode(repeat('30', 32), 'hex')), + ('91400000-0000-0000-0000-000000000019'::uuid, 'reward_configuration_activation', 'reward_vault', 'CtoRewardConfigurationActivated', decode(repeat('40', 32), 'hex')), + ('91400000-0000-0000-0000-000000000020'::uuid, 'reward_vault', 'reward_vault', 'BeneficiaryFeesClaimed', decode(repeat('31', 32), 'hex')), + ('91400000-0000-0000-0000-000000000021'::uuid, 'claim', 'reward_vault', 'BeneficiaryFeesClaimed', decode(repeat('32', 32), 'hex')), + ('91400000-0000-0000-0000-000000000022'::uuid, 'reward_vault', 'reward_vault', 'PayoutWalletChanged', decode(repeat('33', 32), 'hex')) +) as rule(rule_id, projection_kind, source_role, event_type, commitment); +select programmable_private.append_release_launch_requirement( + requirement_id, '91000000-0000-0000-0000-000000000001', ordinal, + occurrence_role, event_type, required_when, commitment, + '2026-07-31T03:00:00.900Z' +) +from (values + ('91500000-0000-0000-0000-000000000001'::uuid, 0, 'vault_factory', 'ClassicRewardVaultDeployed', 'always', decode(repeat('11', 32), 'hex')), + ('91500000-0000-0000-0000-000000000002'::uuid, 1, 'vault_factory', 'ClassicRewardVaultDeployed', 'reward_vault', decode(repeat('12', 32), 'hex')), + ('91500000-0000-0000-0000-000000000003'::uuid, 2, 'vesting_factory', 'ClassicInitialBuyVestingWalletDeployed', 'locked_custody', decode(repeat('13', 32), 'hex')), + ('91500000-0000-0000-0000-000000000004'::uuid, 3, 'coordinator', 'StockPairedEthTokenLaunched', 'eth_funded', decode(repeat('14', 32), 'hex')) +) as requirement( + requirement_id, ordinal, occurrence_role, event_type, required_when, commitment +); +select programmable_private.activate_release_epoch( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', + 0, 1, decode(repeat('93', 32), 'hex'), + '2026-07-31T03:00:01Z' +); +select programmable_private.register_rpc_provider_deployment( + '92000000-0000-0000-0000-000000000001', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('a3', 32), 'hex'), + decode(repeat('21', 32), 'hex'), decode(repeat('22', 32), 'hex'), + decode(repeat('23', 32), 'hex'), '2026-07-31T03:00:02Z' +); +select programmable_private.register_rpc_provider_deployment( + '92000000-0000-0000-0000-000000000002', + 1, 'quicknode', 'rpc-provider-v1', + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('b3', 32), 'hex'), + decode(repeat('24', 32), 'hex'), decode(repeat('25', 32), 'hex'), + decode(repeat('26', 32), 'hex'), '2026-07-31T03:00:03Z' +); +select programmable_private.register_provider_deployment( + '92000000-0000-0000-0000-000000000003', + 'envio_deployment', 'seed-envio', + decode(repeat('27', 32), 'hex'), decode(repeat('28', 32), 'hex'), + decode(repeat('29', 32), 'hex'), '2026-07-31T03:00:04Z' +); +select programmable_private.open_run( + '910c0000-0000-0000-0000-000000000001', + 'ingestion', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('30', 32), 'hex'), + '2026-07-31T03:00:05Z' +); +select programmable_private.open_run( + '93000000-0000-0000-0000-000000000001', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('31', 32), 'hex'), + '2026-07-31T03:01:00Z' +); +select programmable_private.append_safe_head_observation( + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000140', 'hex'), + decode(repeat('41', 32), 'hex'), '2026-07-31T03:01:01Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '95000000-0000-0000-0000-000000000597', + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 25639597, decode(repeat('a6', 32), 'hex'), decode(repeat('a6', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000241', 'hex'), + decode(repeat('42', 32), 'hex'), '2026-07-31T03:01:02Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '95000000-0000-0000-0000-000000000598', + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 25639598, decode(repeat('a8', 32), 'hex'), decode(repeat('a8', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000242', 'hex'), + decode(repeat('43', 32), 'hex'), '2026-07-31T03:01:03Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '95000000-0000-0000-0000-000000000599', + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 25639599, decode(repeat('aa', 32), 'hex'), decode(repeat('aa', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000243', 'hex'), + decode(repeat('44', 32), 'hex'), '2026-07-31T03:01:04Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '95000000-0000-0000-0000-000000000600', + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), decode(repeat('99', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000244', 'hex'), + decode(repeat('45', 32), 'hex'), '2026-07-31T03:01:05Z' +); + +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('a6', 32), 'hex'), decode(repeat('a5', 32), 'hex'), 10), + '93000000-0000-0000-0000-000000000001', + 25639597, + decode(repeat('a6', 32), 'hex'), + decode(repeat('a5', 32), 'hex'), + 1, + 10, + decode(repeat('31', 20), 'hex'), + decode(repeat('32', 32), 'hex'), + 'MemeTokenLaunchedV2', + array[decode(repeat('32', 32), 'hex')], + decode('', 'hex'), + '{"token":"0x7171717171717171717171717171717171717171","poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","hook":"0x3939393939393939393939393939393939393939","quoteAsset":"0x0000000000000000000000000000000000000000"}'::jsonb, + decode(repeat('33', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('a6', 32), 'hex'), decode(repeat('a5', 32), 'hex'), 10), + '92000000-0000-0000-0000-000000000003', + decode(repeat('34', 32), 'hex'), + '2026-07-31T03:01:06Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('a8', 32), 'hex'), decode(repeat('a7', 32), 'hex'), 11), + '93000000-0000-0000-0000-000000000001', + 25639598, + decode(repeat('a8', 32), 'hex'), + decode(repeat('a7', 32), 'hex'), + 2, + 11, + decode(repeat('35', 20), 'hex'), + decode(repeat('36', 32), 'hex'), + 'ClassicInitialBuyVestingWalletDeployed', + array[decode(repeat('36', 32), 'hex')], + decode('', 'hex'), + '{"wallet":"0x7676767676767676767676767676767676767676","configurationCommitment":"0xf6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6"}'::jsonb, + decode(repeat('37', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('a8', 32), 'hex'), decode(repeat('a7', 32), 'hex'), 11), + '92000000-0000-0000-0000-000000000003', + decode(repeat('38', 32), 'hex'), + '2026-07-31T03:01:07Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('a9', 32), 'hex'), 12), + '93000000-0000-0000-0000-000000000001', + 25639599, + decode(repeat('aa', 32), 'hex'), + decode(repeat('a9', 32), 'hex'), + 3, + 12, + decode(repeat('39', 20), 'hex'), + decode(repeat('3a', 32), 'hex'), + 'PoolRegistered', + array[decode(repeat('3a', 32), 'hex')], + decode('', 'hex'), + '{"poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","token":"0x7171717171717171717171717171717171717171","hook":"0x3939393939393939393939393939393939393939","currency0":"0x0000000000000000000000000000000000000000","currency1":"0x7171717171717171717171717171717171717171","positionRecipient":"0x7272727272727272727272727272727272727272","positionTokenId":"1","tokenLiquidityAmount":"999999999999999999999999","lockedTokenDust":"1","sqrtPriceX96":"79228162514264337593543950336","tick":"0","tickLower":"-887220","tickUpper":"887220"}'::jsonb, + decode(repeat('3b', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('a9', 32), 'hex'), 12), + '92000000-0000-0000-0000-000000000003', + decode(repeat('3c', 32), 'hex'), + '2026-07-31T03:01:08Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), 13), + '93000000-0000-0000-0000-000000000001', + 25639600, + decode(repeat('99', 32), 'hex'), + decode(repeat('88', 32), 'hex'), + 4294967295, + 13, + decode(repeat('3d', 20), 'hex'), + decode(repeat('3e', 32), 'hex'), + 'ClassicRewardVaultDeployed', + array[decode(repeat('3e', 32), 'hex')], + decode('010203', 'hex'), + '{"vault":"0x7777777777777777777777777777777777777777","configurationCommitment":"0xf7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7"}'::jsonb, + decode(repeat('3f', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), 13), + '92000000-0000-0000-0000-000000000003', + decode(repeat('40', 32), 'hex'), + '2026-07-31T03:01:09Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20), + '93000000-0000-0000-0000-000000000001', + 25639600, + decode(repeat('99', 32), 'hex'), + decode(repeat('85', 32), 'hex'), + 11, + 20, + decode(repeat('3d', 20), 'hex'), + decode(repeat('3e', 32), 'hex'), + 'ClassicRewardVaultDeployed', + array[decode(repeat('3e', 32), 'hex')], + decode('010a', 'hex'), + '{"vault":"0x7878787878787878787878787878787878787878","configurationCommitment":"0xf8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8"}'::jsonb, + decode(repeat('41', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20), + '92000000-0000-0000-0000-000000000003', + decode(repeat('42', 32), 'hex'), + '2026-07-31T03:01:09.050Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + '910c0000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), + 11, 20, decode(repeat('3d', 20), 'hex'), decode(repeat('3e', 32), 'hex'), + 'ClassicRewardVaultDeployed', array[decode(repeat('3e', 32), 'hex')], + decode('010a', 'hex'), + '{"vault":"0x7878787878787878787878787878787878787878","configurationCommitment":"0xf8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8"}'::jsonb, + decode(repeat('41', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('42', 32), 'hex'), '2026-07-31T03:01:09.060Z', + 'canonical-events', 'ClassicVaultFactory' +); +select programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000020', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + '91100000-0000-0000-0000-000000000004', null, + decode(repeat('57', 32), 'hex'), decode(repeat('dd', 32), 'hex'), + '2026-07-31T03:01:09.070Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), 13 + ), + '910c0000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 4294967295, 13, decode(repeat('3d', 20), 'hex'), + decode(repeat('3e', 32), 'hex'), 'ClassicRewardVaultDeployed', + array[decode(repeat('3e', 32), 'hex')], decode('010203', 'hex'), + '{"vault":"0x7777777777777777777777777777777777777777","configurationCommitment":"0xf7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7"}'::jsonb, + decode(repeat('3f', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), 13 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('40', 32), 'hex'), '2026-07-31T03:01:09.080Z', + 'canonical-events', 'ClassicVaultFactory' +); +select programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000011', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), 13 + ), + '91100000-0000-0000-0000-000000000004', null, + decode(repeat('57', 32), 'hex'), decode(repeat('de', 32), 'hex'), + '2026-07-31T03:01:09.090Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), 15 + ), + '910c0000-0000-0000-0000-000000000001', + 25639599, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), + 6, 15, decode(repeat('39', 20), 'hex'), decode(repeat('3a', 32), 'hex'), + 'PoolRegistered', array[decode(repeat('3a', 32), 'hex')], + decode('0105', 'hex'), '{"releaseVersion":"unresolved"}'::jsonb, + decode(repeat('da', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), 15 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('db', 32), 'hex'), '2026-07-31T03:01:09.095Z', + 'canonical-events', 'ClassicHook' +); +select programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000010', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), 15 + ), + '91100000-0000-0000-0000-000000000003', null, + decode(repeat('55', 32), 'hex'), decode(repeat('dc', 32), 'hex'), + '2026-07-31T03:01:09.097Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('b1', 32), 'hex'), 16), + '93000000-0000-0000-0000-000000000001', + 25639599, + decode(repeat('aa', 32), 'hex'), + decode(repeat('b1', 32), 'hex'), + 7, + 16, + decode(repeat('39', 20), 'hex'), + decode(repeat('f1', 32), 'hex'), + 'CreatorFeesClaimed', + array[decode(repeat('f1', 32), 'hex')], + decode('0106', 'hex'), + '{"poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","rewardVault":"0x7777777777777777777777777777777777777777","caller":"0x7272727272727272727272727272727272727272","amount":"10"}'::jsonb, + decode(repeat('f3', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('b1', 32), 'hex'), 16), + '92000000-0000-0000-0000-000000000003', + decode(repeat('f5', 32), 'hex'), + '2026-07-31T03:01:09.100Z' +); +select programmable_private.append_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('b2', 32), 'hex'), 17), + '93000000-0000-0000-0000-000000000001', + 25639599, + decode(repeat('aa', 32), 'hex'), + decode(repeat('b2', 32), 'hex'), + 8, + 17, + decode(repeat('39', 20), 'hex'), + decode(repeat('f2', 32), 'hex'), + 'LauncherFeesClaimed', + array[decode(repeat('f2', 32), 'hex')], + decode('0107', 'hex'), + '{"treasury":"0x3131313131313131313131313131313131313131","recipient":"0x3232323232323232323232323232323232323232","caller":"0x3333333333333333333333333333333333333333","amount":"20"}'::jsonb, + decode(repeat('f4', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('b2', 32), 'hex'), 17), + '92000000-0000-0000-0000-000000000003', + decode(repeat('f6', 32), 'hex'), + '2026-07-31T03:01:09.200Z' +); + +select programmable_private.append_chain_event_occurrence( + '96000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('a6', 32), 'hex'), decode(repeat('a5', 32), 'hex'), 10), 1, '2026-07-31T02:58:00Z', 'decoder-v1', + decode(repeat('51', 32), 'hex'), + '95000000-0000-0000-0000-000000000597', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310051', 'hex'), + decode(repeat('52', 32), 'hex'), '2026-07-31T03:01:10Z' +); +select programmable_private.append_chain_event_occurrence( + '96000000-0000-0000-0000-000000000003', + '96100000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('a8', 32), 'hex'), decode(repeat('a7', 32), 'hex'), 11), 1, '2026-07-31T02:58:12Z', 'decoder-v1', + decode(repeat('53', 32), 'hex'), + '95000000-0000-0000-0000-000000000598', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310052', 'hex'), + decode(repeat('54', 32), 'hex'), '2026-07-31T03:01:11Z' +); +select programmable_private.append_chain_event_occurrence( + '96000000-0000-0000-0000-000000000004', + '96100000-0000-0000-0000-000000000004', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('a9', 32), 'hex'), 12), 1, '2026-07-31T02:58:24Z', 'decoder-v1', + decode(repeat('55', 32), 'hex'), + '95000000-0000-0000-0000-000000000599', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310053', 'hex'), + decode(repeat('56', 32), 'hex'), '2026-07-31T03:01:12Z' +); +select programmable_private.append_chain_event_occurrence( + '96000000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), 13), 4294967295, '2026-07-31T02:58:36Z', 'decoder-v1', + decode(repeat('57', 32), 'hex'), + '95000000-0000-0000-0000-000000000600', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310054', 'hex'), + decode(repeat('58', 32), 'hex'), '2026-07-31T03:01:13Z' +); +select programmable_private.append_chain_event_occurrence( + '96000000-0000-0000-0000-000000000007', + '96100000-0000-0000-0000-000000000007', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20), 2, '2026-07-31T02:58:36.100Z', + 'decoder-v1', decode(repeat('57', 32), 'hex'), + '95000000-0000-0000-0000-000000000600', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310074', 'hex'), + decode(repeat('59', 32), 'hex'), '2026-07-31T03:01:13.050Z' +); +select programmable_private.append_chain_event_occurrence( + '96000000-0000-0000-0000-000000000005', + '96100000-0000-0000-0000-000000000005', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('b1', 32), 'hex'), 16), 1, '2026-07-31T02:58:25Z', 'decoder-v1', + decode(repeat('55', 32), 'hex'), + '95000000-0000-0000-0000-000000000599', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310056', 'hex'), + decode(repeat('f7', 32), 'hex'), '2026-07-31T03:01:13.100Z' +); +select programmable_private.append_chain_event_occurrence( + '96000000-0000-0000-0000-000000000006', + '96100000-0000-0000-0000-000000000006', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('b2', 32), 'hex'), 17), 1, '2026-07-31T02:58:26Z', 'decoder-v1', + decode(repeat('55', 32), 'hex'), + '95000000-0000-0000-0000-000000000599', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310057', 'hex'), + decode(repeat('f8', 32), 'hex'), '2026-07-31T03:01:13.200Z' +); + +select programmable_private.acquire_projector_lease( + 1, 'classic-v3', 'classic-v3', 'core', 'projector-v1', + '91000000-0000-0000-0000-000000000001', 1, + 0, 1, decode(repeat('aa', 32), 'hex'), 'seed-worker', + '2026-07-31T03:02:00Z', '2026-07-31T03:12:00Z', + decode(repeat('ab', 32), 'hex') +); +select programmable_private.open_run( + '97000000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('61', 32), 'hex'), + '2026-07-31T03:02:01Z' +); +select programmable_private.stage_launch_projection( + '97100000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000001', + decode(repeat('71', 20), 'hex'), decode(repeat('72', 20), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('73', 32), 'hex'), + null, decode(repeat('74', 32), 'hex'), + 'Seed Token', 'SEED', 1000000000000000000000000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:02:02Z' +); +select programmable_private.stage_pool_projection( + '97110000-0000-0000-0000-000000000001', + '97100000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000001', + decode(repeat('00', 20), 'hex'), decode(repeat('71', 20), 'hex'), + 3000, 60, decode(repeat('39', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:02:02.100Z' +); +select programmable_private.stage_pool_fee_configuration( + '97120000-0000-0000-0000-000000000001', + '97110000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000001', + 30, 40, 20, 10, 0, 3000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:02:02.200Z' +); +select programmable_private.stage_launch_occurrence_role( + '97100000-0000-0000-0000-000000000001', 'vault_factory', + '96100000-0000-0000-0000-000000000001', '2026-07-31T03:02:02.300Z' +); +select programmable_private.stage_launch_projection_conditions( + '97100000-0000-0000-0000-000000000001', false, + '2026-07-31T03:02:02.400Z' +); +select throws_ok( + $$ + select programmable_private.stage_launch_position_liquidity_v1( + '97105000-0000-0000-0000-000000000099', + '97100000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000001', + decode(repeat('72', 20), 'hex'), 1, + 999999999999999999999999, 1, + 79228162514264337593543950336, + 0, 0, 887220, + '96100000-0000-0000-0000-000000000004', + decode(repeat('6e', 32), 'hex'), + '2026-07-31T03:02:02.440Z' + ) + $$, + '23514', + 'Classic boundary exception never permits initial_tick = tick_lower' +); +select programmable_private.stage_launch_position_liquidity_v1( + '97105000-0000-0000-0000-000000000001', + '97100000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000001', + decode(repeat('72', 20), 'hex'), 1, + 999999999999999999999999, 1, + 79228162514264337593543950336, + 0, -887220, 0, + '96100000-0000-0000-0000-000000000004', + decode(repeat('6f', 32), 'hex'), + '2026-07-31T03:02:02.450Z' +); +select programmable_private.promote_projection_run( + '97200000-0000-0000-0000-000000000001', + '97300000-0000-0000-0000-000000000001', + '97400000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 0, 1, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000600', + 25639600, decode(repeat('99', 32), 'hex'), + 13, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('88', 32), 'hex'), 13 + ), + array[ + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000003'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid, + '96100000-0000-0000-0000-000000000005'::uuid, + '96100000-0000-0000-0000-000000000006'::uuid, + '96100000-0000-0000-0000-000000000007'::uuid + ], + array[]::uuid[], array[]::uuid[], + array[ + '91220000-0000-0000-0000-000000000010'::uuid, + '91220000-0000-0000-0000-000000000011'::uuid + ], + array['explore-list']::text[], + decode(repeat('75', 32), 'hex'), '2026-07-31T03:02:03Z' +); +select programmable_private.append_dual_rpc_runtime_code_evidence( + '91205000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), + '95000000-0000-0000-0000-000000000600', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('d1', 32), 'hex'), decode(repeat('d1', 32), 'hex'), + decode(repeat('01', 6543), 'hex'), decode(repeat('01', 6543), 'hex'), + 6543, 6543, + decode(repeat('d1', 32), 'hex'), decode(repeat('d1', 32), 'hex'), + decode(repeat('df', 32), 'hex'), + array[decode(repeat('77', 20), 'hex')], + decode(repeat('d9', 32), 'hex'), + decode(repeat('01', 6543), 'hex'), + decode(repeat('d1', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000361', 'hex'), + decode(repeat('da', 32), 'hex'), + decode(repeat('d0', 32), 'hex'), '2026-07-31T03:02:03.050Z' +); +select programmable_private.append_dual_rpc_runtime_code_evidence( + '91205000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + decode(repeat('76', 20), 'hex'), + '95000000-0000-0000-0000-000000000598', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('c1', 32), 'hex'), decode(repeat('c1', 32), 'hex'), + decode(repeat('02', 1234), 'hex'), decode(repeat('02', 1234), 'hex'), + 1234, 1234, + decode(repeat('c1', 32), 'hex'), decode(repeat('c1', 32), 'hex'), + decode(repeat('cf', 32), 'hex'), + array[decode(repeat('76', 20), 'hex')], + decode(repeat('c9', 32), 'hex'), + decode(repeat('02', 1234), 'hex'), + decode(repeat('c1', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000362', 'hex'), + decode(repeat('ca', 32), 'hex'), + decode(repeat('c0', 32), 'hex'), '2026-07-31T03:02:03.060Z' +); +select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000003', + decode(repeat('76', 20), 'hex'), 25639598, + '91205000-0000-0000-0000-000000000002', + decode(repeat('a4', 32), 'hex'), + decode(repeat('c9', 32), 'hex'), + decode(repeat('f6', 32), 'hex'), + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + decode(repeat('c1', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000471', 'hex'), + decode(repeat('cb', 32), 'hex'), + decode(repeat('c4', 32), 'hex'), '2026-07-31T03:02:03.070Z' +); +select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000472', 'hex'), + decode(repeat('db', 32), 'hex'), + decode(repeat('d4', 32), 'hex'), '2026-07-31T03:02:03.100Z' +); +select programmable_private.bind_dynamic_source_release_asset_v1( + '91215000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + '91210000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000004', + decode(repeat('73', 32), 'hex'), + decode(repeat('71', 20), 'hex'), + decode(repeat('39', 20), 'hex'), + decode(repeat('00', 20), 'hex'), + decode(repeat('d8', 32), 'hex'), + '2026-07-31T03:02:03.105Z' +); +select programmable_private.append_dual_rpc_runtime_code_evidence( + '91205000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + decode(repeat('78', 20), 'hex'), + '95000000-0000-0000-0000-000000000600', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('d5', 32), 'hex'), decode(repeat('d5', 32), 'hex'), + decode(repeat('03', 6543), 'hex'), decode(repeat('03', 6543), 'hex'), + 6543, 6543, + decode(repeat('d1', 32), 'hex'), decode(repeat('d1', 32), 'hex'), + decode(repeat('df', 32), 'hex'), + array[decode(repeat('78', 20), 'hex')], + decode(repeat('da', 32), 'hex'), + decode(repeat('03', 6543), 'hex'), + decode(repeat('d5', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000363', 'hex'), + decode(repeat('dc', 32), 'hex'), + decode(repeat('d6', 32), 'hex'), '2026-07-31T03:02:03.110Z' +); +select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000007', + decode(repeat('78', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000003', + decode(repeat('a4', 32), 'hex'), + decode(repeat('da', 32), 'hex'), + decode(repeat('f8', 32), 'hex'), + decode(repeat('b5', 32), 'hex'), decode(repeat('b6', 32), 'hex'), + decode(repeat('d5', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000473', 'hex'), + decode(repeat('dd', 32), 'hex'), + decode(repeat('d7', 32), 'hex'), '2026-07-31T03:02:03.120Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), + '910c0000-0000-0000-0000-000000000001', + 25639600, + decode(repeat('99', 32), 'hex'), + decode(repeat('87', 32), 'hex'), + 5, + 14, + decode(repeat('77', 20), 'hex'), + decode(repeat('d5', 32), 'hex'), + 'BeneficiaryFeesClaimed', + array[decode(repeat('d5', 32), 'hex')], + decode('0104', 'hex'), + '{"releaseVersion":"unresolved"}'::jsonb, + decode(repeat('d6', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), + '92000000-0000-0000-0000-000000000003', + decode(repeat('d7', 32), 'hex'), + '2026-07-31T03:02:03.200Z' +); +select programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), null, + '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('d8', 32), 'hex'), + '2026-07-31T03:02:03.300Z' +); +select programmable_private.append_chain_event_occurrence( + '91230000-0000-0000-0000-000000000001', + '91240000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), + '91220000-0000-0000-0000-000000000001', + 3, '2026-07-31T02:58:37Z', 'decoder-v1', + decode(repeat('d2', 32), 'hex'), + '95000000-0000-0000-0000-000000000600', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310055', 'hex'), + decode(repeat('d9', 32), 'hex'), '2026-07-31T03:02:03.400Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b3', 32), 'hex'), 18), + '910c0000-0000-0000-0000-000000000001', + 25639600, + decode(repeat('99', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), + 9, + 18, + decode(repeat('77', 20), 'hex'), + decode(repeat('e1', 32), 'hex'), + 'CreatorFeesCheckpointed', + array[decode(repeat('e1', 32), 'hex')], + decode('0108', 'hex'), + '{"poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","configurationEpoch":"1","amount":"100","totalCreatorFeesReceived":"1000"}'::jsonb, + decode(repeat('e3', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b3', 32), 'hex'), 18), + '92000000-0000-0000-0000-000000000003', + decode(repeat('ed', 32), 'hex'), + '2026-07-31T03:02:03.410Z' +); +select programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b3', 32), 'hex'), 18), null, + '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('ea', 32), 'hex'), + '2026-07-31T03:02:03.420Z' +); +select programmable_private.append_chain_event_occurrence( + '91230000-0000-0000-0000-000000000002', + '91240000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b3', 32), 'hex'), 18), + '91220000-0000-0000-0000-000000000002', + 1, '2026-07-31T02:58:38Z', 'decoder-v1', + decode(repeat('d2', 32), 'hex'), + '95000000-0000-0000-0000-000000000600', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310058', 'hex'), + decode(repeat('ec', 32), 'hex'), '2026-07-31T03:02:03.430Z' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b4', 32), 'hex'), 19), + '910c0000-0000-0000-0000-000000000001', + 25639600, + decode(repeat('99', 32), 'hex'), + decode(repeat('b4', 32), 'hex'), + 10, + 19, + decode(repeat('77', 20), 'hex'), + decode(repeat('e2', 32), 'hex'), + 'CtoRewardConfigurationActivated', + array[decode(repeat('e2', 32), 'hex')], + decode('0109', 'hex'), + '{"poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","approvalReference":"0xafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafaf","configurationEpoch":"2","previousConfigurationHash":"0xa2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2","newConfigurationHash":"0xa3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3","beneficiaries":["0x1111111111111111111111111111111111111111","0x2222222222222222222222222222222222222222"],"sharesBps":[6000,4000],"effectiveTotalCreatorFeesReceived":"1000"}'::jsonb, + decode(repeat('e4', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b4', 32), 'hex'), 19), + '92000000-0000-0000-0000-000000000003', + decode(repeat('ee', 32), 'hex'), + '2026-07-31T03:02:03.440Z' +); +select programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b4', 32), 'hex'), 19), null, + '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('eb', 32), 'hex'), + '2026-07-31T03:02:03.450Z' +); +select programmable_private.append_chain_event_occurrence( + '91230000-0000-0000-0000-000000000003', + '91240000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('b4', 32), 'hex'), 19), + '91220000-0000-0000-0000-000000000003', + 1, '2026-07-31T02:58:39Z', 'decoder-v1', + decode(repeat('d2', 32), 'hex'), + '95000000-0000-0000-0000-000000000600', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310059', 'hex'), + decode(repeat('ef', 32), 'hex'), '2026-07-31T03:02:03.460Z' +); + +select programmable_private.create_release_epoch( + '91300000-0000-0000-0000-000000000001', + 1, 'stock-paired-v3', 'stock-paired-v3', 'core', 1, + decode(repeat('e0', 32), 'hex'), decode(repeat('e1', 32), 'hex'), + decode(repeat('e2', 32), 'hex'), '2026-07-31T03:02:03.700Z' +); +select programmable_private.append_release_source_binding( + '91310000-0000-0000-0000-000000000001', + '91300000-0000-0000-0000-000000000001', + 'shared-hook', 'hook', 'ethereum_contract', + decode(repeat('39', 20), 'hex'), null, 25639599, + decode(repeat('55', 32), 'hex'), decode(repeat('e1', 32), 'hex'), + decode(repeat('e3', 32), 'hex'), decode(repeat('e4', 32), 'hex'), + '2026-07-31T03:02:03.800Z' +); +select programmable_private.append_release_projection_event_rule( + '91310000-0000-0000-0000-000000000002', + '91300000-0000-0000-0000-000000000001', + 'pool', 'hook', 'PoolRegistered', decode(repeat('e6', 32), 'hex'), + '2026-07-31T03:02:03.850Z' +); +select programmable_private.activate_release_epoch( + 1, 'stock-paired-v3', 'stock-paired-v3', 'core', + '91300000-0000-0000-0000-000000000001', + 0, 1, decode(repeat('e5', 32), 'hex'), '2026-07-31T03:02:03.900Z' +); +select programmable_private.open_run( + '91320000-0000-0000-0000-000000000001', + 'ingestion', 1, 'stock-paired-v3', 'stock-paired-v3', 'core', + '91300000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('e6', 32), 'hex'), + '2026-07-31T03:02:04Z' +); +select programmable_private.resolve_envio_candidate( + '91330000-0000-0000-0000-000000000001', + '91320000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), 15), + '91310000-0000-0000-0000-000000000001', null, + decode(repeat('55', 32), 'hex'), decode(repeat('e7', 32), 'hex'), + '2026-07-31T03:02:04.100Z' +); +select programmable_private.append_safe_head_observation( + '91340000-0000-0000-0000-000000000001', + '91320000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000170', 'hex'), + decode(repeat('e8', 32), 'hex'), '2026-07-31T03:02:04.200Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '91350000-0000-0000-0000-000000000001', + '91340000-0000-0000-0000-000000000001', + '91320000-0000-0000-0000-000000000001', + 25639599, decode(repeat('aa', 32), 'hex'), decode(repeat('aa', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000271', 'hex'), + decode(repeat('e9', 32), 'hex'), '2026-07-31T03:02:04.300Z' +); +select programmable_private.append_chain_event_occurrence( + '91360000-0000-0000-0000-000000000001', + '91370000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), 15), + '91220000-0000-0000-0000-000000000010', + 4, '2026-07-31T02:58:35Z', 'decoder-v1', + decode(repeat('55', 32), 'hex'), + '95000000-0000-0000-0000-000000000599', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310072', 'hex'), + decode(repeat('ea', 32), 'hex'), '2026-07-31T03:02:04.400Z' +); +select programmable_private.append_chain_event_occurrence( + '91360000-0000-0000-0000-000000000001', + '91370000-0000-0000-0000-000000000001', + '91320000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('aa', 32), 'hex'), decode(repeat('86', 32), 'hex'), 15), + '91330000-0000-0000-0000-000000000001', + 4, '2026-07-31T02:58:35Z', 'decoder-v2', + decode(repeat('55', 32), 'hex'), + '91350000-0000-0000-0000-000000000001', + 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310073', 'hex'), + decode(repeat('eb', 32), 'hex'), '2026-07-31T03:02:04.500Z' +); + +select programmable_private.open_run( + '97000000-0000-0000-0000-000000000002', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('62', 32), 'hex'), + '2026-07-31T03:03:00Z' +); +select programmable_private.append_creator_hook_claim_fact( + '91600000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000005', + decode(repeat('73', 32), 'hex'), decode(repeat('77', 20), 'hex'), + null, null, null, decode(repeat('72', 20), 'hex'), 10, + '2026-07-31T03:03:00.100Z' +); +select programmable_private.append_launcher_hook_claim_fact( + '91600000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000006', + decode(repeat('31', 20), 'hex'), decode(repeat('32', 20), 'hex'), + null, decode(repeat('33', 20), 'hex'), 20, + '2026-07-31T03:03:00.200Z' +); +select programmable_private.stage_launch_projection( + '97100000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + decode(repeat('71', 20), 'hex'), decode(repeat('72', 20), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('73', 32), 'hex'), + decode(repeat('77', 20), 'hex'), decode(repeat('74', 32), 'hex'), + 'Seed Token', 'SEED', 1000000000000000000000000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:01Z' +); +select programmable_private.stage_pool_projection( + '97110000-0000-0000-0000-000000000002', + '97100000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + decode(repeat('00', 20), 'hex'), decode(repeat('71', 20), 'hex'), + 3000, 60, decode(repeat('39', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:01.100Z' +); +select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[ + decode(repeat('11', 20), 'hex'), + decode(repeat('22', 20), 'hex') + ], + array[6000::numeric, 4000::numeric], + decode(repeat('a1', 32), 'hex'), + decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), + decode(repeat('a4', 32), 'hex'), + array[ + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['launcher', 'vault_factory', 'hook']::text[], + 1::smallint, + decode('70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000000000000010000000a636c61737369632d76330000000a636c61737369632d7633777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000299999999999999999999999999999999999999999999999999999999999999990000000001873ab00000000400000002111111111111111111111111111111111111111122222222222222222222222222222222222222220000000217700fa0a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a201a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a400000003a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a500000001a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6000000086c61756e63686572a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a700000001a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a800000007666163746f7279a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a900000001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000004686f6f6b', 'hex'), + decode('760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0', 'hex'), + '2026-07-31T03:03:02Z' +); +select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'seed-verifier-v1.0.0', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('a2', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3001b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a477777777777777777777777777777777777777770000000b756e617661696c61626c6500000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b80000000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656', 'hex'), + decode('db14c9fa42eaedfcf77221edc2861e7f2c0251997313951d7a06c65ca73beea3', 'hex'), + '2026-07-31T03:03:03Z', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex') +); +select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000002', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'seed-verifier-v1.0.1', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'matched', + decode(repeat('99', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + decode(repeat('b6', 32), 'hex'), decode(repeat('b6', 32), 'hex'), + decode(repeat('77', 20), 'hex'), decode(repeat('77', 20), 'hex'), + decode(repeat('a2', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a763100760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0000000116c61756e636865725f63616c6c6461746100000014736565642d76657269666965722d76312e302e3101b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b201bf38840601b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a47777777777777777777777777777777777777777000000076d61746368656401999999999999999999999999999999999999999999999999999999999999999901b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b501b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b501b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b601b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6017777777777777777777777777777777777777777017777777777777777777777777777777777777777b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b701b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b801b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b80100000008636f6d706c65746500000003525252525252525252525252525252525252525252525252525252525252525254545454545454545454545454545454545454545454545454545454545454545656565656565656565656565656565656565656565656565656565656565656', 'hex'), + decode('4e99ae66bbff8c7dba24b69f313d30db8c16a059660001195c00df02b9db2c67', 'hex'), + '2026-07-31T03:03:04Z', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex') +); +select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000003', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'historical_getters', 'seed-verifier-v1.0.2', + null, null, null, decode(repeat('c0', 32), 'hex'), + decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'matched', + decode(repeat('99', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + decode(repeat('c6', 32), 'hex'), decode(repeat('c6', 32), 'hex'), + decode(repeat('77', 20), 'hex'), decode(repeat('77', 20), 'hex'), + decode(repeat('a2', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + null, null, + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310060', 'hex'), + decode(repeat('c8', 32), 'hex'), '2026-07-31T03:03:05Z', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex') +); +select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000004', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'coordinator_calldata', 'seed-verifier-v1.0.3', + decode(repeat('3e', 20), 'hex'), decode('deadbeef', 'hex'), + decode(repeat('b9', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, null, null, + decode(repeat('a2', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310061', 'hex'), + decode(repeat('c9', 32), 'hex'), '2026-07-31T03:03:05.100Z', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex') +); +select programmable_private.stage_account_reward_balance( + '98200000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('11', 20), 'hex'), decode(repeat('77', 20), 'hex'), + 593, 0, '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06Z' +); +select programmable_private.stage_account_reward_balance( + '98200000-0000-0000-0000-000000000003', + '97000000-0000-0000-0000-000000000002', + decode(repeat('22', 20), 'hex'), decode(repeat('77', 20), 'hex'), + 395, 0, '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.010Z' +); +select programmable_private.stage_account_reward_balance( + '98200000-0000-0000-0000-000000000004', + '97000000-0000-0000-0000-000000000002', + decode(repeat('33', 20), 'hex'), decode(repeat('77', 20), 'hex'), + 7, 5, '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.020Z' +); +select programmable_private.stage_reward_vault_projection( + '98210000-0000-0000-0000-000000000001', + '97100000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), decode(repeat('73', 32), 'hex'), + null, decode(repeat('a2', 32), 'hex'), + '98000000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.100Z' +); +select programmable_private.stage_reward_allocation_projection( + '98220000-0000-0000-0000-000000000001', + '98210000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + '98000000-0000-0000-0000-000000000001', + 1, 0, decode(repeat('11', 20), 'hex'), decode(repeat('11', 20), 'hex'), + 6000, 25639600, null, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.200Z' +); +select programmable_private.stage_reward_allocation_projection( + '98220000-0000-0000-0000-000000000002', + '98210000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + '98000000-0000-0000-0000-000000000001', + 1, 1, decode(repeat('22', 20), 'hex'), decode(repeat('22', 20), 'hex'), + 4000, 25639600, null, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.300Z' +); + +select programmable_private.stage_pool_fee_configuration( + '98230000-0000-0000-0000-000000000001', + '97110000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + 30, 40, 200, 100, 0, 3000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.400Z' +); +select programmable_private.stage_fee_accrual_fact( + '98240000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('73', 32), 'hex'), null, 1000, 200, 100, + '96100000-0000-0000-0000-000000000002', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.500Z' +); +select programmable_private.stage_pool_fee_total( + '98250000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('73', 32), 'hex'), null, 1000, 200, 100, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.600Z' +); +select programmable_private.stage_claim_projection( + '98260000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), 'creator', + decode(repeat('72', 20), 'hex'), decode(repeat('72', 20), 'hex'), + 10, 10, 100, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.700Z' +); +select programmable_private.stage_payout_change_projection( + '98270000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), decode(repeat('33', 20), 'hex'), + decode(repeat('33', 20), 'hex'), decode(repeat('34', 20), 'hex'), 1, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.800Z' +); +select programmable_private.stage_initial_buy_custody_projection( + '98280000-0000-0000-0000-000000000001', + '97100000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + decode(repeat('13', 20), 'hex'), 1::smallint, 30, 7, + decode(repeat('a9', 32), 'hex'), + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.900Z' +); +select programmable_private.stage_initial_buy_vesting_projection( + '98290000-0000-0000-0000-000000000001', + '98280000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('11', 20), 'hex'), decode(repeat('71', 20), 'hex'), 100, + '2026-07-31T03:03:00Z', '2026-08-30T03:03:00Z', + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:06.950Z' +); +reset role; + +select is( + ( + select count(*) + from programmable_private.chain_event_occurrences + where occurrence_id = '91370000-0000-0000-0000-000000000001' + ), + 1::bigint, + 'shared raw chain data retains one global occurrence identity' +); + +select is( + ( + select count(*) + from programmable_private.chain_event_occurrence_materializations + where occurrence_id = '91370000-0000-0000-0000-000000000001' + and (epoch_id, pointer_generation) in ( + ('91000000-0000-0000-0000-000000000001'::uuid, 1::bigint), + ('91300000-0000-0000-0000-000000000001'::uuid, 1::bigint) + ) + ), + 2::bigint, + 'one raw occurrence materializes independently in two exact release epochs' +); + +set local role programmable_projector; +select lives_ok( + $sql$ + select programmable_private.assert_projection_event_allowed( + '91320000-0000-0000-0000-000000000001', + '91370000-0000-0000-0000-000000000001', + 'pool' + ) + $sql$, + 'the second release authorizes the shared occurrence through its own materialization' +); +reset role; + +select is( + (select count(*) from programmable_private.creator_hook_claim_facts) + + (select count(*) from programmable_private.launcher_hook_claim_facts), + 2::bigint, + 'creator and launcher hook claims persist as distinct typed facts' +); +set local role programmable_projector; +select is( + programmable_private.append_creator_hook_claim_fact( + '91600000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000005', + decode(repeat('73', 32), 'hex'), decode(repeat('77', 20), 'hex'), + null, null, null, decode(repeat('72', 20), 'hex'), 10, + '2026-07-31T03:03:00.100Z' + ), + '91600000-0000-0000-0000-000000000001'::uuid, + 'exact typed event-fact replay is idempotent' +); +select throws_ok( + $sql$ + select programmable_private.append_creator_hook_claim_fact( + '91600000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000005', + decode(repeat('73', 32), 'hex'), decode(repeat('77', 20), 'hex'), + null, null, null, decode(repeat('72', 20), 'hex'), 11, + '2026-07-31T03:03:00.100Z' + ) + $sql$, + '23505', + 'typed event-fact replay cannot change immutable content' +); +select throws_ok( + $sql$ + select programmable_private.assert_projection_event_allowed( + '97000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000002', + 'creator_hook_claim' + ) + $sql$, + '23514', + 'wrong event and source role are rejected by the release writer allowlist' +); +select ok( + ( + select count(*) = 1 + and bool_and(allocation_fact_id = + '98000000-0000-0000-0000-000000000001'::uuid + ) + and bool_and(allocation_evidence_id is not null) + and bool_and(cardinality(ordered_beneficiaries) = 2) + from programmable_private.get_projector_verified_reward_seed_v1( + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex') + ) + ), + 'projector reward-seed reader returns the one exact promotable fact/evidence pair' +); +select throws_ok( + $sql$ + select programmable_private.promote_projection_run( + '97200000-0000-0000-0000-000000000022', + '97300000-0000-0000-0000-000000000022', + '97400000-0000-0000-0000-000000000022', + '97000000-0000-0000-0000-000000000002', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), 1, 2, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000600', + 25639600, decode(repeat('99', 32), 'hex'), + 20, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + array[ + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000003'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + '91220000-0000-0000-0000-000000000001'::uuid, + '91220000-0000-0000-0000-000000000002'::uuid, + '91220000-0000-0000-0000-000000000003'::uuid, + '91220000-0000-0000-0000-000000000020'::uuid + ], + array['explore-list']::text[], decode(repeat('e9', 32), 'hex'), + '2026-07-31T03:03:06.990Z' + ) + $sql$, + '23514', + 'publication rejects a launch whose manifest roles and conditions are missing' +); +select programmable_private.stage_launch_occurrence_role( + '97100000-0000-0000-0000-000000000002', 'vault_factory', + '96100000-0000-0000-0000-000000000001', '2026-07-31T03:03:07Z' +); +select programmable_private.stage_launch_occurrence_role( + '97100000-0000-0000-0000-000000000002', 'vesting_factory', + '96100000-0000-0000-0000-000000000003', '2026-07-31T03:03:07.010Z' +); +select programmable_private.stage_launch_projection_conditions( + '97100000-0000-0000-0000-000000000002', false, + '2026-07-31T03:03:07.020Z' +); + +select is( + public.reward_test_shared_resolution_count(), + 2::bigint, + 'one unresolved shared-source candidate can resolve to two exact release manifests' +); +select throws_ok( + $sql$ + select programmable_private.resolve_envio_candidate( + '91330000-0000-0000-0000-000000000002', + '91320000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), null, + '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('e8', 32), 'hex'), + '2026-07-31T03:03:04.200Z' + ) + $sql$, + '23514', + 'dynamic source attestation cannot be reused across release scope' +); +select is( + programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000003', + decode(repeat('76', 20), 'hex'), 25639598, + '91205000-0000-0000-0000-000000000002', + decode(repeat('a4', 32), 'hex'), + decode(repeat('c9', 32), 'hex'), + decode(repeat('f6', 32), 'hex'), + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + decode(repeat('c1', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000471', 'hex'), + decode(repeat('cb', 32), 'hex'), + decode(repeat('c4', 32), 'hex'), '2026-07-31T03:02:03.070Z' + ), + '91210000-0000-0000-0000-000000000002'::uuid, + 'vesting-wallet template decodes its pinned wallet field rather than vault' +); +select throws_ok( + $sql$ + select programmable_private.append_dual_rpc_runtime_code_evidence( + '91205000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + decode(repeat('79', 20), 'hex'), + '95000000-0000-0000-0000-000000000600', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('ca', 32), 'hex'), decode(repeat('cb', 32), 'hex'), + decode(repeat('04', 6543), 'hex'), decode(repeat('04', 6543), 'hex'), + 6543, 6543, + decode(repeat('d1', 32), 'hex'), decode(repeat('d1', 32), 'hex'), + decode(repeat('df', 32), 'hex'), + array[decode(repeat('79', 20), 'hex')], + decode(repeat('c8', 32), 'hex'), + decode(repeat('04', 6543), 'hex'), + decode(repeat('ca', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000368', 'hex'), + decode(repeat('cd', 32), 'hex'), + decode(repeat('cc', 32), 'hex'), '2026-07-31T03:03:04.300Z' + ) + $sql$, + '23514', + 'disagreeing RPC runtime code hashes cannot become deployment evidence' +); +select throws_ok( + $sql$ + select programmable_private.append_dual_rpc_runtime_code_evidence( + '91205000-0000-0000-0000-000000000004', + '93000000-0000-0000-0000-000000000001', + decode(repeat('79', 20), 'hex'), + '95000000-0000-0000-0000-000000000600', + '92000000-0000-0000-0000-000000000002', + '92000000-0000-0000-0000-000000000001', + decode(repeat('ca', 32), 'hex'), decode(repeat('ca', 32), 'hex'), + decode(repeat('04', 6543), 'hex'), decode(repeat('04', 6543), 'hex'), + 6543, 6543, + decode(repeat('d1', 32), 'hex'), decode(repeat('d1', 32), 'hex'), + decode(repeat('df', 32), 'hex'), + array[decode(repeat('79', 20), 'hex')], + decode(repeat('c8', 32), 'hex'), + decode(repeat('04', 6543), 'hex'), + decode(repeat('ca', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000369', 'hex'), + decode(repeat('ce', 32), 'hex'), + decode(repeat('cc', 32), 'hex'), '2026-07-31T03:03:04.400Z' + ) + $sql$, + '23514', + 'runtime-code evidence must retain the exact ordered safe-head provider pair' +); + +select ok( + public.reward_test_dynamic_occurrence_provenance(), + 'dynamic vault occurrence retains exact attestation and neutral-candidate provenance' +); +select is( + programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000472', 'hex'), + decode(repeat('db', 32), 'hex'), + decode(repeat('d4', 32), 'hex'), '2026-07-31T03:02:03.100Z' + ), + '91210000-0000-0000-0000-000000000001'::uuid, + 'exact dynamic source attestation replay is idempotent' +); +select throws_ok( + $sql$ + select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000008', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000478', 'hex'), + decode(repeat('c5', 32), 'hex'), + decode(repeat('e8', 32), 'hex'), '2026-07-31T03:03:04.500Z' + ) + $sql$, + '23514', + 'dynamic instance init code cannot equal its release artifact creation code' +); +select throws_ok( + $sql$ + select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('78', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000479', 'hex'), + decode(repeat('c6', 32), 'hex'), + decode(repeat('e1', 32), 'hex'), '2026-07-31T03:03:05Z' + ) + $sql$, + '23514', + 'forged emitted vault address cannot be attested' +); +select throws_ok( + $sql$ + select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('e2', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200047a', 'hex'), + decode(repeat('c7', 32), 'hex'), + decode(repeat('e3', 32), 'hex'), '2026-07-31T03:03:06Z' + ) + $sql$, + '23514', + 'wrong dynamic runtime code hash is rejected' +); +select throws_ok( + $sql$ + select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000004', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('e4', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200047b', 'hex'), + decode(repeat('c8', 32), 'hex'), + decode(repeat('e5', 32), 'hex'), '2026-07-31T03:03:07Z' + ) + $sql$, + '23514', + 'wrong dynamic ABI event-set commitment is rejected' +); +select throws_ok( + $sql$ + select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000005', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 25639599, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200047c', 'hex'), + decode(repeat('c9', 32), 'hex'), + decode(repeat('e6', 32), 'hex'), '2026-07-31T03:03:08Z' + ) + $sql$, + '23514', + 'dynamic source cannot start before its factory deployment occurrence' +); +select throws_ok( + $sql$ + select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000006', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000003', + decode(repeat('77', 20), 'hex'), 25639598, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200047d', 'hex'), + decode(repeat('ca', 32), 'hex'), + decode(repeat('e7', 32), 'hex'), '2026-07-31T03:03:09Z' + ) + $sql$, + '23503', + 'wrong factory role and event cannot register a dynamic source' +); +select throws_ok( + $sql$ + select programmable_private.register_dynamic_source_attestation( + '91210000-0000-0000-0000-000000000009', + '93000000-0000-0000-0000-000000000001', + '91200000-0000-0000-0000-000000000001', + '96100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 25639600, + '91205000-0000-0000-0000-000000000001', + decode(repeat('a4', 32), 'hex'), + decode(repeat('d9', 32), 'hex'), + decode(repeat('f7', 32), 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('b4', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200047e', 'hex'), + decode(repeat('cc', 32), 'hex'), + decode(repeat('e9', 32), 'hex'), '2026-07-31T03:03:10Z' + ) + $sql$, + '23505', + 'same dynamic address cannot replay with a different identity or commitment' +); +select throws_ok( + $sql$ + select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), + '910c0000-0000-0000-0000-000000000001', + 25639600, + decode(repeat('99', 32), 'hex'), + decode(repeat('87', 32), 'hex'), + 5, + 14, + decode(repeat('77', 20), 'hex'), + decode(repeat('d5', 32), 'hex'), + 'BeneficiaryFeesClaimed', + array[decode(repeat('d5', 32), 'hex')], + decode('0105', 'hex'), + '{"releaseVersion":"unresolved"}'::jsonb, + decode(repeat('d6', 32), 'hex'), + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), + '92000000-0000-0000-0000-000000000003', + decode(repeat('d7', 32), 'hex'), + '2026-07-31T03:03:11Z' +) + $sql$, + '23505', + 'neutral candidate replay conflict cannot overwrite raw evidence' +); +select throws_ok( + $sql$ + select programmable_private.resolve_envio_candidate( + '91220000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), null, + '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('ea', 32), 'hex'), + '2026-07-31T03:03:12Z' + ) + $sql$, + '23505', + 'conflicting later resolution cannot replace an audited association' +); +select throws_ok( + 'select public.reward_test_orphaned_dynamic_resolution()', + '23514', + 'orphaning the factory occurrence immediately revokes dynamic admission' +); +select throws_ok( + $sql$ + select programmable_private.append_chain_event_occurrence( + '91230000-0000-0000-0000-000000000001', + '91240000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14), + '91220000-0000-0000-0000-000000000001', + 3, '2026-07-31T02:58:37Z', 'decoder-v1', + decode(repeat('e8', 32), 'hex'), + '95000000-0000-0000-0000-000000000600', 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310055', 'hex'), + decode(repeat('d9', 32), 'hex'), '2026-07-31T03:02:03.400Z' + ) + $sql$, + '23503', + 'dynamic occurrence cannot cross-use a resolution under the wrong ABI' +); + +select ok( + not exists ( + with projection_tables(table_name) as ( + values + ('launch_projections'), ('pool_projections'), + ('pool_fee_configurations'), ('fee_accrual_facts'), + ('pool_fee_totals'), ('reward_vault_projections'), + ('reward_allocation_projections'), ('claim_projections'), + ('payout_change_projections'), ('account_reward_balances'), + ('initial_buy_custody_projections'), + ('initial_buy_vesting_projections') + ) + select 1 + from projection_tables as expected + where exists ( + select required.column_name + from ( + values ('chain_id'), ('release_id'), ('model_id'), ('epoch_id'), + ('pointer_generation'), ('projection_run_id'), + ('promoted_block_number'), ('promoted_block_hash'), + ('verified_at') + ) as required(column_name) + where not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + join pg_catalog.pg_class as relation + on relation.oid = attribute.attrelid + join pg_catalog.pg_namespace as namespace + on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname = expected.table_name + and attribute.attname = required.column_name + and attribute.attnum > 0 + and not attribute.attisdropped + ) + ) + or not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + join pg_catalog.pg_class as relation + on relation.oid = attribute.attrelid + join pg_catalog.pg_namespace as namespace + on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname = expected.table_name + and attribute.attname in ( + 'last_source_logical_event_id', 'source_logical_event_id', + 'disclosure_source_logical_event_id' + ) + and attribute.attnum > 0 + and not attribute.attisdropped + ) + or not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + join pg_catalog.pg_class as relation + on relation.oid = attribute.attrelid + join pg_catalog.pg_namespace as namespace + on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname = expected.table_name + and attribute.attname in ( + 'last_source_occurrence_block_hash', + 'source_occurrence_block_hash', + 'disclosure_source_occurrence_block_hash' + ) + and attribute.attnum > 0 + and not attribute.attisdropped + ) + ), + 'every normalized projection carries exact release source run target and verification provenance' +); + +select is( + public.reward_test_evidence_recovery_binding(), + '91100000-0000-0000-0000-000000000005'::uuid, + 'coordinator calldata evidence resolves the exact manifest address and selector binding' +); + +select is( + programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + array[ + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['launcher', 'vault_factory', 'hook']::text[], + 1::smallint, + public.reward_test_allocation_preimage(), + decode('760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0', 'hex'), + '2026-07-31T03:03:02Z' + ), + '98000000-0000-0000-0000-000000000001'::uuid, + 'exact allocation fact replay is idempotent' +); +select is( + programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'seed-verifier-v1.0.0', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('a2', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + public.reward_test_evidence_preimage(), + decode('db14c9fa42eaedfcf77221edc2861e7f2c0251997313951d7a06c65ca73beea3', 'hex'), + '2026-07-31T03:03:03Z', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex') + ), + '98100000-0000-0000-0000-000000000001'::uuid, + 'exact evidence replay is idempotent' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'seed-verifier-v1.0.0', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('a2', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + public.reward_test_evidence_preimage(), + decode('db14c9fa42eaedfcf77221edc2861e7f2c0251997313951d7a06c65ca73beea3', 'hex'), + '2026-07-31T03:03:03Z' + ) + $sql$, + '23505', + 'an attested evidence row cannot replay without the immutable recomputation proof' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + array[ + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['launcher', 'vault_factory', 'hook']::text[], + 1::smallint, + public.reward_test_allocation_preimage() || decode('01', 'hex'), + decode('760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0', 'hex'), + '2026-07-31T03:03:02Z' + ) + $sql$, + '23505', + 'changed allocation preimage with original digest is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + array[ + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['launcher', 'vault_factory', 'hook']::text[], + 1::smallint, + public.reward_test_allocation_preimage(), + decode(repeat('fd', 32), 'hex'), + '2026-07-31T03:03:02Z' + ) + $sql$, + '23505', + 'original allocation preimage with changed digest is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + array[ + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['launcher', 'vault_factory', 'hook']::text[], + 1::smallint, + public.reward_test_allocation_preimage() || decode('02', 'hex'), + decode(repeat('fc', 32), 'hex'), + '2026-07-31T03:03:02Z' + ) + $sql$, + '23505', + 'changed allocation preimage and digest are rejected together' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'seed-verifier-v1.0.0', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + public.reward_test_evidence_preimage(), + decode(repeat('ff', 32), 'hex'), '2026-07-31T03:03:03Z' + ) + $sql$, + '23505', + 'original evidence preimage with changed digest is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'seed-verifier-v1.0.0', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + public.reward_test_evidence_preimage() || decode('01', 'hex'), + decode('db14c9fa42eaedfcf77221edc2861e7f2c0251997313951d7a06c65ca73beea3', 'hex'), + '2026-07-31T03:03:03Z' + ) + $sql$, + '23505', + 'changed evidence preimage with original digest is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'seed-verifier-v1.0.0', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + public.reward_test_evidence_preimage() || decode('02', 'hex'), + decode(repeat('fe', 32), 'hex'), '2026-07-31T03:03:03Z' + ) + $sql$, + '23505', + 'changed evidence preimage and digest are rejected together' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + decode(repeat('a3', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + array[ + '96100000-0000-0000-0000-000000000004'::uuid, + '96100000-0000-0000-0000-000000000003'::uuid, + '96100000-0000-0000-0000-000000000002'::uuid + ], + array['launcher', 'vault_factory', 'hook']::text[], + 1::smallint, + public.reward_test_allocation_preimage(), + decode('760efbc9872c2018c892290c30ca097f4b346240b30c766a22c20568bf4d14f0', 'hex'), + '2026-07-31T03:03:02Z' + ) + $sql$, + '23505', + 'reordered required occurrences cannot replay the fixed allocation vector' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + decode(repeat('78', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000.5::numeric, 3999.5::numeric], + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + decode(repeat('d3', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + array['96100000-0000-0000-0000-000000000001'::uuid], + array['factory']::text[], 1::smallint, + decode('70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310070', 'hex'), + decode(repeat('d4', 32), 'hex'), '2026-07-31T03:03:07Z' + ) + $sql$, + '22023', + 'fractional beneficiary shares abort before domain assignment' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000003', + '97000000-0000-0000-0000-000000000002', + decode(repeat('79', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + decode(repeat('d5', 32), 'hex'), decode(repeat('d6', 32), 'hex'), + decode(repeat('d7', 32), 'hex'), decode(repeat('ff', 32), 'hex'), + array['96100000-0000-0000-0000-000000000001'::uuid], + array['factory']::text[], 1::smallint, + decode('70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310071', 'hex'), + decode(repeat('d8', 32), 'hex'), '2026-07-31T03:03:08Z' + ) + $sql$, + '23514', + 'allocation fact rejects a mismatched release artifact commitment' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000004', + '97000000-0000-0000-0000-000000000002', + decode(repeat('7a', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + decode(repeat('d9', 32), 'hex'), decode(repeat('da', 32), 'hex'), + decode(repeat('db', 32), 'hex'), decode(repeat('a4', 32), 'hex'), + array['96100000-0000-0000-0000-000000000003'::uuid], + array['factory']::text[], 1::smallint, + decode('70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310072', 'hex'), + decode(repeat('dc', 32), 'hex'), '2026-07-31T03:03:08.500Z' + ) + $sql$, + '23514', + 'allocation fact requires the complete ordered launcher factory and hook set' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000009', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'malformed-local-init-code', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), + decode(repeat('b4', 31), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310071', 'hex'), + decode(repeat('d9', 32), 'hex'), '2026-07-31T03:03:08Z' + ) + $sql$, + '22023', + 'per-instance init-code evidence must be an exact bytes32 without equating it to the release artifact' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000010', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'invalid-enrichment', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + decode(repeat('99', 32), 'hex'), null, null, null, null, + null, null, + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310072', 'hex'), + decode(repeat('da', 32), 'hex'), '2026-07-31T03:03:09Z' + ) + $sql$, + '23514', + 'unavailable enrichment rejects every served getter or prediction field' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000011', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'historical_getters', 'incomplete-history', + null, null, null, decode(repeat('c0', 32), 'hex'), + decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'matched', + decode(repeat('99', 32), 'hex'), + decode(repeat('b5', 32), 'hex'), decode(repeat('b5', 32), 'hex'), + null, null, + decode(repeat('77', 20), 'hex'), decode(repeat('77', 20), 'hex'), + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + null, null, 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310073', 'hex'), + decode(repeat('db', 32), 'hex'), '2026-07-31T03:03:10Z' + ) + $sql$, + '23514', + 'historical getters require complete paired getter and prediction results' +); +select throws_ok( + $sql$ + select public.reward_test_quarantine_then_rollback($call$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000012', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'wrong-getter-block', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'matched', + decode(repeat('98', 32), 'hex'), + decode(repeat('b5', 32), 'hex'), decode(repeat('b5', 32), 'hex'), + decode(repeat('b6', 32), 'hex'), decode(repeat('b6', 32), 'hex'), + decode(repeat('77', 20), 'hex'), decode(repeat('77', 20), 'hex'), + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310074', 'hex'), + decode(repeat('dc', 32), 'hex'), '2026-07-31T03:03:11Z' + ) + $call$, '98100000-0000-0000-0000-000000000012') + $sql$, + 'P0001', + 'wrong-block historical evidence is quarantined and the fixture rolls back' +); +select throws_ok( + $sql$ + select public.reward_test_quarantine_then_rollback($call$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000015', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'historical_getters', 'wrong-provider-prediction', + null, null, null, decode(repeat('c0', 32), 'hex'), + decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'matched', + decode(repeat('99', 32), 'hex'), + decode(repeat('b5', 32), 'hex'), decode(repeat('b5', 32), 'hex'), + decode(repeat('b6', 32), 'hex'), decode(repeat('b6', 32), 'hex'), + decode(repeat('77', 20), 'hex'), decode(repeat('76', 20), 'hex'), + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + null, null, 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310075', 'hex'), + decode(repeat('dd', 32), 'hex'), '2026-07-31T03:03:12Z' + ) + $call$, '98100000-0000-0000-0000-000000000015') + $sql$, + 'P0001', + 'contradictory provider predictions are quarantined and the fixture rolls back' +); +select throws_ok( + $sql$ + select public.reward_test_quarantine_then_rollback($call$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000013', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'wrong-create2', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('76', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('b7', 32), 'hex'), decode(repeat('b7', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310075', 'hex'), + decode(repeat('dd', 32), 'hex'), '2026-07-31T03:03:12Z' + ) + $call$, '98100000-0000-0000-0000-000000000013') + $sql$, + 'P0001', + 'contradictory CREATE2 addresses are quarantined and the fixture rolls back' +); +select throws_ok( + $sql$ + select public.reward_test_quarantine_then_rollback($call$ + select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000014', + '98000000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + 'launcher_calldata', 'rpc-disagreement', + decode(repeat('b2', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('b3', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('b7', 32), 'hex'), decode(repeat('b6', 32), 'hex'), + decode(repeat('b8', 32), 'hex'), decode(repeat('b8', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310076', 'hex'), + decode(repeat('de', 32), 'hex'), '2026-07-31T03:03:13Z' + ) + $call$, '98100000-0000-0000-0000-000000000014') + $sql$, + 'P0001', + 'selected-authority RPC disagreement is quarantined and the fixture rolls back' +); +select throws_ok( + $sql$ + select programmable_private.stage_account_reward_balance( + '98200000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + decode(repeat('11', 20), 'hex'), decode(repeat('77', 20), 'hex'), + 0.1, 0, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:14Z' + ) + $sql$, + '22003', + 'fractional projected reward totals are rejected before insert' +); +select throws_ok( + $sql$ + select programmable_private.stage_pool_fee_configuration( + '98230000-0000-0000-0000-000000000002', + '97110000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + 0.1, 40, 200, 100, 0, 3000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:14.100Z' + ) + $sql$, + '23514', + 'fractional fee basis points abort before the integer domain can round them' +); + +reset role; + +select is( + (select count(*) from programmable_private.reward_allocation_facts), + 1::bigint, + 'failed allocation attempts leave the one fixed fact' +); +select is( + (select count(*) from programmable_private.reward_allocation_evidence), + 4::bigint, + 'only release-bound calldata and complete historical evidence survive' +); +select is( + ( + select encode(canonical_preimage, 'hex') + from programmable_private.reward_allocation_facts + where allocation_fact_id = '98000000-0000-0000-0000-000000000001' + ), + '70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310000000000000000010000000a636c61737369632d76330000000a636c61737369632d7633777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000299999999999999999999999999999999999999999999999999999999999999990000000001873ab00000000400000002111111111111111111111111111111111111111122222222222222222222222222222222222222220000000217700fa0a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a201a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a400000003a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a500000001a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6000000086c61756e63686572a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a700000001a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a800000007666163746f7279a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a900000001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000004686f6f6b', + 'SQL stores the reviewed allocation preimage byte-for-byte' +); +select is( + ( + select encode(content_fingerprint, 'hex') + from programmable_private.reward_allocation_evidence + where allocation_evidence_id = '98100000-0000-0000-0000-000000000001' + ), + 'db14c9fa42eaedfcf77221edc2861e7f2c0251997313951d7a06c65ca73beea3', + 'SQL stores the reviewed evidence Keccak digest byte-for-byte' +); +select is( + programmable_private.validate_uint256( + 115792089237316195423570985008687907853269984665640564039457584007913129639935 + ), + 115792089237316195423570985008687907853269984665640564039457584007913129639935::numeric, + 'maximum uint256 validation remains exact without poisoning a live baseline' +); +select is( + ( + with value(amount) as ( + values (999999999999999999999999999999::numeric) + ) + select amount + - pg_catalog.div(amount * 6000, 10000) + - pg_catalog.div(amount * 4000, 10000) + from value + ), + 1::numeric, + 'beneficiary floor allocation preserves an exact one-unit remainder' +); +select is( + ( + select + (select count(*) from programmable_private.pool_fee_configurations) + + (select count(*) from programmable_private.fee_accrual_facts) + + (select count(*) from programmable_private.pool_fee_totals) + + (select count(*) from programmable_private.claim_projections) + + (select count(*) from programmable_private.payout_change_projections) + + (select count(*) from programmable_private.initial_buy_custody_projections) + + (select count(*) from programmable_private.initial_buy_vesting_projections) + ), + 8::bigint, + 'all dedicated fee claim payout custody and vesting writers persist typed rows' +); +select is( + ( + select count(*) + from programmable_private.mutation_audits + where action in ( + 'pool_fee_configuration.stage', 'fee_accrual.stage', + 'pool_fee_total.stage', 'claim_projection.stage', + 'payout_change_projection.stage', 'initial_buy_custody.stage', + 'initial_buy_vesting.stage' + ) + ), + 8::bigint, + 'every dedicated projection writer appends its mutation audit' +); + +set local role programmable_projector; + +select throws_ok( + $sql$ + select programmable_private.append_reward_seed_status( + '98300000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '98100000-0000-0000-0000-000000000001', + 'verified', decode(repeat('e1', 32), 'hex'), + '97000000-0000-0000-0000-000000000002', + '2026-07-31T03:03:15Z' + ) + $sql$, + '42501', + 'append-only seed status cannot bypass promotion to verify a seed' +); +select throws_ok( + $sql$ + select programmable_private.promote_projection_run( + '97200000-0000-0000-0000-000000000020', + '97300000-0000-0000-0000-000000000020', + '97400000-0000-0000-0000-000000000020', + '97000000-0000-0000-0000-000000000002', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), 1, 2, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000600', + 25639600, decode(repeat('99', 32), 'hex'), + 20, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + array[ + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000003'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + '91220000-0000-0000-0000-000000000001'::uuid, + '91220000-0000-0000-0000-000000000002'::uuid, + '91220000-0000-0000-0000-000000000003'::uuid, + '91220000-0000-0000-0000-000000000020'::uuid + ], + array['explore-list']::text[], decode(repeat('e8', 32), 'hex'), + '2026-07-31T03:03:15.100Z' + ) + $sql$, + '22023', + 'promotion rejects a non-canonical occurrence fold order before mutation' +); +select throws_ok( + $sql$ + select programmable_private.promote_projection_run( + '97200000-0000-0000-0000-000000000021', + '97300000-0000-0000-0000-000000000021', + '97400000-0000-0000-0000-000000000021', + '97000000-0000-0000-0000-000000000002', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), 1, 2, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000600', + 25639600, decode(repeat('99', 32), 'hex'), + 20, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + array[ + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000003'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + '91220000-0000-0000-0000-000000000001'::uuid, + '91220000-0000-0000-0000-000000000002'::uuid, + '91220000-0000-0000-0000-000000000003'::uuid, + '91220000-0000-0000-0000-000000000020'::uuid + ], + array['explore-list']::text[], decode(repeat('e8', 32), 'hex'), + '2026-07-31T03:03:15.200Z' + ) + $sql$, + '23514', + 'promotion rejects a fold that omits any staged projection source' +); +select programmable_private.promote_projection_run( + '97200000-0000-0000-0000-000000000002', + '97300000-0000-0000-0000-000000000002', + '97400000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 1, 2, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000600', + 25639600, decode(repeat('99', 32), 'hex'), + 20, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + array[ + '91240000-0000-0000-0000-000000000002'::uuid, + '91240000-0000-0000-0000-000000000003'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000003'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + '91220000-0000-0000-0000-000000000001'::uuid, + '91220000-0000-0000-0000-000000000002'::uuid, + '91220000-0000-0000-0000-000000000003'::uuid, + '91220000-0000-0000-0000-000000000020'::uuid + ], + array['explore-list']::text[], + decode(repeat('e2', 32), 'hex'), '2026-07-31T03:03:16Z' +); +select programmable_private.open_run( + 'a3100000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('a3', 32), 'hex'), + '2026-07-31T03:03:16.010Z' +); +select is( + ( + select pg_catalog.count(*) + from programmable_private.get_projector_reward_balances_by_vault_v1( + 'a3100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + ), + 3::bigint, + 'all-current balance reader retains active and historical beneficiaries' +); +select ok( + ( + select pg_catalog.count(*) = 1 + and pg_catalog.bool_and(claimable_accrued = 7) + and pg_catalog.bool_and(claimed_total = 5) + from programmable_private.get_projector_reward_balances_by_vault_v1( + 'a3100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + where account = decode(repeat('33', 20), 'hex') + ), + 'historical beneficiary keeps nonzero claimable and claimed totals' +); +select is( + ( + select pg_catalog.count(*) + from programmable_private.get_projector_reward_state_by_vault_v1( + 'a3100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + ), + 2::bigint, + 'active-allocation reader remains a separate two-beneficiary channel' +); +select is( + ( + select pg_catalog.count(*) + from programmable_private.get_projector_reward_state_by_vault_v1( + 'a3100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + where beneficiary = decode(repeat('33', 20), 'hex') + ), + 0::bigint, + 'historical beneficiary is not misrepresented as an active allocation' +); +select throws_ok( + $sql$ + select public.reward_test_stale_reward_balance_reorg( + 'a3100000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + $sql$, + '23514', + 'all-current balance reader rejects stale reorg generation bindings' +); +select programmable_private.append_run_outcome( + 'a3100000-0000-0000-0000-000000000002', + 'a3100000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('a4', 32), 'hex'), + '2026-07-31T03:03:16.020Z' +); +select throws_ok( + $sql$ + select programmable_private.append_creator_hook_claim_fact( + '91600000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000005', + decode(repeat('73', 32), 'hex'), decode(repeat('77', 20), 'hex'), + null, null, null, decode(repeat('72', 20), 'hex'), 10, + '2026-07-31T03:03:00.100Z' + ) + $sql$, + '55000', + 'terminal projection runs reject creator-hook claim fact replays' +); +select throws_ok( + $sql$ + select programmable_private.append_launcher_hook_claim_fact( + '91600000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000002', + '96100000-0000-0000-0000-000000000006', + decode(repeat('31', 20), 'hex'), decode(repeat('32', 20), 'hex'), + null, decode(repeat('33', 20), 'hex'), 20, + '2026-07-31T03:03:00.200Z' + ) + $sql$, + '55000', + 'terminal projection runs reject launcher-hook claim fact replays' +); +reset role; + +select is( + ( + select ordered_projection_rows + from programmable_private.projection_fold_manifests + where run_id = '97000000-0000-0000-0000-000000000002' + and projection_row_count = cardinality(ordered_projection_rows) + ), + array[ + 'account_reward_balance:98200000-0000-0000-0000-000000000001', + 'account_reward_balance:98200000-0000-0000-0000-000000000003', + 'account_reward_balance:98200000-0000-0000-0000-000000000004', + 'claim:98260000-0000-0000-0000-000000000001', + 'fee_accrual:98240000-0000-0000-0000-000000000001', + 'initial_buy_custody:98280000-0000-0000-0000-000000000001', + 'initial_buy_vesting:98290000-0000-0000-0000-000000000001', + 'launch:97100000-0000-0000-0000-000000000002', + 'payout_change:98270000-0000-0000-0000-000000000001', + 'pool:97110000-0000-0000-0000-000000000002', + 'pool_fee_configuration:98230000-0000-0000-0000-000000000001', + 'pool_fee_total:98250000-0000-0000-0000-000000000001', + 'reward_allocation:98220000-0000-0000-0000-000000000001', + 'reward_allocation:98220000-0000-0000-0000-000000000002', + 'reward_vault:98210000-0000-0000-0000-000000000001' + ]::text[], + 'promotion persists the complete canonically ordered typed projection fold' +); +select is( + ( + select allocation_evidence_id + from programmable_private.reward_allocation_current_verified + where allocation_fact_id = '98000000-0000-0000-0000-000000000001' + ), + '98100000-0000-0000-0000-000000000001'::uuid, + 'promotion alone selects one verified allocation authority' +); +select is( + ( + select transaction_index::bigint + from programmable_private.chain_event_occurrences + where occurrence_id = '96100000-0000-0000-0000-000000000001' + ), + 4294967295::bigint, + 'full u32 transaction indexes survive candidate ingestion and occurrence materialization' +); +select is( + ( + select receipt_log_ordinal::bigint + from programmable_private.chain_event_occurrences + where occurrence_id = '96100000-0000-0000-0000-000000000001' + ), + 4294967295::bigint, + 'full u32 receipt ordinals survive candidate ingestion and occurrence materialization' +); +select is( + ( + select launch_transaction_index + from programmable_private.recent_launches_v1 + where token = decode(repeat('71', 20), 'hex') + ), + 4294967295::bigint, + 'full u32 transaction indexes survive projection into the direct read model' +); +select is( + ( + select launch_receipt_log_ordinal + from programmable_private.recent_launches_v1 + where token = decode(repeat('71', 20), 'hex') + ), + 4294967295::bigint, + 'full u32 receipt ordinals survive projection into the direct read model' +); +select is( + ( + select status::text + from programmable_private.route_eligibility_current + where route_key = 'explore-list' + ), + 'eligible', + 'verified seed publication leaves its named route eligible' +); + +savepoint null_recomputation_contradiction; +set local role programmable_projector; +select programmable_private.open_run( + '97900000-0000-0000-0000-000000000001', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v2', decode(repeat('79', 32), 'hex'), + '2026-07-31T03:03:16.010Z' +); +select programmable_private.append_reward_allocation_evidence( + '97910000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000001', + '97900000-0000-0000-0000-000000000001', + 'launcher_calldata', 'later-contradiction-v1', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('79', 32), 'hex'), decode(repeat('c0', 32), 'hex'), + decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, null, null, + decode(repeat('ff', 32), 'hex'), decode(repeat('ff', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310090', 'hex'), + decode(repeat('90', 32), 'hex'), '2026-07-31T03:03:16.020Z' +); +reset role; +select is( + ( + select count(*) + from programmable_private.reward_allocation_current_verified + where allocation_fact_id = '98000000-0000-0000-0000-000000000001' + ), + 0::bigint, + 'later contradictory evidence clears the exact verified allocation pointer even without recomputation fields' +); +select is( + ( + select status::text + from programmable_private.route_eligibility_current + where route_key = 'explore-list' + and epoch_id = '91000000-0000-0000-0000-000000000001' + and pointer_generation = 1 + ), + 'quarantined', + 'later contradictory evidence quarantines the exact epoch-generation route' +); +select is( + ( + select count(*) + from programmable_private.reward_allocation_mismatch_evidence + where mismatch_evidence_id = '97910000-0000-0000-0000-000000000001' + ), + 1::bigint, + 'later contradiction is retained while non-attested legacy evidence remains non-promotable' +); +rollback to savepoint null_recomputation_contradiction; + +-- A later publication is deliberately a delta: it stages a second launch and +-- fee total without restaging the first launch's reward-vault subtree. Public +-- current reads must retain both independently published entity versions. +set local role programmable_projector; +select programmable_private.open_run( + '97000000-0000-0000-0000-000000000005', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('65', 32), 'hex'), + '2026-07-31T03:03:16.100Z' +); +select programmable_private.stage_launch_projection( + '97100000-0000-0000-0000-000000000005', + '97000000-0000-0000-0000-000000000005', + decode(repeat('75', 20), 'hex'), decode(repeat('76', 20), 'hex'), + decode(repeat('85', 32), 'hex'), decode(repeat('84', 32), 'hex'), + null, decode(repeat('83', 32), 'hex'), + 'Delta Token', 'DELTA', 1000000000000000000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:16.200Z' +); +select programmable_private.stage_pool_projection( + '97110000-0000-0000-0000-000000000005', + '97100000-0000-0000-0000-000000000005', + '97000000-0000-0000-0000-000000000005', + decode(repeat('00', 20), 'hex'), decode(repeat('75', 20), 'hex'), + 3000, 60, decode(repeat('39', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:16.300Z' +); +select programmable_private.stage_pool_fee_configuration( + '97120000-0000-0000-0000-000000000005', + '97110000-0000-0000-0000-000000000005', + '97000000-0000-0000-0000-000000000005', + 30, 40, 20, 10, 0, 3000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:16.400Z' +); +select programmable_private.stage_pool_fee_total( + '98250000-0000-0000-0000-000000000005', + '97000000-0000-0000-0000-000000000005', + decode(repeat('84', 32), 'hex'), null, + 100, 60, 40, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:03:16.500Z' +); +select programmable_private.stage_launch_occurrence_role( + '97100000-0000-0000-0000-000000000005', 'vault_factory', + '96100000-0000-0000-0000-000000000001', '2026-07-31T03:03:16.510Z' +); +select programmable_private.stage_launch_projection_conditions( + '97100000-0000-0000-0000-000000000005', false, + '2026-07-31T03:03:16.520Z' +); +select programmable_private.promote_projection_run( + '97200000-0000-0000-0000-000000000005', + '97300000-0000-0000-0000-000000000005', + '97400000-0000-0000-0000-000000000005', + '97000000-0000-0000-0000-000000000005', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 2, 3, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000600', + 25639600, decode(repeat('99', 32), 'hex'), + 20, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + array['96100000-0000-0000-0000-000000000001'::uuid], + array[]::uuid[], array[]::uuid[], array[]::uuid[], + array['explore-list']::text[], + decode(repeat('e5', 32), 'hex'), '2026-07-31T03:03:16.600Z' +); + +reset role; + +select is( + (select count(*) from programmable_private.recent_launches_v1), + 2::bigint, + 'delta publication retains the prior launch while adding the new launch' +); +select is( + ( + select + (select count(*) from programmable_private.current_reward_vault_projections_v1) + + (select count(*) from programmable_private.current_account_reward_balances_v1) + + (select count(*) from programmable_private.current_pool_fee_totals_v1) + ), + 6::bigint, + 'delta publication retains prior reward pointers and both fee-total pointers' +); +set local role programmable_api_reader; +select is( + ( + with first_page as ( + select * + from programmable_private.get_recent_launches_v1( + 1, 1, null, null, null + ) + ), second_page as ( + select page.* + from first_page as cursor + cross join lateral programmable_private.get_recent_launches_v1( + 1, 1, cursor.promoted_block_number, + cursor.launch_transaction_hash, cursor.token + ) as page + ), paged as ( + select 1 as page_number, token from first_page + union all + select 2, token from second_page + ) + select pg_catalog.array_agg(token order by page_number) + from paged + ), + ( + select pg_catalog.array_agg( + token order by promoted_block_number desc, + launch_transaction_hash desc, token + ) + from programmable_private.get_recent_launches_v1( + 1, 100, null, null, null + ) + ), + 'composite cursor concatenates same-block pages without omission or duplication' +); +reset role; + +-- Reward deltas may legitimately follow an unrelated launch publication. The +-- staged snapshot binds the current global cursor while retaining the immutable +-- reward-vault entity identity, then promotion proves the exact per-beneficiary +-- transition for every event in the same vault transaction. +set local role programmable_projector; +select programmable_private.append_dual_rpc_block_evidence( + '95000000-0000-0000-0000-000000000601', + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 25639601, decode(repeat('9a', 32), 'hex'), decode(repeat('9a', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000245', 'hex'), + decode(repeat('46', 32), 'hex'), '2026-07-31T03:03:17.000Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '95000000-0000-0000-0000-000000000602', + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 25639602, decode(repeat('9b', 32), 'hex'), decode(repeat('9b', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000246', 'hex'), + decode(repeat('47', 32), 'hex'), '2026-07-31T03:03:17.010Z' +); +select programmable_private.append_dual_rpc_block_evidence( + '95000000-0000-0000-0000-000000000603', + '94000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 25639603, decode(repeat('99', 32), 'hex'), decode(repeat('99', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000247', 'hex'), + decode(repeat('48', 32), 'hex'), '2026-07-31T03:03:17.020Z' +); + +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 21 + ), + '910c0000-0000-0000-0000-000000000001', 25639601, + decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), + 12, 21, decode(repeat('77', 20), 'hex'), + decode(repeat('e6', 32), 'hex'), 'CreatorFeesCheckpointed', + array[decode(repeat('e6', 32), 'hex')], decode('0201', 'hex'), + '{"poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","configurationEpoch":"1","amount":"100","totalCreatorFeesReceived":"1100"}'::jsonb, + decode(repeat('e7', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 21 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('e8', 32), 'hex'), '2026-07-31T03:03:17.100Z' +); +select programmable_private.resolve_envio_candidate( + 'a3210000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 21 + ), null, '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('e9', 32), 'hex'), + '2026-07-31T03:03:17.110Z' +); +select programmable_private.append_chain_event_occurrence( + 'a3230000-0000-0000-0000-000000000001', + 'a3240000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 21 + ), + 'a3210000-0000-0000-0000-000000000001', + 0, '2026-07-31T02:58:41Z', 'decoder-v1', + decode(repeat('d2', 32), 'hex'), + '95000000-0000-0000-0000-000000000601', 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310081', 'hex'), + decode(repeat('f1', 32), 'hex'), '2026-07-31T03:03:17.120Z' +); + +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 22 + ), + '910c0000-0000-0000-0000-000000000001', 25639601, + decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), + 12, 22, decode(repeat('77', 20), 'hex'), + decode(repeat('ea', 32), 'hex'), 'BeneficiaryFeesClaimed', + array[decode(repeat('ea', 32), 'hex')], decode('0202', 'hex'), + '{"beneficiary":"0x1111111111111111111111111111111111111111","amount":"653","beneficiaryTotalClaimed":"653","vaultTotalReceived":"1100"}'::jsonb, + decode(repeat('eb', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 22 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('ec', 32), 'hex'), '2026-07-31T03:03:17.200Z' +); +select programmable_private.resolve_envio_candidate( + 'a3210000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 22 + ), null, '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('ed', 32), 'hex'), + '2026-07-31T03:03:17.210Z' +); +select programmable_private.append_chain_event_occurrence( + 'a3230000-0000-0000-0000-000000000002', + 'a3240000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 22 + ), + 'a3210000-0000-0000-0000-000000000002', + 1, '2026-07-31T02:58:41Z', 'decoder-v1', + decode(repeat('d2', 32), 'hex'), + '95000000-0000-0000-0000-000000000601', 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310082', 'hex'), + decode(repeat('f2', 32), 'hex'), '2026-07-31T03:03:17.220Z' +); + +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 23 + ), + '910c0000-0000-0000-0000-000000000001', 25639602, + decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), + 13, 23, decode(repeat('77', 20), 'hex'), + decode(repeat('ee', 32), 'hex'), 'CreatorFeesCheckpointed', + array[decode(repeat('ee', 32), 'hex')], decode('0203', 'hex'), + '{"poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","configurationEpoch":"1","amount":"100","totalCreatorFeesReceived":"1200"}'::jsonb, + decode(repeat('ef', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 23 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('f3', 32), 'hex'), '2026-07-31T03:03:17.300Z' +); +select programmable_private.resolve_envio_candidate( + 'a3310000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 23 + ), null, '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('f4', 32), 'hex'), + '2026-07-31T03:03:17.310Z' +); +select programmable_private.append_chain_event_occurrence( + 'a3330000-0000-0000-0000-000000000001', + 'a3340000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 23 + ), + 'a3310000-0000-0000-0000-000000000001', + 0, '2026-07-31T02:58:42Z', 'decoder-v1', + decode(repeat('d2', 32), 'hex'), + '95000000-0000-0000-0000-000000000602', 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310083', 'hex'), + decode(repeat('f5', 32), 'hex'), '2026-07-31T03:03:17.320Z' +); + +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 24 + ), + '910c0000-0000-0000-0000-000000000001', 25639602, + decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), + 13, 24, decode(repeat('77', 20), 'hex'), + decode(repeat('f6', 32), 'hex'), 'PayoutWalletChanged', + array[decode(repeat('f6', 32), 'hex')], decode('0204', 'hex'), + '{"poolId":"0x7373737373737373737373737373737373737373737373737373737373737373","allocationIndex":"1","previousPayoutWallet":"0x2222222222222222222222222222222222222222","newPayoutWallet":"0x1111111111111111111111111111111111111111","shareBps":"4000","configurationEpoch":"2","activeConfigurationHash":"0xa5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5","effectiveTotalCreatorFeesReceived":"1200"}'::jsonb, + decode(repeat('f7', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 24 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('f8', 32), 'hex'), '2026-07-31T03:03:17.400Z' +); +select programmable_private.resolve_envio_candidate( + 'a3310000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 24 + ), null, '91210000-0000-0000-0000-000000000001', + decode(repeat('d2', 32), 'hex'), decode(repeat('f9', 32), 'hex'), + '2026-07-31T03:03:17.410Z' +); +select programmable_private.append_chain_event_occurrence( + 'a3330000-0000-0000-0000-000000000002', + 'a3340000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 24 + ), + 'a3310000-0000-0000-0000-000000000002', + 1, '2026-07-31T02:58:42Z', 'decoder-v1', + decode(repeat('d2', 32), 'hex'), + '95000000-0000-0000-0000-000000000602', 1::smallint, + decode('70726f6772616d6d61626c653a6f6363757272656e63653a76310084', 'hex'), + decode(repeat('fa', 32), 'hex'), '2026-07-31T03:03:17.420Z' +); + +select programmable_private.open_run( + 'a3250000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('b1', 32), 'hex'), + '2026-07-31T03:03:18.000Z' +); +select is( + ( + select pg_catalog.count(*) + from programmable_private.get_projector_reward_state_by_vault_v1( + 'a3250000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + ), + 2::bigint, + 'reward reader survives an unrelated global cursor advance' +); +select programmable_private.stage_current_reward_snapshot_v1( + 'a3250000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), decode(repeat('73', 32), 'hex'), + '98000000-0000-0000-0000-000000000001', + 1, decode(repeat('a3', 32), 'hex'), 1100, + array[0, 1], + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex'), + decode(repeat('33', 20), 'hex') + ], + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex'), + decode(repeat('33', 20), 'hex') + ], + array[1::numeric, 434::numeric, 7::numeric], + array[653::numeric, 0::numeric, 5::numeric], + 'a3240000-0000-0000-0000-000000000002', + 25639601, decode(repeat('9a', 32), 'hex'), + '2026-07-31T03:03:18.100Z' +); +select programmable_private.stage_claim_projection( + 'a3260000-0000-0000-0000-000000000001', + 'a3250000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), 'beneficiary', + decode(repeat('11', 20), 'hex'), decode(repeat('11', 20), 'hex'), + 653, 653, 1100, + 'a3240000-0000-0000-0000-000000000002', + 25639601, decode(repeat('9a', 32), 'hex'), + '2026-07-31T03:03:18.110Z' +); +select throws_ok( + $sql$ + select programmable_private.promote_projection_run_v2( + 'reward_snapshot_delta', + 'a3270000-0000-0000-0000-000000000001', + 'a3270000-0000-0000-0000-000000000002', + 'a3270000-0000-0000-0000-000000000003', + 'a3250000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 3, 4, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000601', + 25639601, decode(repeat('9a', 32), 'hex'), 22, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 22 + ), + array[ + 'a3240000-0000-0000-0000-000000000001'::uuid, + 'a3240000-0000-0000-0000-000000000002'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + 'a3210000-0000-0000-0000-000000000001'::uuid, + 'a3210000-0000-0000-0000-000000000002'::uuid + ], + array['explore-list']::text[], decode(repeat('b2', 32), 'hex'), + '2026-07-31T03:03:18.200Z' + ) + $sql$, + '23514', + 'fabricated per-beneficiary reward movement is rejected' +); +reset role; +select is( + ( + select checkpoint_generation + from programmable_private.projector_checkpoint_current + where chain_id = 1 and release_id = 'classic-v3' + ), + 3::bigint, + 'fabricated reward movement cannot advance the checkpoint' +); + +set local role programmable_projector; +select programmable_private.open_run( + 'a3250000-0000-0000-0000-000000000002', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('b3', 32), 'hex'), + '2026-07-31T03:03:18.300Z' +); +select programmable_private.stage_current_reward_snapshot_v1( + 'a3250000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), decode(repeat('73', 32), 'hex'), + '98000000-0000-0000-0000-000000000001', + 1, decode(repeat('a3', 32), 'hex'), 1100, + array[0, 1], + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex')], + array[6000::numeric, 4000::numeric], + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex'), + decode(repeat('33', 20), 'hex') + ], + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex'), + decode(repeat('33', 20), 'hex') + ], + array[0::numeric, 435::numeric, 7::numeric], + array[653::numeric, 0::numeric, 5::numeric], + 'a3240000-0000-0000-0000-000000000002', + 25639601, decode(repeat('9a', 32), 'hex'), + '2026-07-31T03:03:18.400Z' +); +select programmable_private.stage_claim_projection( + 'a3260000-0000-0000-0000-000000000002', + 'a3250000-0000-0000-0000-000000000002', + decode(repeat('77', 20), 'hex'), 'beneficiary', + decode(repeat('11', 20), 'hex'), decode(repeat('11', 20), 'hex'), + 653, 653, 1100, + 'a3240000-0000-0000-0000-000000000002', + 25639601, decode(repeat('9a', 32), 'hex'), + '2026-07-31T03:03:18.410Z' +); +select lives_ok( + $sql$ + select programmable_private.promote_projection_run_v2( + 'reward_snapshot_delta', + 'a3270000-0000-0000-0000-000000000011', + 'a3270000-0000-0000-0000-000000000012', + 'a3270000-0000-0000-0000-000000000013', + 'a3250000-0000-0000-0000-000000000002', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 3, 4, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000601', + 25639601, decode(repeat('9a', 32), 'hex'), 22, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9a', 32), 'hex'), decode(repeat('c4', 32), 'hex'), 22 + ), + array[ + 'a3240000-0000-0000-0000-000000000001'::uuid, + 'a3240000-0000-0000-0000-000000000002'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + 'a3210000-0000-0000-0000-000000000001'::uuid, + 'a3210000-0000-0000-0000-000000000002'::uuid + ], + array['explore-list']::text[], decode(repeat('b4', 32), 'hex'), + '2026-07-31T03:03:18.500Z' + ) + $sql$, + 'exact checkpoint and claim reward movement promotes atomically' +); +reset role; +select is( + ( + select pg_catalog.array_agg( + pg_catalog.format( + '%s:%s:%s', pg_catalog.encode(account, 'hex'), + claimable_accrued, claimed_total + ) order by account + ) + from programmable_private.current_account_reward_balances_v1 + where vault = decode(repeat('77', 20), 'hex') + ), + array[ + repeat('11', 20) || ':0:653', + repeat('22', 20) || ':435:0', + repeat('33', 20) || ':7:5' + ]::text[], + 'successful claim snapshot publishes exact active and historical balances' +); +select is( + ( + select ordered_occurrence_ids + from programmable_private.projection_fold_manifests + where run_id = 'a3250000-0000-0000-0000-000000000002' + ), + array[ + 'a3240000-0000-0000-0000-000000000001'::uuid, + 'a3240000-0000-0000-0000-000000000002'::uuid + ], + 'claim fold manifest retains the complete transaction group in chain order' +); + +set local role programmable_projector; +select programmable_private.open_run( + 'a3350000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('b5', 32), 'hex'), + '2026-07-31T03:03:18.600Z' +); +select programmable_private.stage_current_reward_snapshot_v1( + 'a3350000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), decode(repeat('73', 32), 'hex'), + '98000000-0000-0000-0000-000000000001', + 2, decode(repeat('a5', 32), 'hex'), 1200, + array[0, 1], + array[decode(repeat('11', 20), 'hex'), decode(repeat('11', 20), 'hex')], + array[decode(repeat('11', 20), 'hex'), decode(repeat('11', 20), 'hex')], + array[6000::numeric, 4000::numeric], + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex'), + decode(repeat('33', 20), 'hex') + ], + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex'), + decode(repeat('33', 20), 'hex') + ], + array[60::numeric, 475::numeric, 7::numeric], + array[653::numeric, 0::numeric, 5::numeric], + 'a3340000-0000-0000-0000-000000000002', + 25639602, decode(repeat('9b', 32), 'hex'), + '2026-07-31T03:03:18.700Z' +); +reset role; + +savepoint stale_reward_delta_reorg; +select public.reward_test_private_call($call$ + update programmable_private.projector_checkpoint_current + set reorg_generation = reorg_generation + 1 + where chain_id = 1 + and release_id = 'classic-v3' + and model_id = 'classic-v3' + and source_group = 'core' + and projector_version = 'projector-v1' +$call$); +set local role programmable_projector; +select throws_ok( + $sql$ + select programmable_private.promote_projection_run_v2( + 'reward_snapshot_delta', + 'a3370000-0000-0000-0000-000000000001', + 'a3370000-0000-0000-0000-000000000002', + 'a3370000-0000-0000-0000-000000000003', + 'a3350000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 4, 5, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000602', + 25639602, decode(repeat('9b', 32), 'hex'), 24, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 24 + ), + array[ + 'a3340000-0000-0000-0000-000000000001'::uuid, + 'a3340000-0000-0000-0000-000000000002'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + 'a3310000-0000-0000-0000-000000000001'::uuid, + 'a3310000-0000-0000-0000-000000000002'::uuid + ], + array['explore-list']::text[], decode(repeat('b6', 32), 'hex'), + '2026-07-31T03:03:18.800Z' + ) + $sql$, + '40001', + 'reward delta rejects a stale reorg generation' +); +reset role; +rollback to savepoint stale_reward_delta_reorg; +select ok( + ( + select checkpoint_generation = 4 and reorg_generation = 0 + from programmable_private.projector_checkpoint_current + where chain_id = 1 and release_id = 'classic-v3' + ), + 'stale reorg attempt leaves the current checkpoint unchanged' +); + +set local role programmable_projector; +select lives_ok( + $sql$ + select programmable_private.promote_projection_run_v2( + 'reward_snapshot_delta', + 'a3370000-0000-0000-0000-000000000011', + 'a3370000-0000-0000-0000-000000000012', + 'a3370000-0000-0000-0000-000000000013', + 'a3350000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 4, 5, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000602', + 25639602, decode(repeat('9b', 32), 'hex'), 24, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('9b', 32), 'hex'), decode(repeat('c5', 32), 'hex'), 24 + ), + array[ + 'a3340000-0000-0000-0000-000000000001'::uuid, + 'a3340000-0000-0000-0000-000000000002'::uuid + ], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[ + 'a3310000-0000-0000-0000-000000000001'::uuid, + 'a3310000-0000-0000-0000-000000000002'::uuid + ], + array['explore-list']::text[], decode(repeat('b7', 32), 'hex'), + '2026-07-31T03:03:18.900Z' + ) + $sql$, + 'checkpoint and duplicate payout-address transition promote atomically' +); +reset role; +select is( + ( + select pg_catalog.array_agg( + pg_catalog.format( + '%s:%s:%s', pg_catalog.encode(beneficiary, 'hex'), + pg_catalog.encode(payout_address, 'hex'), share_bps + ) order by allocation_index + ) + from programmable_private.projection_entity_current as entity + join programmable_private.reward_vault_projections as vault + on vault.reward_vault_projection_id = entity.projection_row_id + and vault.projection_run_id = entity.projection_run_id + join programmable_private.reward_allocation_projections as allocation + on allocation.reward_vault_projection_id = + vault.reward_vault_projection_id + and allocation.projection_run_id = vault.projection_run_id + and allocation.effective_to_block is null + where entity.entity_kind = 'reward_vault' + and vault.vault = decode(repeat('77', 20), 'hex') + ), + array[ + repeat('11', 20) || ':' || repeat('11', 20) || ':6000', + repeat('11', 20) || ':' || repeat('11', 20) || ':4000' + ]::text[], + 'active Classic allocations preserve a deliberate duplicate payout wallet' +); +select ok( + ( + select pg_catalog.count(*) = 2 + and pg_catalog.bool_and( + case + when account = decode(repeat('22', 20), 'hex') + then claimable_accrued = 475 and claimed_total = 0 + when account = decode(repeat('33', 20), 'hex') + then claimable_accrued = 7 and claimed_total = 5 + else false + end + ) + from programmable_private.current_account_reward_balances_v1 + where vault = decode(repeat('77', 20), 'hex') + and account in ( + decode(repeat('22', 20), 'hex'), decode(repeat('33', 20), 'hex') + ) + ), + 'payout change retains exact balances for historical accounts' +); +select is( + ( + select checkpoint_generation + from programmable_private.projector_checkpoint_current + where chain_id = 1 and release_id = 'classic-v3' + ), + 5::bigint, + 'two exact reward deltas advance the checkpoint to generation five' +); + +set local role programmable_projector; +select programmable_private.open_run( + '97000000-0000-0000-0000-000000000003', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('63', 32), 'hex'), + '2026-07-31T03:04:00Z' +); +select programmable_private.append_reward_allocation_fact( + '98000000-0000-0000-0000-000000000010', + '97000000-0000-0000-0000-000000000003', + decode(repeat('77', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + array[ + decode(repeat('11', 20), 'hex'), + decode(repeat('22', 20), 'hex') + ], + array[5000::numeric, 5000::numeric], + decode(repeat('f1', 32), 'hex'), + decode(repeat('f2', 32), 'hex'), + decode(repeat('f3', 32), 'hex'), + decode(repeat('a4', 32), 'hex'), + array[ + '96100000-0000-0000-0000-000000000002'::uuid, + '96100000-0000-0000-0000-000000000001'::uuid, + '96100000-0000-0000-0000-000000000004'::uuid + ], + array['launcher', 'vault_factory', 'hook']::text[], + 1::smallint, + decode( + '70726f6772616d6d61626c653a616c6c6f636174696f6e3a76310080', + 'hex' + ), + decode(repeat('f4', 32), 'hex'), + '2026-07-31T03:04:01Z' +); +select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000010', + '98000000-0000-0000-0000-000000000010', + '97000000-0000-0000-0000-000000000003', + 'launcher_calldata', 'seed-verifier-v1.1.0', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('f5', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('b4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, + null, null, + decode(repeat('f2', 32), 'hex'), decode(repeat('f2', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + decode( + '70726f6772616d6d61626c653a65766964656e63653a76310081', + 'hex' + ), + decode(repeat('f8', 32), 'hex'), + '2026-07-31T03:04:02Z', + decode(repeat('f1', 32), 'hex'), decode(repeat('f2', 32), 'hex'), + decode(repeat('f3', 32), 'hex') +); +select programmable_private.quarantine_conflicting_reward_allocations( + '98300000-0000-0000-0000-000000000002', + '98300000-0000-0000-0000-000000000003', + '98000000-0000-0000-0000-000000000001', + '98100000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000010', + '98100000-0000-0000-0000-000000000010', + '97000000-0000-0000-0000-000000000003', + decode(repeat('e3', 32), 'hex'), + '2026-07-31T03:04:03Z' +); +select programmable_private.append_reward_allocation_evidence( + '98100000-0000-0000-0000-000000000011', + '98000000-0000-0000-0000-000000000010', + '97000000-0000-0000-0000-000000000003', + 'launcher_calldata', 'seed-verifier-v1.1.1', + decode(repeat('31', 20), 'hex'), decode('bf388406', 'hex'), + decode(repeat('f5', 32), 'hex'), decode(repeat('c0', 32), 'hex'), decode(repeat('a4', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + decode(repeat('77', 20), 'hex'), 'unavailable', + null, null, null, null, null, null, null, + decode(repeat('f2', 32), 'hex'), decode(repeat('f2', 32), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 1::smallint, + decode('70726f6772616d6d61626c653a65766964656e63653a76310082', 'hex'), + decode(repeat('e9', 32), 'hex'), '2026-07-31T03:04:04Z', + decode(repeat('f1', 32), 'hex'), decode(repeat('ee', 32), 'hex'), + decode(repeat('f3', 32), 'hex') +); + +reset role; + +select is( + (select count(*) from programmable_private.reward_allocation_mismatch_evidence + where mismatch_evidence_id = '98100000-0000-0000-0000-000000000011'), + 1::bigint, + 'attested configuration mismatch is retained as immutable evidence' +); +select is( + (select count(*) from programmable_private.reward_allocation_status_history + where allocation_fact_id = '98000000-0000-0000-0000-000000000010' + and allocation_evidence_id is null and status = 'quarantined'), + 1::bigint, + 'attested mismatch appends quarantine status instead of rolling back' +); + +select is( + (select count(*) from programmable_private.reward_allocation_current_verified), + 0::bigint, + 'conflicting valid evidence removes the selected seed without deleting facts' +); +select is( + ( + select status::text + from programmable_private.route_eligibility_current + where route_key = 'explore-list' + ), + 'quarantined', + 'conflicting allocation evidence quarantines route eligibility atomically' +); + +set local role programmable_projector; +select programmable_private.open_run( + '97000000-0000-0000-0000-000000000004', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('64', 32), 'hex'), + '2026-07-31T03:05:00Z' +); +select programmable_private.stage_launch_projection( + '97100000-0000-0000-0000-000000000004', + '97000000-0000-0000-0000-000000000004', + decode(repeat('71', 20), 'hex'), decode(repeat('72', 20), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('73', 32), 'hex'), + null, decode(repeat('74', 32), 'hex'), + 'Seed Token', 'SEED', 1000000000000000000000000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:05:01Z' +); +select programmable_private.stage_pool_projection( + '97110000-0000-0000-0000-000000000004', + '97100000-0000-0000-0000-000000000004', + '97000000-0000-0000-0000-000000000004', + decode(repeat('00', 20), 'hex'), decode(repeat('71', 20), 'hex'), + 3000, 60, decode(repeat('39', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:05:01.100Z' +); +select programmable_private.stage_pool_fee_configuration( + '97120000-0000-0000-0000-000000000004', + '97110000-0000-0000-0000-000000000004', + '97000000-0000-0000-0000-000000000004', + 30, 40, 20, 10, 0, 3000, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:05:01.200Z' +); +select throws_ok( + $sql$ + select programmable_private.promote_projection_run( + '97200000-0000-0000-0000-000000000004', + '97300000-0000-0000-0000-000000000004', + '97400000-0000-0000-0000-000000000004', + '97000000-0000-0000-0000-000000000004', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + 5, 6, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000600', + 25639600, decode(repeat('99', 32), 'hex'), + 20, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('85', 32), 'hex'), 20 + ), + array['96100000-0000-0000-0000-000000000001'::uuid], + array['98000000-0000-0000-0000-000000000001'::uuid], + array['98100000-0000-0000-0000-000000000001'::uuid], + array[]::uuid[], + array['explore-list']::text[], + decode(repeat('e4', 32), 'hex'), '2026-07-31T03:05:02Z' + ) + $sql$, + '23514', + 'quarantined evidence cannot be re-promoted' +); + +reset role; + +select is( + ( + select checkpoint_generation + from programmable_private.projector_checkpoint_current + where chain_id = 1 and release_id = 'classic-v3' + ), + 5::bigint, + 'failed re-promotion cannot advance the checkpoint' +); +select is( + ( + select count(*) + from programmable_private.run_lifecycle_outcomes + where run_id = '97000000-0000-0000-0000-000000000004' + ), + 0::bigint, + 'failed re-promotion rolls back its outcome' +); +select is( + ( + select count(*) + from programmable_private.reward_allocation_status_history + where allocation_fact_id in ( + '98000000-0000-0000-0000-000000000001', + '98000000-0000-0000-0000-000000000010' + ) + and status = 'conflicted' + ), + 2::bigint, + 'both independently valid conflicting allocations retain conflict history' +); + +set local role programmable_projector; +select throws_ok( + $sql$ + select public.reward_test_private_call($call$ + select programmable_private.append_reward_allocation_fact( + fact.allocation_fact_id, fact.verification_run_id, fact.vault, + fact.factory_occurrence_id, fact.ordered_beneficiaries, + fact.ordered_shares_bps::numeric[], fact.allocation_hash, + fact.configuration_hash, fact.active_configuration_hash, + fact.manifest_artifact_creation_code_commitment, + (select array_agg(required.occurrence_id order by required.occurrence_ordinal) + from programmable_private.reward_allocation_required_occurrences as required + where required.allocation_fact_id = fact.allocation_fact_id), + (select array_agg(required.occurrence_role::text order by required.occurrence_ordinal) + from programmable_private.reward_allocation_required_occurrences as required + where required.allocation_fact_id = fact.allocation_fact_id), + fact.encoding_version, fact.canonical_preimage, + fact.content_fingerprint, fact.created_at + ) + from programmable_private.reward_allocation_facts as fact + where fact.allocation_fact_id = '98000000-0000-0000-0000-000000000001' + $call$) + $sql$, + '55000', + 'terminal projection runs reject allocation-fact replays' +); +select throws_ok( + $sql$ + select public.reward_test_private_call($call$ + select programmable_private.append_reward_allocation_evidence( + evidence.allocation_evidence_id, evidence.allocation_fact_id, + evidence.verification_run_id, evidence.recovery_method::text, + evidence.evidence_version::text, evidence.top_level_destination, + evidence.method_selector, evidence.transaction_input_hash, + evidence.constructor_arguments_commitment, + evidence.local_init_code_hash, evidence.create2_salt, + evidence.local_create2_address, + evidence.historical_enrichment_status::text, evidence.getter_block_hash, + evidence.getter_result_hash_a, evidence.getter_result_hash_b, + evidence.predict_result_hash_a, evidence.predict_result_hash_b, + evidence.predicted_vault_a, evidence.predicted_vault_b, + evidence.selected_rpc_result_hash_a, + evidence.selected_rpc_result_hash_b, + evidence.selected_rpc_transaction_receipt_hash_a, + evidence.selected_rpc_transaction_receipt_hash_b, + evidence.encoding_version, evidence.canonical_preimage, + evidence.content_fingerprint, evidence.verified_at, + evidence.recomputed_allocation_hash, + evidence.recomputed_configuration_hash, + evidence.recomputed_active_configuration_hash + ) + from programmable_private.reward_allocation_evidence as evidence + where evidence.allocation_evidence_id = + '98100000-0000-0000-0000-000000000001' + $call$) + $sql$, + '55000', + 'terminal projection runs reject allocation-evidence replays' +); +select throws_ok( + $sql$ + select programmable_private.stage_pool_fee_total( + '98250000-0000-0000-0000-000000000001', + '97000000-0000-0000-0000-000000000002', + decode(repeat('73', 32), 'hex'), null, 1000, 200, 100, + '96100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:06:00Z' + ) + $sql$, + '55000', + 'terminal projection runs reject typed projection replays' +); + +select programmable_private.append_creator_fee_checkpoint_fact( + '91600000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + '91240000-0000-0000-0000-000000000002', + decode(repeat('73', 32), 'hex'), 1, 100, 1000, + '2026-07-31T03:06:00.100Z' +); +select programmable_private.append_reward_configuration_activation_fact( + '91600000-0000-0000-0000-000000000004', + '93000000-0000-0000-0000-000000000001', + '91240000-0000-0000-0000-000000000003', + decode(repeat('73', 32), 'hex'), decode(repeat('af', 32), 'hex'), 2, + decode(repeat('a2', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex') + ], + array[6000::numeric, 4000::numeric], 1000, + '2026-07-31T03:06:00.200Z' +); +reset role; +select is( + (select count(*) from programmable_private.creator_hook_claim_facts) + + (select count(*) from programmable_private.launcher_hook_claim_facts) + + (select count(*) from programmable_private.creator_fee_checkpoint_facts) + + (select count(*) from programmable_private.reward_configuration_activation_facts), + 4::bigint, + 'hook claims checkpoints and reward activations persist as distinct typed facts' +); +set local role programmable_projector; +select is( + programmable_private.append_creator_fee_checkpoint_fact( + '91600000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + '91240000-0000-0000-0000-000000000002', + decode(repeat('73', 32), 'hex'), 1, 100, 1000, + '2026-07-31T03:06:00.100Z' + ), + '91600000-0000-0000-0000-000000000003'::uuid, + 'exact dynamic-source event-fact replay is idempotent' +); + +-- Three release-neutral candidates intentionally share one block and global +-- log position. Their canonical identifiers are the final lossless ordering +-- component for projector pagination and checkpoint advancement. +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), 21 + ), + '910c0000-0000-0000-0000-000000000001', + 25639603, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), + 21, 21, decode(repeat('3d', 20), 'hex'), decode(repeat('3e', 32), 'hex'), + 'ClassicRewardVaultDeployed', array[decode(repeat('3e', 32), 'hex')], + decode('0201', 'hex'), '{}'::jsonb, decode(repeat('d1', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), 21 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('e1', 32), 'hex'), '2026-07-31T03:06:00.210Z', + 'canonical-events', 'ClassicVaultFactory' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c2', 32), 'hex'), 21 + ), + '910c0000-0000-0000-0000-000000000001', + 25639603, decode(repeat('99', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + 22, 21, decode(repeat('3d', 20), 'hex'), decode(repeat('3e', 32), 'hex'), + 'ClassicRewardVaultDeployed', array[decode(repeat('3e', 32), 'hex')], + decode('0202', 'hex'), '{}'::jsonb, decode(repeat('d2', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c2', 32), 'hex'), 21 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('e2', 32), 'hex'), '2026-07-31T03:06:00.220Z', + 'canonical-events', 'ClassicVaultFactory' +); +select programmable_private.append_release_neutral_envio_candidate( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c3', 32), 'hex'), 21 + ), + '910c0000-0000-0000-0000-000000000001', + 25639603, decode(repeat('99', 32), 'hex'), decode(repeat('c3', 32), 'hex'), + 23, 21, decode(repeat('3d', 20), 'hex'), decode(repeat('3e', 32), 'hex'), + 'ClassicRewardVaultDeployed', array[decode(repeat('3e', 32), 'hex')], + decode('0203', 'hex'), '{}'::jsonb, decode(repeat('d3', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c3', 32), 'hex'), 21 + ), + '92000000-0000-0000-0000-000000000003', + decode(repeat('e3', 32), 'hex'), '2026-07-31T03:06:00.230Z', + 'canonical-events', 'ClassicVaultFactory' +); +select is( + ( + select count(*) + from programmable_private.list_projector_candidate_page_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + null, null, null, 500, '2026-07-31T03:06:00.300Z' + ) + ), + 3::bigint, + 'candidate page exposes each pending release-neutral candidate once' +); +select is( + ( + with first_page as ( + select * + from programmable_private.list_projector_candidate_page_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + null, null, null, 2, '2026-07-31T03:06:00.310Z' + ) + ), cursor_row as ( + select * from first_page + order by block_number desc, block_global_log_index desc, + candidate_id desc + limit 1 + ), all_pages as ( + select candidate_id from first_page + union all + select page.candidate_id + from cursor_row as cursor + cross join lateral programmable_private.list_projector_candidate_page_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + cursor.block_number, cursor.block_global_log_index, + cursor.candidate_id, 2, '2026-07-31T03:06:00.310Z' + ) as page + ) + select count(distinct candidate_id) from all_pages + ), + 3::bigint, + 'full block-log-candidate cursor paginates same-position candidates losslessly' +); +select programmable_private.defer_envio_candidate_v1( + 'a1000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), 21 + ), + 0, 1, '2026-07-31T03:10:00Z', 'retryable-decode', + decode(repeat('f1', 32), 'hex'), '2026-07-31T03:06:01Z' +); +select is( + ( + select count(*) + from programmable_private.list_projector_candidate_page_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + null, null, null, 500, '2026-07-31T03:07:00Z' + ) + ), + 2::bigint, + 'a deferred candidate is excluded before its retry time' +); +select is( + ( + select count(*) + from programmable_private.list_projector_candidate_page_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + null, null, null, 500, '2026-07-31T03:11:00Z' + ) + ), + 3::bigint, + 'a deferred candidate returns to the page only when due' +); +select throws_ok( + $sql$ + select programmable_private.defer_envio_candidate_v1( + 'a1000000-0000-0000-0000-000000000099', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), 21 + ), + 0, 1, '2026-07-31T03:10:01Z', 'stale-retry', + decode(repeat('f2', 32), 'hex'), '2026-07-31T03:06:01.100Z' + ) + $sql$, + '40001', + 'candidate deferral rejects a stale attempt generation' +); +select programmable_private.quarantine_envio_candidate_v1( + 'a2000000-0000-0000-0000-000000000002', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c2', 32), 'hex'), 21 + ), + 0, 'unsupported-payload', decode(repeat('f3', 32), 'hex'), + '2026-07-31T03:06:02Z' +); +select programmable_private.ignore_envio_candidate_v1( + 'a2000000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c3', 32), 'hex'), 21 + ), + 0, 'known-nonrelease-event', decode(repeat('f4', 32), 'hex'), + '2026-07-31T03:06:02.100Z' +); +select programmable_private.open_run( + 'a2f00000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('9d', 32), 'hex'), + '2026-07-31T03:06:02.110Z' +); +select programmable_private.stage_launch_projection( + 'a2f10000-0000-0000-0000-000000000001', + 'a2f00000-0000-0000-0000-000000000001', + decode(repeat('71', 20), 'hex'), decode(repeat('72', 20), 'hex'), + decode(repeat('88', 32), 'hex'), decode(repeat('73', 32), 'hex'), + null, decode(repeat('74', 32), 'hex'), + 'Seed Token', 'SEED', 1000000000000000000000000, + '96100000-0000-0000-0000-000000000001', + 25639603, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:06:02.120Z' +); +select programmable_private.stage_pool_projection( + 'a2f20000-0000-0000-0000-000000000001', + 'a2f10000-0000-0000-0000-000000000001', + 'a2f00000-0000-0000-0000-000000000001', + decode(repeat('00', 20), 'hex'), decode(repeat('71', 20), 'hex'), + 3000, 60, decode(repeat('39', 20), 'hex'), + '96100000-0000-0000-0000-000000000001', + 25639603, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:06:02.130Z' +); +select programmable_private.stage_pool_fee_configuration( + 'a2f30000-0000-0000-0000-000000000001', + 'a2f20000-0000-0000-0000-000000000001', + 'a2f00000-0000-0000-0000-000000000001', + 30, 40, 20, 10, 0, 3000, + '96100000-0000-0000-0000-000000000001', + 25639603, decode(repeat('99', 32), 'hex'), + '2026-07-31T03:06:02.140Z' +); +select programmable_private.stage_launch_occurrence_role( + 'a2f10000-0000-0000-0000-000000000001', 'vault_factory', + '96100000-0000-0000-0000-000000000001', '2026-07-31T03:06:02.200Z' +); +select programmable_private.stage_launch_projection_conditions( + 'a2f10000-0000-0000-0000-000000000001', false, + '2026-07-31T03:06:02.300Z' +); +select throws_ok( + $sql$ + select programmable_private.promote_projection_run( + 'a3000000-0000-0000-0000-000000000001', + 'a3000000-0000-0000-0000-000000000002', + 'a3000000-0000-0000-0000-000000000003', + 'a2f00000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), 5, 6, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000603', + 25639603, decode(repeat('99', 32), 'hex'), 21, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c3', 32), 'hex'), 21 + ), + array[]::uuid[], array[]::uuid[], array[]::uuid[], + array[ + 'a2000000-0000-0000-0000-000000000002'::uuid, + 'a2000000-0000-0000-0000-000000000003'::uuid + ], + array['explore-list']::text[], decode(repeat('a1', 32), 'hex'), + '2026-07-31T03:06:03Z' + ) + $sql$, + '23514', + 'a deferred candidate blocks checkpoint promotion' +); +select programmable_private.resolve_envio_candidate( + 'a2000000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), 21 + ), + '91100000-0000-0000-0000-000000000004', null, + decode(repeat('57', 32), 'hex'), decode(repeat('f5', 32), 'hex'), + '2026-07-31T03:06:04Z' +); +reset role; +select is( + ( + select array_agg(status::text order by changed_at) + from programmable_private.envio_candidate_status_history + where candidate_id = programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), 21 + ) + and epoch_id = '91000000-0000-0000-0000-000000000001' + ), + array['deferred', 'resolved']::text[], + 'candidate history retains deferred and terminal transitions' +); +select is( + ( + select array_agg(status::text order by candidate_id) + from programmable_private.envio_candidate_status_current + where candidate_id in ( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c1', 32), 'hex'), 21 + ), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c2', 32), 'hex'), 21 + ), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c3', 32), 'hex'), 21 + ) + ) + and epoch_id = '91000000-0000-0000-0000-000000000001' + ), + array['resolved', 'quarantined', 'ignored']::text[], + 'release-scoped current state preserves all three terminal dispositions' +); +set local role programmable_projector; +select is( + ( + select count(*) + from programmable_private.list_projector_candidate_page_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + null, null, null, 500, '2026-07-31T03:11:00Z' + ) + ), + 0::bigint, + 'terminal candidates are absent from projector work pages' +); +select lives_ok( + $sql$ + select programmable_private.promote_projection_run( + 'a3000000-0000-0000-0000-000000000011', + 'a3000000-0000-0000-0000-000000000012', + 'a3000000-0000-0000-0000-000000000013', + 'a2f00000-0000-0000-0000-000000000001', + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), 5, 6, 0, + '94000000-0000-0000-0000-000000000001', + '95000000-0000-0000-0000-000000000603', + 25639603, decode(repeat('99', 32), 'hex'), 21, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), decode(repeat('c3', 32), 'hex'), 21 + ), + array[]::uuid[], array[]::uuid[], array[]::uuid[], + array[ + 'a2000000-0000-0000-0000-000000000001'::uuid, + 'a2000000-0000-0000-0000-000000000002'::uuid, + 'a2000000-0000-0000-0000-000000000003'::uuid + ], + array['explore-list']::text[], decode(repeat('a2', 32), 'hex'), + '2026-07-31T03:06:05Z' + ) + $sql$, + 'cursor-only promotion accepts an empty occurrence fold only with exact terminal dispositions' +); +reset role; +select ok( + ( + select cardinality(ordered_occurrence_ids) = 0 + and ordered_candidate_disposition_ids = array[ + 'a2000000-0000-0000-0000-000000000001'::uuid, + 'a2000000-0000-0000-0000-000000000002'::uuid, + 'a2000000-0000-0000-0000-000000000003'::uuid + ] + from programmable_private.projection_fold_manifests + where run_id = 'a2f00000-0000-0000-0000-000000000001' + ), + 'cursor-only fold manifest records no occurrence IDs and every terminal decision ID' +); +set local role programmable_projector; +select ok( + ( + select pg_catalog.jsonb_array_length(source_bindings) = 5 + and pg_catalog.jsonb_array_length(dynamic_source_templates) = 2 + and pg_catalog.jsonb_array_length(projection_event_rules) = 22 + and pg_catalog.jsonb_array_length( + launch_completeness_requirements + ) = 4 + and epoch_id = '91000000-0000-0000-0000-000000000001'::uuid + and pointer_generation = 1 + from programmable_private.get_projector_release_manifest_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1 + ) + ), + 'projector release manifest returns every exact current immutable component' +); +select ok( + ( + select count(*) = 1 + and min(token) = decode(repeat('71', 20), 'hex') + and min(pool_id) = decode(repeat('73', 32), 'hex') + and min(hook) = decode(repeat('39', 20), 'hex') + from programmable_private.get_projector_dynamic_source_attestations_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1 + ) + ), + 'dynamic-source reader returns only exact asset-bound current attestations' +); +select is( + ( + select pg_catalog.array_agg(decision_id order by candidate_id) + from programmable_private.list_projector_candidate_dispositions_v1( + 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', 1, decode(repeat('aa', 32), 'hex'), + null, null, null, 500, '2026-07-31T03:11:00Z' + ) + where candidate_id in ( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), + decode(repeat('c1', 32), 'hex'), 21 + ), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), + decode(repeat('c2', 32), 'hex'), 21 + ), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('99', 32), 'hex'), + decode(repeat('c3', 32), 'hex'), 21 + ) + ) + ), + array[ + 'a2000000-0000-0000-0000-000000000001'::uuid, + 'a2000000-0000-0000-0000-000000000002'::uuid, + 'a2000000-0000-0000-0000-000000000003'::uuid + ], + 'terminal disposition reader reconstructs exact ordered promotion decision IDs' +); +select programmable_private.open_run( + 'a4000000-0000-0000-0000-000000000001', + 'projection', 1, 'classic-v3', 'classic-v3', 'core', + '91000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('a4', 32), 'hex'), + '2026-07-31T03:06:05.100Z' +); +select is( + ( + select count(*) + from programmable_private.get_projector_launch_baseline_v1( + 'a4000000-0000-0000-0000-000000000001', + decode(repeat('71', 20), 'hex') + ) + ), + 1::bigint, + 'projector launch fold reader returns the exact current launch baseline' +); +select ok( + ( + select count(*) = 1 + and bool_and(token = decode(repeat('71', 20), 'hex')) + and bool_and(pool_projection_id = + 'a2f20000-0000-0000-0000-000000000001'::uuid) + and bool_and(pool_fee_configuration_id = + 'a2f30000-0000-0000-0000-000000000001'::uuid) + from programmable_private.get_projector_pool_baseline_by_id_v1( + 'a4000000-0000-0000-0000-000000000001', + decode(repeat('73', 32), 'hex') + ) + ), + 'fee-only pool baseline resolves one exact release-scoped current pool' +); +select is( + ( + select count(*) + from programmable_private.get_projector_pool_fee_total_v1( + 'a4000000-0000-0000-0000-000000000001', + decode(repeat('73', 32), 'hex'), null + ) + ), + 1::bigint, + 'projector pool-fee fold reader returns the exact current total' +); +select is( + ( + select count(*) + from programmable_private.get_projector_vault_baseline_v1( + 'a4000000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + ), + 1::bigint, + 'projector vault fold reader returns the exact current vault baseline' +); +select is( + ( + select count(*) + from programmable_private.list_projector_vault_allocations_v1( + 'a4000000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex') + ) + ), + 2::bigint, + 'projector allocation fold reader returns the complete ordered allocation set' +); +select is( + ( + select count(*) + from programmable_private.get_projector_account_reward_balance_v1( + 'a4000000-0000-0000-0000-000000000001', + decode(repeat('77', 20), 'hex'), decode(repeat('11', 20), 'hex') + ) + ), + 1::bigint, + 'projector account fold reader returns the exact current reward balance' +); +select programmable_private.append_run_outcome( + 'a4000000-0000-0000-0000-000000000002', + 'a4000000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('a5', 32), 'hex'), + '2026-07-31T03:06:05.200Z' +); +select throws_ok( + $sql$ + select * + from programmable_private.get_projector_launch_baseline_v1( + 'a4000000-0000-0000-0000-000000000001', + decode(repeat('71', 20), 'hex') + ) + $sql$, + '55000', + 'fold readers reject terminal projection runs' +); +select is( + ( + select count(*) + from programmable_private.list_projector_checkpoint_ancestors_v1( + 1, 'classic-v3', 'classic-v3', 'core', 'projector-v1', 100 + ) + ), + 6::bigint, + 'checkpoint ancestors retain every promoted full cursor generation' +); +select programmable_private.append_run_outcome( + '91600000-0000-0000-0000-000000000001', + '93000000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('16', 32), 'hex'), + '2026-07-31T03:06:01Z' +); +select programmable_private.append_run_outcome( + '91610000-0000-0000-0000-000000000001', + '910c0000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('17', 32), 'hex'), + '2026-07-31T03:06:01.010Z' +); + +-- The neutral cursor has one explicit, dual-RPC-attested genesis and advances +-- only through the atomic page commit. The fresh cursor candidates below are +-- deliberately isolated from the earlier inbox fixtures so the coverage +-- arrays prove the exact page boundary without relying on test-side reads of +-- FORCE-RLS tables. +select programmable_private.open_run( + 'a4100000-0000-0000-0000-000000000001', + 'ingestion', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('60', 32), 'hex'), + '2026-07-31T03:06:05.300Z' +); +select programmable_private.append_safe_head_observation( + 'a4100000-0000-0000-0000-000000000002', + 'a4100000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000160', 'hex'), + decode(repeat('61', 32), 'hex'), '2026-07-31T03:06:05.310Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'a4100000-0000-0000-0000-000000000003', + 'a4100000-0000-0000-0000-000000000002', + 'a4100000-0000-0000-0000-000000000001', + 25639600, decode(repeat('99', 32), 'hex'), decode(repeat('99', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000261', 'hex'), + decode(repeat('62', 32), 'hex'), '2026-07-31T03:06:05.320Z' +); +select programmable_private.append_run_outcome( + 'a4100000-0000-0000-0000-000000000004', + 'a4100000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('63', 32), 'hex'), + '2026-07-31T03:06:05.330Z' +); +select programmable_private.register_envio_ingestion_genesis_v1( + 'a4100000-0000-0000-0000-000000000005', + 'a4100000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 'a4100000-0000-0000-0000-000000000003', + decode(repeat('64', 32), 'hex'), '2026-07-31T03:06:05.340Z' +); +select ok( + ( + select generation = 0 + and block_number = 25639600 + and block_hash = decode(repeat('99', 32), 'hex') + and block_global_log_index is null + and candidate_id is null + from programmable_private.get_envio_ingestion_cursor_v1( + 1, '92000000-0000-0000-0000-000000000003', 'canonical-events' + ) + ), + 'registered genesis is exposed as the generation-zero cursor' +); + +select programmable_private.open_run( + 'a4200000-0000-0000-0000-000000000001', + 'ingestion', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('65', 32), 'hex'), + '2026-07-31T03:06:05.400Z' +); +select programmable_private.append_safe_head_observation( + 'a4200000-0000-0000-0000-000000000002', + 'a4200000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000162', 'hex'), + decode(repeat('66', 32), 'hex'), '2026-07-31T03:06:05.410Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'a4200000-0000-0000-0000-000000000003', + 'a4200000-0000-0000-0000-000000000002', + 'a4200000-0000-0000-0000-000000000001', + 25639601, decode(repeat('98', 32), 'hex'), decode(repeat('98', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000263', 'hex'), + decode(repeat('67', 32), 'hex'), '2026-07-31T03:06:05.420Z' +); +select programmable_private.commit_envio_ingestion_page_v1( + 'a4200000-0000-0000-0000-000000000004', + 'a4200000-0000-0000-0000-000000000005', + 'a4200000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 0, 1, 25639601, + array[ + row( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), 31 + )::text, + 25639601, decode(repeat('98', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), 31, 31, + decode(repeat('3d', 20), 'hex'), decode(repeat('3e', 32), 'hex'), + 'CursorTestEvent', array[decode(repeat('3e', 32), 'hex')], + decode('0301', 'hex'), '{}'::jsonb, decode(repeat('d4', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), + decode(repeat('d1', 32), 'hex'), 31 + )::text, + decode(repeat('e5', 32), 'hex'), '2026-07-31T03:06:05.430Z', + 'ClassicVaultFactory' + )::programmable_private.envio_candidate_page_item_v1 + ], + 'a4200000-0000-0000-0000-000000000002', + 'a4200000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('68', 32), 'hex'), + array[decode(repeat('e5', 32), 'hex')], + array[decode(repeat('e5', 32), 'hex')], + decode(repeat('b1', 32), 'hex'), decode(repeat('69', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000564', 'hex'), + decode(repeat('6a', 32), 'hex'), decode(repeat('6b', 32), 'hex'), + '2026-07-31T03:06:05.440Z' +); + +select programmable_private.open_run( + 'a4300000-0000-0000-0000-000000000001', + 'ingestion', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('6c', 32), 'hex'), + '2026-07-31T03:06:05.500Z' +); +select programmable_private.append_safe_head_observation( + 'a4300000-0000-0000-0000-000000000002', + 'a4300000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000165', 'hex'), + decode(repeat('6d', 32), 'hex'), '2026-07-31T03:06:05.510Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'a4300000-0000-0000-0000-000000000003', + 'a4300000-0000-0000-0000-000000000002', + 'a4300000-0000-0000-0000-000000000001', + 25639601, decode(repeat('98', 32), 'hex'), decode(repeat('98', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000266', 'hex'), + decode(repeat('6e', 32), 'hex'), '2026-07-31T03:06:05.520Z' +); +select is( + programmable_private.commit_envio_ingestion_page_v1( + 'a4300000-0000-0000-0000-000000000004', + 'a4300000-0000-0000-0000-000000000005', + 'a4300000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 1, 2, 25639601, + array[ + row( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), + decode(repeat('d2', 32), 'hex'), 31 + )::text, + 25639601, decode(repeat('98', 32), 'hex'), + decode(repeat('d2', 32), 'hex'), 32, 31, + decode(repeat('3d', 20), 'hex'), decode(repeat('3e', 32), 'hex'), + 'CursorTestEvent', array[decode(repeat('3e', 32), 'hex')], + decode('0302', 'hex'), '{}'::jsonb, decode(repeat('d5', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), + decode(repeat('d2', 32), 'hex'), 31 + )::text, + decode(repeat('e6', 32), 'hex'), '2026-07-31T03:06:05.530Z', + 'ClassicVaultFactory' + )::programmable_private.envio_candidate_page_item_v1, + row( + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), + decode(repeat('d3', 32), 'hex'), 31 + )::text, + 25639601, decode(repeat('98', 32), 'hex'), + decode(repeat('d3', 32), 'hex'), 33, 31, + decode(repeat('3d', 20), 'hex'), decode(repeat('3e', 32), 'hex'), + 'CursorTestEvent', array[decode(repeat('3e', 32), 'hex')], + decode('0303', 'hex'), '{}'::jsonb, decode(repeat('d6', 32), 'hex'), + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), + decode(repeat('d3', 32), 'hex'), 31 + )::text, + decode(repeat('e7', 32), 'hex'), '2026-07-31T03:06:05.540Z', + 'ClassicVaultFactory' + )::programmable_private.envio_candidate_page_item_v1 + ], + 'a4300000-0000-0000-0000-000000000002', + 'a4300000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('6f', 32), 'hex'), + array[decode(repeat('e6', 32), 'hex'), decode(repeat('e7', 32), 'hex')], + array[decode(repeat('e6', 32), 'hex'), decode(repeat('e7', 32), 'hex')], + decode(repeat('b2', 32), 'hex'), decode(repeat('70', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000567', 'hex'), + decode(repeat('71', 32), 'hex'), decode(repeat('72', 32), 'hex'), + '2026-07-31T03:06:05.550Z' + ), + 2::bigint, + 'atomic release-neutral page commit advances across candidate-ID tie breaks' +); +select throws_ok( + $sql$ + select programmable_private.advance_envio_ingestion_cursor_v1( + 'a4300000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 1, 2, 25639601, decode(repeat('98', 32), 'hex'), 31, + programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), decode(repeat('d3', 32), 'hex'), 31 + ), + decode(repeat('b3', 32), 'hex'), '2026-07-31T03:06:06.200Z' + ) + $sql$, + '42501', + 'projector cannot bypass atomic log coverage with direct cursor advancement' +); +select is( + ( + select generation::text || ':' || candidate_id + from programmable_private.get_envio_ingestion_cursor_v1( + 1, '92000000-0000-0000-0000-000000000003', 'canonical-events' + ) + ), + '2:' || programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), decode(repeat('d3', 32), 'hex'), 31 + )::text, + 'cursor reader returns the exact generation and full candidate identifier' +); +select is( + ( + select count(*) + from programmable_private.list_envio_ingestion_cursor_ancestors_v1( + 1, '92000000-0000-0000-0000-000000000003', + 'canonical-events', 100 + ) + ), + 2::bigint, + 'release-neutral cursor retains both forward ancestors' +); + +select programmable_private.open_run( + 'a4400000-0000-0000-0000-000000000001', + 'ingestion', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('73', 32), 'hex'), + '2026-07-31T03:06:05.600Z' +); +select programmable_private.append_safe_head_observation( + 'a4400000-0000-0000-0000-000000000002', + 'a4400000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000173', 'hex'), + decode(repeat('74', 32), 'hex'), '2026-07-31T03:06:05.610Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'a4400000-0000-0000-0000-000000000003', + 'a4400000-0000-0000-0000-000000000002', + 'a4400000-0000-0000-0000-000000000001', + 25639602, decode(repeat('97', 32), 'hex'), + decode(repeat('97', 32), 'hex'), 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000274', 'hex'), + decode(repeat('75', 32), 'hex'), '2026-07-31T03:06:05.620Z' +); +select throws_ok( + $sql$ + select programmable_private.commit_envio_ingestion_page_v1( + 'a4400000-0000-0000-0000-000000000004', + 'a4400000-0000-0000-0000-000000000005', + 'a4400000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 2, 3, 25639601, + array[]::programmable_private.envio_candidate_page_item_v1[], + 'a4400000-0000-0000-0000-000000000002', + 'a4400000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('76', 32), 'hex'), + array[decode(repeat('77', 32), 'hex')], array[]::bytea[], + decode(repeat('78', 32), 'hex'), decode(repeat('79', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000575', 'hex'), + decode(repeat('7a', 32), 'hex'), decode(repeat('7b', 32), 'hex'), + '2026-07-31T03:06:05.630Z' + ) + $sql$, + '22023', + 'empty page rejects disagreement between the two RPC log arrays' +); +select throws_ok( + $sql$ + select programmable_private.commit_envio_ingestion_page_v1( + 'a4400000-0000-0000-0000-000000000004', + 'a4400000-0000-0000-0000-000000000005', + 'a4400000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 2, 3, 25639601, + array[]::programmable_private.envio_candidate_page_item_v1[], + 'a4400000-0000-0000-0000-000000000002', + 'a4400000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('76', 32), 'hex'), + array[decode(repeat('77', 32), 'hex')], + array[decode(repeat('77', 32), 'hex')], + decode(repeat('78', 32), 'hex'), decode(repeat('79', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000575', 'hex'), + decode(repeat('7a', 32), 'hex'), decode(repeat('7b', 32), 'hex'), + '2026-07-31T03:06:05.630Z' + ) + $sql$, + '22023', + 'empty page rejects any RPC log even when providers agree' +); +select is( + programmable_private.commit_envio_ingestion_page_v1( + 'a4400000-0000-0000-0000-000000000004', + 'a4400000-0000-0000-0000-000000000005', + 'a4400000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 2, 3, 25639601, + array[]::programmable_private.envio_candidate_page_item_v1[], + 'a4400000-0000-0000-0000-000000000002', + 'a4400000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('76', 32), 'hex'), array[]::bytea[], array[]::bytea[], + decode(repeat('78', 32), 'hex'), decode(repeat('79', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000575', 'hex'), + decode(repeat('7a', 32), 'hex'), decode(repeat('7b', 32), 'hex'), + '2026-07-31T03:06:05.630Z' + ), + 3::bigint, + 'atomic empty page advances with exact same-run block evidence' +); +select is( + programmable_private.commit_envio_ingestion_page_v1( + 'a4400000-0000-0000-0000-000000000004', + 'a4400000-0000-0000-0000-000000000005', + 'a4400000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 2, 3, 25639601, + array[]::programmable_private.envio_candidate_page_item_v1[], + 'a4400000-0000-0000-0000-000000000002', + 'a4400000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('76', 32), 'hex'), array[]::bytea[], array[]::bytea[], + decode(repeat('78', 32), 'hex'), decode(repeat('79', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000575', 'hex'), + decode(repeat('7a', 32), 'hex'), decode(repeat('7b', 32), 'hex'), + '2026-07-31T03:06:05.630Z' + ), + 3::bigint, + 'exact terminal retry returns the already committed next generation' +); +select throws_ok( + $sql$ + select programmable_private.commit_envio_ingestion_page_v1( + 'a4400000-0000-0000-0000-000000000004', + 'a4400000-0000-0000-0000-000000000005', + 'a4400000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 2, 3, 25639601, + array[]::programmable_private.envio_candidate_page_item_v1[], + 'a4400000-0000-0000-0000-000000000002', + 'a4400000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('76', 32), 'hex'), array[]::bytea[], array[]::bytea[], + decode(repeat('7c', 32), 'hex'), decode(repeat('79', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000575', 'hex'), + decode(repeat('7a', 32), 'hex'), decode(repeat('7b', 32), 'hex'), + '2026-07-31T03:06:05.630Z' + ) + $sql$, + '23505', + 'terminal retry with any changed immutable page field fails closed' +); +select ok( + ( + select generation = 3 and block_number = 25639602 + and block_hash = decode(repeat('97', 32), 'hex') + and block_global_log_index is null and candidate_id is null + from programmable_private.get_envio_ingestion_cursor_v1( + 1, '92000000-0000-0000-0000-000000000003', 'canonical-events' + ) + ), + 'empty-page cursor persists the covered block/hash with a NULL log point' +); +select programmable_private.open_run( + 'a4500000-0000-0000-0000-000000000001', + 'ingestion', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('7d', 32), 'hex'), + '2026-07-31T03:06:05.700Z' +); +select programmable_private.append_safe_head_observation( + 'a4500000-0000-0000-0000-000000000002', + 'a4500000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200017d', 'hex'), + decode(repeat('7e', 32), 'hex'), '2026-07-31T03:06:05.710Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'a4500000-0000-0000-0000-000000000003', + 'a4500000-0000-0000-0000-000000000002', + 'a4500000-0000-0000-0000-000000000001', + 25639603, decode(repeat('96', 32), 'hex'), + decode(repeat('96', 32), 'hex'), 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200027e', 'hex'), + decode(repeat('7f', 32), 'hex'), '2026-07-31T03:06:05.720Z' +); +select throws_ok( + $sql$ + select programmable_private.commit_envio_ingestion_page_v1( + 'a4500000-0000-0000-0000-000000000004', + 'a4500000-0000-0000-0000-000000000005', + 'a4500000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 2, 3, 25639602, + array[]::programmable_private.envio_candidate_page_item_v1[], + 'a4500000-0000-0000-0000-000000000002', + 'a4500000-0000-0000-0000-000000000003', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + decode(repeat('80', 32), 'hex'), array[]::bytea[], array[]::bytea[], + decode(repeat('81', 32), 'hex'), decode(repeat('82', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200057f', 'hex'), + decode(repeat('83', 32), 'hex'), decode(repeat('84', 32), 'hex'), + '2026-07-31T03:06:05.730Z' + ) + $sql$, + '40001', + 'stale empty-page CAS cannot advance a newer cursor generation' +); +reset role; +select ok( + not exists ( + select 1 from programmable_private.run_lifecycle_outcomes + where run_id = 'a4500000-0000-0000-0000-000000000001' + ) and not exists ( + select 1 from programmable_private.dual_rpc_log_coverage_evidence + where verification_run_id = 'a4500000-0000-0000-0000-000000000001' + ), + 'failed stale CAS rolls back terminal outcome and coverage evidence atomically' +); +set local role programmable_projector; +select programmable_private.open_run( + 'a5000000-0000-0000-0000-000000000001', + 'rewind', 1, 'envio-control', 'envio-control', 'canonical-events', + '70000000-0000-0000-0000-000000000002', 1, + 'envio-adapter-v1', decode(repeat('b4', 32), 'hex'), + '2026-07-31T03:06:06.300Z' +); +select programmable_private.append_safe_head_observation( + 'a5000000-0000-0000-0000-000000000002', + 'a5000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000002', + 1, 1, 25639620, 25639620, 12, 25639608, + decode(repeat('cc', 32), 'hex'), decode(repeat('cc', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000150', 'hex'), + decode(repeat('b5', 32), 'hex'), '2026-07-31T03:06:06.400Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'a5000000-0000-0000-0000-000000000003', + 'a5000000-0000-0000-0000-000000000002', + 'a5000000-0000-0000-0000-000000000001', + 25639601, decode(repeat('98', 32), 'hex'), decode(repeat('98', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000251', 'hex'), + decode(repeat('b6', 32), 'hex'), '2026-07-31T03:06:06.500Z' +); +select programmable_private.append_run_outcome( + 'a5000000-0000-0000-0000-000000000004', + 'a5000000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('b7', 32), 'hex'), + '2026-07-31T03:06:06.600Z' +); +select is( + programmable_private.rewind_envio_ingestion_cursor_v1( + 'a5000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 3, 4, 1, decode(repeat('b8', 32), 'hex'), + '2026-07-31T03:06:06.700Z' + ), + 4::bigint, + 'dual-RPC evidence permits an exact ancestor rewind' +); +select throws_ok( + $sql$ + select programmable_private.rewind_envio_ingestion_cursor_v1( + 'a5000000-0000-0000-0000-000000000001', + '92000000-0000-0000-0000-000000000003', 'canonical-events', + 3, 4, 1, decode(repeat('b9', 32), 'hex'), + '2026-07-31T03:06:06.800Z' + ) + $sql$, + '40001', + 'neutral rewind rejects a stale cursor generation' +); +select is( + ( + select generation::text || ':' || candidate_id + from programmable_private.get_envio_ingestion_cursor_v1( + 1, '92000000-0000-0000-0000-000000000003', 'canonical-events' + ) + ), + '4:' || programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), decode(repeat('d1', 32), 'hex'), 31 + )::text, + 'rewind restores the exact ancestor candidate while advancing generation' +); +reset role; +select ok( + exists ( + select 1 + from programmable_private.envio_ingestion_cursor_history + where generation = 4 and is_rewind and rewound_from_generation = 3 + and candidate_id = programmable_private.derive_envio_candidate_id( + 1, decode(repeat('98', 32), 'hex'), decode(repeat('d1', 32), 'hex'), 31 + ) + ), + 'rewind history is append-only and records its prior generation' +); +set local role programmable_projector; +select throws_ok( + $sql$ + select programmable_private.append_creator_fee_checkpoint_fact( + '91600000-0000-0000-0000-000000000003', + '93000000-0000-0000-0000-000000000001', + '91240000-0000-0000-0000-000000000002', + decode(repeat('73', 32), 'hex'), 1, 100, 1000, + '2026-07-31T03:06:00.100Z' + ) + $sql$, + '55000', + 'terminal ingestion runs reject creator-fee checkpoint fact replays' +); +select throws_ok( + $sql$ + select programmable_private.append_reward_configuration_activation_fact( + '91600000-0000-0000-0000-000000000004', + '93000000-0000-0000-0000-000000000001', + '91240000-0000-0000-0000-000000000003', + decode(repeat('73', 32), 'hex'), decode(repeat('af', 32), 'hex'), 2, + decode(repeat('a2', 32), 'hex'), decode(repeat('a3', 32), 'hex'), + array[ + decode(repeat('11', 20), 'hex'), decode(repeat('22', 20), 'hex') + ], + array[6000::numeric, 4000::numeric], 1000, + '2026-07-31T03:06:00.200Z' + ) + $sql$, + '55000', + 'terminal ingestion runs reject reward-configuration activation replays' +); +select throws_ok( + $sql$ + select public.reward_test_private_call($call$ + select programmable_private.append_dual_rpc_runtime_code_evidence( + evidence.runtime_code_evidence_id, evidence.verification_run_id, + evidence.source_address, evidence.deployment_block_evidence_id, + evidence.provider_a_id, evidence.provider_b_id, + evidence.runtime_code_hash_a, evidence.runtime_code_hash_b, + evidence.runtime_code_a, evidence.runtime_code_b, + evidence.runtime_code_length_a, evidence.runtime_code_length_b, + evidence.normalized_runtime_code_hash_a, + evidence.normalized_runtime_code_hash_b, + evidence.immutable_references_commitment, + evidence.immutable_values, + evidence.immutable_values_commitment, + evidence.reconstructed_runtime_code, + evidence.reconstructed_runtime_code_hash, + evidence.encoding_version, + evidence.canonical_preimage, + evidence.content_fingerprint, + evidence.evidence_commitment, evidence.verified_at + ) + from programmable_private.dual_rpc_runtime_code_evidence as evidence + where evidence.runtime_code_evidence_id = + '91205000-0000-0000-0000-000000000001' + $call$) + $sql$, + '55000', + 'terminal ingestion runs reject runtime-code evidence replays' +); +select throws_ok( + $sql$ + select public.reward_test_private_call($call$ + select programmable_private.register_dynamic_source_attestation( + attestation.dynamic_source_attestation_id, + attestation.verification_run_id, + attestation.dynamic_source_template_id, + attestation.parent_factory_occurrence_id, + attestation.deployed_source_address, + attestation.deployment_block_number, + attestation.runtime_code_evidence_id, + attestation.deployed_artifact_creation_code_commitment, + attestation.expected_immutable_values_commitment, + attestation.factory_configuration_commitment, + attestation.constructor_arguments_commitment, + attestation.local_init_code_hash, + attestation.runtime_code_hash, + attestation.abi_event_set_commitment, + attestation.encoding_version, + attestation.canonical_preimage, + attestation.content_fingerprint, + attestation.attestation_commitment, attestation.created_at + ) + from programmable_private.dynamic_source_attestations as attestation + where attestation.dynamic_source_attestation_id = + '91210000-0000-0000-0000-000000000001' + $call$) + $sql$, + '55000', + 'terminal ingestion runs reject dynamic-source attestation replays' +); +select throws_ok( + $sql$ + select public.reward_test_private_call($call$ + select programmable_private.append_release_neutral_envio_candidate( + candidate.candidate_id, candidate.first_seen_run_id, + candidate.block_number, candidate.block_hash, + candidate.transaction_hash, candidate.transaction_index, + candidate.block_global_log_index, candidate.source_address, + candidate.event_signature, candidate.event_type, + candidate.ordered_topics, candidate.raw_data, + candidate.decoded_payload, candidate.payload_hash, + candidate.provider_cursor, candidate.provider_deployment_id, + candidate.content_commitment, candidate.first_seen_at + ) + from programmable_private.envio_candidate_inbox as candidate + where candidate.candidate_id = programmable_private.derive_envio_candidate_id(1, decode(repeat('99', 32), 'hex'), decode(repeat('87', 32), 'hex'), 14) + $call$) + $sql$, + '55000', + 'terminal ingestion runs reject release-neutral candidate replays' +); +select throws_ok( + $sql$ + select public.reward_test_private_call($call$ + select programmable_private.resolve_envio_candidate( + resolution.candidate_resolution_id, resolution.resolved_by_run_id, + resolution.candidate_id, resolution.release_binding_id, + resolution.dynamic_source_attestation_id, + resolution.abi_event_set_commitment, + resolution.resolution_commitment, resolution.resolved_at + ) + from programmable_private.envio_candidate_resolutions as resolution + where resolution.candidate_resolution_id = + '91220000-0000-0000-0000-000000000001' + $call$) + $sql$, + '55000', + 'terminal ingestion runs reject candidate-resolution replays' +); +select throws_ok( + $sql$ + select public.reward_test_private_call($call$ + select programmable_private.append_chain_event_occurrence( + occurrence.logical_event_id, occurrence.occurrence_id, + occurrence.verification_run_id, + occurrence.first_seen_neutral_candidate_id, + occurrence.candidate_resolution_id, occurrence.receipt_log_ordinal, + occurrence.block_timestamp, occurrence.decoder_version, + occurrence.abi_event_set_commitment, occurrence.block_evidence_id, + occurrence.encoding_version, occurrence.canonical_preimage, + occurrence.content_fingerprint, occurrence.verified_at + ) + from programmable_private.chain_event_occurrences as occurrence + where occurrence.occurrence_id = + '91240000-0000-0000-0000-000000000001' + $call$) + $sql$, + '55000', + 'terminal ingestion runs reject resolved occurrence replays' +); +reset role; + +set local role programmable_api_reader; +select ok( + exists ( + select 1 + from programmable_private.route_snapshot_readiness_v1 + where route_key = 'explore-list' + and route_status = 'eligible' + and route_mode = 'indexed' + and parity_status = 'missing' + and checkpoint_confirmations >= 0 + ), + 'route readiness exposes only an exact current checkpoint and measured confirmations' +); +reset role; +set local role programmable_migrator; +update programmable_private.route_eligibility_current as route +set checkpoint_id = ( + select checkpoint.checkpoint_id + from programmable_private.projector_checkpoints as checkpoint + where checkpoint.chain_id = route.chain_id + and checkpoint.release_id = route.release_id + and checkpoint.model_id = route.model_id + and checkpoint.source_group = route.source_group + and checkpoint.checkpoint_id <> route.checkpoint_id + order by checkpoint.checkpoint_generation + limit 1 +) +where route.route_key = 'explore-list' + and route.chain_id = 1 + and route.release_id = 'classic-v3' + and route.model_id = 'classic-v3' + and route.source_group = 'core'; +reset role; +set local role programmable_api_reader; +select ok( + not exists ( + select 1 + from programmable_private.route_snapshot_readiness_v1 + where route_key = 'explore-list' + and chain_id = 1 and release_id = 'classic-v3' + ), + 'readiness hides a route whose checkpoint ID is no longer exact-current' +); +select is( + ( + select count(*) + from programmable_private.get_recent_launches_v1( + 1, 100, null, null, null + ) + ), + 0::bigint, + 'published route DTO view fails closed with the same stale checkpoint' +); +reset role; + +select ok( + exists ( + select 1 + from programmable_private.launch_position_liquidity_facts as position + join programmable_private.launch_projections as launch + on launch.launch_projection_id = position.launch_projection_id + where launch.chain_id = 1 + and launch.release_id = 'classic-v3' + and launch.model_id = 'classic-v3' + and launch.promoted_block_number > 25639599 + ) + and exists ( + select 1 + from programmable_private.reward_vault_projections as current_snapshot + join programmable_private.reward_vault_projections as baseline_snapshot + on baseline_snapshot.reward_vault_projection_id = + current_snapshot.baseline_reward_vault_projection_id + where current_snapshot.chain_id = 1 + and current_snapshot.release_id = 'classic-v3' + and current_snapshot.model_id = 'classic-v3' + and current_snapshot.snapshot_kind = 'exact_current' + and baseline_snapshot.snapshot_kind in ('initial_seed', 'exact_current') + ), + 'reorg cleanup fixture contains launch liquidity and a reward snapshot chain' +); + +select ok( + exists ( + select 1 + from programmable_private.launch_projections as launch + join programmable_private.launch_position_liquidity_facts as position + on position.launch_projection_id = launch.launch_projection_id + join programmable_private.launch_projection_occurrence_roles as role + on role.launch_projection_id = launch.launch_projection_id + join programmable_private.launch_projection_conditions as condition + on condition.launch_projection_id = launch.launch_projection_id + where launch.projection_run_id = + '97000000-0000-0000-0000-000000000001' + ) + and exists ( + select 1 + from programmable_private.launch_projections as launch + join programmable_private.launch_projection_occurrence_roles as role + on role.launch_projection_id = launch.launch_projection_id + join programmable_private.launch_projection_conditions as condition + on condition.launch_projection_id = launch.launch_projection_id + where launch.projection_run_id in ( + '97000000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000005' + ) + ), + 'mid-block replay fixture contains earlier and later published launch graphs' +); + +set local role programmable_migrator; +select lives_ok( + $sql$ + select programmable_private.delete_projector_projection_replay_scope_v1( + 1, 'classic-v3', 'classic-v3', 25639600, + pg_catalog.decode(pg_catalog.repeat('99', 32), 'hex'), 13 + ) + $sql$, + 'projection replay cleanup accepts an exact legacy mid-block ancestor' +); +reset role; + +select ok( + exists ( + select 1 + from programmable_private.launch_projections as launch + join programmable_private.launch_position_liquidity_facts as position + on position.launch_projection_id = launch.launch_projection_id + join programmable_private.launch_projection_occurrence_roles as role + on role.launch_projection_id = launch.launch_projection_id + join programmable_private.launch_projection_conditions as condition + on condition.launch_projection_id = launch.launch_projection_id + where launch.projection_run_id = + '97000000-0000-0000-0000-000000000001' + ) + and not exists ( + select 1 + from programmable_private.launch_projections + where projection_run_id in ( + '97000000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000005' + ) + ) + and not exists ( + select 1 + from programmable_private.launch_projection_occurrence_roles + where projection_run_id in ( + '97000000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000005' + ) + ) + and not exists ( + select 1 + from programmable_private.launch_projection_conditions + where projection_run_id in ( + '97000000-0000-0000-0000-000000000002', + '97000000-0000-0000-0000-000000000005' + ) + ), + 'mid-block cleanup preserves the ancestor launch graph and removes later runs' +); + +set local role programmable_migrator; +select lives_ok( + $sql$ + select programmable_private.delete_projector_projection_replay_scope_v1( + 1, 'classic-v3', 'classic-v3', 25639599, + pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 0 + ) + $sql$, + 'projection replay cleanup removes populated FK graphs in dependency order' +); +reset role; + +select ok( + not exists ( + select 1 + from programmable_private.launch_position_liquidity_facts + where chain_id = 1 and release_id = 'classic-v3' + and model_id = 'classic-v3' + ) + and not exists ( + select 1 + from programmable_private.reward_vault_projections + where chain_id = 1 and release_id = 'classic-v3' + and model_id = 'classic-v3' + and promoted_block_number > 25639599 + ) + and not exists ( + select 1 + from programmable_private.launch_projections + where chain_id = 1 and release_id = 'classic-v3' + and model_id = 'classic-v3' + and promoted_block_number > 25639599 + ), + 'replay cleanup leaves no invalid launch-liquidity or reward snapshot rows' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/006_provider_join_parity_retention.test.sql b/supabase/tests/database/006_provider_join_parity_retention.test.sql new file mode 100644 index 00000000..e031d730 --- /dev/null +++ b/supabase/tests/database/006_provider_join_parity_retention.test.sql @@ -0,0 +1,1012 @@ +begin; + +create function public.chainlink_latest_round_data_fixture() +returns bytea +language sql +immutable +security invoker +set search_path = '' +as $function$ + select pg_catalog.decode( + '000000000000000000000000000000000000000000000000000000000000002a' + || '00000000000000000000000000000000000000000000000000000045d964b800' + || '0000000000000000000000000000000000000000000000000000000069570a80' + || '0000000000000000000000000000000000000000000000000000000069570bac' + || '000000000000000000000000000000000000000000000000000000000000002a', + 'hex' + ) +$function$; + +set local role programmable_projector; + +select programmable_private.create_release_epoch( + 'a6000000-0000-0000-0000-000000000001', + 1, 'classic-v3', 'classic-v3', 'core', 1, + decode(repeat('10', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + decode(repeat('12', 32), 'hex'), + '2026-01-01T00:00:00Z' +); +select programmable_private.activate_release_epoch( + 1, 'classic-v3', 'classic-v3', 'core', + 'a6000000-0000-0000-0000-000000000001', + 0, 1, decode(repeat('13', 32), 'hex'), + '2026-01-01T00:00:01Z' +); +select programmable_private.register_rpc_provider_deployment( + 'b6000000-0000-0000-0000-000000000001', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('a3', 32), 'hex'), + decode(repeat('21', 32), 'hex'), decode(repeat('22', 32), 'hex'), + decode(repeat('23', 32), 'hex'), '2026-01-01T00:00:02Z' +); +select programmable_private.register_rpc_provider_deployment( + 'b6000000-0000-0000-0000-000000000002', + 1, 'quicknode', 'rpc-provider-v1', + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('b3', 32), 'hex'), + decode(repeat('24', 32), 'hex'), decode(repeat('25', 32), 'hex'), + decode(repeat('26', 32), 'hex'), '2026-01-01T00:00:03Z' +); +select programmable_private.register_provider_deployment( + 'b6000000-0000-0000-0000-000000000003', + 'uniswap_subgraph', 'market-subgraph', + decode(repeat('27', 32), 'hex'), decode(repeat('28', 32), 'hex'), + decode(repeat('29', 32), 'hex'), '2026-01-01T00:00:04Z' +); +select programmable_private.open_run( + 'c6000000-0000-0000-0000-000000000001', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + 'a6000000-0000-0000-0000-000000000001', 1, + 'projector-v1', decode(repeat('31', 32), 'hex'), + '2026-01-01T00:01:00Z' +); + +select plan(62); + +select throws_ok( + $sql$ + select programmable_private.register_provider_deployment( + 'b6000000-0000-0000-0000-0000000000ff', + 'rpc_provider', 'generic-rpc-bypass', + decode(repeat('f1', 32), 'hex'), decode(repeat('f2', 32), 'hex'), + decode(repeat('f3', 32), 'hex'), '2026-01-01T00:00:05Z' + ) + $sql$, + '42501', + 'generic provider registration cannot bypass RPC deployment metadata' +); + +select throws_ok( + $sql$ + select programmable_private.register_rpc_provider_deployment( + 'b6000000-0000-0000-0000-0000000000fe', + 10, 'alchemy', 'rpc-provider-v1', + decode(repeat('d1', 32), 'hex'), decode(repeat('d2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('d3', 32), 'hex'), + decode(repeat('d4', 32), 'hex'), decode(repeat('d5', 32), 'hex'), + decode(repeat('d6', 32), 'hex'), '2026-01-01T00:00:06Z' + ) + $sql$, + '22023', + 'specialized RPC registration is Ethereum mainnet only' +); + +select throws_ok( + $sql$ + select programmable_private.register_rpc_provider_deployment( + 'b6000000-0000-0000-0000-0000000000fd', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('00', 32), 'hex'), decode(repeat('e2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('e3', 32), 'hex'), + decode(repeat('e4', 32), 'hex'), decode(repeat('e5', 32), 'hex'), + decode(repeat('e6', 32), 'hex'), '2026-01-01T00:00:07Z' + ) + $sql$, + '22023', + 'zero RPC endpoint commitments are rejected' +); + +reset role; + +select ok( + exists ( + select 1 + from programmable_private.rpc_provider_deployment_metadata as alchemy + join programmable_private.rpc_provider_deployment_metadata as quicknode + on quicknode.provider_deployment_id = + 'b6000000-0000-0000-0000-000000000002'::uuid + where alchemy.provider_deployment_id = + 'b6000000-0000-0000-0000-000000000001'::uuid + and alchemy.chain_id = 1 + and alchemy.vendor = 'alchemy' + and alchemy.vendor_order = 1 + and quicknode.chain_id = 1 + and quicknode.vendor = 'quicknode' + and quicknode.vendor_order = 2 + and alchemy.constructor_version = 'rpc-provider-v1' + and quicknode.constructor_version = 'rpc-provider-v1' + and alchemy.endpoint_url_commitment = decode(repeat('a1', 32), 'hex') + and alchemy.endpoint_origin_commitment = decode(repeat('a2', 32), 'hex') + and alchemy.endpoint_evidence_domain = 'rpc-endpoint-commitments-v1' + and alchemy.endpoint_evidence_commitment = decode(repeat('a3', 32), 'hex') + ), + 'RPC deployment metadata stores ordered vendors and commitment-only endpoint evidence' +); + +set local role programmable_projector; + +select is( + ( + select state.pointer_generation + from programmable_private.get_projector_runtime_state_v1( + 1, 'classic-v3', 'classic-v3', 'core', 'projector-v1', + array['rpc_provider', 'rpc_provider', 'uniswap_subgraph']::text[], + array['rpc:1:alchemy', 'rpc:1:quicknode', 'market-subgraph']::text[], + array[ + decode(repeat('21', 32), 'hex'), + decode(repeat('24', 32), 'hex'), + decode(repeat('27', 32), 'hex') + ], + array[ + decode(repeat('22', 32), 'hex'), + decode(repeat('25', 32), 'hex'), + decode(repeat('28', 32), 'hex') + ] + ) as state + ), + 1::bigint, + 'stateless projector reads the exact current epoch generation' +); +select ok( + ( + select state.lease_generation = 0 + and state.checkpoint_generation = 0 + and state.reorg_generation = 0 + and state.checkpoint_id is null + and state.provider_redacted_identities = + array['rpc:1:alchemy', 'rpc:1:quicknode', 'market-subgraph']::text[] + from programmable_private.get_projector_runtime_state_v1( + 1, 'classic-v3', 'classic-v3', 'core', 'projector-v1', + array['rpc_provider', 'rpc_provider', 'uniswap_subgraph']::text[], + array['rpc:1:alchemy', 'rpc:1:quicknode', 'market-subgraph']::text[], + array[ + decode(repeat('21', 32), 'hex'), + decode(repeat('24', 32), 'hex'), + decode(repeat('27', 32), 'hex') + ], + array[ + decode(repeat('22', 32), 'hex'), + decode(repeat('25', 32), 'hex'), + decode(repeat('28', 32), 'hex') + ] + ) as state + ), + 'runtime state returns deterministic zero CAS inputs before lease/checkpoint creation' +); +select throws_ok( + $sql$ + select * + from programmable_private.get_projector_runtime_state_v1( + 1, 'classic-v3', 'classic-v3', 'core', 'projector-v1', + array['rpc_provider']::text[], array['rpc:1:alchemy']::text[], + array[decode(repeat('ff', 32), 'hex')], + array[decode(repeat('22', 32), 'hex')] + ) + $sql$, + '23503', + 'runtime state rejects a drifted provider deployment commitment' +); + +select throws_ok( + $sql$ + select programmable_private.register_rpc_provider_deployment( + 'b6000000-0000-0000-0000-000000000004', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('c1', 32), 'hex'), decode(repeat('c2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('c3', 32), 'hex'), + decode(repeat('2a', 32), 'hex'), decode(repeat('2b', 32), 'hex'), + decode(repeat('2c', 32), 'hex'), '2026-01-01T00:01:01Z' + ) + $sql$, + '23505', + 'duplicate chain and RPC vendor registration fails closed' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-000000000001', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 1, 1, 100, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000101', 'hex'), + decode(repeat('41', 32), 'hex'), '2026-01-01T00:01:02Z' + ) + $sql$, + '22023', + 'the two RPC deployments must differ' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-0000000000ff', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 'b6000000-0000-0000-0000-000000000001', + 1, 1, 100, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76320001ff', 'hex'), + decode(repeat('40', 32), 'hex'), '2026-01-01T00:01:02.500Z' + ) + $sql$, + '22023', + 'safe-head evidence enforces Alchemy first and QuickNode second' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-000000000002', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 1, 10, 100, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000102', 'hex'), + decode(repeat('42', 32), 'hex'), '2026-01-01T00:01:03Z' + ) + $sql$, + '22023', + 'either wrong reported chain ID is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-000000000003', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 1, 1, 11, 100, 12, 0, + decode(repeat('00', 32), 'hex'), decode(repeat('00', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000103', 'hex'), + decode(repeat('43', 32), 'hex'), '2026-01-01T00:01:04Z' + ) + $sql$, + '22023', + 'a provider head below finality depth is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-000000000004', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 1, 1, 100, 100, 12, 89, + decode(repeat('89', 32), 'hex'), decode(repeat('89', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000104', 'hex'), + decode(repeat('44', 32), 'hex'), '2026-01-01T00:01:05Z' + ) + $sql$, + '22023', + 'safe block must equal least heads minus twelve exactly' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-000000000005', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 1, 1, 100, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('89', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000105', 'hex'), + decode(repeat('45', 32), 'hex'), '2026-01-01T00:01:06Z' + ) + $sql$, + '22023', + 'unequal safe-block hashes are rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-000000000006', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 1, 1, 100.1, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000106', 'hex'), + decode(repeat('46', 32), 'hex'), '2026-01-01T00:01:07Z' + ) + $sql$, + '22023', + 'fractional RPC heads are rejected before assignment' +); +select is( + programmable_private.append_safe_head_observation( + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 1, 1, 100, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000107', 'hex'), + decode(repeat('47', 32), 'hex'), '2026-01-01T00:01:08Z' + ), + 'd6000000-0000-0000-0000-000000000007'::uuid, + 'a valid dual-RPC safe head is accepted' +); +select throws_ok( + $sql$ + select programmable_private.append_dual_rpc_block_evidence( + 'e6000000-0000-0000-0000-000000000001', + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 89, decode(repeat('89', 32), 'hex'), decode(repeat('89', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000208', 'hex'), + decode(repeat('48', 32), 'hex'), '2026-01-01T00:01:09Z' + ) + $sql$, + '22023', + 'per-block evidence above the accepted safe head is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_dual_rpc_block_evidence( + 'e6000000-0000-0000-0000-000000000002', + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 88, decode(repeat('88', 32), 'hex'), decode(repeat('87', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000209', 'hex'), + decode(repeat('49', 32), 'hex'), '2026-01-01T00:01:10Z' + ) + $sql$, + '22023', + 'per-block provider hashes must agree' +); +select is( + programmable_private.append_dual_rpc_block_evidence( + 'e6000000-0000-0000-0000-000000000003', + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 88, decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200020a', 'hex'), + decode(repeat('4a', 32), 'hex'), '2026-01-01T00:01:11Z' + ), + 'e6000000-0000-0000-0000-000000000003'::uuid, + 'target-at-safe block evidence is accepted' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e6000000-0000-0000-0000-000000000004', + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 80, decode(repeat('62', 32), 'hex'), decode(repeat('62', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200020b', 'hex'), + decode(repeat('4c', 32), 'hex'), '2026-01-01T00:01:12Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e6000000-0000-0000-0000-000000000005', + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 81, decode(repeat('64', 32), 'hex'), decode(repeat('64', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200020c', 'hex'), + decode(repeat('4d', 32), 'hex'), '2026-01-01T00:01:13Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e6000000-0000-0000-0000-000000000006', + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 82, decode(repeat('69', 32), 'hex'), decode(repeat('69', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200020d', 'hex'), + decode(repeat('4e', 32), 'hex'), '2026-01-01T00:01:14Z' +); +select programmable_private.append_dual_rpc_block_evidence( + 'e6000000-0000-0000-0000-000000000007', + 'd6000000-0000-0000-0000-000000000007', + 'c6000000-0000-0000-0000-000000000001', + 83, decode(repeat('6b', 32), 'hex'), decode(repeat('6b', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a763200020e', 'hex'), + decode(repeat('4f', 32), 'hex'), '2026-01-01T00:01:15Z' +); + +select programmable_private.append_run_telemetry( + 'f6000000-0000-0000-0000-000000000001', + 'c6000000-0000-0000-0000-000000000001', + 'success-old', '2026-06-01T00:00:00Z', + 10, 1, '{"sample":"success"}'::jsonb, false +); +select programmable_private.append_run_telemetry( + 'f6000000-0000-0000-0000-000000000002', + 'c6000000-0000-0000-0000-000000000001', + 'failed-old', '2026-01-01T00:02:00Z', + 20, 2, '{"sample":"failed-old"}'::jsonb, true +); +select programmable_private.append_run_telemetry( + 'f6000000-0000-0000-0000-000000000003', + 'c6000000-0000-0000-0000-000000000001', + 'failed-recent', '2026-07-01T00:00:00Z', + 30, 3, '{"sample":"failed-recent"}'::jsonb, true +); +select programmable_private.append_run_outcome( + 'f6100000-0000-0000-0000-000000000001', + 'c6000000-0000-0000-0000-000000000001', + 'succeeded', decode(repeat('4b', 32), 'hex'), + '2026-07-30T00:00:00Z' +); +select throws_ok( + $sql$ + select programmable_private.append_run_telemetry( + 'f6000000-0000-0000-0000-000000000004', + 'c6000000-0000-0000-0000-000000000001', + 'after-terminal', '2026-07-30T00:01:00Z', + 1, 1, '{}'::jsonb, false + ) + $sql$, + '55000', + 'terminal runs reject later telemetry' +); + +reset role; + +select throws_ok( + $$update programmable_private.fingerprint_encoding_versions + set write_enabled = false + where fingerprint_domain = 'occurrence' and encoding_version = 1$$, + '55000', + 'fingerprint encoding definitions cannot be rewritten' +); +select throws_ok( + $$delete from programmable_private.fingerprint_encoding_versions + where fingerprint_domain = 'occurrence' and encoding_version = 1$$, + '55000', + 'old fingerprint encoding versions cannot be deleted' +); +select throws_ok( + $$update programmable_private.release_epochs set epoch_number = 2 + where epoch_id = 'a6000000-0000-0000-0000-000000000001'$$, + '55000', + 'release epochs cannot be updated' +); +select throws_ok( + $$delete from programmable_private.release_epochs + where epoch_id = 'a6000000-0000-0000-0000-000000000001'$$, + '55000', + 'release epochs cannot be deleted' +); +select throws_ok( + $$update programmable_private.run_headers set worker_version = 'changed' + where run_id = 'c6000000-0000-0000-0000-000000000001'$$, + '55000', + 'run headers are immutable' +); +select throws_ok( + $$delete from programmable_private.run_lifecycle_outcomes + where run_id = 'c6000000-0000-0000-0000-000000000001'$$, + '55000', + 'terminal outcomes are retained immutably' +); +select ok( + not exists ( + select 1 + from pg_catalog.pg_constraint as constraint_row + join pg_catalog.pg_class as table_row + on table_row.oid = constraint_row.conrelid + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = table_row.relnamespace + where namespace_row.nspname = 'programmable_private' + and constraint_row.contype = 'f' + and constraint_row.confdeltype not in ('a', 'r') + ), + 'private provenance foreign keys never cascade or set null' +); +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as function_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = function_row.pronamespace + where namespace_row.nspname = 'programmable_private' + and function_row.proname ~* '(digest|keccak|sha3|canonicalize)' + ), + 'Postgres does not recompute codec preimages or cryptographic digests' +); + +set local role programmable_reconciler; + +select programmable_private.open_run( + 'c6000000-0000-0000-0000-000000000002', + 'reconciliation', 1, 'classic-v3', 'classic-v3', 'core', + 'a6000000-0000-0000-0000-000000000001', 1, + 'reconciler-v1', decode(repeat('51', 32), 'hex'), + '2026-01-02T00:00:00Z' +); +select programmable_private.append_reconciliation_record( + 'aa000000-0000-0000-0000-000000000001', + 'c6000000-0000-0000-0000-000000000002', + 'route-match', 'info', 0, 88, 10, 0, + decode(repeat('52', 32), 'hex'), array[]::bytea[], + null, '2026-01-02T00:01:00Z' +); +select programmable_private.append_reconciliation_record( + 'aa000000-0000-0000-0000-000000000002', + 'c6000000-0000-0000-0000-000000000002', + 'route-mismatch', 'warning', 0, 88, 10, 1, + decode(repeat('53', 32), 'hex'), + array[decode(repeat('54', 32), 'hex')], + '2026-01-03T00:00:00Z', '2026-01-02T00:02:00Z' +); +select throws_ok( + $sql$ + select programmable_private.append_global_eth_usd_snapshot_v1( + 'aa100000-0000-0000-0000-000000000001', + 'aa000000-0000-0000-0000-000000000001', + 'e6000000-0000-0000-0000-000000000003', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 43, 300000000000, 8::smallint, '2026-01-02T00:05:00Z', + public.chainlink_latest_round_data_fixture(), + public.chainlink_latest_round_data_fixture(), + decode(repeat('91', 32), 'hex'), decode(repeat('92', 32), 'hex'), + '2026-01-02T00:35:00Z' + ) + $sql$, + '23514', + 'ETH/USD writer rejects a caller-supplied round that differs from raw latestRoundData' +); +select throws_ok( + $sql$ + select programmable_private.append_global_eth_usd_snapshot_v1( + 'aa100000-0000-0000-0000-000000000002', + 'aa000000-0000-0000-0000-000000000001', + 'e6000000-0000-0000-0000-000000000003', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 42, 300000000001, 8::smallint, '2026-01-02T00:05:00Z', + public.chainlink_latest_round_data_fixture(), + public.chainlink_latest_round_data_fixture(), + decode(repeat('93', 32), 'hex'), decode(repeat('94', 32), 'hex'), + '2026-01-02T00:35:00Z' + ) + $sql$, + '23514', + 'ETH/USD writer rejects an arbitrary denormalized answer' +); +select throws_ok( + $sql$ + select programmable_private.append_global_eth_usd_snapshot_v1( + 'aa100000-0000-0000-0000-000000000003', + 'aa000000-0000-0000-0000-000000000001', + 'e6000000-0000-0000-0000-000000000003', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 42, 300000000000, 18::smallint, '2026-01-02T00:05:00Z', + public.chainlink_latest_round_data_fixture(), + public.chainlink_latest_round_data_fixture(), + decode(repeat('95', 32), 'hex'), decode(repeat('96', 32), 'hex'), + '2026-01-02T00:35:00Z' + ) + $sql$, + '23514', + 'ETH/USD writer fixes mainnet feed decimals at eight' +); +select throws_ok( + $sql$ + select programmable_private.append_global_eth_usd_snapshot_v1( + 'aa100000-0000-0000-0000-000000000004', + 'aa000000-0000-0000-0000-000000000001', + 'e6000000-0000-0000-0000-000000000003', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 42, 300000000000, 8::smallint, '2026-01-02T00:05:00Z', + public.chainlink_latest_round_data_fixture(), + public.chainlink_latest_round_data_fixture(), + decode(repeat('97', 32), 'hex'), decode(repeat('98', 32), 'hex'), + '2026-01-02T01:05:01Z' + ) + $sql$, + '23514', + 'ETH/USD writer rejects latestRoundData older than the one-hour ceiling' +); +select is( + programmable_private.append_global_eth_usd_snapshot_v1( + 'aa100000-0000-0000-0000-000000000005', + 'aa000000-0000-0000-0000-000000000001', + 'e6000000-0000-0000-0000-000000000003', + 'b6000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000002', + 42, 300000000000, 8::smallint, '2026-01-02T00:05:00Z', + public.chainlink_latest_round_data_fixture(), + public.chainlink_latest_round_data_fixture(), + decode(repeat('99', 32), 'hex'), decode(repeat('9a', 32), 'hex'), + '2026-01-02T00:35:00Z' + ), + 'aa100000-0000-0000-0000-000000000005'::uuid, + 'exact raw latestRoundData persists after full ABI and freshness validation' +); +reset role; +select ok( + exists ( + select 1 + from programmable_private.global_eth_usd_snapshots + where global_market_snapshot_id = + 'aa100000-0000-0000-0000-000000000005' + and feed_round_id = 42 and answer = 300000000000 + and decimals = 8 and rpc_decoding_version = 1 + and feed_started_at = '2026-01-02T00:00:00Z' + and feed_updated_at = '2026-01-02T00:05:00Z' + and feed_answered_in_round = 42 + ), + 'decoded Chainlink round fields are retained exactly for later audit replay' +); +set local role programmable_reconciler; +select programmable_private.append_parity_record( + 'ab000000-0000-0000-0000-000000000001', + 'aa000000-0000-0000-0000-000000000001', + 'explore-list', decode(repeat('55', 32), 'hex'), + decode(repeat('55', 32), 'hex'), + '2026-01-02T00:03:00Z', null +); +select programmable_private.append_parity_record( + 'ab000000-0000-0000-0000-000000000002', + 'aa000000-0000-0000-0000-000000000002', + 'launch-detail', decode(repeat('56', 32), 'hex'), + decode(repeat('57', 32), 'hex'), + '2026-01-02T00:04:00Z', '2026-01-03T00:00:00Z' +); +select throws_ok( + $sql$ + select programmable_private.append_parity_record( + 'ab000000-0000-0000-0000-000000000003', + 'aa000000-0000-0000-0000-000000000001', + 'invalid-match', decode(repeat('58', 32), 'hex'), + decode(repeat('58', 32), 'hex'), + '2026-01-02T00:05:00Z', '2026-01-03T00:00:00Z' + ) + $sql$, + '22023', + 'matching parity evidence cannot claim a resolution timestamp' +); +select throws_ok( + $sql$ + select programmable_private.append_reconciliation_record( + 'aa000000-0000-0000-0000-000000000003', + 'c6000000-0000-0000-0000-000000000002', + 'fractional-range', 'warning', 0.1, 88, 1, 0, + decode(repeat('59', 32), 'hex'), array[]::bytea[], + null, '2026-01-02T00:06:00Z' + ) + $sql$, + '22023', + 'fractional reconciliation block boundaries are rejected' +); +select programmable_private.append_market_snapshot( + 'ac000000-0000-0000-0000-000000000001', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000004', + decode(repeat('61', 32), 'hex'), 80, decode(repeat('62', 32), 'hex'), + 1000, 2000, 1.25, 2.5, 3.75, null, + '2026-07-20T00:00:00Z', decode(repeat('63', 32), 'hex') +); +select programmable_private.append_market_snapshot( + 'ac000000-0000-0000-0000-000000000002', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000005', + decode(repeat('61', 32), 'hex'), 81, decode(repeat('64', 32), 'hex'), + 1001, 2001, 2.25, 3.5, 4.75, 100, + '2026-07-30T00:00:00Z', decode(repeat('65', 32), 'hex') +); +select is( + programmable_private.append_market_snapshot( + 'ac000000-0000-0000-0000-000000000001', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000004', + decode(repeat('61', 32), 'hex'), 80, decode(repeat('62', 32), 'hex'), + 1000, 2000, 1.25, 2.5, 3.75, null, + '2026-07-20T00:00:00Z', decode(repeat('63', 32), 'hex') + ), + 'ac000000-0000-0000-0000-000000000001'::uuid, + 'exact market snapshot replay is idempotent' +); +select throws_ok( + $sql$ + select programmable_private.append_market_snapshot( + 'ac000000-0000-0000-0000-000000000001', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000004', + decode(repeat('61', 32), 'hex'), 80, decode(repeat('62', 32), 'hex'), + 1000, 2000, 1.25, 2.5, 3.75, null, + '2026-07-20T00:00:00Z', decode(repeat('66', 32), 'hex') + ) + $sql$, + '23505', + 'market replay with a changed audit commitment is rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_market_snapshot( + 'ac000000-0000-0000-0000-000000000003', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000005', + decode(repeat('61', 32), 'hex'), 82, decode(repeat('67', 32), 'hex'), + 1000, 2000, 1, 2, 3, 0.1, + '2026-07-30T00:01:00Z', decode(repeat('68', 32), 'hex') + ) + $sql$, + '22003', + 'fractional hook volume is rejected as uint256' +); +select throws_ok( + $sql$ + select programmable_private.append_market_snapshot( + 'ac000000-0000-0000-0000-000000000004', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000004', + decode(repeat('61', 32), 'hex'), 81, decode(repeat('64', 32), 'hex'), + 1000, 2000, 1, 2, 3, 4, + '2026-07-30T00:02:00Z', decode(repeat('6f', 32), 'hex') + ) + $sql$, + '23514', + 'market snapshot block number and hash must match the exact dual-RPC evidence' +); +select programmable_private.append_market_candle( + 'ad000000-0000-0000-0000-000000000001', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000006', + decode(repeat('61', 32), 'hex'), 'hour', + '2026-01-10T00:00:00Z', '2026-01-10T01:00:00Z', + 10, 12, 9, 11, 100, 200, 300, + decode(repeat('69', 32), 'hex'), decode(repeat('6a', 32), 'hex') +); +select programmable_private.append_market_candle( + 'ad000000-0000-0000-0000-000000000002', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000007', + decode(repeat('61', 32), 'hex'), 'day', + '2026-01-10T00:00:00Z', '2026-01-11T00:00:00Z', + 10, 12, 9, 11, 100, 200, 300, + decode(repeat('6b', 32), 'hex'), decode(repeat('6c', 32), 'hex') +); +select throws_ok( + $sql$ + select programmable_private.append_market_candle( + 'ad000000-0000-0000-0000-000000000003', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000003', + 'e6000000-0000-0000-0000-000000000006', + decode(repeat('61', 32), 'hex'), 'hour', + '2026-07-30T00:00:00Z', '2026-07-30T01:00:00Z', + 10, 'NaN'::numeric, 9, 11, 100, 200, 300, + decode(repeat('69', 32), 'hex'), decode(repeat('6e', 32), 'hex') + ) + $sql$, + '22023', + 'non-finite market values are rejected' +); +select throws_ok( + $sql$ + select programmable_private.append_market_candle( + 'ad000000-0000-0000-0000-000000000004', + 'aa000000-0000-0000-0000-000000000001', + 'b6000000-0000-0000-0000-000000000001', + 'e6000000-0000-0000-0000-000000000006', + decode(repeat('61', 32), 'hex'), 'hour', + '2026-07-30T00:00:00Z', '2026-07-30T01:00:00Z', + 10, 12, 9, 11, 100, 200, 300, + decode(repeat('69', 32), 'hex'), decode(repeat('70', 32), 'hex') + ) + $sql$, + '23503', + 'market candles reject an RPC deployment in place of the immutable subgraph source' +); +select programmable_private.append_dependency_health( + 'ae000000-0000-0000-0000-000000000001', + 'c6000000-0000-0000-0000-000000000002', + 'rpc-a', 'frozen', 3, '2026-07-30T00:09:00Z', null, + decode(repeat('6d', 32), 'hex') +); +select programmable_private.append_run_outcome( + 'f6100000-0000-0000-0000-000000000002', + 'c6000000-0000-0000-0000-000000000002', + 'succeeded', decode(repeat('6f', 32), 'hex'), + '2026-07-30T00:10:00Z' +); +select throws_ok( + $sql$ + select programmable_private.append_parity_record( + 'ab000000-0000-0000-0000-000000000004', + 'aa000000-0000-0000-0000-000000000001', + 'after-terminal', decode(repeat('70', 32), 'hex'), + decode(repeat('71', 32), 'hex'), + '2026-07-30T00:11:00Z', null + ) + $sql$, + '55000', + 'terminal reconciliation runs reject new parity evidence' +); +select throws_ok( + $sql$ + select programmable_private.append_dependency_health( + 'ae000000-0000-0000-0000-000000000001', + 'c6000000-0000-0000-0000-000000000002', + 'rpc-a', 'frozen', 3, '2026-07-30T00:09:00Z', null, + decode(repeat('6d', 32), 'hex') + ) + $sql$, + '55000', + 'terminal reconciliation runs reject dependency-health evidence replays' +); + +reset role; + +select throws_ok( + $$update programmable_private.market_snapshots + set market_volume_token0 = 99 + where market_snapshot_id = 'ac000000-0000-0000-0000-000000000001'$$, + '55000', + 'prunable market facts remain update-immutable' +); +select ok( + has_function_privilege( + 'programmable_reconciler', + 'programmable_private.append_market_snapshot(uuid,uuid,uuid,uuid,bytea,numeric,bytea,numeric,numeric,numeric,numeric,numeric,numeric,timestamp with time zone,bytea)', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_api_reader', + 'programmable_private.append_market_snapshot(uuid,uuid,uuid,uuid,bytea,numeric,bytea,numeric,numeric,numeric,numeric,numeric,numeric,timestamp with time zone,bytea)', + 'EXECUTE' + ), + 'market ingestion is an exact reconciler-only function capability' +); + +set local role programmable_maintenance; + +select throws_ok( + $sql$ + select programmable_private.prune_run_telemetry( + '2026-07-31T06:00:00Z', 10001, decode(repeat('81', 32), 'hex') + ) + $sql$, + '22023', + 'retention calls are hard-capped at ten thousand rows' +); +select is( + programmable_private.prune_run_telemetry( + '2026-07-31T06:00:00Z', 1, decode(repeat('82', 32), 'hex') + ), + 1, + 'telemetry retention honors the caller row limit' +); +select is( + programmable_private.prune_run_telemetry( + '2026-07-31T06:00:00Z', 100, decode(repeat('83', 32), 'hex') + ), + 1, + 'success and failed telemetry use separate age windows' +); +select is( + programmable_private.prune_market_data( + '2026-07-31T06:00:00Z', 1, decode(repeat('84', 32), 'hex') + ), + 1, + 'first bounded market prune removes one expired raw snapshot' +); +select is( + programmable_private.prune_market_data( + '2026-07-31T06:00:00Z', 1, decode(repeat('85', 32), 'hex') + ), + 1, + 'second bounded market prune removes one expired hourly candle' +); +select is( + programmable_private.prune_parity_records( + '2026-07-31T06:00:00Z', 1, decode(repeat('86', 32), 'hex') + ), + 1, + 'matching parity retention is bounded' +); +select is( + programmable_private.prune_parity_records( + '2026-07-31T06:00:00Z', 100, decode(repeat('87', 32), 'hex') + ), + 1, + 'resolved mismatches expire only after their longer window' +); + +reset role; + +select is( + (select count(*) from programmable_private.run_telemetry), + 1::bigint, + 'recent failed telemetry survives the 180-day window' +); +select is( + (select count(*) from programmable_private.run_headers), + 2::bigint, + 'retention never deletes immutable run headers' +); +select is( + (select count(*) from programmable_private.run_lifecycle_outcomes), + 2::bigint, + 'retention never deletes terminal outcomes' +); +select is( + ( + select count(*) + from programmable_private.market_snapshots + where observed_at = '2026-07-30T00:00:00Z' + ), + 1::bigint, + 'recent raw market snapshot survives the seven-day window' +); +select is( + ( + select count(*) + from programmable_private.market_candles + where interval = 'hour' + ), + 0::bigint, + 'expired hourly candles are pruned' +); +select is( + ( + select count(*) + from programmable_private.market_candles + where interval = 'day' + ), + 1::bigint, + 'daily candles are retained indefinitely' +); +select is( + (select count(*) from programmable_private.parity_records), + 0::bigint, + 'eligible parity rows are pruned without deleting reconciliation evidence' +); +select is( + (select count(*) from programmable_private.reconciliation_records), + 2::bigint, + 'reconciliation provenance survives parity and market retention' +); +select ok( + not exists ( + select 1 + from programmable_private.market_snapshots + where reconciliation_id is null or audit_id is null + ) + and not exists ( + select 1 + from programmable_private.market_candles + where reconciliation_id is null or audit_id is null + ), + 'retained market rows keep non-null reconciliation and audit provenance' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/007_metadata_writers.test.sql b/supabase/tests/database/007_metadata_writers.test.sql new file mode 100644 index 00000000..fa25461f --- /dev/null +++ b/supabase/tests/database/007_metadata_writers.test.sql @@ -0,0 +1,438 @@ +begin; + +set local role programmable_profile_recovery; + +select programmable_private.define_profile_hash_version( + 71::smallint, + 'hmac-sha256-v1', + decode(repeat('71', 32), 'hex'), + decode(repeat('72', 32), 'hex'), + '2026-07-31T08:00:00Z' +); +select programmable_private.set_profile_hash_version_state( + '71000000-0000-0000-0000-000000000001', + 71::smallint, + 'current', + decode(repeat('73', 32), 'hex'), + '2026-07-31T08:00:01Z' +); + +reset role; +set local role programmable_profile_binder; + +select programmable_private.bind_profile_subject( + decode(repeat('11', 20), 'hex'), + 71::smallint, + decode(repeat('a1', 32), 'hex'), + 'wallet_signature', + decode(repeat('74', 32), 'hex'), + '2026-07-31T08:00:02Z' +); +select programmable_private.bind_profile_subject( + decode(repeat('22', 20), 'hex'), + 71::smallint, + decode(repeat('a2', 32), 'hex'), + 'wallet_signature', + decode(repeat('75', 32), 'hex'), + '2026-07-31T08:00:03Z' +); + +reset role; + +select plan(26); + +select ok( + ( + select pg_catalog.count(*) = 2 + from pg_catalog.pg_proc as function + join pg_catalog.pg_namespace as namespace + on namespace.oid = function.pronamespace + where namespace.nspname = 'programmable_private' + and function.proname in ( + 'append_token_project_metadata_revision', + 'append_project_metadata_link' + ) + and function.prosecdef + and 'search_path=""' = any(function.proconfig) + ), + 'both metadata writers are SECURITY DEFINER with an empty search_path' +); + +select ok( + has_function_privilege( + 'programmable_profile_writer', + 'programmable_private.append_token_project_metadata_revision(uuid,bytea,smallint,bytea,bigint,bigint,bytea,bigint,text,text,text,bytea,timestamptz)', + 'EXECUTE' + ) + and has_function_privilege( + 'programmable_profile_writer', + 'programmable_private.append_project_metadata_link(uuid,uuid,bytea,smallint,bytea,bigint,bigint,text,text,integer,bytea,timestamptz)', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_projector', + 'programmable_private.append_token_project_metadata_revision(uuid,bytea,smallint,bytea,bigint,bigint,bytea,bigint,text,text,text,bytea,timestamptz)', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_api_reader', + 'programmable_private.append_project_metadata_link(uuid,uuid,bytea,smallint,bytea,bigint,bigint,text,text,integer,bytea,timestamptz)', + 'EXECUTE' + ), + 'only the ordinary profile writer receives both exact metadata signatures' +); + +set local role programmable_profile_writer; + +select lives_ok( + $sql$ + select programmable_private.append_token_project_metadata_revision( + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), + 71::smallint, + decode(repeat('a1', 32), 'hex'), + 1, + 1, + decode(repeat('33', 20), 'hex'), + 0, + 'Project One', + 'First immutable metadata revision', + 'https://assets.example/project-one.png', + decode(repeat('81', 32), 'hex'), + '2026-07-31T08:01:00Z' + ) + $sql$, + 'the bound owner appends the first metadata revision' +); + +select lives_ok( + $sql$ + select programmable_private.append_token_project_metadata_revision( + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), + 71::smallint, + decode(repeat('a1', 32), 'hex'), + 1, + 1, + decode(repeat('33', 20), 'hex'), + 0, + 'Project One', + 'First immutable metadata revision', + 'https://assets.example/project-one.png', + decode(repeat('81', 32), 'hex'), + '2026-07-31T08:01:00Z' + ) + $sql$, + 'an exact metadata replay is idempotent' +); + +select throws_ok( + $sql$ + select programmable_private.append_token_project_metadata_revision( + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + decode(repeat('33', 20), 'hex'), 0, + 'Changed Project', 'First immutable metadata revision', + 'https://assets.example/project-one.png', + decode(repeat('81', 32), 'hex'), '2026-07-31T08:01:00Z' + ) + $sql$, + '23505', + 'an immutable metadata replay cannot change content' +); + +select throws_ok( + $sql$ + select programmable_private.append_token_project_metadata_revision( + '71000000-0000-0000-0000-000000000012', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + decode(repeat('33', 20), 'hex'), 0, + 'Project One stale', null, null, + decode(repeat('82', 32), 'hex'), '2026-07-31T08:01:01Z' + ) + $sql$, + '40001', + 'metadata revision compare-and-swap rejects a stale writer' +); + +select throws_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000021', + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + 'website', 'http://project.example/', 0, + decode(repeat('83', 32), 'hex'), '2026-07-31T08:01:02Z' + ) + $sql$, + '22023', + 'project links reject non-HTTPS URLs before audit mutation' +); + +select lives_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000021', + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + 'website', 'https://project.example/', 0, + decode(repeat('83', 32), 'hex'), '2026-07-31T08:01:02Z' + ) + $sql$, + 'the owner appends a typed link to the current metadata revision' +); + +select lives_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000021', + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + 'website', 'https://project.example/', 0, + decode(repeat('83', 32), 'hex'), '2026-07-31T08:01:02Z' + ) + $sql$, + 'an exact project-link replay is idempotent' +); + +select throws_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000021', + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + 'website', 'https://changed.example/', 0, + decode(repeat('83', 32), 'hex'), '2026-07-31T08:01:02Z' + ) + $sql$, + '23505', + 'an immutable project-link replay cannot change content' +); + +select throws_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000022', + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + 'website', 'https://duplicate.example/', 1, + decode(repeat('84', 32), 'hex'), '2026-07-31T08:01:03Z' + ) + $sql$, + '23505', + 'a metadata revision cannot append a duplicate link kind' +); + +select lives_ok( + $sql$ + select programmable_private.append_token_project_metadata_revision( + '71000000-0000-0000-0000-000000000013', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + decode(repeat('33', 20), 'hex'), 1, + 'Project One', 'Second immutable metadata revision', + 'https://assets.example/project-one-v2.png', + decode(repeat('85', 32), 'hex'), '2026-07-31T08:01:04Z' + ) + $sql$, + 'the current owner advances metadata with revision CAS' +); + +select throws_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000023', + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + 'docs', 'https://docs.example/', 1, + decode(repeat('86', 32), 'hex'), '2026-07-31T08:01:05Z' + ) + $sql$, + '40001', + 'new links cannot be appended to a superseded metadata revision' +); + +select lives_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000021', + '71000000-0000-0000-0000-000000000011', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 1, + 'website', 'https://project.example/', 0, + decode(repeat('83', 32), 'hex'), '2026-07-31T08:01:02Z' + ) + $sql$, + 'an exact old-revision link replay remains idempotent after metadata advances' +); + +select throws_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000025', + '71000000-0000-0000-0000-000000000013', + decode(repeat('22', 20), 'hex'), 71::smallint, + decode(repeat('a2', 32), 'hex'), 1, 2, + 'discord', 'https://discord.example/', 1, + decode(repeat('8a', 32), 'hex'), '2026-07-31T08:01:05Z' + ) + $sql$, + '42501', + 'another bound subject cannot append links to owned metadata' +); + +select throws_ok( + $sql$ + select programmable_private.append_token_project_metadata_revision( + '71000000-0000-0000-0000-000000000014', + decode(repeat('22', 20), 'hex'), 71::smallint, + decode(repeat('a2', 32), 'hex'), 1, 1, + decode(repeat('33', 20), 'hex'), 2, + 'Takeover', null, null, + decode(repeat('87', 32), 'hex'), '2026-07-31T08:01:06Z' + ) + $sql$, + '42501', + 'another bound subject cannot take over token metadata' +); + +select throws_ok( + $sql$ + select programmable_private.append_token_project_metadata_revision( + '71000000-0000-0000-0000-000000000015', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 2, 1, + decode(repeat('33', 20), 'hex'), 2, + 'Stale binding', null, null, + decode(repeat('88', 32), 'hex'), '2026-07-31T08:01:07Z' + ) + $sql$, + '40001', + 'metadata writes reject a stale owner-binding generation' +); + +select lives_ok( + $sql$ + select programmable_private.append_project_metadata_link( + '71000000-0000-0000-0000-000000000024', + '71000000-0000-0000-0000-000000000013', + decode(repeat('11', 20), 'hex'), 71::smallint, + decode(repeat('a1', 32), 'hex'), 1, 2, + 'docs', 'https://docs.example/', 0, + decode(repeat('89', 32), 'hex'), '2026-07-31T08:01:08Z' + ) + $sql$, + 'links can be appended to the current revision only' +); + +reset role; + +select is( + ( + select pg_catalog.count(*) + from programmable_private.token_project_metadata + where chain_id = 1 and token = decode(repeat('33', 20), 'hex') + ), + 2::bigint, + 'only the two successful metadata revisions persist' +); + +select is( + ( + select pg_catalog.array_agg(metadata_revision order by metadata_revision) + from programmable_private.token_project_metadata + where chain_id = 1 and token = decode(repeat('33', 20), 'hex') + ), + array[1::bigint, 2::bigint], + 'metadata revisions form a gap-free compare-and-swap chain' +); + +select is( + ( + select pg_catalog.count(*) + from programmable_private.project_links + where metadata_id in ( + '71000000-0000-0000-0000-000000000011', + '71000000-0000-0000-0000-000000000013' + ) + ), + 2::bigint, + 'only the two successful immutable links persist' +); + +select is( + ( + select pg_catalog.count(*) + from programmable_private.mutation_audits + where action in ('project_metadata.append', 'project_metadata_link.append') + ), + 4::bigint, + 'successful writes append one audit each while replays and failures append none' +); + +select is( + ( + select pg_catalog.count(*) + from programmable_private.profile_audit_records + where action in ('project_metadata.append', 'project_metadata_link.append') + and caller_role = 'programmable_profile_writer' + ), + 4::bigint, + 'metadata writes also persist the bound subject, wallet and binding generation' +); + +select ok( + ( + select pg_catalog.count(distinct subject_id) = 1 + from programmable_private.token_project_metadata + where chain_id = 1 and token = decode(repeat('33', 20), 'hex') + ), + 'all revisions preserve the first bound stable subject' +); + +select ok( + not exists ( + select 1 + from programmable_private.project_links as link + left join programmable_private.mutation_audits as audit + on audit.audit_id = link.audit_id + where audit.audit_id is null + or audit.action <> 'project_metadata_link.append' + or audit.caller_role <> 'programmable_profile_writer' + ), + 'every project link carries its profile-writer mutation audit' +); + +set local role programmable_profile_writer; + +select throws_ok( + $sql$ + insert into programmable_private.token_project_metadata ( + metadata_id, chain_id, token, project_name, description, + logo_reference, metadata_revision, subject_id, created_at, audit_id + ) + values ( + '71000000-0000-0000-0000-000000000099', + 1, decode(repeat('99', 20), 'hex'), null, null, null, 1, + '71000000-0000-0000-0000-000000000099', + '2026-07-31T08:02:00Z', + '71000000-0000-0000-0000-000000000099' + ) + $sql$, + '42501', + 'the metadata capability cannot bypass the function-only table surface' +); + +reset role; + +select * from finish(); + +rollback; diff --git a/supabase/tests/database/007_read_view_scope.test.sql b/supabase/tests/database/007_read_view_scope.test.sql new file mode 100644 index 00000000..2e00e5f1 --- /dev/null +++ b/supabase/tests/database/007_read_view_scope.test.sql @@ -0,0 +1,273 @@ +begin; + +select plan(13); + +select ok( + not exists ( + with launch_views(view_name) as ( + values ('recent_launches_v1'), ('launch_by_token_v1'), + ('launches_by_creator_v1') + ), required(column_name) as ( + values ('currency0'), ('currency1'), ('hook'), ('pool_key_fee'), + ('tick_spacing'), ('buy_swap_fee_bps'), ('sell_swap_fee_bps'), + ('buy_creator_fee_bps'), ('sell_creator_fee_bps'), + ('creator_fee_bps'), ('launcher_fee_bps'), ('transfer_tax_bps'), + ('lp_fee_pips'), ('project_description'), + ('project_logo_reference'), ('project_metadata_revision'), + ('project_links'), ('launch_block_timestamp') + ) + select 1 + from launch_views + cross join required + where not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + join pg_catalog.pg_class as relation on relation.oid = attribute.attrelid + join pg_catalog.pg_namespace as namespace on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname = launch_views.view_name + and attribute.attname = required.column_name + and attribute.attnum > 0 + and not attribute.attisdropped + ) + ), + 'every launch read surface exposes PoolKey fees latest metadata and launch time' +); + +select ok( + not exists ( + select 1 + from unnest(array[ + 'programmable_private.recent_launches_v1'::regclass, + 'programmable_private.launch_by_token_v1'::regclass, + 'programmable_private.launches_by_creator_v1'::regclass + ]) as checked_view(view_oid) + cross join lateral ( + select pg_catalog.pg_get_viewdef(checked_view.view_oid, false) as definition + ) as view_definition + where pg_catalog.strpos(view_definition.definition, 'pool_projections') = 0 + or pg_catalog.strpos(view_definition.definition, 'pool_fee_configurations') = 0 + or pg_catalog.strpos(view_definition.definition, 'current_token_project_metadata_v1') = 0 + or pg_catalog.strpos(view_definition.definition, 'pool_canonical') = 0 + or pg_catalog.strpos(view_definition.definition, 'fee_canonical') = 0 + ), + 'launch DTO joins are exact-run canonical PoolKey fee and current metadata joins' +); + +select ok( + ( + select pg_catalog.strpos(definition, 'metadata_revision') > 0 + and pg_catalog.strpos(definition, 'project_links') > 0 + and pg_catalog.strpos(definition, 'newer') > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.current_token_project_metadata_v1'::regclass, + false + ) as definition + ) as metadata_view + ), + 'project metadata read model selects only the latest audited revision and links' +); + +select ok( + not exists ( + select 1 + from unnest(array[ + 'programmable_private.market_snapshots_v1'::regclass, + 'programmable_private.market_candles_v1'::regclass + ]) as checked_view(view_oid) + cross join lateral ( + select pg_catalog.pg_get_viewdef(checked_view.view_oid, false) as definition + ) as view_definition + where pg_catalog.strpos(view_definition.definition, 'dual_rpc_block_evidence') = 0 + or pg_catalog.strpos(view_definition.definition, 'safe_head_observations') = 0 + or pg_catalog.strpos(view_definition.definition, 'run_lifecycle_outcomes') = 0 + or pg_catalog.strpos(view_definition.definition, 'mismatch_count') = 0 + or pg_catalog.strpos(view_definition.definition, 'uniswap_subgraph') = 0 + or pg_catalog.strpos(view_definition.definition, 'launch_by_token_v1') = 0 + or not has_table_privilege( + 'programmable_api_reader', checked_view.view_oid, 'SELECT' + ) + ), + 'server market views require exact canonical block successful reconciliation and token route' +); + +select ok( + not exists ( + select required.column_name + from (values ('pool_id'), ('hook'), ('quote_asset'), ('entitled')) + as required(column_name) + where not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.account_reward_summaries_v1'::regclass + and attribute.attname = required.column_name + and attribute.attnum > 0 + and not attribute.attisdropped + ) + ), + 'profile reward reads expose pool hook quote asset and exact entitled total' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'explore-list') > 0 + and pg_catalog.strpos(definition, 'run_headers') > 0 + and pg_catalog.strpos(definition, 'source_group') > 0 + and pg_catalog.strpos(definition, 'release_epoch_current') > 0 + and pg_catalog.strpos(definition, 'route_eligibility_current') > 0 + and pg_catalog.strpos(definition, 'chain_event_current_canonical') > 0 + and pg_catalog.strpos( + definition, + 'has_current_verified_reward_seed' + ) > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.recent_launches_v1'::regclass, + false + ) as definition + ) as view_definition + ), + 'recent launches require the exact Explore-list route and current source scope' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'explore-token') > 0 + and pg_catalog.strpos(definition, 'source_group') > 0 + and pg_catalog.strpos(definition, 'chain_event_current_canonical') > 0 + and pg_catalog.strpos( + definition, + 'has_current_verified_reward_seed' + ) > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.launch_by_token_v1'::regclass, + false + ) as definition + ) as view_definition + ), + 'token detail uses its own exact route instead of inheriting list eligibility' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'creator-profile') > 0 + and pg_catalog.strpos(definition, 'source_group') > 0 + and pg_catalog.strpos(definition, 'chain_event_current_canonical') > 0 + and pg_catalog.strpos( + definition, + 'has_current_verified_reward_seed' + ) > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.launches_by_creator_v1'::regclass, + false + ) as definition + ) as view_definition + ), + 'creator launches require creator-profile eligibility and a current source' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'creator-profile') > 0 + and pg_catalog.strpos( + definition, + 'has_current_verified_reward_seed' + ) > 0 + and pg_catalog.strpos(definition, 'release_epoch_current') > 0 + and pg_catalog.strpos(definition, 'chain_event_current_canonical') > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.account_reward_summaries_v1'::regclass, + false + ) as definition + ) as view_definition + ), + 'account rewards require route eligibility, a verified seed and canonical sources' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'classic-v3-profile') > 0 + and pg_catalog.strpos(definition, 'reward_allocation_current_verified') > 0 + and pg_catalog.strpos(definition, 'release_epoch_current') > 0 + and pg_catalog.strpos(definition, 'chain_event_current_canonical') > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.classic_v3_vault_history_v1'::regclass, + false + ) as definition + ) as view_definition + ), + 'Classic vault history requires exact profile-route, seed, epoch and canonical gates' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'creator-profile') > 0 + and pg_catalog.strpos(definition, 'reward_allocation_current_verified') > 0 + and pg_catalog.strpos(definition, 'release_epoch_current') > 0 + and pg_catalog.strpos(definition, 'chain_event_current_canonical') > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.stock_paired_vault_history_v1'::regclass, + false + ) as definition + ) as view_definition + ), + 'Stock-Paired vault history uses the creator-profile route and current evidence' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'launch-lookup') > 0 + and pg_catalog.strpos(definition, 'source_group') > 0 + and pg_catalog.strpos(definition, 'chain_event_current_canonical') > 0 + and pg_catalog.strpos( + definition, + 'has_current_verified_reward_seed' + ) > 0 + from ( + select pg_catalog.pg_get_viewdef( + 'programmable_private.launch_lookup_v1'::regclass, + false + ) as definition + ) as view_definition + ), + 'launch confirmation lookup has independent route and source eligibility' +); + +select ok( + not exists ( + select 1 + from unnest(array[ + 'programmable_private.recent_launches_v1'::regclass, + 'programmable_private.launch_by_token_v1'::regclass, + 'programmable_private.launches_by_creator_v1'::regclass, + 'programmable_private.account_reward_summaries_v1'::regclass, + 'programmable_private.classic_v3_vault_history_v1'::regclass, + 'programmable_private.stock_paired_vault_history_v1'::regclass, + 'programmable_private.launch_lookup_v1'::regclass + ]) as checked_view(view_oid) + cross join lateral ( + select pg_catalog.pg_get_viewdef(checked_view.view_oid, false) as definition + ) as view_definition + where pg_catalog.strpos(view_definition.definition, 'eligible') = 0 + or pg_catalog.strpos(view_definition.definition, 'indexed') = 0 + or pg_catalog.strpos(view_definition.definition, 'checkpoint_id') = 0 + ), + 'every route-specific read view binds eligible indexed mode to its publication checkpoint' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/008_p0_schema_hardening.test.sql b/supabase/tests/database/008_p0_schema_hardening.test.sql new file mode 100644 index 00000000..266989ea --- /dev/null +++ b/supabase/tests/database/008_p0_schema_hardening.test.sql @@ -0,0 +1,509 @@ +begin; + +select plan(37); + +select ok( + to_regprocedure( + 'programmable_private.register_rpc_provider_deployment(uuid,bigint,text,text,bytea,bytea,text,bytea,bytea,bytea,bytea,timestamp with time zone)' + ) is not null, + 'RPC providers have a dedicated metadata-complete registration capability' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.rpc_provider_deployment_metadata'::regclass + and attribute.attname in ( + 'chain_id', 'vendor', 'vendor_order', 'constructor_version', + 'endpoint_url_commitment', 'endpoint_origin_commitment', + 'endpoint_evidence_domain', 'endpoint_evidence_commitment' + ) + and attribute.attnum > 0 + and not attribute.attisdropped + group by attribute.attrelid + having count(*) = 8 + ), + 'RPC deployment metadata records mainnet vendor order, constructor, and endpoint commitments' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.rpc_provider_deployment_metadata'::regclass + and attribute.attname in ( + 'endpoint_url', 'endpoint_origin', 'raw_endpoint_url', + 'raw_endpoint_origin' + ) + and attribute.attnum > 0 + and not attribute.attisdropped + ), + 'production RPC metadata contains no raw endpoint URL or origin fields' +); + +select ok( + exists ( + select 1 + from programmable_private.rpc_endpoint_evidence_domains + where evidence_domain = 'rpc-endpoint-commitments-v1' + and enabled + and definition_commitment <> + decode(repeat('00', 32), 'hex') + ) + and exists ( + select 1 + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.rpc_provider_deployment_metadata'::regclass + and constraint_row.contype = 'f' + and pg_catalog.pg_get_constraintdef(constraint_row.oid) like + '%endpoint_evidence_domain%rpc_endpoint_evidence_domains%' + ), + 'endpoint commitments link to a nonzero allowlisted evidence domain' +); + +select ok( + exists ( + select 1 + from programmable_private.fingerprint_encoding_versions + where fingerprint_domain = 'evidence' and encoding_version = 2 + and definition_commitment = decode( + '45b8e9d1bf3ffc2e70b7fd612ec2346aef5e74ae08348b699eb68ce0afbc9483', + 'hex' + ) + ) + and ( + select pg_catalog.jsonb_object_agg( + evidence_subtype, '0x' || encode(definition_commitment, 'hex') + order by subtype_tag + ) + from programmable_private.provider_evidence_encoding_subtypes + where encoding_version = 2 + ) = jsonb_build_object( + 'safe_head', '0x3a26ae9c9220347568e33b5850ac6f605d120e6443f64e9e8b8742ea8a016f52', + 'block', '0x83948b75a3c05b9d257749f754f09a1b02e658496ba562f36e07bc15be3d7bec', + 'runtime_code', '0x4c191e91130097832a91025e85c2ff3be2705af0e3ea9abc396f09e7cd9dbbc5', + 'dynamic_attestation', '0x206e1f89ad459e55e0591de13eb40856dd94ff62923d76034eba5776706e6de9', + 'log_coverage', '0x4ab7460cb321503613935191917c46872c9e3c9a681b2d4b349b6187f4dc0aec' + ), + 'SQL allowlist commitments exactly match the independent provider-evidence v2 fixture' +); + +select ok( + to_regprocedure( + 'programmable_private.get_recent_launches_v1(bigint,integer,bigint,bytea,bytea)' + ) is not null, + 'recent launches exposes a lossless composite cursor' +); + +select is( + ( + select pg_catalog.string_agg( + procedure.proargnames[argument.ordinality], + ',' order by argument.ordinality + ) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + cross join lateral pg_catalog.generate_subscripts( + procedure.proargnames, 1 + ) as argument(ordinality) + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_account_reward_summary_v1' + and procedure.proargmodes[argument.ordinality] = 't'::"char" + ), + 'chain_id,account,release_id,model_id,vault,pool_id,hook,quote_asset,entitled,claimable_accrued,claimed_total,promoted_block_number,promoted_block_hash,verified_at', + 'account reward rows carry their authoritative chain and account scope' +); + +select ok( + to_regprocedure( + 'programmable_private.get_recent_launches_v1(bigint,integer,bigint)' + ) is null, + 'the lossy block-only pagination overload is removed' +); + +select ok( + to_regprocedure( + 'programmable_private.get_projector_runtime_state_v1(bigint,text,text,text,text,text[],text[],bytea[],bytea[])' + ) is not null, + 'projector has one exact scoped runtime-state reader' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_attribute + where attrelid = 'programmable_private.checkpoint_summary_v1'::regclass + and attname in ('checkpoint_id', 'source_group', 'projector_version') + and attnum > 0 and not attisdropped + group by attrelid + having count(*) = 3 + ) + and pg_catalog.pg_get_viewdef( + 'programmable_private.checkpoint_summary_v1'::regclass, true + ) like '%current_checkpoint.checkpoint_id%', + 'readiness exposes and joins the exact canonical checkpoint identity' +); + +select ok( + to_regprocedure( + 'programmable_private.get_projector_release_manifest_v1(bigint,text,text,text,uuid,bigint)' + ) is not null + and has_function_privilege( + 'programmable_projector', + 'programmable_private.get_projector_release_manifest_v1(bigint,text,text,text,uuid,bigint)', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_api_reader', + 'programmable_private.get_projector_release_manifest_v1(bigint,text,text,text,uuid,bigint)', + 'EXECUTE' + ), + 'exact release manifest reader is fenced to the projector capability' +); + +select ok( + to_regprocedure( + 'programmable_private.get_projector_dynamic_source_attestations_v1(bigint,text,text,text,uuid,bigint)' + ) is not null + and has_function_privilege( + 'programmable_projector', + 'programmable_private.get_projector_dynamic_source_attestations_v1(bigint,text,text,text,uuid,bigint)', + 'EXECUTE' + ), + 'projector can recover only exact current asset-bound dynamic attestations' +); + +select ok( + to_regprocedure( + 'programmable_private.list_projector_candidate_dispositions_v1(bigint,text,text,text,uuid,bigint,text,bigint,bytea,numeric,numeric,text,integer,timestamp with time zone)' + ) is not null + and has_function_privilege( + 'programmable_projector', + 'programmable_private.list_projector_candidate_dispositions_v1(bigint,text,text,text,uuid,bigint,text,bigint,bytea,numeric,numeric,text,integer,timestamp with time zone)', + 'EXECUTE' + ) + and not has_function_privilege( + 'programmable_projector', + 'programmable_private.advance_envio_ingestion_cursor_v1(uuid,uuid,text,bigint,bigint,numeric,bytea,numeric,text,bytea,timestamp with time zone)', + 'EXECUTE' + ), + 'disposition recovery is available while direct cursor advancement stays fenced' +); + +select ok( + to_regclass('programmable_private.dynamic_source_attestations') is not null, + 'dynamic contract sources have an audited append-only attestation ledger' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.release_dynamic_source_templates'::regclass + and attribute.attname = 'runtime_code_hash' + and attribute.attnum > 0 + and not attribute.attisdropped + ) + and exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.release_dynamic_source_templates'::regclass + and attribute.attname in ( + 'deployed_artifact_creation_code_commitment', + 'normalized_runtime_code_hash', + 'immutable_references_commitment', + 'runtime_code_length' + ) + and attribute.attnum > 0 + and not attribute.attisdropped + group by attribute.attrelid + having count(*) = 4 + ), + 'dynamic templates commit to artifact and immutable-normalized runtime shape, not one instance runtime hash' +); + +select ok( + to_regclass( + 'programmable_private.chain_event_occurrence_materializations' + ) is not null, + 'one global raw occurrence has an append-only exact-scope materialization ledger' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.chain_event_occurrence_materializations'::regclass + and attribute.attname in ( + 'event_type', 'decoder_version', 'abi_event_set_commitment', + 'decoded_payload', 'payload_hash', 'release_binding_id', + 'dynamic_source_attestation_id', 'block_evidence_id' + ) + and attribute.attnum > 0 + and not attribute.attisdropped + group by attribute.attrelid + having count(*) = 8 + ), + 'release-scoped decoding, source binding, and evidence live on each materialization' +); + +select ok( + pg_catalog.pg_get_viewdef( + 'programmable_private.recent_launches_v1'::regclass, + true + ) like '%chain_event_materialized_occurrences_v1%' + and pg_catalog.pg_get_viewdef( + 'programmable_private.classic_v3_vault_history_v1'::regclass, + true + ) like '%chain_event_materialized_occurrences_v1%', + 'public read models authorize occurrence scope through exact materializations' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.chain_event_occurrence_materializations'::regclass + and constraint_row.contype = 'u' + and pg_catalog.pg_get_constraintdef(constraint_row.oid) like + '%occurrence_id%epoch_id%pointer_generation%' + ), + 'an occurrence can materialize once per exact epoch generation without duplicating global identity' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.reward_allocation_evidence'::regclass + and attribute.attname in ( + 'constructor_arguments_commitment', + 'local_init_code_hash', + 'create2_salt', + 'local_create2_address' + ) + and attribute.attnum > 0 + and not attribute.attisdropped + group by attribute.attrelid + having count(*) = 4 + ), + 'per-instance CREATE2 evidence keeps constructor arguments, init code, salt, and address separate' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.release_epochs'::regclass + and attribute.attname = 'artifact_creation_code_commitment' + and attribute.attnum > 0 + and not attribute.attisdropped + ) + and not exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.release_epochs'::regclass + and attribute.attname = 'artifact_init_code_commitment' + and attribute.attnum > 0 + and not attribute.attisdropped + ), + 'release epochs name the release-wide artifact creation-code commitment precisely' +); + +select throws_ok( + $sql$ + insert into programmable_private.release_dynamic_source_templates ( + dynamic_source_template_id, epoch_id, + parent_factory_release_binding_id, parent_factory_binding_commitment, + parent_source_role, + factory_event_type, deployed_address_field, deployed_source_role, + deployed_artifact_creation_code_commitment, + normalized_runtime_code_hash, immutable_references_commitment, + immutable_binding_spec, immutable_binding_commitment, + runtime_code_length, abi_event_set_commitment, template_commitment, + created_at, created_by_audit_id + ) values ( + '00000000-0000-0000-0000-000000000801', + '00000000-0000-0000-0000-000000000802', + '00000000-0000-0000-0000-000000000804', + decode(repeat('84', 32), 'hex'), + 'vesting_factory', 'VestingWalletDeployed', 'vault', 'vesting_wallet', + decode(repeat('85', 32), 'hex'), decode(repeat('81', 32), 'hex'), + decode(repeat('86', 32), 'hex'), + '{"factoryConfigurationField":"configurationCommitment","bindings":[{"ordinal":"0","offset":"0","length":"20","source":"deployed_address","encoding":"address"}]}'::jsonb, + decode(repeat('87', 32), 'hex'), 1, + decode(repeat('82', 32), 'hex'), + decode(repeat('83', 32), 'hex'), '2026-07-31T00:00:00Z', + '00000000-0000-0000-0000-000000000803' + ) + $sql$, + '23514', + 'dynamic reward vaults and vesting wallets require their exact vault or wallet field' +); + +select ok( + to_regclass('programmable_private.envio_candidate_inbox') is not null, + 'Envio evidence has a release-neutral immutable inbox' +); + +select ok( + to_regclass('programmable_private.envio_candidate_resolutions') is not null, + 'neutral candidates have append-only scoped provenance resolutions' +); + +select ok( + to_regclass('programmable_private.projection_entity_current') is not null, + 'published entity versions have delta-safe current pointers' +); + +select ok( + exists ( + select 1 from pg_catalog.pg_attribute + where attrelid = 'programmable_private.pool_fee_configurations'::regclass + and attname = 'buy_creator_fee_bps' and attnum > 0 and not attisdropped + ) + and exists ( + select 1 from pg_catalog.pg_attribute + where attrelid = 'programmable_private.pool_fee_configurations'::regclass + and attname = 'sell_creator_fee_bps' and attnum > 0 and not attisdropped + ) + and to_regprocedure( + 'programmable_private.stage_pool_fee_configuration_v2(uuid,uuid,uuid,numeric,numeric,numeric,numeric,numeric,numeric,numeric,uuid,numeric,bytea,timestamp with time zone)' + ) is not null, + 'Classic V3 preserves separate buy and sell creator fee basis points' +); + +select ok( + to_regclass('programmable_private.creator_hook_claim_facts') is not null + and to_regclass('programmable_private.launcher_hook_claim_facts') is not null + and to_regclass('programmable_private.creator_fee_checkpoint_facts') is not null + and to_regclass('programmable_private.reward_configuration_activation_facts') is not null, + 'hook claims checkpoints and reward activations have distinct typed facts' +); + +select ok( + to_regprocedure( + 'programmable_private.append_creator_hook_claim_fact(uuid,uuid,uuid,bytea,bytea,bytea,bytea,bytea,bytea,numeric,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.append_launcher_hook_claim_fact(uuid,uuid,uuid,bytea,bytea,bytea,bytea,numeric,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.append_creator_fee_checkpoint_fact(uuid,uuid,uuid,bytea,numeric,numeric,numeric,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.append_reward_configuration_activation_fact(uuid,uuid,uuid,bytea,bytea,numeric,bytea,bytea,bytea[],numeric[],numeric,timestamp with time zone)' + ) is not null, + 'typed hook and vault fact writers expose exact capabilities' +); + +select ok( + to_regprocedure( + 'programmable_private.append_release_projection_event_rule(uuid,uuid,text,text,text,bytea,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.append_release_launch_requirement(uuid,uuid,integer,text,text,text,bytea,timestamp with time zone)' + ) is not null, + 'event allowlists and completeness manifests have append-only writers' +); + +select ok( + to_regprocedure( + 'programmable_private.stage_launch_occurrence_role(uuid,text,uuid,timestamp with time zone)' + ) is not null, + 'launch projections can bind exact manifest occurrence roles' +); + +select ok( + to_regprocedure( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)' + ) is not null, + 'projection writers share a release event-role admission check' +); + +select ok( + to_regclass('programmable_private.release_projection_event_rules') is not null, + 'typed projection writers have immutable release event-role allowlists' +); + +select ok( + to_regclass( + 'programmable_private.release_launch_completeness_requirements' + ) is not null, + 'launch promotion has immutable release completeness requirements' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.chain_event_occurrences'::regclass + and attribute.attname = 'dynamic_source_attestation_id' + and attribute.attnum > 0 + and not attribute.attisdropped + ), + 'occurrences retain exact dynamic-source provenance' +); + +select ok( + ( + select attribute.atttypid = 'pg_catalog.int8'::regtype + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.launches_by_creator_v1'::regclass + and attribute.attname = 'launch_transaction_index' + and attribute.attnum > 0 + and not attribute.attisdropped + ) + and ( + select attribute.atttypid = 'pg_catalog.int8'::regtype + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.launches_by_creator_v1'::regclass + and attribute.attname = 'launch_receipt_log_ordinal' + and attribute.attnum > 0 + and not attribute.attisdropped + ), + 'creator direct view preserves the full unsigned-32-bit ordinal domain in bigint columns' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.chain_event_occurrences'::regclass + and pg_catalog.pg_get_constraintdef(constraint_row.oid) like + '%2147483647%' + ), + 'occurrence storage has no signed-32-bit narrowing constraint' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.stock_paired_vault_history_v1'::regclass + and attribute.attname = 'quote_asset' + and attribute.attnum > 0 + and not attribute.attisdropped + ), + 'Stock-Paired vault history exposes quote_asset explicitly' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/009_atomic_public_contracts.test.sql b/supabase/tests/database/009_atomic_public_contracts.test.sql new file mode 100644 index 00000000..344f75d3 --- /dev/null +++ b/supabase/tests/database/009_atomic_public_contracts.test.sql @@ -0,0 +1,534 @@ +begin; +select plan(29); + +create function public.large_atomic_projection_fixture_v1() +returns jsonb +language sql +stable +security invoker +set search_path = '' +as $function$ + select programmable_private.build_indexed_token_projection_v2( + pg_catalog.jsonb_build_object( + 'route_key', 'explore-token', + 'chain_id', 1, + 'release_id', 'stock-paired-v3', + 'model_id', 'stock-paired', + 'source_group', 'core', + 'projector_version', 'fixture-v1', + 'epoch_id', '10000000-0000-0000-0000-000000000001', + 'pointer_generation', '1', + 'checkpoint_id', '10000000-0000-0000-0000-000000000002', + 'checkpoint_generation', '2', + 'reorg_generation', '0', + 'checkpoint_block_number', '123', + 'checkpoint_block_hash_hex', '0x' || pg_catalog.repeat('11', 32), + 'snapshot_commitment_hex', '0x' || pg_catalog.repeat('22', 32), + 'projection_run_id', '10000000-0000-0000-0000-000000000003', + 'publication_commitment_hex', '0x' || pg_catalog.repeat('33', 32), + 'promoted_block_number', '123', + 'promoted_block_hash_hex', '0x' || pg_catalog.repeat('44', 32), + 'token_hex', '0x' || pg_catalog.repeat('55', 20), + 'hook_hex', '0x' || pg_catalog.repeat('66', 20), + 'pool_id_hex', '0x' || pg_catalog.repeat('77', 32), + 'creator_hex', '0x' || pg_catalog.repeat('88', 20), + 'position_recipient_hex', '0x' || pg_catalog.repeat('99', 20), + 'position_token_id', '9007199254740993', + 'reward_vault_hex', '0x' || pg_catalog.repeat('aa', 20), + 'launch_hash_hex', '0x' || pg_catalog.repeat('bb', 32), + 'launch_source_block_number', '123', + 'launch_transaction_hash_hex', '0x' || pg_catalog.repeat('cc', 32), + 'launch_transaction_index', '4294967295', + 'launch_receipt_log_ordinal', '4294967295', + 'launch_timestamp_iso', '2026-07-31T12:00:00.000Z', + 'token_name', 'Large Atomic Fixture', + 'token_symbol', 'LARGE', + 'total_supply', '1000000000000000000000000000' + ) || pg_catalog.jsonb_build_object( + 'token_liquidity_amount', '90071992547409931234567890', + 'locked_token_dust', '90071992547409931234567891', + 'market_liquidity', '90071992547409931234567892', + 'market_tick', 1, + 'initial_tick', 2, + 'tick_lower', -10, + 'tick_upper', 10, + 'buy_swap_fee_bps', 100, + 'sell_swap_fee_bps', 100, + 'buy_creator_fee_bps', 90, + 'sell_creator_fee_bps', 90, + 'launcher_fee_bps', 10, + 'transfer_tax_bps', 0, + 'lp_fee_pips', 10000, + 'protocol_fee_pips', 0, + 'token', 'token1', + 'currency0', 'quote0', + 'currency1', 'token1', + 'quote_asset', 'quote0', + 'market_token0_price', '0.5', + 'market_token1_price', '2', + 'market_volume_token0', '9007199254740993.123456789012345678', + 'market_volume_token1', '1', + 'market_volume_native', null, + 'market_swap_count', 100, + 'stock_quote_address_hex', '0x' || pg_catalog.repeat('dd', 20), + 'stock_quote_symbol', 'QUOTE', + 'stock_quote_name', 'Quote Asset', + 'stock_quote_decimals', 18, + 'stock_quote_currency_side', 'currency0', + 'accrued_creator_total', '90071992547409931234567893', + 'accrued_launcher_total', '90071992547409931234567894', + 'creator_claimable_accrued', '90071992547409931234567895', + 'initial_buy_native_wei', '90071992547409931234567896', + 'initial_buy_quote_raw', '90071992547409931234567897', + 'initial_buy_amount', '90071992547409931234567898' + ) + ) +$function$; + +select ok( + to_regprocedure( + 'programmable_private.get_public_explore_page_v1(bigint,text,text,integer,integer)' + ) is not null + and to_regprocedure( + 'programmable_private.get_public_explore_token_v1(bigint,text)' + ) is not null + and to_regprocedure( + 'programmable_private.get_public_token_chart_v1(bigint,text,text)' + ) is not null + and to_regprocedure( + 'programmable_private.get_public_creator_profile_v1(bigint,text)' + ) is not null + and to_regprocedure( + 'programmable_private.get_public_classic_v3_profile_v1(bigint,text)' + ) is not null + and to_regprocedure( + 'programmable_private.get_public_stock_paired_profile_v1(bigint,text)' + ) is not null + and to_regprocedure( + 'programmable_private.get_public_launch_lookup_v1(bigint,text,text,text)' + ) is not null, + 'all seven frozen public raw-envelope RPC signatures exist' +); + +select ok( + to_regprocedure( + 'programmable_private.get_public_explore_page_v1(bigint,text,text,integer,integer,jsonb)' + ) is null, + 'the cursor-taking Explore draft is absent from the final schema' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'programmable_private.get_public_explore_page_v1(bigint,text,text,integer,integer)'::regprocedure, + 'programmable_private.get_public_explore_token_v1(bigint,text)'::regprocedure, + 'programmable_private.get_public_token_chart_v1(bigint,text,text)'::regprocedure, + 'programmable_private.get_public_creator_profile_v1(bigint,text)'::regprocedure, + 'programmable_private.get_public_classic_v3_profile_v1(bigint,text)'::regprocedure, + 'programmable_private.get_public_stock_paired_profile_v1(bigint,text)'::regprocedure, + 'programmable_private.get_public_launch_lookup_v1(bigint,text,text,text)'::regprocedure, + 'programmable_private.get_public_indexer_feed_v1(bigint)'::regprocedure + ]) as function(oid) + where not pg_catalog.has_function_privilege( + 'programmable_api_reader', function.oid, 'EXECUTE' + ) + ), + 'API reader can execute only the frozen public RPC layer' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'programmable_private.get_public_explore_page_v1(bigint,text,text,integer,integer)'::regprocedure, + 'programmable_private.get_public_indexer_feed_v1(bigint)'::regprocedure + ]) as function(oid) + where pg_catalog.has_function_privilege('public', function.oid, 'EXECUTE') + or pg_catalog.has_function_privilege('anon', function.oid, 'EXECUTE') + or pg_catalog.has_function_privilege( + 'authenticated', function.oid, 'EXECUTE' + ) + or pg_catalog.has_function_privilege( + 'service_role', function.oid, 'EXECUTE' + ) + ), + 'browser and Supabase runtime roles cannot execute public route definers' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.get_read_model_performance_dataset_v1(bigint)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_api_reader', + 'programmable_private.get_read_model_performance_dataset_v1(bigint)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.get_read_model_performance_dataset_v1(bigint)'::regprocedure, + 'EXECUTE' + ), + 'performance dataset remains projector-only' +); + +select ok( + to_regprocedure( + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)' + ) is not null, + 'the exact-current reward-state baseline reader exists' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_api_reader', + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ), + 'reward-state baseline reader remains projector-only' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)'::regprocedure + ), 'projector_checkpoint_current' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)'::regprocedure + ), 'chain_event_materialized_occurrences_v1' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)'::regprocedure + ), 'current_account_reward_balances_v1' + ) > 0, + 'reward-state baseline binds current checkpoint, provenance, and balances' +); + +select ok( + to_regprocedure( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)' + ) is not null, + 'the all-current reward-balance reader exists' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_api_reader', + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'service_role', + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ), + 'all-current reward balances remain projector-only' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure + ), 'current_account_reward_balances_v1' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure + ), 'balance_entity.checkpoint_id = baseline.checkpoint_id' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure + ), 'current_checkpoint.reorg_generation =' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure + ), 'balance.epoch_id = header.epoch_id' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure + ), 'payout_change_projections' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure + ), 'allocation.effective_to_block is null' + ) = 0, + 'balance channel is checkpoint, reorg and epoch bound with historical payout resolution' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_function_result( + 'programmable_private.get_projector_reward_state_by_vault_v1(uuid,bytea)'::regprocedure + ), 'allocation_evidence_id uuid' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_function_result( + 'programmable_private.get_projector_reward_balances_by_vault_v1(uuid,bytea)'::regprocedure + ), 'allocation_evidence_id uuid' + ) > 0, + 'both reward readers expose the exact current allocation evidence identifier' +); + +select ok( + to_regprocedure( + 'programmable_private.stage_current_reward_snapshot_v1(uuid,bytea,bytea,uuid,bigint,bytea,numeric,integer[],bytea[],bytea[],numeric[],bytea[],bytea[],numeric[],numeric[],uuid,numeric,bytea,timestamp with time zone)' + ) is not null + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.stage_current_reward_snapshot_v1(uuid,bytea,bytea,uuid,bigint,bytea,numeric,integer[],bytea[],bytea[],numeric[],bytea[],bytea[],numeric[],numeric[],uuid,numeric,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_api_reader', + 'programmable_private.stage_current_reward_snapshot_v1(uuid,bytea,bytea,uuid,bigint,bytea,numeric,integer[],bytea[],bytea[],numeric[],bytea[],bytea[],numeric[],numeric[],uuid,numeric,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'the exact-current snapshot writer exists and remains projector-only' +); + +select ok( + to_regprocedure( + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)' + ) is not null + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.promote_projection_run_v2(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'service_role', + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'provider-bound promotion replaces the retired v2 capability and remains projector-only' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v2(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,timestamp with time zone)'::regprocedure + ), 'complete_group_occurrence_ids is distinct from p_occurrence_ids' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v2(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,timestamp with time zone)'::regprocedure + ), 'source.transaction_hash <> group_transaction_hash' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v2(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,timestamp with time zone)'::regprocedure + ), 'reward claim rows do not reconcile to the transaction' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v2(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,timestamp with time zone)'::regprocedure + ), 'allocation_evidence_id = p_allocation_evidence_ids[1]' + ) > 0, + 'reward deltas bind the complete transaction, claims, and exact verified seed evidence' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_viewdef( + 'programmable_private.classic_v3_vault_history_v1'::regclass, true + ), 'launch.projection_run_id = vault.projection_run_id' + ) = 0 + and pg_catalog.strpos( + pg_catalog.pg_get_viewdef( + 'programmable_private.classic_v3_vault_history_v1'::regclass, true + ), 'current_launch_projections_v1' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_viewdef( + 'programmable_private.stock_paired_vault_history_v1'::regclass, true + ), 'stock-paired-v1' + ) > 0, + 'reward history follows the immutable launch across later exact snapshot runs' +); + +select ok( + pg_catalog.has_table_privilege( + 'programmable_api_reader', + 'programmable_private.public_route_snapshots_v2', 'SELECT' + ) + and pg_catalog.has_table_privilege( + 'programmable_api_reader', + 'programmable_private.public_explore_chart_v1', 'SELECT' + ) + and pg_catalog.has_table_privilege( + 'programmable_api_reader', + 'programmable_private.public_launch_lookup_v1', 'SELECT' + ), + 'API reader can select the raw public evidence views' +); + +select ok( + not pg_catalog.has_table_privilege( + 'anon', 'programmable_private.public_route_snapshots_v2', 'SELECT' + ) + and not pg_catalog.has_table_privilege( + 'authenticated', + 'programmable_private.public_explore_token_v1', 'SELECT' + ) + and not pg_catalog.has_table_privilege( + 'service_role', + 'programmable_private.public_explore_list_v1', 'SELECT' + ), + 'browser and Supabase runtime roles cannot select raw public evidence views' +); + +select ok( + not pg_catalog.has_function_privilege( + 'programmable_api_reader', + 'programmable_private.build_indexed_token_projection_v2(jsonb)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.retarget_indexed_token_projection_v2(jsonb,jsonb,text)'::regprocedure, + 'EXECUTE' + ), + 'raw builders remain private implementation details' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_viewdef( + 'programmable_private.public_route_snapshots_v2'::regclass, true + ), 'all-supported' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_viewdef( + 'programmable_private.public_route_snapshots_v2'::regclass, true + ), 'classic-v3' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_viewdef( + 'programmable_private.public_route_snapshots_v2'::regclass, true + ), 'stock-paired' + ) > 0, + 'public snapshots encode exact all, Classic-v3 and Stock-only scopes' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_read_model_performance_dataset_v1(bigint)'::regprocedure + ), 'release_counts.total_count >= 200' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_read_model_performance_dataset_v1(bigint)'::regprocedure + ), 'candidate_sample.distinct_blocks = 8' + ) > 0, + 'performance corpus is real, complete, and candidate-diverse without padding' +); + +select is( + pg_catalog.jsonb_typeof( + public.large_atomic_projection_fixture_v1() + #> '{liquidity,tokenLiquidityAmountRaw}' + ), + 'string', + 'large token liquidity is emitted as a JSON string' +); + +select is( + public.large_atomic_projection_fixture_v1() + #>> '{liquidity,tokenLiquidityAmountRaw}', + '90071992547409931234567890', + 'large token liquidity survives beyond Number.MAX_SAFE_INTEGER exactly' +); + +select is( + public.large_atomic_projection_fixture_v1() + #>> '{liquidity,lockedTokenDustRaw}', + '90071992547409931234567891', + 'large locked dust survives JSON serialization exactly' +); + +select is( + public.large_atomic_projection_fixture_v1() + #>> '{liquidity,activeLiquidity}', + '90071992547409931234567892', + 'large active liquidity survives JSON serialization exactly' +); + +select is( + public.large_atomic_projection_fixture_v1() + #>> '{initialBuy,quoteRaw}', + '90071992547409931234567897', + 'large initial-buy quote amount is an exact JSON string' +); + +select is( + pg_catalog.jsonb_typeof( + public.large_atomic_projection_fixture_v1() + #> '{quote,grossVolumeQuoteRaw}' + ), + 'string', + 'large quote volume is never emitted as a JSON number' +); + +select is( + pg_catalog.jsonb_typeof( + public.large_atomic_projection_fixture_v1() + #> '{source,checkpointBlockNumber}' + ), + 'string', + 'block quantities use the frozen string boundary' +); + +select is( + pg_catalog.jsonb_typeof( + public.large_atomic_projection_fixture_v1() + #> '{launchTransactionIndex}' + ), + 'number', + 'bounded uint32 transaction ordinals remain JSON numbers' +); + +select is( + public.large_atomic_projection_fixture_v1() + #>> '{launchLogIndex}', + '4294967295', + 'receipt log ordinal preserves the full uint32 range' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/010_projector_runtime_singleton_lease.test.sql b/supabase/tests/database/010_projector_runtime_singleton_lease.test.sql new file mode 100644 index 00000000..466c995b --- /dev/null +++ b/supabase/tests/database/010_projector_runtime_singleton_lease.test.sql @@ -0,0 +1,636 @@ +begin; + +select plan(44); + +select ok( + exists ( + select 1 + from pg_catalog.pg_roles + where rolname = 'programmable_projector_runtime' + and not rolcanlogin + and not rolsuper + and not rolcreatedb + and not rolcreaterole + and not rolinherit + and not rolreplication + and not rolbypassrls + ) + and exists ( + select 1 + from pg_catalog.pg_roles + where rolname = 'programmable_projector_runtime_login' + and rolcanlogin + and not rolsuper + and not rolcreatedb + and not rolcreaterole + and not rolinherit + and not rolreplication + and not rolbypassrls + ), + 'runtime capability and login identities are independently hardened' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_auth_members as membership + join pg_catalog.pg_roles as capability + on capability.oid = membership.roleid + join pg_catalog.pg_roles as login_role + on login_role.oid = membership.member + where capability.rolname = 'programmable_projector_runtime' + and login_role.rolname = 'programmable_projector_runtime_login' + and not membership.inherit_option + and membership.set_option + ), + 'runtime login must explicitly SET ROLE into its capability' +); + +select ok( + to_regprocedure( + 'programmable_private.try_acquire_projector_runtime_lease_v1(text,bytea,timestamp with time zone,timestamp with time zone,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.assert_projector_runtime_lease_v1(text,bigint,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.release_projector_runtime_lease_v1(text,bigint,bytea,timestamp with time zone,bytea)' + ) is not null, + 'all three frozen singleton lease signatures exist' +); + +select is( + ( + select pg_catalog.array_to_string(procedure.proargnames, ',') + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'try_acquire_projector_runtime_lease_v1' + ), + 'p_holder_id,p_lease_token_hash,p_acquired_at,p_expires_at,p_input_commitment,acquired,lease_generation,acquired_at,expires_at', + 'acquire returns the frozen result column names' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname in ( + 'try_acquire_projector_runtime_lease_v1', + 'assert_projector_runtime_lease_v1', + 'release_projector_runtime_lease_v1' + ) + and ( + not procedure.prosecdef + or not ('search_path=""' = any(procedure.proconfig)) + ) + ), + 'lease functions are SECURITY DEFINER with an empty search path' +); + +select ok( + pg_catalog.strpos( + pg_catalog.lower( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_projector_runtime_lease_v1(text,bigint,bytea)'::regprocedure + ) + ), + 'for update' + ) > 0 + and pg_catalog.strpos( + pg_catalog.lower( + pg_catalog.obj_description( + 'programmable_private.assert_projector_runtime_lease_v1(text,bigint,bytea)'::regprocedure, + 'pg_proc' + ) + ), + 'same connection and inside the same transaction' + ) > 0, + 'assertion holds the singleton row lock through the writer transaction commit' +); + +select ok( + ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.projector_runtime_lease_current'::regclass + ) + and ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.projector_runtime_lease_history'::regclass + ), + 'current and history tables enforce RLS even for their owner' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_trigger + where tgrelid = + 'programmable_private.projector_runtime_lease_history'::regclass + and tgname = 'reject_immutable_mutation' + and not tgisinternal + ), + 'lease history rejects update and delete mutations' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector_runtime', + 'programmable_private.try_acquire_projector_runtime_lease_v1(text,bytea,timestamp with time zone,timestamp with time zone,bytea)', + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector_runtime', + 'programmable_private.assert_projector_runtime_lease_v1(text,bigint,bytea)', + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector_runtime', + 'programmable_private.release_projector_runtime_lease_v1(text,bigint,bytea,timestamp with time zone,bytea)', + 'EXECUTE' + ), + 'dedicated runtime capability owns acquire, assert, and release' +); + +select ok( + not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.try_acquire_projector_runtime_lease_v1(text,bytea,timestamp with time zone,timestamp with time zone,bytea)', + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.assert_projector_runtime_lease_v1(text,bigint,bytea)', + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.release_projector_runtime_lease_v1(text,bigint,bytea,timestamp with time zone,bytea)', + 'EXECUTE' + ), + 'projector writer receives only the transaction fencing assertion' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_api_reader', 'programmable_reconciler', + 'programmable_profile_binder', 'programmable_profile_recovery', + 'programmable_profile_writer', 'programmable_maintenance' + ]) as denied(role_name) + cross join pg_catalog.unnest(array[ + 'programmable_private.try_acquire_projector_runtime_lease_v1(text,bytea,timestamp with time zone,timestamp with time zone,bytea)'::regprocedure, + 'programmable_private.assert_projector_runtime_lease_v1(text,bigint,bytea)'::regprocedure, + 'programmable_private.release_projector_runtime_lease_v1(text,bigint,bytea,timestamp with time zone,bytea)'::regprocedure + ]) as protected(function_oid) + where pg_catalog.has_function_privilege( + denied.role_name, protected.function_oid, 'EXECUTE' + ) + ), + 'browser, service, reader, reconciler, and profile roles are denied' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector_runtime', 'programmable_projector', + 'programmable_api_reader', 'programmable_reconciler' + ]) as denied(role_name) + cross join pg_catalog.unnest(array[ + 'programmable_private.projector_runtime_lease_current'::regclass, + 'programmable_private.projector_runtime_lease_history'::regclass + ]) as protected(table_oid) + where pg_catalog.has_table_privilege( + denied.role_name, protected.table_oid, 'SELECT,INSERT,UPDATE,DELETE' + ) + ), + 'lease roles have no direct table privileges' +); + +select ok( + exists ( + select 1 + from programmable_private.projector_runtime_lease_current + where singleton_key = 'canonical-projector-runtime-v1' + and lease_generation = 0 + and holder_id is null + and lease_token_hash is null + ) + and ( + select count(*) = 1 + from programmable_private.projector_runtime_lease_current + ), + 'migration creates exactly one fixed empty singleton row' +); + +set local role programmable_projector_runtime; + +select throws_ok( + $sql$ + select * from programmable_private.try_acquire_projector_runtime_lease_v1( + 'invalid holder', decode(repeat('11', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '75 seconds', + decode(repeat('21', 32), 'hex') + ) + $sql$, + '22023', + 'invalid projector runtime lease acquisition', + 'holder identifiers use the bounded canonical grammar' +); + +select throws_ok( + $sql$ + select * from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-a', decode(repeat('00', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '75 seconds', + decode(repeat('22', 32), 'hex') + ) + $sql$, + '22023', + 'invalid projector runtime lease acquisition', + 'zero lease tokens are rejected' +); + +select throws_ok( + $sql$ + select * from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-a', decode(repeat('11', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '75 seconds', + decode(repeat('00', 32), 'hex') + ) + $sql$, + '22023', + 'invalid projector runtime lease acquisition', + 'zero acquisition commitments are rejected' +); + +select throws_ok( + $sql$ + select * from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-a', decode(repeat('11', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '91 seconds', + decode(repeat('23', 32), 'hex') + ) + $sql$, + '22023', + 'invalid projector runtime lease acquisition', + 'lease TTL cannot exceed ninety seconds' +); + +select is( + ( + select acquired + from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-a', decode(repeat('11', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '75 seconds', + decode(repeat('24', 32), 'hex') + ) + ), + true, + 'the first worker atomically acquires generation one' +); + +reset role; + +select ok( + exists ( + select 1 + from programmable_private.projector_runtime_lease_current + where singleton_key = 'canonical-projector-runtime-v1' + and lease_generation = 1 + and holder_id = 'worker-a' + and lease_token_hash = decode(repeat('11', 32), 'hex') + and released_at is null + and expires_at > acquired_at + and expires_at <= acquired_at + interval '90 seconds' + ), + 'generation one stores only the fenced server-time lease state' +); + +select ok( + exists ( + select 1 + from programmable_private.projector_runtime_lease_history as history + join programmable_private.mutation_audits as audit + on audit.audit_id = history.audit_id + where history.event_kind = 'acquired' + and history.lease_generation = 1 + and history.input_commitment = decode(repeat('24', 32), 'hex') + and audit.action = 'projector_runtime_lease.acquire' + and audit.caller_role = 'programmable_projector_runtime' + ), + 'successful acquisition appends immutable role-attributed evidence' +); + +set local role programmable_projector_runtime; + +select ok( + ( + select not acquired and lease_generation = 1 + from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-b', decode(repeat('12', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '75 seconds', + decode(repeat('25', 32), 'hex') + ) + ), + 'an unexpired holder makes a competing acquisition return busy' +); + +reset role; + +select ok( + ( + select count(*) = 1 + from programmable_private.projector_runtime_lease_history + ) + and ( + select count(*) = 1 + from programmable_private.mutation_audits + where action like 'projector_runtime_lease.%' + ), + 'a busy result leaves current state and audit history unchanged' +); + +set local role programmable_projector; + +select is( + programmable_private.assert_projector_runtime_lease_v1( + 'worker-a', 1, decode(repeat('11', 32), 'hex') + ), + true, + 'the projector writer can fence a commit with the exact live token' +); + +select is( + programmable_private.assert_projector_runtime_lease_v1( + 'worker-a', 1, decode(repeat('12', 32), 'hex') + ), + false, + 'a mismatched token cannot fence a projector commit' +); + +reset role; +set local role programmable_projector_runtime; + +select is( + programmable_private.release_projector_runtime_lease_v1( + 'worker-a', 1, decode(repeat('12', 32), 'hex'), + clock_timestamp(), decode(repeat('26', 32), 'hex') + ), + false, + 'a mismatched token cannot release the current lease' +); + +select is( + programmable_private.release_projector_runtime_lease_v1( + 'worker-a', 1, decode(repeat('11', 32), 'hex'), + clock_timestamp(), decode(repeat('27', 32), 'hex') + ), + true, + 'the exact holder can release its own generation' +); + +reset role; + +select ok( + exists ( + select 1 + from programmable_private.projector_runtime_lease_current + where lease_generation = 1 + and released_at is not null + and release_commitment = decode(repeat('27', 32), 'hex') + ) + and exists ( + select 1 + from programmable_private.projector_runtime_lease_history + where lease_generation = 1 + and event_kind = 'released' + and input_commitment = decode(repeat('27', 32), 'hex') + ), + 'release marks current state and appends immutable release evidence' +); + +set local role programmable_projector_runtime; + +select is( + programmable_private.release_projector_runtime_lease_v1( + 'worker-a', 1, decode(repeat('11', 32), 'hex'), + clock_timestamp(), decode(repeat('28', 32), 'hex') + ), + false, + 'a repeated release cannot append a second release event' +); + +select ok( + ( + select acquired and lease_generation = 2 + from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-b', decode(repeat('12', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '100 milliseconds', + decode(repeat('29', 32), 'hex') + ) + ), + 'a released lease can be taken over at the next generation' +); + +select is( + programmable_private.release_projector_runtime_lease_v1( + 'worker-a', 1, decode(repeat('11', 32), 'hex'), + clock_timestamp(), decode(repeat('2a', 32), 'hex') + ), + false, + 'generation one cannot release its generation-two successor' +); + +reset role; + +select ok( + exists ( + select 1 + from programmable_private.projector_runtime_lease_current + where lease_generation = 2 + and holder_id = 'worker-b' + and lease_token_hash = decode(repeat('12', 32), 'hex') + and released_at is null + ), + 'released takeover replaces current state without resetting generation' +); + +set local role programmable_projector; + +select is( + programmable_private.assert_projector_runtime_lease_v1( + 'worker-a', 1, decode(repeat('11', 32), 'hex') + ), + false, + 'generation one remains fenced after its successor acquires' +); + +select is( + programmable_private.assert_projector_runtime_lease_v1( + 'worker-b', 2, decode(repeat('12', 32), 'hex') + ), + true, + 'generation two can fence work while its short lease is live' +); + +reset role; +select pg_catalog.pg_sleep(0.2); +set local role programmable_projector; + +select is( + programmable_private.assert_projector_runtime_lease_v1( + 'worker-b', 2, decode(repeat('12', 32), 'hex') + ), + false, + 'an expired lease cannot fence a delayed commit' +); + +reset role; +set local role programmable_projector_runtime; + +select ok( + ( + select acquired and lease_generation = 3 + from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-c', decode(repeat('13', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '75 seconds', + decode(repeat('2b', 32), 'hex') + ) + ), + 'an expired crashed holder is recoverable at generation three' +); + +select is( + programmable_private.release_projector_runtime_lease_v1( + 'worker-b', 2, decode(repeat('12', 32), 'hex'), + clock_timestamp(), decode(repeat('2c', 32), 'hex') + ), + false, + 'an expired predecessor cannot release its successor' +); + +select throws_ok( + $sql$ + select programmable_private.release_projector_runtime_lease_v1( + 'worker-c', 3, decode(repeat('13', 32), 'hex'), + clock_timestamp() + interval '31 seconds', + decode(repeat('2d', 32), 'hex') + ) + $sql$, + '22023', + 'invalid projector runtime lease release', + 'release timestamps cannot be moved outside the server clock window' +); + +reset role; + +select ok( + ( + select count(*) = 3 + from programmable_private.projector_runtime_lease_history + where event_kind = 'acquired' + ) + and ( + select count(*) = 1 + from programmable_private.projector_runtime_lease_history + where event_kind = 'released' + ) + and ( + select pg_catalog.array_agg(lease_generation order by lease_generation) + = array[1::bigint, 2::bigint, 3::bigint] + from programmable_private.projector_runtime_lease_history + where event_kind = 'acquired' + ), + 'history records monotonic acquisition generations without stale events' +); + +select throws_ok( + $sql$ + update programmable_private.projector_runtime_lease_history + set event_kind = 'released' + where lease_generation = 2 and event_kind = 'acquired' + $sql$, + '55000', + 'lease history updates are rejected' +); + +select throws_ok( + $sql$ + delete from programmable_private.projector_runtime_lease_history + where lease_generation = 2 and event_kind = 'acquired' + $sql$, + '55000', + 'lease history deletes are rejected' +); + +set local role programmable_projector_runtime; + +select throws_ok( + $sql$ + update programmable_private.projector_runtime_lease_current + set lease_generation = 99 + $sql$, + '42501', + 'runtime capability cannot mutate lease tables directly' +); + +reset role; +set local role programmable_projector_runtime_login; + +select throws_ok( + $sql$ + select * from programmable_private.try_acquire_projector_runtime_lease_v1( + 'worker-d', decode(repeat('14', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '75 seconds', + decode(repeat('2e', 32), 'hex') + ) + $sql$, + '42501', + 'the NOINHERIT login cannot acquire before explicit SET ROLE' +); + +reset role; +set local role programmable_projector_runtime; + +select is( + programmable_private.assert_projector_runtime_lease_v1( + 'worker-c', 3, decode(repeat('13', 32), 'hex') + ), + true, + 'the dedicated runtime capability can assert its current fence' +); + +reset role; + +select ok( + not exists ( + select 1 + from programmable_private.projector_runtime_lease_history + where lease_token_hash = decode(repeat('00', 32), 'hex') + or input_commitment = decode(repeat('00', 32), 'hex') + ) + and not exists ( + select 1 + from programmable_private.mutation_audits + where action like 'projector_runtime_lease.%' + and input_commitment = decode(repeat('00', 32), 'hex') + ), + 'lease evidence contains no zero token or input commitments' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/010_release_probe_nonce_consumption.test.sql b/supabase/tests/database/010_release_probe_nonce_consumption.test.sql new file mode 100644 index 00000000..03e161d5 --- /dev/null +++ b/supabase/tests/database/010_release_probe_nonce_consumption.test.sql @@ -0,0 +1,514 @@ +begin; + +select plan(26); + +select ok( + exists ( + select 1 + from pg_catalog.pg_roles + where rolname = 'programmable_release_probe_nonce' + and not rolcanlogin and not rolinherit and not rolsuper + and not rolcreatedb and not rolcreaterole and not rolreplication + and not rolbypassrls + ) + and exists ( + select 1 + from pg_catalog.pg_roles + where rolname = 'programmable_release_probe_nonce_login' + and rolcanlogin and not rolinherit and not rolsuper + and not rolcreatedb and not rolcreaterole and not rolreplication + and not rolbypassrls + ) + and ( + select rolpassword is null + from pg_catalog.pg_authid + where rolname = 'programmable_release_probe_nonce_login' + ) + and not exists ( + select 1 + from pg_catalog.pg_auth_members as membership + join pg_catalog.pg_roles as member_role + on member_role.oid = membership.member + where member_role.rolname = 'programmable_release_probe_nonce' + ), + 'nonce capability is NOLOGIN and its NOINHERIT gateway starts passwordless' +); + +select is( + ( + select pg_catalog.array_agg( + member_role.rolname || '->' || granted_role.rolname + order by member_role.rolname, granted_role.rolname + ) + from pg_catalog.pg_auth_members as membership + join pg_catalog.pg_roles as member_role + on member_role.oid = membership.member + join pg_catalog.pg_roles as granted_role + on granted_role.oid = membership.roleid + where member_role.rolname = 'programmable_release_probe_nonce_login' + and not membership.admin_option + and not membership.inherit_option + and membership.set_option + ), + array[ + 'programmable_release_probe_nonce_login->programmable_release_probe_nonce' + ]::text[], + 'the gateway has one SET-only non-admin capability membership' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_namespace as namespace + join pg_catalog.pg_roles as owner_role + on owner_role.oid = namespace.nspowner + where namespace.nspname = 'programmable_release_probe_private' + and owner_role.rolname = 'programmable_migrator' + ) + and has_schema_privilege( + 'programmable_release_probe_nonce', + 'programmable_release_probe_private', + 'USAGE' + ) + and not has_schema_privilege( + 'programmable_release_probe_nonce', + 'programmable_release_probe_private', + 'CREATE' + ) + and not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_api_reader', 'programmable_projector', + 'programmable_reconciler', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_api_reader_login', + 'programmable_projector_login', 'programmable_reconciler_login', + 'programmable_release_probe_nonce_login' + ]) as checked_role(role_name) + where has_schema_privilege( + checked_role.role_name, + 'programmable_release_probe_private', + 'USAGE' + ) or has_schema_privilege( + checked_role.role_name, + 'programmable_release_probe_private', + 'CREATE' + ) + ) + and not exists ( + select 1 + from pg_catalog.pg_default_acl as defaults + join pg_catalog.pg_roles as owner_role + on owner_role.oid = defaults.defaclrole + join pg_catalog.pg_namespace as namespace + on namespace.oid = defaults.defaclnamespace + cross join lateral pg_catalog.aclexplode(defaults.defaclacl) as acl + left join pg_catalog.pg_roles as grantee on grantee.oid = acl.grantee + where owner_role.rolname = 'programmable_migrator' + and namespace.nspname = 'programmable_release_probe_private' + and ( + acl.grantee = 0 + or grantee.rolname <> 'programmable_migrator' + ) + ), + 'the migrator owns a deny-by-default schema crossed only by the capability' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_constraint + where conrelid = + 'programmable_release_probe_private.release_probe_nonce_consumptions_v1'::regclass + and contype = 'p' + and pg_catalog.pg_get_constraintdef(oid) = + 'PRIMARY KEY (route_key, nonce_digest)' + ) + and exists ( + select 1 + from pg_catalog.pg_indexes + where schemaname = 'programmable_release_probe_private' + and indexname = 'release_probe_nonce_consumptions_v1_expiry_idx' + and indexdef like '%(route_key, expires_at, nonce_digest)%' + ) + and ( + select pg_catalog.array_agg( + enum_value.enumlabel::text order by enum_value.enumsortorder + ) + from pg_catalog.pg_enum as enum_value + where enum_value.enumtypid = + 'programmable_release_probe_private.release_probe_route_key_v1'::regtype + ) = array[ + 'explore-list', 'explore-token', 'explore-chart', + 'creator-profile', 'classic-v3-profile', 'launch-lookup' + ]::text[] + and not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_api_reader', 'programmable_projector', + 'programmable_reconciler', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_api_reader_login', + 'programmable_projector_login', 'programmable_reconciler_login', + 'programmable_release_probe_nonce', + 'programmable_release_probe_nonce_login' + ]) as checked_role(role_name) + where pg_catalog.has_type_privilege( + checked_role.role_name, + 'programmable_release_probe_private.release_probe_route_key_v1', + 'USAGE' + ) + ), + 'nonce rows have the exact replay key and bounded-pruning access path' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_roles as owner_role on owner_role.oid = class.relowner + where class.oid = + 'programmable_release_probe_private.release_probe_nonce_consumptions_v1'::regclass + and class.relrowsecurity and class.relforcerowsecurity + and owner_role.rolname = 'programmable_migrator' + ) + and exists ( + select 1 + from pg_catalog.pg_policy as policy + where policy.polrelid = + 'programmable_release_probe_private.release_probe_nonce_consumptions_v1'::regclass + and policy.polcmd = '*' + and policy.polroles = array[ + ( + select oid from pg_catalog.pg_roles + where rolname = 'programmable_migrator' + ) + ]::oid[] + ), + 'the private nonce table is migrator-owned with forced owner-only RLS' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_api_reader', 'programmable_projector', + 'programmable_reconciler', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_api_reader_login', + 'programmable_projector_login', 'programmable_reconciler_login', + 'programmable_release_probe_nonce', + 'programmable_release_probe_nonce_login' + ]) as checked_role(role_name) + where pg_catalog.has_table_privilege( + checked_role.role_name, + 'programmable_release_probe_private.release_probe_nonce_consumptions_v1', + 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE' + ) + ), + 'no browser, service, gateway or capability role has base-table access' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_roles as owner_role + on owner_role.oid = procedure.proowner + where procedure.oid = + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure + and procedure.prosecdef + and procedure.provolatile = 'v' + and 'search_path=""' = any(procedure.proconfig) + and owner_role.rolname = 'programmable_migrator' + ), + 'the frozen nonce API is a volatile, empty-search-path migrator definer' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_release_probe_nonce', + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)', + 'EXECUTE' + ) + and not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_api_reader', 'programmable_projector', + 'programmable_reconciler', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_api_reader_login', + 'programmable_projector_login', 'programmable_reconciler_login', + 'programmable_release_probe_nonce_login' + ]) as checked_role(role_name) + where pg_catalog.has_function_privilege( + checked_role.role_name, + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)', + 'EXECUTE' + ) + ), + 'only the dedicated capability can execute nonce consumption' +); + +select ok( + pg_catalog.obj_description( + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure, + 'pg_proc' + ) like '%session_user programmable_release_probe_nonce_login%' + and pg_catalog.obj_description( + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure, + 'pg_proc' + ) like '%verify current_role%' + and pg_catalog.obj_description( + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure, + 'pg_proc' + ) like '%SET LOCAL ROLE programmable_release_probe_nonce%', + 'database documentation freezes the exact gateway session and role preflight' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'on conflict (route_key, nonce_digest) do nothing' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'pg_catalog.pg_advisory_xact_lock(1347571538, route_lock_slot)' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'limit 256' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_release_probe_private.consume_release_probe_nonce_v1(text,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'limit 4096' + ) > 0, + 'atomic insert, route serialization, bounded prune and hard capacity are structural' +); + +set role programmable_release_probe_nonce; +select throws_ok( + $sql$ + select programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('01', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ) + $sql$, + '42501', + 'a privileged postgres session cannot impersonate only the capability role' +); +reset role; + +set session authorization programmable_release_probe_nonce_login; +set role programmable_release_probe_nonce; + +select ok( + session_user::text = 'programmable_release_probe_nonce_login' + and current_role::text = 'programmable_release_probe_nonce', + 'runtime preflight observes the exact login and explicitly selected role' +); + +select ok( + programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('11', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ), + 'a fresh valid nonce is consumed once' +); + +select ok( + not programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('11', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ), + 'a replay on the same route returns false' +); + +select ok( + programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-token', decode(repeat('11', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ), + 'the same digest is independent across supported routes' +); + +select throws_ok( + $sql$ + select programmable_release_probe_private.consume_release_probe_nonce_v1( + 'unsupported', decode(repeat('12', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ) + $sql$, + '22023', + 'unsupported routes are rejected' +); + +select throws_ok( + $sql$ + select programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('12', 31), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ) + $sql$, + '22023', + 'non-SHA-256 digest lengths are rejected' +); + +select throws_ok( + $sql$ + select programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('13', 32), 'hex'), + clock_timestamp() - interval '2 minutes', + clock_timestamp() - interval '1 minute' + ) + $sql$, + '22023', + 'expired envelopes are rejected using database time' +); + +select throws_ok( + $sql$ + select programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('14', 32), 'hex'), + clock_timestamp() + interval '31 seconds', + clock_timestamp() + interval '2 minutes' + ) + $sql$, + '22023', + 'issued-at future skew is limited to thirty seconds' +); + +select throws_ok( + $sql$ + select programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-list', decode(repeat('15', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '5 minutes 1 second' + ) + $sql$, + '22023', + 'nonce TTL is capped at five minutes' +); + +reset role; +set session authorization postgres; + +set role programmable_migrator; +insert into programmable_release_probe_private.release_probe_nonce_consumptions_v1 ( + route_key, nonce_digest, issued_at, expires_at, consumed_at +) +select + 'explore-chart', + decode(pg_catalog.lpad(pg_catalog.to_hex(series.value), 64, '0'), 'hex'), + clock_timestamp() - interval '10 minutes', + clock_timestamp() - interval '9 minutes', + clock_timestamp() - interval '9 minutes 30 seconds' +from pg_catalog.generate_series(1, 300) as series(value); + +insert into programmable_release_probe_private.release_probe_nonce_consumptions_v1 ( + route_key, nonce_digest, issued_at, expires_at, consumed_at +) values ( + 'explore-chart', decode(repeat('ab', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '4 minutes', + clock_timestamp() +); +reset role; + +set session authorization programmable_release_probe_nonce_login; +set role programmable_release_probe_nonce; +select ok( + programmable_release_probe_private.consume_release_probe_nonce_v1( + 'explore-chart', decode(repeat('ee', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ), + 'a valid call runs the bounded expiry prune before consumption' +); +reset role; +set session authorization postgres; + +select is( + ( + select pg_catalog.count(*) + from programmable_release_probe_private.release_probe_nonce_consumptions_v1 + where route_key = 'explore-chart' + and expires_at <= clock_timestamp() + ), + 44::bigint, + 'one call prunes at most 256 of 300 expired rows' +); + +select ok( + exists ( + select 1 + from programmable_release_probe_private.release_probe_nonce_consumptions_v1 + where route_key = 'explore-chart' + and nonce_digest = decode(repeat('ab', 32), 'hex') + and expires_at > clock_timestamp() + ), + 'bounded pruning never removes an unexpired row' +); + +set role programmable_migrator; +insert into programmable_release_probe_private.release_probe_nonce_consumptions_v1 ( + route_key, nonce_digest, issued_at, expires_at, consumed_at +) +select + 'launch-lookup', + decode(pg_catalog.lpad(pg_catalog.to_hex(series.value), 64, '0'), 'hex'), + clock_timestamp(), + clock_timestamp() + interval '4 minutes', + clock_timestamp() +from pg_catalog.generate_series(1, 4096) as series(value); +reset role; + +set session authorization programmable_release_probe_nonce_login; +set role programmable_release_probe_nonce; +select ok( + not programmable_release_probe_private.consume_release_probe_nonce_v1( + 'launch-lookup', decode(repeat('ff', 32), 'hex'), + clock_timestamp(), clock_timestamp() + interval '2 minutes' + ), + 'the per-route hard ceiling rejects growth beyond 4096 retained rows' +); +reset role; +set session authorization postgres; + +select is( + ( + select pg_catalog.count(*) + from programmable_release_probe_private.release_probe_nonce_consumptions_v1 + where route_key = 'launch-lookup' + ), + 4096::bigint, + 'capacity rejection leaves the bounded route state unchanged' +); + +set role programmable_migrator; +select throws_ok( + $sql$ + insert into programmable_release_probe_private.release_probe_nonce_consumptions_v1 ( + route_key, nonce_digest, issued_at, expires_at, consumed_at + ) values ( + 'explore-list', decode(repeat('99', 32), 'hex'), + '-infinity', clock_timestamp() + interval '1 minute', + clock_timestamp() + ) + $sql$, + '23514', + 'table constraints reject non-finite timestamp state' +); +reset role; + +select * from finish(); +rollback; diff --git a/supabase/tests/database/011_reconciler_preparity_contract.test.sql b/supabase/tests/database/011_reconciler_preparity_contract.test.sql new file mode 100644 index 00000000..de90ec9c --- /dev/null +++ b/supabase/tests/database/011_reconciler_preparity_contract.test.sql @@ -0,0 +1,361 @@ +begin; + +select plan(21); + +select ok( + to_regprocedure( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)' + ) is not null + and to_regprocedure( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)' + ) is not null, + 'the narrow read and atomic append signatures exist' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + join pg_catalog.pg_roles as owner_role + on owner_role.oid = procedure.proowner + where namespace.nspname = 'programmable_private' + and procedure.proname in ( + 'get_reconciler_preparity_contract_v1', + 'commit_reconciler_preparity_result_v1' + ) + and owner_role.rolname <> 'programmable_migrator' + ), + 'both contracts are owned by the migration role' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname in ( + 'get_reconciler_preparity_contract_v1', + 'commit_reconciler_preparity_result_v1' + ) + and ( + not procedure.prosecdef + or not ('search_path=""' = any(procedure.proconfig)) + ) + ), + 'both contracts are SECURITY DEFINER with an empty search path' +); + +select ok( + pg_catalog.obj_description( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure, + 'pg_proc' + ) like '%does not require or manufacture prior parity%' + and pg_catalog.obj_description( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure, + 'pg_proc' + ) like '%Atomically appends%terminal outcome%' + , + 'catalog comments state the bootstrap and atomicity boundaries' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure, + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'the reconciler capability can execute both contracts' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector', 'programmable_api_reader', + 'programmable_profile_binder', 'programmable_profile_recovery', + 'programmable_profile_writer', 'programmable_maintenance' + ]) as denied(role_name) + cross join pg_catalog.unnest(array[ + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure, + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ]) as protected(function_oid) + where pg_catalog.has_function_privilege( + denied.role_name, protected.function_oid, 'EXECUTE' + ) + ), + 'browser, service, projector, reader, profile and maintenance roles are denied' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'programmable_private.projector_checkpoints'::regclass, + 'programmable_private.projection_fold_manifests'::regclass, + 'programmable_private.projection_entity_current'::regclass, + 'programmable_private.route_eligibility_current'::regclass, + 'programmable_private.reconciliation_records'::regclass, + 'programmable_private.parity_records'::regclass, + 'programmable_private.route_checkpoint_parity_bindings'::regclass + ]) as protected(table_oid) + where pg_catalog.has_table_privilege( + 'programmable_reconciler', protected.table_oid, + 'SELECT,INSERT,UPDATE,DELETE' + ) + ), + 'the new contract grants no general table access' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'assert_caller(''programmable_reconciler'')' + ) > 0, + 'the read contract verifies the active reconciler capability' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'projector_checkpoint_current' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'release_epoch_current' + ) > 0, + 'the read contract requires the exact current checkpoint and epoch' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'route_eligibility_current_exact_v1' + ) > 0, + 'route coverage is selected through the exact checkpoint boundary' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'parity_records' + ) = 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'route_snapshot_readiness_v1' + ) = 0, + 'pre-parity reads have no dependency on an earlier parity record' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'p_maximum_entity_count > 10000' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_preparity_contract_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'reconciler pre-parity entity limit exceeded' + ) > 0, + 'the only pre-parity data window is explicitly bounded' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'open_run(' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'append_reconciliation_record(' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'append_parity_record(' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'bind_route_checkpoint_parity_v1(' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'append_run_outcome(' + ) > 0, + 'the writer contains the complete append and binding sequence' +); + +select ok( + pg_catalog.strpos( + pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + )), + 'for share of current_checkpoint, checkpoint' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'reconciler epoch changed before commit' + ) > 0, + 'the writer fences concurrent epoch and checkpoint changes' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'p_route_keys is distinct from expected_route_keys' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'for route_index in 1..expected_route_count' + ) > 0, + 'route coverage and order are closed over the exact release route matrix' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'when mismatch_count = 0 then ''succeeded''' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.commit_reconciler_preparity_result_v1(uuid,uuid,uuid[],uuid[],uuid,bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,text,text[],bytea[],bytea[],bytea[],bytea[],bytea,bytea,bytea,timestamp with time zone,timestamp with time zone,timestamp with time zone)'::regprocedure + ), + 'else ''failed''' + ) > 0, + 'a mismatch can never produce a successful terminal outcome' +); + +set local role programmable_reconciler; + +select throws_ok( + $sql$ + select * + from programmable_private.get_reconciler_preparity_contract_v1( + 1, 'classic-v3', 'classic', 'core', + '11000000-0000-0000-0000-000000000001', 1, + '11000000-0000-0000-0000-000000000002', 1.5, + decode(repeat('11', 32), 'hex'), 100 + ) + $sql$, + '22023', + 'fractional checkpoint input is rejected before any private read' +); + +select throws_ok( + $sql$ + select * + from programmable_private.get_reconciler_preparity_contract_v1( + 1, 'classic-v3', 'classic', 'core', + '11000000-0000-0000-0000-000000000001', 1, + '11000000-0000-0000-0000-000000000002', 100, + null::bytea, 100 + ) + $sql$, + '22023', + 'null checkpoint evidence is rejected before any private read' +); + +select throws_ok( + $sql$ + select programmable_private.commit_reconciler_preparity_result_v1( + '12000000-0000-0000-0000-000000000001', + '12000000-0000-0000-0000-000000000002', + array[]::uuid[], array[]::uuid[], + '12000000-0000-0000-0000-000000000003', + 1, 'classic-v3', 'classic', 'core', + '12000000-0000-0000-0000-000000000004', 1, + '12000000-0000-0000-0000-000000000005', 100, + decode(repeat('12', 32), 'hex'), 'reconciler-v1', + array['explore-list']::text[], + array[]::bytea[], array[]::bytea[], array[]::bytea[], array[]::bytea[], + decode(repeat('13', 32), 'hex'), + decode(repeat('14', 32), 'hex'), + decode(repeat('15', 32), 'hex'), + '2026-08-01T00:00:00Z', '2026-08-01T00:00:01Z', + '2026-08-01T00:00:02Z' + ) + $sql$, + '22023', + 'partial route coverage is rejected before a run is opened' +); + +reset role; +set local role programmable_api_reader; + +select throws_ok( + $sql$ + select * + from programmable_private.get_reconciler_preparity_contract_v1( + 1, 'classic-v3', 'classic', 'core', + '13000000-0000-0000-0000-000000000001', 1, + '13000000-0000-0000-0000-000000000002', 100, + decode(repeat('13', 32), 'hex'), 100 + ) + $sql$, + '42501', + 'the API reader cannot execute the pre-parity reader' +); + +select throws_ok( + $sql$ + select * from programmable_private.projection_entity_current + $sql$, + '42501', + 'the API reader remains unable to read projection state directly' +); + +reset role; + +select is( + ( + select pg_catalog.count(*) + from programmable_private.run_headers + where run_id = '12000000-0000-0000-0000-000000000001' + ), + 0::bigint, + 'a rejected atomic call leaves no partial run header' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/012_market_projector_contract.test.sql b/supabase/tests/database/012_market_projector_contract.test.sql new file mode 100644 index 00000000..7e8ba988 --- /dev/null +++ b/supabase/tests/database/012_market_projector_contract.test.sql @@ -0,0 +1,667 @@ +begin; + +select plan(35); + +select ok( + to_regclass( + 'programmable_private.market_projector_runtime_lease_current' + ) is not null + and to_regclass( + 'programmable_private.market_projector_runtime_lease_history' + ) is not null, + 'market projector owns a durable singleton lease and immutable history' +); + +select ok( + ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.market_projector_runtime_lease_current'::regclass + ) + and ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.market_projector_runtime_lease_history'::regclass + ) + and exists ( + select 1 from pg_catalog.pg_trigger + where tgrelid = + 'programmable_private.market_projector_runtime_lease_history'::regclass + and tgname = 'reject_immutable_mutation' + and not tgisinternal + ), + 'market lease relations force RLS and retain immutable history' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.try_acquire_market_projector_runtime_lease_v1(text,bytea,timestamp with time zone,timestamp with time zone,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.try_acquire_market_projector_runtime_lease_v1(text,bytea,timestamp with time zone,timestamp with time zone,bytea)'::regprocedure, + 'EXECUTE' + ), + 'only the reconciler capability can acquire the market runtime lease' +); + +select ok( + to_regclass('programmable_private.market_projector_cursor_history') + is not null + and to_regclass('programmable_private.market_projector_cursor_current') + is not null, + 'market projector owns explicit current and append-only cursor relations' +); + +select ok( + ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.market_projector_cursor_history'::regclass + ) + and ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.market_projector_cursor_current'::regclass + ), + 'market cursor relations force RLS' +); + +select ok( + to_regclass( + 'programmable_private.market_snapshot_lineage_memberships' + ) is not null + and to_regclass( + 'programmable_private.market_candle_lineage_memberships' + ) is not null, + 'snapshot and candle facts have explicit reorg-lineage memberships' +); + +select ok( + ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.market_snapshot_lineage_memberships'::regclass + ) + and ( + select relrowsecurity and relforcerowsecurity + from pg_catalog.pg_class + where oid = + 'programmable_private.market_candle_lineage_memberships'::regclass + ), + 'market fact lineage relations force RLS' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_trigger + where tgrelid = + 'programmable_private.market_projector_cursor_history'::regclass + and tgname = 'reject_immutable_mutation' + and not tgisinternal + ), + 'market cursor history is immutable' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_trigger + where tgrelid = + 'programmable_private.market_snapshot_lineage_memberships'::regclass + and tgname = 'reject_immutable_mutation' + and not tgisinternal + ) + and exists ( + select 1 + from pg_catalog.pg_trigger + where tgrelid = + 'programmable_private.market_candle_lineage_memberships'::regclass + and tgname = 'reject_immutable_mutation' + and not tgisinternal + ), + 'market fact lineage memberships are immutable' +); + +select ok( + exists ( + select 1 + from pg_catalog.pg_constraint + where conrelid = + 'programmable_private.global_eth_usd_snapshots'::regclass + and contype = 'u' + and pg_catalog.pg_get_constraintdef(oid) = + 'UNIQUE (epoch_id, pointer_generation, block_hash)' + ) + and exists ( + select 1 + from pg_catalog.pg_constraint + where conrelid = + 'programmable_private.global_eth_usd_snapshots'::regclass + and contype = 'u' + and pg_catalog.pg_get_constraintdef(oid) = + 'UNIQUE (epoch_id, pointer_generation, result_commitment)' + ), + 'global prices remain singular for an exact epoch, pointer, and block' +); + +select ok( + exists ( + select 1 from pg_catalog.pg_constraint + where conname = 'market_snapshots_reconciliation_fact_key' + and conrelid = 'programmable_private.market_snapshots'::regclass + and pg_catalog.strpos( + pg_catalog.pg_get_constraintdef(oid), 'reconciliation_id' + ) > 0 + ) + and exists ( + select 1 from pg_catalog.pg_constraint + where conname = 'market_candles_reconciliation_fact_key' + and conrelid = 'programmable_private.market_candles'::regclass + and pg_catalog.strpos( + pg_catalog.pg_get_constraintdef(oid), 'reconciliation_id' + ) > 0 + ) + and exists ( + select 1 from pg_catalog.pg_constraint + where conname = 'market_block_closes_reconciliation_block_key' + and conrelid = 'programmable_private.market_block_closes'::regclass + and pg_catalog.strpos( + pg_catalog.pg_get_constraintdef(oid), 'reconciliation_id' + ) > 0 + ), + 'pool market facts can be replayed into a new reconciliation lineage' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_snapshots_v1'::regclass, true + ), 'market_snapshot_lineage_memberships') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_snapshots_v1'::regclass, true + ), 'current_cursor.reorg_generation = membership.reorg_generation') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_candles_v1'::regclass, true + ), 'market_candle_lineage_memberships') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_candles_v1'::regclass, true + ), 'current_cursor.reorg_generation = membership.reorg_generation') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_snapshots_v1'::regclass, true + ), 'source_tip_checkpoint.reorg_generation = cursor_history.source_reorg_generation') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_candles_v1'::regclass, true + ), 'source_tip_checkpoint.reorg_generation = cursor_history.source_reorg_generation') > 0, + 'published market facts require active market and source reorg lineages' +); + +select ok( + to_regprocedure( + 'programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)' + ) is not null + and to_regprocedure( + 'programmable_private.resolve_market_block_evidence_v1(uuid,numeric,bytea,uuid,uuid)' + ) is not null + and to_regprocedure( + 'programmable_private.resolve_market_close_anchor_v1(uuid,bytea,numeric,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.get_market_block_evidence_context_v1(uuid,uuid)' + ) is not null + and to_regprocedure( + 'programmable_private.get_market_global_snapshot_v1(uuid,uuid)' + ) is not null + and to_regprocedure( + 'programmable_private.list_market_close_anchors_v1(bigint,text,text,text,text,bytea,numeric,numeric,integer,numeric)' + ) is not null + and to_regprocedure( + 'programmable_private.resolve_market_candle_close_v1(uuid,bytea,timestamp with time zone,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.append_market_snapshot_v2(uuid,uuid,uuid,uuid,bytea,numeric,bytea,numeric,numeric,numeric,numeric,numeric,numeric,timestamp with time zone,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.append_market_snapshot_details_v2(uuid,uuid,text,bigint,uuid,integer,numeric,numeric,numeric,numeric,numeric,bigint,bytea,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.append_market_block_close_v2(uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid,bytea,bytea,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.get_market_projector_cursor_v1(bigint,text,text,text,text,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)' + ) is not null + and to_regprocedure( + 'programmable_private.append_market_candle_v2(uuid,uuid,uuid,uuid,bytea,text,timestamp with time zone,timestamp with time zone,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bytea,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)' + ) is not null, + 'the complete narrow market projector contract exists' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname in ( + 'resolve_market_graph_provider_v1', + 'list_market_projector_pools_v1', + 'resolve_market_block_evidence_v1', + 'resolve_market_close_anchor_v1', + 'get_market_block_evidence_context_v1', + 'get_market_global_snapshot_v1', + 'list_market_close_anchors_v1', + 'resolve_market_candle_close_v1', + 'append_market_snapshot_v2', + 'append_market_snapshot_details_v2', + 'append_market_block_close_v2', + 'get_market_projector_cursor_v1', + 'advance_market_projector_cursor_v1', + 'append_market_candle_v2', + 'append_market_candle_details_v2' + ) + and ( + not procedure.prosecdef + or not ('search_path=""' = any(procedure.proconfig)) + ) + ), + 'all market projector entrypoints are SECURITY DEFINER with empty search path' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea)'::regprocedure, + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure, + 'programmable_private.resolve_market_block_evidence_v1(uuid,numeric,bytea,uuid,uuid)'::regprocedure, + 'programmable_private.resolve_market_close_anchor_v1(uuid,bytea,numeric,bytea)'::regprocedure, + 'programmable_private.get_market_block_evidence_context_v1(uuid,uuid)'::regprocedure, + 'programmable_private.get_market_global_snapshot_v1(uuid,uuid)'::regprocedure, + 'programmable_private.list_market_close_anchors_v1(bigint,text,text,text,text,bytea,numeric,numeric,integer,numeric)'::regprocedure, + 'programmable_private.resolve_market_candle_close_v1(uuid,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure, + 'programmable_private.append_market_snapshot_v2(uuid,uuid,uuid,uuid,bytea,numeric,bytea,numeric,numeric,numeric,numeric,numeric,numeric,timestamp with time zone,bytea)'::regprocedure, + 'programmable_private.append_market_snapshot_details_v2(uuid,uuid,text,bigint,uuid,integer,numeric,numeric,numeric,numeric,numeric,bigint,bytea,timestamp with time zone)'::regprocedure, + 'programmable_private.append_market_block_close_v2(uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid,bytea,bytea,timestamp with time zone)'::regprocedure, + 'programmable_private.get_market_projector_cursor_v1(bigint,text,text,text,text,bytea)'::regprocedure, + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure, + 'programmable_private.append_market_candle_v2(uuid,uuid,uuid,uuid,bytea,text,timestamp with time zone,timestamp with time zone,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bytea,bytea)'::regprocedure, + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ]) as function(oid) + where not pg_catalog.has_function_privilege( + 'programmable_reconciler', function.oid, 'EXECUTE' + ) + ), + 'reconciler capability can execute every market projector entrypoint' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector', 'programmable_api_reader', + 'programmable_projector_runtime', 'programmable_maintenance' + ]) as denied(role_name) + cross join pg_catalog.unnest(array[ + 'programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea)'::regprocedure, + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure, + 'programmable_private.resolve_market_block_evidence_v1(uuid,numeric,bytea,uuid,uuid)'::regprocedure, + 'programmable_private.resolve_market_close_anchor_v1(uuid,bytea,numeric,bytea)'::regprocedure, + 'programmable_private.get_market_block_evidence_context_v1(uuid,uuid)'::regprocedure, + 'programmable_private.get_market_global_snapshot_v1(uuid,uuid)'::regprocedure, + 'programmable_private.list_market_close_anchors_v1(bigint,text,text,text,text,bytea,numeric,numeric,integer,numeric)'::regprocedure, + 'programmable_private.resolve_market_candle_close_v1(uuid,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure, + 'programmable_private.append_market_snapshot_v2(uuid,uuid,uuid,uuid,bytea,numeric,bytea,numeric,numeric,numeric,numeric,numeric,numeric,timestamp with time zone,bytea)'::regprocedure, + 'programmable_private.append_market_snapshot_details_v2(uuid,uuid,text,bigint,uuid,integer,numeric,numeric,numeric,numeric,numeric,bigint,bytea,timestamp with time zone)'::regprocedure, + 'programmable_private.append_market_block_close_v2(uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid,bytea,bytea,timestamp with time zone)'::regprocedure, + 'programmable_private.get_market_projector_cursor_v1(bigint,text,text,text,text,bytea)'::regprocedure, + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure, + 'programmable_private.append_market_candle_v2(uuid,uuid,uuid,uuid,bytea,text,timestamp with time zone,timestamp with time zone,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bytea,bytea)'::regprocedure, + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ]) as protected(function_oid) + where pg_catalog.has_function_privilege( + denied.role_name, protected.function_oid, 'EXECUTE' + ) + ), + 'browser, service, projector, reader, and maintenance roles are denied' +); + +select ok( + not pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.attach_market_snapshot_lineage_v1(uuid,text,bigint,uuid,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.attach_market_candle_lineage_v1(uuid,text,bigint,uuid,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.market_fact_reconciliation_usable_v1(uuid,uuid)'::regprocedure, + 'EXECUTE' + ), + 'fact lineage helpers cannot be invoked as standalone runtime capabilities' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_reconciler', 'programmable_projector', + 'programmable_api_reader', 'programmable_projector_runtime' + ]) as denied(role_name) + cross join pg_catalog.unnest(array[ + 'programmable_private.market_projector_cursor_history'::regclass, + 'programmable_private.market_projector_cursor_current'::regclass, + 'programmable_private.market_snapshot_lineage_memberships'::regclass, + 'programmable_private.market_candle_lineage_memberships'::regclass + ]) as protected(table_oid) + where pg_catalog.has_table_privilege( + denied.role_name, protected.table_oid, 'SELECT,INSERT,UPDATE,DELETE' + ) + ), + 'no runtime role has direct market cursor table access' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea)'::regprocedure + ), 'provider_type = ''uniswap_subgraph''') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea)'::regprocedure + ), 'deployment_commitment = p_deployment_commitment') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_graph_provider_v1(text,bytea,bytea)'::regprocedure + ), 'schema_commitment = p_schema_commitment') > 0, + 'Graph provider resolution binds type, deployment, and schema commitments' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + ), 'projector_checkpoint_current') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + ), 'market_projector_cursor_current') > 0 + and pg_catalog.strpos(pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + )), 'cursor_history.advanced_at asc nulls first') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + ), 'pending_occurrence.block_number > cursor_history.block_number') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + ), 'pending_materialization.decoded_payload ->> ''poolId''') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + ), 'chain_event_current_canonical') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + ), 'projected_close.last_source_occurrence_id') > 0, + 'pool discovery is bounded, fair, and pending only for pool-specific events' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_projector_pools_v1(bigint,text,text,text,text,text,integer)'::regprocedure + ), 'launch_by_token_v2') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_snapshots_v1'::regclass, true + ), 'launch_by_token_v1') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_viewdef( + 'programmable_private.market_candles_v1'::regclass, true + ), 'launch_by_token_v1') > 0, + 'market discovery requires liquidity-complete launches and publication stays on gated launch views' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_block_evidence_v1(uuid,numeric,bytea,uuid,uuid)'::regprocedure + ), 'ambiguous market block identity') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_block_evidence_v1(uuid,numeric,bytea,uuid,uuid)'::regprocedure + ), 'observation.provider_a_id = p_provider_a_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_block_evidence_v1(uuid,numeric,bytea,uuid,uuid)'::regprocedure + ), 'observation.provider_b_id = p_provider_b_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.get_market_block_evidence_context_v1(uuid,uuid)'::regprocedure + ), 'rpc_provider_deployment_metadata') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.get_market_block_evidence_context_v1(uuid,uuid)'::regprocedure + ), 'metadata_a.vendor_order = 1') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.get_market_block_evidence_context_v1(uuid,uuid)'::regprocedure + ), 'metadata_b.vendor_order = 2') > 0, + 'block evidence binds hashes and exact ordered RPC endpoint evidence' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_close_anchor_v1(uuid,bytea,numeric,bytea)'::regprocedure + ), 'chain_event_current_canonical') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_close_anchor_v1(uuid,bytea,numeric,bytea)'::regprocedure + ), 'is_market_fee_event_v1') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_close_anchor_v1(uuid,bytea,numeric,bytea)'::regprocedure + ), 'decoded_payload ->> ''poolId''') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_close_anchors_v1(bigint,text,text,text,text,bytea,numeric,numeric,integer,numeric)'::regprocedure + ), 'chain_event_current_canonical') > 0 + and pg_catalog.strpos(pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.list_market_close_anchors_v1(bigint,text,text,text,text,bytea,numeric,numeric,integer,numeric)'::regprocedure + )), 'order by occurrence.block_number') > 0, + 'close anchors and pages are canonical, exact-pool, and deterministic' +); + +select ok( + pg_catalog.strpos(pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + )), 'for update') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'market cursor CAS lost') > 0, + 'cursor advancement locks and uses explicit CAS generations' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'market cursor target lineage is incomplete') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'market cursor coverage contains a close gap') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'legacy market lineage backfill is incomplete') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'market page lineage is incomplete') > 0, + 'cursor fails closed on missing facts, closes, or lineage membership' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'global_snapshot.block_evidence_id = snapshot.block_evidence_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'close_outcome.status = ''succeeded''') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_snapshot_details_v2(uuid,uuid,text,bigint,uuid,integer,numeric,numeric,numeric,numeric,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ), 'global_snapshot.block_evidence_id = snapshot.block_evidence_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_block_close_v2(uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid,bytea,bytea,timestamp with time zone)'::regprocedure + ), 'is_market_fee_event_v1') > 0 + and not pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.append_market_snapshot_details_v1(uuid,uuid,integer,numeric,numeric,numeric,numeric,numeric,bigint,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.append_market_block_close_v1(uuid,uuid,uuid,uuid,bytea,uuid,numeric,numeric,integer,numeric,numeric,numeric,numeric,numeric,numeric,numeric,bigint,uuid,bytea,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'market facts require exact block prices and only usable canonical closes' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'header.epoch_id <> previous_cursor.epoch_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'p_source_reorg_generation >') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.advance_market_projector_cursor_v1(uuid,uuid,text,text,bytea,bigint,bigint,bigint,bigint,uuid,bigint,bigint,uuid,numeric,bytea,text,timestamp with time zone,timestamp with time zone,bytea,timestamp with time zone)'::regprocedure + ), 'p_next_reorg_generation <> p_expected_reorg_generation + 1') > 0, + 'rebuilds require a current epoch, pointer, or source reorg transition plus one cursor generation' +); + +select ok( + pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ), 'candidate.reconciliation_id = p_reconciliation_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ), 'canonical.occurrence_id = candidate.last_source_occurrence_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ), 'close_outcome.status = ''succeeded''') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ), 'later_close.market_block_close_id') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ), 'p_fees_usd, p_transaction_count') > 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.append_market_candle_details_v2(uuid,uuid,text,bigint,uuid,numeric,bigint,bytea,timestamp with time zone)'::regprocedure + ), 'close_fact.fees_usd, close_fact.transaction_count') = 0 + and pg_catalog.strpos(pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_market_candle_close_v1(uuid,bytea,timestamp with time zone,timestamp with time zone)'::regprocedure + ), 'close_fact.reconciliation_id = p_reconciliation_id') > 0, + 'candle finalization accepts current or succeeded closes and only the last close in-period' +); + +set local role programmable_reconciler; + +select ok( + ( + select acquired + from programmable_private.try_acquire_market_projector_runtime_lease_v1( + 'market-projector:test', decode(repeat('31', 32), 'hex'), + statement_timestamp(), statement_timestamp() + interval '90 seconds', + decode(repeat('32', 32), 'hex') + ) + ) + and not ( + select acquired + from programmable_private.try_acquire_market_projector_runtime_lease_v1( + 'market-projector:overlap', decode(repeat('33', 32), 'hex'), + statement_timestamp(), statement_timestamp() + interval '90 seconds', + decode(repeat('34', 32), 'hex') + ) + ) + and programmable_private.assert_market_projector_runtime_lease_v1( + 'market-projector:test', 1, decode(repeat('31', 32), 'hex') + ) + and programmable_private.release_market_projector_runtime_lease_v1( + 'market-projector:test', 1, decode(repeat('31', 32), 'hex'), + clock_timestamp(), decode(repeat('35', 32), 'hex') + ), + 'market lease serializes overlapping runs and fences the active holder' +); + +select throws_ok( + $sql$ + select * from programmable_private.list_market_projector_pools_v1( + 1, 'classic-v3', 'classic', 'core', 'runtime-v1', 'market-v1', 0 + ) + $sql$, + '22023', + 'invalid market pool page', + 'zero-sized pool pages fail closed' +); + +select throws_ok( + $sql$ + select programmable_private.resolve_market_block_evidence_v1( + gen_random_uuid(), -1, decode(repeat('11', 32), 'hex'), + gen_random_uuid(), gen_random_uuid() + ) + $sql$, + '22023', + 'invalid market block evidence lookup', + 'negative evidence blocks fail before any lookup' +); + +select throws_ok( + $sql$ + select * from programmable_private.get_market_projector_cursor_v1( + 1, 'classic-v3', 'classic', 'core', 'market-v1', + decode(repeat('11', 31), 'hex') + ) + $sql$, + '22023', + 'invalid market cursor identity', + 'malformed pool identities fail closed' +); + +select throws_ok( + $sql$ + select programmable_private.resolve_market_graph_provider_v1( + 'missing-provider', decode(repeat('11', 32), 'hex'), + decode(repeat('22', 32), 'hex') + ) + $sql$, + '23503', + 'exact market provider is not registered', + 'unregistered Graph commitments cannot be substituted' +); + +select throws_ok( + $sql$ + select * from programmable_private.market_projector_cursor_current + $sql$, + '42501', + null, + 'reconciler cannot bypass the cursor API with direct reads' +); + +reset role; + +select ok( + ( + select count(*) = 0 + from programmable_private.market_projector_cursor_history + ) and ( + select count(*) = 0 + from programmable_private.market_projector_cursor_current + ), + 'failed adversarial calls leave no cursor state' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/012_reconciler_route_corpus.test.sql b/supabase/tests/database/012_reconciler_route_corpus.test.sql new file mode 100644 index 00000000..9f6cb0af --- /dev/null +++ b/supabase/tests/database/012_reconciler_route_corpus.test.sql @@ -0,0 +1,572 @@ +begin; + +select plan(31); + +select ok( + to_regprocedure( + 'programmable_private.build_classic_v3_reconciler_reward_v1(bytea,bytea,bytea,text,text,bytea,integer,integer,integer,bytea,bytea,bigint,numeric,numeric,numeric,jsonb,jsonb,jsonb)' + ) is not null, + 'the exact Classic V3 reward DTO builder exists' +); + +select ok( + ( + select owner_role.rolname = 'programmable_migrator' + and not procedure.prosecdef + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + join pg_catalog.pg_roles as owner_role + on owner_role.oid = procedure.proowner + where namespace.nspname = 'programmable_private' + and procedure.proname = 'build_classic_v3_reconciler_reward_v1' + ), + 'the reward DTO builder is migrator-owned SECURITY INVOKER with an empty search path' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.build_classic_v3_reconciler_reward_v1(bytea,bytea,bytea,text,text,bytea,integer,integer,integer,bytea,bytea,bigint,numeric,numeric,numeric,jsonb,jsonb,jsonb)'::regprocedure, + 'EXECUTE' + ), + 'the reconciler capability can execute the reward DTO builder' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector', 'programmable_api_reader', + 'programmable_profile_binder', 'programmable_profile_recovery', + 'programmable_profile_writer', 'programmable_maintenance' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.build_classic_v3_reconciler_reward_v1(bytea,bytea,bytea,text,text,bytea,integer,integer,integer,bytea,bytea,bigint,numeric,numeric,numeric,jsonb,jsonb,jsonb)'::regprocedure, + 'EXECUTE' + ) + ), + 'unrelated capabilities cannot execute the reward DTO builder' +); + +select ok( + to_regprocedure( + 'programmable_private.assemble_reconciler_routes_v1(jsonb,jsonb,jsonb,jsonb,jsonb)' + ) is not null, + 'the pure applicable-route assembler exists' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'launch_count > 256' + ) = 0, + 'the indexed corpus has no legacy 256-launch ceiling' +); + +select ok( + ( + select owner_role.rolname = 'programmable_migrator' + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + join pg_catalog.pg_roles as owner_role + on owner_role.oid = procedure.proowner + where namespace.nspname = 'programmable_private' + and procedure.proname = 'assemble_reconciler_routes_v1' + ), + 'the migration role owns the assembler' +); + +select ok( + ( + select not procedure.prosecdef + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'assemble_reconciler_routes_v1' + ), + 'the pure assembler is SECURITY INVOKER with an empty search path' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.assemble_reconciler_routes_v1(jsonb,jsonb,jsonb,jsonb,jsonb)'::regprocedure, + 'EXECUTE' + ), + 'the reconciler capability can execute the assembler' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector', 'programmable_api_reader', + 'programmable_profile_binder', 'programmable_profile_recovery', + 'programmable_profile_writer', 'programmable_maintenance' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.assemble_reconciler_routes_v1(jsonb,jsonb,jsonb,jsonb,jsonb)'::regprocedure, + 'EXECUTE' + ) + ), + 'unrelated capabilities cannot execute the assembler' +); + +set local role programmable_reconciler; + +select is( + programmable_private.build_classic_v3_reconciler_reward_v1( + decode(repeat('11', 20), 'hex'), + decode(repeat('22', 32), 'hex'), + decode(repeat('33', 20), 'hex'), + 'Reward Fixture', + 'RWD', + decode(repeat('44', 32), 'hex'), + 100, + 200, + 10, + decode(repeat('55', 32), 'hex'), + decode(repeat('66', 32), 'hex'), + 2, + 90, + 10, + 20, + '[{"allocationIndex":0,"payoutAddress":"0x7777777777777777777777777777777777777777","shareBps":10000,"claimableWei":"stale","claimedWei":"stale"}]'::jsonb, + '[{"account":"0x8888888888888888888888888888888888888888","claimableWei":"80","claimedWei":"10","legacy":true}]'::jsonb, + '[{"kind":"checkpoint"}]'::jsonb + ), + pg_catalog.jsonb_build_object( + 'releaseVersion', 'classic-v3', + 'modelId', 'classic', + 'vaultAddress', '0x' || repeat('11', 20), + 'poolId', '0x' || repeat('22', 32), + 'tokenAddress', '0x' || repeat('33', 20), + 'tokenName', 'Reward Fixture', + 'tokenSymbol', 'RWD', + 'launchTransactionHash', '0x' || repeat('44', 32), + 'buySwapFeeBps', 100, + 'sellSwapFeeBps', 200, + 'launcherFeeBps', 10, + 'configurationHash', '0x' || repeat('55', 32), + 'activeConfigurationHash', '0x' || repeat('66', 32), + 'configurationEpoch', '2', + 'totalCreatorFeesReceivedWei', '90', + 'totalCreatorFeesClaimedWei', '10', + 'pendingCreatorFeesWei', '20', + 'allocations', '[{"allocationIndex":0,"payoutAddress":"0x7777777777777777777777777777777777777777","shareBps":10000}]'::jsonb, + 'entitlements', '[{"account":"0x8888888888888888888888888888888888888888","claimableWei":"80","claimedWei":"10"}]'::jsonb, + 'events', '[{"kind":"checkpoint"}]'::jsonb + ), + 'the SQL reward DTO exactly matches the runtime schema and strips stale nested fields' +); + +select is( + ( + select pg_catalog.count(*) + from programmable_private.assemble_reconciler_routes_v1( + '[{"releaseVersion":"classic-v3","modelId":"classic","id":1}]'::jsonb, + '[{"releaseVersion":"classic-v3","modelId":"classic","id":1}]'::jsonb, + '[{"tokens":[{"id":1}]}]'::jsonb, + '[{"id":1}]'::jsonb, + '[{"id":1}]'::jsonb + ) as route + where route.compared_count = 1 + and route.dto ->> 'contractVersion' = + 'programmable-route-corpus-v1' + ), + 6::bigint, + 'the assembler returns all six versioned routes with one shared count' +); + +select is( + ( + select pg_catalog.array_agg(route.route_key order by route.route_key) + from programmable_private.assemble_reconciler_routes_v1( + '[{"releaseVersion":"classic-v2","modelId":"classic","id":1}]'::jsonb, + '[{"releaseVersion":"classic-v2","modelId":"classic","id":1}]'::jsonb, + '[{"tokens":[{"id":1}]}]'::jsonb, + '[]'::jsonb, + '[]'::jsonb + ) as route + ), + array[ + 'creator-profile', 'explore-chart', 'explore-list', 'explore-token' + ]::text[], + 'Classic V2 exposes only its four applicable routes' +); + +select is( + ( + select pg_catalog.array_agg(route.route_key order by route.route_key) + from programmable_private.assemble_reconciler_routes_v1( + '[{"releaseVersion":"stock-paired-v3","modelId":"stock-paired","id":1}]'::jsonb, + '[{"releaseVersion":"stock-paired-v3","modelId":"stock-paired","id":1}]'::jsonb, + '[{"tokens":[{"id":1}]}]'::jsonb, + '[]'::jsonb, + '[{"id":1}]'::jsonb + ) as route + ), + array[ + 'creator-profile', 'explore-chart', 'explore-list', 'explore-token', + 'launch-lookup' + ]::text[], + 'Stock releases expose five applicable routes without Classic rewards' +); + +select throws_ok( + $sql$ + select * + from programmable_private.assemble_reconciler_routes_v1( + '[{"id":1}]'::jsonb, + '[]'::jsonb, + '[{"tokens":[{"id":1}]}]'::jsonb, + '[{"id":1}]'::jsonb, + '[{"id":1}]'::jsonb + ) + $sql$, + '22023', + 'the assembler rejects cross-route cardinality mismatches' +); + +reset role; + +select ok( + to_regprocedure( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)' + ) is not null, + 'the exact route corpus reader exists' +); + +select ok( + ( + select owner_role.rolname = 'programmable_migrator' + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + join pg_catalog.pg_roles as owner_role + on owner_role.oid = procedure.proowner + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_reconciler_route_corpus_v1' + ), + 'the migration role owns the reader' +); + +select ok( + ( + select procedure.prosecdef + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_reconciler_route_corpus_v1' + ), + 'the reader is SECURITY DEFINER with an empty search path' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_reconciler', + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure, + 'EXECUTE' + ), + 'the reconciler capability can execute the reader' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector', 'programmable_api_reader', + 'programmable_profile_binder', 'programmable_profile_recovery', + 'programmable_profile_writer', 'programmable_maintenance' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure, + 'EXECUTE' + ) + ), + 'browser, service, projector, reader, profile and maintenance roles are denied' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'assert_caller(''programmable_reconciler'')' + ) > 0, + 'the reader verifies the active capability role' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'get_reconciler_preparity_contract_v1' + ) > 0, + 'the reader reuses the exact checkpoint, manifest and applicable-route contract' +); + +select ok( + pg_catalog.strpos( + pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )), + 'public_explore' + ) = 0 + and pg_catalog.strpos( + pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )), + 'route_snapshot_readiness' + ) = 0 + and pg_catalog.strpos( + pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )), + 'parity_records' + ) = 0 + and pg_catalog.strpos( + pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )), + 'route_checkpoint_parity_bindings' + ) = 0 + and pg_catalog.strpos( + pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )), + 'launch_by_token_v1' + ) = 0 + and pg_catalog.strpos( + pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )), + 'launch_by_token_v2' + ) = 0, + 'the corpus never self-compares through public, parity or route-gated launch views' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'projection_entity_current as current_launch') > 0 + and pg_catalog.strpos(definition, 'launch_projections as launch') > 0 + and pg_catalog.strpos(definition, 'run_headers as run') > 0 + and pg_catalog.strpos(definition, 'projection_publications as publication') > 0 + and pg_catalog.strpos(definition, 'release_epoch_current as current_epoch') > 0 + and pg_catalog.strpos(definition, 'as launch_canonical') > 0 + and pg_catalog.strpos(definition, 'as pool_canonical') > 0 + and pg_catalog.strpos(definition, 'as fee_canonical') > 0 + and pg_catalog.strpos(definition, 'as liquidity_canonical') > 0 + and pg_catalog.strpos(definition, 'as market_canonical') > 0 + from ( + select pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )) as definition + ) as corpus + ), + 'launch DTOs bind direct current projections to run, publication, epoch and canonical source provenance' +); + +select ok( + ( + select + pg_catalog.strpos( + definition, + 'build_classic_v3_reconciler_reward_v1' + ) > 0 + and pg_catalog.strpos( + definition, + 'current_account_reward_balances_v1' + ) > 0 + and pg_catalog.strpos( + definition, + 'chain_event_materialized_occurrences_v1' + ) > 0 + and pg_catalog.strpos( + definition, + 'chain_event_current_canonical' + ) > 0 + from ( + select pg_catalog.lower(pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + )) as definition + ) as corpus + ), + 'Classic V3 rewards use the exact DTO builder, current balances and canonical lifecycle events' +); + +select ok( + ( + select + pg_catalog.strpos(definition, 'contract_row.current_entities') > 0 + and pg_catalog.strpos(definition, 'entity ->> ''entityKind'' = ''launch''') > 0 + and pg_catalog.strpos( + definition, + 'launch_count <> projected_launch_count' + ) > 0 + from ( + select pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ) as definition + ) as corpus + ), + 'the exact pre-parity entity manifest closes launch coverage without a parity binding' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'launch_count > p_maximum_entity_count' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_reconciler_route_corpus_v1(bigint,text,text,text,uuid,bigint,uuid,numeric,bytea,integer)'::regprocedure + ), + 'vault_count <> launch_count' + ) > 0, + 'launch and reward coverage are bounded and complete' +); + +set local role programmable_reconciler; + +select throws_ok( + $sql$ + select * from programmable_private.get_reconciler_route_corpus_v1( + 1, 'deep-v3', 'deep', 'core', + '12000000-0000-0000-0000-000000000001', 1, + '12000000-0000-0000-0000-000000000002', 100, + decode(repeat('11', 32), 'hex'), 100 + ) + $sql$, + '0A000', + 'unsupported releases fail before any partial corpus is returned' +); + +reset role; +set local role programmable_api_reader; + +select throws_ok( + $sql$ + select * from programmable_private.get_reconciler_route_corpus_v1( + 1, 'classic-v3', 'classic', 'core', + '13000000-0000-0000-0000-000000000001', 1, + '13000000-0000-0000-0000-000000000002', 100, + decode(repeat('13', 32), 'hex'), 100 + ) + $sql$, + '42501', + 'the API reader cannot execute the corpus capability' +); + +reset role; + +select is( + ( + select pg_catalog.count(*) + from programmable_private.route_checkpoint_parity_bindings + ) + ( + select pg_catalog.count(*) + from programmable_private.parity_records + ), + 0::bigint, + 'the bootstrap execution fixture has no prior route or parity binding' +); + +-- Exercise the corpus body without manufacturing the otherwise extensive +-- checkpoint fixture. The replacement is transaction-local and preserves the +-- production signature: it returns one exact empty contract, allowing the +-- reader to reach and plan every direct projection join before it fails closed +-- on the expected zero-launch cardinality guard. +create or replace function programmable_private.get_reconciler_preparity_contract_v1( + p_chain_id bigint, + p_release_id text, + p_model_id text, + p_source_group text, + p_epoch_id uuid, + p_pointer_generation bigint, + p_checkpoint_id uuid, + p_checkpoint_block_number numeric, + p_checkpoint_block_hash bytea, + p_maximum_entity_count integer default 10000 +) +returns table ( + chain_id bigint, + release_id text, + model_id text, + source_group text, + projector_version text, + epoch_id uuid, + pointer_generation bigint, + checkpoint_id uuid, + checkpoint_generation bigint, + reorg_generation bigint, + checkpoint_block_number bigint, + checkpoint_block_hash bytea, + route_keys text[], + route_contract jsonb, + projection_contract jsonb, + current_entities jsonb +) +language sql +stable +security definer +set search_path = '' +as $function$ + select + p_chain_id, + p_release_id, + p_model_id, + p_source_group, + 'projector-v1'::text, + p_epoch_id, + p_pointer_generation, + p_checkpoint_id, + 1::bigint, + 0::bigint, + p_checkpoint_block_number::bigint, + p_checkpoint_block_hash, + array[ + 'explore-list', 'explore-token', 'explore-chart', 'creator-profile' + ]::text[], + '{}'::jsonb, + '{}'::jsonb, + '[]'::jsonb +$function$; + +set local role programmable_reconciler; + +select throws_ok( + $sql$ + select * from programmable_private.get_reconciler_route_corpus_v1( + 1, 'classic-v2', 'classic', 'core', + '14000000-0000-0000-0000-000000000001', 1, + '14000000-0000-0000-0000-000000000002', 100, + decode(repeat('14', 32), 'hex'), 100 + ) + $sql$, + '54000', + 'the direct corpus executes without prior parity and then fails closed on an empty launch manifest' +); + +reset role; +select * from finish(); +rollback; diff --git a/supabase/tests/database/013_projector_provider_evidence_binding.test.sql b/supabase/tests/database/013_projector_provider_evidence_binding.test.sql new file mode 100644 index 00000000..47125781 --- /dev/null +++ b/supabase/tests/database/013_projector_provider_evidence_binding.test.sql @@ -0,0 +1,1028 @@ +begin; +select plan(45); + +create function public.projection_trace_fixture_v1( + p_candidate_batch_size integer default 1, + p_duration_ms integer default 2 +) +returns jsonb +language sql +immutable +set search_path = '' +as $function$ + select pg_catalog.jsonb_build_object( + 'startedAtMs', 1775000000000, + 'completedAtMs', 1775000000005, + 'candidateBatchSize', p_candidate_batch_size, + 'hardDeadlineMs', 75000, + 'maxCallsPerProvider', 128, + 'elapsedMs', 5, + 'providerCallCounts', pg_catalog.jsonb_build_array(1, 0), + 'calls', pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object( + 'providerIdentity', + 'alchemy-mainnet-11111111111111111111111111111111', + 'providerVendorGroup', 'alchemy', + 'providerEndpointCommitment', '0x' || pg_catalog.repeat('33', 32), + 'providerOriginCommitment', '0x' || pg_catalog.repeat('44', 32), + 'operation', 'getTransactionReceipt', + 'attempt', 1, + 'startedOffsetMs', 3, + 'durationMs', p_duration_ms, + 'outcome', 'success' + ) + ) + ) +$function$; + +create function public.reward_trace_fixture_v1() +returns jsonb +language sql +immutable +set search_path = '' +as $function$ + select pg_catalog.jsonb_build_object( + 'startedAtMs', 1775000000000, + 'completedAtMs', 1775000000005, + 'candidateBatchSize', 0, + 'hardDeadlineMs', 75000, + 'maxCallsPerProvider', 128, + 'elapsedMs', 5, + 'providerCallCounts', pg_catalog.jsonb_build_array(14, 14), + 'calls', pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object( + 'providerIdentity', + 'alchemy-mainnet-11111111111111111111111111111111', + 'providerVendorGroup', 'alchemy', + 'providerEndpointCommitment', '0x' || pg_catalog.repeat('33', 32), + 'providerOriginCommitment', '0x' || pg_catalog.repeat('44', 32), + 'operation', 'readRewardSnapshot', + 'attempt', 1, + 'startedOffsetMs', 0, + 'durationMs', 5, + 'outcome', 'success' + ), + pg_catalog.jsonb_build_object( + 'providerIdentity', + 'quicknode-mainnet-55555555555555555555555555555555', + 'providerVendorGroup', 'quicknode', + 'providerEndpointCommitment', '0x' || pg_catalog.repeat('55', 32), + 'providerOriginCommitment', '0x' || pg_catalog.repeat('66', 32), + 'operation', 'readRewardSnapshot', + 'attempt', 1, + 'startedOffsetMs', 0, + 'durationMs', 5, + 'outcome', 'success' + ) + ) + ) +$function$; + +create function public.reward_trace_fixture_multi_v1( + p_chunk_count integer, + p_aggregate_call_count integer +) +returns jsonb +language sql +immutable +set search_path = '' +as $function$ + select pg_catalog.jsonb_build_object( + 'startedAtMs', 1775000000000, + 'completedAtMs', 1775000000010, + 'candidateBatchSize', 0, + 'hardDeadlineMs', 75000, + 'maxCallsPerProvider', 128, + 'elapsedMs', 10, + 'providerCallCounts', pg_catalog.jsonb_build_array( + p_aggregate_call_count, p_aggregate_call_count + ), + 'calls', ( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'providerIdentity', case provider_ordinal + when 1 then + 'alchemy-mainnet-11111111111111111111111111111111' + else + 'quicknode-mainnet-55555555555555555555555555555555' + end, + 'providerVendorGroup', case provider_ordinal + when 1 then 'alchemy' else 'quicknode' + end, + 'providerEndpointCommitment', '0x' || case provider_ordinal + when 1 then pg_catalog.repeat('33', 32) + else pg_catalog.repeat('55', 32) + end, + 'providerOriginCommitment', '0x' || case provider_ordinal + when 1 then pg_catalog.repeat('44', 32) + else pg_catalog.repeat('66', 32) + end, + 'operation', 'readRewardSnapshot', + 'attempt', 1, + 'startedOffsetMs', chunk_ordinal - 1, + 'durationMs', 1, + 'outcome', 'success' + ) + order by provider_ordinal, chunk_ordinal + ) + from pg_catalog.generate_series(1, 2) as providers(provider_ordinal) + cross join pg_catalog.generate_series( + 1, p_chunk_count + ) as chunks(chunk_ordinal) + ) + ) +$function$; + +set local role programmable_projector; + +select programmable_private.register_rpc_provider_deployment( + '10000000-0000-4000-8000-000000000002', 1, + 'alchemy', 'provider-evidence-test-v1', + pg_catalog.decode(pg_catalog.repeat('33', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('44', 32), 'hex'), + 'rpc-endpoint-commitments-v1', + pg_catalog.decode(pg_catalog.repeat('31', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('11', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('32', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('34', 32), 'hex'), + '2026-08-01T00:00:00Z' +); + +select programmable_private.register_rpc_provider_deployment( + '10000000-0000-4000-8000-000000000003', 1, + 'quicknode', 'provider-evidence-test-v1', + pg_catalog.decode(pg_catalog.repeat('55', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('66', 32), 'hex'), + 'rpc-endpoint-commitments-v1', + pg_catalog.decode(pg_catalog.repeat('51', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('55', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('52', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('54', 32), 'hex'), + '2026-08-01T00:00:01Z' +); + +reset role; + +select is( + pg_catalog.encode(version.definition_commitment, 'hex'), + '3234e87ac53489e1cfefafa865b053e9723945930d060265c0e8084669a1e955', + 'provider evidence v3 has the frozen TypeScript contract commitment' +) +from programmable_private.fingerprint_encoding_versions as version +where version.fingerprint_domain = 'evidence' + and version.encoding_version = 3; + +select is( + pg_catalog.encode(subtype.frame_prefix, 'hex'), + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330006', + 'projection execution evidence uses the frozen v3 subtype frame' +) +from programmable_private.provider_evidence_encoding_subtypes as subtype +where subtype.evidence_subtype = 'projection_execution' + and subtype.encoding_version = 3; + +select is( + pg_catalog.encode(subtype.frame_prefix, 'hex'), + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a76330007', + 'reward snapshot evidence uses the frozen v3 subtype frame' +) +from programmable_private.provider_evidence_encoding_subtypes as subtype +where subtype.evidence_subtype = 'reward_snapshot' + and subtype.encoding_version = 3; + +select is( + pg_catalog.encode( + programmable_private.projection_execution_trace_commitment_v1( + public.projection_trace_fixture_v1() + ), 'hex' + ), + '466d9059a360712fd7d40fc9a4fd326cf58ed7d8f4a7f93a31da4edd9bbdc620', + 'SQL and TypeScript freeze the same structural projection trace' +); + +select is( + pg_catalog.encode( + programmable_private.projection_execution_trace_commitment_v1( + public.reward_trace_fixture_v1() + ), 'hex' + ), + '387a035634613b9c1fcf9369aeea587e325815bef91d3d3945e56136d0472043', + 'reward trace binds two logical reads and their raw provider call counts' +); + +select ok( + pg_catalog.encode( + programmable_private.projection_execution_trace_commitment_v1( + public.projection_trace_fixture_v1(1, 1) + ), 'hex' + ) <> '466d9059a360712fd7d40fc9a4fd326cf58ed7d8f4a7f93a31da4edd9bbdc620', + 'changing a trace duration changes its commitment' +); + +select throws_ok( + $sql$ + select programmable_private.projection_execution_trace_preimage_v1( + public.projection_trace_fixture_v1() || '{"extra":true}'::jsonb + ) + $sql$, + '22023', + 'unknown trace fields are rejected' +); + +select ok( + pg_catalog.octet_length( + programmable_private.projection_execution_trace_preimage_v1( + public.projection_trace_fixture_v1(4096, 2) + ) + ) > 0, + 'the structural trace codec accepts the frozen 4096 candidate boundary' +); + +select throws_ok( + $sql$ + select programmable_private.projection_execution_trace_preimage_v1( + public.projection_trace_fixture_v1(4097, 2) + ) + $sql$, + '22023', + 'the structural trace codec rejects candidate batches above 4096' +); + +select is( + pg_catalog.encode( + programmable_private.projection_execution_evidence_preimage_v1( + 1, 'classic-v3', 'classic', 'core', + '70000000-0000-4000-8000-000000000020', 1, + '80000000-0000-4000-8000-000000000001', + '10000000-0000-4000-8000-000000000002', + '10000000-0000-4000-8000-000000000003', + 'alchemy-mainnet-11111111111111111111111111111111', + 'quicknode-mainnet-55555555555555555555555555555555', + 'alchemy', 'quicknode', + pg_catalog.decode(pg_catalog.repeat('33', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('55', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('44', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('66', 32), 'hex'), + 6, 6, 40, 75000, 128, 2, + pg_catalog.decode(pg_catalog.repeat('77', 32), 'hex') + ), 'hex' + ), + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7633000600000000000000010000000a636c61737369632d763300000007636c617373696300000004636f726570000000000040008000000000000020000000000000000180000000000040008000000000000001100000000000400080000000000000021000000000004000800000000000000300000030616c6368656d792d6d61696e6e65742d313131313131313131313131313131313131313131313131313131313131313100000032717569636b6e6f64652d6d61696e6e65742d353535353535353535353535353535353535353535353535353535353535353500000007616c6368656d7900000009717569636b6e6f64653333333333333333333333333333333333333333333333333333333333333333555555555555555555555555555555555555555555555555555555555555555544444444444444444444444444444444444444444444444444444444444444446666666666666666666666666666666666666666666666666666666666666666000000060000000600000028000124f800000080000000027777777777777777777777777777777777777777777777777777777777777777', + 'projection evidence SQL preimage exactly matches the TypeScript fixture' +); + +select is( + pg_catalog.encode( + programmable_private.reward_snapshot_evidence_preimage_v1( + 1, 'classic-v3', 'classic', 'core', + '70000000-0000-4000-8000-000000000020', 1, + '80000000-0000-4000-8000-000000000001', + '81000000-0000-4000-8000-000000000001', + '82000000-0000-4000-8000-000000000001', + pg_catalog.decode(pg_catalog.repeat('88', 20), 'hex'), + 'classic-v3', 25639601, + pg_catalog.decode(pg_catalog.repeat('99', 32), 'hex'), + '10000000-0000-4000-8000-000000000002', + '10000000-0000-4000-8000-000000000003', + pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), + 14, 14, + array[ + pg_catalog.decode(pg_catalog.repeat('11', 20), 'hex'), + pg_catalog.decode(pg_catalog.repeat('22', 20), 'hex') + ], + array[2], + array[pg_catalog.decode(pg_catalog.repeat('dd', 32), 'hex')], + array[pg_catalog.decode(pg_catalog.repeat('dd', 32), 'hex')], + array[14], + array[14], + pg_catalog.decode(pg_catalog.repeat('bb', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('cc', 32), 'hex') + ), 'hex' + ), + '70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7633000700000000000000010000000a636c61737369632d763300000007636c617373696300000004636f726570000000000040008000000000000020000000000000000180000000000040008000000000000001810000000000400080000000000000018200000000004000800000000000000188888888888888888888888888888888888888880000000a636c61737369632d76330000000001873ab199999999999999999999999999999999999999999999999999999999999999991000000000004000800000000000000210000000000040008000000000000003aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000e0000000e0000000211111111111111111111111111111111111111112222222222222222222222222222222222222222000000010000000200000001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd00000001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd000000010000000e000000010000000ebbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + 'reward evidence SQL preimage exactly matches the TypeScript fixture' +); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_class as relation + join pg_catalog.pg_namespace as namespace + on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname in ( + 'projection_provider_execution_evidence', + 'reward_snapshot_provider_evidence', + 'projection_publication_provider_bindings', + 'projection_publication_reward_evidence' + ) + and relation.relrowsecurity + and relation.relforcerowsecurity + ), + 4, + 'all four immutable provider-evidence tables force RLS' +); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_policies as policy + where policy.schemaname = 'programmable_private' + and policy.tablename in ( + 'projection_provider_execution_evidence', + 'reward_snapshot_provider_evidence', + 'projection_publication_provider_bindings', + 'projection_publication_reward_evidence' + ) + and policy.roles = array['programmable_migrator'::name] + and policy.cmd = 'ALL' + ), + 4, + 'only the migrator receives explicit all-row RLS policies' +); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_trigger as trigger + join pg_catalog.pg_class as relation on relation.oid = trigger.tgrelid + join pg_catalog.pg_namespace as namespace + on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname in ( + 'projection_provider_execution_evidence', + 'reward_snapshot_provider_evidence', + 'projection_publication_provider_bindings', + 'projection_publication_reward_evidence' + ) + and not trigger.tgisinternal + and trigger.tgname like '%_immutable' + ), + 4, + 'each provider-evidence table rejects update and delete mutations' +); + +select ok( + not pg_catalog.has_table_privilege( + 'public', + 'programmable_private.projection_provider_execution_evidence', + 'SELECT,INSERT,UPDATE,DELETE' + ) + and not pg_catalog.has_table_privilege( + 'anon', + 'programmable_private.reward_snapshot_provider_evidence', + 'SELECT,INSERT,UPDATE,DELETE' + ) + and not pg_catalog.has_table_privilege( + 'authenticated', + 'programmable_private.projection_publication_provider_bindings', + 'SELECT,INSERT,UPDATE,DELETE' + ) + and not pg_catalog.has_table_privilege( + 'service_role', + 'programmable_private.projection_publication_reward_evidence', + 'SELECT,INSERT,UPDATE,DELETE' + ), + 'browser and Supabase roles have no direct provider-evidence table access' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.append_projection_provider_execution_evidence_v1(uuid,uuid,uuid,uuid[],jsonb,bytea,smallint,bytea,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.append_reward_snapshot_provider_evidence_v1(uuid,uuid,uuid,uuid,bytea,text,text,numeric,bytea,bytea,bytea,integer,integer,bytea[],integer[],bytea[],bytea[],integer[],integer[],bytea,jsonb,bytea,smallint,bytea,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'the projector can append both immutable provider-evidence kinds' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.get_staged_reward_folded_commitment_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.stage_current_reward_snapshot_v2(uuid,bytea,bytea,uuid,bigint,bytea,numeric,integer[],bytea[],bytea[],numeric[],bytea[],bytea[],numeric[],numeric[],uuid,uuid[],numeric,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.stage_verified_dynamic_parents_v2(uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],jsonb,bytea,jsonb,jsonb,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.consume_matching_provisional_sources_v1(uuid,uuid,uuid,uuid,uuid[],timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'the projector can stage and read private parents but cannot consume them outside v3 promotion' +); + +select ok( + not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.reward_snapshot_folded_preimage_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.reward_snapshot_folded_commitment_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.projection_provider_binding_commitment_v1(uuid,uuid,text,uuid,uuid[],timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'internal folded-state and publication-binding helpers are not runtime capabilities' +); + +select ok( + not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.promote_projection_run(uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.promote_projection_run_v2(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'legacy promotion functions cannot bypass provider-evidence binding' +); + +select ok( + not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.get_staged_reward_folded_commitment_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'anon', + 'programmable_private.get_staged_reward_folded_commitment_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'authenticated', + 'programmable_private.get_staged_reward_folded_commitment_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'service_role', + 'programmable_private.get_staged_reward_folded_commitment_v1(uuid,bytea)'::regprocedure, + 'EXECUTE' + ), + 'the folded commitment getter is unavailable to browser and service roles' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.append_reward_snapshot_provider_evidence_v1(uuid,uuid,uuid,uuid,bytea,text,text,numeric,bytea,bytea,bytea,integer,integer,bytea[],integer[],bytea[],bytea[],integer[],integer[],bytea,jsonb,bytea,smallint,bytea,bytea,timestamp with time zone)'::regprocedure + ), + 'reward verification account coverage changed' + ) > 0, + 'reward evidence binds the exact active and changed account read set' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.bind_projection_publication_provider_evidence_v1(uuid,uuid,uuid,text,uuid,uuid[],bytea,timestamp with time zone)'::regprocedure + ), + 'projection_provider_binding_commitment_v1' + ) > 0, + 'publication binding commitments are recomputed from stored evidence' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_classic_reward_block_fold_v1(uuid,bytea,uuid[])'::regprocedure + ), + 'order by requested.ordinal' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_stock_reward_block_fold_v1(uuid,bytea,uuid[])'::regprocedure + ), + 'order by requested.ordinal' + ) > 0, + 'Classic and Stock reward folds consume events in caller-bound chain order' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_current_reward_snapshot_v2(uuid,bytea,bytea,uuid,bigint,bytea,numeric,integer[],bytea[],bytea[],numeric[],bytea[],bytea[],numeric[],numeric[],uuid,uuid[],numeric,bytea,timestamp with time zone)'::regprocedure + ), + 'cardinality(p_occurrence_ids), 0) <= 1' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_current_reward_snapshot_v2(uuid,bytea,bytea,uuid,bigint,bytea,numeric,integer[],bytea[],bytea[],numeric[],bytea[],bytea[],numeric[],numeric[],uuid,uuid[],numeric,bytea,timestamp with time zone)'::regprocedure + ), + 'assert_stock_reward_block_fold_v1' + ) > 0, + 'stage-v2 preserves only the single-occurrence path and handles grouped Classic and Stock transitions' +); + +select ok( + to_regprocedure( + 'programmable_private.projection_execution_trace_preimage_v1(jsonb)' + ) is not null + and to_regprocedure( + 'programmable_private.reward_snapshot_folded_preimage_v1(uuid,bytea)' + ) is not null + and to_regprocedure( + 'programmable_private.projection_provider_binding_preimage_v1(uuid,uuid,text,uuid,uuid[],timestamp with time zone)' + ) is not null, + 'all three structural commitment domains have explicit SQL codecs' +); + +select lives_ok( + $sql$ + select programmable_private.validate_reward_snapshot_execution_trace_v1( + public.reward_trace_fixture_multi_v1(2, 129), + '10000000-0000-4000-8000-000000000002', + '10000000-0000-4000-8000-000000000003', + 129, 129 + ) + $sql$, + 'reward traces support ordered multi-chunk reads above one execution-call window' +); + +select throws_ok( + $sql$ + select programmable_private.validate_reward_snapshot_execution_trace_v1( + pg_catalog.jsonb_set( + public.reward_trace_fixture_multi_v1(2, 129), + '{calls,1}', + public.reward_trace_fixture_multi_v1(2, 129) #> '{calls,2}' + ), + '10000000-0000-4000-8000-000000000002', + '10000000-0000-4000-8000-000000000003', + 129, 129 + ) + $sql$, + '23514', + 'reward traces reject provider interleaving across chunk boundaries' +); + +select lives_ok( + $sql$ + select programmable_private.assert_reward_verification_chunk_manifest_v1( + array( + select pg_catalog.decode( + pg_catalog.lpad(pg_catalog.to_hex(account_number), 40, '0'), + 'hex' + ) + from pg_catalog.generate_series(1, 49) as accounts(account_number) + ), + array[48, 49], + array[ + pg_catalog.decode(pg_catalog.repeat('a1', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('a2', 32), 'hex') + ], + array[ + pg_catalog.decode(pg_catalog.repeat('a1', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('a2', 32), 'hex') + ], + array[128, 1], array[128, 1], 129, 129 + ) + $sql$, + 'a 49-account reward read persists as two exact ordered chunks' +); + +select throws_ok( + $sql$ + select programmable_private.assert_reward_verification_chunk_manifest_v1( + array( + select pg_catalog.decode( + pg_catalog.lpad(pg_catalog.to_hex(account_number), 40, '0'), + 'hex' + ) + from pg_catalog.generate_series(49, 1, -1) + as accounts(account_number) + ), + array[48, 49], + array[ + pg_catalog.decode(pg_catalog.repeat('a1', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('a2', 32), 'hex') + ], + array[ + pg_catalog.decode(pg_catalog.repeat('a1', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('a2', 32), 'hex') + ], + array[128, 1], array[128, 1], 129, 129 + ) + $sql$, + '22023', + 'reward account order is canonical across chunk boundaries' +); + +select lives_ok( + $sql$ + select programmable_private.assert_reward_verification_chunk_manifest_v1( + array( + select pg_catalog.decode( + pg_catalog.lpad(pg_catalog.to_hex(account_number), 40, '0'), + 'hex' + ) + from pg_catalog.generate_series(1, 4096) + as accounts(account_number) + ), + array( + select least(chunk_number * 48, 4096) + from pg_catalog.generate_series(1, 86) as chunks(chunk_number) + ), + array( + select pg_catalog.decode(pg_catalog.repeat('ab', 32), 'hex') + from pg_catalog.generate_series(1, 86) + ), + array( + select pg_catalog.decode(pg_catalog.repeat('ab', 32), 'hex') + from pg_catalog.generate_series(1, 86) + ), + array( + select 128 from pg_catalog.generate_series(1, 86) + ), + array( + select 128 from pg_catalog.generate_series(1, 86) + ), + 11008, 11008 + ) + $sql$, + 'reward evidence accepts the full 4096-account and 11008-call boundary' +); + +select ok( + ( + select pg_catalog.string_agg( + pg_catalog.pg_get_constraintdef(constraint_row.oid), ' ' + ) + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.projection_provider_execution_evidence'::regclass + ) like '%128%' + and ( + select pg_catalog.string_agg( + pg_catalog.pg_get_constraintdef(constraint_row.oid), ' ' + ) + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.projection_provider_execution_evidence'::regclass + ) not like '%11008%' + and ( + select pg_catalog.string_agg( + pg_catalog.pg_get_constraintdef(constraint_row.oid), ' ' + ) + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.reward_snapshot_provider_evidence'::regclass + ) like '%11008%', + 'projection calls remain capped at 128 while reward aggregates reach 11008' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure + ), + 'p_promotion_mode <> ''exact_incremental''' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure + ), + 'reward-bearing promotion events are not in chain order' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure + ), + '''full_launch''' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure + ), + 'promotion cursor is not the exact configured-provider block terminal' + ) > 0, + 'v3 exposes exact incremental promotion with mixed-event ordering and an exact terminal watermark' +); + +select ok( + ( + select pg_catalog.string_agg( + pg_catalog.pg_get_constraintdef(constraint_row.oid), ' ' + ) + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.projection_publication_provider_bindings'::regclass + ) like '%exact_incremental%' + and ( + select pg_catalog.string_agg( + pg_catalog.pg_get_constraintdef(constraint_row.oid), ' ' + ) + from pg_catalog.pg_constraint as constraint_row + where constraint_row.conrelid = + 'programmable_private.projection_publication_provider_bindings'::regclass + ) not like '%reward_snapshot_delta%', + 'public provider bindings cannot expose legacy reward or launch modes' +); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_class as relation + join pg_catalog.pg_namespace as namespace + on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname in ( + 'provisional_dynamic_parent_pages', + 'provisional_dynamic_source_lineages', + 'provisional_dynamic_parent_consumptions' + ) + and relation.relrowsecurity + and relation.relforcerowsecurity + and exists ( + select 1 + from pg_catalog.pg_policies as policy + where policy.schemaname = namespace.nspname + and policy.tablename = relation.relname + and policy.roles = array['programmable_migrator'::name] + and policy.cmd = 'ALL' + ) + ), + 3, + 'all provisional lineage tables force RLS with migrator-only policies' +); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_trigger as trigger + join pg_catalog.pg_class as relation on relation.oid = trigger.tgrelid + join pg_catalog.pg_namespace as namespace + on namespace.oid = relation.relnamespace + where namespace.nspname = 'programmable_private' + and relation.relname in ( + 'provisional_dynamic_parent_pages', + 'provisional_dynamic_source_lineages', + 'provisional_dynamic_parent_consumptions' + ) + and not trigger.tgisinternal + and trigger.tgname like '%_immutable' + ), + 3, + 'provisional page, lineage and consumption records are immutable' +); + +select ok( + not pg_catalog.has_table_privilege( + 'public', + 'programmable_private.provisional_dynamic_parent_pages', + 'SELECT,INSERT,UPDATE,DELETE' + ) + and not pg_catalog.has_table_privilege( + 'authenticated', + 'programmable_private.provisional_dynamic_source_lineages', + 'SELECT,INSERT,UPDATE,DELETE' + ) + and not pg_catalog.has_table_privilege( + 'service_role', + 'programmable_private.provisional_dynamic_parent_consumptions', + 'SELECT,INSERT,UPDATE,DELETE' + ), + 'provisional dynamic parents never become a browser or service capability' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_parents_v2(uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],jsonb,bytea,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'provisional child runtime is not template-attested' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_parents_v2(uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],jsonb,bytea,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + '[1-58][0-9a-f]{3}' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_parents_v2(uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],jsonb,bytea,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'insert into programmable_private.projector_checkpoints' + ) = 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_parents_v2(uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],jsonb,bytea,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'insert into programmable_private.projection_publications' + ) = 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_parents_v2(uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],jsonb,bytea,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'dual_rpc_log_coverage_evidence' + ) = 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_parents_v2(uuid,uuid,text,text,text,text,uuid,bigint,bigint,bigint,bytea,uuid,text,uuid,uuid,uuid,uuid,numeric,bytea,bytea,bytea[],bytea[],jsonb,bytea,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'envio_candidate_inbox' + ) = 0, + 'provisional staging proves child runtime without reserving canonical coverage or moving public state' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_provisional_parent_receipt_ordinals_v1(uuid,uuid,text[],numeric[],timestamp with time zone)'::regprocedure + ), + 'unnest(p_candidate_ids, p_receipt_log_ordinals)' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_provisional_parent_receipt_ordinals_v1(uuid,uuid,text[],numeric[],timestamp with time zone)'::regprocedure + ), + 'pg_catalog.unnest(p_candidate_ids, p_receipt_log_ordinals)' + ) = 0, + 'provisional receipt ordinals use PostgreSQL multi-array unnest syntax' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure + ), + 'release_epoch_current' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure + ), + 'envio_ingestion_cursor_current' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure + ), + 'provisional_dynamic_parent_consumptions' + ) > 0, + 'provisional reads are scoped to the current epoch, cursor and unconsumed lineage' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure + ), + 'dynamic_source_activation_staging' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure + ), + 'cursor.generation > page.expected_cursor_generation' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.get_current_provisional_dynamic_sources_v1(text)'::regprocedure + ), + 'cursor.block_number < page.snapshot_block_number' + ) > 0, + 'activated provisional lineages remain readable after an exact cursor advance' +); + +select is( + ( + select pg_catalog.pg_get_userbyid(procedure.proowner) + from pg_catalog.pg_proc as procedure + where procedure.oid = + 'programmable_private.get_current_provisional_activation_boundaries_v1(text)'::regprocedure + ), + ( + select pg_catalog.pg_get_userbyid(relation.relowner) + from pg_catalog.pg_class as relation + where relation.oid = + 'programmable_private.dynamic_source_activation_staging'::regclass + ), + 'activation boundary reader is owned by the private staging table owner' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_pending_dynamic_source_activations_v1(text,bigint,bytea,bigint)'::regprocedure + ), + 'cursor.generation = p_expected_cursor_generation' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.resolve_pending_dynamic_source_activations_v1(text,bigint,bytea,bigint)'::regprocedure + ), + 'page.expected_cursor_generation = p_expected_cursor_generation' + ) = 0, + 'activation resolution fences the current cursor without rejecting a future verified page' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_source_activations_v1(uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'page.expected_cursor_generation <' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_source_activations_v1(uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'cursor.block_number < page.snapshot_block_number' + ) > 0, + 'activation staging independently accepts only an older fence for a future parent block' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_source_activations_v1(uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'deployment.deployment_commitment' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.stage_verified_dynamic_source_activations_v1(uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,jsonb,jsonb,timestamp with time zone)'::regprocedure + ), + 'deployment.redacted_identity::text as identity' + ) = 0, + 'activation proofs bind endpoint-derived provider identities rather than static labels' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.consume_matching_provisional_sources_v1(uuid,uuid,uuid,uuid,uuid[],timestamp with time zone)'::regprocedure + ), + 'final block omitted a provisional child attestation' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.consume_matching_provisional_sources_v1(uuid,uuid,uuid,uuid,uuid[],timestamp with time zone)'::regprocedure + ), + 'final_runtime.runtime_code_a = staged_runtime.runtime_code_a' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.promote_projection_run_v3(text,uuid,uuid,uuid,uuid,text,bigint,bytea,bigint,bigint,bigint,uuid,uuid,numeric,bytea,numeric,text,uuid[],uuid[],uuid[],uuid[],text[],bytea,uuid,uuid[],uuid,bytea,timestamp with time zone)'::regprocedure + ), + 'consume_matching_provisional_sources_v1' + ) > 0, + 'only final exact promotion can consume complete provider-matched parent lineage' +); + +set local role programmable_projector; +select is( + ( + select pg_catalog.count(*) + from programmable_private.get_current_provisional_dynamic_sources_v1( + 'projector-v1' + ) + ), + 0::bigint, + 'current provisional source reader executes against release generation columns' +); +select is( + ( + select pg_catalog.count(*) + from programmable_private.get_current_provisional_activation_boundaries_v1( + 'projector-v1' + ) + ), + 0::bigint, + 'current activation boundary reader executes through the projector capability' +); +reset role; + +select * from finish(); +rollback; diff --git a/supabase/tests/database/014_candidate_promotion_operator.test.sql b/supabase/tests/database/014_candidate_promotion_operator.test.sql new file mode 100644 index 00000000..6cafa8c0 --- /dev/null +++ b/supabase/tests/database/014_candidate_promotion_operator.test.sql @@ -0,0 +1,132 @@ +begin; + +select plan(6); + +select ok( + has_schema_privilege( + 'programmable_operator', 'programmable_private', 'USAGE' + ) + and not has_schema_privilege( + 'programmable_operator', 'programmable_private', 'CREATE' + ), + 'promotion operator can resolve the private schema but cannot create objects' +); + +select ok( + ( + select pg_catalog.count(*) = 1 + and pg_catalog.bool_and( + function.proname = 'attest_candidate_database_promotion' + ) + from pg_catalog.pg_proc as function + join pg_catalog.pg_namespace as namespace + on namespace.oid = function.pronamespace + where namespace.nspname = 'programmable_private' + and has_function_privilege( + 'programmable_operator', function.oid, 'EXECUTE' + ) + ), + 'promotion operator can execute exactly the promotion attestation function' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relkind in ('r', 'p', 'v') + and ( + has_table_privilege( + 'programmable_operator', class.oid, 'SELECT' + ) + or has_table_privilege( + 'programmable_operator', class.oid, 'INSERT' + ) + or has_table_privilege( + 'programmable_operator', class.oid, 'UPDATE' + ) + or has_table_privilege( + 'programmable_operator', class.oid, 'DELETE' + ) + ) + ), + 'promotion operator has no direct private table or view privileges' +); + +select ok( + not exists ( + select 1 + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relkind = 'S' + and ( + has_sequence_privilege( + 'programmable_operator', class.oid, 'USAGE' + ) + or has_sequence_privilege( + 'programmable_operator', class.oid, 'SELECT' + ) + or has_sequence_privilege( + 'programmable_operator', class.oid, 'UPDATE' + ) + ) + ), + 'promotion operator has no private sequence privileges' +); + +set local role programmable_projector; + +select programmable_private.register_provider_deployment( + '91000000-0000-4000-8000-000000000001', + 'envio_deployment', + 'envio:production-7f24e63', + decode(repeat('11', 32), 'hex'), + decode(repeat('12', 32), 'hex'), + decode(repeat('13', 32), 'hex'), + '2026-07-31T00:00:00Z' +); + +select programmable_private.initialize_candidate_database( + '91000000-0000-4000-8000-000000000001', + decode(repeat('11', 32), 'hex'), + decode(repeat('12', 32), 'hex'), + decode(repeat('14', 32), 'hex'), + '2026-07-31T00:00:00Z' +); + +reset role; +set local role programmable_operator; + +select lives_ok( + $$ + select programmable_private.attest_candidate_database_promotion( + '91000000-0000-4000-8000-000000000001', + decode(repeat('21', 32), 'hex'), + decode(repeat('22', 32), 'hex'), + decode(repeat('23', 32), 'hex'), + decode(repeat('24', 32), 'hex'), + repeat('a', 40), + 'dpl_12345678901234567890', + '2026-07-31T00:05:00Z' + ) + $$, + 'promotion operator can execute the evidence-bound promotion' +); + +select throws_ok( + $$ + select pg_catalog.count(*) + from programmable_private.candidate_database_control + $$, + '42501', + 'promotion operator cannot read the candidate control table directly' +); + +reset role; + +select * from finish(); +rollback; diff --git a/supabase/tests/database/014_classic_v3_dynamic_activation_reward_seed.test.sql b/supabase/tests/database/014_classic_v3_dynamic_activation_reward_seed.test.sql new file mode 100644 index 00000000..9b5015f4 --- /dev/null +++ b/supabase/tests/database/014_classic_v3_dynamic_activation_reward_seed.test.sql @@ -0,0 +1,312 @@ +begin; +select plan(19); + +create function public.substring_count_v1(p_haystack text, p_needle text) +returns integer +language sql +immutable +strict +set search_path = '' +as $function$ + select ( + (pg_catalog.length(p_haystack) - pg_catalog.length( + pg_catalog.replace(p_haystack, p_needle, '') + )) / pg_catalog.length(p_needle) + )::integer +$function$; + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relname in ( + 'provisional_dynamic_parent_receipt_ordinals', + 'dynamic_source_activation_staging', + 'dynamic_source_activation_model_evidence', + 'dynamic_source_activation_consumptions' + ) + and class.relrowsecurity + and class.relforcerowsecurity + ), + 4, + 'all dynamic activation tables force RLS' +); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_constraint as constraint_record + join pg_catalog.pg_class as class + on class.oid = constraint_record.conrelid + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relname = 'dynamic_source_activation_staging' + and constraint_record.contype = 'u' + and pg_catalog.pg_get_constraintdef(constraint_record.oid) + like '%staging_run_id%' + ), + 0, + 'one staging run may contain multiple verified activations' +); + +select ok( + ( + select pg_catalog.pg_get_constraintdef(constraint_record.oid) + from pg_catalog.pg_constraint as constraint_record + join pg_catalog.pg_class as class + on class.oid = constraint_record.conrelid + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relname = 'dynamic_source_activation_staging' + and constraint_record.contype = 'c' + and pg_catalog.pg_get_constraintdef(constraint_record.oid) + like '%launch_transaction_hash%parent_transaction_hash%' + ) like '%launch_block_number%parent_block_number%', + 'earlier-block parents fail closed at the storage boundary' +); + +select is( + public.substring_count_v1( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'resolve_pending_dynamic_source_activations_v1' + ), + 'staged.reorg_generation = p_expected_reorg_generation' + ), + 1, + 'resolver excludes staged rows only in the same reorg generation' +); + +select is( + public.substring_count_v1( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'resolve_pending_dynamic_source_activations_v1' + ), + 'staged.parent_block_hash = source.factory_block_hash' + ), + 1, + 'same-address stale-fork staging does not suppress a replacement hash' +); + +select ok( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'stage_verified_dynamic_source_activations_v1' + ) like '%jsonb_array_length(p_activations) not between 1 and 32%', + 'the atomic stage contract accepts a bounded multi-activation batch' +); + +select is( + public.substring_count_v1( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'stage_verified_dynamic_source_activations_v1' + ), + 'inserted_count := inserted_count + 1' + ), + 2, + 'exact immutable replays count toward the all-or-nothing stage result' +); + +select ok( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'stage_verified_dynamic_source_activations_v1' + ) like '%provider_b_endpoint_origin_commitment%', + 'stage validation binds the full provider B tuple' +); + +select ok( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'stage_verified_dynamic_source_activations_v1' + ) like '%dual_rpc_block_evidence%safe_head_observations%', + 'stage validation binds the safe-head observation to block evidence' +); + +select is( + public.substring_count_v1( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_projector_verified_reward_seed_v1' + ), + 'factory_occurrence.verification_run_id = header.run_id' + ), + 1, + 'factory seed selection binds a noncanonical occurrence to the exact run' +); + +select is( + public.substring_count_v1( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_projector_verified_reward_seed_v1' + ), + 'required_occurrence.verification_run_id = header.run_id' + ), + 1, + 'every noncanonical required occurrence is bound to the exact run' +); + +select ok( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_dynamic_activation_seed_requests_v1' + ) like '%staged.launch_block_hash = p_target_block_hash%' + and ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'get_dynamic_activation_seed_requests_v1' + ) like '%launcher.verification_run_id = p_projection_run_id%', + 'same-height old-hash activation rows are not materialization eligible' +); + +select ok( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'consume_matching_dynamic_activations_v1' + ) like '%Starting from staged activations%' + and ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'consume_matching_dynamic_activations_v1' + ) like '%activation.launch_block_hash = publication.target_block_hash%', + 'consumption counts every exact eligible activation before fact joins' +); + +select ok( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = + 'consume_matching_dynamic_activations_v1' + ) like '%selected_evidence_id%allocation_evidence_id = selected_evidence_id%', + 'consumption uses the exact current verified evidence row' +); + +select is( + public.substring_count_v1( + ( + select pg_catalog.pg_get_functiondef(procedure.oid) + from pg_catalog.pg_proc as procedure + join pg_catalog.pg_namespace as namespace + on namespace.oid = procedure.pronamespace + where namespace.nspname = 'programmable_private' + and procedure.proname = 'promote_projection_run_v3' + ), + 'consume_matching_dynamic_activations_v1' + ), + 1, + 'promotion was extended exactly once' +); + +select ok( + not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.stage_verified_dynamic_source_activations_v1(uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,jsonb,jsonb,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'PUBLIC cannot stage dynamic activations' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.stage_verified_dynamic_source_activations_v1(uuid,text,uuid,bigint,bigint,bigint,bytea,uuid,uuid,uuid,uuid,uuid,jsonb,jsonb,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'only the projector capability can call the stage contract' +); + +select ok( + not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.consume_matching_dynamic_activations_v1(uuid,uuid,uuid,timestamp with time zone)'::regprocedure, + 'EXECUTE' + ), + 'activation consumption remains internal to promotion' +); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_class as class + join pg_catalog.pg_namespace as namespace + on namespace.oid = class.relnamespace + where namespace.nspname = 'programmable_private' + and class.relname in ( + 'dynamic_source_activation_staging', + 'dynamic_source_activation_model_evidence', + 'dynamic_source_activation_consumptions' + ) + and pg_catalog.has_table_privilege('public', class.oid, 'SELECT') + ), + 0, + 'dynamic activation evidence is not publicly readable' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/014_projector_reorg_exact_boundary.test.sql b/supabase/tests/database/014_projector_reorg_exact_boundary.test.sql new file mode 100644 index 00000000..0d9428ec --- /dev/null +++ b/supabase/tests/database/014_projector_reorg_exact_boundary.test.sql @@ -0,0 +1,284 @@ +begin; + +select plan(16); + +select ok( + to_regprocedure( + 'programmable_private.projector_reorg_invalidates_placement_v1(bigint,bytea,bigint,bigint,bytea,bigint)' + ) is not null + and to_regprocedure( + 'programmable_private.projector_reorg_invalidates_projection_run_v1(uuid,bigint,bytea,bigint,bytea,bigint)' + ) is not null, + 'the exact placement and published-run predicates exist' +); + +select ok( + count(*) = 2 + and pg_catalog.bool_and( + not procedure_row.prosecdef + and 'search_path=""' = any(procedure_row.proconfig) + and ( + (procedure_row.proname = + 'projector_reorg_invalidates_placement_v1' + and procedure_row.provolatile = 'i') + or (procedure_row.proname = + 'projector_reorg_invalidates_projection_run_v1' + and procedure_row.provolatile = 's') + ) + ), + 'reorg predicates have pinned invoker scope and declared volatility' +) +from pg_catalog.pg_proc as procedure_row +join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = procedure_row.pronamespace +where namespace_row.nspname = 'programmable_private' + and procedure_row.proname in ( + 'projector_reorg_invalidates_placement_v1', + 'projector_reorg_invalidates_projection_run_v1' + ); + +select ok( + not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.projector_reorg_invalidates_placement_v1(bigint,bytea,bigint,bigint,bytea,bigint)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.projector_reorg_invalidates_placement_v1(bigint,bytea,bigint,bigint,bytea,bigint)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.projector_reorg_invalidates_projection_run_v1(uuid,bigint,bytea,bigint,bytea,bigint)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.projector_reorg_invalidates_projection_run_v1(uuid,bigint,bytea,bigint,bytea,bigint)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'public', + 'programmable_private.delete_projector_projection_replay_scope_v1(bigint,text,text,bigint,bytea,bigint)'::regprocedure, + 'EXECUTE' + ) + and not pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.delete_projector_projection_replay_scope_v1(bigint,text,text,bigint,bytea,bigint)'::regprocedure, + 'EXECUTE' + ), + 'internal reorg cleanup helpers are not projector capabilities' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 7, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 7 + ), + false, + 'a history target preserves its exact chain placement' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 6, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 7 + ), + false, + 'a history target preserves earlier logs on the same block hash' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 8, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 7 + ), + true, + 'a history target invalidates later logs on the same block hash' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('bb', 32), 'hex'), 7, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 7 + ), + true, + 'a history target invalidates the same log index on another block hash' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('bb', 32), 'hex'), 1, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 7 + ), + true, + 'a history target invalidates an earlier log on another block hash' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 9, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), null + ), + false, + 'a genesis target preserves rows on its exact block hash without a log fence' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('bb', 32), 'hex'), 9, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), null + ), + true, + 'a genesis target invalidates same-height rows on another block hash' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), null, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), null + ), + false, + 'materialized state on the ancestor block hash survives replay cleanup' +); + +select is( + programmable_private.projector_reorg_invalidates_placement_v1( + 100, pg_catalog.decode(pg_catalog.repeat('bb', 32), 'hex'), null, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), null + ), + true, + 'materialized state on a stale same-height fork is rebuilt' +); + +select ok( + programmable_private.projector_reorg_invalidates_placement_v1( + 101, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), 0, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), null + ) + and not programmable_private.projector_reorg_invalidates_placement_v1( + 99, pg_catalog.decode(pg_catalog.repeat('bb', 32), 'hex'), 0, + 100, pg_catalog.decode(pg_catalog.repeat('aa', 32), 'hex'), null + ), + 'height ordering still invalidates descendants and preserves ancestors' +); + +with definitions as ( + select procedure_row.proname, + pg_catalog.lower(pg_catalog.pg_get_functiondef(procedure_row.oid)) + as definition + from pg_catalog.pg_proc as procedure_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = procedure_row.pronamespace + where namespace_row.nspname = 'programmable_private' + and procedure_row.proname in ( + 'recover_projector_reorg_v1', + 'delete_projector_projection_replay_scope_v1', + 'projector_reorg_invalidates_projection_run_v1' + ) +) +select ok( + ( + select ( + pg_catalog.length(definition) + - pg_catalog.length(pg_catalog.replace( + definition, 'projector_reorg_invalidates_placement_v1', '' + )) + ) / pg_catalog.length('projector_reorg_invalidates_placement_v1') = 2 + from definitions + where proname = 'recover_projector_reorg_v1' + ) + and ( + select ( + pg_catalog.length(definition) + - pg_catalog.length(pg_catalog.replace( + definition, 'projector_reorg_invalidates_projection_run_v1', '' + )) + ) / pg_catalog.length( + 'projector_reorg_invalidates_projection_run_v1' + ) = 16 + from definitions + where proname = 'delete_projector_projection_replay_scope_v1' + ) + and ( + select ( + pg_catalog.length(definition) + - pg_catalog.length(pg_catalog.replace( + definition, 'projector_reorg_invalidates_placement_v1', '' + )) + ) / pg_catalog.length('projector_reorg_invalidates_placement_v1') = 1 + from definitions + where proname = 'projector_reorg_invalidates_projection_run_v1' + ), + 'recovery and replay cleanup apply exact placement through published run lineage' +); + +with definitions as ( + select procedure_row.proname, + pg_catalog.lower(pg_catalog.pg_get_functiondef(procedure_row.oid)) + as definition + from pg_catalog.pg_proc as procedure_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = procedure_row.pronamespace + where namespace_row.nspname = 'programmable_private' + and procedure_row.proname in ( + 'recover_projector_reorg_v1', + 'delete_projector_projection_replay_scope_v1' + ) +) +select ok( + pg_catalog.strpos( + (select definition from definitions + where proname = 'recover_projector_reorg_v1'), + 'delete_projector_projection_replay_scope_v1' + ) > 0 + and pg_catalog.strpos( + (select definition from definitions + where proname = 'delete_projector_projection_replay_scope_v1'), + 'launch_position_liquidity_facts' + ) > 0 + and pg_catalog.strpos( + (select definition from definitions + where proname = 'delete_projector_projection_replay_scope_v1'), + 'baseline_reward_vault_projection_id' + ) > 0, + 'atomic recovery delegates to FK-ordered launch and reward snapshot cleanup' +); + +with cleanup as ( + select pg_catalog.lower(pg_catalog.pg_get_functiondef(procedure_row.oid)) + as definition + from pg_catalog.pg_proc as procedure_row + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = procedure_row.pronamespace + where namespace_row.nspname = 'programmable_private' + and procedure_row.proname = + 'delete_projector_projection_replay_scope_v1' +), restricted_children as ( + select child_table.relname as child_table + from pg_catalog.pg_constraint as constraint_row + join pg_catalog.pg_class as child_table + on child_table.oid = constraint_row.conrelid + join pg_catalog.pg_class as parent_table + on parent_table.oid = constraint_row.confrelid + join pg_catalog.pg_namespace as namespace_row + on namespace_row.oid = child_table.relnamespace + where constraint_row.contype = 'f' + and constraint_row.confdeltype = 'r' + and namespace_row.nspname = 'programmable_private' + and parent_table.relname in ( + 'launch_projections', 'pool_projections', + 'reward_vault_projections', 'initial_buy_custody_projections' + ) +) +select ok( + pg_catalog.bool_and( + pg_catalog.strpos(cleanup.definition, restricted_children.child_table) > 0 + ), + 'replay cleanup names every RESTRICT child in the projection dependency graph' +) +from cleanup cross join restricted_children; + +select * from finish(); +rollback; diff --git a/supabase/tests/database/015_candidate_projector_unpromoted_gate.test.sql b/supabase/tests/database/015_candidate_projector_unpromoted_gate.test.sql new file mode 100644 index 00000000..de5b072b --- /dev/null +++ b/supabase/tests/database/015_candidate_projector_unpromoted_gate.test.sql @@ -0,0 +1,253 @@ +begin; + +select plan(14); + +select ok( + to_regprocedure( + 'programmable_private.verify_candidate_database_unpromoted_v1(uuid,bytea,bytea,bytea,timestamp with time zone)' + ) is not null, + 'candidate unpromoted verifier exists at the frozen signature' +); + +select ok( + ( + select procedure.prosecdef + and procedure.provolatile = 's' + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + where procedure.oid = + 'programmable_private.verify_candidate_database_unpromoted_v1(uuid,bytea,bytea,bytea,timestamp with time zone)'::regprocedure + ), + 'candidate verifier is stable, SECURITY DEFINER, and has an empty search path' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.verify_candidate_database_unpromoted_v1(uuid,bytea,bytea,bytea,timestamp with time zone)', + 'EXECUTE' + ), + 'only the projector capability receives the verifier' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector_runtime', 'programmable_reconciler', + 'programmable_api_reader', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_operator' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.verify_candidate_database_unpromoted_v1(uuid,bytea,bytea,bytea,timestamp with time zone)', + 'EXECUTE' + ) + ), + 'browser, runtime, reader, reconciler, profile, maintenance, and operator roles are denied' +); + +select ok( + not pg_catalog.has_table_privilege( + 'programmable_projector', + 'programmable_private.candidate_database_control', + 'SELECT,INSERT,UPDATE,DELETE' + ), + 'projector has no direct candidate control table privilege' +); + +set local role programmable_projector; + +select is( + programmable_private.register_provider_deployment( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + 'envio_deployment', 'envio:production-7f24e63', + pg_catalog.decode( + 'a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', + 'hex' + ), + pg_catalog.decode( + '5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', + 'hex' + ), + pg_catalog.decode(pg_catalog.repeat('11', 32), 'hex'), + '2026-07-31T00:00:00Z' + ), + 'd08b62a6-74fb-5e0a-a698-dc6877150db4'::uuid, + 'reviewed candidate provider is registered' +); + +select throws_ok( + $sql$ + select programmable_private.verify_candidate_database_unpromoted_v1( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + decode( + 'a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', + 'hex' + ), + decode( + '5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', + 'hex' + ), + decode( + 'e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', + 'hex' + ), + '2026-07-31T00:00:00Z' + ) + $sql$, + '55000', + 'candidate database is not in the exact unpromoted state', + 'missing candidate control state fails closed' +); + +reset role; + +select is( + (select pg_catalog.count(*) from programmable_private.candidate_database_control), + 0::bigint, + 'missing-state verification never inserts candidate control state' +); + +set local role programmable_projector; + +select is( + programmable_private.initialize_candidate_database( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + pg_catalog.decode( + 'a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', + 'hex' + ), + pg_catalog.decode( + '5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', + 'hex' + ), + pg_catalog.decode( + 'e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', + 'hex' + ), + '2026-07-31T00:00:00Z' + ), + true, + 'reviewed candidate control state initializes exactly once' +); + +select is( + programmable_private.verify_candidate_database_unpromoted_v1( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + pg_catalog.decode( + 'a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', + 'hex' + ), + pg_catalog.decode( + '5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', + 'hex' + ), + pg_catalog.decode( + 'e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', + 'hex' + ), + '2026-07-31T00:00:00Z' + ), + true, + 'exact isolated unpromoted candidate state passes' +); + +savepoint mixed_envio_provider; + +select programmable_private.register_provider_deployment( + 'd08b62a6-74fb-5e0a-a698-dc6877150db5', + 'envio_deployment', 'envio:production-legacy', + pg_catalog.decode(pg_catalog.repeat('21', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('22', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('23', 32), 'hex'), + pg_catalog.clock_timestamp() +); + +select throws_ok( + $sql$ + select programmable_private.verify_candidate_database_unpromoted_v1( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + decode( + 'a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', + 'hex' + ), + decode( + '5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', + 'hex' + ), + decode( + 'e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', + 'hex' + ), + '2026-07-31T00:00:00Z' + ) + $sql$, + '55000', + 'candidate database is not in the exact unpromoted state', + 'a second Envio deployment makes the candidate database mixed and invalid' +); + +rollback to savepoint mixed_envio_provider; +reset role; + +set local role programmable_operator; + +select is( + programmable_private.attest_candidate_database_promotion( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + pg_catalog.decode(pg_catalog.repeat('31', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('32', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('33', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('34', 32), 'hex'), + pg_catalog.repeat('a', 40), + 'dpl_12345678901234567890', + '2026-07-31T00:05:00Z' + ), + true, + 'promotion attestation is recorded for the exact candidate' +); + +reset role; +set local role programmable_projector; + +select throws_ok( + $sql$ + select programmable_private.verify_candidate_database_unpromoted_v1( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + decode( + 'a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', + 'hex' + ), + decode( + '5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', + 'hex' + ), + decode( + 'e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', + 'hex' + ), + '2026-07-31T00:00:00Z' + ) + $sql$, + '55000', + 'candidate database is not in the exact unpromoted state', + 'an exactly promoted candidate is rejected by backfill mode' +); + +reset role; + +select ok( + exists ( + select 1 + from programmable_private.candidate_database_control + where singleton and promoted_at = '2026-07-31T00:05:00Z' + and promotion_attestation_commitment is not null + ), + 'promoted state remains explicit and complete after rejection' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/016_candidate_projector_promoted_gate.test.sql b/supabase/tests/database/016_candidate_projector_promoted_gate.test.sql new file mode 100644 index 00000000..1c73877f --- /dev/null +++ b/supabase/tests/database/016_candidate_projector_promoted_gate.test.sql @@ -0,0 +1,272 @@ +begin; + +select plan(19); + +select ok( + to_regprocedure( + 'programmable_private.verify_candidate_database_promoted_v2(uuid,bytea,bytea,bytea,timestamp with time zone,text,text)' + ) is not null, + 'product-bound candidate verifier exists at the frozen signature' +); + +select ok( + ( + select procedure.prosecdef + and procedure.provolatile = 's' + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + where procedure.oid = + 'programmable_private.verify_candidate_database_promoted_v2(uuid,bytea,bytea,bytea,timestamp with time zone,text,text)'::regprocedure + ), + 'product-bound verifier is stable, SECURITY DEFINER, and has an empty search path' +); + +select ok( + ( + select candidate_constraint.convalidated + from pg_catalog.pg_constraint as candidate_constraint + where candidate_constraint.conname = 'candidate_database_control_product_binding' + and candidate_constraint.conrelid = + 'programmable_private.candidate_database_control'::regclass + ), + 'candidate product binding constraint is validated' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.verify_candidate_database_promoted_v2(uuid,bytea,bytea,bytea,timestamp with time zone,text,text)', + 'EXECUTE' + ), + 'only the projector capability receives the product-bound verifier' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector_runtime', 'programmable_reconciler', + 'programmable_api_reader', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_operator' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.verify_candidate_database_promoted_v2(uuid,bytea,bytea,bytea,timestamp with time zone,text,text)', + 'EXECUTE' + ) + ), + 'browser, runtime, reader, reconciler, profile, maintenance, and operator roles are denied the verifier' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_operator', + 'programmable_private.attest_candidate_database_promotion(uuid,bytea,bytea,bytea,bytea,text,text,timestamp with time zone)', + 'EXECUTE' + ), + 'operator receives only the product-bound promotion capability' +); + +select ok( + not pg_catalog.has_function_privilege( + 'programmable_operator', + 'programmable_private.attest_candidate_database_promotion(uuid,bytea,bytea,bytea,bytea,timestamp with time zone)', + 'EXECUTE' + ), + 'legacy promotion signature is retired' +); + +select ok( + not pg_catalog.has_table_privilege( + 'programmable_projector', + 'programmable_private.candidate_database_control', + 'SELECT,INSERT,UPDATE,DELETE' + ), + 'projector has no direct candidate control table privilege' +); + +set local role programmable_projector; + +select is( + programmable_private.register_provider_deployment( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + 'envio_deployment', 'envio:production-7f24e63', + pg_catalog.decode( + 'a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', + 'hex' + ), + pg_catalog.decode( + '5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', + 'hex' + ), + pg_catalog.decode(pg_catalog.repeat('11', 32), 'hex'), + '2026-07-31T00:00:00Z' + ), + 'd08b62a6-74fb-5e0a-a698-dc6877150db4'::uuid, + 'reviewed candidate provider is registered' +); + +select is( + programmable_private.initialize_candidate_database( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + pg_catalog.decode('a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', 'hex'), + pg_catalog.decode('5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', 'hex'), + pg_catalog.decode('e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', 'hex'), + '2026-07-31T00:00:00Z' + ), + true, + 'candidate control state initializes exactly once' +); + +reset role; +set local role programmable_operator; + +select throws_ok( + $sql$ + select programmable_private.attest_candidate_database_promotion( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + decode(repeat('31', 32), 'hex'), decode(repeat('32', 32), 'hex'), + decode(repeat('33', 32), 'hex'), decode(repeat('34', 32), 'hex'), + repeat('0', 40), 'dpl_12345678901234567890', + '2026-07-31T00:05:00Z' + ) + $sql$, + '23514', + 'candidate product-bound promotion evidence is incomplete', + 'zero product commit fails closed' +); + +reset role; + +select is( + ( + select pg_catalog.count(*) + from programmable_private.candidate_database_control + where singleton + and promoted_at is null + and product_commit is null + and staged_deployment_id is null + ), + 1::bigint, + 'failed promotion leaves the candidate fence unchanged' +); + +set local role programmable_operator; + +select is( + programmable_private.attest_candidate_database_promotion( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + pg_catalog.decode(pg_catalog.repeat('31', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('32', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('33', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('34', 32), 'hex'), + pg_catalog.repeat('a', 40), + 'dpl_12345678901234567890', + '2026-07-31T00:05:00Z' + ), + true, + 'exact product-bound promotion is recorded' +); + +select is( + programmable_private.attest_candidate_database_promotion( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + pg_catalog.decode(pg_catalog.repeat('31', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('32', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('33', 32), 'hex'), + pg_catalog.decode(pg_catalog.repeat('34', 32), 'hex'), + pg_catalog.repeat('a', 40), + 'dpl_12345678901234567890', + '2026-07-31T00:05:00Z' + ), + false, + 'exact product-bound promotion replay is idempotent' +); + +select throws_ok( + $sql$ + select programmable_private.attest_candidate_database_promotion( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + decode(repeat('31', 32), 'hex'), decode(repeat('32', 32), 'hex'), + decode(repeat('33', 32), 'hex'), decode(repeat('34', 32), 'hex'), + repeat('b', 40), 'dpl_12345678901234567890', + '2026-07-31T00:05:00Z' + ) + $sql$, + '23505', + 'candidate product-bound promotion replay conflict', + 'conflicting product replay fails closed' +); + +reset role; +set local role programmable_projector; + +select is( + programmable_private.verify_candidate_database_promoted_v2( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + pg_catalog.decode('a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', 'hex'), + pg_catalog.decode('5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', 'hex'), + pg_catalog.decode('e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', 'hex'), + '2026-07-31T00:00:00Z', + pg_catalog.repeat('a', 40), + 'dpl_12345678901234567890' + ), + true, + 'the exact executing product passes the promoted database gate' +); + +select throws_ok( + $sql$ + select programmable_private.verify_candidate_database_promoted_v2( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + decode('a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', 'hex'), + decode('5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', 'hex'), + decode('e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', 'hex'), + '2026-07-31T00:00:00Z', repeat('b', 40), + 'dpl_12345678901234567890' + ) + $sql$, + '55000', + 'candidate database is not bound to this promoted product', + 'a different Git commit fails closed' +); + +select throws_ok( + $sql$ + select programmable_private.verify_candidate_database_promoted_v2( + 'd08b62a6-74fb-5e0a-a698-dc6877150db4', + decode('a4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259', 'hex'), + decode('5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1', 'hex'), + decode('e3218e30a2a95927427fe5e523a8f721fa0d7826dffaecb7140a126a56d17a44', 'hex'), + '2026-07-31T00:00:00Z', repeat('a', 40), + 'dpl_09876543210987654321' + ) + $sql$, + '55000', + 'candidate database is not bound to this promoted product', + 'a different Vercel deployment fails closed' +); + +reset role; + +select is( + ( + select pg_catalog.count(*) + from programmable_private.candidate_database_control + where singleton + and promoted_at = '2026-07-31T00:05:00Z' + and product_commit = pg_catalog.repeat('a', 40) + and staged_deployment_id = 'dpl_12345678901234567890' + and pg_catalog.octet_length(promotion_baseline_commitment) = 32 + and pg_catalog.octet_length(promotion_parity_commitment) = 32 + and pg_catalog.octet_length(promotion_attestation_commitment) = 32 + and pg_catalog.octet_length(promotion_input_commitment) = 32 + ), + 1::bigint, + 'promoted state remains complete and bound after verifier failures' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/017_projection_writer_event_authorization.test.sql b/supabase/tests/database/017_projection_writer_event_authorization.test.sql new file mode 100644 index 00000000..c303376a --- /dev/null +++ b/supabase/tests/database/017_projection_writer_event_authorization.test.sql @@ -0,0 +1,102 @@ +begin; + +select plan(7); + +select ok( + to_regprocedure( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)' + ) is not null, + 'projection event authorization remains at its frozen signature' +); + +select ok( + ( + select procedure.prosecdef + and procedure.provolatile = 's' + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + where procedure.oid = + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)'::regprocedure + ), + 'projection event authorization is stable, SECURITY DEFINER, and has an empty search path' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)', + 'EXECUTE' + ), + 'the projector capability can authorize projection writers' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_reconciler', 'programmable_api_reader', + 'programmable_profile_binder', 'programmable_profile_recovery', + 'programmable_profile_writer', 'programmable_maintenance', + 'programmable_operator' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)', + 'EXECUTE' + ) + ), + 'browser, reader, profile, maintenance, reconciler, and operator roles remain denied' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)'::regprocedure + ), + 'release_launch_completeness_requirements' + ) > 0, + 'launch occurrence roles are authorized by the exact completeness requirement' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)'::regprocedure + ), + 'pool-registration' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)'::regprocedure + ), + 'fee-disclosure' + ) > 0, + 'pool writers map to their exact semantic event rules' +); + +select ok( + pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)'::regprocedure + ), + 'reward-vault-deployment' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)'::regprocedure + ), + 'initial-buy-custody' + ) > 0 + and pg_catalog.strpos( + pg_catalog.pg_get_functiondef( + 'programmable_private.assert_projection_event_allowed(uuid,uuid,text)'::regprocedure + ), + 'vesting-wallet-deployment' + ) > 0, + 'reward and custody writers map to their exact semantic event rules' +); + +select * from finish(); + +rollback; diff --git a/supabase/tests/database/017_safe_head_reuse.test.sql b/supabase/tests/database/017_safe_head_reuse.test.sql new file mode 100644 index 00000000..35b0f733 --- /dev/null +++ b/supabase/tests/database/017_safe_head_reuse.test.sql @@ -0,0 +1,268 @@ +begin; + +set local role programmable_projector; + +select programmable_private.create_release_epoch( + 'a7000000-0000-4000-8000-000000000001', + 1, 'classic-v3', 'classic-v3', 'core', 1, + decode(repeat('10', 32), 'hex'), + decode(repeat('11', 32), 'hex'), + decode(repeat('12', 32), 'hex'), + '2026-08-01T12:00:00Z' +); +select programmable_private.activate_release_epoch( + 1, 'classic-v3', 'classic-v3', 'core', + 'a7000000-0000-4000-8000-000000000001', + 0, 1, decode(repeat('13', 32), 'hex'), + '2026-08-01T12:00:01Z' +); +select programmable_private.register_rpc_provider_deployment( + 'b7000000-0000-4000-8000-000000000001', + 1, 'alchemy', 'rpc-provider-v1', + decode(repeat('a1', 32), 'hex'), decode(repeat('a2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('a3', 32), 'hex'), + decode(repeat('21', 32), 'hex'), decode(repeat('22', 32), 'hex'), + decode(repeat('23', 32), 'hex'), '2026-08-01T12:00:02Z' +); +select programmable_private.register_rpc_provider_deployment( + 'b7000000-0000-4000-8000-000000000002', + 1, 'quicknode', 'rpc-provider-v1', + decode(repeat('b1', 32), 'hex'), decode(repeat('b2', 32), 'hex'), + 'rpc-endpoint-commitments-v1', decode(repeat('b3', 32), 'hex'), + decode(repeat('24', 32), 'hex'), decode(repeat('25', 32), 'hex'), + decode(repeat('26', 32), 'hex'), '2026-08-01T12:00:03Z' +); +select programmable_private.open_run( + 'c7000000-0000-4000-8000-000000000001', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + 'a7000000-0000-4000-8000-000000000001', 1, + 'projector-v1', decode(repeat('31', 32), 'hex'), + '2026-08-01T12:01:00Z' +); +select programmable_private.open_run( + 'c7000000-0000-4000-8000-000000000002', + 'ingestion', 1, 'classic-v3', 'classic-v3', 'core', + 'a7000000-0000-4000-8000-000000000001', 1, + 'projector-v1', decode(repeat('32', 32), 'hex'), + '2026-08-01T12:01:01Z' +); + +reset role; + +select plan(16); + +select ok( + to_regprocedure( + 'programmable_private.append_or_reuse_safe_head_observation_v1(uuid,uuid,uuid,uuid,bigint,bigint,numeric,numeric,bigint,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)' + ) is not null, + 'safe-head reuse function exists at the frozen signature' +); + +select ok( + ( + select procedure.prosecdef + and procedure.provolatile = 'v' + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + where procedure.oid = + 'programmable_private.append_or_reuse_safe_head_observation_v1(uuid,uuid,uuid,uuid,bigint,bigint,numeric,numeric,bigint,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)'::regprocedure + ), + 'safe-head reuse is volatile, SECURITY DEFINER, and has an empty search path' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.append_or_reuse_safe_head_observation_v1(uuid,uuid,uuid,uuid,bigint,bigint,numeric,numeric,bigint,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)', + 'EXECUTE' + ), + 'projector receives the safe-head reuse capability' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector_runtime', 'programmable_reconciler', + 'programmable_api_reader', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_operator' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.append_or_reuse_safe_head_observation_v1(uuid,uuid,uuid,uuid,bigint,bigint,numeric,numeric,bigint,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)', + 'EXECUTE' + ) + ), + 'browser, runtime, reader, reconciler, profile, maintenance, and operator roles are denied safe-head reuse' +); + +select ok( + to_regprocedure( + 'programmable_private.append_or_reuse_dual_rpc_block_evidence_v1(uuid,uuid,uuid,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)' + ) is not null, + 'block-evidence reuse function exists at the frozen signature' +); + +select ok( + ( + select procedure.prosecdef + and procedure.provolatile = 'v' + and 'search_path=""' = any(procedure.proconfig) + from pg_catalog.pg_proc as procedure + where procedure.oid = + 'programmable_private.append_or_reuse_dual_rpc_block_evidence_v1(uuid,uuid,uuid,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)'::regprocedure + ), + 'block-evidence reuse is volatile, SECURITY DEFINER, and has an empty search path' +); + +select ok( + pg_catalog.has_function_privilege( + 'programmable_projector', + 'programmable_private.append_or_reuse_dual_rpc_block_evidence_v1(uuid,uuid,uuid,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)', + 'EXECUTE' + ), + 'projector receives the block-evidence reuse capability' +); + +select ok( + not exists ( + select 1 + from pg_catalog.unnest(array[ + 'public', 'anon', 'authenticated', 'service_role', + 'programmable_projector_runtime', 'programmable_reconciler', + 'programmable_api_reader', 'programmable_profile_binder', + 'programmable_profile_recovery', 'programmable_profile_writer', + 'programmable_maintenance', 'programmable_operator' + ]) as denied(role_name) + where pg_catalog.has_function_privilege( + denied.role_name, + 'programmable_private.append_or_reuse_dual_rpc_block_evidence_v1(uuid,uuid,uuid,numeric,bytea,bytea,smallint,bytea,bytea,timestamp with time zone)', + 'EXECUTE' + ) + ), + 'browser, runtime, reader, reconciler, profile, maintenance, and operator roles are denied block-evidence reuse' +); + +set local role programmable_projector; + +select is( + programmable_private.append_or_reuse_safe_head_observation_v1( + 'd7000000-0000-4000-8000-000000000001', + 'c7000000-0000-4000-8000-000000000001', + 'b7000000-0000-4000-8000-000000000001', + 'b7000000-0000-4000-8000-000000000002', + 1, 1, 100, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000100', 'hex'), + decode(repeat('41', 32), 'hex'), '2026-08-01T12:01:02Z' + ), + 'd7000000-0000-4000-8000-000000000001'::uuid, + 'first safe-head observation is appended' +); + +select is( + programmable_private.append_or_reuse_safe_head_observation_v1( + 'd7000000-0000-4000-8000-000000000002', + 'c7000000-0000-4000-8000-000000000002', + 'b7000000-0000-4000-8000-000000000001', + 'b7000000-0000-4000-8000-000000000002', + 1, 1, 100, 100, 12, 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000100', 'hex'), + decode(repeat('41', 32), 'hex'), '2026-08-01T12:01:03Z' + ), + 'd7000000-0000-4000-8000-000000000001'::uuid, + 'an exact replay returns the immutable existing observation' +); + +reset role; +set local role programmable_migrator; + +select is( + (select pg_catalog.count(*) from programmable_private.safe_head_observations), + 1::bigint, + 'an exact replay does not duplicate safe-head evidence' +); + +select is( + ( + select pg_catalog.count(*) + from programmable_private.mutation_audits + where action = 'safe_head.append' + ), + 1::bigint, + 'an exact replay does not append a false mutation audit' +); + +set local role programmable_projector; + +select is( + programmable_private.append_dual_rpc_block_evidence( + 'e7000000-0000-4000-8000-000000000001', + 'd7000000-0000-4000-8000-000000000001', + 'c7000000-0000-4000-8000-000000000002', + 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000200', 'hex'), + decode(repeat('51', 32), 'hex'), '2026-08-01T12:01:04Z' + ), + 'e7000000-0000-4000-8000-000000000001'::uuid, + 'a later run can bind block evidence to the reused safe head' +); + +select is( + programmable_private.append_or_reuse_dual_rpc_block_evidence_v1( + 'e7000000-0000-4000-8000-000000000002', + 'd7000000-0000-4000-8000-000000000001', + 'c7000000-0000-4000-8000-000000000002', + 88, + decode(repeat('88', 32), 'hex'), decode(repeat('88', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000200', 'hex'), + decode(repeat('51', 32), 'hex'), '2026-08-01T12:01:04Z' + ), + 'e7000000-0000-4000-8000-000000000001'::uuid, + 'an exact block-evidence replay returns the immutable existing row' +); + +reset role; +set local role programmable_migrator; + +select is( + ( + select pg_catalog.count(*) + from programmable_private.mutation_audits + where action = 'block_evidence.append' + ), + 1::bigint, + 'an exact block-evidence replay does not append a false mutation audit' +); + +set local role programmable_projector; + +select throws_ok( + $sql$ + select programmable_private.append_or_reuse_safe_head_observation_v1( + 'd7000000-0000-4000-8000-000000000003', + 'c7000000-0000-4000-8000-000000000002', + 'b7000000-0000-4000-8000-000000000001', + 'b7000000-0000-4000-8000-000000000002', + 1, 1, 101, 101, 12, 89, + decode(repeat('89', 32), 'hex'), decode(repeat('89', 32), 'hex'), + 2::smallint, + decode('70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7632000100', 'hex'), + decode(repeat('41', 32), 'hex'), '2026-08-01T12:01:05Z' + ) + $sql$, + '23505', + 'safe-head fingerprint replay conflicts with stored evidence', + 'a mismatched replay with the same fingerprint fails closed' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/database/018_reused_receipt_ordinals_across_transactions.test.sql b/supabase/tests/database/018_reused_receipt_ordinals_across_transactions.test.sql new file mode 100644 index 00000000..934f963f --- /dev/null +++ b/supabase/tests/database/018_reused_receipt_ordinals_across_transactions.test.sql @@ -0,0 +1,57 @@ +begin; +select plan(3); + +select is( + ( + select pg_catalog.count(*)::integer + from pg_catalog.pg_constraint as constraint_record + join pg_catalog.pg_class as table_record + on table_record.oid = constraint_record.conrelid + join pg_catalog.pg_namespace as namespace_record + on namespace_record.oid = table_record.relnamespace + where namespace_record.nspname = 'programmable_private' + and table_record.relname = + 'provisional_dynamic_parent_receipt_ordinals' + and constraint_record.contype = 'u' + and pg_catalog.pg_get_constraintdef(constraint_record.oid) = + 'UNIQUE (provisional_page_id, receipt_log_ordinal)' + ), + 0, + 'receipt ordinals may repeat across different parent transactions' +); + +select is( + ( + select pg_catalog.pg_get_constraintdef(constraint_record.oid) + from pg_catalog.pg_constraint as constraint_record + join pg_catalog.pg_class as table_record + on table_record.oid = constraint_record.conrelid + join pg_catalog.pg_namespace as namespace_record + on namespace_record.oid = table_record.relnamespace + where namespace_record.nspname = 'programmable_private' + and table_record.relname = + 'provisional_dynamic_parent_receipt_ordinals' + and constraint_record.contype = 'p' + ), + 'PRIMARY KEY (provisional_page_id, parent_candidate_id)', + 'the immutable parent candidate remains the row identity' +); + +select is( + pg_catalog.col_description( + 'programmable_private.provisional_dynamic_parent_receipt_ordinals'::regclass, + ( + select attribute.attnum + from pg_catalog.pg_attribute as attribute + where attribute.attrelid = + 'programmable_private.provisional_dynamic_parent_receipt_ordinals'::regclass + and attribute.attname = 'receipt_log_ordinal' + and not attribute.attisdropped + ) + ), + 'Zero-based log ordinal within the parent transaction receipt; values may repeat across different parent candidates.', + 'the receipt ordinal scope is documented in the schema' +); + +select * from finish(); +rollback; diff --git a/supabase/tests/pglite/pgtap-compatibility.sql b/supabase/tests/pglite/pgtap-compatibility.sql new file mode 100644 index 00000000..5aa46569 --- /dev/null +++ b/supabase/tests/pglite/pgtap-compatibility.sql @@ -0,0 +1,107 @@ +-- PGlite does not bundle the pgTAP extension. These narrow compatibility +-- assertions execute the repository's SQL tests and raise immediately on a +-- failed assertion. Hosted PostgreSQL/Supabase pgTAP remains the authoritative +-- extension-backed gate; this runner is a deterministic fresh-database gate. +create function public.plan(integer) returns text +language sql as $$ select 'plan'::text $$; + +create function public.ok(boolean, text) returns text +language plpgsql as $$ +begin + if not coalesce($1, false) then + raise exception 'assertion failed: %', $2; + end if; + return $2; +end +$$; + +create function public.is(anycompatible, anycompatible, text) returns text +language plpgsql as $$ +begin + if $1 is distinct from $2 then + raise exception 'assertion failed: % (got %, expected %)', $3, $1, $2; + end if; + return $3; +end +$$; + +create function public.lives_ok(text, text) returns text +language plpgsql as $$ +begin + execute $1; + return $2; +exception when others then + raise exception 'lives_ok failed: % [%] %', $2, sqlstate, sqlerrm; +end +$$; + +create function public.throws_ok(text, text, text) returns text +language plpgsql as $$ +begin + execute $1; + raise exception 'throws_ok failed: % did not throw', $3; +exception when others then + if sqlstate = 'P0001' and sqlerrm like 'throws_ok failed:%' then raise; end if; + if sqlstate <> $2 then + raise exception 'throws_ok failed: % got SQLSTATE %, expected %; %', + $3, sqlstate, $2, sqlerrm; + end if; + return $3; +end +$$; + +create function public.throws_ok(text, text, text, text) returns text +language plpgsql as $$ +begin + execute $1; + raise exception 'throws_ok failed: % did not throw', $4; +exception when others then + if sqlstate = 'P0001' and sqlerrm like 'throws_ok failed:%' then raise; end if; + if sqlstate <> $2 or sqlerrm <> $3 then + raise exception 'throws_ok failed: % got [%] %, expected [%] %', + $4, sqlstate, sqlerrm, $2, $3; + end if; + return $4; +end +$$; + +create function public.has_schema(text) returns boolean +language sql as $$ + select exists (select 1 from pg_catalog.pg_namespace where nspname = $1) +$$; + +create function public.has_schema(text, text) returns text +language plpgsql as $$ +begin + if not public.has_schema($1) then + raise exception 'assertion failed: %', $2; + end if; + return $2; +end +$$; + +create function public.has_domain(text, text) returns boolean +language sql as $$ + select exists ( + select 1 + from pg_catalog.pg_type as type_row + join pg_catalog.pg_namespace as namespace + on namespace.oid = type_row.typnamespace + where namespace.nspname = $1 + and type_row.typname = $2 + and type_row.typtype = 'd' + ) +$$; + +create function public.has_domain(text, text, text) returns text +language plpgsql as $$ +begin + if not public.has_domain($1, $2) then + raise exception 'assertion failed: %', $3; + end if; + return $3; +end +$$; + +create function public.finish() returns setof text +language sql as $$ select 'finish'::text $$; diff --git a/tests/action-rpc-quorum.test.ts b/tests/action-rpc-quorum.test.ts new file mode 100644 index 00000000..8a06a8b6 --- /dev/null +++ b/tests/action-rpc-quorum.test.ts @@ -0,0 +1,229 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + ActionRpcQuorumError, + classicV3ActionRpcProviders, + createActionRpcQuorum, + creatorClaimRpcProviders, + stockPairedActionRpcProviders, + tradeActionRpcProviders, +} from "../lib/server/action-rpc-quorum.server"; + +const ALCHEMY_MAINNET_A = + "https://eth-mainnet.g.alchemy.com/v2/alchemy-key-one"; +const ALCHEMY_MAINNET_B = + "https://eth-mainnet.g.alchemy.com/v2/alchemy-key-two"; +const ALCHEMY_SEPOLIA = + "https://eth-sepolia.g.alchemy.com/v2/alchemy-sepolia-key"; +const QUICKNODE_MAINNET = + "https://quiet-mainnet.quiknode.pro/quicknode-key-one/"; +const QUICKNODE_SEPOLIA_ALIAS = + "https://quiet-sepolia.quiknode.pro/quicknode-key-two/"; +const INFURA_MAINNET_A = + "https://mainnet.infura.io/v3/infura-key-one"; +const INFURA_MAINNET_B = + "https://mainnet.infura.io/v3/infura-key-two"; +const DRPC_PAID_MAINNET = + "https://lb.drpc.org/ogrpc?network=ethereum&dkey=drpc-key-one"; + +function expectSafeFailure(run: () => unknown, secret: string) { + try { + run(); + throw new Error("expected RPC quorum failure"); + } catch (error) { + expect(error).toBeInstanceOf(ActionRpcQuorumError); + expect(String(error)).not.toContain(secret); + expect(JSON.stringify(error)).not.toContain(secret); + } +} + +function expectIndependent( + providers: ReturnType, +) { + expect(providers.length).toBeGreaterThanOrEqual(2); + expect(new Set(providers.map((provider) => provider.vendorGroup)).size).toBe( + providers.length, + ); + expect( + new Set( + providers.map((provider) => provider.endpointOriginCommitment), + ).size, + ).toBe(providers.length); +} + +describe("action RPC provider identity", () => { + it("keeps transport credentials non-enumerable and error output redacted", () => { + const providers = createActionRpcQuorum({ + chainId: 1, + primary: ALCHEMY_MAINNET_A, + secondary: QUICKNODE_MAINNET, + }); + + expectIndependent(providers); + expect(providers[0]?.endpoint).toBe(ALCHEMY_MAINNET_A); + expect(Object.keys(providers[0] ?? {})).not.toContain("endpoint"); + expect(JSON.stringify(providers)).not.toContain("alchemy-key-one"); + expect(JSON.stringify(providers)).not.toContain("quicknode-key-one"); + + expectSafeFailure( + () => + createActionRpcQuorum({ + chainId: 1, + primary: `https://user:super-secret@eth.drpc.org`, + secondary: "https://ethereum-rpc.publicnode.com", + }), + "super-secret", + ); + }); + + it("rejects API-key aliases from the same provider origin", () => { + expectSafeFailure( + () => + createActionRpcQuorum({ + chainId: 1, + primary: ALCHEMY_MAINNET_A, + secondary: ALCHEMY_MAINNET_B, + }), + "alchemy-key-two", + ); + }); + + it("rejects different origins that still belong to the same vendor", () => { + expect(() => + createActionRpcQuorum({ + chainId: 11_155_111, + primary: QUICKNODE_MAINNET, + secondary: QUICKNODE_SEPOLIA_ALIAS, + }), + ).toThrow(ActionRpcQuorumError); + }); + + it("fails closed for unknown providers and wrong-network endpoints", () => { + expect(() => + createActionRpcQuorum({ + chainId: 1, + primary: "https://rpc-one.example/api-key-one", + secondary: "https://rpc-two.example/api-key-two", + }), + ).toThrow(ActionRpcQuorumError); + expect(() => + createActionRpcQuorum({ + chainId: 11_155_111, + primary: "https://ethereum-rpc.publicnode.com", + secondary: "https://rpc.sepolia.org", + }), + ).toThrow(ActionRpcQuorumError); + }); + + it("does not give an alias fallback another quorum vote", () => { + const providers = createActionRpcQuorum({ + chainId: 1, + primary: DRPC_PAID_MAINNET, + secondary: QUICKNODE_MAINNET, + fallbacks: [ + "https://eth.drpc.org", + "https://ethereum-rpc.publicnode.com", + ], + }); + + expectIndependent(providers); + expect(providers.map((provider) => provider.vendorGroup)).toEqual([ + "drpc", + "quicknode", + "publicnode", + ]); + }); +}); + +describe("action-route RPC quorums", () => { + it("requires independent providers for trade preparation", () => { + const providers = tradeActionRpcProviders(1, { + ETHEREUM_RPC_URL: ALCHEMY_MAINNET_A, + ETHEREUM_RPC_URL_B: QUICKNODE_MAINNET, + }); + expectIndependent(providers); + expect(providers).toHaveLength(2); + + expectSafeFailure( + () => + tradeActionRpcProviders(1, { + ETHEREUM_RPC_URL: ALCHEMY_MAINNET_A, + ETHEREUM_RPC_URL_B: ALCHEMY_MAINNET_B, + }), + "alchemy-key-two", + ); + }); + + it("requires independent providers for creator claims", () => { + const providers = creatorClaimRpcProviders({ + chainId: 1, + rpcUrl: DRPC_PAID_MAINNET, + rpcUrlSecondary: QUICKNODE_MAINNET, + }); + expectIndependent(providers); + expect(providers).toHaveLength(2); + + expectSafeFailure( + () => + creatorClaimRpcProviders({ + chainId: 1, + rpcUrl: ALCHEMY_MAINNET_A, + rpcUrlSecondary: ALCHEMY_MAINNET_B, + }), + "alchemy-key-two", + ); + }); + + it("requires independent providers for Classic V3 actions", () => { + const providers = classicV3ActionRpcProviders("production", { + ETHEREUM_RPC_URL: INFURA_MAINNET_A, + ETHEREUM_RPC_URL_B: QUICKNODE_MAINNET, + }); + expectIndependent(providers); + expect(providers).toHaveLength(2); + + expectSafeFailure( + () => + classicV3ActionRpcProviders("production", { + ETHEREUM_RPC_URL: INFURA_MAINNET_A, + ETHEREUM_RPC_URL_B: INFURA_MAINNET_B, + }), + "infura-key-two", + ); + }); + + it("cannot form a Stock-Paired majority from same-provider aliases", () => { + const providers = stockPairedActionRpcProviders({ + ETHEREUM_RPC_URL: ALCHEMY_MAINNET_A, + ETHEREUM_RPC_URL_B: QUICKNODE_MAINNET, + }); + expectIndependent(providers); + expect(providers.map((provider) => provider.vendorGroup)).toEqual([ + "alchemy", + "quicknode", + "publicnode", + "mevblocker", + "drpc", + ]); + + expectSafeFailure( + () => + stockPairedActionRpcProviders({ + ETHEREUM_RPC_URL: ALCHEMY_MAINNET_A, + ETHEREUM_RPC_URL_B: ALCHEMY_MAINNET_B, + }), + "alchemy-key-two", + ); + }); + + it("supports an independent Sepolia pair without accepting mainnet aliases", () => { + const providers = tradeActionRpcProviders(11_155_111, { + SEPOLIA_RPC_URL: ALCHEMY_SEPOLIA, + SEPOLIA_RPC_URL_B: "https://ethereum-sepolia-rpc.publicnode.com", + }); + expectIndependent(providers); + expect(providers).toHaveLength(2); + }); +}); diff --git a/tests/classic-v3-action-activation.test.ts b/tests/classic-v3-action-activation.test.ts new file mode 100644 index 00000000..04321f9c --- /dev/null +++ b/tests/classic-v3-action-activation.test.ts @@ -0,0 +1,287 @@ +import { NextRequest } from "next/server"; +import { getAddress, type Address, type Hex } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const mocks = vi.hoisted(() => ({ + indexedEnabled: true, + lookup: vi.fn(), + createPublicClient: vi.fn(), + runtimeHashes: { + "0x01": + "0x9cc9723456c471d90ac838c02fa4fc47ed4b7e82c85358e71deec978c48d2dc8", + "0x02": + "0x3eba781023d3146ed9b502ac5b402d39cea4c34a14f64c878cb9ea62149590f1", + "0x03": + "0x874ec76f396807bfcbbdd88cc2fd534f10201242ad0479a05fe5d2ee937616ee", + } as const, +})); + +const account = getAddress("0x1111111111111111111111111111111111111111"); +const vault = getAddress("0x2222222222222222222222222222222222222222"); +const token = getAddress("0x3333333333333333333333333333333333333333"); +const launcher = getAddress("0xc3bd04aac2fb2ba58efd7eb673e544e0b80de770"); +const hook = getAddress("0x35fe236ea82f7cf525c9719d7df8f49f94d720cc"); +const factory = getAddress("0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a"); +const poolId = `0x${"44".repeat(32)}` as Hex; +const blockHash = `0x${"55".repeat(32)}` as Hex; +const launchTransactionHash = `0x${"66".repeat(32)}` as Hex; + +const indexedToken = { + chainId: 1 as const, + releaseVersion: "classic-v3" as const, + modelVersion: "classic" as const, + tokenAddress: token, + creatorAddress: account, + launchTransactionHash, + poolId, + rewardVaultAddress: vault, + launchHash: `0x${"77".repeat(32)}` as Hex, + tokenName: "Classic Token", + tokenSymbol: "CLS", + totalSupplyRaw: "1000000000000000000000000000", + launchedAt: "2026-07-31T00:00:00.000Z", + hookAddress: hook, + quoteAssetAddress: null, + totalSwapFeeBps: 100, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + creatorFeeBps: null, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + promotedBlockNumber: "25639608", + promotedBlockHash: blockHash, + verifiedAt: "2026-07-31T00:01:00.000Z", +}; + +const actionReward = { + chainId: 1 as const, + account, + vaultAddress: vault, + poolId, + hookAddress: hook, + quoteAssetAddress: null, + claimableRaw: "110000000000000000", + claimedRaw: "0", + entitledRaw: "110000000000000000", + releaseVersion: "classic-v3" as const, + modelVersion: "classic" as const, + promotedBlockNumber: "25639608", + promotedBlockHash: blockHash, + verifiedAt: "2026-07-31T00:01:00.000Z", + token: indexedToken, +}; + +const launchLog = { + removed: false, + transactionHash: launchTransactionHash, + args: { + deployer: account, + token, + poolId, + feeHook: hook, + rewardVault: vault, + positionRecipient: account, + positionTokenId: 1n, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + rewardConfigurationHash: `0x${"88".repeat(32)}` as Hex, + launchHash: `0x${"77".repeat(32)}` as Hex, + }, +}; + +function codeAt(address: Address) { + const normalized = address.toLowerCase(); + if (normalized === launcher.toLowerCase()) return "0x01" as Hex; + if (normalized === hook.toLowerCase()) return "0x02" as Hex; + if (normalized === factory.toLowerCase()) return "0x03" as Hex; + if (normalized === vault.toLowerCase()) return "0x6004" as Hex; + return "0x" as Hex; +} + +function contractRead(functionName: string) { + switch (functionName) { + case "shareBpsOf": + case "shareBpsAt": + return 10_000n; + case "claimable": + return 10n ** 17n; + case "claimedBy": + return 0n; + case "name": + return "Classic Token"; + case "symbol": + return "CLS"; + case "beneficiaryCount": + return 1n; + case "isFactoryVault": + return true; + case "feeDisclosure": + return [100, 100, 90, 90, 10, 0, 0, vault]; + case "poolFeeConfig": + return [vault, launcher, 100, 100, true, 10n ** 16n]; + case "feeHook": + return hook; + case "poolId": + return poolId; + case "beneficiaryAt": + return account; + default: + throw new Error(`Unexpected function ${functionName}`); + } +} + +function identityClient() { + const getLogs = vi + .fn() + .mockResolvedValueOnce([launchLog]) + .mockResolvedValue([]); + return { + getCode: vi.fn(({ address }: { address: Address }) => + Promise.resolve(codeAt(address)), + ), + getBlockNumber: vi.fn().mockResolvedValue(25_700_000n), + getLogs, + readContract: vi.fn( + ({ functionName }: { functionName: string }) => + Promise.resolve(contractRead(functionName)), + ), + call: vi.fn(), + }; +} + +function actionClient(index: number) { + return { + getCode: vi.fn(({ address }: { address: Address }) => + Promise.resolve(codeAt(address)), + ), + getBlockNumber: vi.fn().mockResolvedValue(25_700_000n), + getBlock: vi.fn().mockResolvedValue({ hash: blockHash }), + readContract: vi.fn( + ({ functionName }: { functionName: string }) => + Promise.resolve(contractRead(functionName)), + ), + call: vi.fn().mockResolvedValue({ data: "0x" }), + estimateGas: vi.fn().mockResolvedValue(100_000n + BigInt(index)), + getGasPrice: vi.fn().mockResolvedValue(2_000_000_000n + BigInt(index)), + getBalance: vi.fn().mockResolvedValue(10n ** 18n), + }; +} + +vi.mock("../lib/data-pipeline/route-activation.server", () => ({ + indexedLaunchLookupEnabled: () => mocks.indexedEnabled, +})); + +vi.mock("../lib/data-pipeline/action-lookup", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + lookupActionReward: mocks.lookup, + }; +}); + +vi.mock("viem", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createPublicClient: mocks.createPublicClient, + keccak256: vi.fn((value: keyof typeof mocks.runtimeHashes) => { + const mocked = mocks.runtimeHashes[value]; + return mocked ?? actual.keccak256(value); + }), + }; +}); + +import { POST } from "../app/api/profile/classic-v3/route"; + +function request() { + return new NextRequest("http://localhost/api/profile/classic-v3", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + action: "claim", + account, + vaultAddress: vault, + chainId: 1, + }), + }); +} + +describe("Classic V3 action identity activation", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubEnv( + "ETHEREUM_RPC_URL", + "https://mainnet.infura.io/v3/infura-classic-key", + ); + vi.stubEnv( + "ETHEREUM_RPC_URL_B", + "https://classic-node.quiknode.pro/quicknode-classic-key/", + ); + mocks.lookup.mockResolvedValue(actionReward); + const legacyClient = identityClient(); + const actionClients = [actionClient(0), actionClient(1)]; + let actionIndex = 0; + let legacyReturned = false; + mocks.createPublicClient.mockImplementation(() => { + if (!mocks.indexedEnabled && !legacyReturned) { + legacyReturned = true; + return legacyClient; + } + return actionClients[actionIndex++]; + }); + }); + + it.each([true, false])( + "uses the same two-provider state checks and simulations with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + + const response = await POST(request()); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + status: "ready", + action: "claim", + account, + vaultAddress: vault, + transaction: { + kind: "claim-classic-v3-rewards", + chainId: 1, + from: account, + to: vault, + }, + }); + expect(mocks.createPublicClient).toHaveBeenCalledTimes( + indexedEnabled ? 2 : 3, + ); + expect(mocks.lookup).toHaveBeenCalledTimes(indexedEnabled ? 1 : 0); + }, + ); + + it.each([true, false])( + "fails closed when the action RPCs are same-provider aliases with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + vi.stubEnv( + "ETHEREUM_RPC_URL_B", + "https://mainnet.infura.io/v3/second-classic-secret", + ); + + const response = await POST(request()); + const serialized = JSON.stringify(await response.json()); + + expect(response.status).toBe(502); + expect(mocks.createPublicClient).toHaveBeenCalledTimes( + indexedEnabled ? 0 : 1, + ); + expect(serialized).not.toContain("infura-classic-key"); + expect(serialized).not.toContain("second-classic-secret"); + }, + ); +}); diff --git a/tests/classic-v3-profile-route.test.ts b/tests/classic-v3-profile-route.test.ts index 304fb536..09d20ac0 100644 --- a/tests/classic-v3-profile-route.test.ts +++ b/tests/classic-v3-profile-route.test.ts @@ -1,6 +1,8 @@ import { NextRequest } from "next/server"; import { describe, expect, it, vi } from "vitest"; +vi.mock("server-only", () => ({})); + const mocks = vi.hoisted(() => { const runtimeCodes = { "0x01": @@ -28,6 +30,19 @@ const mocks = vi.hoisted(() => { return { client, runtimeCodes }; }); +vi.mock("server-only", () => ({})); + +vi.mock("../lib/data-pipeline/action-lookup", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + lookupActionReward: vi.fn(async () => { + throw new actual.ActionLookupError("not-found"); + }), + }; +}); + vi.mock("viem", async (importOriginal) => { const actual = await importOriginal(); return { diff --git a/tests/creator-claim-action-activation.test.ts b/tests/creator-claim-action-activation.test.ts new file mode 100644 index 00000000..ebe37e71 --- /dev/null +++ b/tests/creator-claim-action-activation.test.ts @@ -0,0 +1,246 @@ +import { NextRequest } from "next/server"; +import { + getAddress, + keccak256, + type Address, + type Hex, +} from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { computeOfficialV4PoolId } from "../lib/uniswap/liquidity-launcher-sdk"; + +vi.mock("server-only", () => ({})); + +const mocks = vi.hoisted(() => ({ + indexedEnabled: true, + lookup: vi.fn(), + readLegacy: vi.fn(), + createPublicClient: vi.fn(), +})); + +const creator = getAddress("0x1111111111111111111111111111111111111111"); +const token = getAddress("0x2222222222222222222222222222222222222222"); +const hook = getAddress("0x3333333333333333333333333333333333333333"); +const launcher = getAddress("0x4444444444444444444444444444444444444444"); +const hookCode = "0x6001600155" as Hex; +const launcherCode = "0x6002600255" as Hex; +const blockHash = `0x${"55".repeat(32)}` as Hex; +const poolId = computeOfficialV4PoolId({ + currency0: "0x0000000000000000000000000000000000000000", + currency1: token, + fee: 0, + tickSpacing: 200, + hooks: hook, +}); + +const deployment = { + environment: "production" as const, + releaseVersion: "classic-v2" as const, + chainId: 1 as const, + status: "ready" as const, + launcher, + feeHook: hook, + launcherRuntimeCodeHash: keccak256(launcherCode), + feeHookRuntimeCodeHash: keccak256(hookCode), + deploymentBlock: 1n, + stateView: getAddress("0x6666666666666666666666666666666666666666"), + stateViewRuntimeCodeHash: `0x${"77".repeat(32)}` as Hex, + rpcUrl: "https://eth-mainnet.g.alchemy.com/v2/alchemy-claim-key", + rpcUrlSecondary: + "https://claim-node.quiknode.pro/quicknode-claim-key/", + confirmations: 12n, + logBlockRange: 10_000n, +}; + +const indexedToken = { + chainId: 1 as const, + releaseVersion: "classic-v2" as const, + modelVersion: "classic" as const, + tokenAddress: token, + creatorAddress: creator, + launchTransactionHash: `0x${"88".repeat(32)}` as Hex, + poolId, + rewardVaultAddress: null, + launchHash: `0x${"99".repeat(32)}` as Hex, + tokenName: "Claim Token", + tokenSymbol: "CLM", + totalSupplyRaw: "1000000000000000000000000000", + launchedAt: "2026-07-31T00:00:00.000Z", + hookAddress: hook, + quoteAssetAddress: null, + totalSwapFeeBps: 100, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + creatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + promotedBlockNumber: "100", + promotedBlockHash: blockHash, + verifiedAt: "2026-07-31T00:01:00.000Z", +}; + +const legacyModel = { + status: "ready" as const, + tokens: [ + { + id: `1:${token}`, + name: "Claim Token", + symbol: "CLM", + tokenAddress: token, + hookAddress: hook, + poolId, + creatorAddress: creator, + launchedAt: "2026-07-31T00:00:00.000Z", + totalSwapFeeBps: 100, + buyHookFeeBps: 100, + sellHookFeeBps: 100, + creatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + launchModel: "classic" as const, + liquidityPath: "meme" as const, + }, + ], + snapshot: { + chainId: 1, + blockNumber: "100", + blockHash, + confirmations: 12, + }, + creatorClaims: [], + launcherFeesAccruedWei: "0", + launcherFeesAccruedEth: "0", +}; + +function rpcClient(estimatedGas: bigint, gasPrice: bigint, balance: bigint) { + return { + getBlockNumber: vi.fn().mockResolvedValue(120n), + getBlock: vi.fn().mockResolvedValue({ hash: blockHash }), + getCode: vi.fn(({ address }: { address: Address }) => + Promise.resolve( + address.toLowerCase() === hook.toLowerCase() + ? hookCode + : launcherCode, + ), + ), + readContract: vi.fn( + ({ functionName }: { functionName: string }) => { + if (functionName === "poolFeeConfig") { + return Promise.resolve([creator, launcher, 100, true, 10n ** 16n]); + } + if (functionName === "feeDisclosure") { + return Promise.resolve([100, 100, 90, 10, 0, 0]); + } + throw new Error(`Unexpected function ${functionName}`); + }, + ), + call: vi.fn().mockResolvedValue({ data: "0x" }), + estimateGas: vi.fn().mockResolvedValue(estimatedGas), + getGasPrice: vi.fn().mockResolvedValue(gasPrice), + getBalance: vi.fn().mockResolvedValue(balance), + }; +} + +vi.mock("../lib/data-pipeline/route-activation.server", () => ({ + indexedLaunchLookupEnabled: () => mocks.indexedEnabled, +})); + +vi.mock("../lib/data-pipeline/action-lookup", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + lookupActionTokenByPoolId: mocks.lookup, + }; +}); + +vi.mock("../lib/onchain", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getOnchainDeployment: () => deployment, + readExploreModel: mocks.readLegacy, + }; +}); + +vi.mock("viem", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createPublicClient: mocks.createPublicClient, + }; +}); + +import { POST } from "../app/api/explore/profile/claim/route"; + +function request() { + return new NextRequest("http://localhost/api/explore/profile/claim", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ account: creator, poolId, chainId: 1 }), + }); +} + +describe("creator claim action identity activation", () => { + beforeEach(() => { + vi.clearAllMocks(); + deployment.rpcUrl = + "https://eth-mainnet.g.alchemy.com/v2/alchemy-claim-key"; + deployment.rpcUrlSecondary = + "https://claim-node.quiknode.pro/quicknode-claim-key/"; + mocks.lookup.mockResolvedValue(indexedToken); + mocks.readLegacy.mockResolvedValue(legacyModel); + const clients = [ + rpcClient(100_000n, 2_000_000_000n, 10n ** 18n), + rpcClient(110_000n, 3_000_000_000n, 9n * 10n ** 17n), + ]; + mocks.createPublicClient + .mockImplementationOnce(() => clients[0]) + .mockImplementationOnce(() => clients[1]); + }); + + it.each([true, false])( + "keeps two-provider runtime checks and simulations with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + + const response = await POST(request()); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload).toMatchObject({ + status: "ready", + claim: { account: creator, tokenAddress: token, poolId }, + gas: { + estimatedGas: "110000", + gasPriceWei: "3000000000", + accountBalanceWei: "900000000000000000", + }, + }); + expect(mocks.createPublicClient).toHaveBeenCalledTimes(2); + expect(mocks.lookup).toHaveBeenCalledTimes(indexedEnabled ? 1 : 0); + expect(mocks.readLegacy).toHaveBeenCalledTimes(indexedEnabled ? 0 : 1); + }, + ); + + it.each([true, false])( + "fails closed before simulation for same-provider aliases with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + deployment.rpcUrlSecondary = + "https://eth-mainnet.g.alchemy.com/v2/second-claim-secret"; + + const response = await POST(request()); + const serialized = JSON.stringify(await response.json()); + + expect(response.status).toBe(502); + expect(mocks.createPublicClient).not.toHaveBeenCalled(); + expect(serialized).not.toContain("alchemy-claim-key"); + expect(serialized).not.toContain("second-claim-secret"); + }, + ); +}); diff --git a/tests/data-pipeline-release-binding.test.ts b/tests/data-pipeline-release-binding.test.ts new file mode 100644 index 00000000..53217d5a --- /dev/null +++ b/tests/data-pipeline-release-binding.test.ts @@ -0,0 +1,173 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + getDataPipelineReleaseBinding, + parseDataPipelineReleaseBinding, +} from "../lib/data-pipeline/release-binding.server"; + +describe("data pipeline release binding", () => { + it("loads the exact reviewed Mainnet Envio and Uniswap identities", () => { + const binding = getDataPipelineReleaseBinding(); + + expect(binding).toMatchObject({ + schemaVersion: 1, + chainId: 1, + startBlock: 25_624_130, + envio: { + deploymentLabel: "production-7f24e63", + graphqlEndpoint: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + schemaVersion: "1", + sourceCommit: "7f24e6380d5cf17092f5ade7cbad678465e3ef95", + eventCount: 51, + }, + uniswapV4Subgraph: { + subgraphId: "DiYPVdygkfjDWhbxGSqAQxwBKmfKnkWQojqeM2rkLb3G", + deployment: "QmZsgJLiLQKpb8hxTmQ5LWyrFVvfWzVaL4WK8dfFBn7EeK", + }, + }); + expect(binding.sources).toHaveLength(16); + expect(new Set(binding.sources.map(({ address }) => address)).size).toBe(16); + expect(new Set(binding.sources.map(({ contractName }) => contractName)).size).toBe(16); + expect( + binding.sources.every(({ runtimeCodeHash }) => + /^0x[0-9a-f]{64}$/.test(runtimeCodeHash), + ), + ).toBe(true); + expect(binding.releases.map(({ releaseVersion }) => releaseVersion)).toEqual([ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ]); + expect(binding.releases.map(({ activationBlock }) => activationBlock)).toEqual([ + 25_624_131, + 25_639_596, + 25_637_469, + 25_640_338, + 25_642_745, + ]); + }); + + it("rejects duplicate sources and malformed commitments", () => { + const valid = getDataPipelineReleaseBinding(); + const duplicateSource = { + ...valid, + sources: [...valid.sources, valid.sources[0]], + }; + const malformedCommitment = { + ...valid, + envio: { + ...valid.envio, + eventSetSha256: "0x1234", + }, + }; + const zeroSourceCommit = { + ...valid, + envio: { + ...valid.envio, + sourceCommit: "0".repeat(40), + }, + }; + const zeroArtifactCommitment = { + ...valid, + envio: { + ...valid.envio, + schemaSha256: `0x${"00".repeat(32)}`, + }, + }; + const zeroRuntimeCommitment = { + ...valid, + sources: valid.sources.map((source, index) => + index === 0 + ? { ...source, runtimeCodeHash: `0x${"00".repeat(32)}` } + : source, + ), + }; + const unreviewedEndpoint = { + ...valid, + envio: { + ...valid.envio, + graphqlEndpoint: "https://example.com/graphql", + }, + }; + + expect(() => parseDataPipelineReleaseBinding(duplicateSource)).toThrow( + "Invalid data pipeline release binding", + ); + expect(() => parseDataPipelineReleaseBinding(malformedCommitment)).toThrow( + "Invalid data pipeline release binding", + ); + expect(() => parseDataPipelineReleaseBinding(zeroSourceCommit)).toThrow( + "Invalid data pipeline release binding", + ); + expect(() => + parseDataPipelineReleaseBinding(zeroArtifactCommitment), + ).toThrow("Invalid data pipeline release binding"); + expect(() => parseDataPipelineReleaseBinding(zeroRuntimeCommitment)).toThrow( + "Invalid data pipeline release binding", + ); + expect(() => parseDataPipelineReleaseBinding(unreviewedEndpoint)).toThrow( + "Invalid data pipeline release binding", + ); + }); + + it("rejects orphaned, cross-model, and static-as-dynamic source bindings", () => { + const valid = getDataPipelineReleaseBinding(); + const orphaned = { + ...valid, + releases: valid.releases.map((release) => ({ + ...release, + sourceContracts: release.sourceContracts.filter( + (name) => name !== "ClassicV2Hook", + ), + })), + }; + const crossModel = { + ...valid, + releases: valid.releases.map((release) => + release.releaseVersion === "stock-paired-v1" + ? { + ...release, + sourceContracts: [ + ...release.sourceContracts, + "ClassicV2Hook", + ], + } + : release, + ), + }; + const staticAsDynamic = { + ...valid, + releases: valid.releases.map((release) => + release.releaseVersion === "classic-v2" + ? { ...release, dynamicContracts: ["ClassicV2Hook"] } + : release, + ), + }; + const invalidModelReleaseTuple = { + ...valid, + releases: valid.releases.map((release) => + release.releaseVersion === "classic-v2" + ? { ...release, model: "stock-paired" } + : release, + ), + }; + + expect(() => parseDataPipelineReleaseBinding(orphaned)).toThrow( + "Invalid data pipeline release binding", + ); + expect(() => parseDataPipelineReleaseBinding(crossModel)).toThrow( + "Invalid data pipeline release binding", + ); + expect(() => parseDataPipelineReleaseBinding(staticAsDynamic)).toThrow( + "Invalid data pipeline release binding", + ); + expect(() => + parseDataPipelineReleaseBinding(invalidModelReleaseTuple), + ).toThrow("Invalid data pipeline release binding"); + }); +}); diff --git a/tests/data-pipeline/action-lookup.test.ts b/tests/data-pipeline/action-lookup.test.ts new file mode 100644 index 00000000..697ac0a3 --- /dev/null +++ b/tests/data-pipeline/action-lookup.test.ts @@ -0,0 +1,258 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + actionTokenAsExploreModel, + queryActionReward, + queryActionTokenByAddress, + queryActionTokenByPoolId, +} from "../../lib/data-pipeline/action-lookup"; +import type { + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; + +const TOKEN = "0x1111111111111111111111111111111111111111"; +const CREATOR = "0x2222222222222222222222222222222222222222"; +const QUOTE = "0x3333333333333333333333333333333333333333"; +const HOOK = "0x4444444444444444444444444444444444444444"; +const VAULT = "0x5555555555555555555555555555555555555555"; +const POOL_ID = `0x${"66".repeat(32)}` as `0x${string}`; +const LAUNCH_HASH = `0x${"77".repeat(32)}`; +const TRANSACTION_HASH = `0x${"88".repeat(32)}`; +const BLOCK_HASH = `0x${"99".repeat(32)}`; + +function bytes(hex: string) { + return Uint8Array.from( + hex + .slice(2) + .match(/.{2}/gu)! + .map((part) => Number.parseInt(part, 16)), + ); +} + +function tokenRow(overrides: Record = {}) { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + token: bytes(TOKEN), + creator: bytes(CREATOR), + launch_transaction_hash: bytes(TRANSACTION_HASH), + pool_id: bytes(POOL_ID), + reward_vault: bytes(VAULT), + launch_hash: bytes(LAUNCH_HASH), + token_name: "Programmable Test", + token_symbol: "TEST", + total_supply: "1000000000000000000000000000", + launch_block_timestamp: "2026-07-31T08:00:00.000Z", + hook: bytes(HOOK), + quote_asset: bytes(QUOTE), + total_swap_fee_bps: 100, + buy_swap_fee_bps: 100, + sell_swap_fee_bps: 100, + buy_creator_fee_bps: 90, + sell_creator_fee_bps: 90, + creator_fee_bps: 90, + launcher_fee_bps: 10, + transfer_tax_bps: 0, + lp_fee_pips: 10_000, + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + ...overrides, + }; +} + +function rewardRow(overrides: Record = {}) { + return { + chain_id: "1", + account: bytes(CREATOR), + release_id: "classic-v3", + model_id: "classic", + vault: bytes(VAULT), + pool_id: bytes(POOL_ID), + hook: bytes(HOOK), + quote_asset: bytes(QUOTE), + entitled: "1000", + claimable_accrued: "900", + claimed_total: "100", + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + ...overrides, + }; +} + +type RecordedQuery = { + text: string; + values: readonly PostgresParameter[]; +}; + +function transaction( + responder: ( + text: string, + values: readonly PostgresParameter[], + ) => readonly Record[], +) { + const queries: RecordedQuery[] = []; + const value: PostgresTransaction = { + async query>( + text: string, + values: readonly PostgresParameter[] = [], + ) { + queries.push({ text, values }); + return responder(text, values) as readonly Row[]; + }, + }; + return { transaction: value, queries }; +} + +describe("transaction action lookup", () => { + it("discovers one exact launch through the eligible database view", async () => { + const fake = transaction(() => [tokenRow()]); + + const launch = await queryActionTokenByAddress(fake.transaction, { + chainId: 1, + token: TOKEN, + }); + + expect(launch).toMatchObject({ + chainId: 1, + releaseVersion: "classic-v3", + modelVersion: "classic", + tokenAddress: TOKEN, + creatorAddress: CREATOR, + poolId: POOL_ID, + rewardVaultAddress: VAULT, + hookAddress: HOOK, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + launcherFeeBps: 10, + }); + expect(fake.queries).toHaveLength(1); + expect(fake.queries[0]!.text).toContain( + "programmable_private.launch_by_token_v1", + ); + expect(fake.queries[0]!.text).toContain( + "where chain_id = $1 and token = $2", + ); + expect(fake.queries[0]!.text).not.toContain(TOKEN); + expect(fake.queries[0]!.values[0]).toBe(1); + expect(fake.queries[0]!.values[1]).toBeInstanceOf(Uint8Array); + }); + + it("fails closed when a pool identity is ambiguous", async () => { + const fake = transaction(() => [tokenRow(), tokenRow()]); + + await expect( + queryActionTokenByPoolId(fake.transaction, { + chainId: 1, + poolId: POOL_ID, + }), + ).rejects.toMatchObject({ code: "ambiguous" }); + }); + + it("binds a reward row to the exact launch identity", async () => { + const fake = transaction((text) => + text.includes("get_account_reward_summary_v1") + ? [rewardRow()] + : [tokenRow()], + ); + + const reward = await queryActionReward(fake.transaction, { + chainId: 1, + account: CREATOR, + vaultAddress: VAULT, + }); + + expect(reward).toMatchObject({ + account: CREATOR, + vaultAddress: VAULT, + poolId: POOL_ID, + claimableRaw: "900", + claimedRaw: "100", + entitledRaw: "1000", + token: { tokenAddress: TOKEN, rewardVaultAddress: VAULT }, + }); + expect(fake.queries).toHaveLength(2); + expect(fake.queries[0]!.values).toHaveLength(3); + }); + + it("rejects a reward whose indexed hook disagrees with its launch", async () => { + const fake = transaction((text) => + text.includes("get_account_reward_summary_v1") + ? [rewardRow({ hook: bytes("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") })] + : [tokenRow()], + ); + + await expect( + queryActionReward(fake.transaction, { + chainId: 1, + account: CREATOR, + vaultAddress: VAULT, + }), + ).rejects.toMatchObject({ + code: "scope-mismatch", + }); + }); + + it("rejects internally inconsistent indexed reward balances", async () => { + const fake = transaction((text) => + text.includes("get_account_reward_summary_v1") + ? [rewardRow({ entitled: "999" })] + : [tokenRow()], + ); + + await expect( + queryActionReward(fake.transaction, { + chainId: 1, + account: CREATOR, + vaultAddress: VAULT, + }), + ).rejects.toMatchObject({ + code: "projection-incomplete", + }); + }); + + it("does not project unsupported releases into an action model", async () => { + const fake = transaction(() => [ + tokenRow({ release_id: "deep-v3", model_id: "deep" }), + ]); + + await expect( + queryActionTokenByAddress(fake.transaction, { + chainId: 1, + token: TOKEN, + }), + ).rejects.toMatchObject({ + code: "unsupported-release", + }); + }); + + it("adapts only the verified row needed by the existing trade verifier", async () => { + const fake = transaction(() => [tokenRow()]); + const launch = await queryActionTokenByAddress(fake.transaction, { + chainId: 1, + token: TOKEN, + }); + + expect(actionTokenAsExploreModel(launch)).toMatchObject({ + status: "ready", + tokens: [ + { + tokenAddress: TOKEN, + poolId: POOL_ID, + launchModel: "classic", + launchModelVersion: "classic-v3", + }, + ], + snapshot: { + chainId: 1, + blockNumber: "25650000", + blockHash: BLOCK_HASH, + }, + }); + }); +}); diff --git a/tests/data-pipeline/candidate-projector-runtime-binding.test.ts b/tests/data-pipeline/candidate-projector-runtime-binding.test.ts new file mode 100644 index 00000000..dcb45ab9 --- /dev/null +++ b/tests/data-pipeline/candidate-projector-runtime-binding.test.ts @@ -0,0 +1,367 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + assertCandidateDatabaseBootstrapState, + assertCandidateDatabasePromotedState, + loadCandidateProjectorRuntimeBinding, + selectProjectorRuntimeBinding, +} from "../../lib/data-pipeline/candidate-projector-runtime-binding.server"; +import { createEnvioClient } from "../../lib/data-pipeline/envio"; +import { getDataPipelineReleaseBinding } from "../../lib/data-pipeline/release-binding.server"; + +const PUBLIC_FLAGS = [ + "INDEXED_EXPLORE_LIST_READS_ENABLED", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED", + "INDEXED_EXPLORE_CHART_READS_ENABLED", + "INDEXED_CREATOR_PROFILE_READS_ENABLED", + "INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED", + "INDEXED_LAUNCH_LOOKUP_ENABLED", + "INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", + "INDEXED_READ_SHADOW_COMPARE_ENABLED", +] as const; + +function candidateEnvironment( + overrides: Record = {}, +) { + return { + PROGRAMMABLE_PROJECTOR_BINDING_MODE: "candidate-backfill", + PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT: + "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY: + "envio:production-7f24e63", + ...Object.fromEntries(PUBLIC_FLAGS.map((name) => [name, "false"])), + ...overrides, + }; +} + +function promotedReleaseEnvironment( + overrides: Record = {}, +) { + return candidateEnvironment({ + PROGRAMMABLE_PROJECTOR_BINDING_MODE: "release", + VERCEL_GIT_COMMIT_SHA: "a".repeat(40), + VERCEL_DEPLOYMENT_ID: "dpl_12345678901234567890", + ...overrides, + }); +} + +function canonicalCandidateBinding() { + return loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment(), + activeProductionBinding: getDataPipelineReleaseBinding(), + }).releaseBinding; +} + +function legacyReleaseBinding() { + const canonical = getDataPipelineReleaseBinding(); + return { + ...canonical, + envio: { + deploymentLabel: "production-1e7c381", + graphqlEndpoint: + "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + schemaVersion: "1" as const, + sourceCommit: "1e7c38125714e2f485f8be0c665b12e7d7fb1809", + configSha256: + "0x378e3a799c762cb31107792c7123f5f90b54b5826884c398995e7465176fe1c2" as const, + schemaSha256: + "0x3217def060af2d1053ec3bca854187ff547fb43d91b113bc87a9f3285489362d" as const, + handlerSha256: + "0x241e18c3eda104b96eec4142826459c41c39cbce0474322634b5ea161d2fdf3e" as const, + sourceRegistrySha256: + "0x552e941d2ad7fea1184bf1efb97f840bdce9835c647b76f753f1326c6afe211f" as const, + eventSetSha256: + "0x7481d6fa986d706e46b9834e40574dd84f21be80b041d35e7d47dbfa59d69243" as const, + eventCount: 51, + }, + }; +} + +describe("candidate projector runtime binding", () => { + it("binds only the exact audited candidate and defines an explicit promotion transition", () => { + const binding = loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment(), + activeProductionBinding: getDataPipelineReleaseBinding(), + }); + + expect(binding).toMatchObject({ + mode: "candidate-backfill", + mirrorCommit: "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + releaseBinding: { + envio: { + deploymentLabel: "production-7f24e63", + graphqlEndpoint: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + }, + }, + databaseBootstrap: { + mode: "candidate-only", + providerDeploymentId: "d08b62a6-74fb-5e0a-a698-dc6877150db4", + }, + promotionTransition: { + requiredRuntimeMode: "release", + requiredCanonicalEndpoint: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + requiredCanonicalIdentity: "envio:production-7f24e63", + requiresDatabasePromotionAttestation: true, + }, + }); + }); + + it("leaves the canonical candidate release binding unchanged", () => { + const production = getDataPipelineReleaseBinding(); + const before = structuredClone(production); + loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment(), + activeProductionBinding: production, + }); + + expect(production).toEqual(before); + expect(production.envio.deploymentLabel).toBe("production-7f24e63"); + }); + + it("selects legacy, candidate-backfill, and promoted-release bindings without mutable globals", () => { + const legacy = legacyReleaseBinding(); + const candidate = canonicalCandidateBinding(); + + expect(selectProjectorRuntimeBinding({ + env: {}, + canonicalBinding: legacy, + })).toMatchObject({ + mode: "release", + releaseBinding: { envio: { deploymentLabel: "production-1e7c381" } }, + candidate: null, + promotedDatabase: null, + }); + expect(selectProjectorRuntimeBinding({ + env: candidateEnvironment(), + canonicalBinding: candidate, + })).toMatchObject({ + mode: "candidate-backfill", + releaseBinding: { envio: { deploymentLabel: "production-7f24e63" } }, + promotedDatabase: null, + }); + expect(selectProjectorRuntimeBinding({ + env: promotedReleaseEnvironment(), + canonicalBinding: candidate, + })).toMatchObject({ + mode: "release", + releaseBinding: { envio: { deploymentLabel: "production-7f24e63" } }, + candidate: null, + promotedDatabase: { + providerDeploymentId: "d08b62a6-74fb-5e0a-a698-dc6877150db4", + productCommit: "a".repeat(40), + stagedDeploymentId: "dpl_12345678901234567890", + }, + }); + }); + + it.each([ + ["VERCEL_GIT_COMMIT_SHA", undefined], + ["VERCEL_GIT_COMMIT_SHA", "0".repeat(40)], + ["VERCEL_GIT_COMMIT_SHA", "A".repeat(40)], + ["VERCEL_DEPLOYMENT_ID", undefined], + ["VERCEL_DEPLOYMENT_ID", "production"], + ])("rejects missing or mismatched promoted-release evidence in %s", (name, value) => { + expect(() => selectProjectorRuntimeBinding({ + env: promotedReleaseEnvironment({ [name]: value }), + canonicalBinding: canonicalCandidateBinding(), + })).toThrow(); + }); + + it.each(PUBLIC_FLAGS)("fails closed when %s is not exactly false", (name) => { + expect(() => + loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment({ [name]: "true" }), + activeProductionBinding: getDataPipelineReleaseBinding(), + }), + ).toThrow(); + }); + + it("accepts the exact unpromoted database verification result", async () => { + const binding = loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment(), + activeProductionBinding: getDataPipelineReleaseBinding(), + }); + const query = vi.fn(async (text: string) => { + if (text.includes("current_role::text")) { + return [{ + session_user: "programmable_projector_login", + current_role: "programmable_projector", + }]; + } + if (text === "select session_user::text as session_user") { + return [{ session_user: "programmable_projector_login" }]; + } + if (text.includes("verify_candidate_database_unpromoted_v1")) { + return [{ verified: true }]; + } + return []; + }); + const executor = { + transaction: vi.fn(async (work) => work({ query })), + close: vi.fn(), + } as never; + + await expect( + assertCandidateDatabaseBootstrapState({ executor, binding }), + ).resolves.toBeUndefined(); + expect(query).toHaveBeenCalledWith( + expect.stringContaining("verify_candidate_database_unpromoted_v1"), + expect.arrayContaining([ + "d08b62a6-74fb-5e0a-a698-dc6877150db4", + "2026-08-01T09:00:00.000Z", + ]), + ); + }); + + it("rejects provider identity drift and a promoted or missing candidate database", async () => { + expect(() => + loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment({ + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + }), + activeProductionBinding: getDataPipelineReleaseBinding(), + }), + ).toThrow(); + + const binding = loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment(), + activeProductionBinding: getDataPipelineReleaseBinding(), + }); + const query = vi.fn(async (text: string) => { + if (text.includes("current_role::text")) { + return [{ + session_user: "programmable_projector_login", + current_role: "programmable_projector", + }]; + } + if (text === "select session_user::text as session_user") { + return [{ session_user: "programmable_projector_login" }]; + } + if (text.includes("verify_candidate_database_unpromoted_v1")) { + throw Object.assign(new Error("candidate database promoted"), { + code: "55000", + }); + } + return []; + }); + const executor = { + transaction: vi.fn(async (work) => work({ query })), + close: vi.fn(), + } as never; + + await expect( + assertCandidateDatabaseBootstrapState({ executor, binding }), + ).rejects.toThrow(); + }); + + it("accepts only the exact promoted database verification result", async () => { + const selection = selectProjectorRuntimeBinding({ + env: promotedReleaseEnvironment(), + canonicalBinding: canonicalCandidateBinding(), + }); + if (!selection.promotedDatabase) throw new Error("missing promotion proof"); + const query = vi.fn(async (text: string) => { + if (text.includes("current_role::text")) { + return [{ + session_user: "programmable_projector_login", + current_role: "programmable_projector", + }]; + } + if (text === "select session_user::text as session_user") { + return [{ session_user: "programmable_projector_login" }]; + } + if (text.includes("verify_candidate_database_promoted_v2")) { + return [{ verified: true }]; + } + return []; + }); + const executor = { + transaction: vi.fn(async (work) => work({ query })), + close: vi.fn(), + } as never; + + await expect(assertCandidateDatabasePromotedState({ + executor, + binding: selection.promotedDatabase, + })).resolves.toBeUndefined(); + expect(query).toHaveBeenCalledWith( + expect.stringContaining("verify_candidate_database_promoted_v2"), + expect.arrayContaining([ + "d08b62a6-74fb-5e0a-a698-dc6877150db4", + "2026-08-01T09:00:00.000Z", + "a".repeat(40), + "dpl_12345678901234567890", + ]), + ); + }); + + it("validates real Envio progress against the selected candidate identity", async () => { + const releaseBinding = canonicalCandidateBinding(); + const blockHash = `0x${"11".repeat(32)}`; + const transactionHash = `0x${"22".repeat(32)}`; + const occurrenceId = `1:${blockHash}:${transactionHash}:7`; + const response = { + data: { + _meta: [{ + chainId: 1, + progressBlock: 25_650_010, + bufferBlock: 25_650_010, + sourceBlock: 25_650_022, + isReady: true, + eventsProcessed: 51_234, + }], + IndexerState_by_pk: { + id: "ethereum-mainnet", + schemaVersion: "1", + deployment: releaseBinding.envio.deploymentLabel, + sourceCommit: releaseBinding.envio.sourceCommit, + configSha256: releaseBinding.envio.configSha256, + schemaSha256: releaseBinding.envio.schemaSha256, + handlerSha256: releaseBinding.envio.handlerSha256, + sourceRegistrySha256: releaseBinding.envio.sourceRegistrySha256, + eventSetSha256: releaseBinding.envio.eventSetSha256, + eventCount: releaseBinding.envio.eventCount, + chainId: 1, + progressBlock: "25650000", + progressBlockHash: blockHash, + progressTimestamp: "1785480000", + progressTransactionHash: transactionHash, + progressOccurrenceId: occurrenceId, + }, + }, + }; + const fetcher = vi.fn(async () => new Response(JSON.stringify(response), { + status: 200, + headers: { "content-type": "application/json" }, + })); + const candidateClient = createEnvioClient({ + endpoint: releaseBinding.envio.graphqlEndpoint, + releaseBinding, + fetcher, + }); + + await expect(candidateClient.readProgress({ + requiredBlock: "25650002", + })).resolves.toMatchObject({ + deployment: "production-7f24e63", + progressBlock: "25650010", + isReady: true, + }); + + const legacyClient = createEnvioClient({ + endpoint: legacyReleaseBinding().envio.graphqlEndpoint, + releaseBinding: legacyReleaseBinding(), + fetcher, + }); + await expect(legacyClient.readProgress({ + requiredBlock: "25650002", + })).rejects.toMatchObject({ code: "validation_failed" }); + }); +}); diff --git a/tests/data-pipeline/canonical-fingerprint.test.ts b/tests/data-pipeline/canonical-fingerprint.test.ts new file mode 100644 index 00000000..b3931e39 --- /dev/null +++ b/tests/data-pipeline/canonical-fingerprint.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { keccak256, type Hex } from "viem"; + +import fixtureJson from "../../config/data-pipeline-canonical-fingerprint.v1.json"; +import databaseFixtureJson from "../../supabase/tests/codec/canonical-fingerprint-v1.json"; +import { + canonicalFingerprintBytesToHex, + canonicalFingerprintPreimageV1, + canonicalFingerprintV1, + canonicalizeFingerprintJson, + decodeCanonicalFingerprintHex, + type AllocationFingerprintInput, + type EvidenceFingerprintInput, + type OccurrenceFingerprintInput, +} from "../../lib/data-pipeline/canonical-fingerprint"; + +type FixtureVector = { + name: string; + domain: "occurrence" | "allocation" | "evidence"; + input: unknown; + expected_preimage_hex: Hex; + expected_keccak256: Hex; +}; + +const fixture = fixtureJson as unknown as { + sentinel_vectors: Array<{ + name: string; + expected_preimage_hex: Hex; + expected_keccak256: Hex; + }>; + vectors: FixtureVector[]; +}; + +function encodeVector(vector: FixtureVector) { + if (vector.domain === "occurrence") { + const input = vector.input as OccurrenceFingerprintInput; + return { + preimage: canonicalFingerprintPreimageV1("occurrence", input), + fingerprint: canonicalFingerprintV1("occurrence", input), + }; + } + if (vector.domain === "allocation") { + const input = vector.input as AllocationFingerprintInput; + return { + preimage: canonicalFingerprintPreimageV1("allocation", input), + fingerprint: canonicalFingerprintV1("allocation", input), + }; + } + const input = vector.input as EvidenceFingerprintInput; + return { + preimage: canonicalFingerprintPreimageV1("evidence", input), + fingerprint: canonicalFingerprintV1("evidence", input), + }; +} + +describe("production canonical fingerprint v1", () => { + it("keeps the application and database codec fixtures identical", () => { + expect(fixtureJson).toEqual(databaseFixtureJson); + }); + + it("matches every independent canonical fixture preimage and digest", () => { + expect(fixture.vectors).toHaveLength(7); + + for (const vector of fixture.vectors) { + const encoded = encodeVector(vector); + expect( + canonicalFingerprintBytesToHex(encoded.preimage), + `${vector.name} preimage`, + ).toBe(vector.expected_preimage_hex); + expect(encoded.fingerprint, `${vector.name} fingerprint`).toBe( + vector.expected_keccak256, + ); + } + }); + + it("retains the sentinel distinctions for nulls, ordering and indexed-only data", () => { + for (const sentinel of fixture.sentinel_vectors) { + expect( + keccak256(sentinel.expected_preimage_hex), + sentinel.name, + ).toBe(sentinel.expected_keccak256); + } + + const orderAb = fixture.vectors.find( + ({ name }) => name === "allocation_order_ab_v1", + ); + const orderBa = fixture.vectors.find( + ({ name }) => name === "allocation_order_ba_v1", + ); + const nullOptional = fixture.vectors.find( + ({ name }) => name === "evidence_null_optional_v1", + ); + const presentEmpty = fixture.vectors.find( + ({ name }) => name === "evidence_present_empty_v1", + ); + + expect(orderAb?.expected_keccak256).not.toBe(orderBa?.expected_keccak256); + expect(nullOptional?.expected_keccak256).not.toBe( + presentEmpty?.expected_keccak256, + ); + }); + + it("rejects ambiguous encodings before a fingerprint is produced", () => { + expect(() => decodeCanonicalFingerprintHex("00", 1)).toThrow(); + expect(() => decodeCanonicalFingerprintHex("0x0")).toThrow(); + expect(() => decodeCanonicalFingerprintHex("0x0g")).toThrow(); + expect(() => + decodeCanonicalFingerprintHex(`0x${"11".repeat(19)}`, 20), + ).toThrow(); + expect(() => canonicalizeFingerprintJson(Number.NaN)).toThrow(); + expect(() => canonicalizeFingerprintJson(1.5)).toThrow(); + expect(() => canonicalizeFingerprintJson("\ud800")).toThrow(); + }); + + it("normalizes mixed-case hex without sorting caller-defined arrays", () => { + const occurrence = fixture.vectors.find( + ({ name }) => name === "occurrence_all_fields_v1", + ); + expect(occurrence).toBeDefined(); + const input = structuredClone( + occurrence!.input, + ) as OccurrenceFingerprintInput; + input.source_address = input.source_address.toUpperCase().replace("0X", "0x"); + const normalized = canonicalFingerprintBytesToHex( + canonicalFingerprintPreimageV1("occurrence", input), + ); + expect(normalized).toBe(occurrence!.expected_preimage_hex); + + input.ordered_topics.reverse(); + expect( + canonicalFingerprintBytesToHex( + canonicalFingerprintPreimageV1("occurrence", input), + ), + ).not.toBe(occurrence!.expected_preimage_hex); + }); +}); diff --git a/tests/data-pipeline/classic-v2-reconciler-route-builder.test.ts b/tests/data-pipeline/classic-v2-reconciler-route-builder.test.ts new file mode 100644 index 00000000..1ae3d213 --- /dev/null +++ b/tests/data-pipeline/classic-v2-reconciler-route-builder.test.ts @@ -0,0 +1,752 @@ +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + encodeEventTopics, + encodeFunctionData, + encodeFunctionResult, + getAddress, + parseAbi, + parseAbiItem, + type Abi, + type AbiEvent, + type Address, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +import deployment from "../../contracts/deployments/mainnet-classic-v2.json"; +import dependencies from "../../contracts/dependencies/ethereum-mainnet.json"; +import { + assertClassicV2ReconcilerLaunchCount, + buildClassicV2ExactBlockContribution, + CLASSIC_V2_RECONCILER_LOG_BLOCK_RANGE, + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + classicV2ReconcilerBlockRanges, +} from "../../lib/data-pipeline/classic-v2-reconciler-route-builder.server"; +import type { + ExactBlockRpcClient, + ExactBlockRpcLog, + ExactBlockRpcReceipt, + ExactBlockRpcTransaction, +} from "../../lib/data-pipeline/reconciler-exact-block-reader.server"; +import type { ReconcilerPreParityContract } from "../../lib/data-pipeline/reconciler-preparity"; +import { + creatorFeeHookReadAbi, + stateViewReadAbi, + uerc20ReadAbi, +} from "../../lib/onchain/abis"; + +const ZERO_ADDRESS = `0x${"00".repeat(20)}` as Address; +const TOKEN = getAddress(`0x${"11".repeat(20)}`); +const CREATOR = getAddress(`0x${"22".repeat(20)}`); +const POSITION_RECIPIENT = getAddress(`0x${"33".repeat(20)}`); +const SWAP_SENDER = getAddress(`0x${"44".repeat(20)}`); +const POOL_ID = `0x${"55".repeat(32)}` as const; +const LAUNCH_HASH = `0x${"66".repeat(32)}` as const; +const CREATOR_SALT = `0x${"77".repeat(32)}` as const; +const EFFECTIVE_GRAFFITI = `0x${"88".repeat(32)}` as const; +const LAUNCH_TRANSACTION = `0x${"99".repeat(32)}` as const; +const LAUNCH_BLOCK_HASH = `0x${"aa".repeat(32)}` as const; +const SWAP_TRANSACTION = `0x${"bb".repeat(32)}` as const; +const TINY_SWAP_TRANSACTION = `0x${"bc".repeat(32)}` as const; +const SECOND_SWAP_TRANSACTION = `0x${"bd".repeat(32)}` as const; +const CLAIM_TRANSACTION = `0x${"be".repeat(32)}` as const; +const SWAP_BLOCK_HASH = `0x${"cc".repeat(32)}` as const; +const CLAIM_BLOCK_HASH = `0x${"cd".repeat(32)}` as const; +const CHECKPOINT_HASH = `0x${"dd".repeat(32)}` as const; +const TOTAL_SUPPLY = 1_000_000_000n * 10n ** 18n; +const TOKEN_LIQUIDITY = TOTAL_SUPPLY - 1n; +const INITIAL_BUY_WEI = 1_000_000_000_000_000n; +const SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336n; + +const launcherAbi = parseAbi([ + "function launch((string name,string symbol,uint16 totalSwapFeeBps,bytes32 creatorSalt,(string description,string website,string image,bytes extraData) metadata) parameters) payable", + "function predictTokenAddress(string name,string symbol,address creator,bytes32 creatorSalt) view returns (address token,bytes32 effectiveGraffiti)", + "function predictPositionRecipient(address token,address creator) view returns (address)", + "function poolKey(address token) view returns (address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks)", + "function launchHashOf(address token) view returns (bytes32)", + "function poolManager() view returns (address)", + "function feeHook() view returns (address)", + "function MIN_INITIAL_BUY_WEI() view returns (uint256)", +]); +const hookInfrastructureAbi = parseAbi([ + "function poolManager() view returns (address)", + "function launcherFeeRecipient() view returns (address)", + "function TICK_SPACING() view returns (int24)", +]); + +const launchedEvent = parseAbiItem( + "event MemeTokenLaunched(address indexed creator,address indexed token,bytes32 indexed poolId,address feeHook,address positionRecipient,uint256 positionTokenId,uint16 totalSwapFeeBps,bytes32 launchHash)", +); +const liquidityEvent = parseAbiItem( + "event MemeLiquidityConfigured(address indexed token,uint256 totalSupply,uint256 tokenLiquidityAmount,uint256 lockedTokenDust,int24 initialTick,int24 tickLower,int24 tickUpper,uint24 lpFeePips,bytes32 launchHash)", +); +const initialBuyEvent = parseAbiItem( + "event MemeCreatorInitialBuy(address indexed creator,address indexed token,bytes32 indexed poolId,uint256 nativeAmount,uint256 tokenAmount,bytes32 launchHash)", +); +const registeredEvent = parseAbiItem( + "event PoolRegistered(bytes32 indexed poolId,address indexed token,address indexed creator,address registrar,uint16 totalSwapFeeBps)", +); +const disclosureEvent = parseAbiItem( + "event PoolFeeDisclosure(bytes32 indexed poolId,address indexed token,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,uint16 launcherFeeBps,uint16 transferTaxBps,uint24 lpFeePips)", +); +const feeAccruedEvent = parseAbiItem( + "event NativeSwapFeesAccrued(bytes32 indexed poolId,address indexed swapSender,uint256 grossNativeAmount,uint256 creatorFee,uint256 launcherFee)", +); +const creatorFeesClaimedEvent = parseAbiItem( + "event CreatorFeesClaimed(bytes32 indexed poolId,address indexed creator,address indexed recipient,address caller,uint256 amount)", +); +const swapEvent = parseAbiItem( + "event Swap(bytes32 indexed id,address indexed sender,int128 amount0,int128 amount1,uint160 sqrtPriceX96,uint128 liquidity,int24 tick,uint24 fee)", +); + +type Mutation = + | "none" + | "runtime" + | "calldata" + | "receipt" + | "fee" + | "state" + | "provenance" + | "log-order" + | "tiny-swap" + | "missing-fee" + | "extra-fee" + | "duplicate-fee" + | "reordered-fees" + | "claimed-fees" + | "claimed-fees-third-party" + | "claim-accounting" + | "claim-provenance" + | "unauthorized-redirect"; + +function eventLog(input: { + event: AbiEvent; + args: Readonly>; + address: Address; + blockNumber: bigint; + blockHash: `0x${string}`; + transactionHash: `0x${string}`; + transactionIndex: number; + logIndex: number; +}): ExactBlockRpcLog { + const topics = encodeEventTopics({ + abi: [input.event], + eventName: input.event.name, + args: input.args, + } as never); + const nonIndexed = input.event.inputs.filter((item) => !item.indexed); + const values = nonIndexed.map((item) => input.args[item.name!]); + const data = encodeAbiParameters(nonIndexed, values as never); + return Object.freeze({ + address: input.address, + blockNumber: input.blockNumber, + blockHash: input.blockHash, + transactionHash: input.transactionHash, + transactionIndex: input.transactionIndex, + logIndex: input.logIndex, + topics: Object.freeze(topics as readonly Hex[]), + data, + }); +} + +function encodedResult( + abi: Abi, + functionName: string, + result: unknown, +): Hex { + return encodeFunctionResult({ abi, functionName, result } as never); +} + +function fixture(mutation: Mutation = "none") { + const launcher = getAddress(deployment.addresses.memeLauncher); + const hook = getAddress(deployment.addresses.feeHook); + const poolManager = getAddress(dependencies.contracts.poolManager.address); + const treasury = getAddress(deployment.addresses.treasury); + const startBlock = BigInt(deployment.transactions.memeLauncher.blockNumber); + const launchBlock = startBlock + 1n; + const swapBlock = startBlock + 2n; + const checkpointBlock = startBlock + 5n; + + const registrationLog = eventLog({ + event: registeredEvent, + args: { + poolId: POOL_ID, + token: TOKEN, + creator: CREATOR, + registrar: launcher, + totalSwapFeeBps: 100, + }, + address: hook, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 0, + }); + const disclosureLog = eventLog({ + event: disclosureEvent, + args: { + poolId: POOL_ID, + token: TOKEN, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + }, + address: hook, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 1, + }); + const liquidityLog = eventLog({ + event: liquidityEvent, + args: { + token: TOKEN, + totalSupply: TOTAL_SUPPLY, + tokenLiquidityAmount: TOKEN_LIQUIDITY, + lockedTokenDust: 1n, + initialTick: 204_200, + tickLower: -887_200, + tickUpper: 204_200, + lpFeePips: 0, + launchHash: LAUNCH_HASH, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 2, + }); + const initialBuyLog = eventLog({ + event: initialBuyEvent, + args: { + creator: CREATOR, + token: TOKEN, + poolId: POOL_ID, + nativeAmount: INITIAL_BUY_WEI, + tokenAmount: 10_000n, + launchHash: LAUNCH_HASH, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 3, + }); + const launchLog = eventLog({ + event: launchedEvent, + args: { + creator: CREATOR, + token: TOKEN, + poolId: POOL_ID, + feeHook: hook, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 42n, + totalSwapFeeBps: 100, + launchHash: LAUNCH_HASH, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 4, + }); + const feeLog = eventLog({ + event: feeAccruedEvent, + args: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 1_000_000n, + creatorFee: mutation === "fee" ? 8_999n : 9_000n, + launcherFee: 1_000n, + }, + address: hook, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: SWAP_TRANSACTION, + transactionIndex: 3, + logIndex: 0, + }); + const swapLog = eventLog({ + event: swapEvent, + args: { + id: POOL_ID, + sender: SWAP_SENDER, + amount0: -1_000_000n, + amount1: -1n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: 1_000_000n, + tick: 0, + fee: 0, + }, + address: poolManager, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: mutation === "provenance" + ? (`0x${"ee".repeat(32)}` as const) + : SWAP_TRANSACTION, + transactionIndex: 3, + logIndex: mutation === "reordered-fees" ? 3 : 1, + }); + const tinySwapLog = eventLog({ + event: swapEvent, + args: { + id: POOL_ID, + sender: SWAP_SENDER, + amount0: -1n, + amount1: 1n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: 1_000_000n, + tick: 0, + fee: 0, + }, + address: poolManager, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: TINY_SWAP_TRANSACTION, + transactionIndex: 2, + logIndex: 0, + }); + const secondFeeLog = eventLog({ + event: feeAccruedEvent, + args: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 2_000_000n, + creatorFee: 18_000n, + launcherFee: 2_000n, + }, + address: hook, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: mutation === "reordered-fees" + ? SWAP_TRANSACTION + : SECOND_SWAP_TRANSACTION, + transactionIndex: mutation === "reordered-fees" ? 3 : 4, + logIndex: mutation === "reordered-fees" ? 2 : 0, + }); + const secondSwapLog = eventLog({ + event: swapEvent, + args: { + id: POOL_ID, + sender: SWAP_SENDER, + amount0: -2_000_000n, + amount1: 2n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: 1_000_000n, + tick: 0, + fee: 0, + }, + address: poolManager, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: mutation === "reordered-fees" + ? SWAP_TRANSACTION + : SECOND_SWAP_TRANSACTION, + transactionIndex: mutation === "reordered-fees" ? 3 : 4, + logIndex: 1, + }); + const duplicateFeeLog = eventLog({ + event: feeAccruedEvent, + args: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + grossNativeAmount: 1_000_000n, + creatorFee: 9_000n, + launcherFee: 1_000n, + }, + address: hook, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: SWAP_TRANSACTION, + transactionIndex: 3, + logIndex: 2, + }); + const creatorClaimLog = eventLog({ + event: creatorFeesClaimedEvent, + args: { + poolId: POOL_ID, + creator: mutation === "claim-provenance" ? SWAP_SENDER : CREATOR, + recipient: mutation === "unauthorized-redirect" + ? POSITION_RECIPIENT + : CREATOR, + caller: mutation === "claimed-fees-third-party" || + mutation === "unauthorized-redirect" + ? SWAP_SENDER + : CREATOR, + amount: mutation === "claim-accounting" ? 4_999n : 5_000n, + }, + address: hook, + blockNumber: swapBlock + 1n, + blockHash: CLAIM_BLOCK_HASH, + transactionHash: CLAIM_TRANSACTION, + transactionIndex: 1, + logIndex: 0, + }); + + const launchParameters = { + name: mutation === "calldata" ? "Wrong" : "Fixture Token", + symbol: "FIX", + totalSwapFeeBps: 100, + creatorSalt: CREATOR_SALT, + metadata: { + description: "Fixture description", + website: "https://example.com", + image: "https://example.com/image.png", + extraData: "0x", + }, + } as const; + const transaction: ExactBlockRpcTransaction = Object.freeze({ + transactionHash: LAUNCH_TRANSACTION, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionIndex: 2, + from: CREATOR, + to: launcher, + input: encodeFunctionData({ + abi: launcherAbi, + functionName: "launch", + args: [launchParameters], + }), + value: INITIAL_BUY_WEI, + }); + const receiptSourceLogs = [ + registrationLog, + disclosureLog, + liquidityLog, + initialBuyLog, + launchLog, + ]; + if (mutation === "receipt") receiptSourceLogs.splice(2, 1); + const receipt: ExactBlockRpcReceipt = Object.freeze({ + transactionHash: LAUNCH_TRANSACTION, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionIndex: 2, + status: 1n, + logs: Object.freeze(receiptSourceLogs.map((log, receiptLogIndex) => + Object.freeze({ ...log, receiptLogIndex }) + )), + }); + + const runtimeHashes = [ + deployment.runtimeCodeHashes.hookFactory, + deployment.runtimeCodeHashes.feeHook, + deployment.runtimeCodeHashes.memeLauncher, + deployment.runtimeCodeHashes.positionForwarderFactory, + dependencies.contracts.poolManager.runtimeCodeHash, + dependencies.contracts.stateView.runtimeCodeHash, + ] as readonly `0x${string}`[]; + let runtimeCursor = 0; + let callBatch = 0; + const rpc: ExactBlockRpcClient = Object.freeze({ + endpointCommitment: `0x${"01".repeat(32)}`, + endpointOriginCommitment: `0x${"02".repeat(32)}`, + requestCount: () => 0, + logicalRequestCount: () => 0, + createPartitionClient: () => rpc, + assertCheckpoint: async () => 1_700_000_000n, + call: async () => { + throw new Error("unexpected single call"); + }, + async callMany({ calls, blockHash }) { + expect(blockHash).toBe(CHECKPOINT_HASH); + callBatch += 1; + if (callBatch === 1) { + expect(calls).toHaveLength(8); + return Object.freeze([ + encodedResult(launcherAbi, "poolManager", poolManager), + encodedResult(launcherAbi, "feeHook", hook), + encodedResult(launcherAbi, "MIN_INITIAL_BUY_WEI", 600_000_000_000_000n), + encodedResult(hookInfrastructureAbi, "poolManager", poolManager), + encodedResult(hookInfrastructureAbi, "launcherFeeRecipient", treasury), + encodedResult(creatorFeeHookReadAbi, "LAUNCHER_FEE_BPS", 10), + encodedResult(creatorFeeHookReadAbi, "LP_FEE_PIPS", 0), + encodedResult(hookInfrastructureAbi, "TICK_SPACING", 200), + ]); + } + expect(callBatch).toBe(2); + expect(calls).toHaveLength(14); + return Object.freeze([ + encodedResult( + uerc20ReadAbi, + "name", + mutation === "state" ? "Wrong state" : "Fixture Token", + ), + encodedResult(uerc20ReadAbi, "symbol", "FIX"), + encodedResult(uerc20ReadAbi, "decimals", 18), + encodedResult(uerc20ReadAbi, "totalSupply", TOTAL_SUPPLY), + encodedResult(uerc20ReadAbi, "creator", launcher), + encodedResult(uerc20ReadAbi, "metadata", [ + "Fixture description", + "https://example.com", + "https://example.com/image.png", + "0x", + ]), + encodedResult(stateViewReadAbi, "getSlot0", [ + SQRT_PRICE_X96, + 0, + 0, + 0, + ]), + encodedResult(stateViewReadAbi, "getLiquidity", 1_000_000n), + encodedResult(creatorFeeHookReadAbi, "feeDisclosure", [ + 100, + 100, + 90, + 10, + 0, + 0, + ]), + encodedResult(creatorFeeHookReadAbi, "poolFeeConfig", [ + CREATOR, + launcher, + 100, + true, + mutation === "claimed-fees" || + mutation === "claimed-fees-third-party" || + mutation === "claim-accounting" || + mutation === "claim-provenance" || + mutation === "unauthorized-redirect" + ? 4_000n + : 9_000n, + ]), + encodedResult(launcherAbi, "launchHashOf", LAUNCH_HASH), + encodedResult(launcherAbi, "predictTokenAddress", [ + TOKEN, + EFFECTIVE_GRAFFITI, + ]), + encodedResult( + launcherAbi, + "predictPositionRecipient", + POSITION_RECIPIENT, + ), + encodedResult(launcherAbi, "poolKey", [ + ZERO_ADDRESS, + TOKEN, + 0, + 200, + hook, + ]), + ]); + }, + async getCodeHash({ blockHash }) { + expect(blockHash).toBe(CHECKPOINT_HASH); + const expected = runtimeHashes[runtimeCursor++]!; + return mutation === "runtime" && runtimeCursor === 1 + ? (`0x${"03".repeat(32)}` as const) + : expected; + }, + async getLogs({ addresses, fromBlock, toBlock, maximumLogs }) { + expect(toBlock - fromBlock).toBeLessThan(CLASSIC_V2_RECONCILER_LOG_BLOCK_RANGE); + expect(maximumLogs).toBe(20_000); + const values = (Array.isArray(addresses) ? addresses : [addresses]) + .map((address) => address.toLowerCase()); + if (values.includes(launcher.toLowerCase())) { + const logs = [liquidityLog, initialBuyLog, launchLog]; + return Object.freeze(mutation === "log-order" + ? [logs[1]!, logs[0]!, logs[2]!] + : logs); + } + if (values.includes(hook.toLowerCase())) { + const logs = [registrationLog, disclosureLog]; + if (mutation !== "missing-fee") logs.push(feeLog); + if (mutation === "extra-fee" || mutation === "reordered-fees") { + logs.push(secondFeeLog); + } + if (mutation === "duplicate-fee") logs.push(duplicateFeeLog); + if ( + mutation === "claimed-fees" || + mutation === "claimed-fees-third-party" || + mutation === "claim-accounting" || + mutation === "claim-provenance" || + mutation === "unauthorized-redirect" + ) { + logs.push(creatorClaimLog); + } + return Object.freeze(logs); + } + expect(values).toContain(poolManager.toLowerCase()); + if (mutation === "tiny-swap") { + return Object.freeze([tinySwapLog, swapLog]); + } + if (mutation === "reordered-fees") { + return Object.freeze([secondSwapLog, swapLog]); + } + return Object.freeze([swapLog]); + }, + getBlockTimestamp: async () => 1_700_000_000n, + getBlockTimestamps: async ({ blocks }) => Object.freeze( + blocks.map(() => 1_700_000_000n), + ), + getTransactionReceipt: async () => receipt, + getTransactionReceipts: async () => Object.freeze([receipt]), + getTransaction: async () => transaction, + getTransactions: async () => Object.freeze([transaction]), + }); + const contract: ReconcilerPreParityContract = Object.freeze({ + chainId: "1", + releaseId: "classic-v2", + modelId: "classic", + sourceGroup: "core", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "1", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "1", + reorgGeneration: "0", + checkpointBlockNumber: checkpointBlock.toString(), + checkpointBlockHash: CHECKPOINT_HASH, + routeKeys: CLASSIC_V2_RECONCILER_ROUTE_KEYS, + routeContract: {}, + projectionContract: {}, + currentEntities: [{ + entityKind: "launch", + entityKey: TOKEN.toLowerCase(), + }], + }); + return { rpc, contract, checkpointBlock }; +} + +describe("Classic V2 exact-block contribution builder", () => { + it("builds deterministic token and chart contributions at one exact checkpoint", async () => { + const { rpc, contract, checkpointBlock } = fixture(); + const contribution = await buildClassicV2ExactBlockContribution({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + + expect(Object.keys(contribution).sort()).toEqual(["charts", "tokens"]); + expect(contribution.tokens).toHaveLength(1); + expect(contribution.charts).toHaveLength(1); + expect(contribution.tokens[0]).toMatchObject({ + releaseVersion: "classic-v2", + modelId: "classic", + tokenAddress: TOKEN.toLowerCase(), + creatorAddress: CREATOR.toLowerCase(), + rewardVaultAddress: null, + quoteAssetAddress: ZERO_ADDRESS, + launchLogIndex: 4, + }); + expect(contribution.charts[0]).toMatchObject({ + releaseVersion: "classic-v2", + modelId: "classic", + tokenAddress: TOKEN.toLowerCase(), + quoteAssetAddress: ZERO_ADDRESS, + state: { + transactionHash: SWAP_TRANSACTION, + blockHash: SWAP_BLOCK_HASH, + sqrtPriceX96: SQRT_PRICE_X96.toString(), + }, + volume: { + quoteAssetAddress: ZERO_ADDRESS, + grossQuoteRaw: "1000000", + creatorFeeQuoteRaw: "9000", + launcherFeeQuoteRaw: "1000", + }, + }); + }); + + it("accepts a 1-wei swap whose rounded fee is zero and emits no fee event", async () => { + const { rpc, contract, checkpointBlock } = fixture("tiny-swap"); + const contribution = await buildClassicV2ExactBlockContribution({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + + expect(contribution.charts[0]).toMatchObject({ + state: { transactionHash: SWAP_TRANSACTION }, + volume: { + grossQuoteRaw: "1000000", + creatorFeeQuoteRaw: "9000", + launcherFeeQuoteRaw: "1000", + }, + }); + }); + + it("reconciles claimed creator fees with the remaining exact-block balance", async () => { + const { rpc, contract, checkpointBlock } = fixture("claimed-fees"); + const contribution = await buildClassicV2ExactBlockContribution({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + + expect(contribution.charts[0]).toMatchObject({ + volume: { creatorFeeQuoteRaw: "9000" }, + }); + }); + + it("accepts a third-party trigger when the immutable creator receives the claim", async () => { + const { rpc, contract, checkpointBlock } = fixture( + "claimed-fees-third-party", + ); + await expect(buildClassicV2ExactBlockContribution({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + })).resolves.toBeDefined(); + }); + + it.each([ + ["runtime hash", "runtime"], + ["launch calldata", "calldata"], + ["receipt companion", "receipt"], + ["fee conservation", "fee"], + ["current state", "state"], + ["swap provenance", "provenance"], + ["log ordering", "log-order"], + ["missing nonzero rounded fee event", "missing-fee"], + ["extra fee event", "extra-fee"], + ["duplicate fee event", "duplicate-fee"], + ["reordered fee events", "reordered-fees"], + ["creator claim accounting", "claim-accounting"], + ["creator claim provenance", "claim-provenance"], + ["unauthorized creator claim redirect", "unauthorized-redirect"], + ] as const)("fails closed on a bad %s", async (_label, mutation) => { + const { rpc, contract, checkpointBlock } = fixture(mutation); + await expect(buildClassicV2ExactBlockContribution({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("uses non-overlapping provider-portable 10,000-block ranges", () => { + expect(CLASSIC_V2_RECONCILER_LOG_BLOCK_RANGE).toBe(10_000n); + expect(classicV2ReconcilerBlockRanges(100n, 10_099n)).toEqual([ + { fromBlock: 100n, toBlock: 10_099n }, + ]); + expect(classicV2ReconcilerBlockRanges(100n, 10_100n)).toEqual([ + { fromBlock: 100n, toBlock: 10_099n }, + { fromBlock: 10_100n, toBlock: 10_100n }, + ]); + }); + + it("accepts growth beyond the former 256-launch boundary", () => { + expect([1, 128, 256, 257, 10_000].map( + assertClassicV2ReconcilerLaunchCount, + )).toEqual([1, 128, 256, 257, 10_000]); + expect(() => assertClassicV2ReconcilerLaunchCount(0)).toThrow(); + }); +}); diff --git a/tests/data-pipeline/classic-v3-reconciler-route-builder.test.ts b/tests/data-pipeline/classic-v3-reconciler-route-builder.test.ts new file mode 100644 index 00000000..84047304 --- /dev/null +++ b/tests/data-pipeline/classic-v3-reconciler-route-builder.test.ts @@ -0,0 +1,1658 @@ +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + encodeEventTopics, + encodeFunctionData, + encodeFunctionResult, + getAddress, + keccak256, + parseAbi, + parseAbiItem, + type Abi, + type AbiEvent, + type Address, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +import { + buildClassicV3ExactBlockRoutes, + assertClassicV3ReconcilerLaunchCount, + classicV3ReconcilerBlockRanges, + CLASSIC_V3_RECONCILER_LOG_BLOCK_RANGE, +} from "../../lib/data-pipeline/classic-v3-reconciler-route-builder.server"; +import { + CLASSIC_V3_RECONCILER_ROUTE_CONTRACT, +} from "../../lib/data-pipeline/classic-v3-reconciler-route-contract"; +import { + classicRewardVaultAbi, + classicV3HookAbi, + classicV3LaunchAbi, +} from "../../lib/classic-v3"; +import { getConfiguredClassicV3Release } from "../../lib/classic-v3-release"; +import dependencies from "../../contracts/dependencies/ethereum-mainnet.json"; +import type { + ExactBlockRpcClient, + ExactBlockRpcLog, + ExactBlockRpcReceipt, + ExactBlockRpcTransaction, +} from "../../lib/data-pipeline/reconciler-exact-block-reader.server"; +import { + RECONCILER_ROUTE_KEYS, + type ReconcilerPreParityContract, +} from "../../lib/data-pipeline/reconciler-preparity"; +import { stateViewReadAbi, uerc20ReadAbi } from "../../lib/onchain/abis"; + +const ZERO_ADDRESS = `0x${"00".repeat(20)}` as Address; +const TOKEN = getAddress(`0x${"11".repeat(20)}`); +const DEPLOYER = getAddress(`0x${"22".repeat(20)}`); +const VAULT = getAddress(`0x${"33".repeat(20)}`); +const BENEFICIARY = getAddress(`0x${"44".repeat(20)}`); +const REPLACEMENT = getAddress(`0x${"45".repeat(20)}`); +const CTO_BENEFICIARY = getAddress(`0x${"46".repeat(20)}`); +const POSITION_RECIPIENT = getAddress(`0x${"55".repeat(20)}`); +const SWAP_SENDER = getAddress(`0x${"66".repeat(20)}`); +const POOL_ID = `0x${"77".repeat(32)}` as const; +const WRONG_POOL_ID = `0x${"76".repeat(32)}` as const; +const CONFIGURATION_HASH = `0x${"88".repeat(32)}` as const; +const LAUNCH_HASH = `0x${"99".repeat(32)}` as const; +const CREATOR_SALT = `0x${"aa".repeat(32)}` as const; +const LAUNCH_TRANSACTION = `0x${"bb".repeat(32)}` as const; +const LAUNCH_BLOCK_HASH = `0x${"cc".repeat(32)}` as const; +const SWAP_TRANSACTION = `0x${"dd".repeat(32)}` as const; +const TINY_SWAP_TRANSACTION = `0x${"de".repeat(32)}` as const; +const SECOND_SWAP_TRANSACTION = `0x${"df".repeat(32)}` as const; +const HISTORY_TRANSACTION = `0x${"e0".repeat(32)}` as const; +const SWAP_BLOCK_HASH = `0x${"ee".repeat(32)}` as const; +const CHECKPOINT_HASH = `0x${"ff".repeat(32)}` as const; +const APPROVAL_REFERENCE = `0x${"ab".repeat(32)}` as const; +const TOTAL_SUPPLY = 1_000_000_000n * 10n ** 18n; +const TOKEN_LIQUIDITY = TOTAL_SUPPLY - 1n; +const SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336n; + +const launchedEvent = parseAbiItem( + "event MemeTokenLaunchedV2(address indexed deployer,address indexed token,bytes32 indexed poolId,address feeHook,address rewardVault,address positionRecipient,uint256 positionTokenId,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,bytes32 rewardConfigurationHash,bytes32 launchHash)", +); +const liquidityEvent = parseAbiItem( + "event MemeLiquidityConfiguredV2(address indexed token,uint256 totalSupply,uint256 tokenLiquidityAmount,uint256 lockedTokenDust,int24 initialTick,int24 tickLower,int24 tickUpper,uint24 lpFeePips,bytes32 launchHash)", +); +const initialBuyEvent = parseAbiItem( + "event MemeCreatorInitialBuyV2(address indexed deployer,address indexed token,bytes32 indexed poolId,uint256 nativeAmount,uint256 tokenAmount,bytes32 launchHash)", +); +const custodyEvent = parseAbiItem( + "event MemeCreatorInitialBuyCustodyV2(address indexed deployer,address indexed token,address indexed custody,uint8 mode,uint16 durationDays,uint16 cliffDays,bytes32 configurationHash,bytes32 launchHash)", +); +const registeredEvent = parseAbiItem( + "event PoolRegistered(bytes32 indexed poolId,address indexed token,address indexed rewardVault,address registrar,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,bytes32 rewardConfigurationHash)", +); +const disclosureEvent = parseAbiItem( + "event PoolFeeDisclosure(bytes32 indexed poolId,address indexed token,address indexed rewardVault,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,uint16 buyCreatorFeeBps,uint16 sellCreatorFeeBps,uint16 launcherFeeBps,uint16 transferTaxBps,uint24 lpFeePips)", +); +const feeAccruedEvent = parseAbiItem( + "event NativeSwapFeesAccrued(bytes32 indexed poolId,address indexed swapSender,bool indexed isBuy,uint16 appliedTotalSwapFeeBps,uint256 grossNativeAmount,uint256 creatorFee,uint256 launcherFee)", +); +const vaultDeployedEvent = parseAbiItem( + "event ClassicRewardVaultDeployed(address indexed vault,bytes32 indexed poolId,address indexed feeHook,bytes32 salt,bytes32 configurationHash)", +); +const swapEvent = parseAbiItem( + "event Swap(bytes32 indexed id,address indexed sender,int128 amount0,int128 amount1,uint160 sqrtPriceX96,uint128 liquidity,int24 tick,uint24 fee)", +); +const checkpointEvent = parseAbiItem( + "event CreatorFeesCheckpointed(bytes32 indexed poolId,uint64 indexed configurationEpoch,uint256 amount,uint256 totalCreatorFeesReceived)", +); +const beneficiaryClaimEvent = parseAbiItem( + "event BeneficiaryFeesClaimed(address indexed beneficiary,uint256 amount,uint256 beneficiaryTotalClaimed,uint256 vaultTotalReceived)", +); +const payoutChangedEvent = parseAbiItem( + "event PayoutWalletChanged(bytes32 indexed poolId,uint256 indexed allocationIndex,address indexed previousPayoutWallet,address newPayoutWallet,uint16 shareBps,uint64 configurationEpoch,bytes32 activeConfigurationHash,uint256 effectiveTotalCreatorFeesReceived)", +); +const ctoActivatedEvent = parseAbiItem( + "event CtoRewardConfigurationActivated(bytes32 indexed poolId,bytes32 indexed approvalReference,uint64 indexed configurationEpoch,bytes32 previousConfigurationHash,bytes32 newConfigurationHash,address[] beneficiaries,uint16[] sharesBps,uint256 effectiveTotalCreatorFeesReceived)", +); + +const vaultFactoryAbi = parseAbi([ + "function isFactoryVault(address vault) view returns (bool)", + "function configurationHashOf(address vault) view returns (bytes32)", +]); + +type Mutation = + | "none" + | "runtime" + | "calldata" + | "receipt" + | "fee" + | "direction" + | "reward" + | "log-order" + | "tiny-swap" + | "missing-fee" + | "extra-fee" + | "duplicate-fee" + | "reordered-fees" + | "payout-history" + | "cto-history" + | "fully-claimed-history" + | "swap-filter-address" + | "swap-filter-selector" + | "swap-filter-pool" + | "swap-filter-range"; + +function eventLog(input: { + event: AbiEvent; + args: Readonly>; + address: Address; + blockNumber: bigint; + blockHash: `0x${string}`; + transactionHash: `0x${string}`; + transactionIndex: number; + logIndex: number; +}): ExactBlockRpcLog { + const topics = encodeEventTopics({ + abi: [input.event], + eventName: input.event.name, + args: input.args, + } as never); + const nonIndexed = input.event.inputs.filter((item) => !item.indexed); + const values = nonIndexed.map((item) => input.args[item.name!]); + const data = encodeAbiParameters(nonIndexed, values as never); + return Object.freeze({ + address: input.address, + blockNumber: input.blockNumber, + blockHash: input.blockHash as `0x${string}`, + transactionHash: input.transactionHash as `0x${string}`, + transactionIndex: input.transactionIndex, + logIndex: input.logIndex, + topics: Object.freeze(topics as readonly Hex[]), + data, + }); +} + +function encodedResult( + abi: Abi, + functionName: string, + result: unknown, +): Hex { + return encodeFunctionResult({ abi, functionName, result } as never); +} + +function topicsMatch( + logTopics: readonly Hex[], + requested: readonly (Hex | readonly Hex[] | null)[] | undefined, +): boolean { + if (!requested) return true; + return requested.every((filter, index) => { + if (filter === null) return true; + const actual = (logTopics[index] ?? "").toLowerCase(); + return Array.isArray(filter) + ? filter.some((candidate) => candidate.toLowerCase() === actual) + : (filter as Hex).toLowerCase() === actual; + }); +} + +function fixture(mutation: Mutation = "none") { + const configured = getConfiguredClassicV3Release("production"); + const app = configured.appManifest; + const launcher = getAddress(app.memeLaunchV2!); + const hook = getAddress(app.ethCreatorFeeHookV3!); + const vaultFactory = getAddress(app.classicRewardVaultFactoryV1!); + const startBlock = BigInt(app.deploymentBlocks!.memeLaunchV2!); + const launchBlock = startBlock + 1n; + const swapBlock = startBlock + 2n; + const checkpointBlock = startBlock + 5n; + const currentBeneficiary = mutation === "payout-history" || + mutation === "fully-claimed-history" + ? REPLACEMENT + : mutation === "cto-history" + ? CTO_BENEFICIARY + : BENEFICIARY; + const configurationEpoch = mutation === "payout-history" || + mutation === "cto-history" || + mutation === "fully-claimed-history" + ? 2n + : 1n; + const initialActiveConfigurationHash = keccak256(encodeAbiParameters( + parseAbi([ + "function f(uint256 chainId,address vault,bytes32 configurationHash,uint64 epoch,address[] beneficiaries,uint16[] sharesBps)", + ])[0]!.inputs, + [1n, VAULT, CONFIGURATION_HASH, 1n, [BENEFICIARY], [10_000]], + )); + const activeConfigurationHash = keccak256(encodeAbiParameters( + parseAbi([ + "function f(uint256 chainId,address vault,bytes32 configurationHash,uint64 epoch,address[] beneficiaries,uint16[] sharesBps)", + ])[0]!.inputs, + [ + 1n, + VAULT, + CONFIGURATION_HASH, + configurationEpoch, + [currentBeneficiary], + [10_000], + ], + )); + + const factoryLog = eventLog({ + event: vaultDeployedEvent, + args: { + vault: VAULT, + poolId: POOL_ID, + feeHook: hook, + salt: CREATOR_SALT, + configurationHash: CONFIGURATION_HASH, + }, + address: vaultFactory, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 0, + }); + const registrationLog = eventLog({ + event: registeredEvent, + args: { + poolId: POOL_ID, + token: TOKEN, + rewardVault: VAULT, + registrar: launcher, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + rewardConfigurationHash: CONFIGURATION_HASH, + }, + address: hook, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 1, + }); + const beneficiaryClaimLog = eventLog({ + event: beneficiaryClaimEvent, + args: { + beneficiary: BENEFICIARY, + amount: 90n, + beneficiaryTotalClaimed: 90n, + vaultTotalReceived: 90n, + }, + address: VAULT, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: HISTORY_TRANSACTION, + transactionIndex: 4, + logIndex: 1, + }); + const disclosureLog = eventLog({ + event: disclosureEvent, + args: { + poolId: POOL_ID, + token: TOKEN, + rewardVault: VAULT, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + }, + address: hook, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 2, + }); + const liquidityLog = eventLog({ + event: liquidityEvent, + args: { + token: TOKEN, + totalSupply: TOTAL_SUPPLY, + tokenLiquidityAmount: TOKEN_LIQUIDITY, + lockedTokenDust: 1n, + initialTick: 0, + tickLower: -887_220, + tickUpper: 0, + lpFeePips: 0, + launchHash: LAUNCH_HASH, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 3, + }); + const initialBuyLog = eventLog({ + event: initialBuyEvent, + args: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + nativeAmount: 1_000n, + tokenAmount: 10_000n, + launchHash: LAUNCH_HASH, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 4, + }); + const custodyLog = eventLog({ + event: custodyEvent, + args: { + deployer: DEPLOYER, + token: TOKEN, + custody: ZERO_ADDRESS, + mode: 0, + durationDays: 0, + cliffDays: 0, + configurationHash: CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 5, + }); + const launchLog = eventLog({ + event: launchedEvent, + args: { + deployer: DEPLOYER, + token: TOKEN, + poolId: POOL_ID, + feeHook: hook, + rewardVault: VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 42n, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + rewardConfigurationHash: CONFIGURATION_HASH, + launchHash: LAUNCH_HASH, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionHash: LAUNCH_TRANSACTION, + transactionIndex: 2, + logIndex: 6, + }); + const feeLog = eventLog({ + event: feeAccruedEvent, + args: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + isBuy: mutation !== "direction", + appliedTotalSwapFeeBps: 100, + grossNativeAmount: 10_000n, + creatorFee: mutation === "fee" ? 89n : 90n, + launcherFee: 10n, + }, + address: hook, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: SWAP_TRANSACTION, + transactionIndex: 3, + logIndex: 0, + }); + const encodedSwapLog = eventLog({ + event: swapEvent, + args: { + id: mutation === "swap-filter-pool" ? WRONG_POOL_ID : POOL_ID, + sender: SWAP_SENDER, + amount0: 10_000n, + amount1: -1n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: 1_000_000n, + tick: 0, + fee: 0, + }, + address: mutation === "swap-filter-address" + ? TOKEN + : getAddress(dependencies.contracts.poolManager.address), + blockNumber: mutation === "swap-filter-range" + ? checkpointBlock + 1n + : swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: SWAP_TRANSACTION, + transactionIndex: 3, + logIndex: mutation === "reordered-fees" ? 3 : 1, + }); + const swapLog: ExactBlockRpcLog = mutation === "swap-filter-selector" + ? Object.freeze({ + ...encodedSwapLog, + topics: Object.freeze([ + CONFIGURATION_HASH, + ...encodedSwapLog.topics.slice(1), + ]), + }) + : encodedSwapLog; + const tinySwapLog = eventLog({ + event: swapEvent, + args: { + id: POOL_ID, + sender: SWAP_SENDER, + amount0: 1n, + amount1: -1n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: 1_000_000n, + tick: 0, + fee: 0, + }, + address: getAddress(dependencies.contracts.poolManager.address), + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: TINY_SWAP_TRANSACTION, + transactionIndex: 2, + logIndex: 0, + }); + const secondFeeLog = eventLog({ + event: feeAccruedEvent, + args: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + isBuy: true, + appliedTotalSwapFeeBps: 100, + grossNativeAmount: 20_000n, + creatorFee: 180n, + launcherFee: 20n, + }, + address: hook, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: mutation === "reordered-fees" + ? SWAP_TRANSACTION + : SECOND_SWAP_TRANSACTION, + transactionIndex: mutation === "reordered-fees" ? 3 : 4, + logIndex: mutation === "reordered-fees" ? 2 : 0, + }); + const secondSwapLog = eventLog({ + event: swapEvent, + args: { + id: POOL_ID, + sender: SWAP_SENDER, + amount0: 20_000n, + amount1: -2n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: 1_000_000n, + tick: 0, + fee: 0, + }, + address: getAddress(dependencies.contracts.poolManager.address), + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: mutation === "reordered-fees" + ? SWAP_TRANSACTION + : SECOND_SWAP_TRANSACTION, + transactionIndex: mutation === "reordered-fees" ? 3 : 4, + logIndex: 1, + }); + const duplicateFeeLog = eventLog({ + event: feeAccruedEvent, + args: { + poolId: POOL_ID, + swapSender: SWAP_SENDER, + isBuy: true, + appliedTotalSwapFeeBps: 100, + grossNativeAmount: 10_000n, + creatorFee: 90n, + launcherFee: 10n, + }, + address: hook, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: SWAP_TRANSACTION, + transactionIndex: 3, + logIndex: 2, + }); + const checkpointLog = eventLog({ + event: checkpointEvent, + args: { + poolId: POOL_ID, + configurationEpoch: 1n, + amount: 90n, + totalCreatorFeesReceived: 90n, + }, + address: VAULT, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: HISTORY_TRANSACTION, + transactionIndex: 4, + logIndex: 0, + }); + const payoutChangedLog = eventLog({ + event: payoutChangedEvent, + args: { + poolId: POOL_ID, + allocationIndex: 0n, + previousPayoutWallet: BENEFICIARY, + newPayoutWallet: REPLACEMENT, + shareBps: 10_000, + configurationEpoch: 2n, + activeConfigurationHash, + effectiveTotalCreatorFeesReceived: 90n, + }, + address: VAULT, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: HISTORY_TRANSACTION, + transactionIndex: 4, + logIndex: mutation === "fully-claimed-history" ? 2 : 1, + }); + const ctoActivatedLog = eventLog({ + event: ctoActivatedEvent, + args: { + poolId: POOL_ID, + approvalReference: APPROVAL_REFERENCE, + configurationEpoch: 2n, + previousConfigurationHash: initialActiveConfigurationHash, + newConfigurationHash: activeConfigurationHash, + beneficiaries: [CTO_BENEFICIARY], + sharesBps: [10_000], + effectiveTotalCreatorFeesReceived: 90n, + }, + address: VAULT, + blockNumber: swapBlock, + blockHash: SWAP_BLOCK_HASH, + transactionHash: HISTORY_TRANSACTION, + transactionIndex: 4, + logIndex: 1, + }); + + const launchParameters = { + name: mutation === "calldata" ? "Wrong" : "Fixture Token", + symbol: "FIX", + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + creatorSalt: CREATOR_SALT, + metadata: { + description: "Fixture description", + website: "https://example.com", + image: "https://example.com/image.png", + extraData: "0x", + }, + rewardBeneficiaries: [BENEFICIARY], + rewardSharesBps: [10_000], + initialBuyCustody: { mode: 0, durationDays: 0, cliffDays: 0 }, + } as const; + const transaction: ExactBlockRpcTransaction = Object.freeze({ + transactionHash: LAUNCH_TRANSACTION, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionIndex: 2, + from: DEPLOYER, + to: launcher, + input: encodeFunctionData({ + abi: classicV3LaunchAbi, + functionName: "launch", + args: [launchParameters], + }), + value: 1_000n, + }); + const receiptSourceLogs = [ + factoryLog, + registrationLog, + disclosureLog, + liquidityLog, + initialBuyLog, + custodyLog, + launchLog, + ]; + if (mutation === "receipt") receiptSourceLogs.splice(3, 1); + const receipt: ExactBlockRpcReceipt = Object.freeze({ + transactionHash: LAUNCH_TRANSACTION, + blockNumber: launchBlock, + blockHash: LAUNCH_BLOCK_HASH, + transactionIndex: 2, + status: 1n, + logs: Object.freeze(receiptSourceLogs.map((log, receiptLogIndex) => + Object.freeze({ ...log, receiptLogIndex }) + )), + }); + + const runtimeHashes = [ + app.runtimeCodeHashes!.classicCtoAuthorityV1!, + app.runtimeCodeHashes!.memeLaunchV2!, + app.runtimeCodeHashes!.ethCreatorFeeHookV3!, + app.runtimeCodeHashes!.classicRewardVaultFactoryV1!, + app.runtimeCodeHashes!.classicInitialBuyVestingWalletFactoryV1!, + app.runtimeCodeHashes!.classicLaunchPolicyV1!, + app.runtimeCodeHashes!.ethCreatorFeeHookFactoryV3!, + app.runtimeCodeHashes!.lockedPositionFeeForwarderFactory!, + dependencies.contracts.poolManager.runtimeCodeHash, + dependencies.contracts.stateView.runtimeCodeHash, + ] as readonly `0x${string}`[]; + let runtimeCursor = 0; + let callBatch = 0; + const rpc: ExactBlockRpcClient = Object.freeze({ + endpointCommitment: `0x${"01".repeat(32)}`, + endpointOriginCommitment: `0x${"02".repeat(32)}`, + requestCount: () => 0, + logicalRequestCount: () => 0, + createPartitionClient: () => rpc, + assertCheckpoint: async () => 1_700_000_000n, + call: async () => { + throw new Error("unexpected single call"); + }, + async callMany({ calls, blockHash }) { + expect(blockHash).toBe(CHECKPOINT_HASH); + callBatch += 1; + if (callBatch === 1) { + expect(calls).toHaveLength(21); + return Object.freeze([ + encodedResult(uerc20ReadAbi, "name", "Fixture Token"), + encodedResult(uerc20ReadAbi, "symbol", "FIX"), + encodedResult(uerc20ReadAbi, "decimals", 18), + encodedResult(uerc20ReadAbi, "totalSupply", TOTAL_SUPPLY), + encodedResult(uerc20ReadAbi, "creator", launcher), + encodedResult(uerc20ReadAbi, "metadata", [ + "Fixture description", + "https://example.com", + "https://example.com/image.png", + "0x", + ]), + encodedResult(stateViewReadAbi, "getSlot0", [ + SQRT_PRICE_X96, + 0, + 0, + 0, + ]), + encodedResult(stateViewReadAbi, "getLiquidity", 1_000_000n), + encodedResult(classicV3HookAbi, "feeDisclosure", [ + 100, + 100, + 90, + 90, + 10, + 0, + 0, + VAULT, + ]), + encodedResult(classicV3HookAbi, "poolFeeConfig", [ + VAULT, + launcher, + 100, + 100, + true, + 0n, + ]), + encodedResult(classicV3LaunchAbi, "predictRewardVault", VAULT), + encodedResult(vaultFactoryAbi, "isFactoryVault", true), + encodedResult(vaultFactoryAbi, "configurationHashOf", CONFIGURATION_HASH), + encodedResult(classicRewardVaultAbi, "feeHook", hook), + encodedResult(classicRewardVaultAbi, "poolId", POOL_ID), + encodedResult(classicRewardVaultAbi, "configurationHash", CONFIGURATION_HASH), + encodedResult( + classicRewardVaultAbi, + "activeConfigurationHash", + mutation === "reward" ? LAUNCH_HASH : activeConfigurationHash, + ), + encodedResult( + classicRewardVaultAbi, + "configurationEpoch", + configurationEpoch, + ), + encodedResult(classicRewardVaultAbi, "beneficiaryCount", 1n), + encodedResult(classicRewardVaultAbi, "totalCreatorFeesReceived", 90n), + encodedResult( + classicRewardVaultAbi, + "totalCreatorFeesClaimed", + mutation === "fully-claimed-history" ? 90n : 0n, + ), + ]); + } + if (callBatch === 2) { + expect(calls).toHaveLength(2); + return Object.freeze([ + encodedResult( + classicRewardVaultAbi, + "beneficiaryAt", + currentBeneficiary, + ), + encodedResult(classicRewardVaultAbi, "shareBpsAt", 10_000), + ]); + } + expect(callBatch).toBe(3); + const hasHistory = mutation === "payout-history" || + mutation === "cto-history" || + mutation === "fully-claimed-history"; + expect(calls).toHaveLength(hasHistory ? 4 : 2); + if (hasHistory) { + return Object.freeze([ + encodedResult( + classicRewardVaultAbi, + "claimable", + mutation === "fully-claimed-history" ? 0n : 90n, + ), + encodedResult( + classicRewardVaultAbi, + "claimedBy", + mutation === "fully-claimed-history" ? 90n : 0n, + ), + encodedResult(classicRewardVaultAbi, "claimable", 0n), + encodedResult(classicRewardVaultAbi, "claimedBy", 0n), + ]); + } + return Object.freeze([ + encodedResult(classicRewardVaultAbi, "claimable", 90n), + encodedResult(classicRewardVaultAbi, "claimedBy", 0n), + ]); + }, + async getCodeHash({ blockHash }) { + expect(blockHash).toBe(CHECKPOINT_HASH); + const expected = runtimeHashes[runtimeCursor++]!; + return mutation === "runtime" && runtimeCursor === 1 + ? (`0x${"03".repeat(32)}` as const) + : expected; + }, + async getLogs({ addresses }) { + const values = (Array.isArray(addresses) ? addresses : [addresses]) + .map((address) => address.toLowerCase()); + if (values.includes(launcher.toLowerCase())) { + const logs = [liquidityLog, initialBuyLog, custodyLog, launchLog]; + return Object.freeze(mutation === "log-order" + ? [logs[1]!, logs[0]!, ...logs.slice(2)] + : logs); + } + if (values.includes(hook.toLowerCase())) { + const logs = [registrationLog, disclosureLog]; + if (mutation !== "missing-fee") logs.push(feeLog); + if (mutation === "extra-fee" || mutation === "reordered-fees") { + logs.push(secondFeeLog); + } + if (mutation === "duplicate-fee") logs.push(duplicateFeeLog); + return Object.freeze(logs); + } + if (values.includes(vaultFactory.toLowerCase())) { + return Object.freeze([factoryLog]); + } + if (values.includes(VAULT.toLowerCase())) { + if (mutation === "payout-history") { + return Object.freeze([checkpointLog, payoutChangedLog]); + } + if (mutation === "fully-claimed-history") { + return Object.freeze([ + checkpointLog, + beneficiaryClaimLog, + payoutChangedLog, + ]); + } + if (mutation === "cto-history") { + return Object.freeze([checkpointLog, ctoActivatedLog]); + } + return Object.freeze([]); + } + if (mutation === "tiny-swap") { + return Object.freeze([tinySwapLog, swapLog]); + } + if (mutation === "reordered-fees") { + return Object.freeze([secondSwapLog, swapLog]); + } + return Object.freeze([swapLog]); + }, + getBlockTimestamp: async () => 1_700_000_000n, + getBlockTimestamps: async ({ blocks }) => Object.freeze( + blocks.map(() => 1_700_000_000n), + ), + getTransactionReceipt: async () => receipt, + getTransactionReceipts: async () => Object.freeze([receipt]), + getTransaction: async () => transaction, + getTransactions: async () => Object.freeze([transaction]), + }); + const contract: ReconcilerPreParityContract = Object.freeze({ + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "1", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "1", + reorgGeneration: "0", + checkpointBlockNumber: checkpointBlock.toString(), + checkpointBlockHash: CHECKPOINT_HASH, + routeKeys: RECONCILER_ROUTE_KEYS, + routeContract: {}, + projectionContract: {}, + currentEntities: [{ + entityKind: "launch", + entityKey: TOKEN.toLowerCase(), + }], + }); + return { rpc, contract, checkpointBlock }; +} + +function largeCorpusFixture(launchCount: number) { + const configured = getConfiguredClassicV3Release("production"); + const app = configured.appManifest; + const launcher = getAddress(app.memeLaunchV2!); + const hook = getAddress(app.ethCreatorFeeHookV3!); + const vaultFactory = getAddress(app.classicRewardVaultFactoryV1!); + const poolManager = getAddress(dependencies.contracts.poolManager.address); + const stateView = getAddress(dependencies.contracts.stateView.address); + const startBlock = BigInt(app.deploymentBlocks!.memeLaunchV2!); + const checkpointBlock = startBlock + BigInt(launchCount) + 2n; + const indexedAddress = (domain: number, index: number) => getAddress( + `0x${((BigInt(domain) << 152n) | BigInt(index + 1)).toString(16).padStart(40, "0")}`, + ); + const indexedBytes32 = (domain: number, index: number) => + `0x${((BigInt(domain) << 248n) | BigInt(index + 1)).toString(16).padStart(64, "0")}` as const; + const activeConfigurationParameters = parseAbi([ + "function f(uint256 chainId,address vault,bytes32 configurationHash,uint64 epoch,address[] beneficiaries,uint16[] sharesBps)", + ])[0]!.inputs; + const launcherLogs: ExactBlockRpcLog[] = []; + const hookLogs: ExactBlockRpcLog[] = []; + const factoryLogs: ExactBlockRpcLog[] = []; + const swapLogs: ExactBlockRpcLog[] = []; + const transactions = new Map(); + const receipts = new Map(); + const blockHashes = new Map(); + const callResults = new Map(); + const tokens: Address[] = []; + + const registerCallResult = ( + to: Address, + abi: Abi, + functionName: string, + args: readonly unknown[], + value: unknown, + ) => { + const data = encodeFunctionData({ abi, functionName, args } as never); + callResults.set( + `${to.toLowerCase()}:${data.toLowerCase()}`, + encodedResult(abi, functionName, value), + ); + }; + + for (let index = 0; index < launchCount; index += 1) { + const token = indexedAddress(0x11, index); + const vault = indexedAddress(0x22, index); + const poolId = indexedBytes32(0x33, index); + const configurationHash = indexedBytes32(0x44, index); + const launchHash = indexedBytes32(0x55, index); + const creatorSalt = indexedBytes32(0x66, index); + const transactionHash = indexedBytes32(0x77, index); + const launchBlockHash = indexedBytes32(0x88, index); + const swapTransactionHash = indexedBytes32(0x99, index); + const launchBlock = startBlock + BigInt(index) + 1n; + const name = `Fixture Token ${index + 1}`; + const symbol = `F${index + 1}`; + const activeConfigurationHash = keccak256(encodeAbiParameters( + activeConfigurationParameters, + [1n, vault, configurationHash, 1n, [BENEFICIARY], [10_000]], + )); + tokens.push(token); + blockHashes.set(launchBlock.toString(), launchBlockHash); + + const factoryLog = eventLog({ + event: vaultDeployedEvent, + args: { + vault, + poolId, + feeHook: hook, + salt: creatorSalt, + configurationHash, + }, + address: vaultFactory, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash, + transactionIndex: 2, + logIndex: 0, + }); + const registrationLog = eventLog({ + event: registeredEvent, + args: { + poolId, + token, + rewardVault: vault, + registrar: launcher, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + rewardConfigurationHash: configurationHash, + }, + address: hook, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash, + transactionIndex: 2, + logIndex: 1, + }); + const disclosureLog = eventLog({ + event: disclosureEvent, + args: { + poolId, + token, + rewardVault: vault, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + }, + address: hook, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash, + transactionIndex: 2, + logIndex: 2, + }); + const liquidityLog = eventLog({ + event: liquidityEvent, + args: { + token, + totalSupply: TOTAL_SUPPLY, + tokenLiquidityAmount: TOKEN_LIQUIDITY, + lockedTokenDust: 1n, + initialTick: 0, + tickLower: -887_220, + tickUpper: 0, + lpFeePips: 0, + launchHash, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash, + transactionIndex: 2, + logIndex: 3, + }); + const initialBuyLog = eventLog({ + event: initialBuyEvent, + args: { + deployer: DEPLOYER, + token, + poolId, + nativeAmount: 1_000n, + tokenAmount: 10_000n, + launchHash, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash, + transactionIndex: 2, + logIndex: 4, + }); + const custodyLog = eventLog({ + event: custodyEvent, + args: { + deployer: DEPLOYER, + token, + custody: ZERO_ADDRESS, + mode: 0, + durationDays: 0, + cliffDays: 0, + configurationHash, + launchHash, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash, + transactionIndex: 2, + logIndex: 5, + }); + const launchLog = eventLog({ + event: launchedEvent, + args: { + deployer: DEPLOYER, + token, + poolId, + feeHook: hook, + rewardVault: vault, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: BigInt(index + 1), + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + rewardConfigurationHash: configurationHash, + launchHash, + }, + address: launcher, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash, + transactionIndex: 2, + logIndex: 6, + }); + const feeLog = eventLog({ + event: feeAccruedEvent, + args: { + poolId, + swapSender: SWAP_SENDER, + isBuy: true, + appliedTotalSwapFeeBps: 100, + grossNativeAmount: 10_000n, + creatorFee: 90n, + launcherFee: 10n, + }, + address: hook, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash: swapTransactionHash, + transactionIndex: 3, + logIndex: 0, + }); + const swapLog = eventLog({ + event: swapEvent, + args: { + id: poolId, + sender: SWAP_SENDER, + amount0: 10_000n, + amount1: -1n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: 1_000_000n, + tick: 0, + fee: 0, + }, + address: poolManager, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionHash: swapTransactionHash, + transactionIndex: 3, + logIndex: 1, + }); + launcherLogs.push(liquidityLog, initialBuyLog, custodyLog, launchLog); + hookLogs.push(registrationLog, disclosureLog, feeLog); + factoryLogs.push(factoryLog); + swapLogs.push(swapLog); + + const launchParameters = { + name, + symbol, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + creatorSalt, + metadata: { + description: "Fixture description", + website: "https://example.com", + image: "https://example.com/image.png", + extraData: "0x", + }, + rewardBeneficiaries: [BENEFICIARY], + rewardSharesBps: [10_000], + initialBuyCustody: { mode: 0, durationDays: 0, cliffDays: 0 }, + } as const; + transactions.set(transactionHash.toLowerCase(), Object.freeze({ + transactionHash, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionIndex: 2, + from: DEPLOYER, + to: launcher, + input: encodeFunctionData({ + abi: classicV3LaunchAbi, + functionName: "launch", + args: [launchParameters], + }), + value: 1_000n, + })); + const receiptLogs = [ + factoryLog, + registrationLog, + disclosureLog, + liquidityLog, + initialBuyLog, + custodyLog, + launchLog, + ]; + receipts.set(transactionHash.toLowerCase(), Object.freeze({ + transactionHash, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionIndex: 2, + status: 1n, + logs: Object.freeze(receiptLogs.map((entry, receiptLogIndex) => + Object.freeze({ ...entry, receiptLogIndex }) + )), + })); + + registerCallResult(token, uerc20ReadAbi, "name", [], name); + registerCallResult(token, uerc20ReadAbi, "symbol", [], symbol); + registerCallResult(token, uerc20ReadAbi, "decimals", [], 18); + registerCallResult(token, uerc20ReadAbi, "totalSupply", [], TOTAL_SUPPLY); + registerCallResult(token, uerc20ReadAbi, "creator", [], launcher); + registerCallResult(token, uerc20ReadAbi, "metadata", [], [ + "Fixture description", + "https://example.com", + "https://example.com/image.png", + "0x", + ]); + registerCallResult(stateView, stateViewReadAbi, "getSlot0", [poolId], [ + SQRT_PRICE_X96, + 0, + 0, + 0, + ]); + registerCallResult( + stateView, + stateViewReadAbi, + "getLiquidity", + [poolId], + 1_000_000n, + ); + registerCallResult(hook, classicV3HookAbi, "feeDisclosure", [poolId], [ + 100, + 100, + 90, + 90, + 10, + 0, + 0, + vault, + ]); + registerCallResult(hook, classicV3HookAbi, "poolFeeConfig", [poolId], [ + vault, + launcher, + 100, + 100, + true, + 0n, + ]); + registerCallResult( + launcher, + classicV3LaunchAbi, + "predictRewardVault", + [token, DEPLOYER, [BENEFICIARY], [10_000]], + vault, + ); + registerCallResult( + vaultFactory, + vaultFactoryAbi, + "isFactoryVault", + [vault], + true, + ); + registerCallResult( + vaultFactory, + vaultFactoryAbi, + "configurationHashOf", + [vault], + configurationHash, + ); + registerCallResult(vault, classicRewardVaultAbi, "feeHook", [], hook); + registerCallResult(vault, classicRewardVaultAbi, "poolId", [], poolId); + registerCallResult( + vault, + classicRewardVaultAbi, + "configurationHash", + [], + configurationHash, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "activeConfigurationHash", + [], + activeConfigurationHash, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "configurationEpoch", + [], + 1n, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "beneficiaryCount", + [], + 1n, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "totalCreatorFeesReceived", + [], + 90n, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "totalCreatorFeesClaimed", + [], + 0n, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "beneficiaryAt", + [0n], + BENEFICIARY, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "shareBpsAt", + [0n], + 10_000, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "claimable", + [BENEFICIARY], + 90n, + ); + registerCallResult( + vault, + classicRewardVaultAbi, + "claimedBy", + [BENEFICIARY], + 0n, + ); + } + + const allLogs = Object.freeze([ + ...launcherLogs, + ...hookLogs, + ...factoryLogs, + ...swapLogs, + ]); + const runtimeHashes = new Map(); + runtimeHashes.set( + getAddress(app.classicCtoAuthorityV1!).toLowerCase(), + app.runtimeCodeHashes!.classicCtoAuthorityV1! as Hex, + ); + runtimeHashes.set( + launcher.toLowerCase(), + app.runtimeCodeHashes!.memeLaunchV2! as Hex, + ); + runtimeHashes.set( + hook.toLowerCase(), + app.runtimeCodeHashes!.ethCreatorFeeHookV3! as Hex, + ); + runtimeHashes.set( + vaultFactory.toLowerCase(), + app.runtimeCodeHashes!.classicRewardVaultFactoryV1! as Hex, + ); + runtimeHashes.set( + getAddress(app.classicInitialBuyVestingWalletFactoryV1!).toLowerCase(), + app.runtimeCodeHashes!.classicInitialBuyVestingWalletFactoryV1! as Hex, + ); + runtimeHashes.set( + getAddress(app.classicLaunchPolicyV1!).toLowerCase(), + app.runtimeCodeHashes!.classicLaunchPolicyV1! as Hex, + ); + runtimeHashes.set( + getAddress(app.ethCreatorFeeHookFactoryV3!).toLowerCase(), + app.runtimeCodeHashes!.ethCreatorFeeHookFactoryV3! as Hex, + ); + runtimeHashes.set( + getAddress(app.lockedPositionFeeForwarderFactory!).toLowerCase(), + app.runtimeCodeHashes!.lockedPositionFeeForwarderFactory! as Hex, + ); + runtimeHashes.set( + poolManager.toLowerCase(), + dependencies.contracts.poolManager.runtimeCodeHash as Hex, + ); + runtimeHashes.set( + stateView.toLowerCase(), + dependencies.contracts.stateView.runtimeCodeHash as Hex, + ); + const budget = { physical: 0, logical: 0 }; + const corpusPageSizes: number[] = []; + const timestampBatchSizes: number[] = []; + const charge = (physical: number, logical: number) => { + budget.physical += physical; + budget.logical += logical; + if (budget.physical > 512) { + throw new Error(`physical request budget exceeded: ${budget.physical}`); + } + }; + const batchCharge = (logical: number) => + charge(Math.ceil(logical / 32), logical); + + const rpcAtDepth = (depth: number): ExactBlockRpcClient => Object.freeze({ + endpointCommitment: `0x${"01".repeat(32)}`, + endpointOriginCommitment: `0x${"02".repeat(32)}`, + requestCount: () => budget.physical, + logicalRequestCount: () => budget.logical, + createPartitionClient: (binding) => { + if (depth === 0) { + corpusPageSizes.push(binding.endIndexExclusive - binding.startIndex); + } + return rpcAtDepth(depth + 1); + }, + assertCheckpoint: async () => { + charge(1, 1); + return 1_700_000_000n; + }, + call: async () => { + throw new Error("unexpected single call"); + }, + callMany: async ({ calls, blockHash }) => { + expect(blockHash).toBe(CHECKPOINT_HASH); + batchCharge(calls.length); + return Object.freeze(calls.map((call) => { + const resolved = callResults.get( + `${call.to.toLowerCase()}:${call.data.toLowerCase()}`, + ); + if (!resolved) throw new Error(`missing call result ${call.to}:${call.data}`); + return resolved; + })); + }, + getCodeHash: async ({ address, blockHash }) => { + expect(blockHash).toBe(CHECKPOINT_HASH); + charge(1, 1); + const resolved = runtimeHashes.get(address.toLowerCase()); + if (!resolved) throw new Error(`missing runtime ${address}`); + return resolved; + }, + getLogs: async ({ addresses, topics, fromBlock, toBlock }) => { + charge(1, 1); + const requestedAddresses = new Set( + (Array.isArray(addresses) ? addresses : [addresses]) + .map((address) => address.toLowerCase()), + ); + return Object.freeze(allLogs.filter((entry) => + requestedAddresses.has(entry.address.toLowerCase()) && + entry.blockNumber >= fromBlock && + entry.blockNumber <= toBlock && + topicsMatch(entry.topics, topics) + )); + }, + getBlockTimestamp: async ({ blockNumber, expectedHash }) => { + charge(1, 1); + if (blockHashes.get(blockNumber.toString()) !== expectedHash) { + throw new Error("timestamp hash mismatch"); + } + return 1_700_000_000n + blockNumber - startBlock; + }, + getBlockTimestamps: async ({ blocks }) => { + batchCharge(blocks.length); + timestampBatchSizes.push(blocks.length); + return Object.freeze(blocks.map(({ blockNumber, expectedHash }) => { + if (blockHashes.get(blockNumber.toString()) !== expectedHash) { + throw new Error("timestamp hash mismatch"); + } + return 1_700_000_000n + blockNumber - startBlock; + })); + }, + getTransactionReceipt: async ({ transactionHash }) => { + charge(1, 1); + const resolved = receipts.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing receipt"); + return resolved; + }, + getTransactionReceipts: async ({ receipts: bindings }) => { + batchCharge(bindings.length); + return Object.freeze(bindings.map(({ transactionHash }) => { + const resolved = receipts.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing receipt"); + return resolved; + })); + }, + getTransaction: async ({ transactionHash }) => { + charge(1, 1); + const resolved = transactions.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing transaction"); + return resolved; + }, + getTransactions: async ({ transactions: bindings }) => { + batchCharge(bindings.length); + return Object.freeze(bindings.map(({ transactionHash }) => { + const resolved = transactions.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing transaction"); + return resolved; + })); + }, + }); + const contract: ReconcilerPreParityContract = Object.freeze({ + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "1", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "1", + reorgGeneration: "0", + checkpointBlockNumber: checkpointBlock.toString(), + checkpointBlockHash: CHECKPOINT_HASH, + routeKeys: RECONCILER_ROUTE_KEYS, + routeContract: {}, + projectionContract: {}, + currentEntities: tokens.map((token) => ({ + entityKind: "launch", + entityKey: token.toLowerCase(), + })), + }); + const rpc = rpcAtDepth(0); + return { + rpc, + contract, + checkpointBlock, + budget, + corpusPageSizes, + timestampBatchSizes, + }; +} + +describe("Classic V3 exact route builder", () => { + it("builds all six deterministic routes from exact checkpoint evidence", async () => { + const { rpc, contract, checkpointBlock } = fixture(); + const routes = await buildClassicV3ExactBlockRoutes({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + + expect(routes.map((route) => route.routeKey)).toEqual(RECONCILER_ROUTE_KEYS); + expect(routes.every((route) => route.comparedCount === 1)).toBe(true); + expect(routes.every((route) => + (route.dto as { contractVersion: string }).contractVersion === + CLASSIC_V3_RECONCILER_ROUTE_CONTRACT + )).toBe(true); + const token = (routes[0]!.dto as { + tokens: Array>; + }).tokens[0]!; + expect(token).toMatchObject({ + releaseVersion: "classic-v3", + modelId: "classic", + tokenAddress: TOKEN.toLowerCase(), + rewardVaultAddress: VAULT.toLowerCase(), + quoteAssetAddress: ZERO_ADDRESS, + launchLogIndex: 6, + }); + }); + + it("accepts a 1-wei swap whose rounded fee is zero and emits no fee event", async () => { + const { rpc, contract, checkpointBlock } = fixture("tiny-swap"); + const routes = await buildClassicV3ExactBlockRoutes({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + + expect(routes.map((route) => route.routeKey)).toEqual(RECONCILER_ROUTE_KEYS); + expect(routes.every((route) => route.comparedCount === 1)).toBe(true); + }); + + it.each([ + ["payout-history", REPLACEMENT, "payout-change"], + ["cto-history", CTO_BENEFICIARY, "cto-activation"], + ] as const)( + "preserves old-wallet entitlements after %s", + async (mutation, currentBeneficiary, historyKind) => { + const { rpc, contract, checkpointBlock } = fixture(mutation); + const routes = await buildClassicV3ExactBlockRoutes({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + const profile = routes.find(({ routeKey }) => + routeKey === "classic-v3-profile" + )!.dto as { + rewards: Array<{ + allocations: Array>; + entitlements: Array>; + events: Array>; + }>; + }; + + expect(profile.rewards[0]!.allocations).toEqual([{ + allocationIndex: 0, + payoutAddress: currentBeneficiary.toLowerCase(), + shareBps: 10_000, + }]); + expect(profile.rewards[0]!.entitlements).toEqual([ + { + account: BENEFICIARY.toLowerCase(), + claimableWei: "90", + claimedWei: "0", + }, + { + account: currentBeneficiary.toLowerCase(), + claimableWei: "0", + claimedWei: "0", + }, + ]); + expect(profile.rewards[0]!.events.map(({ kind }) => kind)).toEqual([ + "checkpoint", + historyKind, + ]); + }, + ); + + it("keeps a fully claimed historical-only wallet in the entitlement corpus", async () => { + const { rpc, contract, checkpointBlock } = fixture( + "fully-claimed-history", + ); + const routes = await buildClassicV3ExactBlockRoutes({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + const profile = routes.find(({ routeKey }) => + routeKey === "classic-v3-profile" + )!.dto as { + rewards: Array<{ + totalCreatorFeesClaimedWei: string; + entitlements: Array>; + events: Array>; + }>; + }; + + expect(profile.rewards[0]).toMatchObject({ + totalCreatorFeesClaimedWei: "90", + entitlements: [ + { + account: BENEFICIARY.toLowerCase(), + claimableWei: "0", + claimedWei: "90", + }, + { + account: REPLACEMENT.toLowerCase(), + claimableWei: "0", + claimedWei: "0", + }, + ], + }); + expect(profile.rewards[0]!.events.map(({ kind }) => kind)).toEqual([ + "checkpoint", + "claim", + "payout-change", + ]); + }); + + it.each([ + ["runtime hash", "runtime"], + ["launch calldata", "calldata"], + ["receipt companion", "receipt"], + ["fee conservation", "fee"], + ["fee direction", "direction"], + ["reward configuration", "reward"], + ["log ordering", "log-order"], + ["missing nonzero rounded fee event", "missing-fee"], + ["extra fee event", "extra-fee"], + ["duplicate fee event", "duplicate-fee"], + ["reordered fee events", "reordered-fees"], + ] as const)("fails closed on a bad %s", async (_label, mutation) => { + const { rpc, contract, checkpointBlock } = fixture(mutation); + await expect(buildClassicV3ExactBlockRoutes({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it.each([ + ["PoolManager address", "swap-filter-address"], + ["Swap selector", "swap-filter-selector"], + ["requested pool topic", "swap-filter-pool"], + ["requested block range", "swap-filter-range"], + ] as const)( + "fails before decoding a swap outside the %s filter", + async (_label, mutation) => { + const { rpc, contract, checkpointBlock } = fixture(mutation); + await expect(buildClassicV3ExactBlockRoutes({ + rpc, + contract, + blockNumber: checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "validation_failed", + safeMetadata: { + operation: "classic-v3-swap-log-filter-binding", + }, + }); + }, + ); + + it("uses non-overlapping provider-portable 10,000-block ranges", () => { + expect(CLASSIC_V3_RECONCILER_LOG_BLOCK_RANGE).toBe(10_000n); + expect(classicV3ReconcilerBlockRanges(100n, 10_099n)).toEqual([ + { fromBlock: 100n, toBlock: 10_099n }, + ]); + expect(classicV3ReconcilerBlockRanges(100n, 10_100n)).toEqual([ + { fromBlock: 100n, toBlock: 10_099n }, + { fromBlock: 10_100n, toBlock: 10_100n }, + ]); + }); + + it("covers the current inventory and growth beyond 256 launches", () => { + expect([128, 129, 186, 256, 257, 10_000].map( + assertClassicV3ReconcilerLaunchCount, + )).toEqual([128, 129, 186, 256, 257, 10_000]); + expect(() => assertClassicV3ReconcilerLaunchCount(0)).toThrow(); + }); + + it("builds a real 257-launch three-page corpus inside one global RPC budget", async () => { + const fixture = largeCorpusFixture(257); + const routes = await buildClassicV3ExactBlockRoutes({ + rpc: fixture.rpc, + contract: fixture.contract, + blockNumber: fixture.checkpointBlock, + blockHash: CHECKPOINT_HASH, + signal: new AbortController().signal, + }); + + expect(routes.every(({ comparedCount }) => comparedCount === 257)).toBe(true); + expect(fixture.corpusPageSizes).toEqual([128, 128, 1]); + expect(fixture.timestampBatchSizes).toEqual([128, 128, 1]); + expect(fixture.budget).toEqual({ physical: 265, logical: 7_231 }); + expect(fixture.budget.physical).toBeLessThanOrEqual(512); + expect(fixture.budget.logical).toBeLessThanOrEqual(512 * 32); + expect(fixture.rpc.requestCount()).toBe(265); + expect(fixture.rpc.logicalRequestCount()).toBe(7_231); + }); +}); diff --git a/tests/data-pipeline/classic-v3-reconciler-route-contract.test.ts b/tests/data-pipeline/classic-v3-reconciler-route-contract.test.ts new file mode 100644 index 00000000..a9659cdf --- /dev/null +++ b/tests/data-pipeline/classic-v3-reconciler-route-contract.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + assembleReconcilerRoutesFromContributions, + assertReconcilerRouteSetForKeys, + assertClassicV3ReconcilerRouteSet, + CLASSIC_V3_RECONCILER_ROUTE_CONTRACT, +} from "../../lib/data-pipeline/classic-v3-reconciler-route-contract"; +import { + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + RECONCILER_ROUTE_KEYS, + STOCK_PAIRED_RECONCILER_ROUTE_KEYS, +} from "../../lib/data-pipeline/reconciler-preparity"; +import { + classicV3ReconcilerRouteFixture, + ROUTE_FIXTURE_ADDRESS, +} from "./classic-v3-reconciler-route-fixture"; + +describe("Classic V3 reconciler route contract", () => { + it("assembles and validates the exact six-route DTO set", () => { + const routes = classicV3ReconcilerRouteFixture(); + + expect(routes.map(({ routeKey }) => routeKey)).toEqual( + RECONCILER_ROUTE_KEYS, + ); + expect(routes.every(({ comparedCount }) => comparedCount === 1)).toBe(true); + expect(routes.every(({ dto }) => + (dto as { contractVersion: string }).contractVersion === + CLASSIC_V3_RECONCILER_ROUTE_CONTRACT + )).toBe(true); + expect(JSON.stringify(routes[0]!.dto)).toBe(JSON.stringify(routes[1]!.dto)); + expect(assertClassicV3ReconcilerRouteSet(routes)).toBe(routes); + }); + + it.each([ + { + label: "extra token fields", + routeIndex: 0, + mutate: (dto: Record) => ({ + ...dto, + tokens: [{ + ...((dto.tokens as readonly Record[])[0]!), + unreviewedMetadata: true, + }], + }), + }, + { + label: "incomplete chart state", + routeIndex: 2, + mutate: (dto: Record) => ({ + ...dto, + charts: [{ tokenAddress: ROUTE_FIXTURE_ADDRESS }], + }), + }, + { + label: "mutable reward beneficiary field", + routeIndex: 4, + mutate: (dto: Record) => { + const reward = (dto.rewards as readonly Record[])[0]!; + const allocation = ( + reward.allocations as readonly Record[] + )[0]!; + return { + ...dto, + rewards: [{ + ...reward, + allocations: [{ ...allocation, beneficiary: ROUTE_FIXTURE_ADDRESS }], + }], + }; + }, + }, + { + label: "incorrect contract version", + routeIndex: 5, + mutate: (dto: Record) => ({ + ...dto, + contractVersion: "unreviewed", + }), + }, + ])("fails closed on $label", ({ routeIndex, mutate }) => { + const routes = classicV3ReconcilerRouteFixture().map((route) => ({ + ...route, + dto: structuredClone(route.dto), + })); + routes[routeIndex] = { + ...routes[routeIndex]!, + dto: mutate(routes[routeIndex]!.dto as Record), + }; + + expect(() => assertClassicV3ReconcilerRouteSet(routes)).toThrow(); + }); + + it("rejects divergence between list and token detail bytes", () => { + const routes = classicV3ReconcilerRouteFixture().map((route) => ({ + ...route, + dto: structuredClone(route.dto), + })); + const detail = routes[1]!.dto as { + contractVersion: string; + tokens: Array>; + }; + detail.tokens[0]!.name = "Different"; + + expect(() => assertClassicV3ReconcilerRouteSet(routes)).toThrow(); + }); + + it("assembles only the four applicable Classic V2 routes", () => { + const fixture = classicV3ReconcilerRouteFixture(); + const sourceToken = structuredClone( + (fixture[0]!.dto as { tokens: Array> }).tokens[0]!, + ); + const sourceChart = structuredClone( + (fixture[2]!.dto as { charts: Array> }).charts[0]!, + ); + sourceToken.releaseVersion = "classic-v2"; + sourceToken.rewardVaultAddress = null; + sourceChart.releaseVersion = "classic-v2"; + + const routes = assembleReconcilerRoutesFromContributions([{ + tokens: [sourceToken] as never, + charts: [sourceChart] as never, + }]); + + expect(routes.map(({ routeKey }) => routeKey)).toEqual( + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + ); + expect( + assertReconcilerRouteSetForKeys( + routes, + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + ), + ).toBe(routes); + }); + + it("assembles Stock routes without manufacturing Classic rewards", () => { + const fixture = classicV3ReconcilerRouteFixture(); + const sourceToken = structuredClone( + (fixture[0]!.dto as { tokens: Array> }).tokens[0]!, + ); + const sourceChart = structuredClone( + (fixture[2]!.dto as { charts: Array> }).charts[0]!, + ); + const quoteAsset = `0x${"ab".repeat(20)}`; + sourceToken.releaseVersion = "stock-paired-v3"; + sourceToken.modelId = "stock-paired"; + sourceToken.quoteAssetAddress = quoteAsset; + sourceChart.releaseVersion = "stock-paired-v3"; + sourceChart.modelId = "stock-paired"; + sourceChart.quoteAssetAddress = quoteAsset; + (sourceChart.volume as Record).quoteAssetAddress = + quoteAsset; + + const routes = assembleReconcilerRoutesFromContributions([{ + tokens: [sourceToken] as never, + charts: [sourceChart] as never, + }]); + + expect(routes.map(({ routeKey }) => routeKey)).toEqual( + STOCK_PAIRED_RECONCILER_ROUTE_KEYS, + ); + expect(routes.some(({ routeKey }) => routeKey === "classic-v3-profile")) + .toBe(false); + }); +}); diff --git a/tests/data-pipeline/classic-v3-reconciler-route-fixture.ts b/tests/data-pipeline/classic-v3-reconciler-route-fixture.ts new file mode 100644 index 00000000..46ece32e --- /dev/null +++ b/tests/data-pipeline/classic-v3-reconciler-route-fixture.ts @@ -0,0 +1,139 @@ +import type { CanonicalJsonValue } from "../../lib/data-pipeline/canonical-fingerprint"; +import { + assembleClassicV3ReconcilerRoutes, + type ClassicV3ReconcilerRouteParts, +} from "../../lib/data-pipeline/classic-v3-reconciler-route-contract"; + +export const ROUTE_FIXTURE_ADDRESS = `0x${"11".repeat(20)}`; +export const ROUTE_FIXTURE_CREATOR = `0x${"22".repeat(20)}`; +export const ROUTE_FIXTURE_HOOK = `0x${"33".repeat(20)}`; +export const ROUTE_FIXTURE_VAULT = `0x${"44".repeat(20)}`; +export const ROUTE_FIXTURE_RECIPIENT = `0x${"55".repeat(20)}`; +export const ROUTE_FIXTURE_TRANSACTION = `0x${"66".repeat(32)}`; +export const ROUTE_FIXTURE_BLOCK_HASH = `0x${"77".repeat(32)}`; +export const ROUTE_FIXTURE_POOL = `0x${"88".repeat(32)}`; +export const ROUTE_FIXTURE_LAUNCH_HASH = `0x${"99".repeat(32)}`; + +const token: CanonicalJsonValue = { + releaseVersion: "classic-v3", + modelId: "classic", + tokenAddress: ROUTE_FIXTURE_ADDRESS, + creatorAddress: ROUTE_FIXTURE_CREATOR, + launchTransactionHash: ROUTE_FIXTURE_TRANSACTION, + launchBlockNumber: "25700000", + launchTransactionIndex: 3, + launchLogIndex: 4, + launchedAt: "2026-07-31T12:34:56.000Z", + poolId: ROUTE_FIXTURE_POOL, + hookAddress: ROUTE_FIXTURE_HOOK, + rewardVaultAddress: ROUTE_FIXTURE_VAULT, + positionRecipient: ROUTE_FIXTURE_RECIPIENT, + positionTokenId: "42", + launchHash: ROUTE_FIXTURE_LAUNCH_HASH, + name: "Fixture Token", + symbol: "FIX", + decimals: 18, + totalSupplyRaw: "1000000000000000000000000000", + quoteAssetAddress: `0x${"00".repeat(20)}`, + fees: { + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + }, + liquidity: { + tokenLiquidityAmountRaw: "999999999999999999999999999", + lockedTokenDustRaw: "1", + initialTick: 120, + tickLower: -887220, + tickUpper: 120, + }, +}; + +const chart: CanonicalJsonValue = { + releaseVersion: "classic-v3", + modelId: "classic", + tokenAddress: ROUTE_FIXTURE_ADDRESS, + poolId: ROUTE_FIXTURE_POOL, + quoteAssetAddress: `0x${"00".repeat(20)}`, + state: { + blockNumber: "25700001", + blockHash: ROUTE_FIXTURE_BLOCK_HASH, + transactionHash: ROUTE_FIXTURE_TRANSACTION, + transactionIndex: 5, + logIndex: 8, + sqrtPriceX96: "79228162514264337593543950336", + liquidity: "123456789", + tick: 0, + lpFeePips: 0, + }, + volume: { + quoteAssetAddress: `0x${"00".repeat(20)}`, + grossQuoteRaw: "1000000000000000000", + creatorFeeQuoteRaw: "9000000000000000", + launcherFeeQuoteRaw: "1000000000000000", + }, +}; + +const reward: CanonicalJsonValue = { + releaseVersion: "classic-v3", + modelId: "classic", + vaultAddress: ROUTE_FIXTURE_VAULT, + poolId: ROUTE_FIXTURE_POOL, + tokenAddress: ROUTE_FIXTURE_ADDRESS, + tokenName: "Fixture Token", + tokenSymbol: "FIX", + launchTransactionHash: ROUTE_FIXTURE_TRANSACTION, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + launcherFeeBps: 10, + configurationHash: ROUTE_FIXTURE_LAUNCH_HASH, + activeConfigurationHash: ROUTE_FIXTURE_BLOCK_HASH, + configurationEpoch: "1", + totalCreatorFeesReceivedWei: "9000000000000000", + totalCreatorFeesClaimedWei: "0", + pendingCreatorFeesWei: "0", + allocations: [{ + allocationIndex: 0, + payoutAddress: ROUTE_FIXTURE_RECIPIENT, + shareBps: 10000, + }], + entitlements: [{ + account: ROUTE_FIXTURE_RECIPIENT, + claimableWei: "9000000000000000", + claimedWei: "0", + }], + events: [], +}; + +export const CLASSIC_V3_RECONCILER_ROUTE_FIXTURE_PARTS: + ClassicV3ReconcilerRouteParts = Object.freeze({ + tokens: Object.freeze([token]), + charts: Object.freeze([chart]), + profiles: Object.freeze([{ + account: ROUTE_FIXTURE_CREATOR, + tokens: [{ + releaseVersion: "classic-v3", + modelId: "classic", + tokenAddress: ROUTE_FIXTURE_ADDRESS, + launchTransactionHash: ROUTE_FIXTURE_TRANSACTION, + }], + }]), + rewards: Object.freeze([reward]), + launches: Object.freeze([{ + releaseVersion: "classic-v3", + modelId: "classic", + account: ROUTE_FIXTURE_CREATOR, + launchTransactionHash: ROUTE_FIXTURE_TRANSACTION, + tokenAddress: ROUTE_FIXTURE_ADDRESS, + }]), + }); + +export function classicV3ReconcilerRouteFixture() { + return assembleClassicV3ReconcilerRoutes( + CLASSIC_V3_RECONCILER_ROUTE_FIXTURE_PARTS, + ); +} diff --git a/tests/data-pipeline/classic-v3-reward-commitments.test.ts b/tests/data-pipeline/classic-v3-reward-commitments.test.ts new file mode 100644 index 00000000..66b6e99a --- /dev/null +++ b/tests/data-pipeline/classic-v3-reward-commitments.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; + +import { + classicV3InitialRewardCommitments, + type ClassicV3InitialRewardCommitmentInput, +} from "../../lib/data-pipeline/classic-v3-reward-commitments"; + +const address = (nibble: string) => + `0x${nibble.repeat(40)}` as `0x${string}`; +const bytes32 = (nibble: string) => + `0x${nibble.repeat(64)}` as `0x${string}`; + +const baseline: ClassicV3InitialRewardCommitmentInput = Object.freeze({ + vault: address("1"), + feeHook: address("2"), + poolId: bytes32("3"), + ctoAuthority: address("4"), + salt: bytes32("5"), + factoryConfigurationHash: bytes32("6"), + beneficiaries: Object.freeze([address("7"), address("8")]), + sharesBps: Object.freeze([4_000, 6_000]), +}); + +describe("Classic v3 initial reward commitments", () => { + it("matches the contract ABI order for the canonical vector", () => { + expect(classicV3InitialRewardCommitments(baseline)).toEqual({ + factoryInputCommitment: + "0x60fa193c7de2796505b6bee8623a85680fd79c0de25b3138302914bfa9d6d83b", + constructorArgumentsCommitment: + "0x004927420bbcd0a24eebea2076f4c5a928c8324602eb520ae45bb01657bf0b62", + initialActiveConfigurationHash: + "0x9af5eff7e1dc6804f7d9b822f682aa48f3ea2f8d4a7cfa0dcf359bf53193dc39", + }); + }); + + it.each([ + ["salt", { salt: bytes32("9") }, ["factoryInputCommitment"]], + [ + "fee hook", + { feeHook: address("9") }, + ["factoryInputCommitment", "constructorArgumentsCommitment"], + ], + [ + "pool id", + { poolId: bytes32("9") }, + ["factoryInputCommitment", "constructorArgumentsCommitment"], + ], + [ + "CTO authority", + { ctoAuthority: address("9") }, + ["constructorArgumentsCommitment"], + ], + [ + "vault", + { vault: address("9") }, + ["initialActiveConfigurationHash"], + ], + [ + "factory configuration hash", + { factoryConfigurationHash: bytes32("9") }, + ["initialActiveConfigurationHash"], + ], + [ + "beneficiary order", + { beneficiaries: [address("8"), address("7")] }, + [ + "factoryInputCommitment", + "constructorArgumentsCommitment", + "initialActiveConfigurationHash", + ], + ], + [ + "share order", + { sharesBps: [6_000, 4_000] }, + [ + "factoryInputCommitment", + "constructorArgumentsCommitment", + "initialActiveConfigurationHash", + ], + ], + ] as const)("changes the bound commitment when %s changes", ( + _field, + mutation, + changedCommitments, + ) => { + const original = classicV3InitialRewardCommitments(baseline); + const changed = classicV3InitialRewardCommitments({ + ...baseline, + ...mutation, + }); + + for (const key of changedCommitments) { + expect(changed[key]).not.toBe(original[key]); + } + }); +}); diff --git a/tests/data-pipeline/core.test.ts b/tests/data-pipeline/core.test.ts new file mode 100644 index 00000000..62dc7838 --- /dev/null +++ b/tests/data-pipeline/core.test.ts @@ -0,0 +1,511 @@ +import { describe, expect, it, vi } from "vitest"; +import { rootCertificates } from "node:tls"; + +vi.mock("server-only", () => ({})); + +import { + CACHE_POLICIES, + cachePolicyForRead, + provenanceHeaders, +} from "../../lib/data-pipeline/cache"; +import { CircuitBreaker } from "../../lib/data-pipeline/circuit"; +import { + addressFromBytea, + bytes32FromBytea, + canonicalAddress, + canonicalBytes32, + canonicalRawData, + canonicalSelector, + parseUint256Text, +} from "../../lib/data-pipeline/codecs"; +import { + INDEXED_ROUTE_FLAG_NAMES, + loadDataPipelineConfig, +} from "../../lib/data-pipeline/config"; +import { + DataPipelineError, + dataPipelineError, +} from "../../lib/data-pipeline/errors"; +import { boundedJsonRequest } from "../../lib/data-pipeline/request"; + +describe("data-pipeline configuration", () => { + it("keeps every indexed route off while retaining fail-closed parity and live fallback", () => { + const config = loadDataPipelineConfig({}); + + for (const flag of INDEXED_ROUTE_FLAG_NAMES) { + expect(config.flags[flag]).toBe(false); + } + expect(config.flags.INDEXED_READ_SHADOW_COMPARE_ENABLED).toBe(false); + expect(config.flags.INDEXED_READ_REQUIRE_PARITY_ENABLED).toBe(true); + expect(config.flags.INDEXED_READ_LIVE_FALLBACK_ENABLED).toBe(true); + }); + + it("accepts only exact boolean spellings and bounded server-only settings", () => { + const config = loadDataPipelineConfig({ + INDEXED_EXPLORE_LIST_READS_ENABLED: "true", + INDEXED_READ_LIVE_FALLBACK_ENABLED: "false", + PROGRAMMABLE_POSTGRES_MAX_CONNECTIONS: "4", + PROGRAMMABLE_POSTGRES_CONNECT_TIMEOUT_MS: "900", + PROGRAMMABLE_API_READER_DATABASE_URL: + "postgres://postgres.project:password@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full", + PROGRAMMABLE_RELEASE_PROBE_DATABASE_URL: + "postgres://probe.project:password@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full", + PROGRAMMABLE_POSTGRES_SSL_CA_PEM: rootCertificates[0], + PROGRAMMABLE_ENVIO_GRAPHQL_URL: "https://envio.example/graphql", + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: "https://gateway.thegraph.com", + UNISWAP_V4_SUBGRAPH_API_KEY: "legacy-server-only-graph-key", + }); + + expect(config.flags.INDEXED_EXPLORE_LIST_READS_ENABLED).toBe(true); + expect(config.flags.INDEXED_READ_LIVE_FALLBACK_ENABLED).toBe(false); + expect(config.postgres.maxConnections).toBe(4); + expect(config.postgres.connectTimeoutMs).toBe(900); + expect(config.postgres.sslCaPem).toBe(rootCertificates[0]); + expect(config.postgres.releaseProbeConnectionString).toContain( + "probe.project", + ); + expect(config.envio.endpoint).toBe("https://envio.example/graphql"); + expect(config.uniswap.gatewayBaseUrl).toBe( + "https://gateway.thegraph.com", + ); + expect(config.uniswap.apiKey).toBe("legacy-server-only-graph-key"); + + expect(() => + loadDataPipelineConfig({ + INDEXED_EXPLORE_LIST_READS_ENABLED: "1", + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + NEXT_PUBLIC_PROGRAMMABLE_RELEASE_PROBE_DATABASE_URL: + "postgres://probe:secret@example.invalid:5432/db?sslmode=verify-full", + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + NEXT_PUBLIC_PROGRAMMABLE_SHADOW_PROBE_TOKEN: "x".repeat(48), + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + NEXT_PUBLIC_UNISWAP_V4_SUBGRAPH_API_KEY: "public-graph-key", + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + PROGRAMMABLE_POSTGRES_MAX_CONNECTIONS: "6", + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://secret:password@envio.example/graphql", + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + PROGRAMMABLE_POSTGRES_SSL_CA_PEM: "not-a-certificate", + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + PROGRAMMABLE_POSTGRES_SSL_CA_PEM: rootCertificates[0], + }), + ).toThrowError(DataPipelineError); + expect(() => + loadDataPipelineConfig({ + PROGRAMMABLE_API_READER_DATABASE_URL: + "postgres://postgres.project:password@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full", + }), + ).toThrowError(DataPipelineError); + }); + + it.each(["NODE_ENV", "VERCEL_ENV"] as const)( + "rejects disabled parity when %s marks a production runtime", + (productionMarker) => { + expect(() => + loadDataPipelineConfig({ + [productionMarker]: "production", + INDEXED_READ_REQUIRE_PARITY_ENABLED: "false", + }), + ).toThrowError(DataPipelineError); + }, + ); + + it("allows operators to disable parity outside production", () => { + const config = loadDataPipelineConfig({ + NODE_ENV: "development", + VERCEL_ENV: "preview", + INDEXED_READ_REQUIRE_PARITY_ENABLED: "false", + }); + + expect(config.flags.INDEXED_READ_REQUIRE_PARITY_ENABLED).toBe(false); + }); + + it("rejects disabled parity when the trusted process environment is production", () => { + vi.stubEnv("VERCEL_ENV", "production"); + try { + expect(() => + loadDataPipelineConfig({ + INDEXED_READ_REQUIRE_PARITY_ENABLED: "false", + }), + ).toThrowError(DataPipelineError); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("accepts only the official Graph gateway base in production", () => { + expect( + loadDataPipelineConfig({ + NODE_ENV: "production", + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: + "https://gateway.thegraph.com/", + }).uniswap.gatewayBaseUrl, + ).toBe("https://gateway.thegraph.com"); + + for (const gatewayBaseUrl of [ + "https://graph-proxy.example", + "https://gateway.thegraph.com.evil.example", + "https://gateway.thegraph.com/custom-proxy", + ]) { + expect(() => + loadDataPipelineConfig({ + VERCEL_ENV: "production", + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: gatewayBaseUrl, + }), + ).toThrowError(DataPipelineError); + } + }); + + it("retains custom HTTPS Graph gateways outside production", () => { + const config = loadDataPipelineConfig({ + NODE_ENV: "test", + VERCEL_ENV: "preview", + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: + "https://graph-proxy.example/custom-base/", + }); + + expect(config.uniswap.gatewayBaseUrl).toBe( + "https://graph-proxy.example/custom-base", + ); + }); + + it("prefers the dedicated Graph key over the legacy server-only key", () => { + const config = loadDataPipelineConfig({ + PROGRAMMABLE_UNISWAP_GRAPH_API_KEY: "dedicated-server-key", + UNISWAP_V4_SUBGRAPH_API_KEY: "legacy-server-key", + }); + + expect(config.uniswap.apiKey).toBe("dedicated-server-key"); + }); + + it("rejects a custom Graph gateway when the trusted process environment is production", () => { + vi.stubEnv("VERCEL_ENV", "production"); + try { + expect(() => + loadDataPipelineConfig({ + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: + "https://graph-proxy.example", + }), + ).toThrowError(DataPipelineError); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("rejects browser-prefixed credential paths without echoing a secret", () => { + const secret = "do-not-echo-this-token"; + let thrown: unknown; + try { + loadDataPipelineConfig({ + NEXT_PUBLIC_PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN: secret, + }); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(DataPipelineError); + expect(String(thrown)).not.toContain(secret); + expect(JSON.stringify(thrown)).not.toContain(secret); + expect(() => + loadDataPipelineConfig({ + NEXT_PUBLIC_PROGRAMMABLE_POSTGRES_SSL_CA_PEM: + rootCertificates[0], + }), + ).toThrowError(DataPipelineError); + }); +}); + +describe("strict hex and numeric codecs", () => { + it("canonicalizes known fixed-width vectors and preserves leading zeroes", () => { + expect( + canonicalAddress(`0x${"00".repeat(19)}A1`), + ).toBe(`0x${"00".repeat(19)}a1`); + expect( + canonicalBytes32(`0x${"00".repeat(31)}Ff`), + ).toBe(`0x${"00".repeat(31)}ff`); + expect(canonicalSelector("0xBF388406")).toBe("0xbf388406"); + expect(canonicalRawData("0x")).toBe("0x"); + + expect(addressFromBytea(Uint8Array.from([1, ...Array(19).fill(0)]))).toBe( + `0x01${"00".repeat(19)}`, + ); + expect(bytes32FromBytea(`\\x${"ab".repeat(32)}`)).toBe( + `0x${"ab".repeat(32)}`, + ); + }); + + it("rejects malformed, odd, missing-prefix, and wrong-width values", () => { + for (const value of [ + "", + "00", + "0x0", + "0xzz", + `0x${"11".repeat(19)}`, + `0x${"11".repeat(21)}`, + ]) { + expect(() => canonicalAddress(value)).toThrowError(DataPipelineError); + } + expect(() => canonicalRawData("")).toThrowError(DataPipelineError); + expect(() => canonicalRawData("0x0")).toThrowError(DataPipelineError); + expect(() => canonicalRawData("0xgg")).toThrowError(DataPipelineError); + expect(() => canonicalBytes32("0x")).toThrowError(DataPipelineError); + expect(() => canonicalSelector("0x1234")).toThrowError(DataPipelineError); + }); + + it("parses uint256 as text without entering JavaScript number space", () => { + const maximum = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; + expect(parseUint256Text(maximum)).toBe(maximum); + expect(parseUint256Text("0001")).toBe("1"); + expect(() => + parseUint256Text( + "115792089237316195423570985008687907853269984665640564039457584007913129639936", + ), + ).toThrowError(DataPipelineError); + expect(() => parseUint256Text("-1")).toThrowError(DataPipelineError); + expect(() => parseUint256Text("1e18")).toThrowError(DataPipelineError); + }); +}); + +describe("bounded request helper", () => { + it("aborts the entire request at the exact timeout and makes one attempt", async () => { + vi.useFakeTimers(); + try { + const fetcher = vi.fn( + async (_url: string, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => { + reject(new DOMException("Aborted", "AbortError")); + }); + }), + ); + + const request = boundedJsonRequest({ + dependency: "envio", + endpoint: "https://envio.example/graphql", + timeoutMs: 2_000, + maximumBodyBytes: 1024, + fetcher, + body: { query: "query Ready { ready }" }, + }); + const rejection = expect(request).rejects.toMatchObject({ + code: "timeout", + dependency: "envio", + retryable: true, + }); + await vi.advanceTimersByTimeAsync(1_999); + expect(fetcher).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(1); + + await rejection; + expect(fetcher).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); + + it("rejects declared and streamed oversized bodies", async () => { + await expect( + boundedJsonRequest({ + dependency: "uniswap", + endpoint: "https://gateway.thegraph.com/api/subgraphs/id/fixed", + timeoutMs: 100, + maximumBodyBytes: 16, + fetcher: async () => + new Response("{}", { + status: 200, + headers: { "content-length": "17" }, + }), + body: {}, + }), + ).rejects.toMatchObject({ code: "response_oversize" }); + + await expect( + boundedJsonRequest({ + dependency: "uniswap", + endpoint: "https://gateway.thegraph.com/api/subgraphs/id/fixed", + timeoutMs: 100, + maximumBodyBytes: 16, + fetcher: async () => + new Response(JSON.stringify({ value: "x".repeat(32) }), { + status: 200, + }), + body: {}, + }), + ).rejects.toMatchObject({ code: "response_oversize" }); + }); + + it("rejects invalid JSON and GraphQL errors without exposing endpoint data", async () => { + await expect( + boundedJsonRequest({ + dependency: "envio", + endpoint: "https://envio.example/graphql?token=secret", + timeoutMs: 100, + maximumBodyBytes: 1024, + fetcher: async () => new Response("{", { status: 200 }), + body: {}, + }), + ).rejects.toMatchObject({ code: "invalid_json" }); + + let thrown: unknown; + try { + await boundedJsonRequest({ + dependency: "envio", + endpoint: "https://envio.example/graphql?token=secret", + timeoutMs: 100, + maximumBodyBytes: 1024, + fetcher: async () => + new Response( + JSON.stringify({ + data: { candidate: null }, + errors: [{ message: "database password=secret" }], + }), + { status: 200 }, + ), + body: {}, + }); + } catch (error) { + thrown = error; + } + expect(thrown).toMatchObject({ code: "graphql_error" }); + expect(String(thrown)).not.toContain("secret"); + expect(JSON.stringify(thrown)).not.toContain("secret"); + }); +}); + +describe("independent circuit breaker", () => { + it("opens after three counted failures, admits one half-open probe, and closes on success", async () => { + let now = 1_000; + const circuit = new CircuitBreaker({ + dependency: "envio", + now: () => now, + }); + const dependencyFailure = () => + Promise.reject( + dataPipelineError({ + dependency: "envio", + code: "dependency_unavailable", + retryable: true, + countsTowardCircuit: true, + }), + ); + + for (let attempt = 0; attempt < 3; attempt += 1) { + await expect(circuit.execute(dependencyFailure)).rejects.toBeInstanceOf( + DataPipelineError, + ); + } + expect(circuit.snapshot()).toMatchObject({ + state: "open", + consecutiveFailures: 3, + }); + await expect(circuit.execute(async () => "blocked")).rejects.toMatchObject({ + code: "circuit_open", + }); + + now += 30_000; + let releaseProbe: (() => void) | undefined; + const probe = circuit.execute( + () => + new Promise((resolve) => { + releaseProbe = () => resolve("ready"); + }), + ); + await expect(circuit.execute(async () => "second")).rejects.toMatchObject({ + code: "circuit_open", + }); + releaseProbe?.(); + await expect(probe).resolves.toBe("ready"); + expect(circuit.snapshot()).toEqual({ + state: "closed", + consecutiveFailures: 0, + openUntil: 0, + halfOpenProbeActive: false, + }); + }); + + it("does not share state and does not count caller validation failures", async () => { + const first = new CircuitBreaker({ dependency: "postgres" }); + const second = new CircuitBreaker({ dependency: "postgres" }); + const callerFailure = () => + Promise.reject( + dataPipelineError({ + dependency: "postgres", + code: "invalid_input", + retryable: false, + countsTowardCircuit: false, + }), + ); + + for (let attempt = 0; attempt < 4; attempt += 1) { + await expect(first.execute(callerFailure)).rejects.toBeInstanceOf( + DataPipelineError, + ); + } + expect(first.snapshot().state).toBe("closed"); + expect(second.snapshot()).toEqual({ + state: "closed", + consecutiveFailures: 0, + openUntil: 0, + halfOpenProbeActive: false, + }); + }); +}); + +describe("cache and provenance policy", () => { + it("keeps public reads bounded and sensitive reads private no-store", () => { + expect(cachePolicyForRead("explore-list")).toEqual(CACHE_POLICIES.public); + expect(cachePolicyForRead("token-detail")).toEqual(CACHE_POLICIES.public); + expect(cachePolicyForRead("chart")).toEqual(CACHE_POLICIES.public); + for (const kind of [ + "account-rewards", + "claimability", + "launch-confirmation", + "transaction-adjacent", + ] as const) { + expect(cachePolicyForRead(kind)).toEqual(CACHE_POLICIES.private); + } + }); + + it("emits only the approved bounded provenance headers", () => { + expect( + provenanceHeaders({ + source: "indexed", + projectionBlock: "25650000", + projectionHash: `0x${"ab".repeat(32)}`, + projectionLag: 2, + reconciledAt: "2026-07-31T08:00:00.000Z", + releaseVersion: "classic-v3", + }), + ).toEqual({ + "X-Programmable-Read-Source": "indexed", + "X-Programmable-Projection-Block": "25650000", + "X-Programmable-Projection-Hash": `0x${"ab".repeat(32)}`, + "X-Programmable-Projection-Lag": "2", + "X-Programmable-Reconciled-At": "2026-07-31T08:00:00.000Z", + "X-Programmable-Release-Version": "classic-v3", + }); + }); +}); diff --git a/tests/data-pipeline/dual-rpc.test.ts b/tests/data-pipeline/dual-rpc.test.ts new file mode 100644 index 00000000..6c62de2b --- /dev/null +++ b/tests/data-pipeline/dual-rpc.test.ts @@ -0,0 +1,1344 @@ +import { describe, expect, it, vi } from "vitest"; +import { encodeAbiParameters, keccak256 } from "viem"; + +vi.mock("server-only", () => ({})); + +const { TEST_SOURCE_CODE_HASH } = vi.hoisted(() => ({ + TEST_SOURCE_CODE_HASH: + "0xcf61a6eb3b9b89e75f1dadf3dcd16509616896cb50eac765a68fa27bbbc6de82" as const, +})); + +vi.mock( + "../../lib/data-pipeline/release-binding.server", + async (importOriginal) => { + const original = await importOriginal< + typeof import("../../lib/data-pipeline/release-binding.server") + >(); + const binding = original.getDataPipelineReleaseBinding(); + return { + ...original, + getDataPipelineReleaseBinding: () => ({ + ...binding, + sources: binding.sources.map((source) => + source.contractName === "ClassicV2Launcher" || + source.contractName === "StockV2V3Hook" || + source.contractName === "StockV2V3RewardVaultFactory" + ? { ...source, runtimeCodeHash: TEST_SOURCE_CODE_HASH } + : source, + ), + }), + }; + }, +); + +import { + readDualRpcSafeHead, + readDualRpcTokenMetadata, + verifyEnvioCandidateBatchWithDualRpc, + verifyEnvioCandidateWithDualRpc, + type CandidateRpcClient, + type CandidateRpcReceipt, +} from "../../lib/data-pipeline/dual-rpc"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import { rpcProviderCommitment } from "../../lib/data-pipeline/rpc-provider-commitments"; + +const BLOCK_HASH = `0x${"11".repeat(32)}` as const; +const SAFE_BLOCK_HASH = `0x${"22".repeat(32)}` as const; +const TRANSACTION_HASH = `0x${"33".repeat(32)}` as const; +const SOURCE = "0xd240d06f8586eb799f20056054e5b527405e6bad" as const; +const TOPIC = `0x${"55".repeat(32)}` as const; +const RAW_DATA = "0x1234" as const; +const CANDIDATE_BLOCK = 25_624_131n; +const SAFE_BLOCK = CANDIDATE_BLOCK + 3n; +const PROVIDER_HEAD = SAFE_BLOCK + 12n; +const PAYLOAD_HASH = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [[TOPIC], RAW_DATA], + ), +); +const DYNAMIC_BLOCK_HASH = `0x${"66".repeat(32)}` as const; +const DYNAMIC_SAFE_BLOCK_HASH = `0x${"77".repeat(32)}` as const; +const DYNAMIC_TRANSACTION_HASH = `0x${"99".repeat(32)}` as const; +const DYNAMIC_SOURCE = + "0x4cfe000000000000000000000000000000000001" as const; +const DYNAMIC_BLOCK = 25_639_597n; +const DYNAMIC_SAFE_BLOCK = DYNAMIC_BLOCK + 3n; +const DYNAMIC_PROVIDER_HEAD = DYNAMIC_SAFE_BLOCK + 12n; +const DYNAMIC_CODE = "0x6001600055" as const; +const SHARED_BLOCK_HASH = `0x${"aa".repeat(32)}` as const; +const SHARED_SAFE_BLOCK_HASH = `0x${"bb".repeat(32)}` as const; +const SHARED_TRANSACTION_HASH = `0x${"cc".repeat(32)}` as const; +const SHARED_BLOCK = 25_640_338n; + +function candidate(): EnvioCandidate { + return { + candidateId: `1:${BLOCK_HASH}:${TRANSACTION_HASH}:7`, + chainId: 1, + blockNumber: CANDIDATE_BLOCK.toString(), + blockHash: BLOCK_HASH, + blockTimestamp: "1785480000", + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + blockGlobalLogIndex: 7, + sourceAddress: SOURCE, + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + releaseHint: { model: "classic", releaseVersion: "classic-v2" }, + orderedTopics: [TOPIC], + rawData: RAW_DATA, + decodedPayload: {}, + payloadHash: PAYLOAD_HASH, + }; +} + +function dynamicCandidate(): EnvioCandidate { + return { + candidateId: `1:${DYNAMIC_BLOCK_HASH}:${DYNAMIC_TRANSACTION_HASH}:4`, + chainId: 1, + blockNumber: DYNAMIC_BLOCK.toString(), + blockHash: DYNAMIC_BLOCK_HASH, + blockTimestamp: "1785481000", + transactionHash: DYNAMIC_TRANSACTION_HASH, + transactionIndex: 1, + blockGlobalLogIndex: 4, + sourceAddress: DYNAMIC_SOURCE, + contractName: "ClassicV3RewardVault", + eventName: "Claimed", + releaseHint: { model: "unresolved", releaseVersion: "unresolved" }, + orderedTopics: [TOPIC], + rawData: RAW_DATA, + decodedPayload: {}, + payloadHash: PAYLOAD_HASH, + }; +} + +function dynamicReceipt(): CandidateRpcReceipt { + return { + status: "success", + blockNumber: DYNAMIC_BLOCK, + blockHash: DYNAMIC_BLOCK_HASH, + transactionHash: DYNAMIC_TRANSACTION_HASH, + transactionIndex: 1, + logs: [ + { + address: DYNAMIC_SOURCE, + blockNumber: DYNAMIC_BLOCK, + blockHash: DYNAMIC_BLOCK_HASH, + transactionHash: DYNAMIC_TRANSACTION_HASH, + transactionIndex: 1, + logIndex: 4, + removed: false, + topics: [TOPIC], + data: RAW_DATA, + }, + ], + }; +} + +function dynamicClient( + bytecode: `0x${string}` = DYNAMIC_CODE, +): CandidateRpcClient { + return { + getChainId: async () => 1, + getBlockNumber: async () => DYNAMIC_PROVIDER_HEAD, + getBlock: async ({ blockNumber }) => + blockNumber === DYNAMIC_SAFE_BLOCK + ? { + number: DYNAMIC_SAFE_BLOCK, + hash: DYNAMIC_SAFE_BLOCK_HASH, + timestamp: 1785481100n, + } + : { + number: DYNAMIC_BLOCK, + hash: DYNAMIC_BLOCK_HASH, + timestamp: 1785481000n, + }, + getTransactionReceipt: async () => dynamicReceipt(), + getBytecode: async () => bytecode, + }; +} + +function sharedStaticCandidate(input: { + sourceAddress: `0x${string}`; + contractName: string; + eventName: string; +}): EnvioCandidate { + return { + candidateId: + `1:${SHARED_BLOCK_HASH}:${SHARED_TRANSACTION_HASH}:9`, + chainId: 1, + blockNumber: SHARED_BLOCK.toString(), + blockHash: SHARED_BLOCK_HASH, + blockTimestamp: "1785482000", + transactionHash: SHARED_TRANSACTION_HASH, + transactionIndex: 5, + blockGlobalLogIndex: 9, + sourceAddress: input.sourceAddress, + contractName: input.contractName, + eventName: input.eventName, + releaseHint: { model: "unresolved", releaseVersion: "unresolved" }, + orderedTopics: [TOPIC], + rawData: RAW_DATA, + decodedPayload: {}, + payloadHash: PAYLOAD_HASH, + }; +} + +function sharedStaticClient( + candidate: EnvioCandidate, +): CandidateRpcClient { + const safeBlock = SHARED_BLOCK + 3n; + return { + getChainId: async () => 1, + getBlockNumber: async () => safeBlock + 12n, + getBlock: async ({ blockNumber }) => + blockNumber === safeBlock + ? { + number: safeBlock, + hash: SHARED_SAFE_BLOCK_HASH, + timestamp: 1785482100n, + } + : { + number: SHARED_BLOCK, + hash: SHARED_BLOCK_HASH, + timestamp: 1785482000n, + }, + getTransactionReceipt: async () => ({ + status: "success", + blockNumber: SHARED_BLOCK, + blockHash: SHARED_BLOCK_HASH, + transactionHash: SHARED_TRANSACTION_HASH, + transactionIndex: 5, + logs: [ + { + address: candidate.sourceAddress, + blockNumber: SHARED_BLOCK, + blockHash: SHARED_BLOCK_HASH, + transactionHash: SHARED_TRANSACTION_HASH, + transactionIndex: 5, + logIndex: 9, + removed: false, + topics: [TOPIC], + data: RAW_DATA, + }, + ], + }), + getBytecode: async () => "0x60016000", + }; +} + +function receipt(): CandidateRpcReceipt { + return { + status: "success" as const, + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logs: [ + { + address: SOURCE, + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logIndex: 6, + removed: false, + topics: [`0x${"88".repeat(32)}` as const], + data: "0x" as const, + }, + { + address: SOURCE, + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logIndex: 7, + removed: false, + topics: [TOPIC], + data: "0x1234" as const, + }, + ], + }; +} + +function client( + overrides: Partial = {}, +): CandidateRpcClient { + return { + getChainId: async () => 1, + getBlockNumber: async () => PROVIDER_HEAD, + getBlock: async ({ blockNumber }) => + blockNumber === SAFE_BLOCK + ? { + number: SAFE_BLOCK, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480003n, + } + : { + number: CANDIDATE_BLOCK, + hash: BLOCK_HASH, + timestamp: 1785480000n, + }, + getTransactionReceipt: async () => receipt(), + getBytecode: async () => "0x60016000", + ...overrides, + }; +} + +function provider(identity: string, rpcClient: CandidateRpcClient) { + const endpointOrigin = `https://${identity}.example`; + return { + identity, + vendorGroup: identity.split("-")[0]!, + endpointCommitment: rpcProviderCommitment("endpoint", endpointOrigin), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + endpointOrigin, + ), + client: rpcClient, + }; +} + +describe("dual-RPC Envio candidate verification", () => { + it("batches shared head, block, and receipt reads across candidates", async () => { + const firstClient = client(); + const secondClient = client(); + for (const rpcClient of [firstClient, secondClient]) { + rpcClient.getChainId = vi.fn(rpcClient.getChainId); + rpcClient.getBlockNumber = vi.fn(rpcClient.getBlockNumber); + rpcClient.getBlock = vi.fn(rpcClient.getBlock); + rpcClient.getTransactionReceipt = vi.fn( + rpcClient.getTransactionReceipt, + ); + rpcClient.getBytecode = vi.fn(rpcClient.getBytecode); + } + const earlier: EnvioCandidate = { + ...candidate(), + candidateId: `1:${BLOCK_HASH}:${TRANSACTION_HASH}:6`, + blockGlobalLogIndex: 6, + orderedTopics: [`0x${"88".repeat(32)}`], + rawData: "0x", + payloadHash: keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [[`0x${"88".repeat(32)}`], "0x"], + ), + ), + }; + + const batch = await verifyEnvioCandidateBatchWithDualRpc({ + candidates: [earlier, candidate()], + providers: [ + provider("alchemy-mainnet", firstClient), + provider("quicknode-mainnet", secondClient), + ], + }); + + expect(batch.candidates.map(({ receiptLogOrdinal }) => receiptLogOrdinal)).toEqual([ + 0, + 1, + ]); + for (const rpcClient of [firstClient, secondClient]) { + expect(rpcClient.getChainId).toHaveBeenCalledTimes(1); + expect(rpcClient.getBlockNumber).toHaveBeenCalledTimes(1); + expect(rpcClient.getBlock).toHaveBeenCalledTimes(2); + expect(rpcClient.getTransactionReceipt).toHaveBeenCalledTimes(1); + expect(rpcClient.getBytecode).toHaveBeenCalledTimes(1); + expect(rpcClient.getBytecode).toHaveBeenCalledWith({ + address: SOURCE, + blockHash: BLOCK_HASH, + requireCanonical: true, + }); + } + }); + + it("verifies an explicitly bounded transaction with more than 32 events", async () => { + const candidates = Array.from({ length: 40 }, (_, index) => ({ + ...candidate(), + candidateId: `1:${BLOCK_HASH}:${TRANSACTION_HASH}:${index}`, + blockGlobalLogIndex: index, + } satisfies EnvioCandidate)); + const oversizedReceipt = (): CandidateRpcReceipt => ({ + status: "success", + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logs: candidates.map((entry) => ({ + address: SOURCE, + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logIndex: entry.blockGlobalLogIndex, + removed: false, + topics: [TOPIC], + data: RAW_DATA, + })), + }); + + const batch = await verifyEnvioCandidateBatchWithDualRpc({ + candidates, + maximumCandidateCount: 4_096, + providers: [ + provider("alchemy-mainnet", client({ + getTransactionReceipt: async () => oversizedReceipt(), + })), + provider("quicknode-mainnet", client({ + getTransactionReceipt: async () => oversizedReceipt(), + })), + ], + }); + + expect(batch.candidates).toHaveLength(40); + expect(batch.executionTrace.candidateBatchSize).toBe(40); + expect(batch.executionTrace.providerCallCounts).toEqual([6, 6]); + }); + + it("verifies a hot block with more than 128 distinct transactions through bounded RPC batches", async () => { + const candidates = Array.from({ length: 256 }, (_, index) => { + const transactionHash = `0x${(index + 1) + .toString(16) + .padStart(64, "0")}` as const; + return { + ...candidate(), + candidateId: `1:${BLOCK_HASH}:${transactionHash}:${index}`, + transactionHash, + transactionIndex: index, + blockGlobalLogIndex: index, + } satisfies EnvioCandidate; + }); + const receipts = new Map( + candidates.map((entry) => [ + entry.transactionHash, + { + status: "success" as const, + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: entry.transactionHash, + transactionIndex: entry.transactionIndex, + logs: [{ + address: SOURCE, + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: entry.transactionHash, + transactionIndex: entry.transactionIndex, + logIndex: entry.blockGlobalLogIndex, + removed: false, + topics: [TOPIC], + data: RAW_DATA, + }], + } satisfies CandidateRpcReceipt, + ] as const), + ); + const batchedClient = () => { + const value = client(); + value.getTransactionReceipt = vi.fn(value.getTransactionReceipt); + value.getBytecode = vi.fn(value.getBytecode); + value.getTransactionReceipts = vi.fn(async ({ hashes }) => + hashes.map((hash: `0x${string}`) => receipts.get(hash)!), + ); + value.getBytecodes = vi.fn(async ({ requests }) => + requests.map(() => "0x60016000" as const), + ); + return value; + }; + const first = batchedClient(); + const second = batchedClient(); + + const batch = await verifyEnvioCandidateBatchWithDualRpc({ + candidates, + maximumCandidateCount: 4_096, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }); + + expect(batch.candidates).toHaveLength(256); + expect(batch.executionTrace.providerCallCounts).toEqual([18, 18]); + for (const rpcClient of [first, second]) { + expect(rpcClient.getTransactionReceipts).toHaveBeenCalledTimes(13); + expect(rpcClient.getBytecodes).toHaveBeenCalledTimes(1); + expect(rpcClient.getTransactionReceipt).not.toHaveBeenCalled(); + expect(rpcClient.getBytecode).not.toHaveBeenCalled(); + expect(rpcClient.getBytecodes).toHaveBeenCalledWith({ + requests: [{ + address: SOURCE, + blockHash: BLOCK_HASH, + requireCanonical: true, + }], + }); + } + }); + + it("verifies a sparse backlog spanning more than 128 candidate blocks through bounded RPC batches", async () => { + const blockCount = 256; + const candidates = Array.from({ length: blockCount }, (_, index) => { + const blockNumber = CANDIDATE_BLOCK + BigInt(index); + const blockHash = `0x${(10_000 + index) + .toString(16) + .padStart(64, "0")}` as const; + const transactionHash = `0x${(20_000 + index) + .toString(16) + .padStart(64, "0")}` as const; + return { + ...candidate(), + candidateId: `1:${blockHash}:${transactionHash}:0`, + blockNumber: blockNumber.toString(), + blockHash, + blockTimestamp: String(1_785_480_000 + index), + transactionHash, + transactionIndex: 0, + blockGlobalLogIndex: 0, + } satisfies EnvioCandidate; + }); + const byBlock = new Map(candidates.map((entry) => [ + entry.blockNumber, + entry, + ] as const)); + const receipts = new Map(candidates.map((entry) => [ + entry.transactionHash, + { + status: "success" as const, + blockNumber: BigInt(entry.blockNumber), + blockHash: entry.blockHash, + transactionHash: entry.transactionHash, + transactionIndex: 0, + logs: [{ + address: SOURCE, + blockNumber: BigInt(entry.blockNumber), + blockHash: entry.blockHash, + transactionHash: entry.transactionHash, + transactionIndex: 0, + logIndex: 0, + removed: false, + topics: [TOPIC], + data: RAW_DATA, + }], + } satisfies CandidateRpcReceipt, + ] as const)); + const safeBlock = CANDIDATE_BLOCK + BigInt(blockCount) + 2n; + const batchedClient = () => { + const value = client(); + value.getBlockNumber = vi.fn(async () => safeBlock + 12n); + value.getBlock = vi.fn(value.getBlock); + value.getTransactionReceipt = vi.fn(value.getTransactionReceipt); + value.getBytecode = vi.fn(value.getBytecode); + value.getBlocks = vi.fn(async ({ blockNumbers }) => + blockNumbers.map((blockNumber: bigint) => { + if (blockNumber === safeBlock) { + return { + number: safeBlock, + hash: SAFE_BLOCK_HASH, + timestamp: 1_785_481_000n, + }; + } + const entry = byBlock.get(blockNumber.toString())!; + return { + number: blockNumber, + hash: entry.blockHash, + timestamp: BigInt(entry.blockTimestamp), + }; + }), + ); + value.getTransactionReceipts = vi.fn(async ({ hashes }) => + hashes.map((hash: `0x${string}`) => receipts.get(hash)!), + ); + value.getBytecodes = vi.fn(async ({ requests }) => + requests.map(() => "0x60016000" as const), + ); + return value; + }; + const first = batchedClient(); + const second = batchedClient(); + + const batch = await verifyEnvioCandidateBatchWithDualRpc({ + candidates, + maximumCandidateCount: 4_096, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }); + + expect(batch.candidates).toHaveLength(blockCount); + expect(batch.executionTrace.providerCallCounts).toEqual([41, 41]); + for (const rpcClient of [first, second]) { + expect(rpcClient.getBlocks).toHaveBeenCalledTimes(13); + expect(rpcClient.getTransactionReceipts).toHaveBeenCalledTimes(13); + expect(rpcClient.getBytecodes).toHaveBeenCalledTimes(13); + expect(rpcClient.getBlock).not.toHaveBeenCalled(); + expect(rpcClient.getTransactionReceipt).not.toHaveBeenCalled(); + expect(rpcClient.getBytecode).not.toHaveBeenCalled(); + } + }); + + it("proves a finalized candidate against matching blocks, receipts, logs, and code", async () => { + const result = await verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("alchemy-mainnet", client()), + provider("quicknode-mainnet", client()), + ], + }); + + expect(result).toMatchObject({ + chainId: 1, + providerIdentities: ["alchemy-mainnet", "quicknode-mainnet"], + providerVendorGroups: ["alchemy", "quicknode"], + providerHeads: [PROVIDER_HEAD.toString(), PROVIDER_HEAD.toString()], + safeBlockNumber: SAFE_BLOCK.toString(), + safeBlockHash: SAFE_BLOCK_HASH, + candidateBlockNumber: CANDIDATE_BLOCK.toString(), + candidateBlockHash: BLOCK_HASH, + candidateBlockTimestamp: "1785480000", + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + receiptLogOrdinal: 1, + candidateId: candidate().candidateId, + sourceAddress: SOURCE, + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + sourceKind: "static", + model: "classic", + releaseVersion: "classic-v2", + payloadHash: PAYLOAD_HASH, + }); + expect(result.receiptCommitment).toMatch(/^0x[0-9a-f]{64}$/); + expect(result.rawLogCommitment).toMatch(/^0x[0-9a-f]{64}$/); + expect(result.sourceCodeHash).toMatch(/^0x[0-9a-f]{64}$/); + expect(result.providerEndpointCommitments).toEqual([ + rpcProviderCommitment( + "endpoint", + "https://alchemy-mainnet.example", + ), + rpcProviderCommitment( + "endpoint", + "https://quicknode-mainnet.example", + ), + ]); + expect(result.providerOriginCommitments).toEqual([ + rpcProviderCommitment("origin", "https://alchemy-mainnet.example"), + rpcProviderCommitment("origin", "https://quicknode-mainnet.example"), + ]); + expect(JSON.stringify(result)).not.toContain(".example"); + }); + + it("accepts an unrelated anonymous LOG0 in the same transaction receipt", async () => { + const receiptWithAnonymousLog = (): CandidateRpcReceipt => { + const value = receipt(); + return { + ...value, + logs: [ + { + address: "0x0000000000000000000000000000000000000001", + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logIndex: 5, + removed: false, + topics: [], + data: "0x1234", + }, + ...value.logs, + ], + }; + }; + + const result = await verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("alchemy-mainnet", client({ + getTransactionReceipt: async () => receiptWithAnonymousLog(), + })), + provider("quicknode-mainnet", client({ + getTransactionReceipt: async () => receiptWithAnonymousLog(), + })), + ], + }); + + expect(result.receiptLogOrdinal).toBe(2); + expect(result.receiptCommitment).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it("keeps a known dynamic vault release-neutral until factory proof", async () => { + const result = await verifyEnvioCandidateWithDualRpc({ + candidate: dynamicCandidate(), + providers: [ + provider("alchemy-mainnet", dynamicClient()), + provider("quicknode-mainnet", dynamicClient()), + ], + }); + + expect(result).toMatchObject({ + sourceAddress: DYNAMIC_SOURCE, + contractName: "ClassicV3RewardVault", + sourceKind: "dynamic-unresolved", + model: "unresolved", + releaseVersion: "unresolved", + sourceCodeHash: keccak256(DYNAMIC_CODE), + }); + expect(result).not.toHaveProperty("factoryOccurrenceFingerprint"); + }); + + it("rejects same-height child logs from a fork that does not contain the activation", async () => { + const replacementHash = `0x${"ab".repeat(32)}` as const; + const replacementCandidate: EnvioCandidate = { + ...dynamicCandidate(), + candidateId: + `1:${replacementHash}:${DYNAMIC_TRANSACTION_HASH}:5`, + blockHash: replacementHash, + blockGlobalLogIndex: 5, + }; + + await expect( + verifyEnvioCandidateBatchWithDualRpc({ + candidates: [replacementCandidate], + dynamicSources: [{ + attestationId: "10000000-0000-4000-8000-000000000001", + sourceAddress: DYNAMIC_SOURCE, + contractName: "ClassicV3RewardVault", + model: "classic", + releaseVersion: "classic-v3", + factoryAddress: + "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + factoryContractName: "ClassicV3RewardVaultFactory", + parentOccurrenceId: "10000000-0000-4000-8000-000000000002", + factoryBlockNumber: DYNAMIC_BLOCK.toString(), + factoryBlockGlobalLogIndex: "3", + activationCandidateId: + `1:${DYNAMIC_BLOCK_HASH}:${DYNAMIC_TRANSACTION_HASH}:4`, + activationBlockNumber: DYNAMIC_BLOCK.toString(), + activationBlockHash: DYNAMIC_BLOCK_HASH, + activationBlockGlobalLogIndex: "4", + expectedExactRuntimeCodeHash: keccak256(DYNAMIC_CODE), + expectedNormalizedRuntimeCodeHash: keccak256(DYNAMIC_CODE), + expectedImmutableReferencesCommitment: `0x${"bc".repeat(32)}`, + expectedRuntimeByteLength: "5", + immutableReferences: [], + }], + requireDynamicLineage: true, + providers: [ + provider("alchemy-mainnet", dynamicClient()), + provider("quicknode-mainnet", dynamicClient()), + ], + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "dynamic-source-lineage" }, + }); + }); + + it("keeps shared static hook and factory events release-neutral", async () => { + const fixtures = [ + sharedStaticCandidate({ + sourceAddress: "0x90c67c1e866f86526f0e338459cd435e1f23a0cc", + contractName: "StockV2V3Hook", + eventName: "PoolRegistered", + }), + sharedStaticCandidate({ + sourceAddress: "0x52d70971d6653a754c29385a2a6f241a481952d4", + contractName: "StockV2V3RewardVaultFactory", + eventName: "QuoteAssetFeeSplitVaultDeployed", + }), + ]; + + for (const fixture of fixtures) { + const result = await verifyEnvioCandidateWithDualRpc({ + candidate: fixture, + providers: [ + provider("alchemy-mainnet", sharedStaticClient(fixture)), + provider("quicknode-mainnet", sharedStaticClient(fixture)), + ], + }); + expect(result).toMatchObject({ + contractName: fixture.contractName, + sourceKind: "static", + model: "unresolved", + releaseVersion: "unresolved", + }); + + for (const forged of [ + { + ...fixture, + releaseHint: { + model: "stock-paired" as const, + releaseVersion: "stock-paired-v2", + }, + }, + { ...fixture, blockNumber: "25640337" }, + ]) { + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: forged, + providers: [ + provider("alchemy-mainnet", sharedStaticClient(forged)), + provider("quicknode-mainnet", sharedStaticClient(forged)), + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + } + } + }); + + it("rejects caller-asserted, unknown, premature, or RPC-divergent dynamic vaults", async () => { + const trustedProviders = () => [ + provider("alchemy-mainnet", dynamicClient()), + provider("quicknode-mainnet", dynamicClient()), + ] as const; + for (const forged of [ + { + ...dynamicCandidate(), + releaseHint: { + model: "classic" as const, + releaseVersion: "classic-v3", + }, + }, + { + ...dynamicCandidate(), + contractName: "ClassicV3UnknownVault", + }, + { + ...dynamicCandidate(), + blockNumber: "25639595", + }, + ]) { + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: forged, + providers: trustedProviders(), + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + } + + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: dynamicCandidate(), + providers: [ + provider("alchemy-mainnet", dynamicClient()), + provider("quicknode-mainnet", dynamicClient("0x6002600055")), + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects duplicate providers, wrong chains, and candidates above the shared safe head", async () => { + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("same", client()), + provider("same", client()), + ], + }), + ).rejects.toMatchObject({ code: "invalid_input" }); + + const committedEndpoint = provider("alchemy-primary", client()); + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + committedEndpoint, + { + ...provider("quicknode-secondary", client()), + endpointCommitment: committedEndpoint.endpointCommitment, + }, + ], + }), + ).rejects.toMatchObject({ code: "invalid_input" }); + + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("a", client()), + provider("b", client({ getChainId: async () => 10 })), + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider( + "a", + client({ getBlockNumber: async () => CANDIDATE_BLOCK + 11n }), + ), + provider( + "b", + client({ getBlockNumber: async () => CANDIDATE_BLOCK + 12n }), + ), + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects the same client, endpoint origin commitment, or vendor group under different labels", async () => { + const sharedClient = client(); + const first = provider("alchemy-primary", sharedClient); + const second = provider("quicknode-secondary", sharedClient); + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [first, second], + }), + ).rejects.toMatchObject({ code: "invalid_input" }); + + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + first, + { + ...provider("quicknode-secondary", client()), + endpointOriginCommitment: first.endpointOriginCommitment, + }, + ], + }), + ).rejects.toMatchObject({ code: "invalid_input" }); + + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("alchemy-primary", client()), + { + ...provider("quicknode-secondary", client()), + vendorGroup: "alchemy", + }, + ], + }), + ).rejects.toMatchObject({ code: "invalid_input" }); + }); + + it("rejects divergent safe or candidate block evidence", async () => { + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("a", client()), + { + ...provider("b", client({ + getBlock: async ({ blockNumber }) => + blockNumber === SAFE_BLOCK + ? { + number: SAFE_BLOCK, + hash: `0x${"99".repeat(32)}`, + timestamp: 1785480003n, + } + : { + number: CANDIDATE_BLOCK, + hash: BLOCK_HASH, + timestamp: 1785480000n, + }, + })), + }, + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("a", client()), + { + ...provider("b", client({ + getBlock: async ({ blockNumber }) => + blockNumber === SAFE_BLOCK + ? { + number: SAFE_BLOCK, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480003n, + } + : { + number: CANDIDATE_BLOCK, + hash: `0x${"99".repeat(32)}`, + timestamp: 1785480000n, + }, + })), + }, + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects receipt, selected log, or bytecode disagreements", async () => { + const receiptWithDifferentUnselectedLog = receipt(); + const badReceipt: CandidateRpcReceipt = { + ...receiptWithDifferentUnselectedLog, + logs: receiptWithDifferentUnselectedLog.logs.map((log, index) => + index === 0 ? { ...log, data: "0x12" } : log, + ), + }; + const receiptWithDifferentSelectedLog = receipt(); + const badLog: CandidateRpcReceipt = { + ...receiptWithDifferentSelectedLog, + logs: receiptWithDifferentSelectedLog.logs.map((log, index) => + index === 1 ? { ...log, data: "0x99" } : log, + ), + }; + + for (const second of [ + client({ getTransactionReceipt: async () => badReceipt }), + client({ getTransactionReceipt: async () => badLog }), + client({ + getTransactionReceipt: async () => ({ + ...receipt(), + blockHash: "0x12", + }), + }), + client({ getBytecode: async () => "0x6002" }), + client({ getBytecode: async () => "0x1" }), + client({ getBytecode: async () => undefined }), + ]) { + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider("a", client()), + provider("b", second), + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + } + }); + + it("bounds provider fan-out and retries transient RPC failures", async () => { + const candidates = Array.from({ length: 8 }, (_, index) => { + const byte = (index + 16).toString(16).padStart(2, "0"); + const transactionHash = `0x${byte.repeat(32)}` as const; + const blockGlobalLogIndex = 20 + index; + return { + ...candidate(), + candidateId: `1:${BLOCK_HASH}:${transactionHash}:${blockGlobalLogIndex}`, + transactionHash, + transactionIndex: index, + blockGlobalLogIndex, + } satisfies EnvioCandidate; + }); + + function measuredClient() { + let inFlight = 0; + let maximumInFlight = 0; + let transientBlockFailure = true; + const rpcClient = client({ + getBlock: async ({ blockNumber }) => { + if (transientBlockFailure) { + transientBlockFailure = false; + throw new Error("429"); + } + return blockNumber === SAFE_BLOCK + ? { + number: SAFE_BLOCK, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480003n, + } + : { + number: CANDIDATE_BLOCK, + hash: BLOCK_HASH, + timestamp: 1785480000n, + }; + }, + getTransactionReceipt: async ({ hash }) => { + const current = candidates.find( + (entry) => entry.transactionHash === hash, + )!; + inFlight += 1; + maximumInFlight = Math.max(maximumInFlight, inFlight); + await new Promise((resolve) => setTimeout(resolve, 2)); + inFlight -= 1; + return { + status: "success", + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: current.transactionHash, + transactionIndex: current.transactionIndex, + logs: [ + { + address: SOURCE, + blockNumber: CANDIDATE_BLOCK, + blockHash: BLOCK_HASH, + transactionHash: current.transactionHash, + transactionIndex: current.transactionIndex, + logIndex: current.blockGlobalLogIndex, + removed: false, + topics: [TOPIC], + data: RAW_DATA, + }, + ], + }; + }, + }); + return { + rpcClient, + maximumInFlight: () => maximumInFlight, + }; + } + + const first = measuredClient(); + const second = measuredClient(); + const sleep = vi.fn(async () => undefined); + const result = await verifyEnvioCandidateBatchWithDualRpc({ + candidates, + providers: [ + provider("alchemy-mainnet", first.rpcClient), + provider("quicknode-mainnet", second.rpcClient), + ], + rpcPolicy: { + maxConcurrency: 2, + maxAttempts: 2, + baseBackoffMs: 0, + sleep, + }, + }); + + expect(result.candidates).toHaveLength(8); + expect(first.maximumInFlight()).toBeLessThanOrEqual(2); + expect(second.maximumInFlight()).toBeLessThanOrEqual(2); + expect(sleep).toHaveBeenCalledTimes(2); + }); + + it("charges every safe-head retry against the physical provider budget", async () => { + const first = client(); + const second = client(); + first.getChainId = vi + .fn<() => Promise>() + .mockRejectedValueOnce(new Error("429")) + .mockResolvedValue(1); + second.getChainId = vi + .fn<() => Promise>() + .mockRejectedValueOnce(new Error("429")) + .mockResolvedValue(1); + + await expect( + readDualRpcSafeHead({ + cursor: { + blockNumber: CANDIDATE_BLOCK.toString(), + blockHash: BLOCK_HASH, + }, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { + maxAttempts: 2, + baseBackoffMs: 0, + maxCallsPerProvider: 4, + sleep: async () => undefined, + }, + }), + ).rejects.toMatchObject({ + code: "dependency_unavailable", + }); + expect(first.getChainId).toHaveBeenCalledTimes(2); + expect(second.getChainId).toHaveBeenCalledTimes(2); + }); + + it("fails closed when a provider changes the safe block during a same-height cursor read", async () => { + const replacementHash = `0x${"44".repeat(32)}` as const; + const temporallyInconsistentClient = () => + client({ + getBlock: vi + .fn() + .mockResolvedValueOnce({ + number: SAFE_BLOCK, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480003n, + }) + .mockResolvedValueOnce({ + number: SAFE_BLOCK, + hash: replacementHash, + timestamp: 1785480004n, + }), + }); + + await expect( + readDualRpcSafeHead({ + cursor: { + blockNumber: SAFE_BLOCK.toString(), + blockHash: SAFE_BLOCK_HASH, + }, + providers: [ + provider("alchemy-mainnet", temporallyInconsistentClient()), + provider("quicknode-mainnet", temporallyInconsistentClient()), + ], + }), + ).rejects.toMatchObject({ + code: "validation_failed", + safeMetadata: { operation: "safe-head-provider-disagreement" }, + }); + }); + + it("distinguishes a jointly observed orphaned cursor from provider disagreement", async () => { + const replacementHash = `0x${"44".repeat(32)}` as const; + const orphanAwareClient = () => + client({ + getBlock: vi.fn(async ({ blockNumber }) => + blockNumber === SAFE_BLOCK + ? { + number: SAFE_BLOCK, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480003n, + } + : { + number: CANDIDATE_BLOCK, + hash: replacementHash, + timestamp: 1785480000n, + }, + ), + }); + + await expect( + readDualRpcSafeHead({ + cursor: { + blockNumber: CANDIDATE_BLOCK.toString(), + blockHash: BLOCK_HASH, + }, + providers: [ + provider("alchemy-mainnet", orphanAwareClient()), + provider("quicknode-mainnet", orphanAwareClient()), + ], + }), + ).rejects.toMatchObject({ + code: "validation_failed", + safeMetadata: { operation: "safe-head-cursor-orphaned" }, + }); + }); + + it("never treats provider disagreement as a recoverable cursor orphan", async () => { + const replacementHash = `0x${"44".repeat(32)}` as const; + const first = client(); + const second = client({ + getBlock: vi.fn(async ({ blockNumber }) => + blockNumber === SAFE_BLOCK + ? { + number: SAFE_BLOCK, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480003n, + } + : { + number: CANDIDATE_BLOCK, + hash: replacementHash, + timestamp: 1785480000n, + }, + ), + }); + + await expect( + readDualRpcSafeHead({ + cursor: { + blockNumber: CANDIDATE_BLOCK.toString(), + blockHash: BLOCK_HASH, + }, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + }), + ).rejects.toMatchObject({ + code: "validation_failed", + safeMetadata: { operation: "safe-head-provider-disagreement" }, + }); + }); + + it("charges both metadata eth_calls on every physical retry", async () => { + const first = client(); + const second = client(); + first.readErc20Metadata = vi + .fn<() => Promise<{ name: string; symbol: string }>>() + .mockRejectedValueOnce(new Error("429")) + .mockResolvedValue({ name: "Token", symbol: "TKN" }); + second.readErc20Metadata = vi + .fn<() => Promise<{ name: string; symbol: string }>>() + .mockRejectedValueOnce(new Error("429")) + .mockResolvedValue({ name: "Token", symbol: "TKN" }); + + await expect( + readDualRpcTokenMetadata({ + tokens: [{ + token: SOURCE, + blockNumber: CANDIDATE_BLOCK.toString(), + blockHash: BLOCK_HASH, + }], + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { + maxAttempts: 2, + baseBackoffMs: 0, + maxCallsPerProvider: 2, + sleep: async () => undefined, + }, + }), + ).rejects.toMatchObject({ + code: "dependency_unavailable", + }); + expect(first.readErc20Metadata).toHaveBeenCalledTimes(1); + expect(second.readErc20Metadata).toHaveBeenCalledTimes(1); + expect(first.readErc20Metadata).toHaveBeenCalledWith({ + address: SOURCE, + blockHash: BLOCK_HASH, + requireCanonical: true, + }); + expect(second.readErc20Metadata).toHaveBeenCalledWith({ + address: SOURCE, + blockHash: BLOCK_HASH, + requireCanonical: true, + }); + }); + + it("rejects a forged candidate envelope or an unpinned runtime", async () => { + for (const forged of [ + { + ...candidate(), + sourceAddress: "0x7777777777777777777777777777777777777777" as const, + }, + { + ...candidate(), + payloadHash: `0x${"99".repeat(32)}` as const, + }, + { + ...candidate(), + releaseHint: { + model: "stock-paired" as const, + releaseVersion: "classic-v2", + }, + }, + ]) { + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: forged, + providers: [ + provider("alchemy-mainnet", client()), + provider("quicknode-mainnet", client()), + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + } + + await expect( + verifyEnvioCandidateWithDualRpc({ + candidate: candidate(), + providers: [ + provider( + "alchemy-mainnet", + client({ getBytecode: async () => "0x6002" }), + ), + provider( + "quicknode-mainnet", + client({ getBytecode: async () => "0x6002" }), + ), + ], + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); +}); diff --git a/tests/data-pipeline/envio-candidate-deployment-evidence.test.ts b/tests/data-pipeline/envio-candidate-deployment-evidence.test.ts new file mode 100644 index 00000000..b9e71940 --- /dev/null +++ b/tests/data-pipeline/envio-candidate-deployment-evidence.test.ts @@ -0,0 +1,139 @@ +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const evidencePath = resolve( + "docs/data-pipeline/envio-candidate-7f24e63-deployment-7ffd15c.json", +); + +async function readJson(path: string) { + return JSON.parse(await readFile(resolve(path), "utf8")) as Record< + string, + unknown + >; +} + +async function fileSha256(path: string) { + const bytes = await readFile(resolve(path)); + return `0x${createHash("sha256").update(bytes).digest("hex")}`; +} + +describe("Envio candidate deployment evidence", () => { + it("contains public chain evidence without service credentials", async () => { + const baseline = await readJson( + "docs/data-pipeline/envio-candidate-7f24e63-baseline-20260801T042058Z.json", + ); + const audit = await readJson( + "docs/data-pipeline/envio-candidate-7f24e63-audit-20260801T042059Z.json", + ); + const serialized = JSON.stringify({ baseline, audit }).toLowerCase(); + + for (const forbidden of [ + "alchemy.com", + "quiknode.pro", + "supabase.co", + "api_key", + "apikey", + "authorization", + "bearer ", + "privatekey", + "mnemonic", + "password", + "secret", + "token=", + ]) { + expect(serialized).not.toContain(forbidden); + } + + expect(baseline.endpoint).toBe( + "https://indexer.hyperindex.xyz/f6714ef/v1/graphql", + ); + expect(audit.endpoint).toBe( + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + ); + }); + + it("binds the paired baseline and audit to one exact checkpoint", async () => { + const evidence = await readJson(evidencePath); + const artifacts = evidence.artifacts as Record< + string, + Record + >; + const baseline = await readJson(String(artifacts.baseline.path)); + const audit = await readJson(String(artifacts.candidateAudit.path)); + const identity = await readJson(String(artifacts.identity.path)); + + await expect(fileSha256(String(artifacts.baseline.path))).resolves.toBe( + artifacts.baseline.fileSha256, + ); + await expect( + fileSha256(String(artifacts.candidateAudit.path)), + ).resolves.toBe(artifacts.candidateAudit.fileSha256); + await expect(fileSha256(String(artifacts.identity.path))).resolves.toBe( + artifacts.identity.fileSha256, + ); + + expect(baseline.digest).toBe(artifacts.baseline.internalDigest); + expect(audit.digest).toBe(artifacts.candidateAudit.internalDigest); + expect(baseline.anchor).toEqual(audit.anchor); + expect(audit.anchor).toEqual(evidence.checkpoint); + expect(audit.identity).toEqual(identity); + expect( + (audit.authenticatedCoordinatorCreatorRepairs as unknown[]).length, + ).toBe(artifacts.candidateAudit.authenticatedCoordinatorCreatorRepairs); + }); + + it("preserves pre-promotion evidence while the canonical release selects the candidate", async () => { + const evidence = await readJson(evidencePath); + const prepared = evidence.historicalPreparation as Record; + const candidate = evidence.candidate as Record; + const active = evidence.activeProduction as Record; + const rollback = evidence.rollback as Record; + const promotion = evidence.promotion as Record; + const binding = await readJson("config/data-pipeline-release.v1.json"); + const envio = binding.envio as Record; + + await expect(fileSha256(String(prepared.path))).resolves.toBe( + prepared.fileSha256, + ); + expect(prepared.preservedUnchanged).toBe(true); + expect(candidate.promoted).toBe(false); + expect(candidate.controlPlaneStatus).toBe("none"); + expect(promotion.state).toBe("not-promoted"); + expect(promotion.productionBindingMayChange).toBe(false); + expect(active.deploymentLabel).toBe(rollback.deployment); + expect(active.endpoint).toBe(rollback.graphqlEndpoint); + expect(active.sourceCommit).toBe(rollback.sourceCommit); + expect(envio.deploymentLabel).toBe(candidate.deploymentLabel); + expect(envio.graphqlEndpoint).toBe(candidate.endpoint); + expect(envio).toMatchObject(candidate.identity as Record); + }); + + it("records inventory, repairs and the rejected deployment explicitly", async () => { + const evidence = await readJson(evidencePath); + const inventory = evidence.inventory as Record; + const rejected = evidence.nonPromotableDeployments as Array< + Record + >; + + expect(inventory).toEqual({ + count: 265, + perRelease: { + "classic-v2": 27, + "classic-v3": 186, + "stock-paired-v1": 1, + "stock-paired-v2": 8, + "stock-paired-v3": 43, + }, + }); + expect(rejected).toEqual([ + expect.objectContaining({ + mirrorCommit: "6f2f408e137ce3c01450a13ed11f477ae4ac7240", + promotable: false, + rejection: "candidate IndexerState.sourceCommit is invalid", + }), + ]); + }); +}); diff --git a/tests/data-pipeline/envio.test.ts b/tests/data-pipeline/envio.test.ts new file mode 100644 index 00000000..f59d5e11 --- /dev/null +++ b/tests/data-pipeline/envio.test.ts @@ -0,0 +1,1062 @@ +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + encodeEventTopics, + keccak256, + parseAbiItem, + type AbiParameter, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +import { createEnvioClient } from "../../lib/data-pipeline/envio"; +import { DataPipelineError } from "../../lib/data-pipeline/errors"; +import { parseDataPipelineReleaseBinding } from "../../lib/data-pipeline/release-binding.server"; +import { canonicalPayloadJson } from "../../indexer/src/lib/payload-hash"; +import releaseBinding from "../../config/data-pipeline-release.v1.json"; + +const BLOCK_HASH = `0x${"11".repeat(32)}`; +const TRANSACTION_HASH = `0x${"22".repeat(32)}`; +const SOURCE = "0xd240d06f8586eb799f20056054e5b527405e6bad"; +const CANDIDATE_ID = `1:${BLOCK_HASH}:${TRANSACTION_HASH}:7`; + +const EVENT_ABI = parseAbiItem( + "event MemeTokenLaunched(address indexed creator, address indexed token, bytes32 indexed poolId, address feeHook, address positionRecipient, uint256 positionTokenId, uint16 totalSwapFeeBps, bytes32 launchHash)", +); +const EVENT_ARGS = { + creator: "0x1111111111111111111111111111111111111111", + token: "0x2222222222222222222222222222222222222222", + poolId: `0x${"33".repeat(32)}`, + feeHook: "0x4444444444444444444444444444444444444444", + positionRecipient: "0x5555555555555555555555555555555555555555", + positionTokenId: 42n, + totalSwapFeeBps: 100n, + launchHash: `0x${"66".repeat(32)}`, +} as const; +const EVENT_TOPICS = encodeEventTopics({ + abi: [EVENT_ABI], + eventName: EVENT_ABI.name, + args: EVENT_ARGS, +}) as readonly Hex[]; +const NON_INDEXED_INPUTS = EVENT_ABI.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, +) as readonly AbiParameter[]; +const EVENT_DATA = encodeAbiParameters( + NON_INDEXED_INPUTS, + NON_INDEXED_INPUTS.map( + (input) => EVENT_ARGS[input.name as keyof typeof EVENT_ARGS], + ), +); +const PAYLOAD_HASH = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [EVENT_TOPICS, EVENT_DATA], + ), +); +const DECODED_PAYLOAD = canonicalPayloadJson(EVENT_ARGS); +const DYNAMIC_SOURCE = "0x4cfe000000000000000000000000000000000001"; +const DYNAMIC_CANDIDATE_ID = + `1:${BLOCK_HASH}:${TRANSACTION_HASH}:8`; +const DYNAMIC_EVENT_ABI = parseAbiItem( + "event CreatorFeesCheckpointed(bytes32 indexed poolId, uint64 indexed configurationEpoch, uint256 amount, uint256 totalCreatorFeesReceived)", +); +const DYNAMIC_EVENT_ARGS = { + poolId: `0x${"77".repeat(32)}`, + configurationEpoch: 1n, + amount: 900n, + totalCreatorFeesReceived: 900n, +} as const; +const DYNAMIC_EVENT_TOPICS = encodeEventTopics({ + abi: [DYNAMIC_EVENT_ABI], + eventName: DYNAMIC_EVENT_ABI.name, + args: DYNAMIC_EVENT_ARGS, +}) as readonly Hex[]; +const DYNAMIC_NON_INDEXED_INPUTS = DYNAMIC_EVENT_ABI.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, +) as readonly AbiParameter[]; +const DYNAMIC_EVENT_DATA = encodeAbiParameters( + DYNAMIC_NON_INDEXED_INPUTS, + DYNAMIC_NON_INDEXED_INPUTS.map( + (input) => + DYNAMIC_EVENT_ARGS[input.name as keyof typeof DYNAMIC_EVENT_ARGS], + ), +); +const DYNAMIC_PAYLOAD_HASH = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [DYNAMIC_EVENT_TOPICS, DYNAMIC_EVENT_DATA], + ), +); +const SHARED_HOOK_SOURCE = + "0x90c67c1e866f86526f0e338459cd435e1f23a0cc"; +const SHARED_FACTORY_SOURCE = + "0x52d70971d6653a754c29385a2a6f241a481952d4"; +const SHARED_HOOK_ID = `1:${BLOCK_HASH}:${TRANSACTION_HASH}:9`; +const SHARED_FACTORY_ID = `1:${BLOCK_HASH}:${TRANSACTION_HASH}:10`; +const SHARED_HOOK_EVENT_ABI = parseAbiItem( + "event PoolRegistered(bytes32 indexed poolId, address indexed token, address indexed quoteAsset, address rewardVault, address registrar, bool quoteIsCurrency0, bytes32 rewardConfigurationHash, bytes32 quoteConfigurationHash)", +); +const SHARED_HOOK_EVENT_ARGS = { + poolId: `0x${"88".repeat(32)}`, + token: "0x8888888888888888888888888888888888888888", + quoteAsset: "0x9999999999999999999999999999999999999999", + rewardVault: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + registrar: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + quoteIsCurrency0: true, + rewardConfigurationHash: `0x${"aa".repeat(32)}`, + quoteConfigurationHash: `0x${"bb".repeat(32)}`, +} as const; +const SHARED_FACTORY_EVENT_ABI = parseAbiItem( + "event QuoteAssetFeeSplitVaultDeployed(address indexed vault, address indexed feeHook, bytes32 indexed poolId, address quoteAsset)", +); +const SHARED_FACTORY_EVENT_ARGS = { + vault: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + feeHook: SHARED_HOOK_SOURCE, + poolId: `0x${"88".repeat(32)}`, + quoteAsset: "0x9999999999999999999999999999999999999999", +} as const; + +function encodedEventFixture( + abi: typeof SHARED_HOOK_EVENT_ABI | typeof SHARED_FACTORY_EVENT_ABI, + args: Readonly>, +) { + const topics = encodeEventTopics({ + abi: [abi], + eventName: abi.name, + args, + }) as readonly Hex[]; + const nonIndexedInputs = abi.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, + ) as readonly AbiParameter[]; + const data = encodeAbiParameters( + nonIndexedInputs, + nonIndexedInputs.map((input) => { + if (!input.name) throw new Error("Shared event input must be named"); + return args[input.name]; + }), + ); + return { + topics, + data, + decodedPayload: canonicalPayloadJson(args), + payloadHash: keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [topics, data], + ), + ), + }; +} + +function candidate(overrides: Record = {}) { + return { + id: CANDIDATE_ID, + downstreamLogicalId: null, + receiptLogOrdinal: null, + chainId: 1, + blockNumber: "25624131", + blockHash: BLOCK_HASH, + blockTimestamp: "1785480000", + transactionHash: TRANSACTION_HASH, + transactionIndex: "3", + blockGlobalLogIndex: "7", + sourceAddress: SOURCE, + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + model: "classic", + releaseVersion: "classic-v2", + topics: EVENT_TOPICS, + data: EVENT_DATA, + decodedPayload: DECODED_PAYLOAD, + payloadHash: PAYLOAD_HASH, + ...overrides, + }; +} + +function dynamicCandidate(overrides: Record = {}) { + return { + id: DYNAMIC_CANDIDATE_ID, + downstreamLogicalId: null, + receiptLogOrdinal: null, + chainId: 1, + blockNumber: "25639597", + blockHash: BLOCK_HASH, + blockTimestamp: "1785481000", + transactionHash: TRANSACTION_HASH, + transactionIndex: "4", + blockGlobalLogIndex: "8", + sourceAddress: DYNAMIC_SOURCE, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + model: "unresolved", + releaseVersion: "unresolved", + topics: DYNAMIC_EVENT_TOPICS, + data: DYNAMIC_EVENT_DATA, + decodedPayload: canonicalPayloadJson(DYNAMIC_EVENT_ARGS), + payloadHash: DYNAMIC_PAYLOAD_HASH, + ...overrides, + }; +} + +function sharedStaticCandidates() { + const hook = encodedEventFixture( + SHARED_HOOK_EVENT_ABI, + SHARED_HOOK_EVENT_ARGS, + ); + const factory = encodedEventFixture( + SHARED_FACTORY_EVENT_ABI, + SHARED_FACTORY_EVENT_ARGS, + ); + return [ + { + id: SHARED_HOOK_ID, + downstreamLogicalId: null, + receiptLogOrdinal: null, + chainId: 1, + blockNumber: "25640338", + blockHash: BLOCK_HASH, + blockTimestamp: "1785482000", + transactionHash: TRANSACTION_HASH, + transactionIndex: "5", + blockGlobalLogIndex: "9", + sourceAddress: SHARED_HOOK_SOURCE, + contractName: "StockV2V3Hook", + eventName: "PoolRegistered", + model: "unresolved", + releaseVersion: "unresolved", + ...hook, + }, + { + id: SHARED_FACTORY_ID, + downstreamLogicalId: null, + receiptLogOrdinal: null, + chainId: 1, + blockNumber: "25640338", + blockHash: BLOCK_HASH, + blockTimestamp: "1785482000", + transactionHash: TRANSACTION_HASH, + transactionIndex: "5", + blockGlobalLogIndex: "10", + sourceAddress: SHARED_FACTORY_SOURCE, + contractName: "StockV2V3RewardVaultFactory", + eventName: "QuoteAssetFeeSplitVaultDeployed", + model: "unresolved", + releaseVersion: "unresolved", + ...factory, + }, + ]; +} + +function placedCandidate(input: { + blockNumber: string; + blockGlobalLogIndex: number; + blockHash: Hex; + transactionHash: Hex; +}) { + return candidate({ + id: `1:${input.blockHash}:${input.transactionHash}:${input.blockGlobalLogIndex}`, + blockNumber: input.blockNumber, + blockHash: input.blockHash, + transactionHash: input.transactionHash, + blockGlobalLogIndex: String(input.blockGlobalLogIndex), + }); +} + +function json(body: unknown) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function progressPayload(input: { + meta?: Record; + state?: Record; +} = {}) { + return { + data: { + _meta: [ + { + chainId: 1, + progressBlock: 25_650_010, + bufferBlock: 25_650_010, + sourceBlock: 25_650_022, + isReady: true, + eventsProcessed: 51_234, + ...input.meta, + }, + ], + IndexerState_by_pk: { + id: "ethereum-mainnet", + schemaVersion: "1", + deployment: releaseBinding.envio.deploymentLabel, + sourceCommit: releaseBinding.envio.sourceCommit, + configSha256: releaseBinding.envio.configSha256, + schemaSha256: releaseBinding.envio.schemaSha256, + handlerSha256: releaseBinding.envio.handlerSha256, + sourceRegistrySha256: releaseBinding.envio.sourceRegistrySha256, + eventSetSha256: releaseBinding.envio.eventSetSha256, + eventCount: releaseBinding.envio.eventCount, + chainId: 1, + progressBlock: "25650000", + progressBlockHash: BLOCK_HASH, + progressTimestamp: "1785480000", + progressTransactionHash: TRANSACTION_HASH, + progressOccurrenceId: CANDIDATE_ID, + ...input.state, + }, + }, + }; +} + +describe("Envio candidate adapter", () => { + it("returns a strictly validated upstream candidate without authority fields", async () => { + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(request.query).toContain("ChainEvent_by_pk"); + expect(request.query).toContain("$candidateId: String!"); + expect(request.query).not.toContain("$candidateId: ID!"); + expect(request.variables).toEqual({ candidateId: CANDIDATE_ID }); + expect(init?.headers).toMatchObject({ + authorization: "Bearer envio-secret", + "content-type": "application/json", + }); + return json({ data: { ChainEvent_by_pk: candidate() } }); + }); + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + token: "envio-secret", + fetcher, + }); + + const result = await client.readCandidate(CANDIDATE_ID); + + expect(result).toMatchObject({ + candidateId: CANDIDATE_ID, + chainId: 1, + blockNumber: "25624131", + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 3, + blockGlobalLogIndex: 7, + sourceAddress: SOURCE, + releaseHint: { + model: "classic", + releaseVersion: "classic-v2", + }, + orderedTopics: EVENT_TOPICS, + rawData: EVENT_DATA, + decodedPayload: { + creator: "0x1111111111111111111111111111111111111111", + feeHook: "0x4444444444444444444444444444444444444444", + launchHash: `0x${"66".repeat(32)}`, + poolId: `0x${"33".repeat(32)}`, + positionRecipient: + "0x5555555555555555555555555555555555555555", + positionTokenId: "42", + token: "0x2222222222222222222222222222222222222222", + totalSwapFeeBps: "100", + }, + payloadHash: PAYLOAD_HASH, + }); + expect(result).not.toHaveProperty("canonical"); + expect(result).not.toHaveProperty("verified"); + expect(result).not.toHaveProperty("rewardAuthority"); + expect(fetcher).toHaveBeenCalledTimes(1); + }); + + it("builds source and release provenance from the selected client binding", async () => { + const selectedSource = "0xd240d06f8586eb799f20056054e5b527405e6bae"; + const selectedBinding = parseDataPipelineReleaseBinding({ + ...releaseBinding, + sources: releaseBinding.sources.map((source) => + source.contractName === "ClassicV2Launcher" + ? { ...source, address: selectedSource } + : source, + ), + }); + const fetcher = async () => json({ + data: { + ChainEvent_by_pk: candidate({ sourceAddress: selectedSource }), + }, + }); + const selectedClient = createEnvioClient({ + endpoint: "https://envio.example/graphql", + releaseBinding: selectedBinding, + fetcher, + }); + + await expect(selectedClient.readCandidate(CANDIDATE_ID)).resolves.toMatchObject({ + sourceAddress: selectedSource, + releaseHint: { model: "classic", releaseVersion: "classic-v2" }, + }); + + const legacyClient = createEnvioClient({ + endpoint: "https://envio.example/graphql", + releaseBinding: parseDataPipelineReleaseBinding(releaseBinding), + fetcher, + }); + await expect(legacyClient.readCandidate(CANDIDATE_ID)).rejects.toMatchObject({ + code: "validation_failed", + }); + }); + + it("supports an Envio public endpoint without sending an Authorization header", async () => { + let authorization: string | null = "not-read"; + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async (_url, init) => { + authorization = new Headers(init?.headers).get("authorization"); + return json({ data: { ChainEvent_by_pk: candidate() } }); + }, + }); + + await expect(client.readCandidate(CANDIDATE_ID)).resolves.toMatchObject({ + candidateId: CANDIDATE_ID, + }); + expect(authorization).toBeNull(); + }); + + it("returns null only when the exact candidate id is absent", async () => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + token: "envio-secret", + fetcher: async () => json({ data: { ChainEvent_by_pk: null } }), + }); + + await expect(client.readCandidate(CANDIDATE_ID)).resolves.toBeNull(); + }); + + it("accepts canonical decimal uint32 placement values without narrowing", async () => { + const maximum = 4_294_967_295; + const maximumCandidateId = `1:${BLOCK_HASH}:${TRANSACTION_HASH}:${maximum}`; + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ + data: { + ChainEvent_by_pk: candidate({ + id: maximumCandidateId, + transactionIndex: String(maximum), + blockGlobalLogIndex: String(maximum), + }), + }, + }), + }); + + await expect(client.readCandidate(maximumCandidateId)).resolves.toMatchObject({ + transactionIndex: maximum, + blockGlobalLogIndex: maximum, + }); + }); + + it.each([ + ["numeric JSON value", { transactionIndex: 3 }], + ["noncanonical decimal", { transactionIndex: "03" }], + ["transaction overflow", { transactionIndex: "4294967296" }], + ["log-index overflow", { blockGlobalLogIndex: "4294967296" }], + ])("rejects malformed BigInt %s", async (_name, override) => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ data: { ChainEvent_by_pk: candidate(override) } }), + }); + + await expect(client.readCandidate(CANDIDATE_ID)).rejects.toMatchObject({ + dependency: "envio", + code: "validation_failed", + }); + }); + + it("accepts known dynamic vault events only as release-neutral candidates", async () => { + const accepted = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ data: { ChainEvent_by_pk: dynamicCandidate() } }), + }); + + await expect( + accepted.readCandidate(DYNAMIC_CANDIDATE_ID), + ).resolves.toMatchObject({ + sourceAddress: DYNAMIC_SOURCE, + contractName: "ClassicV3RewardVault", + releaseHint: { model: "unresolved", releaseVersion: "unresolved" }, + }); + + for (const override of [ + { model: "classic", releaseVersion: "classic-v3" }, + { contractName: "ClassicV3UnknownVault" }, + { blockNumber: "25639595" }, + ]) { + const rejected = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ + data: { ChainEvent_by_pk: dynamicCandidate(override) }, + }), + }); + await expect( + rejected.readCandidate(DYNAMIC_CANDIDATE_ID), + ).rejects.toMatchObject({ + dependency: "envio", + code: "validation_failed", + }); + } + }); + + it("accepts shared static hook and factory events only as release-neutral candidates", async () => { + for (const fixture of sharedStaticCandidates()) { + const accepted = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ data: { ChainEvent_by_pk: fixture } }), + }); + await expect(accepted.readCandidate(fixture.id)).resolves.toMatchObject({ + contractName: fixture.contractName, + releaseHint: { model: "unresolved", releaseVersion: "unresolved" }, + }); + + for (const override of [ + { + model: "stock-paired", + releaseVersion: "stock-paired-v2", + }, + { blockNumber: "25640337" }, + ]) { + const rejected = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ + data: { + ChainEvent_by_pk: { ...fixture, ...override }, + }, + }), + }); + await expect(rejected.readCandidate(fixture.id)).rejects.toMatchObject({ + dependency: "envio", + code: "validation_failed", + }); + } + } + }); + + it.each([ + ["fork identity", { id: `1:${"0x" + "99".repeat(32)}:${TRANSACTION_HASH}:7` }], + ["block hash", { blockHash: `0x${"99".repeat(32)}` }], + ["global log index", { blockGlobalLogIndex: "8" }], + ["source cutoff", { blockNumber: "25624130" }], + ["source contract", { contractName: "ClassicV2Hook" }], + ["release hint", { releaseVersion: "classic-v3" }], + ["model hint", { model: "stock-paired" }], + ["downstream identity", { downstreamLogicalId: "1:trusted:0" }], + ["receipt ordinal", { receiptLogOrdinal: 0 }], + ["topic width", { topics: ["0x12"] }], + ["event signature", { topics: [`0x${"44".repeat(32)}`, ...EVENT_TOPICS.slice(1)] }], + ["indexed topic count", { topics: EVENT_TOPICS.slice(0, -1) }], + ["raw data", { data: "0x0" }], + ["strict ABI data", { data: "0x" }], + ["event name", { eventName: "MemeLiquidityConfigured" }], + [ + "decoded payload mismatch", + { + decodedPayload: JSON.stringify({ + ...JSON.parse(DECODED_PAYLOAD), + positionTokenId: "43", + }), + }, + ], + ["payload hash", { payloadHash: `0x${"33".repeat(32)}` }], + ["payload JSON", { decodedPayload: "{" }], + ])("rejects malformed %s provenance", async (_name, override) => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + token: "envio-secret", + fetcher: async () => + json({ data: { ChainEvent_by_pk: candidate(override) } }), + }); + + await expect(client.readCandidate(CANDIDATE_ID)).rejects.toMatchObject({ + dependency: "envio", + code: "validation_failed", + }); + }); + + it("rejects configuration failures as caller errors without opening the circuit", () => { + expect(() => + createEnvioClient({ + endpoint: "https://secret:password@envio.example/graphql", + token: "envio-secret", + }), + ).toThrowError(DataPipelineError); + }); +}); + +describe("Envio candidate cursor adapter", () => { + const SECOND_BLOCK_HASH = `0x${"77".repeat(32)}` as const; + const SECOND_TRANSACTION_HASH = `0x${"88".repeat(32)}` as const; + + it("reads a bounded, strictly ordered page after an exclusive cursor", async () => { + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(request.query).toContain("query ProgrammableCandidatesAfter"); + expect(request.query).toContain( + '_nin: ["StockV1RewardVault", "StockV2V3RewardVault"]', + ); + expect(request.query).toContain("$afterBlock: numeric!"); + expect(request.query).toContain("$afterLogIndex: numeric!"); + expect(request.query).toContain("$afterCandidateId: String!"); + expect(request.query).toContain("blockGlobalLogIndex: asc"); + expect(request.query).toContain("{ id: { _gt: $afterCandidateId } }"); + expect(request.query).toContain("id: asc"); + expect(request.variables).toEqual({ + afterBlock: "25624130", + afterLogIndex: "-1", + afterCandidateId: "", + first: 2, + }); + expect(new Headers(init?.headers).get("authorization")).toBe( + "Bearer envio-secret", + ); + return json({ + data: { + ChainEvent: [ + candidate(), + placedCandidate({ + blockNumber: "25624132", + blockGlobalLogIndex: 0, + blockHash: SECOND_BLOCK_HASH, + transactionHash: SECOND_TRANSACTION_HASH, + }), + ], + }, + }); + }); + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + token: "envio-secret", + fetcher, + }); + + const page = await client.readCandidatesAfter({ + cursor: { + blockNumber: "25624130", + blockGlobalLogIndex: -1, + candidateId: "", + }, + limit: 2, + }); + + expect(page.map((row) => row.candidateId)).toEqual([ + CANDIDATE_ID, + `1:${SECOND_BLOCK_HASH}:${SECOND_TRANSACTION_HASH}:0`, + ]); + expect(fetcher).toHaveBeenCalledTimes(1); + }); + + it("keeps retired Stock reward-vault telemetry out of bounded source pages", async () => { + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(request.query).toContain("query ProgrammableCandidatesWindow"); + expect(request.query).toContain( + '_nin: ["StockV1RewardVault", "StockV2V3RewardVault"]', + ); + return json({ data: { ChainEvent: [] } }); + }); + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher, + }); + + await expect( + client.readCandidatesWindow({ + cursor: { + blockNumber: "25624130", + blockGlobalLogIndex: -1, + candidateId: "", + }, + throughBlock: "25650000", + limit: 32, + }), + ).resolves.toEqual([]); + }); + + it.each([ + [{ blockNumber: "-1", blockGlobalLogIndex: -1, candidateId: "" }, 10], + [{ blockNumber: "25624130", blockGlobalLogIndex: -2, candidateId: "" }, 10], + [{ blockNumber: "25624130", blockGlobalLogIndex: 1.5, candidateId: "" }, 10], + [ + { + blockNumber: "25624130", + blockGlobalLogIndex: 4_294_967_296, + candidateId: "", + }, + 10, + ], + [{ blockNumber: "25624130", blockGlobalLogIndex: 0, candidateId: "" }, 10], + [ + { + blockNumber: "25624130", + blockGlobalLogIndex: 0, + candidateId: "not-a-candidate-id", + }, + 10, + ], + [{ blockNumber: "25624130", blockGlobalLogIndex: -1, candidateId: "" }, 0], + [{ blockNumber: "25624130", blockGlobalLogIndex: -1, candidateId: "" }, 33], + ])("rejects an invalid cursor or limit before fetching", async (cursor, limit) => { + const fetcher = vi.fn(); + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher, + }); + + await expect( + client.readCandidatesAfter({ cursor, limit }), + ).rejects.toMatchObject({ code: "invalid_input" }); + expect(fetcher).not.toHaveBeenCalled(); + }); + + it("rejects a row at or before the exclusive cursor", async () => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ data: { ChainEvent: [candidate()] } }), + }); + + await expect( + client.readCandidatesAfter({ + cursor: { + blockNumber: "25624131", + blockGlobalLogIndex: 7, + candidateId: CANDIDATE_ID, + }, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("accepts the registered predecessor-block cursor used for generation zero", async () => { + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { + variables: Record; + }; + expect(body.variables.afterBlock).toBe("25624129"); + expect(body.variables.afterLogIndex).toBe("4294967295"); + expect(body.variables.afterCandidateId).toBe(""); + return json({ data: { ChainEvent: [] } }); + }); + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher, + }); + + await expect(client.readCandidatesWindow({ + cursor: { + blockNumber: "25624129", + blockGlobalLogIndex: 4_294_967_295, + candidateId: "", + }, + throughBlock: "25624130", + limit: 1, + })).resolves.toEqual([]); + expect(fetcher).toHaveBeenCalledTimes(1); + }); + + it("uses candidate identity as a stable-snapshot tie breaker", async () => { + const forkReplacement = placedCandidate({ + blockNumber: "25624131", + blockGlobalLogIndex: 7, + blockHash: SECOND_BLOCK_HASH, + transactionHash: SECOND_TRANSACTION_HASH, + }); + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json({ data: { ChainEvent: [forkReplacement] } }), + }); + + await expect( + client.readCandidatesAfter({ + cursor: { + blockNumber: "25624131", + blockGlobalLogIndex: 7, + candidateId: CANDIDATE_ID, + }, + }), + ).resolves.toEqual([ + expect.objectContaining({ + candidateId: + `1:${SECOND_BLOCK_HASH}:${SECOND_TRANSACTION_HASH}:7`, + }), + ]); + }); + + it("snapshots cursor fields before validating and requesting a page", async () => { + let ordinalReads = 0; + const cursor = { + blockNumber: "25624130", + get blockGlobalLogIndex() { + ordinalReads += 1; + return ordinalReads === 1 ? -1 : 0; + }, + candidateId: "", + }; + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async (_url, init) => { + const request = JSON.parse(String(init?.body)) as { + variables: Record; + }; + expect(request.variables).toMatchObject({ + afterBlock: "25624130", + afterLogIndex: "-1", + afterCandidateId: "", + }); + return json({ data: { ChainEvent: [] } }); + }, + }); + + await expect(client.readCandidatesAfter({ cursor })).resolves.toEqual([]); + expect(ordinalReads).toBe(1); + }); + + it("rejects a lexical predecessor at the same block and log position", async () => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => json({ data: { ChainEvent: [candidate()] } }), + }); + + await expect( + client.readCandidatesAfter({ + cursor: { + blockNumber: "25624131", + blockGlobalLogIndex: 7, + candidateId: + `1:${SECOND_BLOCK_HASH}:${SECOND_TRANSACTION_HASH}:7`, + }, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects duplicate or non-ascending candidate placement", async () => { + const second = placedCandidate({ + blockNumber: "25624132", + blockGlobalLogIndex: 0, + blockHash: SECOND_BLOCK_HASH, + transactionHash: SECOND_TRANSACTION_HASH, + }); + for (const rows of [[second, second], [second, candidate()]]) { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => json({ data: { ChainEvent: rows } }), + }); + + await expect( + client.readCandidatesAfter({ + cursor: { + blockNumber: "25624130", + blockGlobalLogIndex: -1, + candidateId: "", + }, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + } + }); + + it("rejects malformed page envelopes and oversized responses", async () => { + for (const response of [ + { data: { ChainEvent: null } }, + { data: { ChainEvent: [candidate(), candidate()] } }, + { data: { ChainEvent: [], unexpected: true } }, + ]) { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => json(response), + }); + + await expect( + client.readCandidatesAfter({ + cursor: { + blockNumber: "25624130", + blockGlobalLogIndex: -1, + candidateId: "", + }, + limit: 1, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + } + }); +}); + +describe("Envio progress adapter", () => { + it("derives readiness from official _meta while retaining the last handled event identity", async () => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + token: "envio-secret", + fetcher: async (_url, init) => { + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(request.query).toContain("_meta"); + expect(request.query).toContain("sourceBlock"); + expect(request.query).toContain("IndexerState_by_pk"); + expect(request.query).toContain("$stateId: String!"); + expect(request.variables).toEqual({ stateId: "ethereum-mainnet" }); + return json(progressPayload()); + }, + }); + + await expect( + client.readProgress({ requiredBlock: "25650002" }), + ).resolves.toEqual({ + chainId: 1, + deployment: "production-7f24e63", + schemaVersion: "1", + progressBlock: "25650010", + bufferBlock: "25650010", + sourceBlock: "25650022", + eventsProcessed: "51234", + lastHandledEventBlock: "25650000", + lastHandledEventBlockHash: BLOCK_HASH, + lastHandledEventTimestamp: "1785480000", + lastHandledEventTransactionHash: TRANSACTION_HASH, + lastHandledEventOccurrenceId: CANDIDATE_ID, + requiredBlock: "25650002", + lagBlocks: "0", + isReady: true, + }); + }); + + it("rejects malformed chain, schema, hash, occurrence, and future arithmetic", async () => { + for (const override of [ + { chainId: 10 }, + { schemaVersion: "2" }, + { progressBlockHash: "0x12" }, + { progressOccurrenceId: "not-an-occurrence" }, + { progressBlock: "-1" }, + ]) { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + token: "envio-secret", + fetcher: async () => + json(progressPayload({ state: override })), + }); + await expect( + client.readProgress({ requiredBlock: "25650002" }), + ).rejects.toMatchObject({ code: "validation_failed" }); + } + }); + + it.each([ + { + progressOccurrenceId: `1:${`0x${"77".repeat(32)}`}:${TRANSACTION_HASH}:7`, + }, + { + progressOccurrenceId: `1:${BLOCK_HASH}:${`0x${"88".repeat(32)}`}:7`, + }, + ])( + "rejects a well-formed progress occurrence with inconsistent embedded identity", + async (override) => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + token: "envio-secret", + fetcher: async () => + json(progressPayload({ state: override })), + }); + + await expect( + client.readProgress({ requiredBlock: "25650002" }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }, + ); + + it.each([ + ["chain", { chainId: 10 }], + ["progress after buffer", { progressBlock: 25_650_011 }], + ["buffer after source", { bufferBlock: 25_650_023 }], + ["negative progress", { progressBlock: -1 }], + ["negative event count", { eventsProcessed: -1 }], + ])("rejects invalid official _meta %s", async (_name, meta) => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => json(progressPayload({ meta })), + }); + + await expect( + client.readProgress({ requiredBlock: "25650002" }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects a last handled event beyond official Envio progress", async () => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json(progressPayload({ state: { progressBlock: "25650011" } })), + }); + + await expect( + client.readProgress({ requiredBlock: "25650002" }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("fails closed on an unexpected deployment label", async () => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json(progressPayload({ state: { deployment: "production-other" } })), + }); + + await expect( + client.readProgress({ requiredBlock: "25650002" }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it.each([ + ["source commit", { sourceCommit: "f".repeat(40) }], + ["config hash", { configSha256: `0x${"ff".repeat(32)}` }], + ["schema hash", { schemaSha256: `0x${"ff".repeat(32)}` }], + ["handler hash", { handlerSha256: `0x${"ff".repeat(32)}` }], + [ + "source registry hash", + { sourceRegistrySha256: `0x${"ff".repeat(32)}` }, + ], + ["event set hash", { eventSetSha256: `0x${"ff".repeat(32)}` }], + ["event count", { eventCount: 50 }], + ])("fails closed on an unexpected deployment %s", async (_name, state) => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => json(progressPayload({ state })), + }); + + await expect( + client.readProgress({ requiredBlock: "25650002" }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("reports official lag and readiness without treating a syncing indexer as malformed", async () => { + const client = createEnvioClient({ + endpoint: "https://envio.example/graphql", + fetcher: async () => + json(progressPayload({ meta: { isReady: false } })), + }); + + await expect( + client.readProgress({ requiredBlock: "25650015" }), + ).resolves.toMatchObject({ + progressBlock: "25650010", + requiredBlock: "25650015", + lagBlocks: "5", + isReady: false, + }); + }); +}); diff --git a/tests/data-pipeline/event-manifest.test.ts b/tests/data-pipeline/event-manifest.test.ts new file mode 100644 index 00000000..7d721db4 --- /dev/null +++ b/tests/data-pipeline/event-manifest.test.ts @@ -0,0 +1,314 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + encodeEventTopics, + parseAbiItem, + type AbiEvent, + type AbiParameter, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +import { + PROGRAMMABLE_EVENT_SIGNATURES, + canonicalizeAbiEventArguments, + decodeManifestEvent, +} from "../../lib/data-pipeline/event-manifest"; +import { canonicalPayloadJson } from "../../indexer/src/lib/payload-hash"; + +function configuredEventSignatures() { + const yaml = readFileSync( + resolve(process.cwd(), "indexer/config.yaml"), + "utf8", + ); + const manifest: Record = {}; + let inContracts = false; + let contractName: string | undefined; + + for (const line of yaml.split(/\r?\n/u)) { + if (line === "contracts:") { + inContracts = true; + continue; + } + if (line === "chains:") break; + if (!inContracts) continue; + + const contract = /^ - name: ([A-Za-z][A-Za-z0-9]*)$/u.exec(line); + if (contract) { + contractName = contract[1]; + manifest[contractName] = []; + continue; + } + const event = /^ - event: "([^"]+)"$/u.exec(line); + if (event && contractName) manifest[contractName].push(event[1]); + } + + return manifest; +} + +function encodeEvent( + signature: string, + args: Readonly>, +) { + const abi = parseAbiItem(`event ${signature}`) as AbiEvent; + const topics = encodeEventTopics({ + abi: [abi], + eventName: abi.name, + args, + }) as readonly Hex[]; + const nonIndexed = abi.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, + ) as readonly AbiParameter[]; + const data = encodeAbiParameters( + nonIndexed, + nonIndexed.map((input) => args[input.name!]), + ); + return { abi, topics, data }; +} + +const LAUNCH_SIGNATURE = + "MemeTokenLaunched(address indexed creator, address indexed token, bytes32 indexed poolId, address feeHook, address positionRecipient, uint256 positionTokenId, uint16 totalSwapFeeBps, bytes32 launchHash)"; +const LAUNCH_ARGS = { + creator: "0x1111111111111111111111111111111111111111", + token: "0x2222222222222222222222222222222222222222", + poolId: `0x${"33".repeat(32)}`, + feeHook: "0x4444444444444444444444444444444444444444", + positionRecipient: "0x5555555555555555555555555555555555555555", + positionTokenId: 42n, + totalSwapFeeBps: 100n, + launchHash: `0x${"66".repeat(32)}`, +} as const; +const LAUNCH_EVENT = encodeEvent(LAUNCH_SIGNATURE, LAUNCH_ARGS); +const LAUNCH_PROVIDER_PAYLOAD = JSON.parse( + canonicalPayloadJson(LAUNCH_ARGS), +) as Record; + +describe("Programmable runtime event manifest", () => { + it("exactly covers every contract/event pair configured by the indexer", () => { + expect(PROGRAMMABLE_EVENT_SIGNATURES).toEqual( + configuredEventSignatures(), + ); + }); + + it("strictly decodes a real launcher event and returns canonical payload", () => { + expect( + decodeManifestEvent({ + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + topics: LAUNCH_EVENT.topics, + data: LAUNCH_EVENT.data, + providerPayload: LAUNCH_PROVIDER_PAYLOAD, + }), + ).toEqual({ + creator: LAUNCH_ARGS.creator, + feeHook: LAUNCH_ARGS.feeHook, + launchHash: LAUNCH_ARGS.launchHash, + poolId: LAUNCH_ARGS.poolId, + positionRecipient: LAUNCH_ARGS.positionRecipient, + positionTokenId: "42", + token: LAUNCH_ARGS.token, + totalSwapFeeBps: "100", + }); + }); + + it("canonicalizes dynamic arrays, bigint values, hex, and object key order", () => { + const signature = + "CtoRewardConfigurationActivated(bytes32 indexed poolId, bytes32 indexed approvalReference, uint64 indexed configurationEpoch, bytes32 previousConfigurationHash, bytes32 newConfigurationHash, address[] beneficiaries, uint16[] sharesBps, uint256 effectiveTotalCreatorFeesReceived)"; + const args = { + poolId: `0x${"AA".repeat(32)}`, + approvalReference: `0x${"BB".repeat(32)}`, + configurationEpoch: 7n, + previousConfigurationHash: `0x${"CC".repeat(32)}`, + newConfigurationHash: `0x${"DD".repeat(32)}`, + beneficiaries: [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + ], + sharesBps: [6_000n, 4_000n], + effectiveTotalCreatorFeesReceived: 123_456_789n, + } as const; + const encoded = encodeEvent(signature, args); + const providerPayload = JSON.parse( + canonicalPayloadJson(args), + ) as Record; + + expect( + decodeManifestEvent({ + contractName: "ClassicV3RewardVault", + eventName: "CtoRewardConfigurationActivated", + topics: encoded.topics, + data: encoded.data, + providerPayload, + }), + ).toEqual(providerPayload); + expect(providerPayload).toMatchObject({ + configurationEpoch: "7", + effectiveTotalCreatorFeesReceived: "123456789", + poolId: `0x${"aa".repeat(32)}`, + sharesBps: ["6000", "4000"], + }); + }); + + it("canonicalizes signed int24 and uint24 fields as decimal strings", () => { + const signature = + "MemeLiquidityConfigured(address indexed token, uint256 totalSupply, uint256 tokenLiquidityAmount, uint256 lockedTokenDust, int24 initialTick, int24 tickLower, int24 tickUpper, uint24 lpFeePips, bytes32 launchHash)"; + const args = { + token: "0x1111111111111111111111111111111111111111", + totalSupply: 1_000_000_000n, + tokenLiquidityAmount: 999_999_999n, + lockedTokenDust: 1n, + initialTick: -120n, + tickLower: -887_220n, + tickUpper: 887_220n, + lpFeePips: 10_000n, + launchHash: `0x${"EE".repeat(32)}`, + } as const; + const encoded = encodeEvent(signature, args); + const providerPayload = JSON.parse( + canonicalPayloadJson(args), + ) as Record; + + expect( + decodeManifestEvent({ + contractName: "ClassicV2Launcher", + eventName: "MemeLiquidityConfigured", + topics: encoded.topics, + data: encoded.data, + providerPayload, + }), + ).toMatchObject({ + initialTick: "-120", + lpFeePips: "10000", + tickLower: "-887220", + tickUpper: "887220", + }); + }); + + it("canonicalizes uint8 and uint16 fields as decimal strings", () => { + const signature = + "MemeCreatorInitialBuyCustodyV2(address indexed deployer, address indexed token, address indexed custody, uint8 mode, uint16 durationDays, uint16 cliffDays, bytes32 configurationHash, bytes32 launchHash)"; + const args = { + deployer: "0x1111111111111111111111111111111111111111", + token: "0x2222222222222222222222222222222222222222", + custody: "0x3333333333333333333333333333333333333333", + mode: 2n, + durationDays: 365n, + cliffDays: 30n, + configurationHash: `0x${"44".repeat(32)}`, + launchHash: `0x${"55".repeat(32)}`, + } as const; + const encoded = encodeEvent(signature, args); + const providerPayload = JSON.parse( + canonicalPayloadJson(args), + ) as Record; + + expect( + decodeManifestEvent({ + contractName: "ClassicV3Launcher", + eventName: "MemeCreatorInitialBuyCustodyV2", + topics: encoded.topics, + data: encoded.data, + providerPayload, + }), + ).toMatchObject({ mode: "2", durationDays: "365", cliffDays: "30" }); + }); + + it("recursively canonicalizes tuple arrays and nested integer arrays", () => { + const abi = parseAbiItem( + "event Nested((uint8 level, int24 delta, address target)[] rules, uint16[][] weights)", + ) as AbiEvent; + + expect( + canonicalizeAbiEventArguments(abi.inputs, { + rules: [ + { + target: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + level: 2, + delta: -17, + }, + ], + weights: [ + [1, 2], + [3], + ], + }), + ).toEqual({ + rules: [ + { + delta: "-17", + level: "2", + target: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ], + weights: [["1", "2"], ["3"]], + }); + }); + + it("rejects a provider number where Envio emits a decimal string", () => { + expect(() => + decodeManifestEvent({ + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + topics: LAUNCH_EVENT.topics, + data: LAUNCH_EVENT.data, + providerPayload: { + ...LAUNCH_PROVIDER_PAYLOAD, + totalSwapFeeBps: 100, + }, + }), + ).toThrow(); + }); + + const mismatchCases: Array< + [ + string, + Partial[0]>, + ] + > = [ + [ + "contract name", + { contractName: "ClassicV2Hook" }, + ], + [ + "event name", + { eventName: "MemeLiquidityConfigured" }, + ], + [ + "topic0", + { + topics: [ + `0x${"99".repeat(32)}`, + ...LAUNCH_EVENT.topics.slice(1), + ], + }, + ], + ["indexed topic count", { topics: LAUNCH_EVENT.topics.slice(0, -1) }], + ["strict ABI data", { data: "0x" }], + [ + "provider payload", + { + providerPayload: { + ...LAUNCH_PROVIDER_PAYLOAD, + totalSwapFeeBps: "101", + }, + }, + ], + ]; + + it.each(mismatchCases)("rejects a mismatched %s", (_name, override) => { + expect(() => + decodeManifestEvent({ + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + topics: LAUNCH_EVENT.topics, + data: LAUNCH_EVENT.data, + providerPayload: LAUNCH_PROVIDER_PAYLOAD, + ...override, + }), + ).toThrow(); + }); +}); diff --git a/tests/data-pipeline/legacy-index-ops-route.test.ts b/tests/data-pipeline/legacy-index-ops-route.test.ts new file mode 100644 index 00000000..efd5e1a9 --- /dev/null +++ b/tests/data-pipeline/legacy-index-ops-route.test.ts @@ -0,0 +1,105 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + getOperationalOnchainDeployment: vi.fn(), + readLiveExploreModel: vi.fn(), + writeDurableExploreModel: vi.fn(), + writePortfolioHistorySnapshot: vi.fn(), +})); + +vi.mock("../../lib/onchain", () => ({ + getOperationalOnchainDeployment: mocks.getOperationalOnchainDeployment, + readLiveExploreModel: mocks.readLiveExploreModel, + writeDurableExploreModel: mocks.writeDurableExploreModel, +})); + +vi.mock("../../lib/profile/portfolio-history-storage.server", () => ({ + writePortfolioHistorySnapshot: mocks.writePortfolioHistorySnapshot, +})); + +import { NextRequest } from "next/server"; + +import { GET as getClosedAlias } from "../../app/api/ops/index/route"; +import { GET as getCanonicalIndex } from "../../app/api/ops/index-v2/route"; + +const SECRET = "legacy-index-test-secret-32-characters"; + +function request(secret = SECRET) { + return new NextRequest("https://programmable.family/api/ops/index-v2", { + headers: { authorization: `Bearer ${secret}` }, + }); +} + +describe("legacy index operations routes", () => { + beforeEach(() => { + process.env.CRON_SECRET = SECRET; + Object.values(mocks).forEach((mock) => mock.mockReset()); + mocks.getOperationalOnchainDeployment.mockReturnValue({ status: "ready" }); + mocks.readLiveExploreModel.mockResolvedValue({ status: "ready" }); + mocks.writeDurableExploreModel.mockResolvedValue({ + blockNumber: "25600000", + tokenCount: 265, + updated: true, + deepReleaseVersion: null, + deepLifecycleEvidenceHash: null, + }); + mocks.writePortfolioHistorySnapshot.mockResolvedValue({ + status: "stored", + path: "history.json", + tokenCount: 265, + blockNumber: "25600000", + }); + }); + + afterEach(() => { + delete process.env.CRON_SECRET; + vi.restoreAllMocks(); + }); + + it("rejects missing, weak and overlong UTF-8 secrets before any write", async () => { + const missing = await getCanonicalIndex( + new NextRequest("https://programmable.family/api/ops/index-v2"), + ); + expect(missing.status).toBe(401); + expect(missing.headers.get("cache-control")).toBe("no-store"); + + process.env.CRON_SECRET = "too-short"; + expect((await getCanonicalIndex(request("too-short"))).status).toBe(401); + + process.env.CRON_SECRET = "🌸".repeat(300); + expect((await getCanonicalIndex(request(SECRET))).status).toBe(401); + + expect(mocks.readLiveExploreModel).not.toHaveBeenCalled(); + expect(mocks.writeDurableExploreModel).not.toHaveBeenCalled(); + }); + + it("runs the durable refresh only through the canonical route", async () => { + vi.spyOn(console, "info").mockImplementation(() => undefined); + + const response = await getCanonicalIndex(request()); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toMatchObject({ + ok: true, + blockNumber: "25600000", + tokenCount: 265, + }); + expect(mocks.readLiveExploreModel).toHaveBeenCalledTimes(1); + expect(mocks.writeDurableExploreModel).toHaveBeenCalledTimes(1); + expect(mocks.writePortfolioHistorySnapshot).toHaveBeenCalledTimes(1); + }); + + it("keeps the old writer alias permanently closed", async () => { + const response = await getClosedAlias(); + + expect(response.status).toBe(410); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ + error: "Legacy index route closed", + code: "legacy_index_route_closed", + }); + expect(mocks.readLiveExploreModel).not.toHaveBeenCalled(); + expect(mocks.writeDurableExploreModel).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/data-pipeline/market-projector-postgres.test.ts b/tests/data-pipeline/market-projector-postgres.test.ts new file mode 100644 index 00000000..364dc95e --- /dev/null +++ b/tests/data-pipeline/market-projector-postgres.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { createMarketProjectorDatabaseGateway } from "../../lib/data-pipeline/market-projector-runtime.server"; +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; + +type RecordedQuery = Readonly<{ + text: string; + values: readonly PostgresParameter[]; +}>; + +class FakeMarketExecutor implements PostgresExecutor { + readonly queries: RecordedQuery[] = []; + readonly close = vi.fn(async () => undefined); + + constructor( + private readonly sessionUser = "programmable_reconciler_login", + private readonly currentRole = "programmable_reconciler", + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if (text === "select session_user::text as session_user") { + return [ + { session_user: this.sessionUser }, + ] as unknown as readonly Row[]; + } + if ( + text === + "select session_user::text as session_user, current_role::text as current_role" + ) { + return [ + { + session_user: this.sessionUser, + current_role: this.currentRole, + }, + ] as unknown as readonly Row[]; + } + return [] as unknown as readonly Row[]; + }, + }); + } +} + +describe("market projector Postgres gateway", () => { + it("authenticates the dedicated login and verifies the narrow capability role", async () => { + const executor = new FakeMarketExecutor(); + const gateway = createMarketProjectorDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "ok")).resolves.toBe("ok"); + expect(executor.queries.map(({ text }) => text)).toEqual([ + "select session_user::text as session_user", + "set local role programmable_reconciler", + "set local statement_timeout = '900ms'", + "set local lock_timeout = '200ms'", + "set local idle_in_transaction_session_timeout = '2000ms'", + "select session_user::text as session_user, current_role::text as current_role", + ]); + }); + + it.each([ + "postgres", + "programmable_reconciler_login_admin", + "supabase_admin", + ])("fails before SET ROLE for authenticated login %s", async (sessionUser) => { + const executor = new FakeMarketExecutor(sessionUser); + const gateway = createMarketProjectorDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "never")).rejects.toMatchObject({ + name: "DataPipelineError", + dependency: "postgres", + code: "invalid_input", + retryable: false, + }); + expect(executor.queries.map(({ text }) => text)).toEqual([ + "select session_user::text as session_user", + ]); + }); + + it("fails when SET ROLE does not produce the reconciler capability", async () => { + const executor = new FakeMarketExecutor( + "programmable_reconciler_login", + "programmable_reconciler_login", + ); + const gateway = createMarketProjectorDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "never")).rejects.toMatchObject({ + name: "DataPipelineError", + dependency: "postgres", + code: "invalid_input", + retryable: false, + }); + expect(executor.queries.at(-1)?.text).toBe( + "select session_user::text as session_user, current_role::text as current_role", + ); + }); +}); diff --git a/tests/data-pipeline/market-projector-route.test.ts b/tests/data-pipeline/market-projector-route.test.ts new file mode 100644 index 00000000..99c46e53 --- /dev/null +++ b/tests/data-pipeline/market-projector-route.test.ts @@ -0,0 +1,147 @@ +import { NextRequest } from "next/server"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + run: vi.fn(), + safeError: vi.fn(() => ({ + dependency: "rpc", + code: "dependency_unavailable", + retryable: true, + })), +})); + +vi.mock("../../lib/data-pipeline/market-projector-runtime.server", () => ({ + runConfiguredMarketProjectorCycle: mocks.run, + safeMarketProjectorError: mocks.safeError, +})); + +import { GET } from "../../app/api/ops/market-projector/route"; + +describe("market projector ops route", () => { + const cronSecret = "market-projector-secret-at-least-32-bytes"; + + beforeEach(() => { + vi.clearAllMocks(); + vi.stubEnv("CRON_SECRET", cronSecret); + mocks.run.mockResolvedValue({ + status: "committed", + releaseId: "classic-v3", + poolId: `0x${"11".repeat(32)}`, + blockNumber: "200", + lagBlocks: "4", + closeCount: 8, + candleCount: 1, + caughtUp: false, + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("requires the timing-safe cron bearer boundary", async () => { + const response = await GET( + new NextRequest("https://programmable.family/api/ops/market-projector"), + ); + + expect(response.status).toBe(401); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + expect(mocks.run).not.toHaveBeenCalled(); + }); + + it("returns only the finite safe cycle result", async () => { + const response = await GET( + new NextRequest("https://programmable.family/api/ops/market-projector", { + headers: { authorization: `Bearer ${cronSecret}` }, + }), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + expect(await response.json()).toEqual({ + status: "committed", + releaseId: "classic-v3", + poolId: `0x${"11".repeat(32)}`, + blockNumber: "200", + lagBlocks: "4", + closeCount: 8, + candleCount: 1, + caughtUp: false, + }); + }); + + it("returns the bounded disabled result without treating it as a failure", async () => { + mocks.run.mockResolvedValue({ + status: "disabled", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }); + const response = await GET( + new NextRequest("https://programmable.family/api/ops/market-projector", { + headers: { authorization: `Bearer ${cronSecret}` }, + }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + status: "disabled", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }); + }); + + it("returns a bounded busy result for an overlapping scheduled run", async () => { + mocks.run.mockResolvedValue({ + status: "busy", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }); + const response = await GET( + new NextRequest("https://programmable.family/api/ops/market-projector", { + headers: { authorization: `Bearer ${cronSecret}` }, + }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + status: "busy", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }); + }); + + it("rejects cron secrets outside the bounded credential length", async () => { + vi.stubEnv("CRON_SECRET", "too-short"); + const response = await GET( + new NextRequest("https://programmable.family/api/ops/market-projector", { + headers: { authorization: "Bearer too-short" }, + }), + ); + + expect(response.status).toBe(401); + expect(mocks.run).not.toHaveBeenCalled(); + }); + + it("does not disclose provider or database failures", async () => { + mocks.run.mockRejectedValue(new Error("postgres://user:secret@host/db")); + const response = await GET( + new NextRequest("https://programmable.family/api/ops/market-projector", { + headers: { authorization: `Bearer ${cronSecret}` }, + }), + ); + + expect(response.status).toBe(503); + expect(await response.json()).toEqual({ + error: "Market projection failed", + }); + expect(mocks.safeError).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/data-pipeline/market-projector-runtime.test.ts b/tests/data-pipeline/market-projector-runtime.test.ts new file mode 100644 index 00000000..ff49d19e --- /dev/null +++ b/tests/data-pipeline/market-projector-runtime.test.ts @@ -0,0 +1,869 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it, vi } from "vitest"; +import { keccak256, toBytes } from "viem"; + +vi.mock("server-only", () => ({})); + +import { + createDualRpcMarketReader, + createPostgresMarketProjectorStore, + MARKET_GRAPH_QUERY_CONTRACT, + MARKET_GRAPH_SCHEMA_COMMITMENT, + runConfiguredMarketProjectorCycle, + runMarketProjectorCycle, + type MarketAnalytics, + type MarketCloseAnchor, + type MarketPoolPlan, + type MarketProjectorStore, + type MarketRpc, + type PreparedMarketPage, +} from "../../lib/data-pipeline/market-projector-runtime.server"; +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; +import type { + CandleAnalytics, + PoolSnapshot, +} from "../../lib/data-pipeline/uniswap"; +import { + OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + UNISWAP_ANALYTICS_QUERY_CONTRACT, +} from "../../lib/data-pipeline/uniswap"; + +const POOL_ID = `0x${"11".repeat(32)}` as const; +const BLOCK_HASH = `0x${"22".repeat(32)}` as const; +const GRAPH_COMMITMENT = `0x${"33".repeat(32)}` as const; +const NATIVE = "0x0000000000000000000000000000000000000000" as const; +const TOKEN = "0x1111111111111111111111111111111111111111" as const; +const HOOK = "0x2222222222222222222222222222222222222222" as const; +const TARGET_TIME = new Date("2026-07-31T12:03:20.000Z"); + +function canonicalJson(value: unknown): string { + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + return JSON.stringify(value); + } + if (typeof value === "number") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (typeof value === "object") { + const entries = Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`); + return `{${entries.join(",")}}`; + } + throw new Error("unsupported canonical value"); +} + +function graphSchemaCommitment(value: unknown) { + return keccak256( + toBytes( + `programmable:market-projector:graph-query-contract:v1\0${canonicalJson(value)}`, + ), + ); +} + +function snapshot(): PoolSnapshot { + return { + id: POOL_ID, + createdAtTimestamp: "1785499200", + createdAtBlockNumber: "100", + token0: { id: NATIVE, decimals: 18 }, + token1: { id: TOKEN, decimals: 18 }, + hooks: HOOK, + appliedFeeTier: "0", + tickSpacing: 60, + liquidity: "1000000000000000000", + sqrtPriceX96: "79228162514264337593543950336", + tick: 0, + transactionCount: "25", + marketVolumeToken0: "10", + marketVolumeToken1: "10", + marketVolumeUsd: "20", + totalValueLockedToken0: "5", + totalValueLockedToken1: "5", + totalValueLockedUsd: "10", + }; +} + +function candle(periodStart: number): CandleAnalytics { + return { + id: `hour-${periodStart}`, + periodStart, + poolId: POOL_ID, + liquidity: "1000000000000000000", + sqrtPriceX96: "79228162514264337593543950336", + token0Price: "1", + token1Price: "1", + tick: 0, + tvlUsd: "10", + marketVolumeToken0: "10", + marketVolumeToken1: "10", + marketVolumeUsd: "20", + feesUsd: "0.2", + transactionCount: "25", + open: "1", + high: "1", + low: "1", + close: "1", + }; +} + +function cursor( + overrides: Partial> = {}, +) { + return { + id: "11111111-1111-8111-8111-111111111111", + epochId: "22222222-2222-8222-8222-222222222222", + pointerGeneration: "1", + cursorGeneration: "1", + reorgGeneration: "0", + sourceCheckpointId: "33333333-3333-8333-8333-333333333333", + sourceCheckpointGeneration: "1", + sourceReorgGeneration: "0", + blockEvidenceId: "44444444-4444-8444-8444-444444444444", + blockNumber: "100", + blockHash: `0x${"44".repeat(32)}` as const, + providerCursor: "block:100:4444444444444444", + hourCoverageEnd: null, + dayCoverageEnd: null, + advancedAt: new Date("2026-07-31T12:01:00.000Z"), + ...overrides, + }; +} + +function plan(overrides: Partial = {}): MarketPoolPlan { + return { + scope: { releaseId: "classic-v3", modelId: "classic", sourceGroup: "core" }, + epochId: "22222222-2222-8222-8222-222222222222", + pointerGeneration: "1", + sourceCheckpointId: "55555555-5555-8555-8555-555555555555", + sourceCheckpointGeneration: "2", + sourceReorgGeneration: "0", + sourceCheckpointBlockNumber: "200", + sourceCheckpointBlockHash: BLOCK_HASH, + sourceCheckpointBlockEvidenceId: "66666666-6666-8666-8666-666666666666", + token: TOKEN, + poolKey: { + poolId: POOL_ID, + currency0: NATIVE, + currency1: TOKEN, + hooks: HOOK, + fee: 0, + tickSpacing: 60, + token0Decimals: 18, + token1Decimals: 18, + }, + totalSupply: "1000000000000000000000000000", + launchBlockNumber: "100", + launchBlockTimestamp: new Date("2026-07-31T12:00:10.000Z"), + cursor: cursor(), + ...overrides, + }; +} + +function anchor(block: number): MarketCloseAnchor { + const hex = block.toString(16).padStart(64, "0"); + return { + occurrenceId: `${hex.slice(0, 8)}-${hex.slice(8, 12)}-8${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`, + logicalEventId: `${hex.slice(0, 8)}-${hex.slice(8, 12)}-9${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`, + blockEvidenceId: `${hex.slice(0, 8)}-${hex.slice(8, 12)}-a${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`, + blockNumber: String(block), + blockHash: `0x${hex}`, + blockTimestamp: new Date(TARGET_TIME.getTime() + block * 1_000), + transactionHash: `0x${(block + 1).toString(16).padStart(64, "0")}`, + transactionIndex: "1", + blockGlobalLogIndex: "1", + }; +} + +function analytics(overrides: Record = {}): MarketAnalytics { + return { + readPoolSnapshot: vi.fn( + async (input: { block: { number: string; hash: string } }) => ({ + status: "ready", + data: snapshot(), + provenance: { + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + blockNumber: input.block.number, + blockHash: input.block.hash, + }, + }), + ), + readHourSeries: vi.fn( + async (input: { + from: number; + block: { number: string; hash: string }; + }) => ({ + status: "ready", + data: [candle(input.from)], + provenance: { + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + blockNumber: input.block.number, + blockHash: input.block.hash, + }, + }), + ), + readDaySeries: vi.fn( + async (input: { block: { number: string; hash: string } }) => ({ + status: "ready", + data: [], + provenance: { + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + blockNumber: input.block.number, + blockHash: input.block.hash, + }, + }), + ), + ...overrides, + } as unknown as MarketAnalytics; +} + +function rpc(): MarketRpc { + const readChainlinkBlock: MarketRpc["readChainlinkBlock"] = vi.fn( + async ({ blockNumber, expectedBlockHash }) => ({ + blockNumber, + blockHash: expectedBlockHash, + blockTimestamp: new Date(TARGET_TIME), + rawResult: `0x${"00".repeat(160)}` as const, + feedRoundId: "1", + answer: "350000000000", + feedUpdatedAt: new Date(TARGET_TIME.getTime() - 10_000), + }), + ); + return { + readChainlinkBlock, + }; +} + +function store( + plans: readonly MarketPoolPlan[], + anchors: readonly MarketCloseAnchor[] = [], +) { + const committed: PreparedMarketPage[] = []; + const value: MarketProjectorStore = { + tryAcquireLease: vi.fn(async () => ({ + holderId: "market-projector:test", + generation: "1", + tokenHash: `0x${"ab".repeat(32)}` as const, + acquiredAt: new Date("2026-07-31T12:00:00.000Z"), + expiresAt: new Date("2026-07-31T12:01:30.000Z"), + })), + releaseLease: vi.fn(async () => undefined), + loadPlans: vi.fn(async () => plans), + listCloseAnchors: vi.fn(async () => anchors), + resolveGraphProvider: vi.fn( + async () => "77777777-7777-8777-8777-777777777777", + ), + commit: vi.fn(async (page: PreparedMarketPage) => { + committed.push(page); + const lag = + BigInt(page.plan.sourceCheckpointBlockNumber) - + BigInt(page.target.blockNumber); + return { + status: lag === 0n ? ("caught-up" as const) : ("committed" as const), + releaseId: page.plan.scope.releaseId, + poolId: page.plan.poolKey.poolId, + blockNumber: page.target.blockNumber, + lagBlocks: lag.toString(), + closeCount: page.closes.length, + candleCount: page.candles.length, + caughtUp: lag === 0n, + }; + }), + close: vi.fn(async () => undefined), + }; + return { value, committed }; +} + +const graphProvider = { + redactedIdentity: "uniswap-v4-official", + deploymentCommitment: GRAPH_COMMITMENT, + schemaCommitment: `0x${"55".repeat(32)}` as const, +}; + +function uintWord(value: bigint) { + return value.toString(16).padStart(64, "0"); +} + +function chainlinkResult(input: { answer: bigint; updatedAt: bigint }) { + return `0x${[1n, input.answer, input.updatedAt - 10n, input.updatedAt, 1n] + .map(uintWord) + .join("")}`; +} + +function rpcFetcher(input: { quicknodeAnswer?: bigint; updatedAt: bigint }) { + return vi.fn(async (endpoint: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + id: number; + method: string; + params: unknown[]; + }; + const quicknode = endpoint.includes("quiknode.pro"); + const result = + request.method === "eth_getBlockByHash" + ? { + number: "0xc8", + hash: BLOCK_HASH, + timestamp: `0x${BigInt(Math.floor(TARGET_TIME.getTime() / 1_000)).toString(16)}`, + } + : chainlinkResult({ + answer: quicknode + ? (input.quicknodeAnswer ?? 350000000000n) + : 350000000000n, + updatedAt: input.updatedAt, + }); + return new Response( + JSON.stringify({ jsonrpc: "2.0", id: request.id, result }), + ); + }); +} + +describe("market projector runtime", () => { + it("stays disabled without constructing configured dependencies", async () => { + await expect(runConfiguredMarketProjectorCycle({ env: {} })).resolves.toEqual( + { + status: "disabled", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }, + ); + await expect( + runConfiguredMarketProjectorCycle({ + env: { PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE: "false" }, + }), + ).resolves.toEqual({ + status: "disabled", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: false, + }); + }); + + it("fails closed on an ambiguous activation value", async () => { + await expect( + runConfiguredMarketProjectorCycle({ + env: { PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE: "yes" }, + }), + ).rejects.toMatchObject({ + dependency: "config", + code: "invalid_input", + }); + }); + + it("binds Graph provenance to the exact query documents and parser contract", () => { + expect(MARKET_GRAPH_QUERY_CONTRACT.analytics).toBe( + UNISWAP_ANALYTICS_QUERY_CONTRACT, + ); + expect(graphSchemaCommitment(MARKET_GRAPH_QUERY_CONTRACT)).toBe( + MARKET_GRAPH_SCHEMA_COMMITMENT, + ); + expect(MARKET_GRAPH_SCHEMA_COMMITMENT).toBe( + "0xd0d2087059ca0a7c1e7c633999ff75ea34fcc00d42cee8985a79d0ef76e6813c", + ); + const parserSource = readFileSync( + resolve(process.cwd(), UNISWAP_ANALYTICS_QUERY_CONTRACT.parser.sourcePath), + "utf8", + ); + expect(keccak256(toBytes(parserSource))).toBe( + UNISWAP_ANALYTICS_QUERY_CONTRACT.parser.sourceCommitment, + ); + + const changedQuery = { + ...MARKET_GRAPH_QUERY_CONTRACT, + analytics: { + ...UNISWAP_ANALYTICS_QUERY_CONTRACT, + queries: { + ...UNISWAP_ANALYTICS_QUERY_CONTRACT.queries, + poolSnapshot: `${UNISWAP_ANALYTICS_QUERY_CONTRACT.queries.poolSnapshot}\n# changed`, + }, + }, + }; + const changedParser = { + ...MARKET_GRAPH_QUERY_CONTRACT, + analytics: { + ...UNISWAP_ANALYTICS_QUERY_CONTRACT, + parser: { + ...UNISWAP_ANALYTICS_QUERY_CONTRACT.parser, + contractVersion: "uniswap-analytics-parser-v2", + }, + }, + }; + expect(graphSchemaCommitment(changedQuery)).not.toBe( + MARKET_GRAPH_SCHEMA_COMMITMENT, + ); + expect(graphSchemaCommitment(changedParser)).not.toBe( + MARKET_GRAPH_SCHEMA_COMMITMENT, + ); + }); + + it("does not open a commit for a fully caught-up source cursor", async () => { + const caughtUp = plan({ + cursor: cursor({ + blockNumber: "200", + blockHash: BLOCK_HASH, + sourceCheckpointGeneration: "2", + }), + }); + const fixture = store([caughtUp]); + + await expect( + runMarketProjectorCycle({ + store: fixture.value, + analytics: analytics(), + rpc: rpc(), + graphProvider, + }), + ).resolves.toEqual({ + status: "idle", + lagBlocks: "0", + closeCount: 0, + candleCount: 0, + caughtUp: true, + }); + expect(fixture.value.commit).not.toHaveBeenCalled(); + }); + + it("bounds each page at eight canonical fee blocks", async () => { + const anchors = Array.from({ length: 8 }, (_, index) => + anchor(101 + index), + ); + const fixture = store([plan()], anchors); + + await runMarketProjectorCycle({ + store: fixture.value, + analytics: analytics(), + rpc: rpc(), + graphProvider, + }); + + expect(fixture.committed).toHaveLength(1); + expect(fixture.committed[0]).toMatchObject({ + target: { blockNumber: "108", blockHash: anchors[7]!.blockHash }, + targetEvidenceId: anchors[7]!.blockEvidenceId, + isReorg: false, + }); + expect(fixture.committed[0]!.closes).toHaveLength(8); + }); + + it("processes a bounded batch instead of one pool per invocation", async () => { + const plans = Array.from({ length: 5 }, (_, index) => + plan({ + cursor: cursor({ + advancedAt: new Date(Date.UTC(2026, 6, 31, 11, index, 0)), + }), + }), + ); + const fixture = store(plans); + + await runMarketProjectorCycle({ + store: fixture.value, + analytics: analytics(), + rpc: rpc(), + graphProvider, + }); + + expect(fixture.committed).toHaveLength(4); + }); + + it("fails before commit when the exact Graph snapshot is pending", async () => { + const fixture = store([plan()]); + const pending = analytics({ + readPoolSnapshot: vi.fn(async () => ({ + status: "pending", + reason: "dependency_unavailable", + })), + }); + + await expect( + runMarketProjectorCycle({ + store: fixture.value, + analytics: pending, + rpc: rpc(), + graphProvider, + }), + ).rejects.toMatchObject({ + dependency: "uniswap", + code: "dependency_unavailable", + }); + expect(fixture.value.commit).not.toHaveBeenCalled(); + }); + + it("does not let a failed pool consume the four-success cycle budget", async () => { + const poolIds = ["10", "20", "30", "40", "50"].map( + (prefix) => `0x${prefix.repeat(32)}` as const, + ); + const fixture = store( + poolIds.map((poolId, index) => + plan({ + poolKey: { ...plan().poolKey, poolId }, + cursor: cursor({ + advancedAt: new Date(Date.UTC(2026, 6, 31, 11, index, 0)), + }), + }), + ), + ); + const marketAnalytics = analytics({ + readPoolSnapshot: vi.fn( + async (input: { + poolKey: { poolId: typeof POOL_ID }; + block: { number: string; hash: string }; + }) => { + if (input.poolKey.poolId === poolIds[0]) { + return { + status: "pending" as const, + reason: "dependency_unavailable" as const, + }; + } + return { + status: "ready" as const, + data: { ...snapshot(), id: input.poolKey.poolId }, + provenance: { + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + blockNumber: input.block.number, + blockHash: input.block.hash, + }, + }; + }, + ), + }); + const warning = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await expect( + runMarketProjectorCycle({ + store: fixture.value, + analytics: marketAnalytics, + rpc: rpc(), + graphProvider, + }), + ).rejects.toMatchObject({ + dependency: "uniswap", + code: "dependency_unavailable", + }); + + expect(fixture.committed.map((page) => page.plan.poolKey.poolId)).toEqual( + poolIds.slice(1), + ); + expect(warning).toHaveBeenCalledWith( + "Market projector skipped one pool", + expect.objectContaining({ poolId: poolIds[0] }), + ); + warning.mockRestore(); + }); + + it("rescans the current block when a later source checkpoint arrives", async () => { + const currentHash = cursor().blockHash; + const laterAnchor = { + ...anchor(100), + blockHash: currentHash, + blockEvidenceId: "66666666-6666-8666-8666-666666666666", + } satisfies MarketCloseAnchor; + const sameBlock = plan({ + sourceCheckpointBlockNumber: "100", + sourceCheckpointBlockHash: currentHash, + cursor: cursor({ + blockNumber: "100", + blockHash: currentHash, + sourceCheckpointGeneration: "1", + }), + }); + const fixture = store([sameBlock], [laterAnchor]); + + await runMarketProjectorCycle({ + store: fixture.value, + analytics: analytics(), + rpc: rpc(), + graphProvider, + }); + + expect(fixture.value.listCloseAnchors).toHaveBeenCalledWith( + expect.objectContaining({ + fromBlockExclusive: "99", + toBlockInclusive: "100", + }), + ); + expect(fixture.committed).toHaveLength(1); + expect(fixture.committed[0]!.closes).toHaveLength(1); + }); + + it("persists exact interval fees and transaction counts for candles", async () => { + const calls: Array<{ text: string; values: readonly unknown[] }> = []; + const closingId = "99999999-9999-8999-8999-999999999999"; + const globalId = "aaaaaaaa-aaaa-8aaa-8aaa-aaaaaaaaaaaa"; + const transaction: PostgresTransaction = { + async query>( + text: string, + values: readonly PostgresParameter[] = [], + ) { + calls.push({ text, values }); + if (text === "select session_user::text as session_user") { + return [ + { session_user: "programmable_reconciler_login" }, + ] as unknown as readonly Row[]; + } + if ( + text === + "select session_user::text as session_user, current_role::text as current_role" + ) { + return [ + { + session_user: "programmable_reconciler_login", + current_role: "programmable_reconciler", + }, + ] as unknown as readonly Row[]; + } + if (text.includes("get_market_global_snapshot_v1")) { + return [{ id: globalId }] as unknown as readonly Row[]; + } + if (text.includes("resolve_market_candle_close_v1")) { + return [{ id: closingId }] as unknown as readonly Row[]; + } + if (text.includes("try_acquire_market_projector_runtime_lease_v1")) { + return [ + { + acquired: true, + lease_generation: "1", + acquired_at: "2026-07-31T12:00:00.000Z", + expires_at: "2026-07-31T12:01:30.000Z", + }, + ] as unknown as readonly Row[]; + } + if (text.includes("assert_market_projector_runtime_lease_v1")) { + return [{ valid: true }] as unknown as readonly Row[]; + } + if (text.includes("release_market_projector_runtime_lease_v1")) { + return [{ released: true }] as unknown as readonly Row[]; + } + return []; + }, + }; + const executor: PostgresExecutor = { + transaction: async (work) => work(transaction), + close: async () => undefined, + }; + const postgresStore = createPostgresMarketProjectorStore({ + executor, + sourceProjectorVersion: "projector-v1", + rpcProviders: [ + { + identity: "rpc-a", + endpointCommitment: `0x${"aa".repeat(32)}`, + endpointOriginCommitment: `0x${"ab".repeat(32)}`, + }, + { + identity: "rpc-b", + endpointCommitment: `0x${"ba".repeat(32)}`, + endpointOriginCommitment: `0x${"bb".repeat(32)}`, + }, + ], + uuid: vi + .fn() + .mockReturnValueOnce("11111111-1111-4111-8111-111111111111") + .mockReturnValueOnce("22222222-2222-4222-8222-222222222222"), + now: () => new Date("2026-07-31T12:00:00.000Z"), + }); + const marketPlan = plan(); + const interval = { + ...candle(Math.floor(Date.parse("2026-07-31T11:00:00.000Z") / 1_000)), + feesUsd: "7.5", + transactionCount: "9", + }; + const page: PreparedMarketPage = { + plan: marketPlan, + graphProviderId: "77777777-7777-8777-8777-777777777777", + targetEvidenceId: marketPlan.sourceCheckpointBlockEvidenceId, + target: await rpc().readChainlinkBlock({ + blockNumber: marketPlan.sourceCheckpointBlockNumber, + expectedBlockHash: marketPlan.sourceCheckpointBlockHash, + }), + targetSnapshot: snapshot(), + targetToken0Price: "1", + targetToken1Price: "1", + closes: [], + candles: [ + { + interval: "hour", + periodStart: new Date("2026-07-31T11:00:00.000Z"), + periodEnd: new Date("2026-07-31T12:00:00.000Z"), + data: interval, + }, + ], + nextHourCoverageEnd: new Date("2026-07-31T12:00:00.000Z"), + nextDayCoverageEnd: null, + providerCursor: "block:200:2222222222222222", + pageCommitment: `0x${"cc".repeat(32)}`, + isReorg: false, + }; + + const lease = await postgresStore.tryAcquireLease(); + expect(lease).not.toBeNull(); + await postgresStore.commit(page); + await postgresStore.releaseLease(lease!); + + const detail = calls.find((call) => + call.text.includes("append_market_candle_details_v2"), + ); + expect(detail?.values[5]).toBe("7.5"); + expect(detail?.values[6]).toBe("9"); + expect( + calls.filter( + ({ text }) => text === "select session_user::text as session_user", + ), + ).toHaveLength(3); + expect( + calls.filter( + ({ text }) => text === "set local role programmable_reconciler", + ), + ).toHaveLength(3); + expect( + calls.filter( + ({ text }) => + text === + "select session_user::text as session_user, current_role::text as current_role", + ), + ).toHaveLength(3); + }); + + it("rebuilds from launch when the source reorg generation advances", async () => { + const reorg = plan({ + pointerGeneration: "2", + sourceReorgGeneration: "1", + launchBlockTimestamp: new Date("2026-07-31T10:00:10.000Z"), + cursor: cursor({ + pointerGeneration: "1", + sourceReorgGeneration: "0", + hourCoverageEnd: new Date("2026-07-31T12:00:00.000Z"), + }), + }); + const fixture = store([reorg]); + const marketAnalytics = analytics(); + + await runMarketProjectorCycle({ + store: fixture.value, + analytics: marketAnalytics, + rpc: rpc(), + graphProvider, + }); + + expect(fixture.committed[0]!.isReorg).toBe(true); + expect(fixture.value.listCloseAnchors).toHaveBeenCalledWith( + expect.objectContaining({ fromBlockExclusive: "99" }), + ); + expect(marketAnalytics.readHourSeries).toHaveBeenCalledWith( + expect.objectContaining({ + from: Math.floor( + new Date("2026-07-31T10:00:00.000Z").getTime() / 1_000, + ), + toExclusive: Math.floor( + new Date("2026-07-31T12:00:00.000Z").getTime() / 1_000, + ), + }), + ); + }); + + it("rebuilds a caught-up pool when its current release epoch changes", async () => { + const nextEpoch = plan({ + epochId: "88888888-8888-8888-8888-888888888888", + cursor: cursor({ blockNumber: "200", blockHash: BLOCK_HASH }), + }); + const fixture = store([nextEpoch]); + + await runMarketProjectorCycle({ + store: fixture.value, + analytics: analytics(), + rpc: rpc(), + graphProvider, + }); + + expect(fixture.committed).toHaveLength(1); + expect(fixture.committed[0]!.isReorg).toBe(true); + }); + + it("rejects disagreement between the independent RPC results", async () => { + const fetcher = rpcFetcher({ + quicknodeAnswer: 350000000001n, + updatedAt: BigInt(Math.floor(TARGET_TIME.getTime() / 1_000) - 10), + }); + const reader = createDualRpcMarketReader({ + endpoints: [ + "https://eth-mainnet.g.alchemy.com/v2/abcdefgh", + "https://blue.quiknode.pro/abcdefgh/", + ], + fetcher, + }); + + await expect( + reader.readChainlinkBlock({ + blockNumber: "200", + expectedBlockHash: BLOCK_HASH, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "provider-mismatch" }, + }); + const requests = fetcher.mock.calls.map( + (call) => + JSON.parse(String(call[1]?.body)) as { + method: string; + params: unknown[]; + }, + ); + expect( + requests + .filter((request) => request.method === "eth_getBlockByHash") + .map(({ method, params }) => ({ method, params })), + ).toEqual([ + { method: "eth_getBlockByHash", params: [BLOCK_HASH, false] }, + { method: "eth_getBlockByHash", params: [BLOCK_HASH, false] }, + ]); + expect(requests.filter((request) => request.method === "eth_call")).toEqual( + [ + expect.objectContaining({ + params: [ + expect.any(Object), + { blockHash: BLOCK_HASH, requireCanonical: true }, + ], + }), + expect.objectContaining({ + params: [ + expect.any(Object), + { blockHash: BLOCK_HASH, requireCanonical: true }, + ], + }), + ], + ); + }); + + it("rejects a stale Chainlink answer at the exact historical block", async () => { + const reader = createDualRpcMarketReader({ + endpoints: [ + "https://eth-mainnet.g.alchemy.com/v2/abcdefgh", + "https://blue.quiknode.pro/abcdefgh/", + ], + fetcher: rpcFetcher({ + updatedAt: BigInt(Math.floor(TARGET_TIME.getTime() / 1_000) - 3_601), + }), + }); + + await expect( + reader.readChainlinkBlock({ + blockNumber: "200", + expectedBlockHash: BLOCK_HASH, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "chainlink-freshness" }, + }); + }); +}); diff --git a/tests/data-pipeline/performance-contract.test.ts b/tests/data-pipeline/performance-contract.test.ts new file mode 100644 index 00000000..d99bb964 --- /dev/null +++ b/tests/data-pipeline/performance-contract.test.ts @@ -0,0 +1,1324 @@ +import { createHmac } from "node:crypto"; +import { + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as deployPolicy from "../../scripts/perf/read-model-deploy-policy.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as gateCore from "../../scripts/perf/read-model-gate-core.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as liveVerifier from "../../scripts/perf/read-model-live-verifier.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as providerBinding from "../../scripts/perf/read-model-provider-binding.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as releaseProbe from "../../scripts/perf/read-model-release-probe.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as sourceContracts from "../../scripts/perf/read-model-source-contracts.mjs"; + +const temporaryDirectories: string[] = []; +const GIT_HEAD = "1".repeat(40); +const DEPLOYMENT_ID = `dpl_${"A".repeat(24)}`; +const TARGET_URL = "https://programmable-perf-abc.vercel.app/"; +const RUNTIME_CAPTURE_PATH_FIXTURE = + "/api/ops/read-model-performance-capture"; +const CAPTURE_NONCE = `0x${"55".repeat(32)}`; +const RUNTIME_RPC_ENVIRONMENT = { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: + "https://eth-mainnet.g.alchemy.com/v2/abcdefgh", + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: + "https://programmable.quiknode.pro/abcdefgh", +}; +const RUNTIME_PROVIDER_BINDINGS = + providerBinding.runtimeProductionProviderBindingsFromUrls( + RUNTIME_RPC_ENVIRONMENT, + ); +const ENDPOINT_COMMITMENTS = Object.fromEntries( + RUNTIME_PROVIDER_BINDINGS.map( + (binding: { vendorGroup: string; endpointCommitment: string }) => [ + binding.vendorGroup, + binding.endpointCommitment, + ], + ), +); +const ORIGIN_COMMITMENTS = { + alchemy: `0x${"aa".repeat(32)}`, + quicknode: `0x${"bb".repeat(32)}`, +}; + +type MutableHttpSample = { + route: string; + datasetKey: string; + startedAtMs: number; + completedAtMs: number; + durationMs: number; + status: number; +}; + +function readJson(path: string) { + return JSON.parse(readFileSync(resolve(process.cwd(), path), "utf8")); +} + +function profileFixture(release = false) { + return readJson( + release + ? "config/read-model-release-profile.v1.json" + : "config/read-model-load-profile.v1.json", + ); +} + +function expectedProviders() { + return ["alchemy", "quicknode"].map((vendorGroup) => { + const endpointCommitment = + ENDPOINT_COMMITMENTS[vendorGroup as keyof typeof ENDPOINT_COMMITMENTS]; + return { + vendorGroup, + endpointCommitment, + identity: `${vendorGroup}-mainnet-${endpointCommitment.slice(2, 34)}`, + }; + }); +} + +function candidateFixture(index: number) { + const candidateBlockNumber = (20_000_000 + index).toString(); + const candidateBlockHash = `0x${(10_000 + index) + .toString(16) + .padStart(64, "0")}`; + const transactionHash = `0x${(20_000 + index) + .toString(16) + .padStart(64, "0")}`; + return { + candidateId: `1:${candidateBlockHash}:${transactionHash}:${index}`, + candidateBlockNumber, + candidateBlockHash, + transactionHash, + sourceAddress: `0x${(30_000 + index) + .toString(16) + .padStart(40, "0")}`, + }; +} + +function rawRpcTrace( + capturedAtMs: number, + profile = profileFixture(), +) { + const candidateCount = profile.datasetCoverage.candidateSampleCount; + const elapsedMs = candidateCount === 32 ? 300 : 100; + const candidateEvidence = Array.from( + { length: candidateCount }, + (_, index) => candidateFixture(index + 1), + ); + const calls = expectedProviders().flatMap((provider) => { + const operations = [ + "getChainId", + "getBlockNumber", + ...Array.from({ length: candidateCount + 1 }, () => "getBlock"), + ...Array.from({ length: candidateCount }, () => "getTransactionReceipt"), + ...Array.from({ length: candidateCount }, () => "getBytecode"), + ]; + return operations.map((operation, index) => ({ + providerIdentity: provider.identity, + providerVendorGroup: provider.vendorGroup, + providerEndpointCommitment: provider.endpointCommitment, + providerOriginCommitment: + ORIGIN_COMMITMENTS[ + provider.vendorGroup as keyof typeof ORIGIN_COMMITMENTS + ], + operation, + attempt: 1, + startedOffsetMs: index * 2, + durationMs: 1, + outcome: "success", + })); + }); + return { + schemaVersion: 1, + profileId: profile.profileId, + gitHead: GIT_HEAD, + targetUrl: TARGET_URL, + vercelDeploymentId: DEPLOYMENT_ID, + captureNonce: CAPTURE_NONCE, + startedAtMs: capturedAtMs - 125_000, + completedAtMs: capturedAtMs - 125_000 + elapsedMs, + candidateBatchSize: candidateCount, + hardDeadlineMs: 75_000, + maxCallsPerProvider: profile.projector.rpc.maxCallsPerProviderPerRun, + elapsedMs, + providerCallCounts: [3 + candidateCount * 3, 3 + candidateCount * 3], + candidateEvidence, + calls, + }; +} + +function rawHttpSamples( + profile: ReturnType, + keys: { + tokenAddresses: string[]; + accountAddresses: string[]; + classicLaunches: { account: string; transactionHash: string }[]; + stockLaunches: { account: string; transactionHash: string }[]; + }, + eligibleLaunches: { + account: string; + transactionHash: string; + tokenAddress: string; + releaseVersion: string; + }[], + capturedAtMs: number, +) { + const routes = (Object.entries(profile.load.routeMixBps) as [string, number][]).flatMap( + ([route, basisPoints]) => + Array.from({ length: basisPoints / 10 }, () => route), + ); + const base = capturedAtMs - 120_000; + const classIndexes = new Map([ + ["token", 0], + ["account", 0], + ["classic", 0], + ["stock", 0], + ]); + const distributed = (values: T[], sequence: number) => + values[sequence % values.length]; + return routes.map((route, index) => { + const wave = Math.floor(index / profile.load.concurrency); + const startedAtMs = base + wave * 1_225; + const durationMs = 100; + const shadow = profile.shadow.requiredRoutes.includes(route); + const keyClass = + route === "classicLaunchLookup" + ? "classic" + : route === "stockLaunchLookup" + ? "stock" + : ["creatorProfile", "classicProfile", "stockProfile"].includes(route) + ? "account" + : route === "health" + ? undefined + : "token"; + const classIndex = keyClass === undefined ? 0 : classIndexes.get(keyClass)!; + if (keyClass !== undefined) classIndexes.set(keyClass, classIndex + 1); + return { + route, + requestKey: `${route}-${index}`, + datasetKey: + route === "health" + ? "health" + : route === "classicLaunchLookup" + ? distributed(keys.classicLaunches, classIndex)!.transactionHash + : route === "stockLaunchLookup" + ? distributed(keys.stockLaunches, classIndex)!.transactionHash + : ["creatorProfile", "classicProfile", "stockProfile"].includes(route) + ? distributed(keys.accountAddresses, classIndex) + : distributed(keys.tokenAddresses, classIndex), + keyMatched: true, + startedAtMs, + completedAtMs: startedAtMs + durationMs, + durationMs, + status: 200, + cacheControl: + profile.shadow.requiredRoutes.includes(route) + ? profile.load.probeCacheControl + : profile.cacheContracts[route], + vercelCache: "MISS", + bodySha256: "3".repeat(64), + bodyBytes: 128, + shadowOverheadMs: shadow ? 5 : null, + parity: shadow ? "match" : "not-observed", + readSource: shadow ? "rpc" : "not-observed", + fallback: shadow ? false : null, + }; + }); +} + +function createBundle(release = false) { + const directory = mkdtempSync(join(tmpdir(), "read-model-gate-")); + temporaryDirectories.push(directory); + const profile = profileFixture(release); + const capturedAtMs = Date.now(); + const address = (value: number) => + `0x${value.toString(16).padStart(40, "0")}`; + const launchCount = release ? 264 : 260; + const classicEnd = release ? 212 : 208; + const stockV1End = classicEnd + 1; + const stockV2End = stockV1End + 8; + const eligibleLaunches = Array.from({ length: launchCount }, (_, index) => { + const releaseVersion = + index < 27 + ? "classic-v2" + : index < classicEnd + ? "classic-v3" + : index < stockV1End + ? "stock-paired-v1" + : index < stockV2End + ? "stock-paired-v2" + : "stock-paired-v3"; + return { + account: address((index % 100) + 1_001), + transactionHash: `0x${(index + 1).toString(16).padStart(64, "0")}`, + tokenAddress: address(index + 1), + releaseVersion, + }; + }); + const eligibleClassicLaunches = eligibleLaunches.filter((launch) => + launch.releaseVersion === "classic-v3", + ); + const eligibleStockLaunches = eligibleLaunches.filter((launch) => + launch.releaseVersion.startsWith("stock-paired-"), + ); + const keys = { + tokenAddresses: Array.from( + { length: profile.datasetCoverage.tokenSampleCount }, + (_, index) => address(index + 1), + ), + accountAddresses: Array.from( + { length: 100 }, + (_, index) => address(index + 1_001), + ), + classicLaunches: eligibleClassicLaunches + .slice(0, 32) + .map(({ account, transactionHash }) => ({ account, transactionHash })), + stockLaunches: eligibleStockLaunches + .slice(0, 32) + .map(({ account, transactionHash }) => ({ account, transactionHash })), + candidateIds: Array.from( + { length: profile.datasetCoverage.candidateSampleCount }, + (_, index) => candidateFixture(index + 1).candidateId, + ), + }; + const datasetManifest = { + schemaVersion: 1, + profileId: profile.profileId, + generatedAt: new Date(capturedAtMs - 130_000).toISOString(), + counts: { + launches: launchCount, + chainEvents: launchCount * 3, + marketSnapshots: launchCount, + marketCandles: launchCount, + accounts: 100, + rewardRows: launchCount, + }, + releaseCounts: { + "classic-v2": 27, + "classic-v3": release ? 185 : 181, + "stock-paired-v1": 1, + "stock-paired-v2": 8, + "stock-paired-v3": 43, + }, + eligibleLaunches, + accountEvidence: keys.accountAddresses.map((account) => ({ + account, + profileRows: 1, + rewardRows: 0, + })), + accessEvidence: { + projectorSessionUser: "programmable_projector_login", + projectorCurrentRole: "programmable_projector", + projectorCurrentSettingRole: "programmable_projector", + apiReaderSessionUser: "programmable_api_reader_login", + apiReaderCurrentRole: "programmable_api_reader", + apiReaderCurrentSettingRole: "programmable_api_reader", + apiReaderDeniedSqlstate: "42501", + apiReaderFunctionExecute: false, + apiReaderViewSelect: false, + }, + keys, + }; + const files = { + datasetManifest: "dataset-manifest.v1.json", + httpSamples: "http-samples.v1.jsonl", + rpcTrace: "rpc-trace.v1.json", + }; + const contents = { + datasetManifest: `${JSON.stringify(datasetManifest)}\n`, + httpSamples: `${rawHttpSamples(profile, keys, eligibleLaunches, capturedAtMs) + .map((sample) => JSON.stringify(sample)) + .join("\n")}\n`, + rpcTrace: `${JSON.stringify(rawRpcTrace(capturedAtMs, profile))}\n`, + }; + for (const key of Object.keys(files) as (keyof typeof files)[]) { + writeFileSync(join(directory, files[key]), contents[key]); + } + const evidence = { + schemaVersion: 1, + profileId: profile.profileId, + evidenceKind: "production-canary", + capturedAt: new Date(capturedAtMs).toISOString(), + captureNonce: CAPTURE_NONCE, + target: { + url: TARGET_URL, + vercelDeploymentId: DEPLOYMENT_ID, + gitHead: GIT_HEAD, + }, + artifacts: Object.fromEntries( + (Object.keys(files) as (keyof typeof files)[]).map((key) => [ + key, + { + file: files[key], + sha256: gateCore.sha256Bytes(Buffer.from(contents[key])), + }, + ]), + ), + }; + const evidencePath = join( + directory, + "read-model-release-evidence.v1.json", + ); + writeFileSync(evidencePath, `${JSON.stringify(evidence)}\n`); + return { + directory, + evidencePath, + profile, + evidence, + datasetManifest, + }; +} + +function rewriteDatasetManifest(fixture: ReturnType) { + const contents = `${JSON.stringify(fixture.datasetManifest)}\n`; + writeFileSync(join(fixture.directory, "dataset-manifest.v1.json"), contents); + fixture.evidence.artifacts.datasetManifest.sha256 = gateCore.sha256Bytes( + Buffer.from(contents), + ); + writeFileSync(fixture.evidencePath, `${JSON.stringify(fixture.evidence)}\n`); +} + +function rewriteHttpSamples( + fixture: ReturnType, + samples: Record[], +) { + const contents = `${samples.map((sample) => JSON.stringify(sample)).join("\n")}\n`; + writeFileSync(join(fixture.directory, "http-samples.v1.jsonl"), contents); + fixture.evidence.artifacts.httpSamples.sha256 = gateCore.sha256Bytes( + Buffer.from(contents), + ); + writeFileSync(fixture.evidencePath, `${JSON.stringify(fixture.evidence)}\n`); +} + +function loadBundle(evidencePath: string, release = false) { + return gateCore.loadReadModelReleaseEvidence({ + profile: profileFixture(release), + evidencePath, + }); +} + +afterEach(() => { + while (temporaryDirectories.length > 0) { + rmSync(temporaryDirectories.pop()!, { recursive: true, force: true }); + } +}); + +describe("read-model performance contract", () => { + it("signs route-bound release probes without sending the server secret", () => { + const secret = "s".repeat(32); + const nonce = `1700000000000-${"55".repeat(32)}-7`; + const expected = createHmac("sha256", secret) + .update(`programmable-release-probe-v1\nexplore-token\n${nonce}`, "utf8") + .digest("hex"); + expect( + releaseProbe.signReadModelReleaseProbe({ + route: "tokenDetail", + nonce, + secret, + }), + ).toBe(expected); + expect( + releaseProbe.buildReadModelReleaseProbe({ + route: "tokenDetail", + issuedAtMs: 1_700_000_000_000, + captureNonce: CAPTURE_NONCE, + sequence: 7, + secret, + }), + ).toEqual({ nonce, signature: expected }); + expect( + Object.fromEntries( + [ + "exploreList", + "tokenDetail", + "tokenChart", + "creatorProfile", + "classicProfile", + "stockProfile", + "classicLaunchLookup", + "stockLaunchLookup", + ].map((route) => [ + route, + releaseProbe.indexedRouteForPerformanceRoute(route), + ]), + ), + ).toEqual({ + exploreList: "explore-list", + tokenDetail: "explore-token", + tokenChart: "explore-chart", + creatorProfile: "creator-profile", + classicProfile: "classic-v3-profile", + stockProfile: "creator-profile", + classicLaunchLookup: "launch-lookup", + stockLaunchLookup: "launch-lookup", + }); + expect(() => + releaseProbe.signReadModelReleaseProbe({ + route: "publicIndexer", + nonce, + secret, + }), + ).toThrow("no indexed release-probe binding"); + expect(() => + releaseProbe.signReadModelReleaseProbe({ + route: "tokenDetail", + nonce, + secret: "too-short", + }), + ).toThrow("secret is invalid"); + }); + + it("pins the executable deadline and exact retry mathematics", () => { + const profile = gateCore.parseReadModelLoadProfile(profileFixture()); + expect(profile.projector.hardDeadlineMs).toBe(75_000); + expect(profile.projector.hostingDeadlineMs).toBe(90_000); + expect(gateCore.projectorCallsPerProviderPerAttempt(profile, 8)).toBe(27); + expect(gateCore.projectorWorstCaseRetryContract(profile, 8)).toEqual({ + callsPerProvider: 81, + durationMs: 121_200, + }); + }); + + it("gates the 32-candidate release ceiling against the full 264-launch corpus", () => { + const profile = gateCore.parseReadModelLoadProfile(profileFixture(true)); + expect(profile.projector).toMatchObject({ + smokeCandidateBatchSize: 8, + maximumCandidateBatchSize: 32, + hardDeadlineMs: 75_000, + }); + expect(gateCore.projectorCallsPerProviderPerAttempt(profile, 32)).toBe(99); + expect(profile.projector.rpc).toMatchObject({ + maxCallsPerProviderPerRun: 128, + maxAggregateCallsPerRun: 256, + }); + expect( + sourceContracts.evaluateReadModelSourceContracts(process.cwd(), profile) + .ok, + ).toBe(true); + + const fixture = createBundle(true); + const result = gateCore.evaluateReadModelReleaseEvidence( + loadBundle(fixture.evidencePath, true), + { gitHead: GIT_HEAD, expectedProviders: expectedProviders() }, + ); + expect(result.failures).toEqual([]); + expect(result.releaseEvidenceAccepted).toBe(true); + expect(result.checks).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "release-corpus-cycles", + status: "pass", + }), + expect.objectContaining({ + id: "projector-runtime-policy", + status: "pass", + }), + expect.objectContaining({ + id: "rpc-provider-trace", + status: "pass", + }), + ]), + ); + }); + + it("accepts only digested raw evidence bound to the exact release", () => { + const fixture = createBundle(); + const bundle = loadBundle(fixture.evidencePath); + const result = gateCore.evaluateReadModelReleaseEvidence(bundle, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }); + expect(result.releaseEvidenceAccepted).toBe(true); + expect(result.failures).toEqual([]); + expect(result.checks).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: "dataset-cardinality", status: "pass" }), + expect.objectContaining({ + id: "deterministic-real-samples", + status: "pass", + }), + expect.objectContaining({ id: "projector-only-corpus", status: "pass" }), + expect.objectContaining({ + id: "throughput-key-distribution", + status: "pass", + }), + ]), + ); + expect(result.artifactDigests).toEqual( + expect.objectContaining({ + datasetManifest: expect.stringMatching(/^[0-9a-f]{64}$/u), + httpSamples: expect.stringMatching(/^[0-9a-f]{64}$/u), + rpcTrace: expect.stringMatching(/^[0-9a-f]{64}$/u), + }), + ); + }); + + it("keeps full launch cardinality separate from the repeated load corpus", () => { + const fixture = createBundle(); + expect(fixture.datasetManifest.eligibleLaunches).toHaveLength(260); + expect(fixture.datasetManifest.keys).toMatchObject({ + tokenAddresses: expect.arrayContaining([ + fixture.datasetManifest.keys.tokenAddresses[0], + ]), + accountAddresses: expect.any(Array), + classicLaunches: expect.any(Array), + stockLaunches: expect.any(Array), + }); + expect(fixture.datasetManifest.keys.tokenAddresses).toHaveLength(100); + expect(fixture.datasetManifest.keys.accountAddresses).toHaveLength(100); + expect(fixture.datasetManifest.keys.classicLaunches).toHaveLength(32); + expect(fixture.datasetManifest.keys.stockLaunches).toHaveLength(32); + const result = gateCore.evaluateReadModelReleaseEvidence( + loadBundle(fixture.evidencePath), + { gitHead: GIT_HEAD, expectedProviders: expectedProviders() }, + ); + expect(result.releaseEvidenceAccepted).toBe(true); + }); + + it("rejects undersized cardinality without inflating the throughput samples", () => { + const fixture = createBundle(); + fixture.datasetManifest.eligibleLaunches = + fixture.datasetManifest.eligibleLaunches.filter( + (_, index) => index < 147 || index >= 208, + ); + fixture.datasetManifest.counts.launches = 199; + fixture.datasetManifest.counts.chainEvents = 597; + fixture.datasetManifest.counts.marketSnapshots = 199; + fixture.datasetManifest.counts.marketCandles = 199; + fixture.datasetManifest.counts.rewardRows = 199; + fixture.datasetManifest.releaseCounts["classic-v3"] = 120; + rewriteDatasetManifest(fixture); + const result = gateCore.evaluateReadModelReleaseEvidence( + loadBundle(fixture.evidencePath), + { gitHead: GIT_HEAD, expectedProviders: expectedProviders() }, + ); + expect(result.failures).toContainEqual( + expect.objectContaining({ id: "dataset-cardinality" }), + ); + }); + + it("rejects padded, synthetic and privilege-escalated corpus manifests", () => { + const duplicate = createBundle(); + duplicate.datasetManifest.keys.tokenAddresses[1] = + duplicate.datasetManifest.keys.tokenAddresses[0]; + rewriteDatasetManifest(duplicate); + expect(() => loadBundle(duplicate.evidencePath)).toThrow( + "addresses must be unique", + ); + + const synthetic = createBundle(); + synthetic.datasetManifest.keys.tokenAddresses[0] = `0x${"ff".repeat(20)}`; + rewriteDatasetManifest(synthetic); + expect(() => loadBundle(synthetic.evidencePath)).toThrow( + "contains a non-eligible token", + ); + + const unevidencedAccount = createBundle(); + unevidencedAccount.datasetManifest.accountEvidence[0].profileRows = 0; + unevidencedAccount.datasetManifest.accountEvidence[0].rewardRows = 0; + rewriteDatasetManifest(unevidencedAccount); + expect(() => loadBundle(unevidencedAccount.evidencePath)).toThrow( + "account must have real profile or reward evidence", + ); + + const readerPrivilege = createBundle(); + readerPrivilege.datasetManifest.accessEvidence.apiReaderFunctionExecute = true; + rewriteDatasetManifest(readerPrivilege); + expect(() => loadBundle(readerPrivilege.evidencePath)).toThrow( + "must prove projector-only corpus access", + ); + + const missingRelease = createBundle(); + missingRelease.datasetManifest.releaseCounts["stock-paired-v1"] = 0; + rewriteDatasetManifest(missingRelease); + expect(() => loadBundle(missingRelease.evidencePath)).toThrow( + "greater than or equal to 1", + ); + }); + + it("fails closed on incomparable parity and absent or true fallback evidence", () => { + const incomparable = loadBundle(createBundle().evidencePath); + incomparable.httpSamples[0].parity = "incomparable"; + const incomparableResult = gateCore.evaluateReadModelReleaseEvidence( + incomparable, + { gitHead: GIT_HEAD, expectedProviders: expectedProviders() }, + ); + expect(incomparableResult.failures).toContainEqual( + expect.objectContaining({ id: "shadow-parity" }), + ); + + const fallback = loadBundle(createBundle().evidencePath); + fallback.httpSamples[0].fallback = true; + const fallbackResult = gateCore.evaluateReadModelReleaseEvidence(fallback, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }); + expect(fallbackResult.failures).toContainEqual( + expect.objectContaining({ id: "live-fallbacks" }), + ); + + const missing = createBundle(); + const samples = readFileSync( + join(missing.directory, "http-samples.v1.jsonl"), + "utf8", + ) + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + samples[0].fallback = null; + rewriteHttpSamples(missing, samples); + expect(() => loadBundle(missing.evidencePath)).toThrow( + "missing raw fallback result", + ); + }); + + it("enforces concurrency, key distribution, p95, p99 and zero errors independently", () => { + const lowConcurrency = loadBundle(createBundle().evidencePath); + const lowConcurrencyBase = Date.parse(lowConcurrency.evidence.capturedAt) - 120_000; + lowConcurrency.httpSamples.forEach( + (sample: MutableHttpSample, index: number) => { + sample.startedAtMs = lowConcurrencyBase + index * 100; + sample.completedAtMs = sample.startedAtMs + sample.durationMs; + }, + ); + expect( + gateCore.evaluateReadModelReleaseEvidence(lowConcurrency, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }).failures, + ).toContainEqual(expect.objectContaining({ id: "throughput-shape" })); + + const missingKey = loadBundle(createBundle().evidencePath); + const omittedToken = missingKey.datasetManifest.keys.tokenAddresses[99]; + const replacementToken = missingKey.datasetManifest.keys.tokenAddresses[0]; + missingKey.httpSamples.forEach((sample: MutableHttpSample) => { + if (sample.datasetKey.toLowerCase() === omittedToken.toLowerCase()) { + sample.datasetKey = replacementToken; + } + }); + expect( + gateCore.evaluateReadModelReleaseEvidence(missingKey, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }).failures, + ).toContainEqual( + expect.objectContaining({ id: "throughput-key-distribution" }), + ); + + const p95 = loadBundle(createBundle().evidencePath); + p95.httpSamples + .filter((sample: MutableHttpSample) => sample.route === "exploreList") + .forEach((sample: MutableHttpSample) => { + sample.durationMs = 805; + sample.completedAtMs = sample.startedAtMs + sample.durationMs; + }); + const p95Result = gateCore.evaluateReadModelReleaseEvidence(p95, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }); + expect(p95Result.failures).toContainEqual( + expect.objectContaining({ id: "route-latency-p95-exploreList" }), + ); + expect(p95Result.failures).not.toContainEqual( + expect.objectContaining({ id: "route-latency-p99-exploreList" }), + ); + + const p99 = loadBundle(createBundle().evidencePath); + p99.httpSamples + .filter((sample: MutableHttpSample) => sample.route === "tokenDetail") + .slice(0, 2) + .forEach((sample: MutableHttpSample) => { + sample.durationMs = 1_605; + sample.completedAtMs = sample.startedAtMs + sample.durationMs; + }); + const p99Result = gateCore.evaluateReadModelReleaseEvidence(p99, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }); + expect(p99Result.failures).toContainEqual( + expect.objectContaining({ id: "route-latency-p99-tokenDetail" }), + ); + + const error = loadBundle(createBundle().evidencePath); + error.httpSamples[0].status = 500; + expect( + gateCore.evaluateReadModelReleaseEvidence(error, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }).failures, + ).toContainEqual(expect.objectContaining({ id: "throughput-errors" })); + }); + + it("separates selected-path latency from rollout-direction comparison cost", () => { + const bundle = loadBundle(createBundle().evidencePath); + for (const sample of bundle.httpSamples) { + if (!bundle.profile.shadow.requiredRoutes.includes(sample.route)) continue; + sample.readSource = "indexed"; + sample.shadowOverheadMs = 5_000; + sample.durationMs = 5_100; + sample.completedAtMs = sample.startedAtMs + sample.durationMs; + } + const accepted = gateCore.evaluateReadModelReleaseEvidence(bundle, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }); + expect(accepted.releaseEvidenceAccepted).toBe(true); + + for (const sample of bundle.httpSamples) { + if (!bundle.profile.shadow.requiredRoutes.includes(sample.route)) continue; + sample.shadowOverheadMs = 26_000; + sample.durationMs = 26_100; + sample.completedAtMs = sample.startedAtMs + sample.durationMs; + } + const rejected = gateCore.evaluateReadModelReleaseEvidence(bundle, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }); + expect(rejected.failures).toContainEqual( + expect.objectContaining({ id: "shadow-overhead" }), + ); + }); + + it("rejects an artifact changed after the manifest was signed", () => { + const fixture = createBundle(); + writeFileSync( + join(fixture.directory, "http-samples.v1.jsonl"), + "{}\n", + ); + expect(() => loadBundle(fixture.evidencePath)).toThrow( + "artifact digest mismatch", + ); + }); + + it("rejects stale, parity, fallback and provider-binding regressions", () => { + const stale = loadBundle(createBundle().evidencePath); + const staleResult = gateCore.evaluateReadModelReleaseEvidence(stale, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + nowMs: Date.parse(stale.evidence.capturedAt) + 1_801_000, + }); + expect(staleResult.failures).toContainEqual( + expect.objectContaining({ id: "freshness" }), + ); + + const raw = loadBundle(createBundle().evidencePath); + raw.httpSamples[0].parity = "mismatch"; + raw.httpSamples[1].fallback = true; + const result = gateCore.evaluateReadModelReleaseEvidence(raw, { + gitHead: GIT_HEAD, + expectedProviders: [ + expectedProviders()[0], + { + ...expectedProviders()[1], + endpointCommitment: `0x${"44".repeat(32)}`, + }, + ], + }); + expect(result.failures.map((failure: { id: string }) => failure.id)).toEqual( + expect.arrayContaining([ + "shadow-parity", + "live-fallbacks", + "rpc-provider-trace", + ]), + ); + + const replayed = loadBundle(createBundle().evidencePath); + const capturedAtMs = Date.parse(replayed.evidence.capturedAt); + replayed.rpcTrace.startedAtMs = capturedAtMs - 1_900_100; + replayed.rpcTrace.completedAtMs = capturedAtMs - 1_900_000; + replayed.httpSamples[0].vercelCache = "HIT"; + const replayedResult = gateCore.evaluateReadModelReleaseEvidence(replayed, { + gitHead: GIT_HEAD, + expectedProviders: expectedProviders(), + }); + expect( + replayedResult.failures.map((failure: { id: string }) => failure.id), + ).toEqual( + expect.arrayContaining([ + "rpc-trace-freshness", + "throughput-cache-and-identity", + ]), + ); + }); + + it("enforces the exact legacy-only deployment boundary", () => { + const exactFalse = deployPolicy.RELEASE_GATED_FLAG_NAMES.map( + (name: string) => `${name}="false"`, + ).join("\n"); + const legacy = deployPolicy.evaluateReadModelDeployPolicy(exactFalse, {}); + expect(legacy).toMatchObject({ + mode: "legacy-only", + evidenceRequired: false, + commitmentsReady: true, + }); + const indexedEnvironment = `${exactFalse.replace( + "INDEXED_EXPLORE_TOKEN_READS_ENABLED=\"false\"", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED=\"true\"", + )}\nPROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL="${RUNTIME_RPC_ENVIRONMENT.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL}"\nPROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL="${RUNTIME_RPC_ENVIRONMENT.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL}"`; + const indexed = deployPolicy.evaluateReadModelDeployPolicy( + indexedEnvironment, + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.alchemy, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.quicknode, + }, + ); + expect(indexed).toMatchObject({ + mode: "indexed-or-shadow", + evidenceRequired: true, + commitmentsReady: true, + runtimeProviderBinding: "verified", + }); + const sensitiveRuntimeEnvironment = `${exactFalse.replace( + "INDEXED_EXPLORE_TOKEN_READS_ENABLED=\"false\"", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED=\"[sensitive]\"", + )}\nPROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL="[sensitive]"\nPROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL="[sensitive]"`; + expect( + deployPolicy.evaluateReadModelDeployPolicy( + sensitiveRuntimeEnvironment, + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.alchemy, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.quicknode, + }, + ), + ).toMatchObject({ + mode: "indexed-or-shadow", + evidenceRequired: true, + commitmentsReady: true, + runtimeProviderBinding: "deferred-stage", + }); + expect( + deployPolicy.evaluateReadModelDeployPolicy( + `${sensitiveRuntimeEnvironment}\nETHEREUM_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/abcdefgh"`, + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.alchemy, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.quicknode, + }, + ), + ).toMatchObject({ + evidenceRequired: true, + commitmentsReady: false, + runtimeProviderBinding: "unverified", + }); + const publicFeedEnvironment = `${exactFalse.replace( + "INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED=\"false\"", + "INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED=\"true\"", + )}\nPROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL="${RUNTIME_RPC_ENVIRONMENT.PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL}"\nPROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL="${RUNTIME_RPC_ENVIRONMENT.PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL}"`; + expect( + deployPolicy.evaluateReadModelDeployPolicy(publicFeedEnvironment, { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.alchemy, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.quicknode, + }), + ).toMatchObject({ + mode: "indexed-or-shadow", + evidenceRequired: true, + commitmentsReady: true, + }); + expect( + deployPolicy.evaluateReadModelDeployPolicy(indexedEnvironment, { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + `0x${"77".repeat(32)}`, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.quicknode, + }), + ).toMatchObject({ + evidenceRequired: true, + commitmentsReady: false, + invalidCommitmentNames: ["runtime-provider-commitment-mismatch"], + }); + expect( + deployPolicy.evaluateReadModelDeployPolicy( + exactFalse.split("\n").slice(1).join("\n"), + {}, + ), + ).toMatchObject({ evidenceRequired: true, commitmentsReady: false }); + }); + + it("derives release identities from two pinned non-secret commitments", () => { + const bindings = providerBinding.expectedProductionProviderBindings({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.alchemy, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.quicknode, + }); + expect(bindings).toEqual(expectedProviders()); + expect(() => + providerBinding.expectedProductionProviderBindings({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + ENDPOINT_COMMITMENTS.alchemy, + }), + ).toThrow("both pinned provider commitments"); + }); + + it("binds approved Alchemy and QuickNode server-only endpoints", async () => { + const environment = { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: + "https://eth-mainnet.g.alchemy.com/v2/abcdefgh", + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: + "https://example.quiknode.pro/abcdefgh", + }; + const bindings = providerBinding.expectedProductionProviderBindings({ + ...environment, + }); + expect(bindings.map((binding: { vendorGroup: string }) => binding.vendorGroup)).toEqual([ + "alchemy", + "quicknode", + ]); + expect(bindings[0].endpointCommitment).not.toBe( + bindings[1].endpointCommitment, + ); + const { createProductionDualRpcProviders } = await import( + "../../lib/data-pipeline/rpc-providers.server" + ); + const providers = createProductionDualRpcProviders(environment); + expect(providers.map(({ vendorGroup }) => vendorGroup)).toEqual([ + "alchemy", + "quicknode", + ]); + expect( + providers.map(({ identity, vendorGroup, endpointCommitment }) => ({ + identity, + vendorGroup, + endpointCommitment, + })), + ).toEqual(bindings); + expect(JSON.stringify(providers)).not.toContain("abcdefgh"); + }); + + it("checks live Vercel identity and real cache headers and keys", async () => { + const fixture = createBundle(); + const expectedCache = fixture.profile.cacheContracts; + const fetchImpl = async (input: URL | RequestInfo) => { + const url = new URL(String(input)); + if (url.hostname === "api.vercel.com") { + if (url.pathname === "/v6/deployments") { + return new Response( + JSON.stringify({ + deployments: [ + { + id: `dpl_${"B".repeat(24)}`, + projectId: "prj_test", + readyState: "READY", + target: "production", + alias: ["programmable.family"], + }, + ], + }), + { status: 200 }, + ); + } + return new Response( + JSON.stringify({ + id: DEPLOYMENT_ID, + url: new URL(TARGET_URL).hostname, + projectId: "prj_test", + readyState: "READY", + meta: { githubCommitSha: GIT_HEAD }, + }), + { status: 200 }, + ); + } + let body: unknown; + let cacheControl: string; + if (url.pathname === "/api/explore") { + body = { query: url.searchParams.get("q") }; + cacheControl = expectedCache.exploreList; + } else if (url.pathname === "/api/explore/token") { + body = { token: { tokenAddress: url.searchParams.get("address") } }; + cacheControl = expectedCache.tokenDetail; + } else if (url.pathname === "/api/explore/token/chart") { + body = { + address: url.searchParams.get("address"), + range: url.searchParams.get("range"), + }; + cacheControl = expectedCache.tokenChart; + } else if (url.pathname === "/api/explore/profile") { + body = { account: url.searchParams.get("account") }; + cacheControl = expectedCache.creatorProfile; + } else if (url.pathname === "/api/profile/classic-v3") { + body = url.searchParams.has("launch") + ? { + status: "ready", + launch: { + launchTransactionHash: url.searchParams.get("launch"), + }, + } + : { + status: "ready", + account: url.searchParams.get("account"), + rewards: [], + }; + cacheControl = url.searchParams.has("launch") + ? expectedCache.classicLaunchLookup + : expectedCache.classicProfile; + } else if (url.pathname === "/api/profile/stock-paired") { + body = { + status: "ready", + account: url.searchParams.get("account"), + rewards: [], + }; + cacheControl = expectedCache.stockProfile; + } else if (url.pathname === "/api/explore/launch/stock-paired") { + body = { + status: "ready", + launch: { transactionHash: url.searchParams.get("transaction") }, + }; + cacheControl = expectedCache.stockLaunchLookup; + } else if (url.pathname === "/api/indexers/v1/tokens") { + body = { address: url.searchParams.get("address") }; + cacheControl = expectedCache.publicIndexer; + } else if (url.pathname === "/api/indexers/v1/token-list") { + body = { tokens: [{}] }; + cacheControl = expectedCache.tokenList; + } else { + body = { status: "healthy" }; + cacheControl = expectedCache.health; + } + return new Response(JSON.stringify(body), { + status: 200, + headers: { "cache-control": cacheControl }, + }); + }; + const vercel = await liveVerifier.verifyLiveVercelBinding({ + evidence: fixture.evidence, + gitHead: GIT_HEAD, + token: "token", + teamId: "team", + projectId: "prj_test", + fetchImpl, + }); + expect(vercel.ok).toBe(true); + const rollback = await liveVerifier.verifyLiveRollbackTarget({ + stagedDeploymentId: DEPLOYMENT_ID, + token: "token", + teamId: "team", + projectId: "prj_test", + productionDomain: "programmable.family", + fetchImpl, + }); + expect(rollback).toMatchObject({ + ok: true, + rollbackDeploymentId: `dpl_${"B".repeat(24)}`, + }); + const noRollback = await liveVerifier.verifyLiveRollbackTarget({ + stagedDeploymentId: DEPLOYMENT_ID, + token: "token", + teamId: "team", + projectId: "prj_test", + productionDomain: "programmable.family", + fetchImpl: async () => + new Response( + JSON.stringify({ + deployments: [ + { + id: DEPLOYMENT_ID, + projectId: "prj_test", + readyState: "READY", + target: "production", + alias: ["programmable.family"], + }, + ], + }), + { status: 200 }, + ), + }); + expect(noRollback.failures).toContainEqual( + expect.objectContaining({ id: "vercel-rollback-target" }), + ); + const cache = await liveVerifier.verifyLiveCacheAndKeyContracts({ + profile: fixture.profile, + evidence: fixture.evidence, + datasetManifest: fixture.datasetManifest, + fetchImpl, + }); + expect(cache.ok).toBe(true); + const wrongHeader = await liveVerifier.verifyLiveCacheAndKeyContracts({ + profile: fixture.profile, + evidence: fixture.evidence, + datasetManifest: fixture.datasetManifest, + fetchImpl: async (input: URL | RequestInfo) => { + const response = await fetchImpl(input); + return new URL(String(input)).pathname === "/api/ops/health" + ? new Response(await response.text(), { + status: 200, + headers: { "cache-control": "public, max-age=3600" }, + }) + : response; + }, + }); + expect(wrongHeader.failures).toContainEqual( + expect.objectContaining({ id: "live-cache-headers" }), + ); + const leakedChart = await liveVerifier.verifyLiveCacheAndKeyContracts({ + profile: fixture.profile, + evidence: fixture.evidence, + datasetManifest: fixture.datasetManifest, + fetchImpl: async (input: URL | RequestInfo) => { + const response = await fetchImpl(input); + if (new URL(String(input)).pathname !== "/api/explore/token/chart") { + return response; + } + const body = JSON.parse(await response.text()); + return new Response( + JSON.stringify({ + ...body, + address: fixture.datasetManifest.keys.tokenAddresses[0], + }), + { + status: 200, + headers: { + "cache-control": fixture.profile.cacheContracts.tokenChart, + }, + }, + ); + }, + }); + expect(leakedChart.failures).toContainEqual( + expect.objectContaining({ id: "cache-key-chart-address" }), + ); + }); + + it("detects source drift and keeps smoke distinct from release evidence", () => { + const profile = gateCore.parseReadModelLoadProfile(profileFixture()); + const result = sourceContracts.evaluateReadModelSourceContracts( + process.cwd(), + profile, + ); + expect(result.ok).toBe(true); + expect(result.checks).toHaveLength(33); + + const dualRpcPath = "lib/data-pipeline/dual-rpc.ts"; + const dualRpcSource = readFileSync(resolve(process.cwd(), dualRpcPath), "utf8"); + const formattingOnly = dualRpcSource + .replace( + "const DEFAULT_RPC_CONCURRENCY = 4;", + "const DEFAULT_RPC_CONCURRENCY\n =\n 4;", + ) + .replace( + "executionTrace: Object.freeze({", + "executionTrace :\n Object.freeze ( {", + ); + expect(formattingOnly).not.toBe(dualRpcSource); + expect( + sourceContracts.evaluateReadModelSourceContracts(process.cwd(), profile, { + sourceOverrides: { [dualRpcPath]: formattingOnly }, + }).ok, + ).toBe(true); + + const semanticDrift = sourceContracts.evaluateReadModelSourceContracts( + process.cwd(), + profile, + { + sourceOverrides: { + [dualRpcPath]: dualRpcSource.replace( + "const DEFAULT_RPC_CONCURRENCY = 4;", + "const DEFAULT_RPC_CONCURRENCY = 5;", + ), + }, + }, + ); + expect( + semanticDrift.failures.map((failure: { id: string }) => failure.id), + ).toContain("source-rpc-concurrency"); + const packageJson = readJson("package.json"); + expect(packageJson.scripts["perf:read-model:smoke"]).toContain( + "read-model-smoke.mjs", + ); + expect(packageJson.scripts["perf:read-model:gate"]).toContain( + "--require-release-evidence", + ); + expect(packageJson.scripts.verify).toContain( + "perf:read-model:release-if-present", + ); + const deployWorkflow = readFileSync( + resolve(process.cwd(), ".github/workflows/deploy-production.yml"), + "utf8", + ); + expect(deployWorkflow).toContain("--prod --skip-domain"); + expect(deployWorkflow).toContain("perf:read-model:capture"); + expect(deployWorkflow).toContain("actions/upload-artifact@"); + expect(deployWorkflow).toContain("npm run perf:read-model:gate"); + expect(deployWorkflow).toContain("vercel promote"); + expect(deployWorkflow.indexOf("perf:read-model:capture")).toBeLessThan( + deployWorkflow.indexOf("npm run perf:read-model:gate"), + ); + expect(deployWorkflow.indexOf("npm run perf:read-model:gate")).toBeLessThan( + deployWorkflow.indexOf("vercel promote"), + ); + const captureSource = readFileSync( + resolve(process.cwd(), "scripts/perf/read-model-capture.mjs"), + "utf8", + ); + expect(captureSource).toContain(RUNTIME_CAPTURE_PATH_FIXTURE); + expect(captureSource).toContain( + 'headers["x-programmable-shadow-probe-signature"]', + ); + expect(captureSource).not.toContain("x-programmable-shadow-probe-token"); + expect(captureSource).not.toContain('"dataset-manifest"'); + expect(captureSource).not.toContain('"rpc-trace"'); + }); + + it("pins distributed release-probe failure and replay semantics", () => { + const profile = gateCore.parseReadModelLoadProfile(profileFixture()); + const coordinatorPath = "lib/data-pipeline/route-coordinator.server.ts"; + const noncePath = "lib/data-pipeline/release-probe-nonce.server.ts"; + const readinessPath = "lib/data-pipeline/public-route-readiness.server.ts"; + const coordinator = readFileSync( + resolve(process.cwd(), coordinatorPath), + "utf8", + ); + const nonceConsumer = readFileSync(resolve(process.cwd(), noncePath), "utf8"); + const readiness = readFileSync(resolve(process.cwd(), readinessPath), "utf8"); + const drift = sourceContracts.evaluateReadModelSourceContracts( + process.cwd(), + profile, + { + sourceOverrides: { + [coordinatorPath]: coordinator + .replace( + "const RELEASE_PROBE_MAX_AGE_MS = 5 * 60 * 1_000;", + "const RELEASE_PROBE_MAX_AGE_MS = 15 * 60 * 1_000;", + ) + .replace("provenanceHeaders({ source: result.source })", "discardedHeaders({ source: result.source })"), + [noncePath]: nonceConsumer.replace( + 'const RELEASE_PROBE_LOGIN = "programmable_release_probe_nonce_login";', + 'const RELEASE_PROBE_LOGIN = "shared_runtime_login";', + ), + [readinessPath]: readiness + .replace("status: 503,", "status: 500,") + .replace("if (releaseProbe) {", "if (true) {"), + }, + }, + ); + expect( + drift.failures.map((failure: { id: string }) => failure.id), + ).toEqual( + expect.arrayContaining([ + "source-release-probe-freshness", + "source-release-probe-distributed-replay", + "source-release-probe-private-failure", + "source-release-probe-replay-validation", + "source-release-probe-selected-provenance", + ]), + ); + }); +}); diff --git a/tests/data-pipeline/postgres-connection.test.ts b/tests/data-pipeline/postgres-connection.test.ts new file mode 100644 index 00000000..71ca1bfa --- /dev/null +++ b/tests/data-pipeline/postgres-connection.test.ts @@ -0,0 +1,249 @@ +import { describe, expect, it, vi } from "vitest"; +import { rootCertificates } from "node:tls"; + +vi.mock("server-only", () => ({})); + +import { + validatedPostgresConnectionString, + validatedPostgresSslCa, +} from "../../lib/data-pipeline/postgres-connection.server"; +import { DataPipelineError } from "../../lib/data-pipeline/errors"; +import { + createPostgresExecutor, + postgresJson, +} from "../../lib/data-pipeline/postgres"; + +const TEST_CA = rootCertificates[0]!; + +describe("Postgres connection boundary", () => { + it("requires certificate and hostname verification for every remote database", () => { + const verified = + "postgresql://reader:password@db.example:5432/postgres?sslmode=verify-full"; + const encodedCredential = + "postgresql://reader:p%40ssword@db.example:5432/postgres?sslmode=verify-full"; + const officialPooler = + "postgres://postgres.project:password@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full"; + + expect(validatedPostgresConnectionString(verified)).toBe(verified); + expect(validatedPostgresConnectionString(encodedCredential)).toBe( + encodedCredential, + ); + expect(validatedPostgresConnectionString(officialPooler)).toBe( + officialPooler, + ); + for (const connectionString of [ + "postgresql://reader:password@db.example/postgres", + "postgresql://reader:password@db.example/postgres?sslmode=require", + "postgresql://reader:password@db.example/postgres?sslmode=prefer", + "postgresql://reader:password@db.example/postgres?sslmode=disable", + "postgresql://reader:password@db.example/postgres?sslmode=verify-full&sslmode=require", + "postgresql://reader:password@db.example/postgres?sslmode=verify-full&ssl=disable", + "postgresql://reader:s3cr3t@attacker.example,unused@localhost/postgres", + "postgresql://reader:s3cr3t@attacker.example%2Clocalhost/postgres", + "postgresql://reader:s3cr3t@attacker.example@localhost/postgres", + "postgresql://reader:s3cr3t@localhost\\@attacker.example/postgres", + "postgresql://reader:s3cr3t@localhost/postgres?sslmode=disable&host=attacker.example", + "postgresql://reader:s3cr3t@127.000.000.001/postgres", + "postgresql://reader:s3cr3t@localhost.example/postgres", + "postgresql://postgres:postgres@[::1]:54322/postgres", + "postgresql://reader:password@db.example/postgres?sslmode=verify-full", + "postgresql://reader:password@DB.example:5432/postgres?sslmode=verify-full", + "postgresql://reader:password@d\u0131.example:5432/postgres?sslmode=verify-full", + ]) { + expect(() => + validatedPostgresConnectionString(connectionString), + ).toThrowError(DataPipelineError); + } + }); + + it("allows an explicit non-TLS connection only for a loopback test database", () => { + for (const connectionString of [ + "postgresql://postgres:postgres@localhost:54322/postgres", + "postgresql://postgres:postgres@127.0.0.1:54322/postgres?sslmode=disable", + ]) { + expect(validatedPostgresConnectionString(connectionString)).toBe( + connectionString, + ); + } + }); + + it("rejects malformed credentials and never reflects them in the error", () => { + const secret = "do-not-reflect-this-password"; + let thrown: unknown; + try { + validatedPostgresConnectionString( + `postgresql://reader:${secret}@db.example/postgres?sslmode=require`, + ); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(DataPipelineError); + expect(String(thrown)).not.toContain(secret); + expect(JSON.stringify(thrown)).not.toContain(secret); + + for (const connectionString of [ + "postgresql://db.example/postgres?sslmode=verify-full", + "postgresql://reader:password@/postgres?sslmode=verify-full", + "postgresql://reader:password@db.example/?sslmode=verify-full", + "postgresql://reader:password@db.example/postgres?sslmode=verify-full#fragment", + ]) { + expect(() => + validatedPostgresConnectionString(connectionString), + ).toThrowError(DataPipelineError); + } + }); + + it("requires a real CA and pins verified TLS for a remote pooler", async () => { + const connectionString = + "postgres://postgres.project:password@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full"; + const factory = vi.fn((_url: string, options: unknown) => + ({ + begin: vi.fn(), + end: vi.fn(async () => undefined), + options, + }) as never, + ); + + expect(() => + createPostgresExecutor({ connectionString, postgresFactory: factory }), + ).toThrowError(DataPipelineError); + expect(factory).not.toHaveBeenCalled(); + + const executor = createPostgresExecutor({ + connectionString, + sslCaPem: TEST_CA, + postgresFactory: factory, + }); + const options = factory.mock.calls[0]?.[1]; + expect(options).toMatchObject({ + ssl: { ca: TEST_CA, rejectUnauthorized: true }, + fetch_types: true, + connection: { + application_name: "programmable-read-model", + }, + }); + expect(options).not.toMatchObject({ + connection: { + statement_timeout: expect.anything(), + }, + }); + await executor.close(); + }); + + it("binds SQL arrays through the postgres driver array encoder", async () => { + const encodedArray = Object.freeze({ kind: "driver-array" }); + const array = vi.fn(() => encodedArray); + const unsafe = vi.fn(async () => []); + const begin = vi.fn(async (work: (transaction: unknown) => unknown) => + work({ unsafe }), + ); + const factory = vi.fn(() => + ({ + array, + begin, + end: vi.fn(async () => undefined), + }) as never, + ); + const executor = createPostgresExecutor({ + connectionString: + "postgresql://postgres:postgres@127.0.0.1:54322/postgres?sslmode=disable", + allowInsecureLoopback: true, + postgresFactory: factory, + }); + const bytes = [new Uint8Array([1, 2]), new Uint8Array([3, 4])]; + + await executor.transaction((transaction) => + transaction.query("select $1::bytea[], $2::text", [bytes, "value"]), + ); + + expect(array).toHaveBeenCalledWith(bytes); + expect(unsafe).toHaveBeenCalledWith( + "select $1::bytea[], $2::text", + [encodedArray, "value"], + ); + await executor.close(); + }); + + it("binds JSON arrays through the JSON encoder without double encoding", async () => { + const encodedJson = Object.freeze({ kind: "driver-json" }); + const json = vi.fn(() => encodedJson); + const unsafe = vi.fn(async () => []); + const begin = vi.fn(async (work: (transaction: unknown) => unknown) => + work({ unsafe }), + ); + const factory = vi.fn(() => + ({ + array: vi.fn(), + json, + begin, + end: vi.fn(async () => undefined), + }) as never, + ); + const executor = createPostgresExecutor({ + connectionString: + "postgresql://postgres:postgres@127.0.0.1:54322/postgres?sslmode=disable", + allowInsecureLoopback: true, + postgresFactory: factory, + }); + const value = [{ orderedTopics: ["0x01", "0x02"] }]; + + await executor.transaction((transaction) => + transaction.query("select $1::jsonb", [postgresJson(value)]), + ); + + expect(json).toHaveBeenCalledWith(value); + expect(unsafe).toHaveBeenCalledWith("select $1::jsonb", [encodedJson]); + await executor.close(); + }); + + it("rejects a global TLS bypass even when the connection has its own CA", () => { + vi.stubEnv("NODE_TLS_REJECT_UNAUTHORIZED", "0"); + try { + expect(() => + createPostgresExecutor({ + connectionString: + "postgres://postgres.project:password@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full", + sslCaPem: TEST_CA, + postgresFactory: vi.fn(() => ({}) as never), + }), + ).toThrowError(DataPipelineError); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("requires an explicit opt-in before allowing plaintext loopback", () => { + const connectionString = + "postgresql://postgres:postgres@127.0.0.1:54322/postgres?sslmode=disable"; + const factory = vi.fn(() => + ({ begin: vi.fn(), end: vi.fn(async () => undefined) }) as never, + ); + + expect(() => + createPostgresExecutor({ connectionString, postgresFactory: factory }), + ).toThrowError(DataPipelineError); + expect(factory).not.toHaveBeenCalled(); + + expect(() => + createPostgresExecutor({ + connectionString, + allowInsecureLoopback: true, + postgresFactory: factory, + }), + ).not.toThrow(); + }); + + it("validates a CA certificate without accepting keys or arbitrary PEM", () => { + expect(validatedPostgresSslCa(TEST_CA)).toBe(TEST_CA); + for (const value of [ + undefined, + "not-a-certificate", + "-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----", + ]) { + expect(() => validatedPostgresSslCa(value)).toThrowError( + DataPipelineError, + ); + } + }); +}); diff --git a/tests/data-pipeline/postgres-final-contract.test.ts b/tests/data-pipeline/postgres-final-contract.test.ts new file mode 100644 index 00000000..e6565386 --- /dev/null +++ b/tests/data-pipeline/postgres-final-contract.test.ts @@ -0,0 +1,553 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + createPostgresReadModel, + type PostgresExecutor, + type PostgresParameter, + type PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; + +const TOKEN = "0x1111111111111111111111111111111111111111"; +const CREATOR = "0x2222222222222222222222222222222222222222"; +const QUOTE = "0x3333333333333333333333333333333333333333"; +const HOOK = "0x4444444444444444444444444444444444444444"; +const VAULT = "0x5555555555555555555555555555555555555555"; +const POOL_ID = `0x${"66".repeat(32)}`; +const LAUNCH_HASH = `0x${"77".repeat(32)}`; +const TRANSACTION_HASH = `0x${"88".repeat(32)}`; +const BLOCK_HASH = `0x${"99".repeat(32)}`; +const DEPLOYMENT_COMMITMENT = `0x${"aa".repeat(32)}`; +const SCHEMA_COMMITMENT = `0x${"bb".repeat(32)}`; +const RECONCILIATION_COMMITMENT = `0x${"cc".repeat(32)}`; + +function bytes(hex: string) { + return Uint8Array.from( + hex + .slice(2) + .match(/.{2}/g)! + .map((part) => Number.parseInt(part, 16)), + ); +} + +function launchRow() { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + token: bytes(TOKEN), + creator: bytes(CREATOR), + launch_transaction_hash: bytes(TRANSACTION_HASH), + pool_id: bytes(POOL_ID), + reward_vault: bytes(VAULT), + launch_hash: bytes(LAUNCH_HASH), + token_name: "Programmable Test", + token_symbol: "TEST", + total_supply: "1000000000000000000000000000", + launch_block_timestamp: "2026-07-31T08:00:00.000Z", + launch_transaction_index: 7, + launch_receipt_log_ordinal: 2, + currency0: bytes(TOKEN), + currency1: bytes(QUOTE), + hook: bytes(HOOK), + quote_asset: bytes(QUOTE), + pool_key_fee: "8388608", + tick_spacing: 200, + buy_swap_fee_bps: 100, + sell_swap_fee_bps: 100, + creator_fee_bps: 90, + launcher_fee_bps: 10, + transfer_tax_bps: 0, + lp_fee_pips: "10000", + total_swap_fee_bps: 100, + project_name: "Programmable Test", + project_description: "Canonical metadata", + project_logo_reference: "https://programmable.family/test.png", + project_metadata_revision: "3", + project_metadata_created_at: "2026-07-31T08:00:30.000Z", + project_links: [ + { + kind: "website", + url: "https://programmable.family", + displayOrder: 0, + }, + { + kind: "x", + url: "https://x.com/0xProgrammable", + displayOrder: 1, + }, + ], + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + }; +} + +function rewardRow() { + return { + chain_id: "1", + account: bytes(CREATOR), + release_id: "classic-v3", + model_id: "classic", + vault: bytes(VAULT), + pool_id: bytes(POOL_ID), + hook: bytes(HOOK), + quote_asset: bytes(QUOTE), + entitled: "1000", + claimable_accrued: "900", + claimed_total: "100", + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + }; +} + +function vaultHistoryRow() { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + vault: bytes(VAULT), + pool_id: bytes(POOL_ID), + quote_asset: bytes(QUOTE), + configuration_hash: bytes(RECONCILIATION_COMMITMENT), + configuration_epoch: "1", + allocation_index: 0, + beneficiary: bytes(CREATOR), + payout_address: bytes(CREATOR), + share_bps: 9000, + effective_from_block: "25650000", + effective_to_block: null, + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + }; +} + +function lookupRow() { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + token: bytes(TOKEN), + creator: bytes(CREATOR), + launch_transaction_hash: bytes(TRANSACTION_HASH), + pool_id: bytes(POOL_ID), + reward_vault: bytes(VAULT), + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + }; +} + +function snapshotRow() { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + token: bytes(TOKEN), + pool_id: bytes(POOL_ID), + source_deployment_commitment: bytes(DEPLOYMENT_COMMITMENT), + source_schema_commitment: bytes(SCHEMA_COMMITMENT), + block_number: "25650012", + block_hash: bytes(BLOCK_HASH), + sqrt_price_x96: "79228162514264337593543950336", + liquidity: "1000000000000000000", + market_volume_token0: "1234.500000000000000000", + market_volume_token1: "2.500000000000000000", + market_volume_usd: "6123.45", + hook_gross_volume: "2500000000000000000", + observed_at: "2026-07-31T08:05:00.000Z", + reconciliation_evidence_commitment: bytes(RECONCILIATION_COMMITMENT), + reconciled_at: "2026-07-31T08:05:02.000Z", + }; +} + +function candleRow() { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + token: bytes(TOKEN), + pool_id: bytes(POOL_ID), + source_deployment_commitment: bytes(DEPLOYMENT_COMMITMENT), + source_schema_commitment: bytes(SCHEMA_COMMITMENT), + source_block_number: "25650012", + source_block_hash: bytes(BLOCK_HASH), + interval: "hour", + period_start: "2026-07-31T08:00:00.000Z", + period_end: "2026-07-31T09:00:00.000Z", + open: "1.10", + high: "1.40", + low: "1.00", + close: "1.25", + volume_token0: "300.5", + volume_token1: "0.7", + volume_usd: "1500.25", + reconciliation_evidence_commitment: bytes(RECONCILIATION_COMMITMENT), + reconciled_at: "2026-07-31T09:00:02.000Z", + }; +} + +type RecordedQuery = { + text: string; + values: readonly PostgresParameter[]; +}; + +class FakeExecutor implements PostgresExecutor { + readonly queries: RecordedQuery[] = []; + readonly close = vi.fn(async () => undefined); + + constructor( + private readonly responder: ( + text: string, + values: readonly PostgresParameter[], + ) => Promise[]>, + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if ( + text === + "select session_user::text as session_user, current_role::text as current_role" + ) { + return [ + { + session_user: "programmable_api_reader_login", + current_role: "programmable_api_reader", + }, + ] as unknown as Row[]; + } + if (text === "select session_user::text as session_user") { + return [ + { session_user: "programmable_api_reader_login" }, + ] as unknown as Row[]; + } + return (await this.responder(text, values)) as Row[]; + }, + }); + } +} + +describe("final private read-model database contract", () => { + it("reads rich launches only through the final bounded v1 functions", async () => { + const executor = new FakeExecutor(async (text) => + text.includes("get_recent_launches_v1") ? [launchRow()] : [], + ); + const model = createPostgresReadModel({ executor }); + + await expect( + model.recentLaunches({ + chainId: "1", + limit: 25, + cursor: { + blockNumber: "25660000", + transactionHash: `0x${"ff".repeat(32)}`, + token: "0xffffffffffffffffffffffffffffffffffffffff", + }, + }), + ).resolves.toEqual([ + expect.objectContaining({ + chainId: "1", + releaseVersion: "classic-v3", + modelVersion: "classic", + token: TOKEN, + creator: CREATOR, + tokenName: "Programmable Test", + tokenSymbol: "TEST", + totalSupply: "1000000000000000000000000000", + currency0: TOKEN, + currency1: QUOTE, + hook: HOOK, + quoteAsset: QUOTE, + poolKeyFee: "8388608", + tickSpacing: 200, + creatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: "10000", + project: { + name: "Programmable Test", + description: "Canonical metadata", + logoReference: "https://programmable.family/test.png", + revision: "3", + createdAt: "2026-07-31T08:00:30.000Z", + links: [ + { + kind: "website", + url: "https://programmable.family", + displayOrder: 0, + }, + { + kind: "x", + url: "https://x.com/0xProgrammable", + displayOrder: 1, + }, + ], + }, + }), + ]); + + const query = executor.queries.at(-1)!; + expect(query.text).toContain( + "programmable_private.get_recent_launches_v1($1, $2, $3, $4, $5)", + ); + expect(query.values).toEqual([ + "1", + 25, + "25660000", + bytes(`0x${"ff".repeat(32)}`), + bytes("0xffffffffffffffffffffffffffffffffffffffff"), + ]); + }); + + it("reads creator launches and reward balances from their exact gated surfaces", async () => { + const executor = new FakeExecutor(async (text) => { + if (text.includes("launches_by_creator_v1")) { + return [ + { + ...launchRow(), + launch_transaction_index: "7", + launch_receipt_log_ordinal: "2", + }, + ]; + } + if (text.includes("get_account_reward_summary_v1")) { + return [rewardRow()]; + } + return []; + }); + const model = createPostgresReadModel({ executor }); + + await expect( + model.publicProfile({ + chainId: "1", + account: CREATOR, + limit: 20, + offset: 0, + }), + ).resolves.toMatchObject({ + launches: [{ token: TOKEN, project: { revision: "3" } }], + rewards: [ + { + vault: VAULT, + poolId: POOL_ID, + hook: HOOK, + quoteAsset: QUOTE, + entitled: "1000", + claimable: "900", + claimed: "100", + }, + ], + }); + + const sql = executor.queries.map(({ text }) => text).join("\n"); + expect(sql).toContain("programmable_private.launches_by_creator_v1"); + expect(sql).toContain( + "programmable_private.get_account_reward_summary_v1($1, $2)", + ); + }); + + it("accepts every final circuit state without inventing legacy spellings", async () => { + const executor = new FakeExecutor(async (text) => { + if (text.includes("checkpoint_summary_v1")) return []; + if (text.includes("parity_summary_v1")) return []; + if (text.includes("health_summary_v1")) { + return [ + { + dependency: "envio", + circuit_status: "half_open", + observed_at: "2026-07-31T08:10:00.000Z", + failure_count: "2", + retry_after: "2026-07-31T08:11:00.000Z", + }, + { + dependency: "rpc-a", + circuit_status: "frozen", + observed_at: "2026-07-31T08:10:00.000Z", + failure_count: "3", + retry_after: null, + }, + ]; + } + return []; + }); + const model = createPostgresReadModel({ executor }); + + await expect(model.health()).resolves.toMatchObject({ + circuits: [ + { dependency: "envio", state: "half_open" }, + { dependency: "rpc-a", state: "frozen" }, + ], + }); + }); + + it("serves only reconciled snapshot and candle rows with immutable provenance", async () => { + const executor = new FakeExecutor(async (text) => { + if (text.includes("market_snapshots_v1")) return [snapshotRow()]; + if (text.includes("market_candles_v1")) return [candleRow()]; + return []; + }); + const model = createPostgresReadModel({ executor }); + + await expect( + model.marketSnapshot({ chainId: "1", token: TOKEN }), + ).resolves.toMatchObject({ + token: TOKEN, + blockNumber: "25650012", + marketVolumeUsd: "6123.45", + sourceDeploymentCommitment: DEPLOYMENT_COMMITMENT, + sourceSchemaCommitment: SCHEMA_COMMITMENT, + reconciliationEvidenceCommitment: RECONCILIATION_COMMITMENT, + }); + await expect( + model.marketCandles({ + chainId: "1", + token: TOKEN, + interval: "hour", + from: "2026-07-31T00:00:00.000Z", + to: "2026-08-01T00:00:00.000Z", + limit: 168, + }), + ).resolves.toEqual([ + expect.objectContaining({ + interval: "hour", + open: "1.10", + high: "1.40", + low: "1.00", + close: "1.25", + volumeUsd: "1500.25", + }), + ]); + + const dataQueries = executor.queries.filter( + ({ text }) => + !/^set local/i.test(text) && !/select session_user/i.test(text), + ); + expect(dataQueries[0]!.values).toEqual(["1", bytes(TOKEN)]); + expect(dataQueries[1]!.values).toEqual([ + "1", + bytes(TOKEN), + "hour", + new Date("2026-07-31T00:00:00.000Z"), + new Date("2026-08-01T00:00:00.000Z"), + 168, + ]); + }); + + it("fails closed on malformed descriptive metadata instead of leaking it to routes", async () => { + const malformed = launchRow(); + malformed.project_links = [ + { + kind: "website", + url: "javascript:alert(1)", + displayOrder: 0, + }, + ]; + const executor = new FakeExecutor(async () => [malformed]); + const model = createPostgresReadModel({ executor }); + + await expect( + model.recentLaunches({ chainId: "1", limit: 1 }), + ).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + }); + }); + + it("fails closed when a private read surface returns a row outside the requested scope", async () => { + const wrongAddress = "0xffffffffffffffffffffffffffffffffffffffff"; + const wrongTransaction = `0x${"ff".repeat(32)}`; + const cases: Array<{ + row: Record; + run: (model: ReturnType) => Promise; + }> = [ + { + row: { ...launchRow(), chain_id: "10" }, + run: (model) => model.recentLaunches({ chainId: "1", limit: 1 }), + }, + { + row: { ...launchRow(), token: bytes(wrongAddress) }, + run: (model) => model.launchByToken({ chainId: "1", token: TOKEN }), + }, + { + row: { ...launchRow(), creator: bytes(wrongAddress) }, + run: (model) => + model.publicProfile({ + chainId: "1", + account: CREATOR, + limit: 1, + offset: 0, + }), + }, + { + row: { ...rewardRow(), account: bytes(wrongAddress) }, + run: (model) => + model.publicProfile({ + chainId: "1", + account: CREATOR, + limit: 1, + offset: 0, + }), + }, + { + row: { ...snapshotRow(), token: bytes(wrongAddress) }, + run: (model) => model.marketSnapshot({ chainId: "1", token: TOKEN }), + }, + { + row: { ...candleRow(), interval: "day" }, + run: (model) => + model.marketCandles({ + chainId: "1", + token: TOKEN, + interval: "hour", + from: "2026-07-31T00:00:00.000Z", + to: "2026-08-01T00:00:00.000Z", + limit: 1, + }), + }, + { + row: { + ...lookupRow(), + launch_transaction_hash: bytes(wrongTransaction), + }, + run: (model) => + model.launchLookup({ + chainId: "1", + transactionHash: TRANSACTION_HASH, + limit: 1, + }), + }, + { + row: { ...vaultHistoryRow(), vault: bytes(wrongAddress) }, + run: (model) => + model.classicVaultHistory({ chainId: "1", vault: VAULT, limit: 1 }), + }, + ]; + + for (const testCase of cases) { + const executor = new FakeExecutor(async (text) => { + if (text.includes("launches_by_creator_v1")) { + return "creator" in testCase.row ? [testCase.row] : []; + } + if (text.includes("get_account_reward_summary_v1")) { + return "account" in testCase.row ? [testCase.row] : []; + } + return [testCase.row]; + }); + await expect( + testCase.run(createPostgresReadModel({ executor })), + ).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + }); + } + }); +}); diff --git a/tests/data-pipeline/postgres-projector-store.test.ts b/tests/data-pipeline/postgres-projector-store.test.ts new file mode 100644 index 00000000..bc38a9e0 --- /dev/null +++ b/tests/data-pipeline/postgres-projector-store.test.ts @@ -0,0 +1,2657 @@ +import { describe, expect, it, vi } from "vitest"; +import { concat, encodeAbiParameters, keccak256, toBytes } from "viem"; + +vi.mock("server-only", () => ({})); + +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; +import { + createPostgresReleaseProjectionStore, + createPostgresProjectorStore, + type ProjectorProviderDatabaseBinding, + type ProjectorReleaseDatabaseScope, +} from "../../lib/data-pipeline/postgres-projector"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import type { DualRpcCandidateWindowEvidence } from "../../lib/data-pipeline/dual-rpc"; +import { projectorOccurrenceUuid } from "../../lib/data-pipeline/projector-ids"; +import { foldProjectorRewardState } from "../../lib/data-pipeline/projector-reward-fold"; +import { runtimeBytecodeEvidence } from "../../lib/data-pipeline/runtime-bytecode"; + +const bytes32 = (byte: string) => `0x${byte.repeat(64)}` as `0x${string}`; +const address = (byte: string) => `0x${byte.repeat(40)}` as `0x${string}`; +const bytes = (hex: string) => Buffer.from(hex.slice(2), "hex"); +const executionTrace = (candidateBatchSize = 0) => ({ + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 48, + elapsedMs: 1, + providerCallCounts: [0, 0] as const, + calls: [], +}); + +const PROVIDERS: readonly ProjectorProviderDatabaseBinding[] = [ + { + type: "envio_deployment", + redactedIdentity: "envio-mainnet-v1", + deploymentCommitment: bytes32("1"), + schemaCommitment: bytes32("2"), + }, + { + type: "rpc_provider", + redactedIdentity: "rpc:1:alchemy", + deploymentCommitment: bytes32("3"), + schemaCommitment: bytes32("4"), + }, + { + type: "rpc_provider", + redactedIdentity: "rpc:1:quicknode", + deploymentCommitment: bytes32("5"), + schemaCommitment: bytes32("6"), + }, +] as const; +const RPC_EVIDENCE_BINDINGS = [ + { + identity: "alchemy", + vendorGroup: "alchemy", + endpointCommitment: bytes32("3"), + endpointOriginCommitment: bytes32("4"), + }, + { + identity: "quicknode", + vendorGroup: "quicknode", + endpointCommitment: bytes32("5"), + endpointOriginCommitment: bytes32("6"), + }, +] as const; + +const projectionExecutionTrace = { + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize: 1, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 48, + elapsedMs: 1, + providerCallCounts: [1, 1] as const, + calls: RPC_EVIDENCE_BINDINGS.map((binding) => ({ + providerIdentity: binding.identity, + providerVendorGroup: binding.vendorGroup, + providerEndpointCommitment: binding.endpointCommitment, + providerOriginCommitment: binding.endpointOriginCommitment, + operation: "getChainId" as const, + attempt: 1, + startedOffsetMs: 0, + durationMs: 1, + outcome: "success" as const, + })), +}; + +const RELEASE_SCOPES = [ + { releaseId: "classic-v2", modelId: "classic", sourceGroup: "core" }, + { releaseId: "classic-v3", modelId: "classic", sourceGroup: "core" }, + { releaseId: "stock-paired-v1", modelId: "stock-paired", sourceGroup: "core" }, + { releaseId: "stock-paired-v2", modelId: "stock-paired", sourceGroup: "core" }, + { releaseId: "stock-paired-v3", modelId: "stock-paired", sourceGroup: "core" }, +] as const; + +const IDS = [ + "10000000-0000-4000-8000-000000000001", + "10000000-0000-4000-8000-000000000002", + "10000000-0000-4000-8000-000000000003", +] as const; + +const RUNTIME_FENCE = Object.freeze({ + holderId: "projector-runtime-test", + generation: "7", + tokenHash: bytes32("a"), +}); + +type QueryRecord = { text: string; values: readonly PostgresParameter[] }; + +class StoreExecutor implements PostgresExecutor { + readonly queries: QueryRecord[] = []; + readonly close = vi.fn(async () => undefined); + transactionCount = 0; + commitGeneration = "8"; + cursorBlockNumber = "25650000"; + includeHistoricalStock = false; + provisionalRows: readonly Record[] = []; + provisionalActivationRows: readonly Record[] = []; + pendingActivationResolutionRows: readonly Record[] = []; + reorgTargetRows: readonly Record[] = []; + reorgRecoveryRows: readonly Record[] = [{ + cursor_generation: "8", + reorg_generation: "1", + release_checkpoint_count: "5", + }]; + omitReorgGeneration = false; + classicNormalizedRuntimeCodeHash = bytes32("e"); + classicImmutableReferencesCommitment = bytes32("f"); + reusedSafeHeadObservationId: string | null = null; + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + this.transactionCount += 1; + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if (text === "select session_user::text as session_user") { + return [ + { session_user: "programmable_projector_login" }, + ] as unknown as Row[]; + } + if (text.includes("current_role::text")) { + return [ + { + session_user: "programmable_projector_login", + current_role: "programmable_projector", + }, + ] as unknown as Row[]; + } + if (text.includes("assert_projector_runtime_lease_v1")) { + return [{ asserted: true }] as unknown as Row[]; + } + if (text.includes("get_projector_runtime_state_v1")) { + const release = values[1]; + const row: Record = { + epoch_id: + release === "envio-control" + ? "70000000-0000-4000-8000-000000000002" + : "70000000-0000-4000-8000-000000000010", + pointer_generation: "1", + provider_deployment_ids: IDS, + provider_types: PROVIDERS.map(({ type }) => type), + provider_redacted_identities: PROVIDERS.map( + ({ redactedIdentity }) => redactedIdentity, + ), + lease_generation: "0", + lease_holder_id: null, + lease_acquired_at: null, + lease_expires_at: null, + checkpoint_id: null, + checkpoint_generation: "0", + reorg_generation: "0", + checkpoint_block_number: null, + checkpoint_block_hash: null, + checkpoint_cursor_block_global_log_index: null, + checkpoint_cursor_candidate_id: null, + }; + if (this.omitReorgGeneration) delete row.reorg_generation; + return [row] as unknown as Row[]; + } + if (text.includes("get_envio_ingestion_cursor_v1")) { + return [ + { + generation: "7", + block_number: this.cursorBlockNumber, + block_hash: bytes(bytes32("7")), + block_global_log_index: "9", + candidate_id: `1:${bytes32("7")}:${bytes32("8")}:9`, + }, + ] as unknown as Row[]; + } + if (text.includes("get_projector_release_manifest_v1")) { + if (values[1] === "stock-paired-v1" && this.includeHistoricalStock) { + return [ + { + epoch_id: "70000000-0000-4000-8000-000000000010", + pointer_generation: "1", + epoch_commitment: bytes(bytes32("9")), + artifact_creation_code_commitment: bytes(bytes32("a")), + source_bindings: [ + { + binding_id: "21000000-0000-4000-8000-000000000001", + source_name: "StockV1RewardVaultFactory", + source_role: "vault_factory", + source_type: "ethereum_contract", + source_address: + "0xd430d9162c153afdf9e4caca6d2317e72a044441", + inclusive_start_block: "25637469", + abi_event_set_commitment: bytes32("4"), + binding_commitment: bytes32("5"), + }, + ], + dynamic_source_templates: [ + { + dynamic_source_template_id: + "31000000-0000-4000-8000-000000000001", + parent_factory_release_binding_id: + "21000000-0000-4000-8000-000000000001", + parent_factory_binding_commitment: bytes32("5"), + parent_source_role: "vault_factory", + factory_event_type: "QuoteAssetFeeSplitVaultDeployed", + deployed_address_field: "vault", + deployed_source_role: "reward_vault", + deployed_artifact_creation_code_commitment: bytes32("6"), + normalized_runtime_code_hash: bytes32("7"), + expected_instance_runtime_code_hash: null, + immutable_references_commitment: bytes32("8"), + immutable_binding_spec: { + factoryConfigurationField: "configurationCommitment", + bindings: [ + { + ordinal: "0", + offset: "4", + length: "20", + source: "deployed_address", + encoding: "address", + }, + ], + }, + immutable_binding_commitment: bytes32("9"), + runtime_code_length: "220", + abi_event_set_commitment: bytes32("a"), + template_commitment: bytes32("b"), + }, + ], + projection_event_rules: [], + launch_completeness_requirements: [], + }, + ] as unknown as Row[]; + } + if (values[1] !== "classic-v3") { + return [ + { + epoch_id: "70000000-0000-4000-8000-000000000010", + pointer_generation: "1", + epoch_commitment: bytes(bytes32("9")), + artifact_creation_code_commitment: bytes(bytes32("a")), + source_bindings: [], + dynamic_source_templates: [], + projection_event_rules: [], + launch_completeness_requirements: [], + }, + ] as unknown as Row[]; + } + return [ + { + epoch_id: "70000000-0000-4000-8000-000000000010", + pointer_generation: "1", + epoch_commitment: bytes(bytes32("9")), + artifact_creation_code_commitment: bytes(bytes32("a")), + source_bindings: [ + { + binding_id: "20000000-0000-4000-8000-000000000001", + source_name: "ClassicV3RewardVaultFactory", + source_role: "vault_factory", + source_type: "ethereum_contract", + source_address: "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + recovery_selector: null, + inclusive_start_block: "25640000", + abi_event_set_commitment: bytes32("b"), + artifact_creation_code_commitment: bytes32("a"), + binding_commitment: bytes32("c"), + }, + ], + dynamic_source_templates: [ + { + dynamic_source_template_id: + "30000000-0000-4000-8000-000000000001", + parent_factory_release_binding_id: + "20000000-0000-4000-8000-000000000001", + parent_factory_binding_commitment: bytes32("c"), + parent_source_role: "vault_factory", + factory_event_type: "ClassicRewardVaultDeployed", + deployed_address_field: "vault", + deployed_source_role: "reward_vault", + deployed_artifact_creation_code_commitment: bytes32("d"), + normalized_runtime_code_hash: + this.classicNormalizedRuntimeCodeHash, + expected_instance_runtime_code_hash: null, + immutable_references_commitment: + this.classicImmutableReferencesCommitment, + immutable_binding_spec: { + factoryConfigurationField: "configurationCommitment", + bindings: [ + { + ordinal: "0", + offset: "4", + length: "20", + source: "deployed_address", + encoding: "address", + }, + ], + }, + immutable_binding_commitment: bytes32("1"), + runtime_code_length: "200", + abi_event_set_commitment: bytes32("2"), + template_commitment: bytes32("3"), + }, + ], + projection_event_rules: [], + launch_completeness_requirements: [], + }, + ] as unknown as Row[]; + } + if (text.includes("get_projector_dynamic_source_attestations_v1")) { + if (values[1] === "stock-paired-v1" && this.includeHistoricalStock) { + return [ + { + dynamic_source_attestation_id: + "41000000-0000-4000-8000-000000000001", + dynamic_source_template_id: + "31000000-0000-4000-8000-000000000001", + runtime_code_evidence_id: + "41000000-0000-4000-8000-000000000002", + deployed_source_address: bytes(address("e")), + deployed_source_role: "reward_vault", + deployment_block_number: "25646000", + runtime_code_hash: bytes(bytes32("c")), + normalized_runtime_code_hash: bytes(bytes32("7")), + expected_instance_runtime_code_hash: null, + runtime_code_length: "220", + immutable_references_commitment: bytes(bytes32("8")), + immutable_binding_commitment: bytes(bytes32("9")), + abi_event_set_commitment: bytes(bytes32("a")), + template_commitment: bytes(bytes32("b")), + parent_factory_occurrence_id: + "41000000-0000-4000-8000-000000000003", + parent_factory_release_binding_id: + "21000000-0000-4000-8000-000000000001", + parent_factory_binding_commitment: bytes(bytes32("5")), + }, + ] as unknown as Row[]; + } + if (values[1] !== "classic-v3") { + return [] as unknown as Row[]; + } + return [ + { + dynamic_source_attestation_id: + "40000000-0000-4000-8000-000000000001", + dynamic_source_template_id: + "30000000-0000-4000-8000-000000000001", + runtime_code_evidence_id: + "40000000-0000-4000-8000-000000000002", + deployed_source_address: bytes(address("a")), + deployed_source_role: "reward_vault", + deployment_block_number: "25645000", + runtime_code_hash: bytes(bytes32("4")), + normalized_runtime_code_hash: bytes( + this.classicNormalizedRuntimeCodeHash, + ), + expected_instance_runtime_code_hash: null, + runtime_code_length: "200", + immutable_references_commitment: bytes( + this.classicImmutableReferencesCommitment, + ), + immutable_binding_spec: { + factoryConfigurationField: "configurationCommitment", + bindings: [ + { + ordinal: "0", + offset: "4", + length: "20", + source: "deployed_address", + encoding: "address", + }, + ], + }, + immutable_binding_commitment: bytes(bytes32("1")), + abi_event_set_commitment: bytes(bytes32("2")), + template_commitment: bytes(bytes32("3")), + attestation_commitment: bytes(bytes32("5")), + parent_factory_occurrence_id: + "40000000-0000-4000-8000-000000000003", + parent_factory_release_binding_id: + "20000000-0000-4000-8000-000000000001", + parent_factory_binding_commitment: bytes(bytes32("c")), + dynamic_source_release_asset_binding_id: + "40000000-0000-4000-8000-000000000004", + launch_occurrence_id: + "40000000-0000-4000-8000-000000000005", + pool_occurrence_id: + "40000000-0000-4000-8000-000000000006", + token: bytes(address("b")), + pool_id: bytes(bytes32("6")), + hook: bytes(address("c")), + quote_asset: bytes(address("d")), + asset_binding_commitment: bytes(bytes32("7")), + }, + ] as unknown as Row[]; + } + if (text.includes("get_current_provisional_dynamic_sources_v1")) { + return this.provisionalRows as unknown as Row[]; + } + if ( + text.includes( + "get_current_provisional_activation_boundaries_v1", + ) + ) { + return this.provisionalActivationRows as unknown as Row[]; + } + if (text.includes("resolve_pending_dynamic_source_activations_v1")) { + return this.pendingActivationResolutionRows as unknown as Row[]; + } + if (text.includes("get_projector_reorg_generation_v1")) { + return [{ generation: "0" }] as unknown as Row[]; + } + if (text.includes("get_projector_reorg_targets_v1")) { + return this.reorgTargetRows as unknown as Row[]; + } + if (text.includes("recover_projector_reorg_v1")) { + return this.reorgRecoveryRows as unknown as Row[]; + } + if ( + text.includes("open_run") || + text.includes("append_dual_rpc_runtime_code_evidence") || + text.includes("stage_verified_dynamic_parents_v2") || + text.includes("stage_provisional_parent_receipt_ordinals_v1") || + text.includes("append_run_outcome") + ) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("append_or_reuse_safe_head_observation_v1")) { + return [{ + id: this.reusedSafeHeadObservationId ?? values[0], + }] as unknown as Row[]; + } + if (text.includes("append_or_reuse_dual_rpc_block_evidence_v1")) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("append_projection_provider_execution_evidence_v1")) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("commit_envio_ingestion_page_v1")) { + return [{ generation: this.commitGeneration }] as unknown as Row[]; + } + return [] as unknown as Row[]; + }, + }); + } +} + +function candidate(): EnvioCandidate { + return { + candidateId: `1:${bytes32("d")}:${bytes32("e")}:10`, + chainId: 1, + blockNumber: "25650001", + blockHash: bytes32("d"), + blockTimestamp: "1750000000", + transactionHash: bytes32("e"), + transactionIndex: 2, + blockGlobalLogIndex: 10, + sourceAddress: "0x1c6433659fcbafe482c4bc5941752a2674d17d6a", + contractName: "ClassicV3Launcher", + eventName: "ClassicV3TokenLaunched", + releaseHint: { model: "classic", releaseVersion: "classic-v3" }, + orderedTopics: [bytes32("f")], + rawData: "0x", + decodedPayload: {}, + payloadHash: bytes32("1"), + }; +} + +function reorgPlan() { + return { + cursor: { + generation: "7", + blockNumber: "25650000", + blockHash: bytes32("7"), + blockGlobalLogIndex: 9, + candidateId: `1:${bytes32("7")}:${bytes32("8")}:9`, + isBlockBoundary: false, + }, + dynamicSources: [], + provisionalSourceAddresses: [], + dynamicSourceTemplates: [], + database: { + epochId: "70000000-0000-4000-8000-000000000002", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: IDS[0], + rpcProviderDeploymentIds: [IDS[1], IDS[2]] as const, + }, + } as const; +} + +function reorgRecovery() { + return { + action: "rewind-and-replay" as const, + expectedGeneration: "7", + nextGeneration: "8", + targetHistoryGeneration: "6", + targetBlockNumber: "25650000", + targetBlockHash: bytes32("7"), + targetBlockGlobalLogIndex: 9, + targetCandidateId: `1:${bytes32("7")}:${bytes32("8")}:9`, + genesisPointId: null, + expectedReorgGeneration: "0", + nextReorgGeneration: "1", + providerIdentities: [ + "rpc:1:alchemy", + "rpc:1:quicknode", + ] as const, + providerEndpointCommitments: [bytes32("3"), bytes32("5")] as const, + providerOriginCommitments: [bytes32("4"), bytes32("6")] as const, + providerBlockHashes: [bytes32("7"), bytes32("7")] as const, + providerBlockTimestamps: ["1750000000", "1750000000"] as const, + providerChainIds: [1, 1] as const, + providerHeads: ["25650020", "25650021"] as const, + finalityDepth: "12" as const, + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + providerSafeBlockHashes: [bytes32("9"), bytes32("9")] as const, + checkedDepth: 1, + }; +} + +describe("concrete projector Postgres store", () => { + it("rejects missing, duplicate, reordered, or wrong-model release scope sets", () => { + const executor = new StoreExecutor(); + const create = (releaseScopes: readonly ProjectorReleaseDatabaseScope[]) => + createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes, + runtimeFence: RUNTIME_FENCE, + }); + + expect(() => create(RELEASE_SCOPES.slice(0, 4))).toThrow(); + expect(() => + create([...RELEASE_SCOPES.slice(0, 4), RELEASE_SCOPES[0]!]), + ).toThrow(); + expect(() => + create([RELEASE_SCOPES[1]!, RELEASE_SCOPES[0]!, ...RELEASE_SCOPES.slice(2)]), + ).toThrow(); + expect(() => + create( + RELEASE_SCOPES.map((scope, index) => + index === 0 ? { ...scope, modelId: "classic-v2" } : scope, + ), + ), + ).toThrow(); + }); + + it("reads the neutral cursor and only asset-bound current dynamic sources", async () => { + const executor = new StoreExecutor(); + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + + const plan = await store.readPlan(); + + expect(plan).toMatchObject({ + cursor: { generation: "7", blockNumber: "25650000" }, + database: { + envioProviderDeploymentId: IDS[0], + rpcProviderDeploymentIds: [IDS[1], IDS[2]], + }, + dynamicSources: [ + { + attestationId: "40000000-0000-4000-8000-000000000001", + contractName: "ClassicV3RewardVault", + parentOccurrenceId: "40000000-0000-4000-8000-000000000003", + expectedExactRuntimeCodeHash: bytes32("4"), + immutableReferences: [{ start: 4, length: 20 }], + }, + ], + }); + expect(plan.dynamicSourceTemplates).toMatchObject([ + { + contractName: "ClassicV3RewardVault", + parentFactoryContractName: "ClassicV3RewardVaultFactory", + factoryEventName: "ClassicRewardVaultDeployed", + database: { + scope: { releaseId: "classic-v3" }, + reorgGeneration: "0", + }, + }, + ]); + }); + + it("rejects runtime state without an explicit reorg generation", async () => { + const executor = new StoreExecutor(); + executor.omitReorgGeneration = true; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + + await expect(store.readPlan()).rejects.toMatchObject({ + disposition: "fatal-codec-or-caller", + }); + }); + + it("reads bounded reorg history together with the registered genesis anchor", async () => { + const executor = new StoreExecutor(); + executor.reorgTargetRows = [ + { + target_kind: "history", + history_generation: "6", + block_number: "25650000", + block_hash: bytes(bytes32("7")), + block_global_log_index: "9", + candidate_id: `1:${bytes32("7")}:${bytes32("8")}:9`, + genesis_point_id: null, + current_reorg_generation: "0", + }, + { + target_kind: "genesis", + history_generation: "0", + block_number: "0", + block_hash: bytes(bytes32("1")), + block_global_log_index: null, + candidate_id: null, + genesis_point_id: "70000000-0000-4000-8000-000000000006", + current_reorg_generation: "0", + }, + ]; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + + await expect(store.readReorgRecoveryState({ + plan: reorgPlan(), + maximumDepth: 128, + })).resolves.toEqual({ + ancestors: [{ + kind: "history", + historyGeneration: "6", + blockNumber: "25650000", + blockHash: bytes32("7"), + blockGlobalLogIndex: 9, + candidateId: `1:${bytes32("7")}:${bytes32("8")}:9`, + }], + genesis: { + kind: "genesis", + historyGeneration: "0", + genesisPointId: "70000000-0000-4000-8000-000000000006", + blockNumber: "0", + blockHash: bytes32("1"), + blockGlobalLogIndex: null, + candidateId: null, + }, + currentReorgGeneration: "0", + }); + const query = executor.queries.find(({ text }) => + text.includes("get_projector_reorg_targets_v1"), + ); + expect(query?.values).toEqual([IDS[0], "canonical-events", 128]); + }); + + it("persists one CAS-bound recovery and all provider evidence in one transaction", async () => { + const executor = new StoreExecutor(); + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + uuid: (() => { + let suffix = 1; + return () => + `62000000-0000-4000-8000-${String(suffix++).padStart(12, "0")}`; + })(), + now: () => new Date("2026-08-01T03:00:00.000Z"), + }); + + await expect(store.recoverCanonicalReorg({ + plan: reorgPlan(), + recovery: reorgRecovery(), + })).resolves.toEqual({ + generation: "8", + reorgGeneration: "1", + releaseCheckpointCount: 5, + }); + expect(executor.transactionCount).toBe(1); + const statements = executor.queries.map(({ text }) => text); + expect(statements).toEqual(expect.arrayContaining([ + expect.stringContaining("assert_projector_runtime_lease_v1"), + expect.stringContaining("open_run"), + expect.stringContaining("append_or_reuse_safe_head_observation_v1"), + expect.stringContaining("append_or_reuse_dual_rpc_block_evidence_v1"), + expect.stringContaining("append_run_outcome"), + expect.stringContaining("recover_projector_reorg_v1"), + ])); + expect(statements.at(-1)).toContain("recover_projector_reorg_v1"); + const recoveryQuery = executor.queries.at(-1)!; + expect(recoveryQuery.values.slice(7, 12)).toEqual([ + "7", + "8", + "6", + "0", + "1", + ]); + expect(recoveryQuery.values.slice(17, 19)).toEqual([ + RUNTIME_FENCE.holderId, + RUNTIME_FENCE.generation, + ]); + expect( + Buffer.from(recoveryQuery.values[19] as Uint8Array), + ).toEqual(bytes(RUNTIME_FENCE.tokenHash)); + }); + + it("rejects a recovery result that does not advance every release checkpoint", async () => { + const executor = new StoreExecutor(); + executor.reorgRecoveryRows = [{ + cursor_generation: "8", + reorg_generation: "1", + release_checkpoint_count: "4", + }]; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + + await expect(store.recoverCanonicalReorg({ + plan: reorgPlan(), + recovery: reorgRecovery(), + })).rejects.toMatchObject({ disposition: "fatal-codec-or-caller" }); + }); + + it("keeps historical Stock lineage readable without exposing a Stock discovery template", async () => { + const executor = new StoreExecutor(); + executor.includeHistoricalStock = true; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + + const plan = await store.readPlan(); + + expect(plan.dynamicSources).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + attestationId: "41000000-0000-4000-8000-000000000001", + contractName: "StockV1RewardVault", + releaseVersion: "stock-paired-v1", + }), + ]), + ); + expect(plan.dynamicSourceTemplates).toHaveLength(1); + expect(plan.dynamicSourceTemplates[0]?.contractName).toBe( + "ClassicV3RewardVault", + ); + }); + + it("carries an exact current Classic provisional lineage into dynamic coverage", async () => { + const executor = new StoreExecutor(); + executor.provisionalRows = [ + { + provisional_page_id: "42000000-0000-4000-8000-000000000001", + provisional_lineage_id: "42000000-0000-4000-8000-000000000002", + release_epoch_id: "70000000-0000-4000-8000-000000000010", + release_pointer_generation: "1", + ingestion_epoch_id: "70000000-0000-4000-8000-000000000002", + ingestion_pointer_generation: "1", + reorg_generation: "0", + snapshot_block_number: "25650001", + snapshot_block_hash: bytes(bytes32("d")), + expected_cursor_generation: "7", + expected_cursor_block_hash: bytes(bytes32("7")), + envio_provider_deployment_id: IDS[0], + rpc_provider_a_id: IDS[1], + rpc_provider_b_id: IDS[2], + provisional_coverage_commitment: bytes(bytes32("1")), + runtime_code_evidence_id: + "42000000-0000-4000-8000-000000000003", + dynamic_source_template_id: + "30000000-0000-4000-8000-000000000001", + dynamic_source_attestation_id: + "42000000-0000-4000-8000-000000000004", + deployed_source_address: bytes(address("f")), + contract_name: "ClassicV3RewardVault", + model: "classic", + release_version: "classic-v3", + factory_address: bytes( + "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + ), + factory_contract_name: "ClassicV3RewardVaultFactory", + factory_candidate_id: `1:${bytes32("d")}:${bytes32("e")}:4`, + factory_block_number: "25650001", + factory_block_hash: bytes(bytes32("d")), + factory_block_global_log_index: "4", + parent_candidate_commitment: bytes(bytes32("2")), + expected_exact_runtime_code_hash: bytes(bytes32("6")), + expected_normalized_runtime_code_hash: bytes(bytes32("e")), + expected_immutable_references_commitment: bytes(bytes32("f")), + expected_runtime_byte_length: "200", + immutable_references: [{ start: "4", length: "20" }], + staged_at: "2026-08-01T02:00:00.000Z", + }, + ]; + executor.provisionalActivationRows = [ + { + provisional_lineage_id: + "42000000-0000-4000-8000-000000000002", + dynamic_source_attestation_id: + "42000000-0000-4000-8000-000000000004", + deployed_source_address: bytes(address("f")), + activation_candidate_id: + `1:${bytes32("d")}:${bytes32("e")}:10`, + activation_occurrence_id: + "42000000-0000-4000-8000-000000000005", + activation_block_number: "25650001", + activation_block_hash: bytes(bytes32("d")), + activation_block_global_log_index: "10", + }, + ]; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + + const plan = await store.readPlan(); + + expect(plan.dynamicSources).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + sourceAddress: address("f"), + contractName: "ClassicV3RewardVault", + factoryBlockNumber: "25650001", + activationBlockNumber: "25650001", + activationBlockGlobalLogIndex: "10", + }), + ]), + ); + expect(plan.provisionalSourceAddresses).toEqual([address("f")]); + }); + + it("retains a future lineage across a cursor advance but rejects it after its block", async () => { + const executor = new StoreExecutor(); + executor.provisionalRows = [ + { + provisional_page_id: "42000000-0000-4000-8000-000000000001", + provisional_lineage_id: "42000000-0000-4000-8000-000000000002", + release_epoch_id: "70000000-0000-4000-8000-000000000010", + release_pointer_generation: "1", + ingestion_epoch_id: "70000000-0000-4000-8000-000000000002", + ingestion_pointer_generation: "1", + reorg_generation: "0", + snapshot_block_number: "25650001", + snapshot_block_hash: bytes(bytes32("d")), + expected_cursor_generation: "6", + expected_cursor_block_hash: bytes(bytes32("7")), + envio_provider_deployment_id: IDS[0], + rpc_provider_a_id: IDS[1], + rpc_provider_b_id: IDS[2], + provisional_coverage_commitment: bytes(bytes32("1")), + runtime_code_evidence_id: + "42000000-0000-4000-8000-000000000003", + dynamic_source_template_id: + "30000000-0000-4000-8000-000000000001", + dynamic_source_attestation_id: + "42000000-0000-4000-8000-000000000004", + deployed_source_address: bytes(address("f")), + contract_name: "ClassicV3RewardVault", + model: "classic", + release_version: "classic-v3", + factory_address: bytes( + "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + ), + factory_contract_name: "ClassicV3RewardVaultFactory", + factory_candidate_id: `1:${bytes32("d")}:${bytes32("e")}:4`, + factory_block_number: "25650001", + factory_block_hash: bytes(bytes32("d")), + factory_block_global_log_index: "4", + parent_candidate_commitment: bytes(bytes32("2")), + expected_exact_runtime_code_hash: bytes(bytes32("6")), + expected_normalized_runtime_code_hash: bytes(bytes32("e")), + expected_immutable_references_commitment: bytes(bytes32("f")), + expected_runtime_byte_length: "200", + immutable_references: [{ start: "4", length: "20" }], + staged_at: "2026-08-01T02:00:00.000Z", + }, + ]; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + + await expect(store.readPlan()).resolves.toMatchObject({ + provisionalSourceAddresses: [address("f")], + }); + + executor.cursorBlockNumber = "25650002"; + await expect(store.readPlan()).rejects.toMatchObject({ + disposition: "fatal-codec-or-caller", + }); + }); + + it("resolves pending activations across more than one candidate page", async () => { + const executor = new StoreExecutor(); + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + const plan = await store.readPlan(); + executor.queries.splice(0); + + await expect( + store.resolvePendingDynamicSourceActivations({ + expectedCursorGeneration: plan.cursor.generation, + expectedCursorBlockHash: plan.cursor.blockHash, + expectedReorgGeneration: plan.database.reorgGeneration, + candidates: Array.from({ length: 33 }, candidate), + }), + ).resolves.toEqual([]); + + expect( + executor.queries.some(({ text }) => + text.includes("resolve_pending_dynamic_source_activations_v1") + ), + ).toBe(true); + }); + + it("defers a staged activation until its parent enters the candidate window", async () => { + const executor = new StoreExecutor(); + executor.pendingActivationResolutionRows = [{ + parent_candidate_id: `1:${bytes32("a")}:${bytes32("b")}:11`, + }]; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + const plan = await store.readPlan(); + + await expect( + store.resolvePendingDynamicSourceActivations({ + expectedCursorGeneration: plan.cursor.generation, + expectedCursorBlockHash: plan.cursor.blockHash, + expectedReorgGeneration: plan.database.reorgGeneration, + candidates: [candidate()], + }), + ).resolves.toEqual([]); + }); + + it("ignores a permissionless vault deployment without a launcher event", async () => { + const executor = new StoreExecutor(); + const parentCandidateId = `1:${bytes32("d")}:${bytes32("e")}:10`; + executor.pendingActivationResolutionRows = [{ + parent_candidate_id: parentCandidateId, + source_address: bytes(address("f")), + dynamic_source_template_id: + "30000000-0000-4000-8000-000000000001", + release_epoch_id: "70000000-0000-4000-8000-000000000010", + release_pointer_generation: "1", + reorg_generation: "0", + parent_receipt_log_ordinal: "10", + }]; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + }); + const plan = await store.readPlan(); + const template = plan.dynamicSourceTemplates[0]!; + const parent: EnvioCandidate = { + ...candidate(), + candidateId: parentCandidateId, + sourceAddress: template.parentFactoryAddress, + contractName: template.parentFactoryContractName, + eventName: template.factoryEventName, + decodedPayload: { + vault: address("f"), + configurationCommitment: bytes32("9"), + }, + }; + + await expect( + store.resolvePendingDynamicSourceActivations({ + expectedCursorGeneration: plan.cursor.generation, + expectedCursorBlockHash: plan.cursor.blockHash, + expectedReorgGeneration: plan.database.reorgGeneration, + candidates: [parent], + }), + ).resolves.toEqual([]); + }); + + it("stages one exact Classic parent and runtime without advancing the ingestion cursor", async () => { + const executor = new StoreExecutor(); + const sourceAddress = address("f"); + const rawRuntimeBytes = Buffer.alloc(200, 0x60); + bytes(sourceAddress).copy(rawRuntimeBytes, 4); + const rawRuntimeCode = `0x${rawRuntimeBytes.toString("hex")}` as const; + const canonicalRuntimeEvidence = runtimeBytecodeEvidence({ + runtimeBytecode: rawRuntimeCode, + expectedByteLength: 200, + immutableReferences: [{ start: 4, length: 20 }], + }); + executor.classicNormalizedRuntimeCodeHash = + canonicalRuntimeEvidence.normalizedRuntimeCodeHash; + executor.classicImmutableReferencesCommitment = + canonicalRuntimeEvidence.immutableReferencesCommitment; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + now: () => new Date("2026-08-01T02:00:00.000Z"), + }); + const plan = await store.readPlan(); + executor.queries.splice(0); + const template = plan.dynamicSourceTemplates[0]!; + const runtimeCodeHash = keccak256(rawRuntimeCode); + const immutableValuesCommitment = keccak256( + concat([ + toBytes("programmable:data-pipeline:immutable-values:v1\0"), + encodeAbiParameters([{ type: "bytes[]" }], [[sourceAddress]]), + ]), + ); + const parent: EnvioCandidate = { + candidateId: `1:${bytes32("d")}:${bytes32("e")}:10`, + chainId: 1, + blockNumber: "25650001", + blockHash: bytes32("d"), + blockTimestamp: "1750000000", + transactionHash: bytes32("e"), + transactionIndex: 2, + blockGlobalLogIndex: 10, + sourceAddress: template.parentFactoryAddress, + contractName: template.parentFactoryContractName, + eventName: template.factoryEventName, + releaseHint: { model: "classic", releaseVersion: "classic-v3" }, + orderedTopics: [bytes32("f")], + rawData: "0x", + decodedPayload: { + vault: sourceAddress, + configurationCommitment: bytes32("9"), + }, + payloadHash: bytes32("1"), + }; + const evidence: DualRpcCandidateWindowEvidence = { + chainId: 1, + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("4")], + providerOriginCommitments: [bytes32("5"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + executionTrace: projectionExecutionTrace, + candidates: [ + { + chainId: 1, + candidateId: parent.candidateId, + sourceAddress: parent.sourceAddress, + contractName: parent.contractName, + eventName: parent.eventName, + sourceKind: "static", + model: "classic", + releaseVersion: "classic-v3", + payloadHash: parent.payloadHash, + rawLogCommitment: bytes32("2"), + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("4")], + providerOriginCommitments: [bytes32("5"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + candidateBlockNumber: parent.blockNumber, + candidateBlockHash: parent.blockHash, + candidateBlockTimestamp: parent.blockTimestamp, + transactionHash: parent.transactionHash, + transactionIndex: parent.transactionIndex, + receiptCommitment: bytes32("7"), + sourceCodeHash: bytes32("8"), + receiptLogOrdinal: 0, + }, + ], + coveredCandidateCount: 1, + coverage: { + fromBlockNumber: parent.blockNumber, + throughBlockNumber: parent.blockNumber, + throughBlockHash: parent.blockHash, + throughBlockGlobalLogIndex: String(0xffff_ffff), + filterCommitment: bytes32("a"), + providerLogCommitments: [bytes32("2"), bytes32("2")], + }, + }; + const runtimeObservation = { + chainId: 1 as const, + parentCandidateId: parent.candidateId, + sourceAddress, + deploymentBlockNumber: parent.blockNumber, + deploymentBlockHash: parent.blockHash, + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [bytes32("3"), bytes32("4")] as const, + providerOriginCommitments: [bytes32("5"), bytes32("6")] as const, + rawRuntimeCodeA: rawRuntimeCode, + rawRuntimeCodeB: rawRuntimeCode, + runtimeCodeHashA: runtimeCodeHash, + runtimeCodeHashB: runtimeCodeHash, + normalizedRuntimeCodeHashA: + template.expectedNormalizedRuntimeCodeHash, + normalizedRuntimeCodeHashB: + template.expectedNormalizedRuntimeCodeHash, + runtimeByteLengthA: "200", + runtimeByteLengthB: "200", + immutableReferences: template.immutableReferences, + immutableReferencesCommitment: + template.expectedImmutableReferencesCommitment, + immutableValues: [sourceAddress], + immutableValuesCommitment, + reconstructedRuntimeCode: rawRuntimeCode, + reconstructedRuntimeCodeHash: runtimeCodeHash, + factoryConfigurationCommitment: bytes32("9"), + deferredAllocationEvidenceCommitment: null, + template, + startedAtMs: 1, + completedAtMs: 2, + elapsedMs: 1, + hardDeadlineMs: 75_000, + providerCallCounts: [1, 1] as const, + }; + const stageInput = { + plan, + snapshotBlock: parent.blockNumber, + candidates: [parent], + evidence, + runtimeObservations: [runtimeObservation], + blockComplete: false, + } as const; + + await expect( + store.stageVerifiedDynamicParents({ + ...stageInput, + runtimeObservations: [{ + ...runtimeObservation, + immutableValuesCommitment: bytes32("8"), + }], + }), + ).rejects.toMatchObject({ disposition: "fatal-codec-or-caller" }); + expect( + executor.queries.some(({ text }) => text.includes("open_run")), + ).toBe(false); + + await expect( + store.stageVerifiedDynamicParents(stageInput), + ).resolves.toBeUndefined(); + + const statements = executor.queries.map(({ text }) => text); + expect(statements.some((text) => + text.includes("append_dual_rpc_runtime_code_evidence"), + )).toBe(true); + const runtimeWrite = executor.queries.find(({ text }) => + text.includes("append_dual_rpc_runtime_code_evidence"), + ); + expect(runtimeWrite?.values).toHaveLength(24); + expect(Buffer.from(runtimeWrite?.values[2] as Uint8Array)).toEqual( + bytes(sourceAddress), + ); + expect(runtimeWrite?.values[4]).toBe(IDS[1]); + expect(runtimeWrite?.values[5]).toBe(IDS[2]); + expect(runtimeWrite?.values[10]).toBe("200"); + expect(runtimeWrite?.values[11]).toBe("200"); + expect( + (runtimeWrite?.values[15] as readonly Uint8Array[]).map((value) => + Buffer.from(value) + ), + ).toEqual([bytes(sourceAddress)]); + expect(Buffer.from(runtimeWrite?.values[16] as Uint8Array)).toEqual( + bytes(immutableValuesCommitment), + ); + expect(runtimeWrite?.values[22]).toEqual(runtimeWrite?.values[21]); + const stage = executor.queries.find(({ text }) => + text.includes("stage_verified_dynamic_parents_v2"), + ); + expect(stage?.values[22]).toMatchObject({ + kind: "programmable-postgres-json-v1", + }); + expect(stage?.values[24]).toMatchObject({ + kind: "programmable-postgres-json-v1", + }); + expect(stage?.values[25]).toMatchObject({ + kind: "programmable-postgres-json-v1", + }); + expect(stage?.values).toHaveLength(27); + expect(stage?.values.slice(2, 10)).toEqual([ + "classic-v3", + "classic", + "core", + "projector-v1", + "70000000-0000-4000-8000-000000000010", + "1", + "0", + "7", + ]); + expect(stage?.values[11]).toBe(IDS[0]); + expect(stage?.values[12]).toBe("canonical-events"); + expect(stage?.values.slice(13, 15)).toEqual([IDS[1], IDS[2]]); + expect(stage?.values[17]).toBe(parent.blockNumber); + expect( + (stage?.values[20] as readonly Uint8Array[]).map((value) => + Buffer.from(value) + ), + ).toEqual([bytes(bytes32("2"))]); + expect(stage?.values[21]).toEqual(stage?.values[20]); + expect(statements.some((text) => + text.includes("commit_envio_ingestion_page_v1"), + )).toBe(false); + expect(statements.at(-1)).toContain("append_run_outcome"); + }); + + it("opens evidence and commits one verified page in a single final transaction", async () => { + const executor = new StoreExecutor(); + executor.reusedSafeHeadObservationId = + "49000000-0000-4000-8000-000000000001"; + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + uuid: (() => { + let suffix = 1; + return () => + `50000000-0000-4000-8000-${String(suffix++).padStart(12, "0")}`; + })(), + now: () => new Date("2026-07-31T18:00:00.000Z"), + }); + const plan = { + cursor: { + generation: "7", + blockNumber: "25650000", + blockHash: bytes32("7"), + blockGlobalLogIndex: 9, + candidateId: `1:${bytes32("7")}:${bytes32("8")}:9`, + isBlockBoundary: false, + }, + dynamicSources: [], + provisionalSourceAddresses: [], + dynamicSourceTemplates: [], + database: { + epochId: "70000000-0000-4000-8000-000000000002", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: IDS[0], + rpcProviderDeploymentIds: [IDS[1], IDS[2]] as const, + }, + }; + const first = candidate(); + const second: EnvioCandidate = { + ...candidate(), + candidateId: `1:${bytes32("c")}:${bytes32("b")}:11`, + blockNumber: "25650002", + blockHash: bytes32("c"), + blockTimestamp: "1750000012", + transactionHash: bytes32("b"), + transactionIndex: 0, + blockGlobalLogIndex: 11, + payloadHash: bytes32("2"), + }; + const firstRawLogCommitment = bytes32("2"); + const secondRawLogCommitment = bytes32("1"); + const evidence: DualRpcCandidateWindowEvidence = { + chainId: 1, + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("4")], + providerOriginCommitments: [bytes32("5"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + executionTrace: executionTrace(2), + candidates: [ + { + chainId: 1, + candidateId: first.candidateId, + sourceAddress: first.sourceAddress, + contractName: first.contractName, + eventName: first.eventName, + sourceKind: "static", + model: "classic", + releaseVersion: "classic-v3", + payloadHash: first.payloadHash, + rawLogCommitment: firstRawLogCommitment, + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("4")], + providerOriginCommitments: [bytes32("5"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + candidateBlockNumber: first.blockNumber, + candidateBlockHash: first.blockHash, + candidateBlockTimestamp: first.blockTimestamp, + transactionHash: first.transactionHash, + transactionIndex: first.transactionIndex, + receiptCommitment: bytes32("7"), + sourceCodeHash: bytes32("8"), + receiptLogOrdinal: 0, + }, + { + chainId: 1, + candidateId: second.candidateId, + sourceAddress: second.sourceAddress, + contractName: second.contractName, + eventName: second.eventName, + sourceKind: "static", + model: "classic", + releaseVersion: "classic-v3", + payloadHash: second.payloadHash, + rawLogCommitment: secondRawLogCommitment, + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("4")], + providerOriginCommitments: [bytes32("5"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + candidateBlockNumber: second.blockNumber, + candidateBlockHash: second.blockHash, + candidateBlockTimestamp: second.blockTimestamp, + transactionHash: second.transactionHash, + transactionIndex: second.transactionIndex, + receiptCommitment: bytes32("6"), + sourceCodeHash: bytes32("8"), + receiptLogOrdinal: 0, + }, + ], + coveredCandidateCount: 2, + coverage: { + fromBlockNumber: plan.cursor.blockNumber, + throughBlockNumber: second.blockNumber, + throughBlockHash: second.blockHash, + throughBlockGlobalLogIndex: "11", + filterCommitment: bytes32("a"), + providerLogCommitments: [bytes32("b"), bytes32("b")], + }, + }; + + await expect( + store.commitVerifiedPage({ + plan, + snapshotBlock: second.blockNumber, + candidates: [first, second], + evidence, + blockComplete: true, + }), + ).resolves.toEqual({ generation: "8" }); + + const statements = executor.queries.map(({ text }) => text); + expect(statements.some((text) => text.includes("open_run"))).toBe(true); + expect( + statements.some((text) => + text.includes("append_or_reuse_safe_head_observation_v1") + ), + ).toBe(true); + expect(statements.some((text) => text.includes("append_or_reuse_dual_rpc_block_evidence_v1"))).toBe(true); + const blockWrites = executor.queries.filter(({ text }) => + text.includes("append_or_reuse_dual_rpc_block_evidence_v1"), + ); + expect(blockWrites.map(({ values }) => values[3])).toEqual([ + first.blockNumber, + second.blockNumber, + ]); + expect(blockWrites.every(({ values }) => + values[1] === executor.reusedSafeHeadObservationId + )).toBe(true); + expect(statements.at(-1)).toContain("commit_envio_ingestion_page_v1"); + const commit = executor.queries.at(-1)!; + expect(commit.values[9]).toBe(executor.reusedSafeHeadObservationId); + expect( + commit.values.some( + (value) => + Array.isArray(value) && + value.some( + (item) => + item instanceof Uint8Array && + `0x${Buffer.from(item).toString("hex")}` === + firstRawLogCommitment, + ), + ), + ).toBe(true); + expect(JSON.stringify(commit.values)).not.toContain("undefined"); + }); + + it("commits a verified empty page without fabricating an Envio candidate", async () => { + const executor = new StoreExecutor(); + const store = createPostgresProjectorStore({ + executor, + providers: PROVIDERS, + releaseScopes: RELEASE_SCOPES, + runtimeFence: RUNTIME_FENCE, + uuid: (() => { + let suffix = 1; + return () => + `60000000-0000-0000-0000-${String(suffix++).padStart(12, "0")}`; + })(), + now: () => new Date("2026-07-31T18:01:00.000Z"), + }); + const plan = { + cursor: { + generation: "7", + blockNumber: "25650000", + blockHash: bytes32("7"), + blockGlobalLogIndex: 9, + candidateId: `1:${bytes32("7")}:${bytes32("8")}:9`, + isBlockBoundary: false, + }, + dynamicSources: [], + provisionalSourceAddresses: [], + dynamicSourceTemplates: [], + database: { + epochId: "70000000-0000-0000-0000-000000000002", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: IDS[0], + rpcProviderDeploymentIds: [IDS[1], IDS[2]] as const, + }, + }; + const terminalHash = bytes32("c"); + const emptyCommitment = bytes32("b"); + await expect( + store.commitVerifiedPage({ + plan, + snapshotBlock: "25650002", + candidates: [], + blockComplete: true, + evidence: { + chainId: 1, + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("4")], + providerOriginCommitments: [bytes32("5"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + candidates: [], + executionTrace: executionTrace(), + coveredCandidateCount: 0, + coverage: { + fromBlockNumber: "25650000", + throughBlockNumber: "25650002", + throughBlockHash: terminalHash, + throughBlockGlobalLogIndex: "4294967295", + filterCommitment: bytes32("a"), + providerLogCommitments: [emptyCommitment, emptyCommitment], + }, + }, + }), + ).resolves.toEqual({ generation: "8" }); + const commit = executor.queries.at(-1)!; + expect(commit.text).toContain("commit_envio_ingestion_page_v1"); + expect(commit.values[8]).toEqual({ + kind: "programmable-postgres-json-v1", + value: [], + }); + expect(commit.values[14]).toEqual([]); + expect(commit.values[15]).toEqual([]); + }); +}); + +class ReleaseProjectionExecutor implements PostgresExecutor { + readonly queries: QueryRecord[] = []; + readonly close = vi.fn(async () => undefined); + leaseGeneration = "0"; + assertRuntimeFence = true; + decisionId: string | null = null; + readonly decisionIds = new Map(); + candidateRows: readonly Record[] | null = null; + checkpointRow: Record | null = null; + manifestRow: Record | null = null; + dynamicRows: readonly Record[] | null = null; + poolBaselineRow: Record | null = null; + rewardStateActiveRows: readonly Record[] | null = null; + rewardStateBalanceRows: readonly Record[] | null = null; + ingestionCursorRow: Record | null = null; + readonly candidateId = `1:${bytes32("d")}:${bytes32("e")}:10`; + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if (text === "select session_user::text as session_user") { + return [{ session_user: "programmable_projector_login" }] as unknown as Row[]; + } + if (text.includes("current_role::text")) { + return [{ + session_user: "programmable_projector_login", + current_role: "programmable_projector", + }] as unknown as Row[]; + } + if (text.includes("assert_projector_runtime_lease_v1")) { + return [{ asserted: this.assertRuntimeFence }] as unknown as Row[]; + } + if (text.includes("get_projector_runtime_state_v1")) { + return [{ + epoch_id: "70000000-0000-4000-8000-000000000020", + pointer_generation: "1", + provider_deployment_ids: IDS, + provider_types: PROVIDERS.map(({ type }) => type), + provider_redacted_identities: PROVIDERS.map( + ({ redactedIdentity }) => redactedIdentity, + ), + lease_generation: this.leaseGeneration, + lease_holder_id: null, + lease_acquired_at: null, + lease_expires_at: null, + checkpoint_id: this.checkpointRow?.checkpoint_id ?? null, + checkpoint_generation: + this.checkpointRow?.checkpoint_generation ?? "0", + reorg_generation: "0", + checkpoint_block_number: + this.checkpointRow?.checkpoint_block_number ?? null, + checkpoint_block_hash: + this.checkpointRow?.checkpoint_block_hash ?? null, + checkpoint_cursor_block_global_log_index: + this.checkpointRow?.checkpoint_cursor_block_global_log_index ?? + null, + checkpoint_cursor_candidate_id: + this.checkpointRow?.checkpoint_cursor_candidate_id ?? null, + }] as unknown as Row[]; + } + if (text.includes("get_envio_ingestion_cursor_v1")) { + if (this.ingestionCursorRow) { + return [this.ingestionCursorRow] as unknown as Row[]; + } + const terminal = this.candidateRows?.at(-1); + return [{ + generation: "8", + block_number: terminal?.block_number ?? "25650001", + block_hash: terminal?.block_hash ?? bytes(bytes32("d")), + block_global_log_index: null, + candidate_id: null, + }] as unknown as Row[]; + } + if (text.includes("acquire_projector_lease")) { + this.leaseGeneration = "1"; + return [{ acquired: true }] as unknown as Row[]; + } + if (text.includes("get_projector_release_manifest_v1")) { + if (this.manifestRow) { + return [this.manifestRow] as unknown as Row[]; + } + return [{ + epoch_id: "70000000-0000-4000-8000-000000000020", + pointer_generation: "1", + epoch_commitment: bytes(bytes32("1")), + artifact_creation_code_commitment: bytes(bytes32("2")), + source_bindings: [], + dynamic_source_templates: [], + projection_event_rules: [], + launch_completeness_requirements: [], + }] as unknown as Row[]; + } + if (text.includes("get_projector_dynamic_source_attestations_v1")) { + return (this.dynamicRows ?? []) as unknown as Row[]; + } + if (text.includes("list_projector_candidate_page_v1")) { + if (this.candidateRows) { + const afterCandidateId = values[11]; + const limit = Number(values[12]); + const afterIndex = afterCandidateId === null + ? -1 + : this.candidateRows.findIndex( + (row) => row.candidate_id === afterCandidateId, + ); + return this.candidateRows.slice( + afterIndex + 1, + afterIndex + 1 + limit, + ) as unknown as Row[]; + } + return [{ + candidate_id: this.candidateId, + provider_deployment_id: IDS[0], + block_number: "25650001", + block_hash: bytes(bytes32("d")), + transaction_hash: bytes(bytes32("e")), + transaction_index: "2", + block_global_log_index: "10", + source_address: bytes(address("1")), + event_signature: bytes(bytes32("f")), + event_type: "UnknownEvent", + ordered_topics: [bytes(bytes32("f"))], + raw_data: Buffer.alloc(0), + decoded_payload: {}, + payload_hash: bytes(bytes32("1")), + content_commitment: bytes(bytes32("2")), + contract_name: "UnknownContract", + status: "pending", + attempt_count: "0", + }] as unknown as Row[]; + } + if (text.includes("open_run")) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("append_or_reuse_safe_head_observation_v1")) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("append_or_reuse_dual_rpc_block_evidence_v1")) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("append_projection_provider_execution_evidence_v1")) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("get_projector_pool_baseline_by_id_v1")) { + return (this.poolBaselineRow ? [this.poolBaselineRow] : []) as unknown as Row[]; + } + if (text.includes("get_projector_reward_state_by_vault_v1")) { + return (this.rewardStateActiveRows ?? []) as unknown as Row[]; + } + if (text.includes("get_projector_reward_balances_by_vault_v1")) { + return (this.rewardStateBalanceRows ?? []) as unknown as Row[]; + } + if (text.includes("resolve_envio_candidate")) { + this.decisionIds.set(String(values[2]), String(values[0])); + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("append_chain_event_occurrence")) { + return [{ id: values[1] }] as unknown as Row[]; + } + if ( + text.includes("append_creator_fee_checkpoint_fact") || + text.includes("stage_current_reward_snapshot_v2") || + text.includes("append_reward_snapshot_provider_evidence_v1") + ) { + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("get_staged_reward_folded_commitment_v1")) { + return [{ commitment: bytes(bytes32("c")) }] as unknown as Row[]; + } + if (text.includes("ignore_envio_candidate_v1")) { + this.decisionId = String(values[0]); + this.decisionIds.set(String(values[2]), String(values[0])); + return [{ id: values[0] }] as unknown as Row[]; + } + if (text.includes("list_projector_candidate_dispositions_v1")) { + if (this.candidateRows) { + const afterCandidateId = values[11]; + const limit = Number(values[12]); + const afterIndex = afterCandidateId === null + ? -1 + : this.candidateRows.findIndex( + (row) => row.candidate_id === afterCandidateId, + ); + return this.candidateRows + .slice(afterIndex + 1, afterIndex + 1 + limit) + .map((row) => ({ + candidate_id: row.candidate_id, + block_number: row.block_number, + block_hash: row.block_hash, + transaction_hash: row.transaction_hash, + transaction_index: row.transaction_index, + block_global_log_index: row.block_global_log_index, + status: "ignored", + attempt_count: "0", + decision_id: this.decisionIds.get(String(row.candidate_id)), + reason_code: "outside-release-manifest", + reason_commitment: bytes(bytes32("3")), + changed_at: "2026-07-31T18:00:00.000Z", + })) as unknown as Row[]; + } + return [{ + candidate_id: this.candidateId, + block_number: "25650001", + block_hash: bytes(bytes32("d")), + transaction_hash: bytes(bytes32("e")), + transaction_index: "2", + block_global_log_index: "10", + status: "ignored", + attempt_count: "0", + decision_id: this.decisionId, + reason_code: "outside-release-manifest", + reason_commitment: bytes(bytes32("3")), + changed_at: "2026-07-31T18:00:00.000Z", + }] as unknown as Row[]; + } + if (text.includes("promote_projection_run_v3")) { + return [{ id: values[1] }] as unknown as Row[]; + } + if (text.includes("promote_projection_run")) { + return [{ id: values[0] }] as unknown as Row[]; + } + return [] as unknown as Row[]; + }, + }); + } +} + +describe("release-scoped projector Postgres commit", () => { + it("completes a transaction larger than 32 rows and advances to the following transaction", async () => { + const executor = new ReleaseProjectionExecutor(); + const blockHash = bytes32("d"); + const firstTransactionHash = bytes32("e"); + const candidateRow = ( + index: number, + transactionHash: `0x${string}`, + transactionIndex: number, + ) => ({ + candidate_id: `1:${blockHash}:${transactionHash}:${index}`, + provider_deployment_id: IDS[0], + block_number: "25650001", + block_hash: bytes(blockHash), + transaction_hash: bytes(transactionHash), + transaction_index: String(transactionIndex), + block_global_log_index: String(index), + source_address: bytes(address("1")), + event_signature: bytes(bytes32("f")), + event_type: "UnknownEvent", + ordered_topics: [bytes(bytes32("f"))], + raw_data: Buffer.alloc(0), + decoded_payload: {}, + payload_hash: bytes(bytes32("1")), + content_commitment: bytes(bytes32("2")), + contract_name: "UnknownContract", + status: "pending", + attempt_count: "0", + }); + executor.candidateRows = [ + ...Array.from({ length: 40 }, (_, index) => + candidateRow(index, firstTransactionHash, 1) + ), + candidateRow(40, bytes32("a"), 2), + candidateRow(41, bytes32("b"), 3), + ]; + let sequence = 1; + const store = createPostgresReleaseProjectionStore({ + executor, + providers: PROVIDERS, + rpcEvidenceBindings: RPC_EVIDENCE_BINDINGS, + scope: { + releaseId: "classic-v2", + modelId: "classic", + sourceGroup: "core", + }, + runtimeFence: RUNTIME_FENCE, + uuid: () => + `81000000-0000-4000-8000-${String(sequence++).padStart(12, "0")}`, + now: () => new Date("2026-07-31T18:00:00.000Z"), + }); + + const oversized = await store.readProjectionPlan(); + expect(oversized).toMatchObject({ batchKind: "oversized-transaction" }); + expect(oversized?.entries).toHaveLength(40); + expect( + new Set(oversized?.entries.map(({ candidate }) => + candidate.transactionHash + )), + ).toEqual(new Set([firstTransactionHash])); + + const terminal = oversized!.entries.at(-1)!.candidate; + executor.checkpointRow = { + checkpoint_id: "82000000-0000-4000-8000-000000000001", + checkpoint_generation: "1", + checkpoint_block_number: terminal.blockNumber, + checkpoint_block_hash: bytes(terminal.blockHash), + checkpoint_cursor_block_global_log_index: + String(terminal.blockGlobalLogIndex), + checkpoint_cursor_candidate_id: terminal.candidateId, + }; + const following = await store.readProjectionPlan(); + expect(following).toMatchObject({ batchKind: "normal" }); + expect(following?.entries.map(({ candidate }) => + candidate.blockGlobalLogIndex + )).toEqual([40, 41]); + }); + + it.each([500, 501, 4_096])( + "pages exact dispositions for a %i-candidate atomic transaction", + async (candidateCount) => { + const executor = new ReleaseProjectionExecutor(); + const blockHash = bytes32("d"); + const transactionHash = bytes32("e"); + executor.candidateRows = Array.from( + { length: candidateCount }, + (_value, index) => ({ + candidate_id: `1:${blockHash}:${transactionHash}:${index}`, + provider_deployment_id: IDS[0], + block_number: "25650001", + block_hash: bytes(blockHash), + transaction_hash: bytes(transactionHash), + transaction_index: "2", + block_global_log_index: String(index), + source_address: bytes(address("1")), + event_signature: bytes(bytes32("f")), + event_type: "UnknownEvent", + ordered_topics: [bytes(bytes32("f"))], + raw_data: Buffer.alloc(0), + decoded_payload: {}, + payload_hash: bytes(bytes32("1")), + content_commitment: bytes(bytes32("2")), + contract_name: "UnknownContract", + status: "pending", + attempt_count: "0", + }), + ); + let sequence = 1; + const store = createPostgresReleaseProjectionStore({ + executor, + providers: PROVIDERS, + rpcEvidenceBindings: RPC_EVIDENCE_BINDINGS, + scope: { + releaseId: "classic-v2", + modelId: "classic", + sourceGroup: "core", + }, + runtimeFence: RUNTIME_FENCE, + uuid: () => + `83000000-0000-4000-8000-${String(sequence++).padStart(12, "0")}`, + now: () => new Date("2026-07-31T18:00:00.000Z"), + }); + const plan = await store.readProjectionPlan(); + expect(plan).toMatchObject({ batchKind: "oversized-transaction" }); + expect(plan?.entries).toHaveLength(candidateCount); + const freshCandidates = plan!.entries.map(({ candidate }) => ({ + ...candidate, + blockTimestamp: "1750000000", + releaseHint: { model: "unresolved" as const, releaseVersion: "unresolved" }, + })); + const evidence = { + chainId: 1 as const, + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [bytes32("3"), bytes32("5")] as const, + providerOriginCommitments: [bytes32("4"), bytes32("6")] as const, + providerHeads: ["25650020", "25650021"] as const, + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + executionTrace: { + ...projectionExecutionTrace, + candidateBatchSize: candidateCount, + }, + candidates: freshCandidates.map((candidate) => ({ + chainId: 1 as const, + candidateId: candidate.candidateId, + sourceAddress: candidate.sourceAddress, + contractName: candidate.contractName, + eventName: candidate.eventName, + sourceKind: "static" as const, + model: "unresolved" as const, + releaseVersion: "unresolved", + payloadHash: candidate.payloadHash, + rawLogCommitment: bytes32("2"), + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [bytes32("3"), bytes32("5")] as const, + providerOriginCommitments: [bytes32("4"), bytes32("6")] as const, + providerHeads: ["25650020", "25650021"] as const, + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + candidateBlockNumber: candidate.blockNumber, + candidateBlockHash: candidate.blockHash, + candidateBlockTimestamp: candidate.blockTimestamp, + transactionHash: candidate.transactionHash, + transactionIndex: candidate.transactionIndex, + receiptCommitment: bytes32("7"), + sourceCodeHash: bytes32("8"), + receiptLogOrdinal: 0, + })), + }; + const result = await store.commitVerifiedProjection({ + plan: plan!, + freshCandidates, + ignoredCandidateIds: freshCandidates.map(({ candidateId }) => + candidateId + ), + evidence, + fold: { occurrences: [], facts: [], launches: [], knownPools: [] }, + rewardSnapshot: null, + }); + + expect(result).toEqual({ checkpointGeneration: "1" }); + const dispositionQueries = executor.queries.filter(({ text }) => + text.includes("list_projector_candidate_dispositions_v1") + ); + expect(dispositionQueries).toHaveLength(Math.ceil(candidateCount / 500)); + expect(dispositionQueries.at(-1)?.values[11]).toBe( + candidateCount <= 500 + ? null + : executor.candidateRows[ + Math.floor((candidateCount - 1) / 500) * 500 - 1 + ]?.candidate_id, + ); + }, + 30_000, + ); + + it("atomically checkpoints an irrelevant candidate as ignored", async () => { + const executor = new ReleaseProjectionExecutor(); + let sequence = 1; + const store = createPostgresReleaseProjectionStore({ + executor, + providers: PROVIDERS, + rpcEvidenceBindings: RPC_EVIDENCE_BINDINGS, + scope: { + releaseId: "classic-v2", + modelId: "classic", + sourceGroup: "core", + }, + runtimeFence: RUNTIME_FENCE, + uuid: () => + `80000000-0000-4000-8000-${String(sequence++).padStart(12, "0")}`, + now: () => new Date("2026-07-31T18:00:00.000Z"), + }); + const plan = await store.readProjectionPlan(); + expect(plan).not.toBeNull(); + expect(plan?.entries[0]?.action).toBe("ignore"); + const item = plan!.entries[0]!.candidate; + const projection = { + plan: plan!, + freshCandidates: [{ + ...item, + blockTimestamp: "1750000000", + releaseHint: { model: "unresolved", releaseVersion: "unresolved" }, + }], + ignoredCandidateIds: [item.candidateId], + evidence: { + chainId: 1, + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("5")], + providerOriginCommitments: [bytes32("4"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + executionTrace: projectionExecutionTrace, + candidates: [{ + chainId: 1, + candidateId: item.candidateId, + sourceAddress: item.sourceAddress, + contractName: item.contractName, + eventName: item.eventName, + sourceKind: "static", + model: "unresolved", + releaseVersion: "unresolved", + payloadHash: item.payloadHash, + rawLogCommitment: bytes32("2"), + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [bytes32("3"), bytes32("5")], + providerOriginCommitments: [bytes32("4"), bytes32("6")], + providerHeads: ["25650020", "25650021"], + safeBlockNumber: "25650008", + safeBlockHash: bytes32("9"), + candidateBlockNumber: item.blockNumber, + candidateBlockHash: item.blockHash, + candidateBlockTimestamp: "1750000000", + transactionHash: item.transactionHash, + transactionIndex: item.transactionIndex, + receiptCommitment: bytes32("7"), + sourceCodeHash: bytes32("8"), + receiptLogOrdinal: 0, + }], + }, + fold: { occurrences: [], facts: [], launches: [], knownPools: [] }, + rewardSnapshot: null, + } as const; + + for (const forgedEvidence of [ + { + ...projection.evidence, + providerEndpointCommitments: [bytes32("0"), bytes32("5")], + }, + { + ...projection.evidence, + executionTrace: { + ...projection.evidence.executionTrace, + calls: projection.evidence.executionTrace.calls.map((call, index) => + index === 0 + ? { ...call, providerIdentity: "substituted-provider" } + : call + ), + }, + }, + { + ...projection.evidence, + executionTrace: { + ...projection.evidence.executionTrace, + candidateBatchSize: 0, + }, + }, + ]) { + const queryStart = executor.queries.length; + await expect( + store.commitVerifiedProjection({ + ...projection, + evidence: forgedEvidence, + } as never), + ).rejects.toThrow(); + expect( + executor.queries + .slice(queryStart) + .some(({ text }) => text.includes("open_run")), + ).toBe(false); + } + + executor.assertRuntimeFence = false; + const staleQueryStart = executor.queries.length; + await expect(store.commitVerifiedProjection(projection)).rejects.toThrow(); + const staleStatements = executor.queries + .slice(staleQueryStart) + .map(({ text }) => text); + expect( + staleStatements.some((text) => text.includes("promote_projection_run")), + ).toBe(false); + expect(staleStatements.some((text) => text.includes("open_run"))).toBe(false); + + executor.assertRuntimeFence = true; + const result = await store.commitVerifiedProjection(projection); + + expect(result).toEqual({ checkpointGeneration: "1" }); + const statements = executor.queries.map(({ text }) => text); + expect(statements.some((text) => text.includes("ignore_envio_candidate_v1"))).toBe(true); + expect(statements.at(-1)).toContain("promote_projection_run"); + const promotion = executor.queries.at(-1)!; + expect(promotion.values[21]).toEqual([ + "classic-v3-profile", + "creator-profile", + "explore-chart", + "explore-list", + "explore-token", + "launch-lookup", + ]); + }); + + it("does not treat an empty candidate fetch as a complete block", async () => { + const executor = new ReleaseProjectionExecutor(); + const blockHash = bytes32("d"); + const transactionHash = bytes32("e"); + executor.candidateRows = Array.from({ length: 33 }, (_value, index) => ({ + candidate_id: `1:${blockHash}:${transactionHash}:${index}`, + provider_deployment_id: IDS[0], + block_number: "25650001", + block_hash: bytes(blockHash), + transaction_hash: bytes(transactionHash), + transaction_index: "1", + block_global_log_index: String(index), + source_address: bytes(address("1")), + event_signature: bytes(bytes32("f")), + event_type: "UnknownEvent", + ordered_topics: [bytes(bytes32("f"))], + raw_data: Buffer.alloc(0), + decoded_payload: {}, + payload_hash: bytes(bytes32("1")), + content_commitment: bytes(bytes32("2")), + contract_name: "UnknownContract", + status: "pending", + attempt_count: "0", + })); + executor.ingestionCursorRow = { + generation: "8", + block_number: "25650001", + block_hash: bytes(blockHash), + block_global_log_index: "32", + candidate_id: `1:${blockHash}:${transactionHash}:32`, + }; + let sequence = 1; + const store = createPostgresReleaseProjectionStore({ + executor, + providers: PROVIDERS, + rpcEvidenceBindings: RPC_EVIDENCE_BINDINGS, + scope: { + releaseId: "classic-v2", + modelId: "classic", + sourceGroup: "core", + }, + runtimeFence: RUNTIME_FENCE, + uuid: () => + `84000000-0000-4000-8000-${String(sequence++).padStart(12, "0")}`, + now: () => new Date("2026-07-31T18:00:00.000Z"), + }); + + await expect(store.readProjectionPlan()).resolves.toBeNull(); + expect( + executor.queries.some(({ text }) => text.includes("open_run")), + ).toBe(false); + }); + + it("persists a 49-account reward snapshot through the exact v2/v3 contracts", async () => { + const executor = new ReleaseProjectionExecutor(); + const rewardVault = address("a"); + const poolId = bytes32("6"); + const configurationHash = bytes32("4"); + const blockHash = bytes32("d"); + const transactionHashes = [bytes32("e"), bytes32("f")] as const; + const eventSignature = bytes32("9"); + const candidateRows = transactionHashes.map((transactionHash, index) => ({ + candidate_id: `1:${blockHash}:${transactionHash}:${10 + index}`, + provider_deployment_id: IDS[0], + block_number: "25650001", + block_hash: bytes(blockHash), + transaction_hash: bytes(transactionHash), + transaction_index: String(index + 1), + block_global_log_index: String(index + 10), + source_address: bytes(rewardVault), + event_signature: bytes(eventSignature), + event_type: "CreatorFeesCheckpointed", + ordered_topics: [bytes(eventSignature)], + raw_data: Buffer.alloc(0), + decoded_payload: { + poolId, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: String((index + 1) * 10), + }, + payload_hash: bytes(bytes32(index === 0 ? "1" : "2")), + content_commitment: bytes(bytes32(index === 0 ? "3" : "4")), + contract_name: "ClassicV3RewardVault", + status: "pending", + attempt_count: "0", + })); + executor.candidateRows = candidateRows; + executor.manifestRow = { + epoch_id: "70000000-0000-4000-8000-000000000020", + pointer_generation: "1", + epoch_commitment: bytes(bytes32("1")), + artifact_creation_code_commitment: bytes(bytes32("2")), + source_bindings: [{ + binding_id: "20000000-0000-4000-8000-000000000001", + source_name: "ClassicV3RewardVaultFactory", + source_role: "vault_factory", + source_type: "ethereum_contract", + source_address: "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + inclusive_start_block: "25640000", + abi_event_set_commitment: bytes32("b"), + binding_commitment: bytes32("c"), + }], + dynamic_source_templates: [{ + dynamic_source_template_id: + "30000000-0000-4000-8000-000000000001", + parent_factory_release_binding_id: + "20000000-0000-4000-8000-000000000001", + parent_factory_binding_commitment: bytes32("c"), + parent_source_role: "vault_factory", + factory_event_type: "ClassicRewardVaultDeployed", + deployed_address_field: "vault", + deployed_source_role: "reward_vault", + deployed_artifact_creation_code_commitment: bytes32("d"), + normalized_runtime_code_hash: bytes32("e"), + expected_instance_runtime_code_hash: null, + immutable_references_commitment: bytes32("f"), + immutable_binding_spec: { + factoryConfigurationField: "configurationCommitment", + bindings: [{ + ordinal: "0", + offset: "4", + length: "20", + source: "deployed_address", + encoding: "address", + }], + }, + immutable_binding_commitment: bytes32("1"), + runtime_code_length: "200", + abi_event_set_commitment: bytes32("2"), + template_commitment: bytes32("3"), + }], + projection_event_rules: [{ + projection_event_rule_id: + "30000000-0000-4000-8000-000000000002", + projection_kind: "creator-fee-checkpoint", + source_role: "reward_vault", + event_type: "CreatorFeesCheckpointed", + rule_commitment: bytes32("4"), + }], + launch_completeness_requirements: [], + }; + executor.dynamicRows = [{ + dynamic_source_attestation_id: + "40000000-0000-4000-8000-000000000001", + dynamic_source_template_id: + "30000000-0000-4000-8000-000000000001", + runtime_code_evidence_id: + "40000000-0000-4000-8000-000000000002", + deployed_source_address: bytes(rewardVault), + deployed_source_role: "reward_vault", + deployment_block_number: "25645000", + runtime_code_hash: bytes(bytes32("5")), + normalized_runtime_code_hash: bytes(bytes32("e")), + expected_instance_runtime_code_hash: null, + runtime_code_length: "200", + immutable_references_commitment: bytes(bytes32("f")), + immutable_binding_spec: { + bindings: [{ + ordinal: "0", + offset: "4", + length: "20", + source: "deployed_address", + encoding: "address", + }], + }, + immutable_binding_commitment: bytes(bytes32("1")), + abi_event_set_commitment: bytes(bytes32("2")), + template_commitment: bytes(bytes32("3")), + attestation_commitment: bytes(bytes32("5")), + parent_factory_occurrence_id: + "40000000-0000-4000-8000-000000000003", + parent_factory_release_binding_id: + "20000000-0000-4000-8000-000000000001", + parent_factory_binding_commitment: bytes(bytes32("c")), + dynamic_source_release_asset_binding_id: + "40000000-0000-4000-8000-000000000004", + launch_occurrence_id: "40000000-0000-4000-8000-000000000005", + pool_occurrence_id: "40000000-0000-4000-8000-000000000006", + token: bytes(address("b")), + pool_id: bytes(poolId), + hook: bytes(address("c")), + quote_asset: bytes(address("d")), + asset_binding_commitment: bytes(bytes32("7")), + }]; + executor.poolBaselineRow = { + pool_projection_id: "50000000-0000-4000-8000-000000000001", + launch_projection_id: "50000000-0000-4000-8000-000000000002", + token: bytes(address("b")), + creator: bytes(address("8")), + reward_vault: bytes(rewardVault), + currency0: bytes(address("b")), + currency1: bytes(address("0")), + pool_key_fee: "10000", + tick_spacing: "200", + hook: bytes(address("c")), + pool_fee_configuration_id: null, + buy_swap_fee_bps: null, + sell_swap_fee_bps: null, + buy_creator_fee_bps: null, + sell_creator_fee_bps: null, + launcher_fee_bps: null, + transfer_tax_bps: null, + lp_fee_pips: null, + last_source_occurrence_id: + "50000000-0000-4000-8000-000000000003", + }; + const beneficiary = `0x${"1".padStart(40, "0")}` as `0x${string}`; + const rewardHeader = { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + source_group: "core", + epoch_id: "70000000-0000-4000-8000-000000000020", + pointer_generation: "1", + checkpoint_id: "51000000-0000-4000-8000-000000000001", + projector_version: "projector-v1", + checkpoint_generation: "1", + reorg_generation: "0", + checkpoint_block_number: "25650000", + checkpoint_block_hash: bytes(bytes32("7")), + reward_vault_projection_id: + "51000000-0000-4000-8000-000000000002", + allocation_fact_id: "51000000-0000-4000-8000-000000000003", + allocation_evidence_id: "51000000-0000-4000-8000-000000000004", + vault: bytes(rewardVault), + pool_id: bytes(poolId), + quote_asset: null, + configuration_hash: bytes(configurationHash), + active_configuration_hash: bytes(configurationHash), + total_creator_fees_received: "0", + configuration_epoch: "1", + baseline_projection_run_id: + "51000000-0000-4000-8000-000000000005", + baseline_publication_commitment: bytes(bytes32("8")), + baseline_promoted_block_number: "25650000", + baseline_promoted_block_hash: bytes(bytes32("7")), + vault_source_occurrence_id: + "51000000-0000-4000-8000-000000000006", + vault_source_logical_event_id: + "51000000-0000-4000-8000-000000000007", + vault_source_block_hash: bytes(bytes32("7")), + }; + executor.rewardStateActiveRows = [{ + ...rewardHeader, + allocation_index: "0", + beneficiary: bytes(beneficiary), + payout_address: bytes(beneficiary), + share_bps: "10000", + claimable_accrued: "0", + claimed_total: "0", + balance_projection_run_id: + "51000000-0000-4000-8000-000000000008", + balance_publication_commitment: bytes(bytes32("8")), + balance_promoted_block_number: "25650000", + balance_promoted_block_hash: bytes(bytes32("7")), + allocation_source_occurrence_id: + "51000000-0000-4000-8000-000000000009", + allocation_source_logical_event_id: + "51000000-0000-4000-8000-000000000010", + allocation_source_block_hash: bytes(bytes32("7")), + balance_source_occurrence_id: + "51000000-0000-4000-8000-000000000011", + balance_source_logical_event_id: + "51000000-0000-4000-8000-000000000012", + balance_source_block_hash: bytes(bytes32("7")), + verified_at: "2026-07-31T17:00:00.000Z", + }]; + executor.rewardStateBalanceRows = [{ + ...rewardHeader, + account_reward_balance_id: + "52000000-0000-4000-8000-000000000001", + account: bytes(beneficiary), + payout_address: bytes(beneficiary), + payout_source_kind: "initial", + payout_configuration_epoch: "1", + claimable_accrued: "0", + claimed_total: "0", + balance_projection_run_id: + "52000000-0000-4000-8000-000000000002", + balance_publication_commitment: bytes(bytes32("8")), + balance_promoted_block_number: "25650000", + balance_promoted_block_hash: bytes(bytes32("7")), + payout_projection_run_id: + "52000000-0000-4000-8000-000000000003", + payout_publication_commitment: bytes(bytes32("8")), + payout_promoted_block_number: "25650000", + payout_promoted_block_hash: bytes(bytes32("7")), + payout_source_occurrence_id: + "52000000-0000-4000-8000-000000000004", + payout_source_logical_event_id: + "52000000-0000-4000-8000-000000000005", + payout_source_block_hash: bytes(bytes32("7")), + balance_source_occurrence_id: + "52000000-0000-4000-8000-000000000006", + balance_source_logical_event_id: + "52000000-0000-4000-8000-000000000007", + balance_source_block_hash: bytes(bytes32("7")), + verified_at: "2026-07-31T17:00:00.000Z", + }]; + + let sequence = 1; + const store = createPostgresReleaseProjectionStore({ + executor, + providers: PROVIDERS, + rpcEvidenceBindings: RPC_EVIDENCE_BINDINGS, + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + }, + runtimeFence: RUNTIME_FENCE, + uuid: () => + `83000000-0000-4000-8000-${String(sequence++).padStart(12, "0")}`, + now: () => new Date("2026-07-31T18:00:00.000Z"), + }); + const plan = await store.readProjectionPlan(); + expect(plan).toMatchObject({ batchKind: "reward-block" }); + expect(plan?.entries).toHaveLength(2); + + const freshCandidates = plan!.entries.map(({ candidate }, index) => ({ + ...candidate, + blockTimestamp: "1750000000", + releaseHint: { + model: "classic" as const, + releaseVersion: "classic-v3", + }, + decodedPayload: { + poolId, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: String((index + 1) * 10), + }, + })); + const occurrenceIds = freshCandidates.map((candidate) => + projectorOccurrenceUuid({ + transactionHash: candidate.transactionHash, + receiptLogOrdinal: "0", + blockHash: candidate.blockHash, + }) + ); + const baseline = { + vault: rewardVault, + poolId, + configurationEpoch: "1", + activeConfigurationHash: configurationHash, + totalCreatorFeesReceived: "0", + allocations: [{ + allocationIndex: 0, + beneficiary, + payoutAddress: beneficiary, + shareBps: "10000", + }], + balances: [{ + account: beneficiary, + payoutAddress: beneficiary, + claimableAccrued: "0", + claimedTotal: "0", + }], + } as const; + const rewardEvents = occurrenceIds.map((occurrenceId, index) => ({ + occurrenceId, + vault: rewardVault, + blockNumber: "25650001", + transactionIndex: String(index + 1), + blockGlobalLogIndex: String(index + 10), + kind: "creator-fee-checkpoint" as const, + values: { + poolId, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: String((index + 1) * 10), + }, + })); + const rewardSnapshot = foldProjectorRewardState({ + model: "classic-v3", + baseline, + events: rewardEvents, + }); + const occurrences = freshCandidates.map((candidate, index) => ({ + candidateId: candidate.candidateId, + chainId: "1" as const, + releaseId: "classic-v3" as const, + modelId: "classic" as const, + sourceGroup: "core" as const, + blockNumber: candidate.blockNumber, + blockHash: candidate.blockHash, + blockTimestamp: candidate.blockTimestamp, + transactionHash: candidate.transactionHash, + transactionIndex: String(candidate.transactionIndex), + receiptLogOrdinal: "0", + blockGlobalLogIndex: String(candidate.blockGlobalLogIndex), + sourceAddress: candidate.sourceAddress, + eventSignature, + eventType: "CreatorFeesCheckpointed", + orderedTopics: candidate.orderedTopics, + rawData: candidate.rawData, + decodedPayload: rewardEvents[index]!.values, + payloadHash: candidate.payloadHash, + dynamicSourceAttestationId: + "40000000-0000-4000-8000-000000000001", + })); + const facts = freshCandidates.map((candidate, index) => ({ + sourceCandidateId: candidate.candidateId, + sourceRole: "reward_vault" as const, + kind: "creator-fee-checkpoint" as const, + procedure: "append_creator_fee_checkpoint_fact" as const, + values: rewardEvents[index]!.values, + })); + const candidateEvidence = freshCandidates.map((candidate, index) => ({ + chainId: 1 as const, + candidateId: candidate.candidateId, + sourceAddress: candidate.sourceAddress, + contractName: candidate.contractName, + eventName: candidate.eventName, + sourceKind: "dynamic-attested" as const, + model: "classic" as const, + releaseVersion: "classic-v3", + payloadHash: candidate.payloadHash, + rawLogCommitment: bytes32(index === 0 ? "a" : "b"), + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [bytes32("3"), bytes32("5")] as const, + providerOriginCommitments: [bytes32("4"), bytes32("6")] as const, + providerHeads: ["25650020", "25650021"] as const, + safeBlockNumber: "25650008", + safeBlockHash: bytes32("8"), + candidateBlockNumber: candidate.blockNumber, + candidateBlockHash: candidate.blockHash, + candidateBlockTimestamp: candidate.blockTimestamp, + transactionHash: candidate.transactionHash, + transactionIndex: candidate.transactionIndex, + receiptCommitment: bytes32(index === 0 ? "c" : "d"), + sourceCodeHash: bytes32("e"), + receiptLogOrdinal: 0, + dynamicSourceAttestationId: + "40000000-0000-4000-8000-000000000001", + normalizedRuntimeCodeHash: bytes32("e"), + immutableReferencesCommitment: bytes32("f"), + runtimeByteLength: "200", + })); + const verificationAccounts = Array.from({ length: 49 }, (_value, index) => + `0x${(index + 1).toString(16).padStart(40, "0")}` as `0x${string}` + ); + const rewardCalls = [0, 0, 1, 1].map((providerIndex) => { + const binding = RPC_EVIDENCE_BINDINGS[providerIndex]!; + return { + providerIdentity: binding.identity, + providerVendorGroup: binding.vendorGroup, + providerEndpointCommitment: binding.endpointCommitment, + providerOriginCommitment: binding.endpointOriginCommitment, + operation: "readRewardSnapshot" as const, + attempt: 1, + startedOffsetMs: 0, + durationMs: 1, + outcome: "success" as const, + }; + }); + const rewardEvidence = { + ...rewardSnapshot, + model: "classic-v3" as const, + blockNumber: "25650001", + blockHash, + configurationHash, + totalCreatorFeesClaimed: "0", + rpcCallCount: 228, + verificationAccounts, + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [bytes32("3"), bytes32("5")] as const, + providerOriginCommitments: [bytes32("4"), bytes32("6")] as const, + providerCallCounts: [114, 114] as const, + providerSnapshotCommitments: [bytes32("7"), bytes32("7")] as const, + chunks: [{ + chunkIndex: 0, + verificationAccounts: verificationAccounts.slice(0, 48), + providerCallCounts: [104, 104] as const, + providerSnapshotCommitments: [bytes32("8"), bytes32("8")] as const, + }, { + chunkIndex: 1, + verificationAccounts: verificationAccounts.slice(48), + providerCallCounts: [10, 10] as const, + providerSnapshotCommitments: [bytes32("9"), bytes32("9")] as const, + }], + executionTrace: { + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize: 0, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 128, + elapsedMs: 1, + providerCallCounts: [114, 114] as const, + calls: rewardCalls, + }, + } as const; + const projection = { + plan: plan!, + freshCandidates, + ignoredCandidateIds: [], + evidence: { + chainId: 1 as const, + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [bytes32("3"), bytes32("5")] as const, + providerOriginCommitments: [bytes32("4"), bytes32("6")] as const, + providerHeads: ["25650020", "25650021"] as const, + safeBlockNumber: "25650008", + safeBlockHash: bytes32("8"), + executionTrace: { + ...projectionExecutionTrace, + candidateBatchSize: 2, + }, + candidates: candidateEvidence, + }, + fold: { + occurrences, + facts, + launches: [], + knownPools: [], + }, + rewardSnapshot, + rewardSnapshots: [rewardSnapshot], + rewardEvidence: [rewardEvidence], + } as const; + + const malformedQueryStart = executor.queries.length; + await expect( + store.commitVerifiedProjection({ + ...projection, + rewardEvidence: [{ + ...rewardEvidence, + chunks: [ + rewardEvidence.chunks[0], + { ...rewardEvidence.chunks[1], chunkIndex: 0 }, + ], + }], + } as never), + ).rejects.toThrow(); + expect( + executor.queries + .slice(malformedQueryStart) + .some(({ text }) => text.includes("open_run")), + ).toBe(false); + + await expect(store.commitVerifiedProjection(projection)).resolves.toEqual({ + checkpointGeneration: "1", + }); + const stage = executor.queries.find(({ text }) => + text.includes("stage_current_reward_snapshot_v2") + ); + expect(stage?.values).toHaveLength(20); + expect(stage?.values[16]).toEqual(occurrenceIds); + expect(stage?.values[15]).toBe(occurrenceIds[1]); + expect( + executor.queries.some(({ text }) => + text.includes("stage_current_reward_snapshot_v1") + ), + ).toBe(false); + const appended = executor.queries.find(({ text }) => + text.includes("append_reward_snapshot_provider_evidence_v1") + ); + expect(appended?.values).toHaveLength(26); + expect(appended?.values[11]).toBe(114); + expect(appended?.values[12]).toBe(114); + expect(appended?.values[13]).toHaveLength(49); + expect(appended?.values[14]).toEqual([48, 49]); + expect(appended?.values[17]).toEqual([104, 10]); + expect(appended?.values[18]).toEqual([104, 10]); + expect( + executor.queries.some(({ text }) => + text.includes("projection_provider_binding_commitment_v1") + ), + ).toBe(false); + const promotion = executor.queries.at(-1)!; + expect(promotion.text).toContain("promote_projection_run_v3"); + expect(promotion.values).toHaveLength(28); + expect(promotion.values[0]).toBe("exact_incremental"); + expect(promotion.values[24]).toHaveLength(1); + expect(promotion.values[26]).toBeInstanceOf(Uint8Array); + expect((promotion.values[26] as Uint8Array).byteLength).toBe(32); + expect( + executor.queries.some(({ text }) => + text.includes("promote_projection_run_v2") + ), + ).toBe(false); + }); +}); diff --git a/tests/data-pipeline/postgres-projector.test.ts b/tests/data-pipeline/postgres-projector.test.ts new file mode 100644 index 00000000..79129a1f --- /dev/null +++ b/tests/data-pipeline/postgres-projector.test.ts @@ -0,0 +1,188 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; +import { + ProjectorDatabaseError, + classifyProjectorSqlState, + createProjectorDatabaseGateway, + projectionProviderBindingCommitmentV1, +} from "../../lib/data-pipeline/postgres-projector"; + +type RecordedQuery = { + text: string; + values: readonly PostgresParameter[]; +}; + +class FakeExecutor implements PostgresExecutor { + readonly queries: RecordedQuery[] = []; + readonly close = vi.fn(async () => undefined); + + constructor( + private readonly sessionUser = "programmable_projector_login", + private readonly currentRole = "programmable_projector", + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if (/select session_user::text as session_user$/iu.test(text.trim())) { + return [{ session_user: this.sessionUser }] as unknown as Row[]; + } + if (/current_role::text as current_role/iu.test(text)) { + return [ + { + session_user: this.sessionUser, + current_role: this.currentRole, + }, + ] as unknown as Row[]; + } + return [] as unknown as Row[]; + }, + }); + } +} + +describe("projector Postgres gateway", () => { + it("verifies the login identity before assuming the capability role", async () => { + const executor = new FakeExecutor(); + const gateway = createProjectorDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "ok")).resolves.toBe("ok"); + + expect(executor.queries.map(({ text }) => text)).toEqual([ + "select session_user::text as session_user", + "set local role programmable_projector", + "set local statement_timeout = '1000ms'", + "set local lock_timeout = '250ms'", + "set local idle_in_transaction_session_timeout = '2000ms'", + "select session_user::text as session_user, current_role::text as current_role", + ]); + }); + + it("fails before SET ROLE when the gateway authenticated a different login", async () => { + const executor = new FakeExecutor("postgres"); + const gateway = createProjectorDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "never")).rejects.toMatchObject({ + name: "ProjectorDatabaseError", + disposition: "fatal-gateway-membership", + retryable: false, + }); + expect(executor.queries).toHaveLength(1); + expect(executor.queries[0]!.text).toBe( + "select session_user::text as session_user", + ); + }); + + it("rejects a changed session identity or missing capability role", async () => { + const executor = new FakeExecutor( + "programmable_projector_login", + "programmable_projector_login", + ); + const gateway = createProjectorDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "never")).rejects.toMatchObject({ + disposition: "fatal-gateway-membership", + }); + }); +}); + +describe("projector SQLSTATE policy", () => { + it.each([ + ["40001", "retry-serialization", true], + ["40P01", "retry-serialization", true], + ["55P03", "transient-no-candidate-penalty", true], + ["57014", "transient-no-candidate-penalty", true], + ["08006", "transient-no-candidate-penalty", true], + ["57P01", "transient-no-candidate-penalty", true], + ["42501", "fatal-gateway-membership", false], + ["22023", "fatal-codec-or-caller", false], + ["22P02", "fatal-codec-or-caller", false], + ["22003", "fatal-codec-or-caller", false], + ["23505", "immutable-replay-conflict", false], + ["55000", "idempotence-reread", true], + ] as const)("maps %s without relying on provider messages", (sqlState, disposition, retryable) => { + expect(classifyProjectorSqlState({ sqlState, scope: "batch" })).toEqual({ + sqlState, + disposition, + retryable, + }); + }); + + it("quarantines only candidate-local check violations", () => { + expect( + classifyProjectorSqlState({ sqlState: "23514", scope: "candidate-local" }), + ).toMatchObject({ disposition: "quarantine-candidate" }); + expect( + classifyProjectorSqlState({ sqlState: "23514", scope: "batch" }), + ).toMatchObject({ disposition: "abort-batch-invariant" }); + }); + + it("defers an FK violation only for an expected dynamic parent", () => { + expect( + classifyProjectorSqlState({ sqlState: "23503", scope: "dynamic-parent" }), + ).toMatchObject({ disposition: "defer-dynamic-parent", retryable: true }); + expect( + classifyProjectorSqlState({ sqlState: "23503", scope: "batch" }), + ).toMatchObject({ disposition: "fatal-integrity", retryable: false }); + }); + + it("never exposes a database message or secret through its public error", () => { + const secret = "postgres://projector:secret@example.invalid/db"; + const error = ProjectorDatabaseError.fromUnknown( + { code: "40001", message: secret, detail: secret }, + "batch", + ); + + expect(String(error)).not.toContain(secret); + expect(JSON.stringify(error)).not.toContain(secret); + expect(error).toMatchObject({ + sqlState: "40001", + disposition: "retry-serialization", + retryable: true, + }); + }); +}); + +describe("projection provider binding commitment", () => { + it("matches PostgreSQL uuid, integer, timestamp and SHA-256 encoding", () => { + expect(projectionProviderBindingCommitmentV1({ + publicationId: "11111111-1111-4111-8111-111111111111", + runId: "22222222-2222-4222-8222-222222222222", + promotionMode: "exact_incremental", + executionEvidenceId: "33333333-3333-4333-8333-333333333333", + executionFingerprint: `0x${"11".repeat(32)}`, + rewardEvidence: [{ + evidenceId: "44444444-4444-4444-8444-444444444444", + fingerprint: `0x${"22".repeat(32)}`, + }], + boundAt: "2026-08-02T00:10:50.427Z", + })).toBe( + "0xee251437622bcfd0f66145fb4c6893a42a7bb4a572001fb409279cae2cdefd84", + ); + }); + + it("rejects malformed runtime inputs before building a commitment", () => { + expect(() => projectionProviderBindingCommitmentV1({ + publicationId: "not-a-uuid", + runId: "22222222-2222-4222-8222-222222222222", + promotionMode: "exact_incremental", + executionEvidenceId: "33333333-3333-4333-8333-333333333333", + executionFingerprint: `0x${"11".repeat(32)}`, + rewardEvidence: [], + boundAt: "2026-08-02T00:10:50.427Z", + })).toThrow(ProjectorDatabaseError); + }); +}); diff --git a/tests/data-pipeline/postgres-read-model-route-adapter.test.ts b/tests/data-pipeline/postgres-read-model-route-adapter.test.ts new file mode 100644 index 00000000..dd0b012c --- /dev/null +++ b/tests/data-pipeline/postgres-read-model-route-adapter.test.ts @@ -0,0 +1,206 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + createPostgresPublicRouteSnapshotAdapters, + type IndexedRouteSnapshotQueries, +} from "../../lib/data-pipeline/postgres-read-model.server"; +import type { PostgresTransaction } from "../../lib/data-pipeline/postgres"; +import type { + IndexedExploreListDataV2, + IndexedRouteEnvelopeV2, + IndexedSnapshotIdentityV2, + SupportedIndexedReleaseVersionV2, +} from "../../lib/data-pipeline/route-adapters.server"; + +const BLOCK_HASH = `0x${"11".repeat(32)}` as const; +const SNAPSHOT_COMMITMENT = `0x${"22".repeat(32)}` as const; +const RELEASES = [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const; + +function modelFor(release: SupportedIndexedReleaseVersionV2) { + return release.startsWith("stock-paired") + ? ("stock-paired" as const) + : ("classic" as const); +} + +function snapshot(routeKey: "explore-list" | "explore-token"): + IndexedSnapshotIdentityV2 { + return { + adapterVersion: "indexed-route-adapters-v2", + snapshotCommitment: SNAPSHOT_COMMITMENT, + chainId: 1, + blockNumber: "25660000", + blockHash: BLOCK_HASH, + confirmations: 12, + capturedAt: "2026-07-31T10:00:00.000Z", + releasePointers: RELEASES.map((release, index) => ({ + routeKey, + chainId: 1, + releaseVersion: release, + modelVersion: modelFor(release), + sourceGroup: `source-${release}`, + projectorVersion: "public-route-projector-v2", + epochId: `10000000-0000-4000-8000-00000000000${index + 1}`, + pointerGeneration: String(index + 1), + checkpointId: `20000000-0000-4000-8000-00000000000${index + 1}`, + checkpointGeneration: "9", + reorgGeneration: "0", + checkpointBlockNumber: "25660000", + checkpointBlockHash: BLOCK_HASH, + })), + }; +} + +function exploreEnvelope(): IndexedRouteEnvelopeV2 { + return { + status: "ready", + snapshot: snapshot("explore-list"), + data: { + request: { + query: "", + sort: "newest", + requestedPage: 1, + pageSize: 12, + }, + page: { + resolvedPage: 1, + totalCount: "0", + valuationUnit: null, + startAfter: null, + endAt: null, + }, + launcherFeesAccruedWei: "0", + tokens: [], + }, + }; +} + +function harness() { + const transaction: PostgresTransaction = { + async query() { + throw new Error("adapter must not issue boundary queries"); + }, + }; + const unsupported = async () => { + throw new Error("unexpected route query"); + }; + const explore = vi.fn(async () => exploreEnvelope()); + const tokenDetail = vi.fn(async () => ({ + status: "ready" as const, + snapshot: snapshot("explore-token"), + data: { + address: "0x1111111111111111111111111111111111111111", + token: null, + }, + })); + const queries = { + explore, + tokenDetail, + tokenChart: unsupported, + creatorProfile: unsupported, + classicV3Profile: unsupported, + stockPairedProfile: unsupported, + launchLookup: unsupported, + } as unknown as IndexedRouteSnapshotQueries; + return { transaction, queries, explore, tokenDetail }; +} + +describe("Postgres public route snapshot adapters", () => { + it("uses the coordinator-owned transaction and returns evidence inputs", async () => { + const test = harness(); + const adapters = createPostgresPublicRouteSnapshotAdapters({ + queries: test.queries, + }); + const request = { + chainId: 1 as const, + query: "", + sort: "newest" as const, + page: 1, + pageSize: 12, + }; + + const result = await adapters.explore(test.transaction, request); + + expect(test.explore).toHaveBeenCalledTimes(1); + expect(test.explore).toHaveBeenCalledWith(test.transaction, request); + expect(result).toEqual({ + status: "ready", + routeKey: "explore-list", + snapshot: expect.objectContaining({ + adapterVersion: "indexed-route-adapters-v2", + releasePointers: expect.arrayContaining([ + expect.objectContaining({ + projectorVersion: "public-route-projector-v2", + reorgGeneration: "0", + }), + ]), + }), + recordSources: [], + response: { + status: 200, + body: expect.objectContaining({ + status: "ready", + total: 0, + page: 1, + tokens: [], + }), + headers: { + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }, + }, + }); + }); + + it("keeps a verified empty token lookup non-cacheable", async () => { + const test = harness(); + const adapters = createPostgresPublicRouteSnapshotAdapters({ + queries: test.queries, + }); + const request = { + chainId: 1 as const, + address: "0x1111111111111111111111111111111111111111", + }; + + const result = await adapters.tokenDetail(test.transaction, request); + + expect(test.tokenDetail).toHaveBeenCalledWith(test.transaction, request); + expect(result.status).toBe("ready"); + if (result.status !== "ready") throw new Error("expected ready result"); + expect(result.response.status).toBe(404); + expect(result.response.headers).toEqual({ "Cache-Control": "no-store" }); + expect(result.recordSources).toEqual([]); + }); + + it("returns not-ready without fabricating payload or evidence", async () => { + const test = harness(); + test.explore.mockResolvedValueOnce({ + status: "not-ready", + reason: "reconciliation-incomplete", + } as never); + const adapters = createPostgresPublicRouteSnapshotAdapters({ + queries: test.queries, + }); + + await expect( + adapters.explore(test.transaction, { + chainId: 1, + query: "", + sort: "newest", + page: 1, + pageSize: 12, + }), + ).resolves.toEqual({ + status: "not-ready", + routeKey: "explore-list", + reason: "reconciliation-incomplete", + }); + }); +}); diff --git a/tests/data-pipeline/postgres-reconciler-route-corpus-store.test.ts b/tests/data-pipeline/postgres-reconciler-route-corpus-store.test.ts new file mode 100644 index 00000000..f62c5896 --- /dev/null +++ b/tests/data-pipeline/postgres-reconciler-route-corpus-store.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; +import { assembleReconcilerRoutesFromContributions } from "../../lib/data-pipeline/classic-v3-reconciler-route-contract"; +import { createPostgresReconcilerRouteCorpusStore } from "../../lib/data-pipeline/postgres-reconciler-route-corpus-store"; +import { + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + RECONCILER_ROUTE_KEYS, + type ReconcilerPreParityContract, +} from "../../lib/data-pipeline/reconciler-preparity"; +import { + classicV3ReconcilerRouteFixture, + ROUTE_FIXTURE_ADDRESS, +} from "./classic-v3-reconciler-route-fixture"; + +const HASH = `0x${"11".repeat(32)}` as const; +const contract: ReconcilerPreParityContract = { + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "ethereum-mainnet", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "11", + reorgGeneration: "0", + checkpointBlockNumber: "25700000", + checkpointBlockHash: HASH, + routeKeys: RECONCILER_ROUTE_KEYS, + routeContract: {}, + projectionContract: {}, + currentEntities: [], +}; + +class CorpusExecutor implements PostgresExecutor { + readonly applicationQueries: { + text: string; + values: readonly PostgresParameter[]; + }[] = []; + readonly close = vi.fn(async () => undefined); + + constructor( + private readonly rows: readonly Record[], + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + if (/select session_user::text as session_user$/iu.test(text.trim())) { + return [{ session_user: "programmable_reconciler_login" }] as unknown as Row[]; + } + if (/current_role::text as current_role/iu.test(text)) { + return [{ + session_user: "programmable_reconciler_login", + current_role: "programmable_reconciler", + }] as unknown as Row[]; + } + if (/^set local /iu.test(text.trim())) return [] as unknown as Row[]; + this.applicationQueries.push({ text, values }); + return this.rows as unknown as Row[]; + }, + }); + } +} + +function validRows() { + return classicV3ReconcilerRouteFixture().map((route) => ({ + route_key: route.routeKey, + compared_count: String(route.comparedCount), + dto: route.dto, + })); +} + +describe("reconciler route corpus Postgres store", () => { + it("uses only the exact bounded corpus capability", async () => { + const executor = new CorpusExecutor(validRows()); + const store = createPostgresReconcilerRouteCorpusStore({ executor }); + const result = await store.readExactIndexedRouteCorpus({ + contract, + maximumEntityCount: 321, + signal: new AbortController().signal, + }); + + expect(result.map(({ routeKey }) => routeKey)).toEqual( + RECONCILER_ROUTE_KEYS, + ); + expect(executor.applicationQueries).toHaveLength(1); + expect(executor.applicationQueries[0]!.text).toContain( + "get_reconciler_route_corpus_v1", + ); + expect(executor.applicationQueries[0]!.values[9]).toBe(321); + expect(executor.applicationQueries[0]!.text).not.toMatch( + /public_(?:explore|creator|classic|launch)|route_snapshot_readiness/iu, + ); + }); + + it("accepts the exact four-route Classic V2 corpus", async () => { + const fixture = classicV3ReconcilerRouteFixture(); + const token = structuredClone( + (fixture[0]!.dto as { tokens: Array> }).tokens[0]!, + ); + const chart = structuredClone( + (fixture[2]!.dto as { charts: Array> }).charts[0]!, + ); + token.releaseVersion = "classic-v2"; + token.rewardVaultAddress = null; + chart.releaseVersion = "classic-v2"; + const routes = assembleReconcilerRoutesFromContributions([{ + tokens: [token] as never, + charts: [chart] as never, + }]); + const v2Contract: ReconcilerPreParityContract = { + ...contract, + releaseId: "classic-v2", + routeKeys: CLASSIC_V2_RECONCILER_ROUTE_KEYS, + routeContract: { routes: [...CLASSIC_V2_RECONCILER_ROUTE_KEYS] }, + }; + const executor = new CorpusExecutor(routes.map((route) => ({ + route_key: route.routeKey, + compared_count: String(route.comparedCount), + dto: route.dto, + }))); + const store = createPostgresReconcilerRouteCorpusStore({ executor }); + + await expect(store.readExactIndexedRouteCorpus({ + contract: v2Contract, + maximumEntityCount: 10_000, + signal: new AbortController().signal, + })).resolves.toEqual(routes); + }); + + it("rejects missing, reordered and empty route rows", async () => { + for (const rows of [ + validRows().slice(0, 5), + [validRows()[1]!, validRows()[0]!, ...validRows().slice(2)], + validRows().map((row, index) => + index === 0 ? { ...row, compared_count: "0" } : row + ), + ]) { + const store = createPostgresReconcilerRouteCorpusStore({ + executor: new CorpusExecutor(rows), + }); + await expect(store.readExactIndexedRouteCorpus({ + contract, + maximumEntityCount: 10_000, + signal: new AbortController().signal, + })).rejects.toBeDefined(); + } + }); + + it("fails closed on a structurally incompatible indexed DTO", async () => { + const rows = validRows(); + const chartDto = rows[2]!.dto as Readonly>; + rows[2] = { + ...rows[2]!, + dto: { + ...chartDto, + charts: [{ tokenAddress: ROUTE_FIXTURE_ADDRESS }], + }, + }; + const store = createPostgresReconcilerRouteCorpusStore({ + executor: new CorpusExecutor(rows), + }); + + await expect(store.readExactIndexedRouteCorpus({ + contract, + maximumEntityCount: 10_000, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "validation_failed", + }); + }); + + it("does not enter a database transaction after cancellation", async () => { + const executor = new CorpusExecutor(validRows()); + const store = createPostgresReconcilerRouteCorpusStore({ executor }); + const controller = new AbortController(); + controller.abort(); + + await expect(store.readExactIndexedRouteCorpus({ + contract, + maximumEntityCount: 10_000, + signal: controller.signal, + })).rejects.toMatchObject({ code: "validation_failed" }); + expect(executor.applicationQueries).toHaveLength(0); + }); +}); diff --git a/tests/data-pipeline/postgres-reconciler-store.test.ts b/tests/data-pipeline/postgres-reconciler-store.test.ts new file mode 100644 index 00000000..95e95b21 --- /dev/null +++ b/tests/data-pipeline/postgres-reconciler-store.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; +import { createPostgresReconcilerPreParityStore } from "../../lib/data-pipeline/postgres-reconciler-store"; +import { + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + RECONCILER_ROUTE_KEYS, + type ReconcilerCheckpointRequest, + type ReconcilerCommitInput, + type ReconcilerPreParityContract, + type ReconcilerRouteKey, +} from "../../lib/data-pipeline/reconciler-preparity"; + +const HASH = `0x${"11".repeat(32)}` as const; +const EPOCH_ID = "10000000-0000-4000-8000-000000000001"; +const CHECKPOINT_ID = "10000000-0000-4000-8000-000000000002"; + +const request: ReconcilerCheckpointRequest = { + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "ethereum-mainnet", + epochId: EPOCH_ID, + pointerGeneration: "7", + checkpointId: CHECKPOINT_ID, + checkpointBlockNumber: "25700000", + checkpointBlockHash: HASH, + maximumEntityCount: 10_000, +}; + +class ScriptedExecutor implements PostgresExecutor { + readonly applicationQueries: { + text: string; + values: readonly PostgresParameter[]; + }[] = []; + readonly close = vi.fn(async () => undefined); + + constructor( + private readonly scopeRequest: ReconcilerCheckpointRequest = request, + private readonly scopeRouteKeys: readonly ReconcilerRouteKey[] = + RECONCILER_ROUTE_KEYS, + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + if (/select session_user::text as session_user$/iu.test(text.trim())) { + return [ + { session_user: "programmable_reconciler_login" }, + ] as unknown as Row[]; + } + if (/current_role::text as current_role/iu.test(text)) { + return [ + { + session_user: "programmable_reconciler_login", + current_role: "programmable_reconciler", + }, + ] as unknown as Row[]; + } + if (/^set local /iu.test(text.trim())) return [] as unknown as Row[]; + this.applicationQueries.push({ text, values }); + if (/get_reconciler_preparity_contract_v1/iu.test(text)) { + return [ + { + chain_id: "1", + release_id: this.scopeRequest.releaseId, + model_id: this.scopeRequest.modelId, + source_group: this.scopeRequest.sourceGroup, + projector_version: "projector-v1", + epoch_id: this.scopeRequest.epochId, + pointer_generation: this.scopeRequest.pointerGeneration, + checkpoint_id: this.scopeRequest.checkpointId, + checkpoint_generation: "11", + reorg_generation: "0", + checkpoint_block_number: this.scopeRequest.checkpointBlockNumber, + checkpoint_block_hash: HASH, + route_keys: [...this.scopeRouteKeys], + route_contract: { routes: [...this.scopeRouteKeys] }, + projection_contract: { resultCommitment: HASH }, + current_entities: [], + }, + ] as unknown as Row[]; + } + if (/commit_reconciler_preparity_result_v1/iu.test(text)) { + return [ + { + result: { + runId: values[0], + reconciliationId: values[1], + checkpointId: values[11], + checkpointBlockNumber: values[12], + checkpointBlockHash: HASH, + routeCount: this.scopeRouteKeys.length, + mismatchCount: 0, + status: "succeeded", + }, + }, + ] as unknown as Row[]; + } + throw new Error("unexpected query"); + }, + }); + } +} + +function uuid(index: number) { + return `20000000-0000-4000-8000-${String(index).padStart(12, "0")}`; +} + +function validCommit( + contract: ReconcilerPreParityContract, + routeKeys: readonly ReconcilerRouteKey[], +): ReconcilerCommitInput { + return { + runId: uuid(1), + reconciliationId: uuid(2), + parityRecordIds: routeKeys.map((_, index) => uuid(index + 3)), + parityBindingIds: routeKeys.map((_, index) => + uuid(index + 3 + routeKeys.length) + ), + outcomeId: uuid(3 + routeKeys.length * 2), + contract, + workerVersion: "reconciler-preparity-v1", + routeKeys, + legacyDtoHashes: routeKeys.map(() => HASH), + indexedDtoHashes: routeKeys.map(() => HASH), + routeEvidenceCommitments: routeKeys.map(() => HASH), + parityBindingCommitments: routeKeys.map(() => HASH), + requestCommitment: HASH, + reconciliationEvidenceCommitment: HASH, + resultCommitment: HASH, + startedAt: "2026-08-01T00:00:00.000Z", + comparedAt: "2026-08-01T00:00:01.000Z", + finishedAt: "2026-08-01T00:00:02.000Z", + }; +} + +describe("reconciler Postgres store", () => { + it("uses only the narrow read function and one atomic commit function", async () => { + const executor = new ScriptedExecutor(); + const store = createPostgresReconcilerPreParityStore({ executor }); + + const contract = await store.readExactContract(request); + expect(contract).toMatchObject({ + checkpointId: CHECKPOINT_ID, + checkpointBlockHash: HASH, + reorgGeneration: "0", + routeKeys: RECONCILER_ROUTE_KEYS, + }); + + const commit = validCommit(contract, RECONCILER_ROUTE_KEYS); + + await expect(store.commitResult(commit)).resolves.toMatchObject({ + status: "succeeded", + routeCount: 6, + mismatchCount: 0, + }); + expect(executor.applicationQueries).toHaveLength(2); + expect(executor.applicationQueries[0]!.text).toContain( + "get_reconciler_preparity_contract_v1", + ); + expect(executor.applicationQueries[1]!.text).toContain( + "commit_reconciler_preparity_result_v1", + ); + expect(executor.applicationQueries.map(({ text }) => text).join("\n")) + .not.toMatch(/\b(?:insert|update|delete)\b/iu); + }); + + it("preserves the exact four-route Classic V2 commit cardinality", async () => { + const v2Request: ReconcilerCheckpointRequest = { + ...request, + releaseId: "classic-v2", + }; + const executor = new ScriptedExecutor( + v2Request, + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + ); + const store = createPostgresReconcilerPreParityStore({ executor }); + const contract = await store.readExactContract(v2Request); + + await expect(store.commitResult(validCommit( + contract, + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + ))).resolves.toMatchObject({ + status: "succeeded", + routeCount: 4, + mismatchCount: 0, + }); + }); +}); diff --git a/tests/data-pipeline/postgres-reconciler.test.ts b/tests/data-pipeline/postgres-reconciler.test.ts new file mode 100644 index 00000000..93aa6c75 --- /dev/null +++ b/tests/data-pipeline/postgres-reconciler.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; +import { + ReconcilerDatabaseError, + classifyReconcilerSqlState, + createReconcilerDatabaseGateway, +} from "../../lib/data-pipeline/postgres-reconciler"; + +type RecordedQuery = { + text: string; + values: readonly PostgresParameter[]; +}; + +class FakeExecutor implements PostgresExecutor { + readonly queries: RecordedQuery[] = []; + readonly close = vi.fn(async () => undefined); + + constructor( + private readonly sessionUser = "programmable_reconciler_login", + private readonly currentRole = "programmable_reconciler", + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if (/select session_user::text as session_user$/iu.test(text.trim())) { + return [{ session_user: this.sessionUser }] as unknown as Row[]; + } + if (/current_role::text as current_role/iu.test(text)) { + return [ + { + session_user: this.sessionUser, + current_role: this.currentRole, + }, + ] as unknown as Row[]; + } + return [] as unknown as Row[]; + }, + }); + } +} + +describe("reconciler Postgres gateway", () => { + it("authenticates the dedicated login before assuming the narrow capability role", async () => { + const executor = new FakeExecutor(); + const gateway = createReconcilerDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "ok")).resolves.toBe("ok"); + expect(executor.queries.map(({ text }) => text)).toEqual([ + "select session_user::text as session_user", + "set local role programmable_reconciler", + "set local statement_timeout = '3000ms'", + "set local lock_timeout = '500ms'", + "set local idle_in_transaction_session_timeout = '5000ms'", + "select session_user::text as session_user, current_role::text as current_role", + ]); + }); + + it("fails before SET ROLE for any other authenticated login", async () => { + const executor = new FakeExecutor("postgres"); + const gateway = createReconcilerDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "never")).rejects.toMatchObject({ + name: "ReconcilerDatabaseError", + disposition: "fatal-gateway-membership", + retryable: false, + }); + expect(executor.queries).toHaveLength(1); + }); + + it("fails when SET ROLE did not produce the reconciler capability", async () => { + const executor = new FakeExecutor( + "programmable_reconciler_login", + "programmable_reconciler_login", + ); + const gateway = createReconcilerDatabaseGateway({ executor }); + + await expect(gateway.transaction(async () => "never")).rejects.toMatchObject({ + disposition: "fatal-gateway-membership", + }); + }); +}); + +describe("reconciler SQLSTATE policy", () => { + it.each([ + ["40001", "retry-serialization", true], + ["40P01", "retry-serialization", true], + ["55P03", "retry-transient", true], + ["57014", "retry-transient", true], + ["08006", "retry-transient", true], + ["42501", "fatal-gateway-membership", false], + ["22023", "fatal-codec-or-caller", false], + ["23505", "immutable-replay-conflict", false], + ["23514", "fatal-integrity", false], + ["55000", "stale-checkpoint", true], + ] as const)("maps %s to %s", (sqlState, disposition, retryable) => { + expect(classifyReconcilerSqlState(sqlState)).toEqual({ + sqlState, + disposition, + retryable, + }); + }); + + it("never exposes database text, credentials or query details", () => { + const secret = "postgres://reconciler:secret@example.invalid/database"; + const error = ReconcilerDatabaseError.fromUnknown({ + code: "40001", + message: secret, + detail: secret, + query: `select '${secret}'`, + }); + + expect(String(error)).not.toContain(secret); + expect(JSON.stringify(error)).not.toContain(secret); + expect(error).toMatchObject({ + disposition: "retry-serialization", + retryable: true, + }); + }); +}); diff --git a/tests/data-pipeline/postgres.integration.test.ts b/tests/data-pipeline/postgres.integration.test.ts new file mode 100644 index 00000000..cd88f852 --- /dev/null +++ b/tests/data-pipeline/postgres.integration.test.ts @@ -0,0 +1,368 @@ +import { randomBytes } from "node:crypto"; +import { readFileSync, readdirSync } from "node:fs"; + +import postgres from "postgres"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + createPostgresExecutor, + createPostgresReadModel, + type PostgresExecutor, +} from "../../lib/data-pipeline/postgres"; + +const configuredDatabaseUrl = process.env.PROGRAMMABLE_TEST_DATABASE_URL; +const UINT256_MAX = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; +const TOKEN = "0x1111111111111111111111111111111111111111"; +const TRANSACTION_HASH = `0x${"22".repeat(32)}`; + +function requireLocalDatabaseUrl(raw: string): string { + let parsed: URL; + try { + parsed = new URL(raw); + } catch { + throw new Error("PROGRAMMABLE_TEST_DATABASE_URL must be a PostgreSQL URL"); + } + const localHosts = new Set(["localhost", "127.0.0.1", "[::1]"]); + if ( + parsed.protocol !== "postgresql:" || + !localHosts.has(parsed.hostname) || + parsed.username.length === 0 || + parsed.pathname.length <= 1 + ) { + throw new Error( + "PROGRAMMABLE_TEST_DATABASE_URL must target a loopback PostgreSQL database", + ); + } + return parsed.toString(); +} + +const localDatabaseUrl = configuredDatabaseUrl + ? requireLocalDatabaseUrl(configuredDatabaseUrl) + : undefined; + +function repositoryMigrationVersions(): string[] { + return readdirSync( + new URL("../../supabase/migrations/", import.meta.url), + { withFileTypes: true }, + ) + .filter((entry) => entry.isFile() && /^\d+_.+\.sql$/.test(entry.name)) + .map((entry) => entry.name.slice(0, entry.name.indexOf("_"))) + .sort(); +} + +function required(value: T | undefined, label: string): T { + if (value === undefined) throw new Error(`${label} was not initialized`); + return value; +} + +function runtimeConnectionString(adminUrl: string, role: string, password: string) { + const parsed = new URL(adminUrl); + parsed.username = role; + parsed.password = password; + return parsed.toString(); +} + +async function expectPostgresErrorCode( + operation: Promise, + expectedCode: string, +) { + try { + await operation; + } catch (error) { + expect(error).toMatchObject({ code: expectedCode }); + return; + } + throw new Error(`expected PostgreSQL error ${expectedCode}`); +} + +describe.skipIf(!localDatabaseUrl)( + "Postgres adapter against the migrated local schema", + () => { + let admin: ReturnType | undefined; + let executor: PostgresExecutor | undefined; + let readModel: ReturnType | undefined; + const roleName = "programmable_api_reader_login"; + const attackerRoleName = `programmable_adapter_${process.pid}_${randomBytes(4).toString("hex")}`; + const rolePassword = randomBytes(24).toString("hex"); + const attackerRolePassword = randomBytes(24).toString("hex"); + + beforeAll(async () => { + const adminUrl = required(localDatabaseUrl, "local database URL"); + admin = postgres(adminUrl, { + prepare: false, + max: 1, + connect_timeout: 3, + idle_timeout: 5, + }); + + const expectedMigrations = repositoryMigrationVersions(); + const migrationRows = (await admin.unsafe( + `select version::text as version + from supabase_migrations.schema_migrations + where version = any($1::text[]) + order by version`, + [expectedMigrations], + )) as unknown as { version: string }[]; + if ( + expectedMigrations.length === 0 || + migrationRows.map(({ version }) => version).join(",") !== + expectedMigrations.join(",") + ) { + throw new Error( + "PROGRAMMABLE_TEST_DATABASE_URL does not contain every repository migration", + ); + } + + // Passwords are generated from strict lowercase alphanumeric alphabets and + // exist only for this loopback test database. Runtime credentials remain + // outside migrations and application code. + await admin.unsafe( + `alter role "${roleName}" password '${rolePassword}'`, + ); + await admin.unsafe( + `create role "${attackerRoleName}" login noinherit password '${attackerRolePassword}'`, + ); + await admin.unsafe( + `grant programmable_api_reader to "${attackerRoleName}"`, + ); + + executor = createPostgresExecutor({ + connectionString: runtimeConnectionString( + adminUrl, + roleName, + rolePassword, + ), + maxConnections: 1, + connectTimeoutMs: 3_000, + idleTimeoutMs: 5_000, + allowInsecureLoopback: true, + }); + readModel = createPostgresReadModel({ executor }); + }, 30_000); + + afterAll(async () => { + if (executor) await executor.close(); + if (!admin) return; + await admin.unsafe( + `select pg_catalog.pg_terminate_backend(pid) + from pg_catalog.pg_stat_activity + where usename = any($1::text[]) and pid <> pg_catalog.pg_backend_pid()`, + [[roleName, attackerRoleName]], + ); + await admin.unsafe(`alter role "${roleName}" password null`); + await admin.unsafe(`drop role if exists "${attackerRoleName}"`); + await admin.end({ timeout: 5 }); + }, 30_000); + + it("uses postgres 3.4.7 against every checked-in migration and approved read object", async () => { + const packageMetadata = JSON.parse( + readFileSync( + new URL("../../node_modules/postgres/package.json", import.meta.url), + "utf8", + ), + ) as { version?: unknown }; + expect(packageMetadata.version).toBe("3.4.7"); + + const rows = (await required(admin, "admin connection").unsafe( + `select + pg_catalog.to_regnamespace('programmable_private')::text as schema_name, + pg_catalog.to_regrole('programmable_api_reader')::text as reader_role, + pg_catalog.to_regclass('programmable_private.recent_launches_v1')::text as launch_view, + pg_catalog.to_regprocedure( + 'programmable_private.get_recent_launches_v1(bigint,integer,bigint,bytea,bytea)' + )::text as recent_function, + pg_catalog.to_regprocedure( + 'programmable_private.get_launch_by_token_v1(bigint,bytea)' + )::text as token_function`, + )) as unknown as Record[]; + expect(rows).toEqual([ + { + schema_name: "programmable_private", + reader_role: "programmable_api_reader", + launch_view: "programmable_private.recent_launches_v1", + recent_function: + "programmable_private.get_recent_launches_v1(bigint,integer,bigint,bytea,bytea)", + token_function: + "programmable_private.get_launch_by_token_v1(bigint,bytea)", + }, + ]); + }); + + it("sets the approved login to programmable_api_reader only inside its transaction", async () => { + const rows = await required(executor, "runtime executor").transaction( + async (transaction) => { + const before = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + await transaction.query("set local role programmable_api_reader"); + const after = await transaction.query<{ + session_user: unknown; + current_role: unknown; + }>( + "select session_user::text as session_user, current_role::text as current_role", + ); + return { before, after }; + }, + ); + + expect(rows.before).toEqual([ + { session_user: roleName, current_role: roleName }, + ]); + expect(rows.after).toEqual([ + { + session_user: roleName, + current_role: "programmable_api_reader", + }, + ]); + }); + + it("rejects privileged and arbitrary member logins even when they can assume the reader role", async () => { + const adminUrl = required(localDatabaseUrl, "local database URL"); + const candidates = [ + adminUrl, + runtimeConnectionString( + adminUrl, + attackerRoleName, + attackerRolePassword, + ), + ]; + + for (const connectionString of candidates) { + const wrongExecutor = createPostgresExecutor({ + connectionString, + maxConnections: 1, + connectTimeoutMs: 3_000, + idleTimeoutMs: 5_000, + allowInsecureLoopback: true, + }); + try { + const wrongModel = createPostgresReadModel({ + executor: wrongExecutor, + }); + await expect( + wrongModel.recentLaunches({ chainId: "1", limit: 1 }), + ).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + safeMetadata: { operation: "runtime-login-role" }, + }); + } finally { + await wrongExecutor.close(); + } + } + }); + + it("reads every approved adapter function and view through the runtime role", async () => { + const model = required(readModel, "Postgres read model"); + + await expect( + model.recentLaunches({ chainId: "1", limit: 1 }), + ).resolves.toEqual(expect.any(Array)); + await expect( + model.launchByToken({ chainId: "1", token: TOKEN }), + ).resolves.toSatisfy((value) => value === null || value.token === TOKEN); + await expect( + model.publicProfile({ + chainId: "1", + account: TOKEN, + limit: 1, + offset: 0, + }), + ).resolves.toMatchObject({ + launches: expect.any(Array), + rewards: expect.any(Array), + }); + await expect( + model.classicVaultHistory({ chainId: "1", vault: TOKEN, limit: 1 }), + ).resolves.toEqual(expect.any(Array)); + await expect( + model.stockPairedVaultHistory({ + chainId: "1", + vault: TOKEN, + limit: 1, + }), + ).resolves.toEqual(expect.any(Array)); + await expect( + model.launchLookup({ + chainId: "1", + transactionHash: TRANSACTION_HASH, + limit: 1, + }), + ).resolves.toEqual(expect.any(Array)); + await expect(model.health()).resolves.toMatchObject({ + checkpoints: expect.any(Array), + parity: expect.any(Array), + circuits: expect.any(Array), + }); + }); + + it("binds parameters and decodes bytea, OID, bigint, and uint256 numeric values losslessly", async () => { + const rawBytes = Uint8Array.from([0, 1, 127, 128, 254, 255]); + const beyondSafeInteger = "9007199254740993"; + const opaqueText = "'); delete from programmable_private.release_epochs; --"; + const rows = await required(executor, "runtime executor").transaction( + async (transaction) => { + await transaction.query("set local role programmable_api_reader"); + return transaction.query>( + `select + $1::bytea as raw_bytes, + $2::bigint as bigint_value, + $3::numeric(78,0) as uint256_value, + $4::text as opaque_text, + 'bytea'::pg_catalog.regtype::pg_catalog.oid::integer as bytea_oid, + 'bigint'::pg_catalog.regtype::pg_catalog.oid::integer as bigint_oid, + 'numeric'::pg_catalog.regtype::pg_catalog.oid::integer as numeric_oid`, + [rawBytes, beyondSafeInteger, UINT256_MAX, opaqueText], + ); + }, + ); + + expect(rows).toHaveLength(1); + expect(Array.from(rows[0]!.raw_bytes as Uint8Array)).toEqual([ + 0, 1, 127, 128, 254, 255, + ]); + expect(rows[0]).toMatchObject({ + bigint_value: beyondSafeInteger, + uint256_value: UINT256_MAX, + opaque_text: opaqueText, + bytea_oid: 17, + bigint_oid: 20, + numeric_oid: 1700, + }); + }); + + it("rejects direct base-table reads for the API reader", async () => { + await expectPostgresErrorCode( + required(executor, "runtime executor").transaction( + async (transaction) => { + await transaction.query("set local role programmable_api_reader"); + await transaction.query( + "select * from programmable_private.release_epochs limit 1", + ); + }, + ), + "42501", + ); + }); + + it("rejects writes for the API reader", async () => { + await expectPostgresErrorCode( + required(executor, "runtime executor").transaction( + async (transaction) => { + await transaction.query("set local role programmable_api_reader"); + await transaction.query( + "delete from programmable_private.release_epochs where false", + ); + }, + ), + "42501", + ); + }); + }, +); diff --git a/tests/data-pipeline/postgres.test.ts b/tests/data-pipeline/postgres.test.ts new file mode 100644 index 00000000..1cae930c --- /dev/null +++ b/tests/data-pipeline/postgres.test.ts @@ -0,0 +1,469 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + createPostgresReadModel, + postgresDriverOptions, + type PostgresExecutor, + type PostgresParameter, + type PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; + +const TOKEN = "0x1111111111111111111111111111111111111111"; +const CREATOR = "0x2222222222222222222222222222222222222222"; +const HOOK = "0x3333333333333333333333333333333333333333"; +const VAULT = "0x4444444444444444444444444444444444444444"; +const POOL_ID = `0x${"55".repeat(32)}`; +const LAUNCH_HASH = `0x${"66".repeat(32)}`; +const TRANSACTION_HASH = `0x${"77".repeat(32)}`; +const BLOCK_HASH = `0x${"88".repeat(32)}`; + +function bytes(hex: string) { + return Uint8Array.from( + hex + .slice(2) + .match(/.{2}/g)! + .map((part) => Number.parseInt(part, 16)), + ); +} + +function launchRow() { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + token: bytes(TOKEN), + creator: bytes(CREATOR), + launch_transaction_hash: bytes(TRANSACTION_HASH), + reward_vault: bytes(VAULT), + pool_id: bytes(POOL_ID), + launch_hash: bytes(LAUNCH_HASH), + token_name: "Test", + token_symbol: "TEST", + total_supply: "1000000000000000000000000000", + launch_block_timestamp: "2026-07-31T08:00:00.000Z", + launch_transaction_index: 2, + launch_receipt_log_ordinal: 1, + currency0: bytes(TOKEN), + currency1: bytes(CREATOR), + hook: bytes(HOOK), + quote_asset: bytes(CREATOR), + pool_key_fee: "8388608", + tick_spacing: 200, + total_swap_fee_bps: 100, + buy_swap_fee_bps: 100, + sell_swap_fee_bps: 100, + creator_fee_bps: 90, + launcher_fee_bps: 10, + transfer_tax_bps: 0, + lp_fee_pips: "10000", + project_name: null, + project_description: null, + project_logo_reference: null, + project_metadata_revision: null, + project_metadata_created_at: null, + project_links: [], + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + }; +} + +function rewardRow() { + return { + chain_id: "1", + account: bytes(CREATOR), + release_id: "classic-v3", + model_id: "classic", + vault: bytes(VAULT), + pool_id: bytes(POOL_ID), + hook: bytes(HOOK), + quote_asset: bytes(CREATOR), + entitled: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + claimed_total: "1", + claimable_accrued: + "115792089237316195423570985008687907853269984665640564039457584007913129639934", + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + }; +} + +type RecordedQuery = { + text: string; + values: readonly PostgresParameter[]; +}; + +class FakeExecutor implements PostgresExecutor { + readonly queries: RecordedQuery[] = []; + readonly close = vi.fn(async () => undefined); + constructor( + private readonly responder: ( + text: string, + values: readonly PostgresParameter[], + ) => Promise[]>, + private readonly sessionUser = "programmable_api_reader_login", + private readonly capabilityRole = "programmable_api_reader", + private readonly postSetSessionUser = sessionUser, + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if ( + text === + "select session_user::text as session_user, current_role::text as current_role" + ) { + return [ + { + session_user: this.postSetSessionUser, + current_role: this.capabilityRole, + }, + ] as unknown as Row[]; + } + if (text === "select session_user::text as session_user") { + return [{ session_user: this.sessionUser }] as unknown as Row[]; + } + return (await this.responder(text, values)) as Row[]; + }, + }); + } +} + +describe("private Postgres read-model adapter", () => { + it("uses transaction-mode-safe driver defaults", () => { + expect( + postgresDriverOptions({ + maxConnections: 3, + connectTimeoutMs: 900, + idleTimeoutMs: 5_000, + }), + ).toMatchObject({ + prepare: false, + max: 3, + connect_timeout: 1, + idle_timeout: 5, + fetch_types: true, + connection: { + application_name: "programmable-read-model", + }, + }); + }); + + it("does not return a synthetic timeout while a transaction continues", async () => { + const executor = new FakeExecutor(async (text) => { + if (text.includes("get_recent_launches_v1")) { + await new Promise((resolve) => setTimeout(resolve, 1_050)); + } + return []; + }); + const readModel = createPostgresReadModel({ executor }); + + await expect( + readModel.recentLaunches({ chainId: "1", limit: 1 }), + ).resolves.toEqual([]); + }, 5_000); + + it("sets only the API-reader role and returns bytea/bigint-safe eligible launches", async () => { + const executor = new FakeExecutor(async (text) => + text.includes("get_recent_launches_v1") ? [launchRow()] : [], + ); + const readModel = createPostgresReadModel({ executor }); + + const launches = await readModel.recentLaunches({ + chainId: "1", + limit: 25, + }); + + expect(launches).toEqual([ + expect.objectContaining({ + chainId: "1", + token: TOKEN, + creator: CREATOR, + quoteAsset: CREATOR, + hook: HOOK, + rewardVault: VAULT, + poolId: POOL_ID, + totalSwapFeeBps: 100, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + releaseVersion: "classic-v3", + modelVersion: "classic", + launchHash: LAUNCH_HASH, + launchTransactionHash: TRANSACTION_HASH, + launchBlockTimestamp: "2026-07-31T08:00:00.000Z", + promotedBlockNumber: "25650000", + promotedBlockHash: BLOCK_HASH, + verifiedAt: "2026-07-31T08:01:00.000Z", + }), + ]); + expect(executor.queries.slice(0, 6).map((query) => query.text)).toEqual([ + "select session_user::text as session_user", + "set local role programmable_api_reader", + "select session_user::text as session_user, current_role::text as current_role", + "set local statement_timeout = '1000ms'", + "set local lock_timeout = '250ms'", + "set local idle_in_transaction_session_timeout = '2000ms'", + ]); + const dataQuery = executor.queries.at(-1)!; + expect(dataQuery.text).toContain( + "programmable_private.get_recent_launches_v1($1, $2, $3, $4, $5)", + ); + expect(dataQuery.values).toEqual(["1", 25, null, null, null]); + }); + + it.each([ + "postgres", + "service_role", + "programmable_projector_login", + "arbitrary_reader_member", + ])( + "rejects the %s login before it can assume the API-reader capability", + async (sessionUser) => { + const executor = new FakeExecutor(async () => [], sessionUser); + const readModel = createPostgresReadModel({ executor }); + + await expect( + readModel.recentLaunches({ chainId: "1", limit: 1 }), + ).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + safeMetadata: { operation: "runtime-login-role" }, + }); + expect(executor.queries.map(({ text }) => text)).toEqual([ + "select session_user::text as session_user", + ]); + }, + ); + + it.each([ + { + label: "a changed session identity", + capabilityRole: "programmable_api_reader", + postSetSessionUser: "postgres", + }, + { + label: "the wrong capability role", + capabilityRole: "programmable_projector", + postSetSessionUser: "programmable_api_reader_login", + }, + ])( + "rejects $label after SET ROLE", + async ({ capabilityRole, postSetSessionUser }) => { + const executor = new FakeExecutor( + async () => [], + "programmable_api_reader_login", + capabilityRole, + postSetSessionUser, + ); + const readModel = createPostgresReadModel({ executor }); + + await expect( + readModel.recentLaunches({ chainId: "1", limit: 1 }), + ).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + safeMetadata: { operation: "runtime-role" }, + }); + expect(executor.queries.map(({ text }) => text)).toEqual([ + "select session_user::text as session_user", + "set local role programmable_api_reader", + "select session_user::text as session_user, current_role::text as current_role", + ]); + }, + ); + + it("parameterizes token, creator, account, limits, and offsets without base-table or write SQL", async () => { + const executor = new FakeExecutor(async (text) => { + if (text.includes("get_launch_by_token_v1")) return [launchRow()]; + if (text.includes("launches_by_creator_v1")) return [launchRow()]; + if (text.includes("get_account_reward_summary_v1")) return [rewardRow()]; + return []; + }); + const readModel = createPostgresReadModel({ executor }); + + await expect( + readModel.launchByToken({ chainId: "1", token: TOKEN }), + ).resolves.toMatchObject({ token: TOKEN }); + await expect( + readModel.publicProfile({ + chainId: "1", + account: CREATOR, + limit: 20, + offset: 0, + }), + ).resolves.toMatchObject({ + launches: [{ creator: CREATOR }], + rewards: [ + { + entitled: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + claimable: + "115792089237316195423570985008687907853269984665640564039457584007913129639934", + }, + ], + }); + + const dataQueries = executor.queries.filter( + ({ text }) => + !/^set local/i.test(text) && !/select session_user/i.test(text), + ); + expect(dataQueries).toHaveLength(3); + for (const query of dataQueries) { + expect(query.text).not.toMatch( + /\b(?:insert|update|delete|alter|create|drop|truncate)\b/i, + ); + expect(query.text).not.toMatch( + /\b(?:launch_projections|account_reward_balances|chain_event_occurrences)\b/i, + ); + expect(query.text).not.toContain(TOKEN); + expect(query.text).not.toContain(CREATOR); + } + expect( + dataQueries.some(({ values }) => + values.some( + (value) => + value instanceof Uint8Array && + Buffer.from(value).equals(Buffer.from(bytes(CREATOR))), + ), + ), + ).toBe(true); + }); + + it("uses only approved history, lookup, and health views with bounded filters", async () => { + const executor = new FakeExecutor(async (text) => { + if (text.includes("classic_v3_vault_history_v1")) { + return [ + { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + vault: bytes(VAULT), + pool_id: bytes(POOL_ID), + configuration_hash: bytes(LAUNCH_HASH), + configuration_epoch: "1", + allocation_index: 0, + beneficiary: bytes(CREATOR), + payout_address: bytes(CREATOR), + share_bps: 10_000, + effective_from_block: "25650000", + effective_to_block: null, + promoted_block_number: "25650000", + promoted_block_hash: bytes(BLOCK_HASH), + verified_at: "2026-07-31T08:01:00.000Z", + }, + ]; + } + if (text.includes("stock_paired_vault_history_v1")) return []; + if (text.includes("launch_lookup_v1")) return []; + if (text.includes("checkpoint_summary_v1")) return []; + if (text.includes("parity_summary_v1")) { + return [ + { + route_key: "explore-list", + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + comparison_count: "5", + matching_count: "5", + mismatch_count: "0", + last_compared_at: "2026-07-31T08:01:00.000Z", + last_resolved_at: null, + }, + ]; + } + if (text.includes("health_summary_v1")) return []; + return []; + }); + const readModel = createPostgresReadModel({ executor }); + + await expect( + readModel.classicVaultHistory({ + chainId: "1", + vault: VAULT, + limit: 50, + }), + ).resolves.toMatchObject([ + { + vault: VAULT, + configurationEpoch: "1", + effectiveToBlock: null, + }, + ]); + await readModel.stockPairedVaultHistory({ + chainId: "1", + vault: VAULT, + limit: 50, + }); + await readModel.launchLookup({ + chainId: "1", + transactionHash: TRANSACTION_HASH, + limit: 10, + }); + await expect(readModel.health()).resolves.toMatchObject({ + parity: [{ matchingCount: "5", mismatchCount: "0" }], + }); + + const dataSql = executor.queries + .filter( + ({ text }) => + !/^set local/i.test(text) && !/select session_user/i.test(text), + ) + .map(({ text }) => text) + .join("\n"); + for (const view of [ + "classic_v3_vault_history_v1", + "stock_paired_vault_history_v1", + "launch_lookup_v1", + "checkpoint_summary_v1", + "parity_summary_v1", + "health_summary_v1", + ]) { + expect(dataSql).toContain(view); + } + }); + + it("rejects out-of-range pagination before a query", async () => { + const executor = new FakeExecutor(async () => []); + const readModel = createPostgresReadModel({ executor }); + + await expect( + readModel.recentLaunches({ chainId: "1", limit: 101 }), + ).rejects.toMatchObject({ + code: "invalid_input", + countsTowardCircuit: false, + }); + expect(executor.queries).toHaveLength(0); + }); + + it("redacts connection and query failures and closes explicitly", async () => { + const secret = "postgresql://reader:password@db.example/postgres"; + const executor = new FakeExecutor(async () => { + throw new Error(`connection failed for ${secret}`); + }); + const readModel = createPostgresReadModel({ executor }); + let thrown: unknown; + try { + await readModel.recentLaunches({ chainId: "1", limit: 10 }); + } catch (error) { + thrown = error; + } + + expect(thrown).toMatchObject({ + dependency: "postgres", + code: "query_failed", + }); + expect(String(thrown)).not.toContain(secret); + expect(JSON.stringify(thrown)).not.toContain(secret); + await readModel.close(); + expect(executor.close).toHaveBeenCalledOnce(); + }); +}); diff --git a/tests/data-pipeline/projector-dual-rpc-coverage.test.ts b/tests/data-pipeline/projector-dual-rpc-coverage.test.ts new file mode 100644 index 00000000..7059949c --- /dev/null +++ b/tests/data-pipeline/projector-dual-rpc-coverage.test.ts @@ -0,0 +1,1287 @@ +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + encodeEventTopics, + keccak256, + parseAbiItem, + type AbiParameter, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +const { TEST_SOURCE_CODE_HASH } = vi.hoisted(() => ({ + TEST_SOURCE_CODE_HASH: + "0x7efcce47028dabcb0d42f3a7eda8820bf6f7f4e618398c2547d52f703cafb073" as const, +})); + +vi.mock( + "../../lib/data-pipeline/release-binding.server", + async (importOriginal) => { + const original = await importOriginal< + typeof import("../../lib/data-pipeline/release-binding.server") + >(); + const binding = original.getDataPipelineReleaseBinding(); + return { + ...original, + getDataPipelineReleaseBinding: () => ({ + ...binding, + sources: binding.sources.map((source) => + source.contractName === "ClassicV2Launcher" + ? { ...source, runtimeCodeHash: TEST_SOURCE_CODE_HASH } + : source, + ), + }), + }; + }, +); + +import { + verifyDynamicRuntimeAtActivationWithDualRpc, + verifyDynamicRuntimeAtBlockWithDualRpc, + verifyDynamicRuntimesAtBlockWithDualRpc, + verifyEnvioCandidateWindowWithDualRpc, + type CandidateRpcClient, + type CandidateRpcLog, + type DualRpcCandidateBatchEvidence, + type DualRpcCandidateWindowEvidence, + type ProjectorDynamicSourceTemplate, +} from "../../lib/data-pipeline/dual-rpc"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import type { CanonicalDynamicSourceDeploymentEvidence } from "../../lib/data-pipeline/projector-dynamic-activation"; +import { immutableReferencesCommitment } from "../../lib/data-pipeline/runtime-bytecode"; +import { canonicalPayloadJson } from "../../indexer/src/lib/payload-hash"; +import { rpcProviderCommitment } from "../../lib/data-pipeline/rpc-provider-commitments"; + +const SOURCE = "0xd240d06f8586eb799f20056054e5b527405e6bad" as const; +const BLOCK_NUMBER = 25_624_131n; +const BLOCK_HASH = `0x${"11".repeat(32)}` as const; +const SAFE_BLOCK_NUMBER = BLOCK_NUMBER + 12n; +const SAFE_BLOCK_HASH = `0x${"22".repeat(32)}` as const; +const TRANSACTION_HASH = `0x${"33".repeat(32)}` as const; +const EVENT = parseAbiItem( + "event MemeTokenLaunched(address indexed creator, address indexed token, bytes32 indexed poolId, address feeHook, address positionRecipient, uint256 positionTokenId, uint16 totalSwapFeeBps, bytes32 launchHash)", +); +const ARGS = { + creator: "0x1111111111111111111111111111111111111111", + token: "0x2222222222222222222222222222222222222222", + poolId: `0x${"44".repeat(32)}`, + feeHook: "0x5555555555555555555555555555555555555555", + positionRecipient: "0x6666666666666666666666666666666666666666", + positionTokenId: 42n, + totalSwapFeeBps: 100n, + launchHash: `0x${"77".repeat(32)}`, +} as const; +const TOPICS = encodeEventTopics({ + abi: [EVENT], + eventName: EVENT.name, + args: ARGS, +}) as readonly Hex[]; +const NON_INDEXED = EVENT.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, +) as readonly AbiParameter[]; +const DATA = encodeAbiParameters( + NON_INDEXED, + NON_INDEXED.map((input) => ARGS[input.name as keyof typeof ARGS]), +); +const PAYLOAD_HASH = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [TOPICS, DATA], + ), +); +const DYNAMIC_FACTORY = + "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a" as const; +const DYNAMIC_CHILD = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as const; +const DYNAMIC_CONFIGURATION = `0x${"88".repeat(32)}` as const; +const DYNAMIC_POOL_ID = `0x${"98".repeat(32)}` as const; +const DYNAMIC_FEE_HOOK = + "0x9999999999999999999999999999999999999999" as const; +const CLASSIC_V3_LAUNCHER = + "0xc3bd04aac2fb2ba58efd7eb673e544e0b80de770" as const; +const ACTIVATION_BLOCK_NUMBER = BLOCK_NUMBER + 20_000n; +const ACTIVATION_BLOCK_HASH = `0x${"a1".repeat(32)}` as const; +const ACTIVATION_TRANSACTION_HASH = `0x${"a2".repeat(32)}` as const; +const DYNAMIC_REFERENCES = [{ start: 0, length: 20 }] as const; +const DYNAMIC_NORMALIZED_HASH = keccak256( + `0x${"00".repeat(20)}`, +); +const DYNAMIC_REFERENCES_COMMITMENT = immutableReferencesCommitment( + DYNAMIC_REFERENCES, + 20, +); + +const DYNAMIC_PARENT_EVENT = parseAbiItem( + "event ClassicRewardVaultDeployed(address indexed vault, bytes32 indexed poolId, address indexed feeHook, bytes32 salt, bytes32 configurationHash)", +); +const DYNAMIC_LAUNCH_EVENT = parseAbiItem( + "event MemeTokenLaunchedV2(address indexed deployer, address indexed token, bytes32 indexed poolId, address feeHook, address rewardVault, address positionRecipient, uint256 positionTokenId, uint16 buySwapFeeBps, uint16 sellSwapFeeBps, bytes32 rewardConfigurationHash, bytes32 launchHash)", +); + +function encodedEventCandidate(input: { + event: typeof DYNAMIC_PARENT_EVENT | typeof DYNAMIC_LAUNCH_EVENT; + eventName: "ClassicRewardVaultDeployed" | "MemeTokenLaunchedV2"; + args: Record; + base: EnvioCandidate; +}): EnvioCandidate { + const topics = encodeEventTopics({ + abi: [input.event], + eventName: input.eventName, + args: input.args, + }) as readonly Hex[]; + const nonIndexed = input.event.inputs.filter( + (parameter) => !("indexed" in parameter) || parameter.indexed !== true, + ) as readonly AbiParameter[]; + const data = encodeAbiParameters( + nonIndexed, + nonIndexed.map( + (parameter) => input.args[parameter.name as keyof typeof input.args], + ), + ); + return { + ...input.base, + orderedTopics: [...topics], + rawData: data, + decodedPayload: JSON.parse(canonicalPayloadJson(input.args)), + payloadHash: keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [topics, data], + ), + ), + }; +} + +function dynamicParentCandidate(): EnvioCandidate { + const base = { + ...candidate(), + sourceAddress: DYNAMIC_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + releaseHint: { model: "classic", releaseVersion: "classic-v3" }, + } as EnvioCandidate; + return encodedEventCandidate({ + event: DYNAMIC_PARENT_EVENT, + eventName: "ClassicRewardVaultDeployed", + base, + args: { + vault: DYNAMIC_CHILD, + poolId: DYNAMIC_POOL_ID, + feeHook: DYNAMIC_FEE_HOOK, + salt: `0x${"87".repeat(32)}`, + configurationHash: DYNAMIC_CONFIGURATION, + }, + }); +} + +function dynamicLaunchCandidate( + overrides: Partial = {}, +): EnvioCandidate { + const base = { + ...candidate(), + candidateId: + `1:${ACTIVATION_BLOCK_HASH}:${ACTIVATION_TRANSACTION_HASH}:4`, + blockNumber: ACTIVATION_BLOCK_NUMBER.toString(), + blockHash: ACTIVATION_BLOCK_HASH, + transactionHash: ACTIVATION_TRANSACTION_HASH, + transactionIndex: 1, + blockGlobalLogIndex: 4, + sourceAddress: CLASSIC_V3_LAUNCHER, + contractName: "ClassicV3Launcher", + eventName: "MemeTokenLaunchedV2", + releaseHint: { model: "classic", releaseVersion: "classic-v3" }, + } as EnvioCandidate; + return encodedEventCandidate({ + event: DYNAMIC_LAUNCH_EVENT, + eventName: "MemeTokenLaunchedV2", + base, + args: { + deployer: "0x1111111111111111111111111111111111111111", + token: "0x2222222222222222222222222222222222222222", + poolId: DYNAMIC_POOL_ID, + rewardVault: DYNAMIC_CHILD, + feeHook: DYNAMIC_FEE_HOOK, + positionRecipient: "0x3333333333333333333333333333333333333333", + positionTokenId: 1n, + buySwapFeeBps: 100n, + sellSwapFeeBps: 100n, + rewardConfigurationHash: DYNAMIC_CONFIGURATION, + launchHash: `0x${"89".repeat(32)}`, + ...overrides, + }, + }); +} + +function dynamicActivationEvidence( + providers: readonly [ReturnType, ReturnType], + launch = dynamicLaunchCandidate(), +): DualRpcCandidateBatchEvidence { + const providerIdentities = providers.map(({ identity }) => identity) as [ + string, + string, + ]; + const providerVendorGroups = providers.map(({ vendorGroup }) => vendorGroup) as [ + string, + string, + ]; + const providerEndpointCommitments = providers.map( + ({ endpointCommitment }) => endpointCommitment, + ) as [`0x${string}`, `0x${string}`]; + const providerOriginCommitments = providers.map( + ({ endpointOriginCommitment }) => endpointOriginCommitment, + ) as [`0x${string}`, `0x${string}`]; + return { + chainId: 1, + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads: [ + (ACTIVATION_BLOCK_NUMBER + 12n).toString(), + (ACTIVATION_BLOCK_NUMBER + 12n).toString(), + ], + safeBlockNumber: ACTIVATION_BLOCK_NUMBER.toString(), + safeBlockHash: ACTIVATION_BLOCK_HASH, + executionTrace: { + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize: 1, + hardDeadlineMs: 1_000, + maxCallsPerProvider: 128, + elapsedMs: 1, + providerCallCounts: [0, 0], + calls: [], + }, + candidates: [ + { + chainId: 1, + candidateId: launch.candidateId, + sourceAddress: launch.sourceAddress, + contractName: launch.contractName, + eventName: launch.eventName, + sourceKind: "static", + model: "classic", + releaseVersion: "classic-v3", + payloadHash: launch.payloadHash, + rawLogCommitment: `0x${"a3".repeat(32)}`, + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads: [ + (ACTIVATION_BLOCK_NUMBER + 12n).toString(), + (ACTIVATION_BLOCK_NUMBER + 12n).toString(), + ], + safeBlockNumber: ACTIVATION_BLOCK_NUMBER.toString(), + safeBlockHash: ACTIVATION_BLOCK_HASH, + candidateBlockNumber: ACTIVATION_BLOCK_NUMBER.toString(), + candidateBlockHash: ACTIVATION_BLOCK_HASH, + candidateBlockTimestamp: launch.blockTimestamp, + transactionHash: launch.transactionHash, + transactionIndex: launch.transactionIndex, + receiptCommitment: `0x${"a4".repeat(32)}`, + sourceCodeHash: `0x${"a5".repeat(32)}`, + receiptLogOrdinal: 0, + }, + ], + }; +} + +function dynamicTemplate(): ProjectorDynamicSourceTemplate { + const hash = `0x${"77".repeat(32)}` as const; + return { + templateId: "10000000-0000-4000-8000-000000000001", + contractName: "ClassicV3RewardVault", + model: "classic", + releaseVersion: "classic-v3", + parentFactoryAddress: DYNAMIC_FACTORY, + parentFactoryContractName: "ClassicV3RewardVaultFactory", + parentFactoryBindingId: "10000000-0000-4000-8000-000000000002", + parentFactoryBindingCommitment: hash, + parentSourceRole: "vault_factory", + factoryEventName: "ClassicRewardVaultDeployed", + deployedAddressField: "vault", + deployedSourceRole: "reward_vault", + deployedArtifactCreationCodeCommitment: hash, + expectedExactRuntimeCodeHash: null, + expectedNormalizedRuntimeCodeHash: DYNAMIC_NORMALIZED_HASH, + expectedImmutableReferencesCommitment: + DYNAMIC_REFERENCES_COMMITMENT, + expectedRuntimeByteLength: "20", + immutableReferences: DYNAMIC_REFERENCES, + immutableBindingSpec: { + factoryConfigurationField: "configurationHash", + bindings: [ + { + ordinal: "0", + offset: "0", + length: "20", + source: "deployed_address", + encoding: "address", + }, + ], + }, + immutableBindingCommitment: hash, + abiEventSetCommitment: hash, + templateCommitment: hash, + database: { + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "canonical-events", + }, + epochId: "10000000-0000-4000-8000-000000000003", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: + "10000000-0000-4000-8000-000000000004", + rpcProviderDeploymentIds: [ + "10000000-0000-4000-8000-000000000005", + "10000000-0000-4000-8000-000000000006", + ], + }, + }; +} + +function dynamicParentEvidence( + providers: readonly [ReturnType, ReturnType], +): DualRpcCandidateWindowEvidence { + const parent = dynamicParentCandidate(); + const providerIdentities = providers.map(({ identity }) => identity) as [ + string, + string, + ]; + const providerVendorGroups = providers.map(({ vendorGroup }) => vendorGroup) as [ + string, + string, + ]; + const providerEndpointCommitments = providers.map( + ({ endpointCommitment }) => endpointCommitment, + ) as [`0x${string}`, `0x${string}`]; + const providerOriginCommitments = providers.map( + ({ endpointOriginCommitment }) => endpointOriginCommitment, + ) as [`0x${string}`, `0x${string}`]; + return { + chainId: 1, + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads: [ + (SAFE_BLOCK_NUMBER + 12n).toString(), + (SAFE_BLOCK_NUMBER + 12n).toString(), + ], + safeBlockNumber: SAFE_BLOCK_NUMBER.toString(), + safeBlockHash: SAFE_BLOCK_HASH, + executionTrace: { + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize: 1, + hardDeadlineMs: 1_000, + maxCallsPerProvider: 128, + elapsedMs: 1, + providerCallCounts: [0, 0], + calls: [], + }, + candidates: [ + { + chainId: 1, + candidateId: parent.candidateId, + sourceAddress: DYNAMIC_FACTORY, + contractName: parent.contractName, + eventName: parent.eventName, + sourceKind: "static", + model: "classic", + releaseVersion: "classic-v3", + payloadHash: parent.payloadHash, + rawLogCommitment: `0x${"66".repeat(32)}`, + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads: [ + (SAFE_BLOCK_NUMBER + 12n).toString(), + (SAFE_BLOCK_NUMBER + 12n).toString(), + ], + safeBlockNumber: SAFE_BLOCK_NUMBER.toString(), + safeBlockHash: SAFE_BLOCK_HASH, + candidateBlockNumber: BLOCK_NUMBER.toString(), + candidateBlockHash: BLOCK_HASH, + candidateBlockTimestamp: parent.blockTimestamp, + transactionHash: parent.transactionHash, + transactionIndex: parent.transactionIndex, + receiptCommitment: `0x${"55".repeat(32)}`, + sourceCodeHash: `0x${"44".repeat(32)}`, + receiptLogOrdinal: 0, + }, + ], + coveredCandidateCount: 1, + coverage: { + fromBlockNumber: (BLOCK_NUMBER - 1n).toString(), + throughBlockNumber: BLOCK_NUMBER.toString(), + throughBlockHash: BLOCK_HASH, + throughBlockGlobalLogIndex: String(0xffff_ffff), + filterCommitment: `0x${"22".repeat(32)}`, + providerLogCommitments: [ + `0x${"33".repeat(32)}`, + `0x${"33".repeat(32)}`, + ], + }, + }; +} + +function canonicalDeploymentEvidence( + providers: readonly [ReturnType, ReturnType], + parent = dynamicParentCandidate(), + overrides: Partial = {}, +): CanonicalDynamicSourceDeploymentEvidence { + const template = dynamicTemplate(); + return { + provisionalPageId: "20000000-0000-8000-8000-000000000001", + provisionalLineageId: "20000000-0000-8000-8000-000000000002", + dynamicSourceAttestationId: + "20000000-0000-8000-8000-000000000003", + runtimeCodeEvidenceId: "20000000-0000-8000-8000-000000000004", + dynamicSourceTemplateId: template.templateId, + parentOccurrenceId: "20000000-0000-8000-8000-000000000005", + parentCandidateId: parent.candidateId, + parentBlockNumber: parent.blockNumber, + parentBlockHash: parent.blockHash, + parentBlockGlobalLogIndex: parent.blockGlobalLogIndex, + parentTransactionHash: parent.transactionHash, + parentTransactionIndex: parent.transactionIndex, + parentSourceAddress: parent.sourceAddress, + parentContractName: parent.contractName, + parentEventName: parent.eventName, + parentPayloadHash: parent.payloadHash, + parentRawLogCommitment: keccak256( + encodeAbiParameters( + [{ type: "address" }, { type: "bytes32[]" }, { type: "bytes" }], + [parent.sourceAddress, parent.orderedTopics, parent.rawData], + ), + ), + canonicalStatusHistoryId: + "20000000-0000-8000-8000-000000000006", + safeHeadObservationId: "20000000-0000-8000-8000-000000000007", + blockEvidenceId: "20000000-0000-8000-8000-000000000008", + reorgGeneration: template.database.reorgGeneration, + envioProviderDeploymentId: template.database.envioProviderDeploymentId, + rpcProviderDeploymentIds: template.database.rpcProviderDeploymentIds, + providerIdentities: providers.map(({ identity }) => identity) as [ + string, + string, + ], + providerVendorGroups: providers.map(({ vendorGroup }) => vendorGroup) as [ + string, + string, + ], + providerEndpointCommitments: providers.map( + ({ endpointCommitment }) => endpointCommitment, + ) as [`0x${string}`, `0x${string}`], + providerOriginCommitments: providers.map( + ({ endpointOriginCommitment }) => endpointOriginCommitment, + ) as [`0x${string}`, `0x${string}`], + ...overrides, + }; +} + +function candidate(): EnvioCandidate { + return { + candidateId: `1:${BLOCK_HASH}:${TRANSACTION_HASH}:7`, + chainId: 1, + blockNumber: BLOCK_NUMBER.toString(), + blockHash: BLOCK_HASH, + blockTimestamp: "1785480000", + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + blockGlobalLogIndex: 7, + sourceAddress: SOURCE, + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + releaseHint: { model: "classic", releaseVersion: "classic-v2" }, + orderedTopics: [...TOPICS] as `0x${string}`[], + rawData: DATA, + decodedPayload: JSON.parse(canonicalPayloadJson(ARGS)), + payloadHash: PAYLOAD_HASH, + }; +} + +function canonicalLog(overrides: Partial = {}): CandidateRpcLog { + return { + address: SOURCE, + blockNumber: BLOCK_NUMBER, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logIndex: 7, + removed: false, + topics: TOPICS, + data: DATA, + ...overrides, + }; +} + +function client(logs: readonly CandidateRpcLog[]): CandidateRpcClient { + const filteredLogs = ({ + addresses, + topic0, + fromBlock, + toBlock, + }: Parameters>[0]) => + logs.filter( + (log) => + log.blockNumber !== null && + log.blockNumber >= fromBlock && + log.blockNumber <= toBlock && + addresses.includes(log.address as `0x${string}`) && + log.topics[0] !== undefined && + topic0.includes(log.topics[0] as `0x${string}`), + ); + return { + getChainId: async () => 1, + getBlockNumber: async () => SAFE_BLOCK_NUMBER + 12n, + getBlock: async ({ blockNumber }) => + blockNumber === SAFE_BLOCK_NUMBER + ? { + number: SAFE_BLOCK_NUMBER, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480100n, + } + : { + number: BLOCK_NUMBER, + hash: BLOCK_HASH, + timestamp: 1785480000n, + }, + getTransactionReceipt: async () => ({ + status: "success", + blockNumber: BLOCK_NUMBER, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logs: [canonicalLog()], + }), + getBytecode: async () => "0x6001600055", + getLogs: vi.fn(async (filter) => filteredLogs(filter)), + getLogsBatch: vi.fn(async ({ requests }) => + requests.map(filteredLogs), + ), + }; +} + +function provider(identity: string, rpcClient: CandidateRpcClient) { + const endpointOrigin = `https://${identity}.example`; + return { + identity, + vendorGroup: identity.split("-")[0]!, + endpointCommitment: rpcProviderCommitment("endpoint", endpointOrigin), + endpointOriginCommitment: rpcProviderCommitment("origin", endpointOrigin), + client: rpcClient, + }; +} + +function verify(firstLogs: readonly CandidateRpcLog[], secondLogs = firstLogs) { + return verifyEnvioCandidateWindowWithDualRpc({ + candidates: [candidate()], + cursor: { + blockNumber: (BLOCK_NUMBER - 1n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 7, + candidateId: candidate().candidateId, + }, + providers: [ + provider("alchemy-mainnet", client(firstLogs)), + provider("quicknode-mainnet", client(secondLogs)), + ], + rpcPolicy: { maxAttempts: 1 }, + }); +} + +describe("dual-RPC exact Envio window coverage", () => { + it("accepts only an exact independent getLogs match", async () => { + await expect(verify([canonicalLog()])).resolves.toMatchObject({ + coveredCandidateCount: 1, + coverage: { + fromBlockNumber: (BLOCK_NUMBER - 1n).toString(), + throughBlockNumber: BLOCK_NUMBER.toString(), + throughBlockHash: BLOCK_HASH, + }, + }); + }); + + it("rejects a provider omission", async () => { + await expect(verify([], [canonicalLog()])).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + }); + + it("rejects an authorized RPC log when Envio returns an empty window", async () => { + await expect( + verifyEnvioCandidateWindowWithDualRpc({ + candidates: [], + cursor: { + blockNumber: (BLOCK_NUMBER - 1n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 4_294_967_295, + candidateId: "empty-page", + }, + providers: [ + provider("alchemy-mainnet", client([canonicalLog()])), + provider("quicknode-mainnet", client([canonicalLog()])), + ], + rpcPolicy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("rejects a terminal watermark when providers disagree on its block hash", async () => { + const first = client([]); + const second = client([]); + second.getBlock = async ({ blockNumber }) => + blockNumber === SAFE_BLOCK_NUMBER + ? { + number: SAFE_BLOCK_NUMBER, + hash: SAFE_BLOCK_HASH, + timestamp: 1785480100n, + } + : { + number: BLOCK_NUMBER, + hash: `0x${"99".repeat(32)}`, + timestamp: 1785480000n, + }; + + await expect( + verifyEnvioCandidateWindowWithDualRpc({ + candidates: [], + cursor: { + blockNumber: (BLOCK_NUMBER - 1n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 4_294_967_295, + candidateId: "empty-page", + }, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("binds a same-block dynamic child to one exact bytecode read per provider", async () => { + const first = client([canonicalLog()]); + const second = client([canonicalLog()]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const parentCandidate = dynamicParentCandidate(); + const parentEvidence = dynamicParentEvidence(providers); + first.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + second.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + + await expect( + verifyDynamicRuntimeAtBlockWithDualRpc({ + parentCandidate, + sourceAddress: DYNAMIC_CHILD, + deploymentBlockNumber: BLOCK_NUMBER.toString(), + deploymentBlockHash: BLOCK_HASH, + template: dynamicTemplate(), + parentEvidence, + providers, + deadlineMs: 1_000, + }), + ).resolves.toMatchObject({ + parentCandidateId: parentCandidate.candidateId, + sourceAddress: DYNAMIC_CHILD, + deploymentBlockNumber: BLOCK_NUMBER.toString(), + deploymentBlockHash: BLOCK_HASH, + rawRuntimeCodeA: DYNAMIC_CHILD, + rawRuntimeCodeB: DYNAMIC_CHILD, + normalizedRuntimeCodeHashA: DYNAMIC_NORMALIZED_HASH, + normalizedRuntimeCodeHashB: DYNAMIC_NORMALIZED_HASH, + runtimeByteLengthA: "20", + runtimeByteLengthB: "20", + immutableReferencesCommitment: DYNAMIC_REFERENCES_COMMITMENT, + immutableValues: [DYNAMIC_CHILD], + reconstructedRuntimeCode: DYNAMIC_CHILD, + factoryConfigurationCommitment: DYNAMIC_CONFIGURATION, + providerCallCounts: [1, 1], + }); + expect(first.getBytecode).toHaveBeenCalledTimes(1); + expect(second.getBytecode).toHaveBeenCalledTimes(1); + expect(first.getBytecode).toHaveBeenCalledWith({ + address: DYNAMIC_CHILD, + blockHash: BLOCK_HASH, + requireCanonical: true, + }); + }); + + it("batches more than 100 dynamic runtimes while preserving exact provider binding", async () => { + const first = client([canonicalLog()]); + const second = client([canonicalLog()]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const items = Array.from({ length: 101 }, (_, index) => { + const sourceAddress = `0x${(index + 1_000) + .toString(16) + .padStart(40, "0")}` as const; + const transactionHash = `0x${(index + 1) + .toString(16) + .padStart(64, "0")}` as const; + const parentCandidate = { + ...dynamicParentCandidate(), + candidateId: `1:${BLOCK_HASH}:${transactionHash}:${index}`, + transactionHash, + transactionIndex: index, + blockGlobalLogIndex: index, + decodedPayload: { + ...dynamicParentCandidate().decodedPayload, + vault: sourceAddress, + }, + } satisfies EnvioCandidate; + return { + parentCandidate, + sourceAddress, + deploymentBlockNumber: BLOCK_NUMBER.toString(), + deploymentBlockHash: BLOCK_HASH, + template: dynamicTemplate(), + } as const; + }); + const baseParentEvidence = dynamicParentEvidence(providers); + const baseCandidateEvidence = baseParentEvidence.candidates[0]!; + const parentEvidence: DualRpcCandidateWindowEvidence = { + ...baseParentEvidence, + executionTrace: { + ...baseParentEvidence.executionTrace, + candidateBatchSize: items.length, + }, + candidates: items.map(({ parentCandidate }) => ({ + ...baseCandidateEvidence, + candidateId: parentCandidate.candidateId, + payloadHash: parentCandidate.payloadHash, + transactionHash: parentCandidate.transactionHash, + transactionIndex: parentCandidate.transactionIndex, + receiptLogOrdinal: parentCandidate.blockGlobalLogIndex, + })), + coveredCandidateCount: items.length, + }; + const installBatchReader = (rpcClient: CandidateRpcClient) => { + rpcClient.getBytecode = vi.fn(rpcClient.getBytecode); + rpcClient.getBytecodes = vi.fn(async ( + { requests }: + Parameters>[0], + ) => requests.map(({ address }) => address)); + }; + installBatchReader(first); + installBatchReader(second); + + const observations = await verifyDynamicRuntimesAtBlockWithDualRpc({ + items, + parentEvidence, + providers, + deadlineMs: 2_000, + }); + + expect(observations).toHaveLength(101); + expect(observations.map(({ sourceAddress }) => sourceAddress)).toEqual( + items.map(({ sourceAddress }) => sourceAddress), + ); + for (const rpcClient of [first, second]) { + expect(rpcClient.getBytecodes).toHaveBeenCalledTimes(6); + expect(rpcClient.getBytecode).not.toHaveBeenCalled(); + const requests = vi.mocked(rpcClient.getBytecodes!).mock.calls.flatMap( + ([input]) => input.requests, + ); + expect(requests).toHaveLength(101); + expect( + vi.mocked(rpcClient.getBytecodes!).mock.calls.map( + ([input]) => input.requests.length, + ), + ).toEqual([20, 20, 20, 20, 20, 1]); + expect(requests).toEqual( + items.map(({ sourceAddress }) => ({ + address: sourceAddress, + blockHash: BLOCK_HASH, + requireCanonical: true, + })), + ); + } + + const divergentRuntime = + "0xffffffffffffffffffffffffffffffffffffffff" as const; + second.getBytecodes = vi.fn(async ( + { requests }: + Parameters>[0], + ) => requests.map(({ address }) => + address === items[0]!.sourceAddress ? divergentRuntime : address, + )); + await expect( + verifyDynamicRuntimesAtBlockWithDualRpc({ + items, + parentEvidence, + providers, + deadlineMs: 2_000, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "dynamic-runtime-code-agreement" }, + }); + expect(second.getBytecodes).toHaveBeenCalledTimes(6); + }); + + it("fails closed when providers disagree on the dynamic child bytecode", async () => { + const first = client([canonicalLog()]); + const second = client([canonicalLog()]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const parentCandidate = dynamicParentCandidate(); + const parentEvidence = dynamicParentEvidence(providers); + first.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + second.getBytecode = vi.fn( + async () => "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" as const, + ); + + await expect( + verifyDynamicRuntimeAtBlockWithDualRpc({ + parentCandidate, + sourceAddress: DYNAMIC_CHILD, + deploymentBlockNumber: BLOCK_NUMBER.toString(), + deploymentBlockHash: BLOCK_HASH, + template: dynamicTemplate(), + parentEvidence, + providers, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + expect(first.getBytecode).toHaveBeenCalledTimes(1); + expect(second.getBytecode).toHaveBeenCalledTimes(1); + }); + + it("re-verifies a staged reward vault at the exact canonical launch block", async () => { + const first = client([]); + const second = client([]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const parentCandidate = dynamicParentCandidate(); + const launchCandidate = dynamicLaunchCandidate(); + first.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + second.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + + await expect( + verifyDynamicRuntimeAtActivationWithDualRpc({ + parentCandidate, + launchCandidate, + sourceAddress: DYNAMIC_CHILD, + template: dynamicTemplate(), + canonicalDeployment: canonicalDeploymentEvidence( + providers, + parentCandidate, + ), + activationEvidence: dynamicActivationEvidence( + providers, + launchCandidate, + ), + providers, + deadlineMs: 1_000, + }), + ).resolves.toMatchObject({ + parentCandidateId: parentCandidate.candidateId, + launchCandidateId: launchCandidate.candidateId, + sourceAddress: DYNAMIC_CHILD, + deploymentBlockNumber: BLOCK_NUMBER.toString(), + deploymentBlockHash: BLOCK_HASH, + activationBlockNumber: ACTIVATION_BLOCK_NUMBER.toString(), + activationBlockHash: ACTIVATION_BLOCK_HASH, + activationBlockGlobalLogIndex: 4, + rawRuntimeCodeA: DYNAMIC_CHILD, + rawRuntimeCodeB: DYNAMIC_CHILD, + factoryConfigurationCommitment: DYNAMIC_CONFIGURATION, + providerCallCounts: [1, 1], + }); + expect(first.getBytecode).toHaveBeenCalledTimes(1); + expect(second.getBytecode).toHaveBeenCalledTimes(1); + expect(first.getBytecode).toHaveBeenCalledWith({ + address: DYNAMIC_CHILD, + blockHash: ACTIVATION_BLOCK_HASH, + requireCanonical: true, + }); + }); + + it("rejects a launch whose reward configuration does not bind the staged parent", async () => { + const first = client([]); + const second = client([]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const launchCandidate = dynamicLaunchCandidate({ + rewardConfigurationHash: `0x${"b1".repeat(32)}`, + }); + first.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + second.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + + await expect( + verifyDynamicRuntimeAtActivationWithDualRpc({ + parentCandidate: dynamicParentCandidate(), + launchCandidate, + sourceAddress: DYNAMIC_CHILD, + template: dynamicTemplate(), + canonicalDeployment: canonicalDeploymentEvidence( + providers, + dynamicParentCandidate(), + ), + activationEvidence: dynamicActivationEvidence( + providers, + launchCandidate, + ), + providers, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + expect(first.getBytecode).not.toHaveBeenCalled(); + expect(second.getBytecode).not.toHaveBeenCalled(); + }); + + it("rejects a canonical parent whose raw log no longer decodes to its payload", async () => { + const first = client([]); + const second = client([]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const originalParent = dynamicParentCandidate(); + const maliciousParent = { + ...originalParent, + rawData: "0x00" as const, + }; + const launchCandidate = dynamicLaunchCandidate(); + first.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + second.getBytecode = vi.fn(async () => DYNAMIC_CHILD); + + await expect( + verifyDynamicRuntimeAtActivationWithDualRpc({ + parentCandidate: maliciousParent, + launchCandidate, + sourceAddress: DYNAMIC_CHILD, + template: dynamicTemplate(), + canonicalDeployment: canonicalDeploymentEvidence( + providers, + maliciousParent, + ), + activationEvidence: dynamicActivationEvidence( + providers, + launchCandidate, + ), + providers, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + expect(first.getBytecode).not.toHaveBeenCalled(); + expect(second.getBytecode).not.toHaveBeenCalled(); + }); + + it("rejects a same-height parent from a replacement fork", async () => { + const first = client([]); + const second = client([]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const originalParent = dynamicParentCandidate(); + const parentCandidate = { + ...originalParent, + candidateId: + `1:${BLOCK_HASH}:${originalParent.transactionHash}:3` as const, + blockNumber: ACTIVATION_BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 3, + }; + const launchCandidate = dynamicLaunchCandidate(); + + await expect( + verifyDynamicRuntimeAtActivationWithDualRpc({ + parentCandidate, + launchCandidate, + sourceAddress: DYNAMIC_CHILD, + template: dynamicTemplate(), + canonicalDeployment: canonicalDeploymentEvidence( + providers, + parentCandidate, + ), + activationEvidence: dynamicActivationEvidence( + providers, + launchCandidate, + ), + providers, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + }); + + it("rejects a parent that appears after the launch in the same block", async () => { + const first = client([]); + const second = client([]); + const providers = [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ] as const; + const originalParent = dynamicParentCandidate(); + const parentCandidate = { + ...originalParent, + candidateId: + `1:${ACTIVATION_BLOCK_HASH}:${originalParent.transactionHash}:5` as const, + blockNumber: ACTIVATION_BLOCK_NUMBER.toString(), + blockHash: ACTIVATION_BLOCK_HASH, + blockGlobalLogIndex: 5, + }; + const launchCandidate = dynamicLaunchCandidate(); + + await expect( + verifyDynamicRuntimeAtActivationWithDualRpc({ + parentCandidate, + launchCandidate, + sourceAddress: DYNAMIC_CHILD, + template: dynamicTemplate(), + canonicalDeployment: canonicalDeploymentEvidence( + providers, + parentCandidate, + ), + activationEvidence: dynamicActivationEvidence( + providers, + launchCandidate, + ), + providers, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + }); + + it("rejects an extra manifest event omitted by Envio", async () => { + const extra = canonicalLog({ + transactionHash: `0x${"88".repeat(32)}`, + transactionIndex: 3, + logIndex: 6, + }); + await expect( + verify([extra, canonicalLog()], [extra, canonicalLog()]), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("rejects provider disagreement even when one side matches Envio", async () => { + await expect( + verify( + [canonicalLog()], + [canonicalLog({ blockHash: `0x${"99".repeat(32)}` })], + ), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("fails closed on a provider page above the exact 10,000-log cap", async () => { + const oversizedPage = Array.from( + { length: 10_001 }, + () => canonicalLog(), + ); + const first = client([canonicalLog()]); + const second = client([canonicalLog()]); + first.getLogsBatch = vi.fn(async ({ requests }) => + requests.map(() => oversizedPage), + ); + + await expect( + verifyEnvioCandidateWindowWithDualRpc({ + candidates: [candidate()], + cursor: { + blockNumber: (BLOCK_NUMBER - 1n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 7, + candidateId: candidate().candidateId, + }, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("bounds getLogs block ranges and request count", async () => { + const first = client([canonicalLog()]); + const second = client([canonicalLog()]); + await verifyEnvioCandidateWindowWithDualRpc({ + candidates: [candidate()], + cursor: { + blockNumber: (BLOCK_NUMBER - 1_200n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 7, + candidateId: candidate().candidateId, + }, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + coveragePolicy: { maximumBlockSpan: 500, maximumRequests: 8 }, + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }); + for (const rpcClient of [first, second]) { + const getLogsBatch = vi.mocked(rpcClient.getLogsBatch!); + expect(getLogsBatch).toHaveBeenCalledTimes(61); + for (const [batch] of getLogsBatch.mock.calls) { + expect(batch.requests.length).toBeLessThanOrEqual(20); + for (const request of batch.requests) { + expect(request.toBlock - request.fromBlock + 1n).toBe(1n); + expect(request.addresses.length).toBeLessThanOrEqual(512); + expect(request.topic0.length).toBeGreaterThan(0); + expect(request.topic0.length).toBeLessThanOrEqual(64); + } + } + } + }); + + it("fails closed when the provider call budget is insufficient", async () => { + const first = client([canonicalLog()]); + const second = client([canonicalLog()]); + first.getLogsBatch = undefined; + second.getLogsBatch = undefined; + await expect( + verifyEnvioCandidateWindowWithDualRpc({ + candidates: [candidate()], + cursor: { + blockNumber: (BLOCK_NUMBER - 1n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 7, + candidateId: candidate().candidateId, + }, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { maxAttempts: 1, maxProviderCalls: 5 }, + }), + ).rejects.toMatchObject({ code: "invalid_input" }); + }); + + it("does not let getLogs retries amplify past the physical call cap", async () => { + const first = client([canonicalLog()]); + const second = client([canonicalLog()]); + first.getLogsBatch = undefined; + second.getLogsBatch = undefined; + first.getLogs = vi + .fn>() + .mockRejectedValueOnce(new Error("429")) + .mockResolvedValue([canonicalLog()]); + second.getLogs = vi + .fn>() + .mockRejectedValueOnce(new Error("429")) + .mockResolvedValue([canonicalLog()]); + + await expect( + verifyEnvioCandidateWindowWithDualRpc({ + candidates: [candidate()], + cursor: { + blockNumber: (BLOCK_NUMBER - 1n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 7, + candidateId: candidate().candidateId, + }, + providers: [ + provider("alchemy-mainnet", first), + provider("quicknode-mainnet", second), + ], + rpcPolicy: { + maxAttempts: 2, + baseBackoffMs: 0, + maxCallsPerProvider: 8, + sleep: async () => undefined, + }, + }), + ).rejects.toMatchObject({ + code: "invalid_input", + }); + // Preflight accounts for the whole physical shape and rejects before a + // retryable provider call can amplify beyond the configured budget. + expect(first.getLogs).not.toHaveBeenCalled(); + expect(second.getLogs).not.toHaveBeenCalled(); + }); + + it("enforces one hard deadline across batch and coverage", async () => { + const hanging = client([canonicalLog()]); + hanging.getBlockNumber = () => new Promise(() => undefined); + const startedAt = Date.now(); + await expect( + verifyEnvioCandidateWindowWithDualRpc({ + candidates: [candidate()], + cursor: { + blockNumber: (BLOCK_NUMBER - 1n).toString(), + blockGlobalLogIndex: -1, + candidateId: "", + }, + through: { + blockNumber: BLOCK_NUMBER.toString(), + blockGlobalLogIndex: 7, + candidateId: candidate().candidateId, + }, + providers: [ + provider("alchemy-mainnet", hanging), + provider("quicknode-mainnet", client([canonicalLog()])), + ], + rpcPolicy: { + maxAttempts: 1, + deadlineMs: 20, + }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "timeout" }); + expect(Date.now() - startedAt).toBeLessThan(250); + }); +}); diff --git a/tests/data-pipeline/projector-dynamic-lineage.test.ts b/tests/data-pipeline/projector-dynamic-lineage.test.ts new file mode 100644 index 00000000..9deb712b --- /dev/null +++ b/tests/data-pipeline/projector-dynamic-lineage.test.ts @@ -0,0 +1,238 @@ +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + keccak256, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +import { + verifyEnvioCandidateBatchWithDualRpc, + type CandidateRpcClient, +} from "../../lib/data-pipeline/dual-rpc"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import type { VerifiedDynamicSourceLineage } from "../../lib/data-pipeline/projector-identities"; +import { rpcProviderCommitment } from "../../lib/data-pipeline/rpc-provider-commitments"; +import { runtimeBytecodeEvidence } from "../../lib/data-pipeline/runtime-bytecode"; + +const SOURCE = "0x4cfe000000000000000000000000000000000001" as const; +const FACTORY = "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a" as const; +const BLOCK = 25_639_597n; +const BLOCK_HASH = `0x${"11".repeat(32)}` as const; +const SAFE_BLOCK = BLOCK + 3n; +const SAFE_BLOCK_HASH = `0x${"22".repeat(32)}` as const; +const TRANSACTION_HASH = `0x${"33".repeat(32)}` as const; +const TOPIC = `0x${"44".repeat(32)}` as const; +const DATA = "0x1234" as const; +const RUNTIME = "0x6001aabb6000" as const; +const IMMUTABLE_REFERENCES = [{ start: 2, length: 2 }] as const; +const RUNTIME_EVIDENCE = runtimeBytecodeEvidence({ + runtimeBytecode: RUNTIME, + expectedByteLength: 6, + immutableReferences: IMMUTABLE_REFERENCES, +}); + +function candidate(): EnvioCandidate { + return { + candidateId: `1:${BLOCK_HASH}:${TRANSACTION_HASH}:4`, + chainId: 1, + blockNumber: BLOCK.toString(), + blockHash: BLOCK_HASH, + blockTimestamp: "1785481000", + transactionHash: TRANSACTION_HASH, + transactionIndex: 1, + blockGlobalLogIndex: 4, + sourceAddress: SOURCE, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + releaseHint: { model: "unresolved", releaseVersion: "unresolved" }, + orderedTopics: [TOPIC], + rawData: DATA, + decodedPayload: {}, + payloadHash: keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [[TOPIC], DATA], + ), + ), + }; +} + +function lineage( + overrides: Partial = {}, +): VerifiedDynamicSourceLineage { + return { + attestationId: "10000000-0000-4000-8000-000000000001", + sourceAddress: SOURCE, + contractName: "ClassicV3RewardVault", + model: "classic", + releaseVersion: "classic-v3", + factoryAddress: FACTORY, + factoryContractName: "ClassicV3RewardVaultFactory", + factoryCandidateId: + `1:0x${"55".repeat(32)}:0x${"66".repeat(32)}:3`, + factoryBlockNumber: (BLOCK - 1n).toString(), + factoryBlockGlobalLogIndex: "3", + activationCandidateId: + `1:${BLOCK_HASH}:0x${"77".repeat(32)}:3`, + activationBlockNumber: BLOCK.toString(), + activationBlockHash: BLOCK_HASH, + activationBlockGlobalLogIndex: "3", + expectedExactRuntimeCodeHash: RUNTIME_EVIDENCE.exactRuntimeCodeHash, + expectedNormalizedRuntimeCodeHash: + RUNTIME_EVIDENCE.normalizedRuntimeCodeHash, + expectedImmutableReferencesCommitment: + RUNTIME_EVIDENCE.immutableReferencesCommitment, + expectedRuntimeByteLength: "6", + immutableReferences: IMMUTABLE_REFERENCES, + ...overrides, + }; +} + +function client(runtime: Hex = RUNTIME): CandidateRpcClient { + return { + getChainId: async () => 1, + getBlockNumber: async () => SAFE_BLOCK + 12n, + getBlock: async ({ blockNumber }) => + blockNumber === SAFE_BLOCK + ? { + number: SAFE_BLOCK, + hash: SAFE_BLOCK_HASH, + timestamp: 1785481100n, + } + : { + number: BLOCK, + hash: BLOCK_HASH, + timestamp: 1785481000n, + }, + getTransactionReceipt: async () => ({ + status: "success", + blockNumber: BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 1, + logs: [ + { + address: SOURCE, + blockNumber: BLOCK, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 1, + logIndex: 4, + removed: false, + topics: [TOPIC], + data: DATA, + }, + ], + }), + getBytecode: async () => runtime, + }; +} + +function provider(identity: string, rpcClient = client()) { + const endpoint = `https://${identity}.example`; + return { + identity, + vendorGroup: identity.split("-")[0]!, + endpointCommitment: rpcProviderCommitment("endpoint", endpoint), + endpointOriginCommitment: rpcProviderCommitment("origin", endpoint), + client: rpcClient, + }; +} + +function verify(dynamicSources?: readonly VerifiedDynamicSourceLineage[]) { + return verifyEnvioCandidateBatchWithDualRpc({ + candidates: [candidate()], + providers: [provider("alchemy-mainnet"), provider("quicknode-mainnet")], + dynamicSources, + requireDynamicLineage: true, + rpcPolicy: { maxAttempts: 1 }, + }); +} + +describe("two-phase dynamic source lineage", () => { + it("rejects an unresolved dynamic source without a prior attestation", async () => { + await expect(verify()).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + }); + + it("verifies exact runtime, normalized template, and immutable layout", async () => { + await expect(verify([lineage()])).resolves.toMatchObject({ + candidates: [ + expect.objectContaining({ + sourceKind: "dynamic-attested", + model: "classic", + releaseVersion: "classic-v3", + sourceCodeHash: RUNTIME_EVIDENCE.exactRuntimeCodeHash, + dynamicSourceAttestationId: + "10000000-0000-4000-8000-000000000001", + }), + ], + }); + }); + + it("rejects null or zero runtime commitments", async () => { + await expect( + verify([ + lineage({ + expectedExactRuntimeCodeHash: `0x${"00".repeat(32)}`, + }), + ]), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects a runtime whose immutable-normalized template differs", async () => { + const changed = "0x6001aabb6001" as const; + await expect( + verifyEnvioCandidateBatchWithDualRpc({ + candidates: [candidate()], + providers: [ + provider("alchemy-mainnet", client(changed)), + provider("quicknode-mainnet", client(changed)), + ], + dynamicSources: [lineage()], + requireDynamicLineage: true, + rpcPolicy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects a same-or-later factory parent placement", async () => { + await expect( + verify([ + lineage({ + factoryBlockNumber: BLOCK.toString(), + factoryBlockGlobalLogIndex: "4", + }), + ]), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects a same-height activation from a replacement fork", async () => { + const replacementHash = `0x${"99".repeat(32)}` as const; + await expect( + verify([ + lineage({ + activationCandidateId: + `1:${replacementHash}:0x${"77".repeat(32)}:3`, + activationBlockHash: replacementHash, + }), + ]), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); + + it("rejects a child at or before its launch activation boundary", async () => { + await expect( + verify([ + lineage({ + activationCandidateId: + `1:${BLOCK_HASH}:0x${"77".repeat(32)}:4`, + activationBlockGlobalLogIndex: "4", + }), + ]), + ).rejects.toMatchObject({ code: "validation_failed" }); + }); +}); diff --git a/tests/data-pipeline/projector-envio-window.test.ts b/tests/data-pipeline/projector-envio-window.test.ts new file mode 100644 index 00000000..4e6c2bfd --- /dev/null +++ b/tests/data-pipeline/projector-envio-window.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + encodeEventTopics, + keccak256, + parseAbiItem, + type AbiParameter, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +import { createEnvioClient } from "../../lib/data-pipeline/envio"; +import { canonicalPayloadJson } from "../../indexer/src/lib/payload-hash"; + +const EVENT = parseAbiItem( + "event MemeTokenLaunched(address indexed creator, address indexed token, bytes32 indexed poolId, address feeHook, address positionRecipient, uint256 positionTokenId, uint16 totalSwapFeeBps, bytes32 launchHash)", +); +const ARGS = { + creator: "0x1111111111111111111111111111111111111111", + token: "0x2222222222222222222222222222222222222222", + poolId: `0x${"33".repeat(32)}`, + feeHook: "0x4444444444444444444444444444444444444444", + positionRecipient: "0x5555555555555555555555555555555555555555", + positionTokenId: 42n, + totalSwapFeeBps: 100n, + launchHash: `0x${"66".repeat(32)}`, +} as const; +const TOPICS = encodeEventTopics({ + abi: [EVENT], + eventName: EVENT.name, + args: ARGS, +}) as readonly Hex[]; +const NON_INDEXED = EVENT.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, +) as readonly AbiParameter[]; +const DATA = encodeAbiParameters( + NON_INDEXED, + NON_INDEXED.map((input) => ARGS[input.name as keyof typeof ARGS]), +); +const PAYLOAD_HASH = keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [TOPICS, DATA], + ), +); + +function row(blockNumber: string, logIndex: number) { + const blockHash = `0x${BigInt(blockNumber).toString(16).padStart(64, "0")}`; + const transactionHash = `0x${BigInt(logIndex + 1).toString(16).padStart(64, "0")}`; + return { + id: `1:${blockHash}:${transactionHash}:${logIndex}`, + downstreamLogicalId: null, + receiptLogOrdinal: null, + chainId: 1, + blockNumber, + blockHash, + blockTimestamp: "1785480000", + transactionHash, + transactionIndex: "0", + blockGlobalLogIndex: String(logIndex), + sourceAddress: "0xd240d06f8586eb799f20056054e5b527405e6bad", + contractName: "ClassicV2Launcher", + eventName: "MemeTokenLaunched", + model: "classic", + releaseVersion: "classic-v2", + topics: TOPICS, + data: DATA, + decodedPayload: canonicalPayloadJson(ARGS), + payloadHash: PAYLOAD_HASH, + }; +} + +describe("Envio frozen projector windows", () => { + it("pins an inclusive upper block bound into the GraphQL request", async () => { + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(body.query).toContain("blockNumber: { _lte: $throughBlock }"); + expect(body.variables).toEqual({ + afterBlock: "25650000", + afterLogIndex: "3", + afterCandidateId: `1:0x${"11".repeat(32)}:0x${"22".repeat(32)}:3`, + throughBlock: "25650100", + first: 25, + }); + return new Response( + JSON.stringify({ data: { ChainEvent: [row("25650001", 4)] } }), + { status: 200, headers: { "content-type": "application/json" } }, + ); + }); + const client = createEnvioClient({ + endpoint: "https://indexer.example/graphql", + fetcher, + }); + + await expect( + client.readCandidatesWindow({ + cursor: { + blockNumber: "25650000", + blockGlobalLogIndex: 3, + candidateId: `1:0x${"11".repeat(32)}:0x${"22".repeat(32)}:3`, + }, + throughBlock: "25650100", + }), + ).resolves.toHaveLength(1); + }); + + it("rejects an upstream row beyond the frozen upper bound", async () => { + const fetcher = vi.fn(async () => + new Response( + JSON.stringify({ data: { ChainEvent: [row("25650101", 4)] } }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + const client = createEnvioClient({ + endpoint: "https://indexer.example/graphql", + fetcher, + }); + + await expect( + client.readCandidatesWindow({ + cursor: { + blockNumber: "25650000", + blockGlobalLogIndex: -1, + candidateId: "", + }, + throughBlock: "25650100", + }), + ).rejects.toMatchObject({ + dependency: "envio", + code: "validation_failed", + }); + }); + + it("rejects a window behind its cursor before making a request", async () => { + const fetcher = vi.fn(); + const client = createEnvioClient({ + endpoint: "https://indexer.example/graphql", + fetcher, + }); + + await expect( + client.readCandidatesWindow({ + cursor: { + blockNumber: "25650101", + blockGlobalLogIndex: -1, + candidateId: "", + }, + throughBlock: "25650100", + }), + ).rejects.toMatchObject({ code: "invalid_input" }); + expect(fetcher).not.toHaveBeenCalled(); + }); + + it("preserves the full uint32 log-index range through numeric GraphQL scalars", async () => { + const maximum = 4_294_967_295; + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(body.query).toContain("$afterLogIndex: numeric!"); + expect(body.variables.afterLogIndex).toBe("4294967294"); + const fixture = row("25650100", maximum); + return new Response( + JSON.stringify({ + data: { + ChainEvent: [ + { ...fixture, blockGlobalLogIndex: "4294967295" }, + ], + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ); + }); + const client = createEnvioClient({ + endpoint: "https://indexer.example/graphql", + fetcher, + }); + await expect( + client.readCandidatesWindow({ + cursor: { + blockNumber: "25650100", + blockGlobalLogIndex: maximum - 1, + candidateId: + `1:0x${"11".repeat(32)}:0x${"22".repeat(32)}:${maximum - 1}`, + }, + throughBlock: "25650100", + }), + ).resolves.toEqual([ + expect.objectContaining({ blockGlobalLogIndex: maximum }), + ]); + }); +}); diff --git a/tests/data-pipeline/projector-fold.test.ts b/tests/data-pipeline/projector-fold.test.ts new file mode 100644 index 00000000..ee9e04f7 --- /dev/null +++ b/tests/data-pipeline/projector-fold.test.ts @@ -0,0 +1,490 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import type { DualRpcCandidateEvidence } from "../../lib/data-pipeline/dual-rpc"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import { + foldProjectorEvents, + projectorFoldManifestCoverage, + translateProjectorEvent, +} from "../../lib/data-pipeline/projector-fold"; + +const ZERO = "0x0000000000000000000000000000000000000000" as const; +const CREATOR = "0x1111111111111111111111111111111111111111" as const; +const TOKEN = "0x2222222222222222222222222222222222222222" as const; +const HOOK = "0x025a386eaa79f6067d29848fd05ccc71beab20cc" as const; +const LAUNCHER = "0xd240d06f8586eb799f20056054e5b527405e6bad" as const; +const POSITION = "0x3333333333333333333333333333333333333333" as const; +const POOL = `0x${"44".repeat(32)}` as const; +const LAUNCH_HASH = `0x${"55".repeat(32)}` as const; +const BLOCK_HASH = `0x${"66".repeat(32)}` as const; +const TX_HASH = `0x${"77".repeat(32)}` as const; +const PAYLOAD_HASH = `0x${"88".repeat(32)}` as const; +const TOPIC = `0x${"99".repeat(32)}` as const; +const V3_HOOK = "0x35fe236ea82f7cf525c9719d7df8f49f94d720cc" as const; +const V3_LAUNCHER = "0xc3bd04aac2fb2ba58efd7eb673e544e0b80de770" as const; +const V3_FACTORY = "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a" as const; +const V3_VAULT = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as const; +const CONFIGURATION = `0x${"bc".repeat(32)}` as const; + +function event( + contractName: string, + eventName: string, + decodedPayload: Record, + logIndex: number, + sourceAddress: string = contractName.includes("Hook") ? HOOK : LAUNCHER, + releaseVersion = "classic-v2", +): { candidate: EnvioCandidate; evidence: DualRpcCandidateEvidence } { + const candidate: EnvioCandidate = { + candidateId: `1:${BLOCK_HASH}:${TX_HASH}:${logIndex}`, + chainId: 1, + blockNumber: "25624131", + blockHash: BLOCK_HASH, + blockTimestamp: "1785460000", + transactionHash: TX_HASH, + transactionIndex: 3, + blockGlobalLogIndex: logIndex, + sourceAddress: sourceAddress as `0x${string}`, + contractName, + eventName, + releaseHint: { + model: releaseVersion.startsWith("classic") ? "classic" : "stock-paired", + releaseVersion, + }, + orderedTopics: [TOPIC], + rawData: "0x", + decodedPayload, + payloadHash: PAYLOAD_HASH, + }; + return { + candidate, + evidence: { + chainId: 1, + candidateId: candidate.candidateId, + sourceAddress: candidate.sourceAddress, + contractName, + eventName, + sourceKind: "static", + model: releaseVersion.startsWith("classic") ? "classic" : "stock-paired", + releaseVersion, + payloadHash: PAYLOAD_HASH, + rawLogCommitment: `0x${"aa".repeat(32)}`, + providerIdentities: ["alchemy", "quicknode"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [ + `0x${"ab".repeat(32)}`, + `0x${"ac".repeat(32)}`, + ], + providerOriginCommitments: [ + `0x${"ad".repeat(32)}`, + `0x${"ae".repeat(32)}`, + ], + providerHeads: ["25624150", "25624151"], + safeBlockNumber: "25624139", + safeBlockHash: `0x${"af".repeat(32)}`, + candidateBlockNumber: candidate.blockNumber, + candidateBlockHash: candidate.blockHash, + candidateBlockTimestamp: candidate.blockTimestamp, + transactionHash: candidate.transactionHash, + transactionIndex: candidate.transactionIndex, + receiptCommitment: `0x${"ba".repeat(32)}`, + sourceCodeHash: `0x${"bb".repeat(32)}`, + receiptLogOrdinal: logIndex, + }, + }; +} + +function classicV2LaunchEvents() { + return [ + event( + "ClassicV2Hook", + "PoolRegistered", + { + poolId: POOL, + token: TOKEN, + creator: CREATOR, + registrar: LAUNCHER, + totalSwapFeeBps: "100", + }, + 1, + ), + event( + "ClassicV2Hook", + "PoolFeeDisclosure", + { + poolId: POOL, + token: TOKEN, + buySwapFeeBps: "100", + sellSwapFeeBps: "100", + launcherFeeBps: "10", + transferTaxBps: "0", + lpFeePips: "0", + }, + 2, + ), + event( + "ClassicV2Launcher", + "MemeTokenLaunched", + { + creator: CREATOR, + token: TOKEN, + poolId: POOL, + feeHook: HOOK, + positionRecipient: POSITION, + positionTokenId: "42", + totalSwapFeeBps: "100", + launchHash: LAUNCH_HASH, + }, + 3, + ), + event( + "ClassicV2Launcher", + "MemeLiquidityConfigured", + { + token: TOKEN, + totalSupply: "1000000000000000000000000000", + tokenLiquidityAmount: "999999999999999999999999999", + lockedTokenDust: "1", + initialTick: "76000", + tickLower: "-887200", + tickUpper: "76000", + lpFeePips: "0", + launchHash: LAUNCH_HASH, + }, + 4, + ), + event( + "ClassicV2Launcher", + "MemeCreatorInitialBuy", + { + creator: CREATOR, + token: TOKEN, + poolId: POOL, + nativeAmount: "6000000000000000", + tokenAmount: "1000000", + launchHash: LAUNCH_HASH, + }, + 5, + ), + ]; +} + +function atClassicV3Block>(item: T): T { + item.candidate.blockNumber = "25639596"; + item.evidence = { ...item.evidence, candidateBlockNumber: "25639596" }; + return item; +} + +function classicV3UnlockedLaunchEvents() { + return [ + atClassicV3Block( + event( + "ClassicV3RewardVaultFactory", + "ClassicRewardVaultDeployed", + { + vault: V3_VAULT, + poolId: POOL, + feeHook: V3_HOOK, + salt: `0x${"bd".repeat(32)}`, + configurationHash: CONFIGURATION, + }, + 1, + V3_FACTORY, + "classic-v3", + ), + ), + atClassicV3Block( + event( + "ClassicV3Hook", + "PoolRegistered", + { + poolId: POOL, + token: TOKEN, + rewardVault: V3_VAULT, + registrar: V3_LAUNCHER, + buySwapFeeBps: "300", + sellSwapFeeBps: "500", + rewardConfigurationHash: CONFIGURATION, + }, + 2, + V3_HOOK, + "classic-v3", + ), + ), + atClassicV3Block( + event( + "ClassicV3Hook", + "PoolFeeDisclosure", + { + poolId: POOL, + token: TOKEN, + rewardVault: V3_VAULT, + buySwapFeeBps: "300", + sellSwapFeeBps: "500", + buyCreatorFeeBps: "290", + sellCreatorFeeBps: "490", + launcherFeeBps: "10", + transferTaxBps: "0", + lpFeePips: "0", + }, + 3, + V3_HOOK, + "classic-v3", + ), + ), + atClassicV3Block( + event( + "ClassicV3Launcher", + "MemeTokenLaunchedV2", + { + deployer: CREATOR, + token: TOKEN, + poolId: POOL, + feeHook: V3_HOOK, + rewardVault: V3_VAULT, + positionRecipient: POSITION, + positionTokenId: "42", + buySwapFeeBps: "300", + sellSwapFeeBps: "500", + rewardConfigurationHash: CONFIGURATION, + launchHash: LAUNCH_HASH, + }, + 4, + V3_LAUNCHER, + "classic-v3", + ), + ), + atClassicV3Block( + event( + "ClassicV3Launcher", + "MemeLiquidityConfiguredV2", + { + token: TOKEN, + totalSupply: "1000000000000000000000000000", + tokenLiquidityAmount: "999999999999999999999999999", + lockedTokenDust: "1", + initialTick: "76000", + tickLower: "-887200", + tickUpper: "76000", + lpFeePips: "0", + launchHash: LAUNCH_HASH, + }, + 5, + V3_LAUNCHER, + "classic-v3", + ), + ), + atClassicV3Block( + event( + "ClassicV3Launcher", + "MemeCreatorInitialBuyV2", + { + deployer: CREATOR, + token: TOKEN, + poolId: POOL, + nativeAmount: "6000000000000000", + tokenAmount: "1000000", + launchHash: LAUNCH_HASH, + }, + 6, + V3_LAUNCHER, + "classic-v3", + ), + ), + atClassicV3Block( + event( + "ClassicV3Launcher", + "MemeCreatorInitialBuyCustodyV2", + { + deployer: CREATOR, + token: TOKEN, + custody: ZERO, + mode: "0", + durationDays: "0", + cliffDays: "0", + configurationHash: `0x${"be".repeat(32)}`, + launchHash: LAUNCH_HASH, + }, + 7, + V3_LAUNCHER, + "classic-v3", + ), + ), + ]; +} + +describe("projector fold manifest", () => { + it("covers every frozen release event and excludes non-P0 models", () => { + const coverage = projectorFoldManifestCoverage(); + expect(coverage).toHaveLength(51); + expect(coverage.some((item) => /deep|adaptive/iu.test(item.contractName))).toBe(false); + expect(new Set(coverage.map((item) => `${item.contractName}:${item.eventName}`)).size).toBe(51); + }); + + it("emits exact occurrence placement and decimal fee facts", () => { + const input = event( + "ClassicV2Hook", + "NativeSwapFeesAccrued", + { + poolId: POOL, + swapSender: CREATOR, + grossNativeAmount: "900719925474099300000", + creatorFee: "9007199254740993000", + launcherFee: "900719925474099300", + }, + 9, + ); + const translated = translateProjectorEvent(input); + expect(translated.occurrence).toMatchObject({ + releaseId: "classic-v2", + modelId: "classic", + transactionIndex: "3", + receiptLogOrdinal: "9", + blockGlobalLogIndex: "9", + eventSignature: TOPIC, + }); + expect(translated.fact).toMatchObject({ + kind: "fee-accrual", + procedure: "stage_fee_accrual_fact", + values: { + grossAmount: "900719925474099300000", + creatorFee: "9007199254740993000", + launcherFee: "900719925474099300", + }, + }); + }); + + it("rejects missing, extra, non-canonical, or evidence-mismatched input", () => { + const missing = event( + "ClassicV2Hook", + "PoolRegistered", + { poolId: POOL }, + 1, + ); + expect(() => translateProjectorEvent(missing)).toThrow(/payload/iu); + + const extra = classicV2LaunchEvents()[0]!; + extra.candidate.decodedPayload = { + ...extra.candidate.decodedPayload, + unexpected: "0", + }; + expect(() => translateProjectorEvent(extra)).toThrow(/payload/iu); + + const mismatch = classicV2LaunchEvents()[0]!; + mismatch.evidence = { ...mismatch.evidence, eventName: "PoolFeeDisclosure" }; + expect(() => translateProjectorEvent(mismatch)).toThrow(/evidence/iu); + }); +}); + +describe("projector semantic fold", () => { + it("builds one complete Classic v2 launch projection from parent-first events", () => { + const result = foldProjectorEvents({ + events: classicV2LaunchEvents(), + tokenMetadata: { + [TOKEN]: { name: "Flower", symbol: "FLOWER" }, + }, + }); + expect(result.occurrences).toHaveLength(5); + expect(result.facts).toHaveLength(5); + expect(result.launches).toHaveLength(1); + expect(result.launches[0]).toMatchObject({ + releaseVersion: "classic-v2", + token: TOKEN, + creator: CREATOR, + poolId: POOL, + launchHash: LAUNCH_HASH, + tokenName: "Flower", + tokenSymbol: "FLOWER", + totalSupply: "1000000000000000000000000000", + pool: { + currency0: ZERO, + currency1: TOKEN, + poolKeyFee: "0", + tickSpacing: "200", + hook: HOOK, + }, + feeConfiguration: { + buySwapFeeBps: "100", + sellSwapFeeBps: "100", + buyCreatorFeeBps: "90", + sellCreatorFeeBps: "90", + launcherFeeBps: "10", + transferTaxBps: "0", + lpFeePips: "0", + }, + initialBuy: { + fundingAsset: ZERO, + fundingAmount: "6000000000000000", + tokenAmount: "1000000", + }, + ethFunded: true, + }); + expect(result.launches[0]!.liquidity.initialSqrtPriceX96).toMatch(/^[1-9]\d*$/u); + }); + + it("rejects child-before-parent and incomplete launch sequences", () => { + const sequence = classicV2LaunchEvents(); + expect(() => + foldProjectorEvents({ + events: [sequence[2]!, sequence[0]!, sequence[1]!, sequence[3]!, sequence[4]!], + tokenMetadata: { [TOKEN]: { name: "Flower", symbol: "FLOWER" } }, + }), + ).toThrow(/order|parent/iu); + expect(() => + foldProjectorEvents({ + events: sequence.slice(0, 4), + tokenMetadata: { [TOKEN]: { name: "Flower", symbol: "FLOWER" } }, + }), + ).toThrow(/incomplete/iu); + }); + + it("requires verified dynamic lineage before translating vault events", () => { + const input = event( + "ClassicV3RewardVault", + "CreatorFeesCheckpointed", + { + poolId: POOL, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: "10", + }, + 1, + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "classic-v3", + ); + input.candidate.releaseHint = { model: "unresolved", releaseVersion: "unresolved" }; + input.evidence = { + ...input.evidence, + sourceKind: "dynamic-unresolved", + }; + expect(() => translateProjectorEvent(input)).toThrow(/dynamic|attestation/iu); + }); + + it("requires the Classic v3 vault parent and preserves directional fees", () => { + const result = foldProjectorEvents({ + events: classicV3UnlockedLaunchEvents(), + tokenMetadata: { [TOKEN]: { name: "Directional", symbol: "DIR" } }, + }); + expect(result.launches).toHaveLength(1); + expect(result.launches[0]).toMatchObject({ + releaseVersion: "classic-v3", + rewardVault: V3_VAULT, + feeConfiguration: { + buySwapFeeBps: "300", + sellSwapFeeBps: "500", + buyCreatorFeeBps: "290", + sellCreatorFeeBps: "490", + }, + custody: { + address: ZERO, + mode: "0", + vestingSourceCandidateId: null, + }, + }); + expect(result.launches[0]!.occurrenceRoles).toContainEqual({ + sourceRole: "vault_factory", + candidateId: classicV3UnlockedLaunchEvents()[0]!.candidate.candidateId, + }); + }); +}); diff --git a/tests/data-pipeline/projector-ops-route.test.ts b/tests/data-pipeline/projector-ops-route.test.ts new file mode 100644 index 00000000..2908e629 --- /dev/null +++ b/tests/data-pipeline/projector-ops-route.test.ts @@ -0,0 +1,600 @@ +import { NextRequest } from "next/server"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + projectorRuntimeActivationState: vi.fn(), + runConfiguredProjectorCycle: vi.fn(), +})); + +vi.mock("server-only", () => ({})); + +vi.mock( + "../../lib/data-pipeline/projector-runtime-config.server", + () => ({ + projectorRuntimeActivationState: mocks.projectorRuntimeActivationState, + runConfiguredProjectorCycle: mocks.runConfiguredProjectorCycle, + }), +); + +import { + GET, + dynamic, + maxDuration, + runtime, +} from "../../app/api/ops/projector/route"; + +const SECRET = "projector-route-secret-at-least-32-bytes"; +const projections = [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +].map((releaseId) => ({ releaseId, status: "idle", pageCount: 1 })); + +function readiness( + status: "caught-up" | "progressed" | "incomplete", + snapshotBlock: string | null, + overrides: Record = {}, +) { + return { + status, + activationReady: status === "caught-up", + lagging: status !== "caught-up", + terminalSweepComplete: status === "caught-up", + stoppedForDeadline: false, + completedRounds: 1, + snapshotBlock, + ...overrides, + }; +} + +function request(token?: string, cutover = false) { + return new NextRequest("https://programmable.family/api/ops/projector", { + headers: token === undefined && !cutover + ? undefined + : { + ...(token === undefined + ? {} + : { authorization: `Bearer ${token}` }), + ...(cutover + ? { "x-programmable-cutover-mode": "raw-backfill-v1" } + : {}), + }, + }); +} + +describe("projector operations route", () => { + beforeEach(() => { + vi.stubEnv("CRON_SECRET", SECRET); + vi.stubEnv("PROGRAMMABLE_CUTOVER_BACKFILL_ACTIVE", "false"); + vi.stubEnv( + "PROGRAMMABLE_CUTOVER_OPERATOR_SECRET", + "cutover-operator-secret-at-least-32-bytes", + ); + mocks.projectorRuntimeActivationState.mockReset(); + mocks.projectorRuntimeActivationState.mockReturnValue("active"); + mocks.runConfiguredProjectorCycle.mockReset(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it("pins the long-running route to the Node runtime without caching", () => { + expect(dynamic).toBe("force-dynamic"); + expect(maxDuration).toBe(90); + expect(runtime).toBe("nodejs"); + }); + + it("returns a bounded disabled status without opening the runtime", async () => { + const info = vi.spyOn(console, "info").mockImplementation(() => undefined); + mocks.projectorRuntimeActivationState.mockReturnValue("disabled"); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ + ok: true, + status: "disabled", + readiness: { + status: "disabled", + activationReady: false, + lagging: true, + }, + }); + expect(mocks.runConfiguredProjectorCycle).not.toHaveBeenCalled(); + expect(info).toHaveBeenCalledWith( + "Programmable projector cycle completed", + expect.objectContaining({ status: "disabled" }), + ); + }); + + it("fails closed for an invalid activation value without running", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.projectorRuntimeActivationState.mockImplementation(() => { + throw new Error("invalid activation"); + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Projector cycle failed", + }); + expect(mocks.runConfiguredProjectorCycle).not.toHaveBeenCalled(); + }); + + it.each([ + undefined, + "", + "wrong-secret", + `${SECRET}x`, + ])("rejects a missing or mismatched bearer without starting the runtime", async (token) => { + const response = await GET(request(token)); + + expect(response.status).toBe(401); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ error: "Unauthorized" }); + expect(mocks.runConfiguredProjectorCycle).not.toHaveBeenCalled(); + }); + + it("rejects a matching bearer when the configured cron secret is too short", async () => { + vi.stubEnv("CRON_SECRET", "short-secret"); + + const response = await GET(request("short-secret")); + + expect(response.status).toBe(401); + expect(mocks.runConfiguredProjectorCycle).not.toHaveBeenCalled(); + }); + + it("keeps the larger ingestion-only window behind an independent cutover secret", async () => { + const cutoverSecret = "cutover-operator-secret-at-least-32-bytes"; + vi.stubEnv("PROGRAMMABLE_CUTOVER_BACKFILL_ACTIVE", "true"); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 512, + pageCount: 1, + generation: "43", + snapshotBlock: "25650512", + atomicGroupCount: 1, + }, + projections: projections.map(({ releaseId }) => ({ + releaseId, + status: "deferred", + pageCount: 0, + })), + readiness: readiness("progressed", "25650512", { + terminalSweepComplete: false, + }), + }); + + const rejected = await GET(request(SECRET, true)); + expect(rejected.status).toBe(401); + expect(mocks.runConfiguredProjectorCycle).not.toHaveBeenCalled(); + + const response = await GET(request(cutoverSecret, true)); + expect(response.status).toBe(200); + expect(mocks.runConfiguredProjectorCycle).toHaveBeenCalledWith({ + ingestionOnly: true, + preferredCandidatesPerCommit: 512, + }); + }); + + it("runs exactly one configured cycle and returns only bounded status data", async () => { + const info = vi.spyOn(console, "info").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 8, + pageCount: 2, + generation: "42", + snapshotBlock: "25650000", + }, + projections, + readiness: readiness("caught-up", "25650000"), + internalConnection: "postgres://writer:secret@db.example", + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 8, + pageCount: 2, + generation: "42", + snapshotBlock: "25650000", + }, + projections, + readiness: readiness("caught-up", "25650000"), + }); + expect(mocks.runConfiguredProjectorCycle).toHaveBeenCalledOnce(); + expect(info).toHaveBeenCalledWith( + "Programmable projector cycle completed", + { + durationMs: expect.any(Number), + ok: true, + ingestion: { + status: "committed", + candidateCount: 8, + pageCount: 2, + }, + projections: projections.map(({ releaseId, pageCount }) => ({ + releaseId, + status: "idle", + candidateCount: 0, + pageCount, + })), + readiness: readiness("caught-up", "25650000"), + }, + ); + expect(JSON.stringify(info.mock.calls)).not.toContain("postgres://"); + }); + + it("returns an exact busy status when another runtime owns the singleton", async () => { + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + status: "busy", + readiness: { + status: "busy", + activationReady: false, + lagging: true, + }, + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ + ok: true, + status: "busy", + readiness: { + status: "busy", + activationReady: false, + lagging: true, + }, + }); + }); + + it("reports staged dynamic-parent progress without a generation or release work", async () => { + const deferredProjections = projections.map(({ releaseId }) => ({ + releaseId, + status: "deferred", + pageCount: 0, + })); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "staged-dynamic-parent", + candidateCount: 1, + pageCount: 1, + snapshotBlock: "25650123", + atomicGroupCount: 1, + }, + projections: deferredProjections, + readiness: readiness("progressed", "25650123", { + terminalSweepComplete: false, + }), + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + ok: true, + ingestion: { + status: "staged-dynamic-parent", + candidateCount: 1, + pageCount: 1, + snapshotBlock: "25650123", + atomicGroupCount: 1, + }, + projections: deferredProjections, + readiness: readiness("progressed", "25650123", { + terminalSweepComplete: false, + }), + }); + }); + + it("fails closed if staged dynamic-parent progress includes a generation", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "staged-dynamic-parent", + candidateCount: 1, + pageCount: 1, + snapshotBlock: "25650123", + generation: "52", + }, + projections: projections.map(({ releaseId }) => ({ + releaseId, + status: "deferred", + pageCount: 0, + })), + readiness: readiness("progressed", "25650123", { + terminalSweepComplete: false, + }), + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Projector cycle failed", + }); + }); + + it("fails closed if deferred projections are returned without staged ingestion", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "idle", + candidateCount: 0, + pageCount: 1, + snapshotBlock: "25650123", + }, + projections: projections.map(({ releaseId }) => ({ + releaseId, + status: "deferred", + pageCount: 0, + })), + readiness: readiness("incomplete", "25650123", { + terminalSweepComplete: false, + }), + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Projector cycle failed", + }); + }); + + it("fails closed if staged ingestion claims caught-up readiness", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "staged-dynamic-parent", + candidateCount: 1, + pageCount: 1, + snapshotBlock: "25650123", + }, + projections: projections.map(({ releaseId }) => ({ + releaseId, + status: "deferred", + pageCount: 0, + })), + readiness: readiness("caught-up", "25650123"), + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Projector cycle failed", + }); + }); + + it("fails closed when the runtime returns a malformed checkpoint", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 0, + generation: "42", + snapshotBlock: "25650000", + }, + projections, + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Projector cycle failed", + }); + }); + + it("accepts the bounded eight-page runtime maximum", async () => { + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 256, + pageCount: 8, + generation: "50", + snapshotBlock: "25650100", + }, + projections: projections.map(({ releaseId }) => ({ + releaseId, + status: "committed", + projectedCandidateCount: 200, + ignoredCandidateCount: 56, + pageCount: 8, + checkpointGeneration: "50", + })), + readiness: readiness("progressed", "25650100", { + completedRounds: 8, + }), + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + ingestion: { candidateCount: 256, pageCount: 8 }, + projections: expect.arrayContaining([ + expect.objectContaining({ + projectedCandidateCount: 200, + ignoredCandidateCount: 56, + pageCount: 8, + }), + ]), + }); + }); + + it("accepts one explicitly reported atomic projection group", async () => { + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "committed-empty", + candidateCount: 0, + pageCount: 1, + generation: "51", + snapshotBlock: "25650101", + }, + projections: projections.map(({ releaseId }) => + releaseId === "classic-v3" + ? { + releaseId, + status: "committed", + projectedCandidateCount: 4_096, + ignoredCandidateCount: 0, + pageCount: 1, + atomicGroupCount: 1, + checkpointGeneration: "51", + } + : { releaseId, status: "idle", pageCount: 1 } + ), + readiness: readiness("progressed", "25650101", { + completedRounds: 1, + }), + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + projections: expect.arrayContaining([ + expect.objectContaining({ + releaseId: "classic-v3", + projectedCandidateCount: 4_096, + atomicGroupCount: 1, + }), + ]), + }); + }); + + it("never exposes more than 256 candidates from one bounded cycle", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 264, + pageCount: 2, + generation: "51", + snapshotBlock: "25650101", + }, + projections: projections.map(({ releaseId }) => + releaseId === "classic-v3" + ? { + releaseId, + status: "committed", + projectedCandidateCount: 264, + ignoredCandidateCount: 0, + pageCount: 2, + checkpointGeneration: "51", + } + : { releaseId, status: "idle", pageCount: 2 } + ), + readiness: readiness("progressed", "25650101", { + completedRounds: 2, + }), + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Projector cycle failed", + }); + }); + + it("keeps an idle but explicitly incomplete cycle non-activatable", async () => { + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "idle", + candidateCount: 0, + pageCount: 1, + snapshotBlock: "25650102", + }, + projections, + readiness: readiness("incomplete", "25650102", { + terminalSweepComplete: false, + completedRounds: 1, + }), + }); + + const response = await GET(request(SECRET)); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + readiness: { + status: "incomplete", + activationReady: false, + lagging: true, + }, + }); + }); + + it("rejects page-inconsistent aggregates", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockResolvedValue({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 257, + pageCount: 1, + generation: "50", + snapshotBlock: "25650100", + }, + projections, + }); + + const response = await GET(request(SECRET)); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: "Projector cycle failed", + }); + }); + + it("fails closed without reflecting provider or database errors", async () => { + const log = vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.runConfiguredProjectorCycle.mockRejectedValue( + new Error("postgres://writer:secret@db.example and https://rpc.example/key"), + ); + + const response = await GET(request(SECRET)); + const body = await response.text(); + + expect(response.status).toBe(503); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(body).toBe('{"error":"Projector cycle failed"}'); + expect(body).not.toContain("secret"); + expect(body).not.toContain("rpc.example"); + expect(log).toHaveBeenCalledWith( + "Programmable projector cycle failed", + expect.objectContaining({ + errorName: "Error", + durationMs: expect.any(Number), + }), + ); + }); +}); diff --git a/tests/data-pipeline/projector-projection.test.ts b/tests/data-pipeline/projector-projection.test.ts new file mode 100644 index 00000000..313ff5fd --- /dev/null +++ b/tests/data-pipeline/projector-projection.test.ts @@ -0,0 +1,540 @@ +import { describe, expect, it, vi } from "vitest"; +import { toEventSelector } from "viem"; + +vi.mock("server-only", () => ({})); + +import type { + CandidateRpcProvider, + DualRpcCandidateBatchEvidence, +} from "../../lib/data-pipeline/dual-rpc"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import { + runReleaseProjectionCycle, + type ReleaseProjectionPlan, + type ReleaseProjectionStore, +} from "../../lib/data-pipeline/projector-projection"; + +const hash = (digit: string) => `0x${digit.repeat(64)}` as `0x${string}`; +const address = (digit: string) => + `0x${digit.repeat(40)}` as `0x${string}`; +const executionTrace = (candidateBatchSize = 0) => ({ + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 48, + elapsedMs: 1, + providerCallCounts: [0, 0] as const, + calls: [], +}); + +function candidate(overrides: Partial = {}): EnvioCandidate { + const blockHash = hash("1"); + const transactionHash = hash("2"); + return { + candidateId: `1:${blockHash}:${transactionHash}:4`, + chainId: 1, + blockNumber: "100", + blockHash, + blockTimestamp: "1000", + transactionHash, + transactionIndex: 3, + blockGlobalLogIndex: 4, + sourceAddress: address("3"), + contractName: "ClassicV2Hook", + eventName: "LauncherFeesClaimed", + releaseHint: { model: "classic", releaseVersion: "classic-v2" }, + orderedTopics: [hash("4")], + rawData: "0x" as `0x${string}`, + decodedPayload: {}, + payloadHash: hash("5"), + ...overrides, + }; +} + +function plan(entries: ReleaseProjectionPlan["entries"]): ReleaseProjectionPlan { + return { + scope: { + releaseId: "classic-v2", + modelId: "classic", + sourceGroup: "core", + }, + entries, + dynamicSources: [], + knownPools: [], + lease: { generation: "1", expiresAt: "2026-07-31T12:00:00.000Z" }, + checkpoint: { + generation: "0", + reorgGeneration: "0", + blockNumber: "99", + blockHash: hash("6"), + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "", + }, + rewardVerification: null, + }; +} + +const providers = [ + { + identity: "alchemy-test", + vendorGroup: "alchemy", + endpointCommitment: hash("7"), + endpointOriginCommitment: hash("8"), + client: {}, + }, + { + identity: "quicknode-test", + vendorGroup: "quicknode", + endpointCommitment: hash("9"), + endpointOriginCommitment: hash("a"), + client: {}, + }, +] as unknown as readonly [CandidateRpcProvider, CandidateRpcProvider]; + +function emptyEvidence(): DualRpcCandidateBatchEvidence { + return { + chainId: 1, + providerIdentities: ["alchemy-test", "quicknode-test"], + providerVendorGroups: ["alchemy", "quicknode"], + providerEndpointCommitments: [hash("7"), hash("9")], + providerOriginCommitments: [hash("8"), hash("a")], + providerHeads: ["120", "121"], + safeBlockNumber: "108", + safeBlockHash: hash("b"), + candidates: [], + executionTrace: executionTrace(), + }; +} + +describe("release projection orchestrator", () => { + it("keeps provider work between the two database phases and commits ignored pages", async () => { + const expected = candidate(); + const order: string[] = []; + const commitVerifiedProjection = vi.fn(async ( + projection: Parameters< + ReleaseProjectionStore["commitVerifiedProjection"] + >[0], + ) => { + order.push("commit"); + expect(projection.ignoredCandidateIds).toEqual([expected.candidateId]); + expect(projection.fold.occurrences).toEqual([]); + return { checkpointGeneration: "1" }; + }); + const store: ReleaseProjectionStore = { + async readProjectionPlan() { + order.push("read-plan"); + return plan([ + { candidate: expected, action: "ignore", attemptCount: "0" }, + ]); + }, + commitVerifiedProjection, + }; + const result = await runReleaseProjectionCycle({ + store, + envio: { + async readCandidate() { + order.push("envio"); + return structuredClone(expected); + }, + }, + providers, + verifyBatch: async (input) => { + order.push("rpc"); + expect(input.candidates).toEqual([expected]); + return emptyEvidence(); + }, + readMetadata: async (input) => { + order.push("metadata"); + expect(input.tokens).toEqual([]); + return []; + }, + }); + + expect(order).toEqual([ + "read-plan", + "envio", + "rpc", + "metadata", + "commit", + ]); + expect(result).toEqual({ + status: "committed", + releaseId: "classic-v2", + projectedCandidateCount: 0, + ignoredCandidateCount: 1, + checkpointGeneration: "1", + batchKind: "normal", + }); + }); + + it("fails closed when the fresh Envio object differs from the stored candidate", async () => { + const expected = candidate(); + const commitVerifiedProjection = vi.fn(); + await expect( + runReleaseProjectionCycle({ + store: { + readProjectionPlan: async () => + plan([ + { candidate: expected, action: "ignore", attemptCount: "0" }, + ]), + commitVerifiedProjection, + }, + envio: { + readCandidate: async () => ({ + ...expected, + decodedPayload: { changed: true }, + }), + }, + providers, + verifyBatch: async () => emptyEvidence(), + readMetadata: async () => [], + }), + ).rejects.toMatchObject({ name: "DataPipelineError" }); + expect(commitVerifiedProjection).not.toHaveBeenCalled(); + }); + + it("rejects mixed actions inside one transaction before any provider call", async () => { + const first = candidate(); + const second = candidate({ + candidateId: `1:${first.blockHash}:${first.transactionHash}:5`, + blockGlobalLogIndex: 5, + }); + const envio = { readCandidate: vi.fn() }; + await expect( + runReleaseProjectionCycle({ + store: { + readProjectionPlan: async () => + plan([ + { candidate: first, action: "project", attemptCount: "0" }, + { candidate: second, action: "ignore", attemptCount: "0" }, + ]), + commitVerifiedProjection: vi.fn(), + }, + envio, + providers, + verifyBatch: async () => emptyEvidence(), + readMetadata: async () => [], + }), + ).rejects.toMatchObject({ name: "DataPipelineError" }); + expect(envio.readCandidate).not.toHaveBeenCalled(); + }); + + it("requires an exact-block reward snapshot before committing a reward delta", async () => { + const rewardVault = address("7"); + const alice = address("1"); + const bob = address("2"); + const poolId = hash("3"); + const configurationHash = hash("4"); + const eventSignature = toEventSelector( + "CreatorFeesCheckpointed(bytes32,uint64,uint256,uint256)", + ); + const reward = candidate({ + blockNumber: "25639601", + sourceAddress: rewardVault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + releaseHint: { model: "classic", releaseVersion: "classic-v3" }, + orderedTopics: [eventSignature], + decodedPayload: { + poolId, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: "10", + }, + }); + const rewardEvidence = { + chainId: 1 as const, + candidateId: reward.candidateId, + sourceAddress: reward.sourceAddress, + contractName: reward.contractName, + eventName: reward.eventName, + sourceKind: "dynamic-attested" as const, + model: "classic" as const, + releaseVersion: "classic-v3", + payloadHash: reward.payloadHash, + rawLogCommitment: hash("5"), + providerIdentities: ["alchemy-test", "quicknode-test"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [hash("7"), hash("9")] as const, + providerOriginCommitments: [hash("8"), hash("a")] as const, + providerHeads: ["120", "121"] as const, + safeBlockNumber: "108", + safeBlockHash: hash("b"), + candidateBlockNumber: reward.blockNumber, + candidateBlockHash: reward.blockHash, + candidateBlockTimestamp: reward.blockTimestamp, + transactionHash: reward.transactionHash, + transactionIndex: reward.transactionIndex, + receiptCommitment: hash("c"), + sourceCodeHash: hash("d"), + receiptLogOrdinal: 0, + dynamicSourceAttestationId: "80000000-0000-8000-8000-000000000001", + normalizedRuntimeCodeHash: hash("e"), + immutableReferencesCommitment: hash("f"), + runtimeByteLength: "1", + } as const; + const rewardPlan: ReleaseProjectionPlan = { + ...plan([{ candidate: reward, action: "project", attemptCount: "0" }]), + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + }, + knownPools: [{ + releaseVersion: "classic-v3", + poolId, + token: address("5"), + quoteAsset: null, + rewardVault, + }], + rewardVerification: { + model: "classic-v3", + baseline: { + vault: rewardVault, + poolId, + configurationEpoch: "1", + activeConfigurationHash: configurationHash, + allocations: [ + { + allocationIndex: 0, + beneficiary: alice, + payoutAddress: alice, + shareBps: "4000", + }, + { + allocationIndex: 1, + beneficiary: bob, + payoutAddress: bob, + shareBps: "6000", + }, + ], + balances: [ + { + account: alice, + payoutAddress: alice, + claimableAccrued: "0", + claimedTotal: "0", + }, + { + account: bob, + payoutAddress: bob, + claimableAccrued: "0", + claimedTotal: "0", + }, + ], + }, + }, + }; + const readRewardSnapshot = vi.fn(async ({ expected }) => ({ + ...expected, + model: "classic-v3" as const, + blockNumber: "25639601", + configurationHash, + totalCreatorFeesClaimed: "0", + rpcCallCount: 14, + })); + const commitVerifiedProjection = vi.fn(async (projection) => { + expect(projection.rewardSnapshot).toMatchObject({ + vault: rewardVault, + totalCreatorFeesReceived: "10", + balances: [ + { account: alice, claimableAccrued: "4" }, + { account: bob, claimableAccrued: "6" }, + ], + }); + return { checkpointGeneration: "1" }; + }); + + await runReleaseProjectionCycle({ + store: { + readProjectionPlan: async () => rewardPlan, + commitVerifiedProjection, + }, + envio: { readCandidate: async () => reward }, + providers, + verifyBatch: async () => ({ + ...emptyEvidence(), + candidates: [rewardEvidence], + executionTrace: executionTrace(1), + }), + readMetadata: async () => [], + readRewardSnapshot: readRewardSnapshot as never, + }); + + expect(readRewardSnapshot).toHaveBeenCalledWith( + expect.objectContaining({ + model: "classic-v3", + blockNumber: "25639601", + rpcPolicy: expect.objectContaining({ maxAttempts: 1 }), + }), + ); + expect(commitVerifiedProjection).toHaveBeenCalledOnce(); + }); + + it("verifies every vault touched in one reward block before one atomic commit", async () => { + const blockHash = hash("1"); + const eventSignature = toEventSelector( + "CreatorFeesCheckpointed(bytes32,uint64,uint256,uint256)", + ); + const vaults = [address("7"), address("8")] as const; + const poolIds = [hash("3"), hash("4")] as const; + const amounts = ["10", "20"] as const; + const transactions = [hash("2"), hash("c")] as const; + const rewards = vaults.map((vault, index) => + candidate({ + candidateId: + `1:${blockHash}:${transactions[index]}:${4 + index}`, + blockNumber: "25639601", + blockHash, + transactionHash: transactions[index], + transactionIndex: 3 + index, + blockGlobalLogIndex: 4 + index, + sourceAddress: vault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + releaseHint: { model: "classic", releaseVersion: "classic-v3" }, + orderedTopics: [eventSignature], + decodedPayload: { + poolId: poolIds[index], + configurationEpoch: "1", + amount: amounts[index], + totalCreatorFeesReceived: amounts[index], + }, + }), + ); + const rewardEvidence = rewards.map((reward, index) => ({ + chainId: 1 as const, + candidateId: reward.candidateId, + sourceAddress: reward.sourceAddress, + contractName: reward.contractName, + eventName: reward.eventName, + sourceKind: "dynamic-attested" as const, + model: "classic" as const, + releaseVersion: "classic-v3", + payloadHash: reward.payloadHash, + rawLogCommitment: hash("5"), + providerIdentities: ["alchemy-test", "quicknode-test"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [hash("7"), hash("9")] as const, + providerOriginCommitments: [hash("8"), hash("a")] as const, + providerHeads: ["120", "121"] as const, + safeBlockNumber: "108", + safeBlockHash: hash("b"), + candidateBlockNumber: reward.blockNumber, + candidateBlockHash: reward.blockHash, + candidateBlockTimestamp: reward.blockTimestamp, + transactionHash: reward.transactionHash, + transactionIndex: reward.transactionIndex, + receiptCommitment: hash(index === 0 ? "c" : "d"), + sourceCodeHash: hash("d"), + receiptLogOrdinal: 0, + dynamicSourceAttestationId: + `80000000-0000-8000-8000-00000000000${index + 1}`, + normalizedRuntimeCodeHash: hash("e"), + immutableReferencesCommitment: hash("f"), + runtimeByteLength: "1", + })); + const alice = address("1"); + const rewardPlan: ReleaseProjectionPlan = { + ...plan(rewards.map((reward) => ({ + candidate: reward, + action: "project" as const, + attemptCount: "0", + }))), + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + }, + knownPools: vaults.map((rewardVault, index) => ({ + releaseVersion: "classic-v3", + poolId: poolIds[index], + token: address(index === 0 ? "5" : "6"), + quoteAsset: null, + rewardVault, + })), + rewardVerification: null, + rewardVerifications: vaults.map((vault, index) => ({ + model: "classic-v3" as const, + baseline: { + vault, + poolId: poolIds[index], + configurationEpoch: "1", + activeConfigurationHash: hash(index === 0 ? "4" : "5"), + allocations: [{ + allocationIndex: 0, + beneficiary: alice, + payoutAddress: alice, + shareBps: "10000", + }], + balances: [{ + account: alice, + payoutAddress: alice, + claimableAccrued: "0", + claimedTotal: "0", + }], + }, + })), + batchKind: "reward-block", + }; + const readRewardSnapshot = vi.fn(async (input: { + expected: { vault: string; totalCreatorFeesReceived: string }; + blockNumber: string; + blockHash: string; + }) => ({ + ...input.expected, + model: "classic-v3" as const, + blockNumber: input.blockNumber, + blockHash: input.blockHash, + configurationHash: hash("4"), + totalCreatorFeesClaimed: "0", + rpcCallCount: 10, + })); + const commitVerifiedProjection = vi.fn(async ( + projection: Parameters< + ReleaseProjectionStore["commitVerifiedProjection"] + >[0], + ) => { + expect(projection.rewardSnapshots).toHaveLength(2); + expect( + projection.rewardSnapshots?.map((snapshot) => [ + snapshot.vault, + snapshot.totalCreatorFeesReceived, + ]), + ).toEqual([ + [vaults[0], "10"], + [vaults[1], "20"], + ]); + expect(projection.rewardEvidence).toHaveLength(2); + return { checkpointGeneration: "1" }; + }); + + await expect( + runReleaseProjectionCycle({ + store: { + readProjectionPlan: async () => rewardPlan, + commitVerifiedProjection, + }, + envio: { + readCandidate: async (candidateId) => + rewards.find((reward) => reward.candidateId === candidateId) ?? null, + }, + providers, + verifyBatch: async () => ({ + ...emptyEvidence(), + candidates: rewardEvidence, + executionTrace: executionTrace(2), + }), + readMetadata: async () => [], + readRewardSnapshot: readRewardSnapshot as never, + }), + ).resolves.toMatchObject({ + status: "committed", + batchKind: "reward-block", + projectedCandidateCount: 2, + }); + expect(readRewardSnapshot).toHaveBeenCalledTimes(2); + expect(commitVerifiedProjection).toHaveBeenCalledOnce(); + }); +}); diff --git a/tests/data-pipeline/projector-reorg.test.ts b/tests/data-pipeline/projector-reorg.test.ts new file mode 100644 index 00000000..e81a0cae --- /dev/null +++ b/tests/data-pipeline/projector-reorg.test.ts @@ -0,0 +1,425 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + buildEnvioCursorRecoveryPlan, + findCanonicalAncestorWithDualRpc, + type ReorgHistoryAncestor, +} from "../../lib/data-pipeline/projector-reorg"; +import type { + CandidateRpcBlock, + CandidateRpcClient, + CandidateRpcProvider, +} from "../../lib/data-pipeline/dual-rpc"; +import { rpcProviderCommitment } from "../../lib/data-pipeline/rpc-provider-commitments"; + +const HASH_100 = `0x${"10".repeat(32)}` as const; +const HASH_90 = `0x${"09".repeat(32)}` as const; +const HASH_80 = `0x${"08".repeat(32)}` as const; +const ORPHAN = `0x${"ff".repeat(32)}` as const; + +function ancestor( + generation: string, + blockNumber: string, + blockHash: `0x${string}`, +): ReorgHistoryAncestor { + return { + kind: "history", + historyGeneration: generation, + blockNumber, + blockHash, + blockGlobalLogIndex: 7, + candidateId: `1:${blockHash}:0x${"aa".repeat(32)}:7`, + }; +} + +function client( + blocks: Readonly>, +): CandidateRpcClient { + return { + getChainId: vi.fn(async () => 1), + getBlockNumber: vi.fn(async () => 120n), + getBlock: vi.fn(async ({ blockNumber }) => + blocks[blockNumber.toString()] ?? { + number: blockNumber, + hash: ORPHAN, + timestamp: 1_785_480_000n + blockNumber, + }, + ), + getTransactionReceipt: vi.fn(), + getBytecode: vi.fn(), + }; +} + +function provider( + identity: "alchemy-mainnet" | "quicknode-mainnet", + rpcClient: CandidateRpcClient, +): CandidateRpcProvider { + const origin = `https://${identity}.example`; + return { + identity, + vendorGroup: identity.split("-")[0]!, + endpointCommitment: rpcProviderCommitment("endpoint", origin), + endpointOriginCommitment: rpcProviderCommitment("origin", origin), + client: rpcClient, + }; +} + +function block( + number: bigint, + hash: `0x${string}` | null, +): CandidateRpcBlock { + return { number, hash, timestamp: 1_785_480_000n + number }; +} + +function providers( + first: Readonly>, + second = first, +) { + return [ + provider("alchemy-mainnet", client(first)), + provider("quicknode-mainnet", client(second)), + ] as const; +} + +describe("projector reorg recovery", () => { + it("selects the newest history generation both providers prove canonical", async () => { + const pair = providers({ + "100": block(100n, ORPHAN), + "90": block(90n, HASH_90), + }); + await expect( + findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ + ancestor("5", "100", HASH_100), + ancestor("4", "90", HASH_90), + ], + genesis: { + kind: "genesis", + historyGeneration: "0", + genesisPointId: "70000000-0000-0000-0000-000000000008", + blockNumber: "80", + blockHash: HASH_80, + blockGlobalLogIndex: null, + candidateId: null, + }, + policy: { maxAttempts: 1 }, + }), + ).resolves.toMatchObject({ + kind: "history", + historyGeneration: "4", + blockNumber: "90", + blockHash: HASH_90, + checkedDepth: 2, + providerBlockHashes: [HASH_90, HASH_90], + }); + expect(pair[0].client.getBlock).toHaveBeenCalledTimes(4); + expect(pair[1].client.getBlock).toHaveBeenCalledTimes(4); + }); + + it("rejects an ancestor proof when the agreed safe head changes during the search", async () => { + const changedSafeHash = `0x${"77".repeat(32)}` as const; + const driftingClient = () => { + const value = client({}); + let safeReads = 0; + value.getBlock = vi.fn(async ({ blockNumber }) => { + if (blockNumber === 108n) { + safeReads += 1; + return block( + 108n, + safeReads === 1 ? ORPHAN : changedSafeHash, + ); + } + if (blockNumber === 90n) return block(90n, HASH_90); + return block(blockNumber, ORPHAN); + }); + return value; + }; + + await expect( + findCanonicalAncestorWithDualRpc({ + providers: [ + provider("alchemy-mainnet", driftingClient()), + provider("quicknode-mainnet", driftingClient()), + ], + ancestors: [ancestor("4", "90", HASH_90)], + policy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "reorg-safe-head-changed" }, + }); + }); + + it("fails closed immediately when providers disagree", async () => { + const pair = providers( + { "100": block(100n, HASH_100) }, + { "100": block(100n, ORPHAN) }, + ); + await expect( + findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ancestor("5", "100", HASH_100)], + policy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("rejects a null provider block hash instead of treating it as an orphan", async () => { + const pair = providers({ "100": block(100n, null) }); + await expect( + findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ancestor("5", "100", HASH_100)], + policy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("uses the registered generation-zero genesis only after history is exhausted", async () => { + const pair = providers({ + "100": block(100n, ORPHAN), + "80": block(80n, HASH_80), + }); + const target = await findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ancestor("5", "100", HASH_100)], + genesis: { + kind: "genesis", + historyGeneration: "0", + genesisPointId: "70000000-0000-0000-0000-000000000008", + blockNumber: "80", + blockHash: HASH_80, + blockGlobalLogIndex: null, + candidateId: null, + }, + policy: { maxAttempts: 1 }, + }); + expect(target).toMatchObject({ + kind: "genesis", + historyGeneration: "0", + blockNumber: "80", + blockGlobalLogIndex: null, + candidateId: null, + checkedDepth: 2, + }); + + expect( + buildEnvioCursorRecoveryPlan({ + expectedGeneration: "5", + currentReorgGeneration: "2", + target, + }), + ).toEqual({ + action: "rewind-and-replay", + expectedGeneration: "5", + nextGeneration: "6", + targetHistoryGeneration: "0", + targetBlockNumber: "80", + targetBlockHash: HASH_80, + targetBlockGlobalLogIndex: null, + targetCandidateId: null, + genesisPointId: "70000000-0000-0000-0000-000000000008", + expectedReorgGeneration: "2", + nextReorgGeneration: "3", + providerIdentities: ["alchemy-mainnet", "quicknode-mainnet"], + providerEndpointCommitments: [ + pair[0].endpointCommitment, + pair[1].endpointCommitment, + ], + providerOriginCommitments: [ + pair[0].endpointOriginCommitment, + pair[1].endpointOriginCommitment, + ], + providerBlockHashes: [HASH_80, HASH_80], + providerBlockTimestamps: ["1785480080", "1785480080"], + providerChainIds: [1, 1], + providerHeads: ["120", "120"], + finalityDepth: "12", + safeBlockNumber: "108", + safeBlockHash: ORPHAN, + providerSafeBlockHashes: [ORPHAN, ORPHAN], + checkedDepth: 2, + }); + }); + + it("supports a prior generation that already rewound to genesis", async () => { + const pair = providers({ "80": block(80n, HASH_80) }); + const target = await findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ + { + kind: "history", + historyGeneration: "4", + blockNumber: "80", + blockHash: HASH_80, + blockGlobalLogIndex: null, + candidateId: null, + }, + ], + policy: { maxAttempts: 1 }, + }); + expect( + buildEnvioCursorRecoveryPlan({ + expectedGeneration: "5", + currentReorgGeneration: "2", + target, + }), + ).toMatchObject({ + targetHistoryGeneration: "4", + targetBlockGlobalLogIndex: null, + targetCandidateId: null, + genesisPointId: null, + }); + }); + + it("fails when neither history nor genesis is canonical", async () => { + await expect( + findCanonicalAncestorWithDualRpc({ + providers: providers({ + "100": block(100n, ORPHAN), + "80": block(80n, ORPHAN), + }), + ancestors: [ancestor("5", "100", HASH_100)], + genesis: { + kind: "genesis", + historyGeneration: "0", + genesisPointId: "70000000-0000-0000-0000-000000000008", + blockNumber: "80", + blockHash: HASH_80, + blockGlobalLogIndex: null, + candidateId: null, + }, + policy: { maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("never selects an agreed block above the shared 12-block safe head", async () => { + const pair = providers({ + "109": block(109n, HASH_100), + "80": block(80n, HASH_80), + }); + await expect( + findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ancestor("5", "109", HASH_100)], + genesis: { + kind: "genesis", + historyGeneration: "0", + genesisPointId: "70000000-0000-0000-0000-000000000008", + blockNumber: "80", + blockHash: HASH_80, + blockGlobalLogIndex: null, + candidateId: null, + }, + policy: { maxAttempts: 1 }, + }), + ).resolves.toMatchObject({ + kind: "genesis", + safeBlockNumber: "108", + checkedDepth: 2, + }); + expect(pair[0].client.getBlock).not.toHaveBeenCalledWith({ + blockNumber: 109n, + }); + }); + + it("enforces depth and provider-call budgets before an unbounded scan", async () => { + const pair = providers({}); + await expect( + findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ + ancestor("5", "100", HASH_100), + ancestor("4", "90", HASH_90), + ], + policy: { maximumDepth: 1, maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "invalid_input" }); + + await expect( + findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ + ancestor("5", "100", HASH_100), + ancestor("4", "90", HASH_90), + ], + policy: { maxProviderCalls: 2, maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "validation_failed" }); + }); + + it("applies one hard deadline to the complete search", async () => { + const hanging = client({}); + hanging.getChainId = vi.fn( + () => new Promise(() => undefined), + ); + await expect( + findCanonicalAncestorWithDualRpc({ + providers: [ + provider("alchemy-mainnet", hanging), + provider("quicknode-mainnet", client({})), + ], + ancestors: [ancestor("5", "100", HASH_100)], + policy: { deadlineMs: 20, maxAttempts: 1 }, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "timeout" }); + }); + + it("rejects stale or non-descending data before making network calls", async () => { + const pair = providers({}); + await expect( + findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ + ancestor("4", "90", HASH_90), + ancestor("5", "100", HASH_100), + ], + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "invalid_input" }); + expect(pair[0].client.getChainId).not.toHaveBeenCalled(); + }); + + it("keeps the recovery plan pure, immutable and CAS-bound", async () => { + const pair = providers({ "90": block(90n, HASH_90) }); + const target = await findCanonicalAncestorWithDualRpc({ + providers: pair, + ancestors: [ancestor("4", "90", HASH_90)], + policy: { maxAttempts: 1 }, + }); + const plan = buildEnvioCursorRecoveryPlan({ + expectedGeneration: "5", + currentReorgGeneration: "8", + target, + }); + expect(plan).toMatchObject({ + expectedGeneration: "5", + nextGeneration: "6", + targetHistoryGeneration: "4", + expectedReorgGeneration: "8", + nextReorgGeneration: "9", + }); + expect(Object.isFrozen(plan)).toBe(true); + expect(() => + buildEnvioCursorRecoveryPlan({ + expectedGeneration: "4", + currentReorgGeneration: "8", + target, + }), + ).toThrow(); + expect(() => + buildEnvioCursorRecoveryPlan({ + expectedGeneration: "5", + currentReorgGeneration: "8", + target: { + ...target, + providerBlockHashes: [ORPHAN, ORPHAN], + }, + }), + ).toThrow(); + }); +}); diff --git a/tests/data-pipeline/projector-reward-dual-rpc.test.ts b/tests/data-pipeline/projector-reward-dual-rpc.test.ts new file mode 100644 index 00000000..b97b3b7e --- /dev/null +++ b/tests/data-pipeline/projector-reward-dual-rpc.test.ts @@ -0,0 +1,1256 @@ +import { describe, expect, it, vi } from "vitest"; +import { + encodeAbiParameters, + encodeEventTopics, + getContractAddress, + keccak256, + parseAbiItem, + toFunctionSelector, + type AbiParameter, + type Hex, +} from "viem"; + +vi.mock("server-only", () => ({})); + +import { verifyClassicV3ActivationModel } from "../../lib/data-pipeline/classic-v3-activation-model"; +import { + readDualRpcInitialRewardConfiguration, + readDualRpcRewardSnapshot, + type CandidateRpcClient, + type DualRpcCandidateWindowEvidence, + type CandidateRpcProvider, + type CandidateRpcRewardSnapshot, +} from "../../lib/data-pipeline/dual-rpc"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import type { CanonicalDynamicSourceDeploymentEvidence } from "../../lib/data-pipeline/projector-dynamic-activation"; +import type { ProjectorRewardSnapshot } from "../../lib/data-pipeline/projector-reward-fold"; +import { + expectedRewardRpcCallCount, + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1, +} from "../../lib/data-pipeline/projector-reward-rpc-contract"; +import { immutableReferencesCommitment } from "../../lib/data-pipeline/runtime-bytecode"; +import { canonicalPayloadJson } from "../../indexer/src/lib/payload-hash"; + +const address = (digit: string) => + `0x${digit.repeat(40)}` as `0x${string}`; +const bytes32 = (digit: string) => + `0x${digit.repeat(64)}` as `0x${string}`; +const alice = address("1"); +const bob = address("2"); +const poolId = bytes32("3"); +const configurationHash = bytes32("4"); +const blockHash = bytes32("9"); +const rewardVaultFactory = address("f"); +const rewardVaultCtoAuthority = address("d"); +const launchToken = address("a"); +const launchDeployer = address("b"); +const rewardVaultSalt = keccak256( + encodeAbiParameters( + [{ type: "string" }, { type: "address" }, { type: "address" }], + [ + "programmable.classic-reward-vault.v1", + launchToken, + launchDeployer, + ], + ), +); +const rewardVaultInitCodeHash = bytes32("2"); +const vault = getContractAddress({ + bytecodeHash: rewardVaultInitCodeHash, + from: rewardVaultFactory, + opcode: "CREATE2", + salt: rewardVaultSalt, +}).toLowerCase() as `0x${string}`; +const factoryEvent = parseAbiItem( + "event ClassicRewardVaultDeployed(address indexed vault, bytes32 indexed poolId, address indexed feeHook, bytes32 salt, bytes32 configurationHash)", +); +const classicV3Launcher = + "0xc3bd04aac2fb2ba58efd7eb673e544e0b80de770" as const; + +function classicActiveHash( + epoch: bigint, + beneficiaries: readonly `0x${string}`[], + shares: readonly number[], + factoryConfigurationHash: `0x${string}`, +) { + return keccak256( + encodeAbiParameters( + [ + { type: "uint256" }, + { type: "address" }, + { type: "bytes32" }, + { type: "uint64" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [1n, vault, factoryConfigurationHash, epoch, [...beneficiaries], [...shares]], + ), + ); +} + +function seedCandidate(input: { + eventName: string; + sourceAddress: `0x${string}`; + logIndex: number; + decodedPayload: Record; + contractName?: string; +}): EnvioCandidate { + const transactionHash = `0x${input.logIndex.toString(16).padStart(64, "0")}` as const; + const candidate: EnvioCandidate = { + candidateId: `1:${blockHash}:${transactionHash}:${input.logIndex}`, + chainId: 1, + blockNumber: "100", + blockHash, + blockTimestamp: "1000", + transactionHash, + transactionIndex: 0, + blockGlobalLogIndex: input.logIndex, + sourceAddress: input.sourceAddress, + contractName: + input.contractName ?? "ClassicV3RewardVault", + eventName: input.eventName, + releaseHint: { model: "classic", releaseVersion: "classic-v3" }, + orderedTopics: [bytes32("a")], + rawData: "0x", + decodedPayload: input.decodedPayload, + payloadHash: bytes32("b"), + }; + if (input.eventName !== "ClassicRewardVaultDeployed") return candidate; + const args = { + vault: input.decodedPayload.vault, + poolId: input.decodedPayload.poolId, + feeHook: input.decodedPayload.feeHook, + salt: input.decodedPayload.salt ?? rewardVaultSalt, + configurationHash: input.decodedPayload.configurationHash, + }; + const topics = encodeEventTopics({ + abi: [factoryEvent], + eventName: factoryEvent.name, + args: args as never, + }) as readonly Hex[]; + const nonIndexed = factoryEvent.inputs.filter( + (parameter) => !("indexed" in parameter) || parameter.indexed !== true, + ) as readonly AbiParameter[]; + const data = encodeAbiParameters( + nonIndexed, + nonIndexed.map((parameter) => args[parameter.name as keyof typeof args]), + ); + return { + ...candidate, + orderedTopics: [...topics], + rawData: data, + decodedPayload: JSON.parse(canonicalPayloadJson(args)), + payloadHash: keccak256( + encodeAbiParameters( + [{ type: "bytes32[]" }, { type: "bytes" }], + [topics, data], + ), + ), + }; +} + +const expected: ProjectorRewardSnapshot = Object.freeze({ + vault, + poolId, + configurationEpoch: "2", + activeConfigurationHash: configurationHash, + totalCreatorFeesReceived: "13", + allocations: Object.freeze([ + Object.freeze({ + allocationIndex: 0, + beneficiary: bob, + payoutAddress: bob, + shareBps: "4000", + }), + Object.freeze({ + allocationIndex: 1, + beneficiary: bob, + payoutAddress: bob, + shareBps: "6000", + }), + ]), + balances: Object.freeze([ + Object.freeze({ + account: alice, + payoutAddress: alice, + claimableAccrued: "0", + claimedTotal: "4", + }), + Object.freeze({ + account: bob, + payoutAddress: bob, + claimableAccrued: "9", + claimedTotal: "0", + }), + ]), + snapshotSourceOccurrenceId: "80000000-0000-8000-8000-000000000001", +}); + +function result( + overrides: Partial = {}, +): CandidateRpcRewardSnapshot { + return { + model: "classic-v3", + vault, + blockNumber: "100", + blockHash, + poolId, + configurationEpoch: "2", + configurationHash, + totalCreatorFeesReceived: "13", + totalCreatorFeesClaimed: "4", + beneficiaryCount: "2", + allocations: expected.allocations, + balances: expected.balances, + rpcCallCount: 14, + ...overrides, + }; +} + +function expectedWithAccountCount(count: number): ProjectorRewardSnapshot { + if (!Number.isSafeInteger(count) || count < 2) throw new Error("account-count"); + const balances = [ + ...expected.balances, + ...Array.from({ length: count - 2 }, (_value, index) => { + const account = `0x${(index + 16).toString(16).padStart(40, "0")}` as const; + return Object.freeze({ + account, + payoutAddress: account, + claimableAccrued: "0", + claimedTotal: "0", + }); + }), + ].sort((left, right) => left.account.localeCompare(right.account)); + return Object.freeze({ ...expected, balances: Object.freeze(balances) }); +} + +function resultFor( + snapshot: ProjectorRewardSnapshot, + balanceAccounts: readonly `0x${string}`[], + overrides: Partial = {}, +): CandidateRpcRewardSnapshot { + const balancesByAccount = new Map( + snapshot.balances.map((balance) => [balance.account, balance]), + ); + return result({ + allocations: snapshot.allocations, + balances: balanceAccounts.map((account) => { + const balance = balancesByAccount.get(account); + if (!balance) throw new Error("missing-test-balance"); + return balance; + }), + rpcCallCount: expectedRewardRpcCallCount( + "classic-v3", + snapshot.allocations.length, + balanceAccounts.length, + ), + ...overrides, + }); +} + +function provider( + identity: string, + vendorGroup: string, + readRewardSnapshot: CandidateRpcClient["readRewardSnapshot"], + factoryConfigurationHash: `0x${string}` = configurationHash, + factoryOverrides: Record = {}, +): CandidateRpcProvider { + return { + identity, + vendorGroup, + endpointCommitment: bytes32(vendorGroup === "alchemy" ? "5" : "6"), + endpointOriginCommitment: bytes32(vendorGroup === "alchemy" ? "7" : "8"), + client: { + readRewardSnapshot, + readClassicRewardFactorySnapshot: async ( + request: Parameters< + NonNullable< + CandidateRpcClient["readClassicRewardFactorySnapshot"] + > + >[0], + ) => ({ + factory: request.factory, + vault: request.vault, + blockNumber: request.blockNumber.toString(), + blockHash: request.blockHash, + configurationHash: factoryConfigurationHash, + ctoAuthority: rewardVaultCtoAuthority, + initCodeHash: rewardVaultInitCodeHash, + predictedVault: vault, + rpcCallCount: 4, + ...factoryOverrides, + }), + getBytecode: async () => vault, + } as unknown as CandidateRpcClient, + }; +} + +function dynamicTemplate( + parentFactoryAddress: `0x${string}`, +) { + const hash = bytes32("6"); + const references = [{ start: 0, length: 20 }] as const; + return { + templateId: "30000000-0000-4000-8000-000000000001", + contractName: "ClassicV3RewardVault" as const, + model: "classic" as const, + releaseVersion: "classic-v3" as const, + parentFactoryAddress, + parentFactoryContractName: "ClassicV3RewardVaultFactory" as const, + parentFactoryBindingId: "30000000-0000-4000-8000-000000000002", + parentFactoryBindingCommitment: hash, + parentSourceRole: "vault_factory", + factoryEventName: "ClassicRewardVaultDeployed" as const, + deployedAddressField: "vault" as const, + deployedSourceRole: "reward_vault" as const, + deployedArtifactCreationCodeCommitment: hash, + expectedExactRuntimeCodeHash: null, + expectedNormalizedRuntimeCodeHash: keccak256(`0x${"00".repeat(20)}`), + expectedImmutableReferencesCommitment: + immutableReferencesCommitment(references, 20), + expectedRuntimeByteLength: "20", + immutableReferences: references, + immutableBindingSpec: { + factoryConfigurationField: "configurationHash", + bindings: [ + { + ordinal: "0", + offset: "0", + length: "20", + source: "deployed_address", + encoding: "address", + }, + ], + }, + immutableBindingCommitment: hash, + abiEventSetCommitment: hash, + templateCommitment: hash, + database: { + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "canonical-events", + }, + epochId: "30000000-0000-4000-8000-000000000003", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: + "30000000-0000-4000-8000-000000000004", + rpcProviderDeploymentIds: [ + "30000000-0000-4000-8000-000000000005", + "30000000-0000-4000-8000-000000000006", + ] as const, + }, + }; +} + +function canonicalDeploymentEvidence( + parent: EnvioCandidate, + providers: readonly [CandidateRpcProvider, CandidateRpcProvider], + overrides: Partial = {}, +): CanonicalDynamicSourceDeploymentEvidence { + const template = dynamicTemplate(parent.sourceAddress); + return { + provisionalPageId: "40000000-0000-4000-8000-000000000001", + provisionalLineageId: "40000000-0000-4000-8000-000000000002", + dynamicSourceAttestationId: + "40000000-0000-4000-8000-000000000003", + runtimeCodeEvidenceId: "40000000-0000-4000-8000-000000000004", + dynamicSourceTemplateId: template.templateId, + parentOccurrenceId: "40000000-0000-4000-8000-000000000005", + parentCandidateId: parent.candidateId, + parentBlockNumber: parent.blockNumber, + parentBlockHash: parent.blockHash, + parentBlockGlobalLogIndex: parent.blockGlobalLogIndex, + parentTransactionHash: parent.transactionHash, + parentTransactionIndex: parent.transactionIndex, + parentSourceAddress: parent.sourceAddress, + parentContractName: parent.contractName, + parentEventName: parent.eventName, + parentPayloadHash: parent.payloadHash, + parentRawLogCommitment: keccak256( + encodeAbiParameters( + [{ type: "address" }, { type: "bytes32[]" }, { type: "bytes" }], + [parent.sourceAddress, parent.orderedTopics, parent.rawData], + ), + ), + canonicalStatusHistoryId: + "40000000-0000-4000-8000-000000000006", + safeHeadObservationId: "40000000-0000-4000-8000-000000000007", + blockEvidenceId: "40000000-0000-4000-8000-000000000008", + reorgGeneration: template.database.reorgGeneration, + envioProviderDeploymentId: template.database.envioProviderDeploymentId, + rpcProviderDeploymentIds: template.database.rpcProviderDeploymentIds, + providerIdentities: providers.map(({ identity }) => identity) as [ + string, + string, + ], + providerVendorGroups: providers.map(({ vendorGroup }) => vendorGroup) as [ + string, + string, + ], + providerEndpointCommitments: providers.map( + ({ endpointCommitment }) => endpointCommitment, + ) as [`0x${string}`, `0x${string}`], + providerOriginCommitments: providers.map( + ({ endpointOriginCommitment }) => endpointOriginCommitment, + ) as [`0x${string}`, `0x${string}`], + ...overrides, + }; +} + +function candidateBatchEvidence( + candidates: readonly EnvioCandidate[], + providers: readonly [CandidateRpcProvider, CandidateRpcProvider], +): DualRpcCandidateWindowEvidence { + const providerIdentities = providers.map(({ identity }) => identity) as [string, string]; + const providerVendorGroups = providers.map(({ vendorGroup }) => vendorGroup) as [string, string]; + const providerEndpointCommitments = providers.map( + ({ endpointCommitment }) => endpointCommitment, + ) as [`0x${string}`, `0x${string}`]; + const providerOriginCommitments = providers.map( + ({ endpointOriginCommitment }) => endpointOriginCommitment, + ) as [`0x${string}`, `0x${string}`]; + return { + chainId: 1, + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads: ["200", "200"], + safeBlockNumber: "188", + safeBlockHash: bytes32("e"), + candidates: candidates.map((candidate) => ({ + chainId: 1, + candidateId: candidate.candidateId, + sourceAddress: candidate.sourceAddress, + contractName: candidate.contractName, + eventName: candidate.eventName, + sourceKind: candidate.contractName === "ClassicV3RewardVault" + ? "dynamic-attested" + : "static", + model: "classic", + releaseVersion: "classic-v3", + payloadHash: candidate.payloadHash, + rawLogCommitment: bytes32("d"), + providerIdentities, + providerVendorGroups, + providerEndpointCommitments, + providerOriginCommitments, + providerHeads: ["200", "200"], + safeBlockNumber: "188", + safeBlockHash: bytes32("e"), + candidateBlockNumber: candidate.blockNumber, + candidateBlockHash: candidate.blockHash, + candidateBlockTimestamp: candidate.blockTimestamp, + transactionHash: candidate.transactionHash, + transactionIndex: candidate.transactionIndex, + receiptCommitment: bytes32("c"), + sourceCodeHash: bytes32("b"), + receiptLogOrdinal: candidate.blockGlobalLogIndex, + })), + executionTrace: { + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize: candidates.length, + hardDeadlineMs: 100, + maxCallsPerProvider: 128, + elapsedMs: 1, + providerCallCounts: [1, 1], + calls: [], + }, + coveredCandidateCount: candidates.length, + coverage: { + fromBlockNumber: "99", + throughBlockNumber: "100", + throughBlockHash: blockHash, + throughBlockGlobalLogIndex: "4294967295", + filterCommitment: bytes32("a"), + providerLogCommitments: [bytes32("b"), bytes32("b")], + }, + }; +} + +describe("dual-RPC exact-block reward snapshots", () => { + it("freezes every selector and keeps the worst case under the provider cap", () => { + const signatures = Object.values( + PROJECTOR_REWARD_RPC_CALL_CONTRACT_V1.models, + ).flatMap((model) => [ + ...model.fixed, + ...model.perAllocation, + ...model.perBalanceAccount, + ]); + expect( + signatures.map(({ signature, selector, blockTag }) => ({ + signature, + selector, + blockTag, + })), + ).toEqual( + signatures.map(({ signature }) => ({ + signature, + selector: toFunctionSelector(signature), + blockTag: "eip-1898-canonical-block-hash", + })), + ); + expect([...new Set(signatures.map(({ signature }) => signature))].sort()) + .toEqual([ + "activeConfigurationHash()", + "beneficiaryAt(uint256)", + "beneficiaryCount()", + "claimable(address)", + "claimedBy(address)", + "configurationEpoch()", + "configurationHash()", + "payoutAddressOf(address)", + "poolId()", + "shareBpsAt(uint256)", + "shareBpsOf(address)", + "totalCreatorFeesClaimed()", + "totalCreatorFeesReceived()", + ]); + expect(expectedRewardRpcCallCount("classic-v3", 5, 48)).toBe(112); + expect(expectedRewardRpcCallCount("stock-paired", 8, 8)).toBe(45); + }); + + it("accepts duplicate Classic allocation wallets and verifies historical balances", async () => { + const left = vi.fn(async () => result()); + const right = vi.fn(async () => result()); + + const snapshot = await readDualRpcRewardSnapshot({ + model: "classic-v3", + expected, + blockNumber: "100", + blockHash, + providers: [ + provider("alchemy-reward", "alchemy", left), + provider("quicknode-reward", "quicknode", right), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }); + + expect(snapshot.allocations.map(({ beneficiary }) => beneficiary)).toEqual([ + bob, + bob, + ]); + expect(snapshot.balances.map(({ account }) => account)).toEqual([ + alice, + bob, + ]); + expect(left).toHaveBeenCalledWith({ + model: "classic-v3", + vault, + blockNumber: 100n, + blockHash, + balanceAccounts: [alice, bob], + }); + expect(right).toHaveBeenCalledOnce(); + }); + + it("fails closed on provider disagreement or an uncommitted hidden call", async () => { + const baseProviders = (right: CandidateRpcRewardSnapshot) => [ + provider("alchemy-reward", "alchemy", async () => result()), + provider("quicknode-reward", "quicknode", async () => right), + ] as const; + + await expect( + readDualRpcRewardSnapshot({ + model: "classic-v3", + expected, + blockNumber: "100", + blockHash, + providers: baseProviders(result({ totalCreatorFeesClaimed: "5" })), + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }), + ).rejects.toThrow(); + + await expect( + readDualRpcRewardSnapshot({ + model: "classic-v3", + expected, + blockNumber: "100", + blockHash, + providers: baseProviders(result({ rpcCallCount: 15 })), + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }), + ).rejects.toThrow(); + }); + + it("binds every call to the canonical block hash and rejects a replacement block", async () => { + const replacementHash = bytes32("a"); + const left = vi.fn(async (request) => { + expect(request.blockHash).toBe(blockHash); + return result(); + }); + const right = vi.fn(async () => result({ blockHash: replacementHash })); + + await expect( + readDualRpcRewardSnapshot({ + model: "classic-v3", + expected, + blockNumber: "100", + blockHash, + providers: [ + provider("alchemy-reward", "alchemy", left), + provider("quicknode-reward", "quicknode", right), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }), + ).rejects.toThrow(); + expect(left).toHaveBeenCalledOnce(); + expect(right).toHaveBeenCalledOnce(); + }); + + it("verifies a vault with more than 48 historical accounts without rereading unchanged history", async () => { + const historical = Array.from({ length: 58 }, (_value, index) => { + const account = `0x${(index + 16).toString(16).padStart(40, "0")}` as const; + return Object.freeze({ + account, + payoutAddress: account, + claimableAccrued: "0", + claimedTotal: "0", + }); + }); + const fullBalances = Object.freeze([ + ...expected.balances, + ...historical, + ].sort((left, right) => left.account.localeCompare(right.account))); + const fullExpected = Object.freeze({ ...expected, balances: fullBalances }); + const baseline = Object.freeze({ + vault, + poolId, + configurationEpoch: "2", + activeConfigurationHash: configurationHash, + allocations: expected.allocations, + balances: fullBalances, + }); + const read = vi.fn(async ({ balanceAccounts }) => { + expect(balanceAccounts).toEqual([bob]); + return result({ + balances: expected.balances.filter(({ account }) => account === bob), + rpcCallCount: 12, + }); + }); + + const snapshot = await readDualRpcRewardSnapshot({ + model: "classic-v3", + baseline, + expected: fullExpected, + blockNumber: "100", + blockHash, + providers: [ + provider("alchemy-reward", "alchemy", read), + provider("quicknode-reward", "quicknode", read), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }); + + expect(snapshot.balances).toHaveLength(60); + expect(snapshot.verificationAccounts).toEqual([bob]); + expect(read).toHaveBeenCalledTimes(2); + }); + + it.each([48, 49, 127, 128, 129])( + "verifies %i freshly changed accounts as one ordered exact-block chunk set", + async (accountCount) => { + const fullExpected = expectedWithAccountCount(accountCount); + const read = vi.fn(async ({ balanceAccounts, blockHash: requestedHash }) => { + expect(requestedHash).toBe(blockHash); + return resultFor(fullExpected, balanceAccounts); + }); + + const snapshot = await readDualRpcRewardSnapshot({ + model: "classic-v3", + expected: fullExpected, + blockNumber: "100", + blockHash, + providers: [ + provider("alchemy-reward", "alchemy", read), + provider("quicknode-reward", "quicknode", read), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + }); + + const expectedChunkSizes = Array.from( + { length: Math.ceil(accountCount / 48) }, + (_value, index) => Math.min(48, accountCount - index * 48), + ); + expect(snapshot.verificationAccounts).toEqual( + fullExpected.balances.map(({ account }) => account), + ); + expect(snapshot.chunks.map((chunk) => chunk.chunkIndex)).toEqual( + expectedChunkSizes.map((_size, index) => index), + ); + expect(snapshot.chunks.map((chunk) => chunk.verificationAccounts.length)) + .toEqual(expectedChunkSizes); + expect(snapshot.chunks.flatMap((chunk) => chunk.verificationAccounts)) + .toEqual(snapshot.verificationAccounts); + expect(read).toHaveBeenCalledTimes(expectedChunkSizes.length * 2); + }, + ); + + it("fails closed when a later chunk disagrees across providers", async () => { + const fullExpected = expectedWithAccountCount(49); + const left = vi.fn(async ({ balanceAccounts }) => + resultFor(fullExpected, balanceAccounts)); + let rightChunk = 0; + const right = vi.fn(async ({ balanceAccounts }) => { + const currentChunk = rightChunk; + rightChunk += 1; + return resultFor( + fullExpected, + balanceAccounts, + currentChunk === 1 ? { totalCreatorFeesClaimed: "5" } : {}, + ); + }); + + await expect(readDualRpcRewardSnapshot({ + model: "classic-v3", + expected: fullExpected, + blockNumber: "100", + blockHash, + providers: [ + provider("alchemy-reward", "alchemy", left), + provider("quicknode-reward", "quicknode", right), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + })).rejects.toThrow(); + expect(left).toHaveBeenCalledTimes(2); + expect(right).toHaveBeenCalledTimes(2); + }); + + it.each(["missing", "duplicate", "reordered"] as const)( + "fails closed on a %s account response inside a chunk", + async (mutation) => { + const fullExpected = expectedWithAccountCount(50); + let chunkIndex = 0; + const malformed = vi.fn(async ({ balanceAccounts }) => { + const canonical = resultFor(fullExpected, balanceAccounts); + const currentChunk = chunkIndex; + chunkIndex += 1; + if (currentChunk !== 1) return canonical; + const balances = [...(canonical.balances as readonly Record[])]; + if (mutation === "missing") balances.pop(); + if (mutation === "duplicate") balances[0] = balances[1]!; + if (mutation === "reordered") balances.reverse(); + return { ...canonical, balances }; + }); + const sound = vi.fn(async ({ balanceAccounts }) => + resultFor(fullExpected, balanceAccounts)); + + await expect(readDualRpcRewardSnapshot({ + model: "classic-v3", + expected: fullExpected, + blockNumber: "100", + blockHash, + providers: [ + provider("alchemy-reward", "alchemy", malformed), + provider("quicknode-reward", "quicknode", sound), + ], + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 128 }, + })).rejects.toThrow(); + }, + ); + + it("reconstructs the immutable seed across a same-block payout change", async () => { + const carol = address("3"); + const factoryConfigurationHash = bytes32("c"); + const initialActiveHash = classicActiveHash( + 1n, + [alice, bob], + [4000, 6000], + factoryConfigurationHash, + ); + const currentActiveHash = classicActiveHash( + 2n, + [carol, bob], + [4000, 6000], + factoryConfigurationHash, + ); + const parent = seedCandidate({ + eventName: "ClassicRewardVaultDeployed", + sourceAddress: address("f"), + contractName: "ClassicV3RewardVaultFactory", + logIndex: 4, + decodedPayload: { + vault, + poolId, + feeHook: address("e"), + configurationHash: factoryConfigurationHash, + }, + }); + const launch = seedCandidate({ + eventName: "MemeTokenLaunchedV2", + sourceAddress: classicV3Launcher, + contractName: "ClassicV3Launcher", + logIndex: 5, + decodedPayload: { + deployer: launchDeployer, + token: launchToken, + rewardVault: vault, + poolId, + feeHook: address("e"), + rewardConfigurationHash: factoryConfigurationHash, + }, + }); + const payout = seedCandidate({ + eventName: "PayoutWalletChanged", + sourceAddress: vault, + logIndex: 6, + decodedPayload: { + poolId, + allocationIndex: "0", + previousPayoutWallet: alice, + newPayoutWallet: carol, + shareBps: "4000", + configurationEpoch: "2", + activeConfigurationHash: currentActiveHash, + effectiveTotalCreatorFeesReceived: "0", + }, + }); + const raw = result({ + configurationEpoch: "2", + configurationHash: currentActiveHash, + totalCreatorFeesReceived: "0", + totalCreatorFeesClaimed: "0", + allocations: [ + { allocationIndex: 0, beneficiary: carol, payoutAddress: carol, shareBps: "4000" }, + { allocationIndex: 1, beneficiary: bob, payoutAddress: bob, shareBps: "6000" }, + ], + balances: [ + { account: vault, payoutAddress: vault, claimableAccrued: "0", claimedTotal: "0" }, + ], + rpcCallCount: expectedRewardRpcCallCount("classic-v3", 2, 1), + }); + const left = vi.fn(async () => raw); + const right = vi.fn(async () => raw); + const providers = [ + provider("alchemy-seed", "alchemy", left, factoryConfigurationHash), + provider( + "quicknode-seed", + "quicknode", + right, + factoryConfigurationHash, + ), + ] as const; + + const verified = await readDualRpcInitialRewardConfiguration({ + parentCandidate: parent, + launchCandidate: launch, + sameBlockVaultEvents: [payout], + candidateEvidence: candidateBatchEvidence([launch, payout], providers), + canonicalDeployment: canonicalDeploymentEvidence(parent, providers), + template: dynamicTemplate(parent.sourceAddress), + providers, + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 32 }, + }); + expect(verified).toMatchObject({ + vault, + poolId, + deploymentBlockNumber: "100", + deploymentBlockHash: blockHash, + activationBlockNumber: "100", + activationBlockHash: blockHash, + activationBlockGlobalLogIndex: 5, + coveredRewardCandidateIds: [payout.candidateId], + factory: rewardVaultFactory, + salt: rewardVaultSalt, + factoryConfigurationHash, + providerFactoryConfigurationHashes: [ + factoryConfigurationHash, + factoryConfigurationHash, + ], + providerInitCodeHashes: [ + rewardVaultInitCodeHash, + rewardVaultInitCodeHash, + ], + providerPredictedVaults: [vault, vault], + locallyPredictedVault: vault, + ctoAuthority: rewardVaultCtoAuthority, + constructorArgumentsCommitment: keccak256( + encodeAbiParameters( + [ + { type: "address" }, + { type: "bytes32" }, + { type: "address" }, + { type: "address[]" }, + { type: "uint16[]" }, + ], + [ + address("e"), + poolId, + rewardVaultCtoAuthority, + [alice, bob], + [4000, 6000], + ], + ), + ), + providerCtoAuthorities: [ + rewardVaultCtoAuthority, + rewardVaultCtoAuthority, + ], + factoryProviderCallCounts: [4, 4], + initialActiveConfigurationHash: initialActiveHash, + allocations: [ + { allocationIndex: 0, beneficiary: alice, shareBps: "4000" }, + { allocationIndex: 1, beneficiary: bob, shareBps: "6000" }, + ], + endConfigurationSnapshot: { + configurationEpoch: "2", + configurationHash: currentActiveHash, + providerCallCounts: [12, 12], + }, + }); + expect(left).toHaveBeenCalledWith( + expect.objectContaining({ blockHash, blockNumber: 100n, vault }), + ); + expect(right).toHaveBeenCalledWith( + expect.objectContaining({ blockHash, blockNumber: 100n, vault }), + ); + + const disagreeingProviders = [ + provider("alchemy-seed", "alchemy", left, factoryConfigurationHash), + provider( + "quicknode-seed", + "quicknode", + right, + factoryConfigurationHash, + { initCodeHash: bytes32("6") }, + ), + ] as const; + await expect(readDualRpcInitialRewardConfiguration({ + parentCandidate: parent, + launchCandidate: launch, + sameBlockVaultEvents: [payout], + candidateEvidence: candidateBatchEvidence( + [launch, payout], + disagreeingProviders, + ), + canonicalDeployment: canonicalDeploymentEvidence( + parent, + disagreeingProviders, + ), + template: dynamicTemplate(parent.sourceAddress), + providers: disagreeingProviders, + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 32 }, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + + const authorityDisagreement = [ + provider("alchemy-seed", "alchemy", left, factoryConfigurationHash), + provider( + "quicknode-seed", + "quicknode", + right, + factoryConfigurationHash, + { ctoAuthority: address("c") }, + ), + ] as const; + await expect(readDualRpcInitialRewardConfiguration({ + parentCandidate: parent, + launchCandidate: launch, + sameBlockVaultEvents: [payout], + candidateEvidence: candidateBatchEvidence( + [launch, payout], + authorityDisagreement, + ), + canonicalDeployment: canonicalDeploymentEvidence( + parent, + authorityDisagreement, + ), + template: dynamicTemplate(parent.sourceAddress), + providers: authorityDisagreement, + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 32 }, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + }); + + it("separately proves checkpoint, claim and payout state after activation", async () => { + const carol = address("3"); + const factoryConfigurationHash = bytes32("c"); + const initialActiveHash = classicActiveHash( + 1n, + [alice, bob], + [4000, 6000], + factoryConfigurationHash, + ); + const currentActiveHash = classicActiveHash( + 2n, + [carol, bob], + [4000, 6000], + factoryConfigurationHash, + ); + const parent = seedCandidate({ + eventName: "ClassicRewardVaultDeployed", + sourceAddress: address("f"), + contractName: "ClassicV3RewardVaultFactory", + logIndex: 4, + decodedPayload: { + vault, + poolId, + feeHook: address("e"), + configurationHash: factoryConfigurationHash, + }, + }); + const launch = seedCandidate({ + eventName: "MemeTokenLaunchedV2", + sourceAddress: classicV3Launcher, + contractName: "ClassicV3Launcher", + logIndex: 5, + decodedPayload: { + deployer: launchDeployer, + token: launchToken, + rewardVault: vault, + poolId, + feeHook: address("e"), + rewardConfigurationHash: factoryConfigurationHash, + }, + }); + const checkpoint = seedCandidate({ + eventName: "CreatorFeesCheckpointed", + sourceAddress: vault, + logIndex: 6, + decodedPayload: { + poolId, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: "10", + }, + }); + const claim = seedCandidate({ + eventName: "BeneficiaryFeesClaimed", + sourceAddress: vault, + logIndex: 7, + decodedPayload: { + beneficiary: alice, + amount: "4", + beneficiaryTotalClaimed: "4", + vaultTotalReceived: "10", + }, + }); + const payout = seedCandidate({ + eventName: "PayoutWalletChanged", + sourceAddress: vault, + logIndex: 8, + decodedPayload: { + poolId, + allocationIndex: "0", + previousPayoutWallet: alice, + newPayoutWallet: carol, + shareBps: "4000", + configurationEpoch: "2", + activeConfigurationHash: currentActiveHash, + effectiveTotalCreatorFeesReceived: "10", + }, + }); + const allocations = [ + { + allocationIndex: 0, + beneficiary: carol, + payoutAddress: carol, + shareBps: "4000", + }, + { + allocationIndex: 1, + beneficiary: bob, + payoutAddress: bob, + shareBps: "6000", + }, + ]; + const balances = [ + { + account: alice, + payoutAddress: alice, + claimableAccrued: "0", + claimedTotal: "4", + }, + { + account: bob, + payoutAddress: bob, + claimableAccrued: "6", + claimedTotal: "0", + }, + { + account: carol, + payoutAddress: carol, + claimableAccrued: "0", + claimedTotal: "0", + }, + ]; + const read = vi.fn(async ({ + balanceAccounts, + }: { + balanceAccounts: readonly `0x${string}`[]; + }) => ({ + model: "classic-v3", + vault, + blockNumber: "100", + blockHash, + poolId, + configurationEpoch: "2", + configurationHash: currentActiveHash, + totalCreatorFeesReceived: "10", + totalCreatorFeesClaimed: "4", + beneficiaryCount: "2", + allocations, + balances: balanceAccounts.map((account) => + account === vault + ? { + account: vault, + payoutAddress: vault, + claimableAccrued: "0", + claimedTotal: "0", + } + : balances.find((balance) => balance.account === account)!), + rpcCallCount: expectedRewardRpcCallCount( + "classic-v3", + 2, + balanceAccounts.length, + ), + })); + const providers = [ + provider("alchemy-seed", "alchemy", read, factoryConfigurationHash), + provider( + "quicknode-seed", + "quicknode", + read, + factoryConfigurationHash, + ), + ] as const; + const candidateEvidence = candidateBatchEvidence( + [launch, checkpoint, claim, payout], + providers, + ); + + const verified = await verifyClassicV3ActivationModel({ + activationId: "70000000-0000-4000-8000-000000000001", + parentCandidate: parent, + launchCandidate: launch, + sameBlockVaultEvents: [checkpoint, claim, payout], + candidateEvidence, + sourceAddress: vault, + template: dynamicTemplate(parent.sourceAddress), + canonicalDeployment: canonicalDeploymentEvidence(parent, providers), + providers, + deadlineMs: 1_000, + }); + + expect( + verified.initialConfiguration.initialActiveConfigurationHash, + ).toBe( + initialActiveHash, + ); + expect(verified.projectedSnapshot).toMatchObject({ + configurationEpoch: "2", + activeConfigurationHash: currentActiveHash, + totalCreatorFeesReceived: "10", + allocations, + balances, + }); + expect(verified.rewardEvidence.verificationAccounts).toEqual([ + alice, + bob, + carol, + ]); + expect( + verified.modelVerificationEvidence.map(({ evidenceKind }) => + evidenceKind), + ).toEqual([ + "classic-v3-runtime-activation-v1", + "classic-v3-initial-reward-configuration-v1", + "classic-v3-launch-reward-conservation-v1", + ]); + const replayed = await verifyClassicV3ActivationModel({ + activationId: "70000000-0000-4000-8000-000000000001", + parentCandidate: parent, + launchCandidate: launch, + sameBlockVaultEvents: [checkpoint, claim, payout], + candidateEvidence, + sourceAddress: vault, + template: dynamicTemplate(parent.sourceAddress), + canonicalDeployment: canonicalDeploymentEvidence(parent, providers), + providers, + deadlineMs: 1_000, + }); + expect( + replayed.modelVerificationEvidence.map( + ({ evidenceCommitment }) => evidenceCommitment, + ), + ).toEqual( + verified.modelVerificationEvidence.map( + ({ evidenceCommitment }) => evidenceCommitment, + ), + ); + expect(read).toHaveBeenCalledTimes(8); + }); + + it("fails closed when epoch one cannot be reconstructed", async () => { + const carol = address("3"); + const factoryConfigurationHash = bytes32("c"); + const currentActiveHash = classicActiveHash( + 2n, + [carol, bob], + [4000, 6000], + factoryConfigurationHash, + ); + const parent = seedCandidate({ + eventName: "ClassicRewardVaultDeployed", + sourceAddress: address("f"), + contractName: "ClassicV3RewardVaultFactory", + logIndex: 4, + decodedPayload: { + vault, + poolId, + feeHook: address("e"), + configurationHash: factoryConfigurationHash, + }, + }); + const launch = seedCandidate({ + eventName: "MemeTokenLaunchedV2", + sourceAddress: classicV3Launcher, + contractName: "ClassicV3Launcher", + logIndex: 5, + decodedPayload: { + deployer: launchDeployer, + token: launchToken, + rewardVault: vault, + poolId, + feeHook: address("e"), + rewardConfigurationHash: factoryConfigurationHash, + }, + }); + const raw = result({ + configurationEpoch: "2", + configurationHash: currentActiveHash, + totalCreatorFeesReceived: "0", + totalCreatorFeesClaimed: "0", + allocations: [ + { allocationIndex: 0, beneficiary: carol, payoutAddress: carol, shareBps: "4000" }, + { allocationIndex: 1, beneficiary: bob, payoutAddress: bob, shareBps: "6000" }, + ], + balances: [ + { account: vault, payoutAddress: vault, claimableAccrued: "0", claimedTotal: "0" }, + ], + rpcCallCount: expectedRewardRpcCallCount("classic-v3", 2, 1), + }); + + const providers = [ + provider("alchemy-seed", "alchemy", async () => raw), + provider("quicknode-seed", "quicknode", async () => raw), + ] as const; + await expect(readDualRpcInitialRewardConfiguration({ + parentCandidate: parent, + launchCandidate: launch, + sameBlockVaultEvents: [], + candidateEvidence: candidateBatchEvidence([launch], providers), + canonicalDeployment: canonicalDeploymentEvidence(parent, providers), + template: dynamicTemplate(parent.sourceAddress), + providers, + rpcPolicy: { maxAttempts: 1, maxCallsPerProvider: 32 }, + })).rejects.toThrow(); + }); +}); diff --git a/tests/data-pipeline/projector-reward-fold.test.ts b/tests/data-pipeline/projector-reward-fold.test.ts new file mode 100644 index 00000000..c89bf882 --- /dev/null +++ b/tests/data-pipeline/projector-reward-fold.test.ts @@ -0,0 +1,343 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + foldProjectorRewardState, + type ProjectorRewardBaseline, + type ProjectorRewardEvent, +} from "../../lib/data-pipeline/projector-reward-fold"; + +const address = (byte: string) => `0x${byte.repeat(40)}` as const; +const bytes32 = (byte: string) => `0x${byte.repeat(64)}` as const; +const occurrence = (suffix: number) => + `00000000-0000-4000-8000-${suffix.toString().padStart(12, "0")}`; + +const vault = address("a"); +const poolId = bytes32("b"); +const alice = address("1"); +const bob = address("2"); +const carol = address("3"); +const payout = address("4"); + +function event( + position: number, + kind: ProjectorRewardEvent["kind"], + values: ProjectorRewardEvent["values"], +): ProjectorRewardEvent { + return { + occurrenceId: occurrence(position), + vault, + blockNumber: String(100 + position), + transactionIndex: "0", + blockGlobalLogIndex: String(position), + kind, + values, + }; +} + +function classicBaseline(): ProjectorRewardBaseline { + return { + vault, + poolId, + configurationEpoch: "1", + activeConfigurationHash: bytes32("c"), + allocations: [ + { + allocationIndex: 0, + beneficiary: alice, + payoutAddress: alice, + shareBps: "4000", + }, + { + allocationIndex: 1, + beneficiary: bob, + payoutAddress: bob, + shareBps: "6000", + }, + ], + balances: [ + { + account: alice, + payoutAddress: alice, + claimableAccrued: "0", + claimedTotal: "0", + }, + { + account: bob, + payoutAddress: bob, + claimableAccrued: "0", + claimedTotal: "0", + }, + ], + }; +} + +describe("projector reward state fold", () => { + it("preserves historical Classic balances through consolidation and claims", () => { + const snapshot = foldProjectorRewardState({ + model: "classic-v3", + baseline: classicBaseline(), + events: [ + event(1, "creator-fee-checkpoint", { + poolId, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: "10", + }), + event(2, "payout-change", { + poolId, + allocationIndex: "0", + previousPayoutWallet: alice, + newPayoutWallet: bob, + shareBps: "4000", + configurationEpoch: "2", + activeConfigurationHash: bytes32("d"), + effectiveTotalCreatorFeesReceived: "10", + }), + event(3, "creator-fee-checkpoint", { + poolId, + configurationEpoch: "2", + amount: "3", + totalCreatorFeesReceived: "13", + }), + event(4, "beneficiary-claim", { + beneficiary: alice, + amount: "4", + beneficiaryTotalClaimed: "4", + vaultTotalReceived: "13", + }), + ], + }); + + expect(snapshot).toMatchObject({ + configurationEpoch: "2", + activeConfigurationHash: bytes32("d"), + totalCreatorFeesReceived: "13", + allocations: [ + { allocationIndex: 0, beneficiary: bob, shareBps: "4000" }, + { allocationIndex: 1, beneficiary: bob, shareBps: "6000" }, + ], + balances: [ + { account: alice, claimableAccrued: "0", claimedTotal: "4" }, + { account: bob, claimableAccrued: "9", claimedTotal: "0" }, + ], + snapshotSourceOccurrenceId: occurrence(4), + }); + }); + + it("keeps old Classic rewards when a CTO activates a new allocation", () => { + const baseline = classicBaseline(); + const snapshot = foldProjectorRewardState({ + model: "classic-v3", + baseline: { + ...baseline, + balances: [ + { ...baseline.balances[0]!, claimableAccrued: "4" }, + { ...baseline.balances[1]!, claimableAccrued: "6" }, + ], + }, + events: [ + event(1, "reward-configuration-activation", { + poolId, + approvalReference: bytes32("e"), + configurationEpoch: "2", + previousConfigurationHash: bytes32("c"), + newConfigurationHash: bytes32("d"), + beneficiaries: [carol], + sharesBps: ["10000"], + effectiveTotalCreatorFeesReceived: "10", + }), + event(2, "creator-fee-checkpoint", { + poolId, + configurationEpoch: "2", + amount: "5", + totalCreatorFeesReceived: "15", + }), + ], + }); + + expect(snapshot.allocations).toEqual([ + { + allocationIndex: 0, + beneficiary: carol, + payoutAddress: carol, + shareBps: "10000", + }, + ]); + expect(snapshot.balances).toEqual([ + { + account: alice, + payoutAddress: alice, + claimableAccrued: "4", + claimedTotal: "0", + }, + { + account: bob, + payoutAddress: bob, + claimableAccrued: "6", + claimedTotal: "0", + }, + { + account: carol, + payoutAddress: carol, + claimableAccrued: "5", + claimedTotal: "0", + }, + ]); + }); + + it("recomputes every Stock balance from the cumulative vault total", () => { + const snapshot = foldProjectorRewardState({ + model: "stock-paired", + baseline: { + vault, + poolId, + configurationEpoch: "1", + activeConfigurationHash: null, + allocations: classicBaseline().allocations, + balances: [ + { + account: alice, + payoutAddress: alice, + claimableAccrued: "4", + claimedTotal: "0", + }, + { + account: bob, + payoutAddress: bob, + claimableAccrued: "6", + claimedTotal: "0", + }, + ], + }, + events: [ + event(1, "payout-change", { + beneficiary: alice, + previousPayoutAddress: alice, + newPayoutAddress: payout, + }), + event(2, "beneficiary-claim", { + beneficiary: alice, + payoutAddress: payout, + quoteAsset: address("f"), + amount: "6", + beneficiaryTotalClaimed: "6", + vaultTotalReceived: "15", + }), + ], + }); + + expect(snapshot.totalCreatorFeesReceived).toBe("15"); + expect(snapshot.allocations[0]).toMatchObject({ payoutAddress: payout }); + expect(snapshot.balances).toEqual([ + { + account: alice, + payoutAddress: payout, + claimableAccrued: "0", + claimedTotal: "6", + }, + { + account: bob, + payoutAddress: bob, + claimableAccrued: "9", + claimedTotal: "0", + }, + ]); + }); + + it("fails closed on gaps, malformed order and impossible transitions", () => { + expect(() => + foldProjectorRewardState({ + model: "classic-v3", + baseline: classicBaseline(), + events: [ + event(1, "creator-fee-checkpoint", { + poolId, + configurationEpoch: "1", + amount: "10", + totalCreatorFeesReceived: "11", + }), + ], + }), + ).toThrow(/checkpoint total/u); + + const later = event(2, "creator-fee-checkpoint", { + poolId, + configurationEpoch: "1", + amount: "1", + totalCreatorFeesReceived: "1", + }); + const earlier = { + ...event(1, "beneficiary-claim", { + beneficiary: alice, + amount: "1", + beneficiaryTotalClaimed: "1", + vaultTotalReceived: "1", + }), + blockNumber: later.blockNumber, + blockGlobalLogIndex: "1", + }; + expect(() => + foldProjectorRewardState({ + model: "classic-v3", + baseline: classicBaseline(), + events: [later, earlier], + }), + ).toThrow(/event order/u); + + const invalidBaseline = classicBaseline(); + expect(() => + foldProjectorRewardState({ + model: "stock-paired", + baseline: { + ...invalidBaseline, + activeConfigurationHash: null, + allocations: [ + invalidBaseline.allocations[0]!, + { + ...invalidBaseline.allocations[1]!, + beneficiary: alice, + payoutAddress: alice, + }, + ], + balances: [invalidBaseline.balances[0]!], + }, + events: [ + event(1, "payout-change", { + beneficiary: alice, + previousPayoutAddress: alice, + newPayoutAddress: payout, + }), + ], + }), + ).toThrow(/immutable beneficiary uniqueness/u); + + expect(() => + foldProjectorRewardState({ + model: "stock-paired", + baseline: { + ...invalidBaseline, + activeConfigurationHash: null, + balances: [ + { + ...invalidBaseline.balances[0]!, + claimableAccrued: "5", + }, + { + ...invalidBaseline.balances[1]!, + claimableAccrued: "5", + }, + ], + }, + events: [ + event(1, "payout-change", { + beneficiary: alice, + previousPayoutAddress: alice, + newPayoutAddress: payout, + }), + ], + }), + ).toThrow(/cumulative baseline entitlement/u); + }); +}); diff --git a/tests/data-pipeline/projector-reward-store.test.ts b/tests/data-pipeline/projector-reward-store.test.ts new file mode 100644 index 00000000..ebda8bd1 --- /dev/null +++ b/tests/data-pipeline/projector-reward-store.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { parseProjectorRewardStateRows } from "../../lib/data-pipeline/postgres-projector"; + +const bytes32 = (byte: string) => Buffer.from(byte.repeat(64), "hex"); +const address = (byte: string) => Buffer.from(byte.repeat(40), "hex"); +const uuid = (suffix: number) => + `00000000-0000-4000-8000-${String(suffix).padStart(12, "0")}`; + +const vault = `0x${"a".repeat(40)}` as const; + +function commonRow() { + return { + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + source_group: "core", + epoch_id: uuid(1), + pointer_generation: "2", + checkpoint_id: uuid(2), + projector_version: "projector-v1", + checkpoint_generation: "3", + reorg_generation: "0", + checkpoint_block_number: "25650000", + checkpoint_block_hash: bytes32("1"), + reward_vault_projection_id: uuid(3), + allocation_fact_id: uuid(4), + allocation_evidence_id: uuid(81), + vault: address("a"), + pool_id: bytes32("2"), + quote_asset: null, + configuration_hash: bytes32("3"), + active_configuration_hash: bytes32("4"), + total_creator_fees_received: "14", + configuration_epoch: "2", + baseline_projection_run_id: uuid(5), + baseline_publication_commitment: bytes32("5"), + baseline_promoted_block_number: "25649990", + baseline_promoted_block_hash: bytes32("6"), + vault_source_occurrence_id: uuid(6), + vault_source_logical_event_id: uuid(7), + vault_source_block_hash: bytes32("7"), + verified_at: "2026-07-31T18:00:00.000Z", + }; +} + +function activeRow(index: number, shareBps: string) { + return { + ...commonRow(), + allocation_index: String(index), + beneficiary: address("2"), + payout_address: address("2"), + share_bps: shareBps, + claimable_accrued: "10", + claimed_total: "0", + balance_projection_run_id: uuid(8), + balance_publication_commitment: bytes32("8"), + balance_promoted_block_number: "25650000", + balance_promoted_block_hash: bytes32("1"), + allocation_source_occurrence_id: uuid(9 + index), + allocation_source_logical_event_id: uuid(11 + index), + allocation_source_block_hash: bytes32("9"), + balance_source_occurrence_id: uuid(13), + balance_source_logical_event_id: uuid(14), + balance_source_block_hash: bytes32("1"), + }; +} + +function balanceRow(accountByte: string, claimable: string, suffix: number) { + return { + ...commonRow(), + account_reward_balance_id: uuid(20 + suffix), + account: address(accountByte), + payout_address: address(accountByte), + payout_source_kind: "reward_snapshot", + payout_configuration_epoch: "2", + claimable_accrued: claimable, + claimed_total: "0", + balance_projection_run_id: uuid(8), + balance_publication_commitment: bytes32("8"), + balance_promoted_block_number: "25650000", + balance_promoted_block_hash: bytes32("1"), + payout_projection_run_id: uuid(30 + suffix), + payout_publication_commitment: bytes32("a"), + payout_promoted_block_number: "25650000", + payout_promoted_block_hash: bytes32("1"), + payout_source_occurrence_id: uuid(40 + suffix), + payout_source_logical_event_id: uuid(50 + suffix), + payout_source_block_hash: bytes32("1"), + balance_source_occurrence_id: uuid(60 + suffix), + balance_source_logical_event_id: uuid(70 + suffix), + balance_source_block_hash: bytes32("1"), + }; +} + +describe("projector reward state readers", () => { + it("preserves duplicate active Classic V3 wallets and historical balances", () => { + const state = parseProjectorRewardStateRows({ + activeRows: [activeRow(0, "4000"), activeRow(1, "6000")], + balanceRows: [balanceRow("1", "4", 1), balanceRow("2", "10", 2)], + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + }, + vault, + }); + + expect(state.model).toBe("classic-v3"); + expect(state.initialAllocationEvidenceId).toBe(uuid(81)); + expect(state.baseline.allocations).toEqual([ + expect.objectContaining({ allocationIndex: 0, beneficiary: `0x${"2".repeat(40)}` }), + expect.objectContaining({ allocationIndex: 1, beneficiary: `0x${"2".repeat(40)}` }), + ]); + expect(state.baseline.balances).toEqual([ + expect.objectContaining({ account: `0x${"1".repeat(40)}`, claimableAccrued: "4" }), + expect.objectContaining({ account: `0x${"2".repeat(40)}`, claimableAccrued: "10" }), + ]); + }); + + it("fails closed on a mixed checkpoint or unsorted historical balance set", () => { + expect(() => + parseProjectorRewardStateRows({ + activeRows: [ + activeRow(0, "4000"), + { ...activeRow(1, "6000"), checkpoint_generation: "4" }, + ], + balanceRows: [balanceRow("1", "4", 1), balanceRow("2", "10", 2)], + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + }, + vault, + }), + ).toThrow(); + + expect(() => + parseProjectorRewardStateRows({ + activeRows: [activeRow(0, "4000"), activeRow(1, "6000")], + balanceRows: [balanceRow("2", "10", 2), balanceRow("1", "4", 1)], + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + }, + vault, + }), + ).toThrow(); + }); +}); diff --git a/tests/data-pipeline/projector-runtime-config.test.ts b/tests/data-pipeline/projector-runtime-config.test.ts new file mode 100644 index 00000000..d738f391 --- /dev/null +++ b/tests/data-pipeline/projector-runtime-config.test.ts @@ -0,0 +1,1117 @@ +import { rootCertificates } from "node:tls"; + +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + assertProjectorRuntimeProviderCommitments, + loadProjectorRuntimeConfig, + loadProjectorRuntimeConfigForBinding, + projectorRuntimeActivationState, + runConfiguredProjectorCycle, +} from "../../lib/data-pipeline/projector-runtime-config.server"; +import { loadCandidateProjectorRuntimeBinding } from "../../lib/data-pipeline/candidate-projector-runtime-binding.server"; +import { + projectorEnvioDeploymentCommitment, + projectorEnvioSchemaCommitment, + projectorRpcDeploymentCommitment, + projectorRpcSchemaCommitment, +} from "../../lib/data-pipeline/projector-provider-commitments"; +import { getDataPipelineReleaseBinding } from "../../lib/data-pipeline/release-binding.server"; + +const bytes32 = (byte: string) => `0x${byte.repeat(64)}`; +const TEST_CA = rootCertificates[0]!; +const ALCHEMY_URL = "https://eth-mainnet.g.alchemy.com/v2/abcdefgh"; +const QUICKNODE_URL = "https://example.quiknode.pro/abcdefgh/"; +const ENVIO_URL = "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql"; +const ENVIO_IDENTITY = "envio:production-7f24e63"; +const RELEASE_BINDING = getDataPipelineReleaseBinding(); +const RPC_SCHEMA_COMMITMENT = projectorRpcSchemaCommitment(); +const EXPECTED_COMMITMENTS = Object.freeze({ + envioDeployment: projectorEnvioDeploymentCommitment({ + endpoint: ENVIO_URL, + redactedIdentity: ENVIO_IDENTITY, + binding: RELEASE_BINDING, + }), + envioSchema: projectorEnvioSchemaCommitment(RELEASE_BINDING), + alchemyDeployment: projectorRpcDeploymentCommitment(ALCHEMY_URL), + quicknodeDeployment: projectorRpcDeploymentCommitment(QUICKNODE_URL), +}); +const RUNTIME_PROVIDERS = Object.freeze([ + Object.freeze({ + vendorGroup: "alchemy", + endpointCommitment: EXPECTED_COMMITMENTS.alchemyDeployment, + }), + Object.freeze({ + vendorGroup: "quicknode", + endpointCommitment: EXPECTED_COMMITMENTS.quicknodeDeployment, + }), +]) as never; + +function environment( + overrides: Record = {}, +): Record { + return { + PROGRAMMABLE_PROJECTOR_ACTIVE: "true", + PROGRAMMABLE_PROJECTOR_DATABASE_URL: + "postgresql://programmable_projector_login:password@db.example:5432/postgres?sslmode=verify-full", + PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_URL: + "postgresql://programmable_projector_runtime_login:password@db.example:5432/postgres?sslmode=verify-full", + PROGRAMMABLE_POSTGRES_SSL_CA_PEM: TEST_CA, + PROGRAMMABLE_ENVIO_GRAPHQL_URL: ENVIO_URL, + PROGRAMMABLE_ENVIO_GRAPHQL_TOKEN: "envio-token", + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY_URL, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE_URL, + PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY: ENVIO_IDENTITY, + PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT: + "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + VERCEL_GIT_COMMIT_SHA: "a".repeat(40), + VERCEL_DEPLOYMENT_ID: "dpl_12345678901234567890", + ...overrides, + }; +} + +function candidateEnvironment() { + return environment({ + PROGRAMMABLE_PROJECTOR_BINDING_MODE: "candidate-backfill", + PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT: + "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY: + "envio:production-7f24e63", + INDEXED_EXPLORE_LIST_READS_ENABLED: "false", + INDEXED_EXPLORE_TOKEN_READS_ENABLED: "false", + INDEXED_EXPLORE_CHART_READS_ENABLED: "false", + INDEXED_CREATOR_PROFILE_READS_ENABLED: "false", + INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED: "false", + INDEXED_LAUNCH_LOOKUP_ENABLED: "false", + INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED: "false", + INDEXED_READ_SHADOW_COMPARE_ENABLED: "false", + }); +} + +function promotedReleaseEnvironment() { + return candidateEnvironmentWithOverrides({ + PROGRAMMABLE_PROJECTOR_BINDING_MODE: "release", + }); +} + +function candidateEnvironmentWithOverrides( + overrides: Record, +) { + return { ...candidateEnvironment(), ...overrides }; +} + +function candidateCanonicalBinding() { + return loadCandidateProjectorRuntimeBinding({ + env: candidateEnvironment(), + activeProductionBinding: RELEASE_BINDING, + }).releaseBinding; +} + +describe("configured projector runtime", () => { + it("keeps the runtime disabled by default without opening dependencies", async () => { + const createExecutor = vi.fn(); + const dependencies = { + createExecutor, + createLeaseController: vi.fn(), + createProviders: vi.fn(), + assertProviders: vi.fn(), + createEnvio: vi.fn(), + createStore: vi.fn(), + createReleaseStore: vi.fn(), + runCycle: vi.fn(), + runReleaseCycle: vi.fn(), + } as never; + + await expect(runConfiguredProjectorCycle({ + env: environment({ PROGRAMMABLE_PROJECTOR_ACTIVE: undefined }), + dependencies, + })).resolves.toEqual({ + ok: true, + status: "disabled", + readiness: { + status: "disabled", + activationReady: false, + lagging: true, + }, + }); + expect(createExecutor).not.toHaveBeenCalled(); + }); + + it("requires the exact activation value and fails closed otherwise", () => { + expect(projectorRuntimeActivationState( + environment({ PROGRAMMABLE_PROJECTOR_ACTIVE: "false" }), + )).toBe("disabled"); + expect(projectorRuntimeActivationState( + environment({ PROGRAMMABLE_PROJECTOR_ACTIVE: "true" }), + )).toBe("active"); + for (const value of ["TRUE", "1", " true", "false "]) { + expect(() => projectorRuntimeActivationState( + environment({ PROGRAMMABLE_PROJECTOR_ACTIVE: value }), + )).toThrow(); + } + }); + + it("builds the exact provider set and every frozen release scope", () => { + const config = loadProjectorRuntimeConfig(environment()); + + expect(config.binding).toMatchObject({ + mode: "release", + releaseBinding: RELEASE_BINDING, + candidate: null, + promotedDatabase: { + productCommit: "a".repeat(40), + stagedDeploymentId: "dpl_12345678901234567890", + }, + }); + expect(config.database).toEqual({ + projectorConnectionString: + "postgresql://programmable_projector_login:password@db.example:5432/postgres?sslmode=verify-full", + runtimeConnectionString: + "postgresql://programmable_projector_runtime_login:password@db.example:5432/postgres?sslmode=verify-full", + sslCaPem: TEST_CA, + }); + expect(config.envio).toEqual({ + endpoint: "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + token: "envio-token", + releaseBinding: RELEASE_BINDING, + }); + expect(config.providers).toEqual([ + { + type: "envio_deployment", + redactedIdentity: ENVIO_IDENTITY, + deploymentCommitment: EXPECTED_COMMITMENTS.envioDeployment, + schemaCommitment: EXPECTED_COMMITMENTS.envioSchema, + }, + { + type: "rpc_provider", + redactedIdentity: "rpc:1:alchemy", + deploymentCommitment: EXPECTED_COMMITMENTS.alchemyDeployment, + schemaCommitment: RPC_SCHEMA_COMMITMENT, + }, + { + type: "rpc_provider", + redactedIdentity: "rpc:1:quicknode", + deploymentCommitment: EXPECTED_COMMITMENTS.quicknodeDeployment, + schemaCommitment: RPC_SCHEMA_COMMITMENT, + }, + ]); + expect(config.releaseScopes).toEqual([ + { releaseId: "classic-v2", modelId: "classic", sourceGroup: "core" }, + { releaseId: "classic-v3", modelId: "classic", sourceGroup: "core" }, + { releaseId: "stock-paired-v1", modelId: "stock-paired", sourceGroup: "core" }, + { releaseId: "stock-paired-v2", modelId: "stock-paired", sourceGroup: "core" }, + { releaseId: "stock-paired-v3", modelId: "stock-paired", sourceGroup: "core" }, + ]); + }); + + it("loads the audited candidate only through the explicit backfill mode", () => { + const config = loadProjectorRuntimeConfig(candidateEnvironment()); + + expect(config.binding).toMatchObject({ + mode: "candidate-backfill", + candidate: { + mirrorCommit: "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + }, + }); + expect(config.envio.endpoint).toBe( + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + ); + expect(config.envio.releaseBinding.envio.deploymentLabel).toBe( + "production-7f24e63", + ); + expect(config.providers[0]).toEqual({ + type: "envio_deployment", + redactedIdentity: "envio:production-7f24e63", + deploymentCommitment: + "0xa4267153060a4b02b630d81063e0f84bb36f6f637a52ef71fb29c117c5384259", + schemaCommitment: + "0x5796791b38f16ba71b7a9a8f9977174c869de663f08c0aa0194e9cc631d93ef1", + }); + }); + + it("loads promoted release mode only with exact server-side evidence", () => { + const config = loadProjectorRuntimeConfigForBinding( + promotedReleaseEnvironment(), + candidateCanonicalBinding(), + ); + + expect(config.binding).toMatchObject({ + mode: "release", + releaseBinding: { + envio: { + deploymentLabel: "production-7f24e63", + graphqlEndpoint: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + }, + }, + candidate: null, + promotedDatabase: { + productCommit: "a".repeat(40), + stagedDeploymentId: "dpl_12345678901234567890", + }, + }); + expect(config.envio.releaseBinding.envio.deploymentLabel).toBe( + "production-7f24e63", + ); + }); + + it("rejects a promoted candidate database before any external provider work", async () => { + const runtimeClose = vi.fn(async () => undefined); + const writerClose = vi.fn(async () => undefined); + const createProviders = vi.fn(); + const createEnvio = vi.fn(); + const createStore = vi.fn(); + const createReleaseStore = vi.fn(); + const runCycle = vi.fn(); + const runReleaseCycle = vi.fn(); + const release = vi.fn(async () => true); + const assertCandidateDatabase = vi.fn(async () => { + throw new Error("candidate database promoted"); + }); + const dependencies = { + createExecutor: vi + .fn() + .mockReturnValueOnce({ close: runtimeClose }) + .mockReturnValueOnce({ close: writerClose }), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release, + })), + createProviders, + assertProviders: vi.fn(), + createEnvio, + createStore, + createReleaseStore, + runCycle, + runReleaseCycle, + assertCandidateDatabase, + } as never; + + await expect( + runConfiguredProjectorCycle({ + env: candidateEnvironment(), + dependencies, + }), + ).rejects.toThrow("candidate database promoted"); + + expect(assertCandidateDatabase).toHaveBeenCalledOnce(); + expect(createProviders).not.toHaveBeenCalled(); + expect(createEnvio).not.toHaveBeenCalled(); + expect(createStore).not.toHaveBeenCalled(); + expect(createReleaseStore).not.toHaveBeenCalled(); + expect(runCycle).not.toHaveBeenCalled(); + expect(runReleaseCycle).not.toHaveBeenCalled(); + expect(release).toHaveBeenCalledOnce(); + expect(writerClose).toHaveBeenCalledOnce(); + expect(runtimeClose).toHaveBeenCalledOnce(); + }); + + it("rejects an unverified promoted release before providers, Envio, or stores", async () => { + const runtimeClose = vi.fn(async () => undefined); + const writerClose = vi.fn(async () => undefined); + const createProviders = vi.fn(); + const createEnvio = vi.fn(); + const createStore = vi.fn(); + const createReleaseStore = vi.fn(); + const runCycle = vi.fn(); + const runReleaseCycle = vi.fn(); + const release = vi.fn(async () => true); + const assertPromotedDatabase = vi.fn(async () => { + throw new Error("promoted database evidence mismatch"); + }); + const promotedEnv = promotedReleaseEnvironment(); + const dependencies = { + loadConfig: (env: Readonly>) => + loadProjectorRuntimeConfigForBinding( + env, + candidateCanonicalBinding(), + ), + createExecutor: vi + .fn() + .mockReturnValueOnce({ close: runtimeClose }) + .mockReturnValueOnce({ close: writerClose }), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-08-01T10:01:00.000Z", + expiresAt: "2026-08-01T10:02:25.000Z", + })), + release, + })), + createProviders, + assertProviders: vi.fn(), + createEnvio, + createStore, + createReleaseStore, + runCycle, + runReleaseCycle, + assertPromotedDatabase, + } as never; + + await expect(runConfiguredProjectorCycle({ + env: promotedEnv, + dependencies, + })).rejects.toThrow("promoted database evidence mismatch"); + + expect(assertPromotedDatabase).toHaveBeenCalledOnce(); + expect(createProviders).not.toHaveBeenCalled(); + expect(createEnvio).not.toHaveBeenCalled(); + expect(createStore).not.toHaveBeenCalled(); + expect(createReleaseStore).not.toHaveBeenCalled(); + expect(runCycle).not.toHaveBeenCalled(); + expect(runReleaseCycle).not.toHaveBeenCalled(); + expect(release).toHaveBeenCalledOnce(); + expect(writerClose).toHaveBeenCalledOnce(); + expect(runtimeClose).toHaveBeenCalledOnce(); + }); + + it.each([ + "PROGRAMMABLE_PROJECTOR_DATABASE_URL", + "PROGRAMMABLE_PROJECTOR_RUNTIME_DATABASE_URL", + "PROGRAMMABLE_POSTGRES_SSL_CA_PEM", + "PROGRAMMABLE_ENVIO_GRAPHQL_URL", + "PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL", + "PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL", + "PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY", + ])("fails closed when %s is absent", (name) => { + expect(() => + loadProjectorRuntimeConfig(environment({ [name]: undefined })), + ).toThrow(); + }); + + it("rejects browser-exposed secrets and provider identity mismatches", () => { + expect(() => + loadProjectorRuntimeConfig( + environment({ + NEXT_PUBLIC_PROGRAMMABLE_PROJECTOR_DATABASE_URL: + "postgresql://public:secret@db.example:5432/postgres?sslmode=verify-full", + }), + ), + ).toThrow(); + expect(() => + loadProjectorRuntimeConfig( + environment({ + PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY: + "envio:unreviewed", + }), + ), + ).toThrow(); + expect(() => + loadProjectorRuntimeConfig( + environment({ + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://indexer.hyperindex.xyz/other123/v1/graphql", + }), + ), + ).toThrow(); + expect(() => + loadProjectorRuntimeConfig( + environment({ + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://example.com/f6714ef/v1/graphql", + }), + ), + ).toThrow(); + }); + + it("rejects a runtime RPC pair that does not match the derived endpoints", () => { + const config = loadProjectorRuntimeConfig(environment()); + expect(() => + assertProjectorRuntimeProviderCommitments( + config.providers, + Object.freeze([ + Object.freeze({ + vendorGroup: "alchemy", + endpointCommitment: bytes32("f"), + }), + Object.freeze({ + vendorGroup: "quicknode", + endpointCommitment: EXPECTED_COMMITMENTS.quicknodeDeployment, + }), + ]) as never, + ), + ).toThrow(); + }); + + it("runs ingestion and every release scope fairly, then closes the writer executor", async () => { + const close = vi.fn(async () => undefined); + const executor = { close }; + const providers = RUNTIME_PROVIDERS; + const envio = {}; + const ingestionStore = {}; + const createReleaseStore = vi.fn(({ scope }) => ({ scope })); + const runReleaseCycle = vi.fn(async () => ({ status: "idle" as const })); + const runCycle = vi + .fn() + .mockResolvedValueOnce({ + status: "idle", + candidateCount: 0, + snapshotBlock: "25650000", + }) + .mockRejectedValueOnce(new Error("runtime failed")); + const dependencies = { + createExecutor: vi.fn(() => executor), + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release: vi.fn(async () => true), + })), + createProviders: vi.fn(() => providers), + assertProviders: vi.fn(), + createEnvio: vi.fn(() => envio), + createStore: vi.fn(() => ingestionStore), + createReleaseStore, + runCycle, + runReleaseCycle, + } as never; + + await expect( + runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }), + ).resolves.toMatchObject({ + ok: true, + ingestion: { status: "idle" }, + projections: [ + { releaseId: "classic-v2", status: "idle" }, + { releaseId: "classic-v3", status: "idle" }, + { releaseId: "stock-paired-v1", status: "idle" }, + { releaseId: "stock-paired-v2", status: "idle" }, + { releaseId: "stock-paired-v3", status: "idle" }, + ], + readiness: { + status: "caught-up", + activationReady: true, + lagging: false, + }, + }); + await expect( + runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }), + ).resolves.toMatchObject({ + ok: false, + ingestion: { status: "failed" }, + readiness: { status: "incomplete", activationReady: false }, + }); + + expect(close).toHaveBeenCalledTimes(4); + expect(runCycle).toHaveBeenCalledWith( + expect.objectContaining({ + store: ingestionStore, + envio, + providers, + deadlineMs: 60_000, + }), + ); + expect(createReleaseStore).toHaveBeenCalledTimes(10); + expect(runReleaseCycle).toHaveBeenCalledTimes(10); + expect(runReleaseCycle).toHaveBeenCalledWith( + expect.objectContaining({ + envio, + providers, + deadlineMs: 10_000, + }), + ); + }); + + it("does no provider or writer work when the singleton is already owned", async () => { + const close = vi.fn(async () => undefined); + const createExecutor = vi.fn(() => ({ close })); + const createProviders = vi.fn(); + const createEnvio = vi.fn(); + const createStore = vi.fn(); + const createReleaseStore = vi.fn(); + const runCycle = vi.fn(); + const runReleaseCycle = vi.fn(); + const release = vi.fn(); + const dependencies = { + createExecutor, + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "busy", + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release, + })), + createProviders, + assertProviders: vi.fn(), + createEnvio, + createStore, + createReleaseStore, + runCycle, + runReleaseCycle, + } as never; + + await expect( + runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }), + ).resolves.toEqual({ + ok: true, + status: "busy", + readiness: { + status: "busy", + activationReady: false, + lagging: true, + }, + }); + + expect(createExecutor).toHaveBeenCalledOnce(); + expect(createExecutor).toHaveBeenCalledWith( + expect.objectContaining({ + connectionString: + "postgresql://programmable_projector_runtime_login:password@db.example:5432/postgres?sslmode=verify-full", + }), + ); + expect(createProviders).not.toHaveBeenCalled(); + expect(createEnvio).not.toHaveBeenCalled(); + expect(createStore).not.toHaveBeenCalled(); + expect(createReleaseStore).not.toHaveBeenCalled(); + expect(runCycle).not.toHaveBeenCalled(); + expect(runReleaseCycle).not.toHaveBeenCalled(); + expect(release).not.toHaveBeenCalled(); + expect(close).toHaveBeenCalledOnce(); + }); + + it("stops before every release projection after staging a dynamic parent", async () => { + const close = vi.fn(async () => undefined); + const executor = { close }; + const runCycle = vi.fn(async () => ({ + status: "staged-dynamic-parent" as const, + candidateCount: 1 as const, + snapshotBlock: "25650123", + })); + const runReleaseCycle = vi.fn(); + const dependencies = { + createExecutor: vi.fn(() => executor), + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release: vi.fn(async () => true), + })), + createProviders: vi.fn(() => RUNTIME_PROVIDERS), + assertProviders: vi.fn(), + createEnvio: vi.fn(() => ({})), + createStore: vi.fn(() => ({})), + createReleaseStore: vi.fn(({ scope }) => ({ scope })), + runCycle, + runReleaseCycle, + } as never; + + await expect( + runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }), + ).resolves.toEqual({ + ok: true, + ingestion: { + status: "staged-dynamic-parent", + candidateCount: 1, + pageCount: 1, + snapshotBlock: "25650123", + atomicGroupCount: 1, + }, + projections: [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ].map((releaseId) => ({ + releaseId, + status: "deferred", + pageCount: 0, + })), + readiness: { + status: "progressed", + activationReady: false, + lagging: true, + terminalSweepComplete: false, + stoppedForDeadline: false, + completedRounds: 1, + snapshotBlock: "25650123", + }, + deadlineMs: 75_000, + }); + + expect(runCycle).toHaveBeenCalledOnce(); + expect(runReleaseCycle).not.toHaveBeenCalled(); + expect(close).toHaveBeenCalledTimes(2); + }); + + it("fails closed before release projection for malformed staged progress", async () => { + const close = vi.fn(async () => undefined); + const executor = { close }; + const runCycle = vi.fn(async () => ({ + status: "staged-dynamic-parent" as const, + candidateCount: 4_097, + snapshotBlock: "25650123", + })); + const runReleaseCycle = vi.fn(); + const dependencies = { + createExecutor: vi.fn(() => executor), + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release: vi.fn(async () => true), + })), + createProviders: vi.fn(() => RUNTIME_PROVIDERS), + assertProviders: vi.fn(), + createEnvio: vi.fn(() => ({})), + createStore: vi.fn(() => ({})), + createReleaseStore: vi.fn(({ scope }) => ({ scope })), + runCycle, + runReleaseCycle, + } as never; + + const result = await runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }); + + expect(result.ok).toBe(false); + if (!("ingestion" in result)) throw new Error("Expected projector result"); + expect(result.ingestion).toEqual({ status: "failed" }); + expect(result.readiness).toMatchObject({ + status: "incomplete", + activationReady: false, + terminalSweepComplete: false, + completedRounds: 1, + }); + expect(runCycle).toHaveBeenCalledOnce(); + expect(runReleaseCycle).not.toHaveBeenCalled(); + expect(close).toHaveBeenCalledTimes(2); + }); + + it("drains bounded pages without starving any release scope", async () => { + const close = vi.fn(async () => undefined); + const executor = { close }; + const providers = RUNTIME_PROVIDERS; + const envio = {}; + const ingestionStore = {}; + const releaseCalls = new Map(); + const runCycle = vi + .fn() + .mockResolvedValueOnce({ + status: "committed", + candidateCount: 32, + snapshotBlock: "25650000", + generation: "41", + }) + .mockResolvedValueOnce({ + status: "committed", + candidateCount: 7, + snapshotBlock: "25650010", + generation: "42", + }) + .mockResolvedValueOnce({ + status: "idle", + candidateCount: 0, + snapshotBlock: "25650010", + }); + const runReleaseCycle = vi.fn(async ({ store }) => { + const releaseId = (store as { scope: { releaseId: string } }).scope.releaseId; + const call = (releaseCalls.get(releaseId) ?? 0) + 1; + releaseCalls.set(releaseId, call); + if (call === 1) { + return { + status: "committed" as const, + projectedCandidateCount: 20, + ignoredCandidateCount: 12, + checkpointGeneration: "51", + }; + } + if (call === 2) { + return { + status: "committed" as const, + projectedCandidateCount: 5, + ignoredCandidateCount: 2, + checkpointGeneration: "52", + }; + } + return { status: "idle" as const }; + }); + const dependencies = { + createExecutor: vi.fn(() => executor), + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release: vi.fn(async () => true), + })), + createProviders: vi.fn(() => providers), + assertProviders: vi.fn(), + createEnvio: vi.fn(() => envio), + createStore: vi.fn(() => ingestionStore), + createReleaseStore: vi.fn(({ scope }) => ({ scope })), + runCycle, + runReleaseCycle, + } as never; + + await expect( + runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }), + ).resolves.toEqual({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 39, + pageCount: 3, + snapshotBlock: "25650010", + generation: "42", + }, + projections: [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ].map((releaseId) => ({ + releaseId, + status: "committed", + projectedCandidateCount: 25, + ignoredCandidateCount: 14, + pageCount: 3, + checkpointGeneration: "52", + })), + readiness: { + status: "caught-up", + activationReady: true, + lagging: false, + terminalSweepComplete: true, + stoppedForDeadline: false, + completedRounds: 3, + snapshotBlock: "25650010", + }, + deadlineMs: 75_000, + }); + + expect(runCycle).toHaveBeenCalledTimes(3); + expect(runReleaseCycle).toHaveBeenCalledTimes(15); + expect([...releaseCalls.values()]).toEqual([3, 3, 3, 3, 3]); + expect(close).toHaveBeenCalledTimes(2); + }); + + it("does not report an atomic-group release as terminally swept", async () => { + const close = vi.fn(async () => undefined); + const executor = { close }; + const releaseCalls = new Map(); + const runCycle = vi.fn(async () => ({ + status: "idle" as const, + candidateCount: 0, + snapshotBlock: "25650200", + })); + const runReleaseCycle = vi.fn(async ({ store }) => { + const releaseId = (store as { scope: { releaseId: string } }).scope + .releaseId; + releaseCalls.set(releaseId, (releaseCalls.get(releaseId) ?? 0) + 1); + if (releaseId === "classic-v3") { + return { + status: "committed" as const, + projectedCandidateCount: 96, + ignoredCandidateCount: 0, + checkpointGeneration: "1", + batchKind: "reward-block" as const, + }; + } + return { status: "idle" as const }; + }); + const dependencies = { + createExecutor: vi.fn(() => executor), + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release: vi.fn(async () => true), + })), + createProviders: vi.fn(() => RUNTIME_PROVIDERS), + assertProviders: vi.fn(), + createEnvio: vi.fn(() => ({})), + createStore: vi.fn(() => ({})), + createReleaseStore: vi.fn(({ scope }) => ({ scope })), + runCycle, + runReleaseCycle, + } as never; + + const result = await runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }); + expect(result).toMatchObject({ + ok: true, + readiness: { + status: "progressed", + activationReady: false, + lagging: true, + terminalSweepComplete: false, + }, + }); + expect( + "projections" in result + ? result.projections.find(({ releaseId }) => + releaseId === "classic-v3" + ) + : null, + ).toMatchObject({ + releaseId: "classic-v3", + status: "committed", + projectedCandidateCount: 96, + atomicGroupCount: 1, + }); + + expect(runCycle).toHaveBeenCalledTimes(2); + expect(releaseCalls.get("classic-v3")).toBe(1); + expect(releaseCalls.get("classic-v2")).toBe(2); + }); + + it("finishes a 264-candidate corpus across two bounded runtime cycles", async () => { + const close = vi.fn(async () => undefined); + const executor = { close }; + const releaseCalls = new Map(); + let ingestionCalls = 0; + const runCycle = vi.fn(async () => { + ingestionCalls += 1; + if (ingestionCalls <= 8) { + return { + status: "committed" as const, + candidateCount: 32, + snapshotBlock: "25650200", + generation: String(ingestionCalls), + }; + } + if (ingestionCalls === 9) { + return { + status: "committed" as const, + candidateCount: 8, + snapshotBlock: "25650201", + generation: "9", + }; + } + return { + status: "idle" as const, + candidateCount: 0, + snapshotBlock: "25650201", + }; + }); + const runReleaseCycle = vi.fn(async ({ store }) => { + const releaseId = (store as { scope: { releaseId: string } }).scope.releaseId; + const call = (releaseCalls.get(releaseId) ?? 0) + 1; + releaseCalls.set(releaseId, call); + if (call <= 8) { + return { + status: "committed" as const, + projectedCandidateCount: 32, + ignoredCandidateCount: 0, + checkpointGeneration: String(call), + }; + } + if (call === 9) { + return { + status: "committed" as const, + projectedCandidateCount: 8, + ignoredCandidateCount: 0, + checkpointGeneration: "9", + }; + } + return { status: "idle" as const }; + }); + const dependencies = { + createExecutor: vi.fn(() => executor), + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release: vi.fn(async () => true), + })), + createProviders: vi.fn(() => RUNTIME_PROVIDERS), + assertProviders: vi.fn(), + createEnvio: vi.fn(() => ({})), + createStore: vi.fn(() => ({})), + createReleaseStore: vi.fn(({ scope }) => ({ scope })), + runCycle, + runReleaseCycle, + } as never; + + const first = await runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }); + expect(first).toMatchObject({ + ok: true, + ingestion: { candidateCount: 256, pageCount: 8 }, + readiness: { + status: "progressed", + activationReady: false, + lagging: true, + completedRounds: 8, + }, + }); + + const second = await runConfiguredProjectorCycle({ + env: environment(), + dependencies, + }); + expect(second).toMatchObject({ + ok: true, + ingestion: { candidateCount: 8, pageCount: 2 }, + readiness: { + status: "caught-up", + activationReady: true, + lagging: false, + completedRounds: 2, + }, + }); + expect(ingestionCalls).toBe(10); + expect([...releaseCalls.values()]).toEqual([10, 10, 10, 10, 10]); + expect(close).toHaveBeenCalledTimes(4); + }); + + it("runs one larger ingestion-only cutover page without projecting releases", async () => { + const close = vi.fn(async () => undefined); + const executor = { close }; + const runCycle = vi.fn(async () => ({ + status: "committed" as const, + candidateCount: 512, + snapshotBlock: "25650512", + generation: "73", + })); + const runReleaseCycle = vi.fn(); + const dependencies = { + createExecutor: vi.fn(() => executor), + assertPromotedDatabase: vi.fn(async () => undefined), + createLeaseController: vi.fn(() => ({ + tryAcquire: vi.fn(async () => ({ + status: "acquired", + fence: { + holderId: "projector-runtime-test", + generation: "1", + tokenHash: bytes32("a"), + }, + acquiredAt: "2026-07-31T18:00:00.000Z", + expiresAt: "2026-07-31T18:01:25.000Z", + })), + release: vi.fn(async () => true), + })), + createProviders: vi.fn(() => RUNTIME_PROVIDERS), + assertProviders: vi.fn(), + createEnvio: vi.fn(() => ({})), + createStore: vi.fn(() => ({})), + createReleaseStore: vi.fn(({ scope }) => ({ scope })), + runCycle, + runReleaseCycle, + } as never; + + await expect(runConfiguredProjectorCycle({ + env: environment(), + dependencies, + ingestionOnly: true, + preferredCandidatesPerCommit: 512, + })).resolves.toMatchObject({ + ok: true, + ingestion: { + status: "committed", + candidateCount: 512, + pageCount: 1, + snapshotBlock: "25650512", + generation: "73", + atomicGroupCount: 1, + }, + projections: [ + { releaseId: "classic-v2", status: "deferred", pageCount: 0 }, + { releaseId: "classic-v3", status: "deferred", pageCount: 0 }, + { releaseId: "stock-paired-v1", status: "deferred", pageCount: 0 }, + { releaseId: "stock-paired-v2", status: "deferred", pageCount: 0 }, + { releaseId: "stock-paired-v3", status: "deferred", pageCount: 0 }, + ], + readiness: { + status: "progressed", + activationReady: false, + lagging: true, + completedRounds: 1, + }, + }); + expect(runCycle).toHaveBeenCalledWith(expect.objectContaining({ + preferredCandidatesPerCommit: 512, + })); + expect(runReleaseCycle).not.toHaveBeenCalled(); + expect(close).toHaveBeenCalledTimes(2); + }); +}); diff --git a/tests/data-pipeline/projector-runtime-lease.test.ts b/tests/data-pipeline/projector-runtime-lease.test.ts new file mode 100644 index 00000000..d77306f5 --- /dev/null +++ b/tests/data-pipeline/projector-runtime-lease.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; +import { createProjectorRuntimeLeaseController } from "../../lib/data-pipeline/projector-runtime-lease.server"; + +const tokenHash = `0x${"a".repeat(64)}` as const; + +class LeaseExecutor implements PostgresExecutor { + readonly queries: Array<{ + text: string; + values: readonly PostgresParameter[]; + }> = []; + readonly close = vi.fn(async () => undefined); + sessionUser = "programmable_projector_runtime_login"; + acquired = true; + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + if (text === "select session_user::text as session_user") { + return [{ session_user: this.sessionUser }] as unknown as Row[]; + } + if (text.includes("current_setting('role')")) { + return [{ + session_user: this.sessionUser, + current_role: "programmable_projector_runtime", + configured_role: "programmable_projector_runtime", + }] as unknown as Row[]; + } + if (text.includes("try_acquire_projector_runtime_lease_v1")) { + const requestedAt = new Date(String(values[2])); + const acquiredAt = this.acquired + ? new Date(requestedAt.valueOf() + 1) + : new Date("2026-07-31T17:59:30.000Z"); + return [{ + acquired: this.acquired, + lease_generation: "7", + acquired_at: acquiredAt.toISOString(), + expires_at: new Date(acquiredAt.valueOf() + 85_000).toISOString(), + }] as unknown as Row[]; + } + if (text.includes("release_projector_runtime_lease_v1")) { + return [{ released: true }] as unknown as Row[]; + } + return [] as unknown as Row[]; + }, + }); + } +} + +describe("projector runtime lease controller", () => { + it("assumes the narrow runtime role, acquires a fenced lease and releases it", async () => { + const executor = new LeaseExecutor(); + const times = [ + new Date("2026-07-31T18:00:00.000Z"), + new Date("2026-07-31T18:00:30.000Z"), + ]; + const controller = createProjectorRuntimeLeaseController({ + executor, + now: () => times.shift()!, + uuid: () => "00000000-0000-4000-8000-000000000001", + tokenHash: () => tokenHash, + }); + + const acquisition = await controller.tryAcquire(); + + expect(acquisition).toMatchObject({ + status: "acquired", + fence: { + holderId: + "projector-runtime-00000000-0000-4000-8000-000000000001", + generation: "7", + tokenHash, + }, + }); + await expect(controller.release(acquisition.fence!)).resolves.toBe(true); + expect( + executor.queries.filter(({ text }) => + text === "set local role programmable_projector_runtime" + ), + ).toHaveLength(2); + }); + + it("returns busy without inventing a fence or releasing another holder", async () => { + const executor = new LeaseExecutor(); + executor.acquired = false; + const controller = createProjectorRuntimeLeaseController({ + executor, + now: () => new Date("2026-07-31T18:00:00.000Z"), + uuid: () => "00000000-0000-4000-8000-000000000001", + tokenHash: () => tokenHash, + }); + + await expect(controller.tryAcquire()).resolves.toEqual({ + status: "busy", + acquiredAt: "2026-07-31T17:59:30.000Z", + expiresAt: "2026-07-31T18:00:55.000Z", + }); + expect( + executor.queries.some(({ text }) => + text.includes("release_projector_runtime_lease_v1") + ), + ).toBe(false); + }); + + it("fails closed before lease mutation under the wrong database login", async () => { + const executor = new LeaseExecutor(); + executor.sessionUser = "programmable_projector_login"; + const controller = createProjectorRuntimeLeaseController({ + executor, + now: () => new Date("2026-07-31T18:00:00.000Z"), + uuid: () => "00000000-0000-4000-8000-000000000001", + tokenHash: () => tokenHash, + }); + + await expect(controller.tryAcquire()).rejects.toThrow(); + expect( + executor.queries.some(({ text }) => + text.includes("try_acquire_projector_runtime_lease_v1") + ), + ).toBe(false); + }); +}); diff --git a/tests/data-pipeline/projector-runtime.test.ts b/tests/data-pipeline/projector-runtime.test.ts new file mode 100644 index 00000000..8451f0a2 --- /dev/null +++ b/tests/data-pipeline/projector-runtime.test.ts @@ -0,0 +1,1526 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { runProjectorCycle } from "../../lib/data-pipeline/projector"; +import { validationError } from "../../lib/data-pipeline/errors"; +import type { EnvioCandidate } from "../../lib/data-pipeline/envio"; +import type { ProjectorDynamicSourceTemplate } from "../../lib/data-pipeline/dual-rpc"; +import type { PendingDynamicSourceActivation } from "../../lib/data-pipeline/projector-dynamic-activation"; + +const CURSOR_HASH = `0x${"11".repeat(32)}` as const; +const SAFE_HASH = `0x${"22".repeat(32)}` as const; +const CANDIDATE_HASH = `0x${"44".repeat(32)}` as const; +const executionTrace = (candidateBatchSize = 0) => ({ + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 128, + elapsedMs: 1, + providerCallCounts: [0, 0] as const, + calls: [], +}); +const CLASSIC_V3_FACTORY = + "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a" as const; + +function dynamicTemplate(): ProjectorDynamicSourceTemplate { + return { + templateId: "10000000-0000-4000-8000-000000000001", + contractName: "ClassicV3RewardVault" as const, + model: "classic" as const, + releaseVersion: "classic-v3" as const, + parentFactoryAddress: CLASSIC_V3_FACTORY, + parentFactoryContractName: "ClassicV3RewardVaultFactory" as const, + parentFactoryBindingId: "10000000-0000-4000-8000-000000000002", + parentFactoryBindingCommitment: SAFE_HASH, + parentSourceRole: "vault_factory", + factoryEventName: "ClassicRewardVaultDeployed" as const, + deployedAddressField: "vault" as const, + deployedSourceRole: "reward_vault" as const, + deployedArtifactCreationCodeCommitment: CURSOR_HASH, + expectedExactRuntimeCodeHash: null, + expectedNormalizedRuntimeCodeHash: SAFE_HASH, + expectedImmutableReferencesCommitment: CURSOR_HASH, + expectedRuntimeByteLength: "2", + immutableReferences: [{ start: 0, length: 1 }], + immutableBindingSpec: { + factoryConfigurationField: "configurationHash", + bindings: [ + { + ordinal: "0", + offset: "0", + length: "1", + source: "constant", + encoding: "bytes", + value: "0x60", + }, + ], + }, + immutableBindingCommitment: SAFE_HASH, + abiEventSetCommitment: CURSOR_HASH, + templateCommitment: SAFE_HASH, + database: { + scope: { + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "canonical-events", + }, + epochId: "10000000-0000-4000-8000-000000000003", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: + "10000000-0000-4000-8000-000000000004", + rpcProviderDeploymentIds: [ + "10000000-0000-4000-8000-000000000005", + "10000000-0000-4000-8000-000000000006", + ] as const, + }, + }; +} + +function candidate( + input: { + blockNumber?: number; + logIndex?: number; + sourceAddress?: `0x${string}`; + contractName?: string; + eventName?: string; + decodedPayload?: Record; + releaseHint?: EnvioCandidate["releaseHint"]; + } = {}, +): EnvioCandidate { + const blockNumber = input.blockNumber ?? 101; + const logIndex = input.logIndex ?? 7; + const transactionHash = `0x${(blockNumber * 10_000 + logIndex + 1) + .toString(16) + .padStart(64, "0")}` as const; + return { + candidateId: `1:${CANDIDATE_HASH}:${transactionHash}:${logIndex}`, + chainId: 1, + blockNumber: String(blockNumber), + blockHash: CANDIDATE_HASH, + blockTimestamp: "1000", + transactionHash, + transactionIndex: 0, + blockGlobalLogIndex: logIndex, + sourceAddress: + input.sourceAddress ?? "0xd240d06f8586eb799f20056054e5b527405e6bad", + contractName: input.contractName ?? "ClassicV2Launcher", + eventName: input.eventName ?? "MemeTokenLaunched", + releaseHint: input.releaseHint ?? { + model: "classic", + releaseVersion: + input.contractName === "ClassicV3RewardVaultFactory" + ? "classic-v3" + : "classic-v2", + }, + orderedTopics: [`0x${"55".repeat(32)}`], + rawData: "0x", + decodedPayload: input.decodedPayload ?? {}, + payloadHash: `0x${"66".repeat(32)}`, + }; +} + +function pendingActivation(input: { + parent: EnvioCandidate; + launch: EnvioCandidate; + sourceAddress: `0x${string}`; + activationId?: string; +}): PendingDynamicSourceActivation { + return { + activationId: + input.activationId ?? "10000000-0000-4000-8000-000000000099", + historicalParentCandidate: input.parent, + launchCandidate: input.launch, + sourceAddress: input.sourceAddress, + template: dynamicTemplate(), + canonicalDeployment: {} as never, + ephemeralLineage: { + sourceAddress: input.sourceAddress, + } as never, + }; +} + +function fixtures() { + let databaseTransactionOpen = false; + const store = { + readPlan: vi.fn(async () => { + databaseTransactionOpen = true; + databaseTransactionOpen = false; + return { + cursor: { + generation: "5", + blockNumber: "100", + blockHash: CURSOR_HASH, + blockGlobalLogIndex: -1, + candidateId: "", + isBlockBoundary: false, + }, + dynamicSources: [], + provisionalSourceAddresses: [], + dynamicSourceTemplates: [dynamicTemplate()], + database: { + epochId: "70000000-0000-0000-0000-000000000002", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: + "70000000-0000-4000-8000-000000000003", + rpcProviderDeploymentIds: [ + "70000000-0000-4000-8000-000000000004", + "70000000-0000-4000-8000-000000000005", + ] as const, + }, + }; + }), + readReorgRecoveryState: vi.fn(async () => ({ + ancestors: [], + genesis: { + kind: "genesis" as const, + historyGeneration: "0" as const, + genesisPointId: "70000000-0000-4000-8000-000000000006", + blockNumber: "0", + blockHash: CURSOR_HASH, + blockGlobalLogIndex: null, + candidateId: null, + }, + currentReorgGeneration: "0", + })), + recoverCanonicalReorg: vi.fn(async () => ({ + generation: "6", + reorgGeneration: "1", + releaseCheckpointCount: 5, + })), + resolvePendingDynamicSourceActivations: vi.fn( + async (): Promise => [], + ), + stageVerifiedDynamicSourceActivations: vi.fn(async () => { + expect(databaseTransactionOpen).toBe(false); + }), + stageVerifiedDynamicParents: vi.fn(async () => { + expect(databaseTransactionOpen).toBe(false); + }), + commitVerifiedPage: vi.fn(async () => { + expect(databaseTransactionOpen).toBe(false); + databaseTransactionOpen = true; + databaseTransactionOpen = false; + return { generation: "6" }; + }), + }; + let candidatePage = 0; + const envio = { + readProgress: vi.fn(async () => { + expect(databaseTransactionOpen).toBe(false); + return { progressBlock: "200" }; + }), + readCandidatesWindow: vi.fn(async (input: { limit: number }) => { + void input; + expect(databaseTransactionOpen).toBe(false); + candidatePage += 1; + return candidatePage === 1 ? [candidate()] : []; + }), + }; + const captureSafeHead = vi.fn(async () => { + expect(databaseTransactionOpen).toBe(false); + return { + providerHeads: ["220", "221"] as const, + safeBlockNumber: "208", + safeBlockHash: SAFE_HASH, + cursorBlockHash: CURSOR_HASH, + }; + }); + const verifyWindow = vi.fn(async (request: { + candidates: readonly EnvioCandidate[]; + through: { blockNumber: string; blockGlobalLogIndex: number }; + }) => { + expect(databaseTransactionOpen).toBe(false); + const throughCandidate = [...request.candidates] + .reverse() + .find((item) => item.blockNumber === request.through.blockNumber); + return { + chainId: 1 as const, + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [SAFE_HASH, CURSOR_HASH] as const, + providerOriginCommitments: [SAFE_HASH, CURSOR_HASH] as const, + providerHeads: ["220", "221"] as const, + safeBlockNumber: "208", + safeBlockHash: SAFE_HASH, + candidates: [], + executionTrace: executionTrace(), + coveredCandidateCount: 1, + coverage: { + fromBlockNumber: "100", + throughBlockNumber: request.through.blockNumber, + throughBlockHash: throughCandidate?.blockHash ?? SAFE_HASH, + throughBlockGlobalLogIndex: String( + request.through.blockGlobalLogIndex, + ), + filterCommitment: SAFE_HASH, + providerLogCommitments: [SAFE_HASH, SAFE_HASH] as const, + }, + }; + }); + const verifyDynamicRuntime = vi.fn(async (request: { + parentCandidate: EnvioCandidate; + sourceAddress: `0x${string}`; + deploymentBlockNumber: string; + deploymentBlockHash: `0x${string}`; + template: ProjectorDynamicSourceTemplate; + }) => ({ + chainId: 1 as const, + parentCandidateId: request.parentCandidate.candidateId, + sourceAddress: request.sourceAddress, + deploymentBlockNumber: request.deploymentBlockNumber, + deploymentBlockHash: request.deploymentBlockHash, + providerIdentities: ["alchemy", "quicknode"] as const, + providerVendorGroups: ["alchemy", "quicknode"] as const, + providerEndpointCommitments: [SAFE_HASH, CURSOR_HASH] as const, + providerOriginCommitments: [SAFE_HASH, CURSOR_HASH] as const, + rawRuntimeCodeA: "0x6000" as const, + rawRuntimeCodeB: "0x6000" as const, + runtimeCodeHashA: SAFE_HASH, + runtimeCodeHashB: SAFE_HASH, + normalizedRuntimeCodeHashA: SAFE_HASH, + normalizedRuntimeCodeHashB: SAFE_HASH, + runtimeByteLengthA: "2", + runtimeByteLengthB: "2", + immutableReferences: request.template.immutableReferences, + immutableReferencesCommitment: CURSOR_HASH, + immutableValues: ["0x60" as const], + immutableValuesCommitment: SAFE_HASH, + reconstructedRuntimeCode: "0x6000" as const, + reconstructedRuntimeCodeHash: SAFE_HASH, + factoryConfigurationCommitment: CURSOR_HASH, + deferredAllocationEvidenceCommitment: null, + template: request.template, + startedAtMs: 1, + completedAtMs: 2, + elapsedMs: 1, + hardDeadlineMs: 1_000, + providerCallCounts: [1, 1] as const, + })); + return { + store, + envio, + captureSafeHead, + verifyWindow, + verifyDynamicRuntime, + }; +} + +describe("projector runtime boundary", () => { + it("finishes all provider work before opening the atomic commit", async () => { + const input = fixtures(); + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).resolves.toEqual({ + status: "committed", + candidateCount: 1, + generation: "6", + snapshotBlock: "101", + }); + expect(input.store.commitVerifiedPage).toHaveBeenCalledTimes(1); + expect( + input.store.resolvePendingDynamicSourceActivations, + ).toHaveBeenCalledTimes(1); + expect( + input.store.stageVerifiedDynamicSourceActivations, + ).not.toHaveBeenCalled(); + expect(input.verifyWindow).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: [expect.objectContaining({ candidateId: candidate().candidateId })], + through: { + blockNumber: "101", + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "empty-page", + }, + dynamicSources: [], + rpcPolicy: expect.objectContaining({ + maxCallsPerProvider: 128, + hardDeadlineMs: expect.any(Number), + }), + }), + ); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ + snapshotBlock: "101", + blockComplete: true, + evidence: expect.objectContaining({ + coverage: expect.objectContaining({ + throughBlockNumber: "101", + throughBlockHash: CANDIDATE_HASH, + throughBlockGlobalLogIndex: "4294967295", + }), + }), + }), + ); + }); + + it("commits a trailing candidate block before advancing later empty blocks", async () => { + const input = fixtures(); + const onlyCandidate = candidate(); + await expect(runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + })).resolves.toMatchObject({ + status: "committed", + candidateCount: 1, + snapshotBlock: "101", + }); + input.store.readPlan.mockResolvedValue({ + ...(await input.store.readPlan()), + cursor: { + generation: "6", + blockNumber: "101", + blockHash: onlyCandidate.blockHash, + blockGlobalLogIndex: onlyCandidate.blockGlobalLogIndex, + candidateId: onlyCandidate.candidateId, + isBlockBoundary: false, + }, + }); + input.envio.readCandidatesWindow.mockReset().mockResolvedValue([]); + input.store.commitVerifiedPage.mockClear(); + await expect(runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + })).resolves.toMatchObject({ + status: "committed-empty", + candidateCount: 0, + snapshotBlock: "200", + }); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: [], + snapshotBlock: "200", + blockComplete: true, + }), + ); + }); + + it("shrinks a long quiet window to a provider-safe prefix for 10,000 sources", async () => { + const input = fixtures(); + input.store.readPlan.mockResolvedValue({ + ...(await input.store.readPlan()), + dynamicSources: Array.from({ length: 10_000 }, (_value, index) => ({ + sourceAddress: `0x${(index + 1).toString(16).padStart(40, "0")}`, + contractName: "ClassicV3RewardVault", + })) as never, + provisionalSourceAddresses: [], + }); + input.captureSafeHead.mockResolvedValue({ + providerHeads: ["10012", "10013"], + safeBlockNumber: "10000", + safeBlockHash: SAFE_HASH, + cursorBlockHash: CURSOR_HASH, + } as never); + input.envio.readProgress.mockResolvedValue({ progressBlock: "10000" }); + input.envio.readCandidatesWindow.mockReset().mockResolvedValue([]); + const batchingClient = { + getBlocks: vi.fn(), + getTransactionReceipts: vi.fn(), + getBytecodes: vi.fn(), + getLogsBatch: vi.fn(), + }; + + await runProjectorCycle({ + ...input, + providers: [ + { client: batchingClient } as never, + { client: { ...batchingClient } } as never, + ], + deadlineMs: 1_000, + }); + + const request = input.verifyWindow.mock.calls.at(-1)?.[0]; + const throughBlock = BigInt(request?.through.blockNumber ?? "0"); + expect(throughBlock).toBeGreaterThanOrEqual(100n); + expect(throughBlock).toBeLessThan(4_599n); + expect(throughBlock - 100n + 1n).toBeLessThanOrEqual(620n); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ snapshotBlock: throughBlock.toString() }), + ); + }); + + it("never commits or advances after coverage failure", async () => { + const input = fixtures(); + input.verifyWindow.mockRejectedValue( + validationError("rpc", "coverage-omission"), + ); + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("enters bounded recovery only for a cursor orphan agreed by both providers", async () => { + const input = fixtures(); + input.captureSafeHead.mockRejectedValue( + validationError("rpc", "safe-head-cursor-orphaned"), + ); + const target = { + kind: "genesis" as const, + historyGeneration: "0" as const, + genesisPointId: "70000000-0000-4000-8000-000000000006", + blockNumber: "0", + blockHash: CURSOR_HASH, + blockGlobalLogIndex: null, + candidateId: null, + providerIdentities: ["alchemy-mainnet", "quicknode-mainnet"] as const, + providerEndpointCommitments: [SAFE_HASH, CURSOR_HASH] as const, + providerOriginCommitments: [SAFE_HASH, CURSOR_HASH] as const, + providerBlockHashes: [CURSOR_HASH, CURSOR_HASH] as const, + providerBlockTimestamps: ["1000", "1000"] as const, + providerChainIds: [1, 1] as const, + providerHeads: ["220", "221"] as const, + finalityDepth: "12", + safeBlockNumber: "208", + safeBlockHash: SAFE_HASH, + providerSafeBlockHashes: [SAFE_HASH, SAFE_HASH] as const, + checkedDepth: 1, + }; + const findCanonicalAncestor = vi.fn(async () => target); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + findCanonicalAncestor: findCanonicalAncestor as never, + deadlineMs: 1_000, + }), + ).resolves.toEqual({ + status: "recovered-reorg", + candidateCount: 0, + generation: "6", + reorgGeneration: "1", + releaseCheckpointCount: 5, + snapshotBlock: "0", + }); + expect(input.store.readReorgRecoveryState).toHaveBeenCalledTimes(1); + expect(findCanonicalAncestor).toHaveBeenCalledWith( + expect.objectContaining({ + ancestors: [], + genesis: expect.objectContaining({ historyGeneration: "0" }), + policy: expect.objectContaining({ + maximumDepth: 128, + maxProviderCalls: 128, + }), + }), + ); + expect(input.store.recoverCanonicalReorg).toHaveBeenCalledWith( + expect.objectContaining({ + recovery: expect.objectContaining({ + expectedGeneration: "5", + nextGeneration: "6", + expectedReorgGeneration: "0", + nextReorgGeneration: "1", + targetHistoryGeneration: "0", + targetBlockNumber: "0", + }), + }), + ); + expect(input.envio.readProgress).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("never enters recovery on provider disagreement", async () => { + const input = fixtures(); + input.captureSafeHead.mockRejectedValue( + validationError("rpc", "safe-head-provider-disagreement"), + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ + code: "validation_failed", + safeMetadata: { operation: "safe-head-provider-disagreement" }, + }); + expect(input.store.readReorgRecoveryState).not.toHaveBeenCalled(); + expect(input.store.recoverCanonicalReorg).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("fails closed when the recovery generation changes after the plan read", async () => { + const input = fixtures(); + input.captureSafeHead.mockRejectedValue( + validationError("rpc", "safe-head-cursor-orphaned"), + ); + input.store.readReorgRecoveryState.mockResolvedValue({ + ...(await input.store.readReorgRecoveryState()), + currentReorgGeneration: "1", + }); + const findCanonicalAncestor = vi.fn(); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + findCanonicalAncestor: findCanonicalAncestor as never, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ + dependency: "postgres", + code: "invalid_input", + }); + expect(findCanonicalAncestor).not.toHaveBeenCalled(); + expect(input.store.recoverCanonicalReorg).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("stages an activation in a bounded cycle before page commit", async () => { + const input = fixtures(); + const vault = "0x4cfe000000000000000000000000000000000001" as const; + const parent = candidate({ + blockNumber: 101, + logIndex: 7, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { vault }, + }); + const launch = candidate({ + blockNumber: 101, + logIndex: 9, + contractName: "ClassicV3Launcher", + eventName: "MemeTokenLaunchedV2", + decodedPayload: { rewardVault: vault }, + }); + const child = candidate({ + blockNumber: 101, + logIndex: 10, + sourceAddress: vault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + }); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce([parent, launch, child]) + .mockResolvedValueOnce([]); + const baseReadPlan = input.store.readPlan.getMockImplementation()!; + input.store.readPlan.mockImplementation((async () => ({ + ...(await baseReadPlan()), + dynamicSources: [{ sourceAddress: vault } as never], + })) as never); + const pending = pendingActivation({ + parent, + launch, + sourceAddress: vault, + }); + const order: string[] = []; + const baseVerifyWindow = input.verifyWindow.getMockImplementation()!; + input.verifyWindow.mockImplementation(async (request) => { + order.push("activation-evidence"); + return baseVerifyWindow(request); + }); + input.store.resolvePendingDynamicSourceActivations.mockImplementation( + async () => { + order.push("resolve"); + return [pending]; + }, + ); + const verifyClassicV3Activation = vi.fn(async () => { + order.push("verify-model"); + return { + runtimeObservation: {}, + modelVerificationEvidence: [], + } as never; + }); + input.store.stageVerifiedDynamicSourceActivations.mockImplementation( + async () => { + order.push("stage"); + }, + ); + input.store.commitVerifiedPage.mockImplementation(async () => { + order.push("commit"); + return { generation: "6" }; + }); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + verifyClassicV3Activation, + }), + ).resolves.toEqual({ + status: "staged-dynamic-parent", + candidateCount: 1, + snapshotBlock: "101", + }); + + expect(order).toEqual([ + "resolve", + "activation-evidence", + "verify-model", + "stage", + ]); + const activationVerification = input.verifyWindow.mock.calls[0]![0] as { + dynamicSources?: readonly { sourceAddress: string }[]; + maximumCandidateCount?: number; + }; + expect(activationVerification.dynamicSources).toEqual([ + pending.ephemeralLineage, + ]); + expect(activationVerification.maximumCandidateCount).toBe(4096); + expect( + new Set( + activationVerification.dynamicSources?.map( + ({ sourceAddress }) => sourceAddress, + ), + ).size, + ).toBe(activationVerification.dynamicSources?.length); + expect(verifyClassicV3Activation).toHaveBeenCalledWith( + expect.objectContaining({ + activationId: pending.activationId, + parentCandidate: parent, + launchCandidate: launch, + sameBlockVaultEvents: [child], + candidateEvidence: expect.objectContaining({ + coverage: expect.objectContaining({ + throughBlockNumber: "101", + throughBlockGlobalLogIndex: "4294967295", + }), + }), + }), + ); + expect( + input.store.stageVerifiedDynamicSourceActivations, + ).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: [parent, launch, child], + blockComplete: false, + activations: [expect.objectContaining({ pending })], + }), + ); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("verifies independent activation blocks before staging them in one cycle", async () => { + const input = fixtures(); + const firstVault = "0x4cfe000000000000000000000000000000000001" as const; + const secondVault = "0x4cfe000000000000000000000000000000000002" as const; + const firstParent = candidate({ + blockNumber: 101, + logIndex: 7, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { vault: firstVault }, + }); + const firstLaunch = candidate({ + blockNumber: 101, + logIndex: 9, + contractName: "ClassicV3Launcher", + eventName: "MemeTokenLaunchedV2", + decodedPayload: { rewardVault: firstVault }, + }); + const firstChild = candidate({ + blockNumber: 101, + logIndex: 10, + sourceAddress: firstVault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + }); + const secondParent = candidate({ + blockNumber: 102, + logIndex: 7, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { vault: secondVault }, + }); + const secondLaunch = candidate({ + blockNumber: 102, + logIndex: 9, + contractName: "ClassicV3Launcher", + eventName: "MemeTokenLaunchedV2", + decodedPayload: { rewardVault: secondVault }, + }); + const secondChild = candidate({ + blockNumber: 102, + logIndex: 10, + sourceAddress: secondVault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + }); + const allCandidates = [ + firstParent, + firstLaunch, + firstChild, + secondParent, + secondLaunch, + secondChild, + ]; + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce(allCandidates) + .mockResolvedValueOnce([]); + const baseReadPlan = input.store.readPlan.getMockImplementation()!; + input.store.readPlan.mockImplementation((async () => ({ + ...(await baseReadPlan()), + dynamicSources: [ + { sourceAddress: firstVault } as never, + { sourceAddress: secondVault } as never, + ], + })) as never); + const firstPending = pendingActivation({ + parent: firstParent, + launch: firstLaunch, + sourceAddress: firstVault, + activationId: "10000000-0000-4000-8000-000000000098", + }); + const secondPending = pendingActivation({ + parent: secondParent, + launch: secondLaunch, + sourceAddress: secondVault, + activationId: "10000000-0000-4000-8000-000000000099", + }); + input.store.resolvePendingDynamicSourceActivations.mockResolvedValue([ + secondPending, + firstPending, + ]); + const order: string[] = []; + const verifyClassicV3Activation = vi.fn(async ({ activationId }) => { + order.push(`verify:${activationId}`); + return { + runtimeObservation: {}, + modelVerificationEvidence: [], + } as never; + }); + input.store.stageVerifiedDynamicSourceActivations.mockImplementation((async ( + stageInput: { + activations: readonly { + pending: PendingDynamicSourceActivation; + }[]; + }, + ) => { + order.push(`stage:${stageInput.activations[0]!.pending.activationId}`); + }) as never); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + verifyClassicV3Activation, + }), + ).resolves.toEqual({ + status: "staged-dynamic-parent", + candidateCount: 2, + snapshotBlock: "102", + }); + + expect(verifyClassicV3Activation).toHaveBeenCalledTimes(2); + expect( + input.store.stageVerifiedDynamicSourceActivations, + ).toHaveBeenCalledTimes(2); + expect(input.store.stageVerifiedDynamicSourceActivations) + .toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + candidates: [firstParent, firstLaunch, firstChild], + }), + ); + expect(input.store.stageVerifiedDynamicSourceActivations) + .toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ candidates: allCandidates }), + ); + expect(order.slice(0, 2).every((entry) => entry.startsWith("verify:"))) + .toBe(true); + expect(order.slice(2)).toEqual([ + `stage:${firstPending.activationId}`, + `stage:${secondPending.activationId}`, + ]); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("does not stage or commit when activation evidence RPC verification fails", async () => { + const input = fixtures(); + const vault = "0x4cfe000000000000000000000000000000000001" as const; + const parent = candidate({ blockNumber: 101, logIndex: 7 }); + const launch = candidate({ blockNumber: 101, logIndex: 9 }); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce([parent, launch]) + .mockResolvedValueOnce([]); + input.store.resolvePendingDynamicSourceActivations.mockResolvedValue([ + pendingActivation({ parent, launch, sourceAddress: vault }), + ]); + input.verifyWindow.mockRejectedValueOnce( + validationError("rpc", "coverage-omission"), + ); + const verifyClassicV3Activation = vi.fn(); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + verifyClassicV3Activation: verifyClassicV3Activation as never, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + expect(verifyClassicV3Activation).not.toHaveBeenCalled(); + expect( + input.store.stageVerifiedDynamicSourceActivations, + ).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("does not stage or commit when activation model verification fails", async () => { + const input = fixtures(); + const vault = "0x4cfe000000000000000000000000000000000001" as const; + const parent = candidate({ blockNumber: 101, logIndex: 7 }); + const launch = candidate({ blockNumber: 101, logIndex: 9 }); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce([parent, launch]) + .mockResolvedValueOnce([]); + input.store.resolvePendingDynamicSourceActivations.mockResolvedValue([ + pendingActivation({ parent, launch, sourceAddress: vault }), + ]); + const verifyClassicV3Activation = vi.fn().mockRejectedValue( + validationError("rpc", "activation-model-runtime"), + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + verifyClassicV3Activation: verifyClassicV3Activation as never, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + expect(verifyClassicV3Activation).toHaveBeenCalledTimes(1); + expect( + input.store.stageVerifiedDynamicSourceActivations, + ).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("stages a same-block factory parent without advancing public state", async () => { + const input = fixtures(); + const vault = "0x4cfe000000000000000000000000000000000001" as const; + const parent = candidate({ + blockNumber: 101, + logIndex: 7, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { vault }, + }); + const child = candidate({ + blockNumber: 101, + logIndex: 8, + sourceAddress: vault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + }); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce([parent, child]) + .mockResolvedValueOnce([]); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).resolves.toEqual({ + status: "staged-dynamic-parent", + candidateCount: 1, + snapshotBlock: "101", + }); + + expect(input.store.stageVerifiedDynamicParents).toHaveBeenCalledWith( + expect.objectContaining({ + snapshotBlock: "101", + candidates: [parent], + runtimeObservations: [ + expect.objectContaining({ + parentCandidateId: parent.candidateId, + sourceAddress: vault, + deploymentBlockNumber: "101", + deploymentBlockHash: parent.blockHash, + providerCallCounts: [1, 1], + }), + ], + blockComplete: false, + }), + ); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + expect(input.verifyWindow).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: [parent], + cursor: { + blockNumber: "100", + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "", + }, + through: { + blockNumber: "101", + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "empty-page", + }, + coverageSourceAddresses: [CLASSIC_V3_FACTORY], + maximumCandidateCount: 4096, + }), + ); + expect(input.verifyDynamicRuntime).toHaveBeenCalledWith( + expect.objectContaining({ + parentCandidate: parent, + sourceAddress: vault, + deploymentBlockNumber: "101", + deploymentBlockHash: parent.blockHash, + template: expect.objectContaining({ + templateId: dynamicTemplate().templateId, + }), + parentEvidence: expect.any(Object), + deadlineMs: expect.any(Number), + }), + ); + }); + + it.each([2, 32, 33])( + "stages all %i previously unknown parents from one block as one page", + async (count) => { + const input = fixtures(); + const parents = Array.from({ length: count }, (_, index) => + candidate({ + blockNumber: 101, + logIndex: index, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { + vault: `0x${(index + 1).toString(16).padStart(40, "0")}`, + configurationHash: CURSOR_HASH, + }, + }), + ); + let offset = 0; + input.envio.readCandidatesWindow.mockReset().mockImplementation( + async ({ limit }: { limit: number }) => { + const page = parents.slice(offset, offset + limit); + offset += page.length; + return page; + }, + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 75_000, + }), + ).resolves.toEqual({ + status: "staged-dynamic-parent", + candidateCount: count, + snapshotBlock: "101", + }); + + expect(input.store.stageVerifiedDynamicParents).toHaveBeenCalledTimes(1); + expect(input.store.stageVerifiedDynamicParents).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: parents, + runtimeObservations: expect.arrayContaining( + parents.map((parent) => + expect.objectContaining({ + parentCandidateId: parent.candidateId, + }), + ), + ), + blockComplete: false, + }), + ); + expect(input.verifyDynamicRuntime).toHaveBeenCalledTimes(count); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }, + ); + + it("verifies and stages several factory blocks in one cycle", async () => { + const input = fixtures(); + const parents = [101, 102, 103].map((blockNumber, index) => + candidate({ + blockNumber, + logIndex: index, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { + vault: `0x${(index + 1).toString(16).padStart(40, "0")}`, + configurationHash: CURSOR_HASH, + }, + }), + ); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce(parents) + .mockResolvedValueOnce([]); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 75_000, + }), + ).resolves.toEqual({ + status: "staged-dynamic-parent", + candidateCount: 3, + snapshotBlock: "103", + }); + + expect(input.verifyWindow).toHaveBeenCalledTimes(3); + expect(input.store.stageVerifiedDynamicParents).toHaveBeenCalledTimes(3); + for (const parent of parents) { + expect(input.store.stageVerifiedDynamicParents).toHaveBeenCalledWith( + expect.objectContaining({ + snapshotBlock: parent.blockNumber, + candidates: [parent], + blockComplete: false, + }), + ); + } + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("does not stage or advance when the child runtime cannot be proved", async () => { + const input = fixtures(); + const vault = "0x4cfe000000000000000000000000000000000001" as const; + const parent = candidate({ + blockNumber: 101, + logIndex: 7, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { vault }, + }); + const child = candidate({ + blockNumber: 101, + logIndex: 8, + sourceAddress: vault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + }); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce([parent, child]) + .mockResolvedValueOnce([]); + input.verifyDynamicRuntime.mockRejectedValue( + validationError("rpc", "dynamic-runtime-code-agreement"), + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + expect(input.store.stageVerifiedDynamicParents).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("does not stage a child emitted before its claimed factory parent", async () => { + const input = fixtures(); + const vault = "0x4cfe000000000000000000000000000000000001" as const; + const child = candidate({ + blockNumber: 101, + logIndex: 7, + sourceAddress: vault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + }); + const parent = candidate({ + blockNumber: 101, + logIndex: 8, + sourceAddress: CLASSIC_V3_FACTORY, + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { vault }, + }); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce([child, parent]) + .mockResolvedValueOnce([]); + input.verifyWindow.mockRejectedValue( + validationError("rpc", "dynamic-source-lineage"), + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).rejects.toMatchObject({ code: "validation_failed" }); + expect(input.store.stageVerifiedDynamicParents).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("replays the complete block after staged lineage becomes current", async () => { + const input = fixtures(); + const vault = "0x4cfe000000000000000000000000000000000001" as const; + const parent = candidate({ + blockNumber: 101, + logIndex: 7, + sourceAddress: "0xf28967f9dfac3ca21384b59d6d75c8106b3eab2a", + contractName: "ClassicV3RewardVaultFactory", + eventName: "ClassicRewardVaultDeployed", + decodedPayload: { vault }, + }); + const child = candidate({ + blockNumber: 101, + logIndex: 8, + sourceAddress: vault, + contractName: "ClassicV3RewardVault", + eventName: "CreatorFeesCheckpointed", + }); + const basePlan = { + cursor: { + generation: "5", + blockNumber: "100", + blockHash: CURSOR_HASH, + blockGlobalLogIndex: -1, + candidateId: "", + isBlockBoundary: false, + }, + dynamicSources: [], + provisionalSourceAddresses: [], + dynamicSourceTemplates: [dynamicTemplate()], + database: { + epochId: "70000000-0000-4000-8000-000000000002", + pointerGeneration: "1", + reorgGeneration: "0", + envioProviderDeploymentId: + "70000000-0000-4000-8000-000000000003", + rpcProviderDeploymentIds: [ + "70000000-0000-4000-8000-000000000004", + "70000000-0000-4000-8000-000000000005", + ] as const, + }, + }; + input.store.readPlan + .mockResolvedValueOnce(basePlan) + .mockResolvedValueOnce({ + ...basePlan, + dynamicSources: [{ sourceAddress: vault } as never], + provisionalSourceAddresses: [], + }); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce([parent, child]) + .mockResolvedValueOnce([parent, child]); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).resolves.toMatchObject({ status: "staged-dynamic-parent" }); + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).resolves.toMatchObject({ + status: "committed", + candidateCount: 2, + snapshotBlock: "101", + }); + + expect(input.store.stageVerifiedDynamicParents).toHaveBeenCalledTimes(1); + expect(input.store.commitVerifiedPage).toHaveBeenCalledTimes(1); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: [parent, child], + blockComplete: true, + }), + ); + }); + + it("publishes the preferred complete block before the next page", async () => { + const input = fixtures(); + const values = [ + ...Array.from({ length: 32 }, (_, index) => + candidate({ blockNumber: 101, logIndex: index }), + ), + ...Array.from({ length: 7 }, (_, index) => + candidate({ blockNumber: 102, logIndex: index }), + ), + ]; + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce(values.slice(0, 32)) + .mockResolvedValueOnce(values.slice(32, 33)); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }), + ).resolves.toMatchObject({ + status: "committed", + candidateCount: 32, + snapshotBlock: "101", + }); + + expect(input.verifyWindow).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: values.slice(0, 32), + through: { + blockNumber: "101", + blockGlobalLogIndex: 0xffff_ffff, + candidateId: "empty-page", + }, + }), + ); + }); + + it("does not infer that an exact-size final candidate page ends its block", async () => { + const input = fixtures(); + const values = Array.from({ length: 32 }, (_, index) => + candidate({ blockNumber: 101, logIndex: index }), + ); + input.envio.readCandidatesWindow + .mockReset() + .mockResolvedValueOnce(values) + .mockResolvedValueOnce([]); + + await runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }); + + expect(input.envio.readCandidatesWindow).toHaveBeenCalledTimes(2); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: values, + snapshotBlock: "101", + blockComplete: true, + }), + ); + }); + + it("cuts a capped Envio window back to the last complete block", async () => { + const input = fixtures(); + const firstBlock = Array.from({ length: 20 }, (_, index) => + candidate({ blockNumber: 101, logIndex: index }), + ); + const nextBlock = Array.from({ length: 4077 }, (_, index) => + candidate({ blockNumber: 102, logIndex: index }), + ); + const values = [...firstBlock, ...nextBlock]; + let offset = 0; + input.envio.readCandidatesWindow.mockReset().mockImplementation( + async ({ limit }: { limit: number }) => { + const page = values.slice(offset, offset + limit); + offset += page.length; + return page; + }, + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 75_000, + }), + ).resolves.toMatchObject({ + candidateCount: 20, + snapshotBlock: "101", + }); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: firstBlock, + snapshotBlock: "101", + blockComplete: true, + }), + ); + }); + + it("commits a preferred-size complete prefix before the atomic ceiling", async () => { + const input = fixtures(); + const firstBlock = Array.from({ length: 30 }, (_, index) => + candidate({ blockNumber: 101, logIndex: index }), + ); + const nextBlock = Array.from({ length: 20 }, (_, index) => + candidate({ blockNumber: 102, logIndex: index }), + ); + const values = [...firstBlock, ...nextBlock]; + let offset = 0; + input.envio.readCandidatesWindow.mockReset().mockImplementation( + async ({ limit }: { limit: number }) => { + const page = values.slice(offset, offset + limit); + offset += page.length; + return page; + }, + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 75_000, + }), + ).resolves.toMatchObject({ + candidateCount: 30, + snapshotBlock: "101", + }); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: firstBlock, + snapshotBlock: "101", + blockComplete: true, + }), + ); + }); + + it.each([32, 33, 4096])( + "commits an exactly complete %i-candidate block", + async (count) => { + const input = fixtures(); + const values = Array.from({ length: count }, (_, index) => + candidate({ blockNumber: 101, logIndex: index }), + ); + let offset = 0; + input.envio.readCandidatesWindow.mockReset().mockImplementation( + async ({ limit }: { limit: number }) => { + const page = values.slice(offset, offset + limit); + offset += page.length; + return page; + }, + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 75_000, + }), + ).resolves.toMatchObject({ + status: "committed", + candidateCount: count, + snapshotBlock: "101", + }); + expect(input.store.commitVerifiedPage).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: values, + blockComplete: true, + }), + ); + if (count === 4096) { + expect(input.envio.readCandidatesWindow).toHaveBeenCalledTimes(129); + } + }, + ); + + it("fails closed when one block contains 4097 candidates", async () => { + const input = fixtures(); + const values = Array.from({ length: 4097 }, (_, index) => + candidate({ blockNumber: 101, logIndex: index }), + ); + let offset = 0; + input.envio.readCandidatesWindow.mockReset().mockImplementation( + async ({ limit }: { limit: number }) => { + const page = values.slice(offset, offset + limit); + offset += page.length; + return page; + }, + ); + + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 75_000, + }), + ).rejects.toMatchObject({ + dependency: "envio", + code: "response_oversize", + retryable: false, + }); + expect(input.verifyWindow).not.toHaveBeenCalled(); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); + + it("caps each Envio query below the atomic commit ceiling", async () => { + const input = fixtures(); + await runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 1_000, + }); + expect(input.envio.readCandidatesWindow).toHaveBeenCalledWith( + expect.objectContaining({ limit: 32 }), + ); + }); + + it("collects a larger reviewed cutover window without changing page size", async () => { + const input = fixtures(); + const values = Array.from({ length: 512 }, (_, index) => + candidate({ + blockNumber: 101 + Math.floor(index / 32), + logIndex: index % 32, + }), + ); + let offset = 0; + input.envio.readCandidatesWindow.mockReset().mockImplementation( + async ({ limit }: { limit: number }) => { + const page = values.slice(offset, offset + limit); + offset += page.length; + return page; + }, + ); + + await expect(runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 75_000, + preferredCandidatesPerCommit: 512, + })).resolves.toMatchObject({ + status: "committed", + candidateCount: 512, + snapshotBlock: "116", + }); + expect(input.envio.readCandidatesWindow).toHaveBeenCalledTimes(17); + expect(input.envio.readCandidatesWindow).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ limit: 32 }), + ); + }); + + it("fails closed on the overall deadline before database commit", async () => { + const input = fixtures(); + input.captureSafeHead.mockImplementation( + () => new Promise(() => undefined), + ); + await expect( + runProjectorCycle({ + ...input, + providers: [] as never, + deadlineMs: 20, + }), + ).rejects.toMatchObject({ dependency: "rpc", code: "timeout" }); + expect(input.store.commitVerifiedPage).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/data-pipeline/projector-wake-route.test.ts b/tests/data-pipeline/projector-wake-route.test.ts new file mode 100644 index 00000000..99c02d7f --- /dev/null +++ b/tests/data-pipeline/projector-wake-route.test.ts @@ -0,0 +1,129 @@ +import { createHmac } from "node:crypto"; + +import { NextRequest } from "next/server"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const mocks = vi.hoisted(() => ({ + after: vi.fn(), + source: vi.fn(), + market: vi.fn(), + safeMarketError: vi.fn(() => ({ + dependency: "market-projector", + code: "internal_error", + retryable: false, + })), +})); + +vi.mock("next/server", async () => { + const actual = await vi.importActual( + "next/server", + ); + return { ...actual, after: mocks.after }; +}); + +vi.mock("../../lib/data-pipeline/projector-runtime-config.server", () => ({ + runConfiguredProjectorCycle: mocks.source, +})); + +vi.mock("../../lib/data-pipeline/market-projector-runtime.server", () => ({ + runConfiguredMarketProjectorCycle: mocks.market, + safeMarketProjectorError: mocks.safeMarketError, +})); + +import { POST } from "../../app/api/ops/projector-wake/route"; + +const SECRET = "quicknode-stream-secret-at-least-32-bytes"; + +function request(input: Readonly<{ signature?: string }> = {}) { + const payload = JSON.stringify({ block: { number: "0x123" } }); + const timestamp = String(Math.floor(Date.now() / 1_000)); + const nonce = "0123456789abcdef0123456789abcdef"; + const signature = + input.signature ?? + createHmac("sha256", SECRET) + .update(nonce) + .update(timestamp) + .update(payload) + .digest("hex"); + return new NextRequest( + "https://programmable.family/api/ops/projector-wake", + { + method: "POST", + headers: { + "content-type": "application/json", + "x-qn-nonce": nonce, + "x-qn-timestamp": timestamp, + "x-qn-signature": signature, + }, + body: payload, + }, + ); +} + +describe("projector stream wake route", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubEnv("PROGRAMMABLE_QUICKNODE_STREAM_SECRET", SECRET); + mocks.source.mockResolvedValue({ status: "caught-up" }); + mocks.market.mockResolvedValue({ status: "caught-up" }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("acknowledges a valid webhook before scheduling sequential projectors", async () => { + let backgroundTask: (() => Promise) | undefined; + mocks.after.mockImplementation((task: () => Promise) => { + backgroundTask = task; + }); + + const response = await POST(request()); + + expect(response.status).toBe(202); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ accepted: true }); + expect(mocks.source).not.toHaveBeenCalled(); + expect(mocks.market).not.toHaveBeenCalled(); + + await backgroundTask?.(); + expect(mocks.source).toHaveBeenCalledTimes(1); + expect(mocks.market).toHaveBeenCalledTimes(1); + expect(mocks.source.mock.invocationCallOrder[0]).toBeLessThan( + mocks.market.mock.invocationCallOrder[0]!, + ); + }); + + it("does not schedule work for an invalid signature", async () => { + const response = await POST(request({ signature: "00".repeat(32) })); + + expect(response.status).toBe(401); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + expect(mocks.after).not.toHaveBeenCalled(); + }); + + it("fails closed when the webhook secret is not configured", async () => { + vi.stubEnv("PROGRAMMABLE_QUICKNODE_STREAM_SECRET", ""); + const response = await POST(request()); + + expect(response.status).toBe(503); + expect(await response.json()).toEqual({ error: "Wake trigger unavailable" }); + expect(mocks.after).not.toHaveBeenCalled(); + }); + + it("still runs the market catch-up when the source cycle fails", async () => { + let backgroundTask: (() => Promise) | undefined; + mocks.after.mockImplementation((task: () => Promise) => { + backgroundTask = task; + }); + mocks.source.mockRejectedValue(new Error("source unavailable")); + + const response = await POST(request()); + expect(response.status).toBe(202); + await backgroundTask?.(); + + expect(mocks.market).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/data-pipeline/provider-evidence-v2.test.ts b/tests/data-pipeline/provider-evidence-v2.test.ts new file mode 100644 index 00000000..be3959d7 --- /dev/null +++ b/tests/data-pipeline/provider-evidence-v2.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import fixture from "../../supabase/tests/codec/provider-evidence-v2.json"; +import { providerEvidenceV2 } from "../../lib/data-pipeline/provider-evidence"; + +describe("provider evidence v2 production codec", () => { + for (const vector of fixture.vectors) { + it(`matches the frozen ${vector.name} vector`, () => { + const evidence = providerEvidenceV2( + vector.subtype as Parameters[0], + vector.input, + ); + + expect(`0x${Buffer.from(evidence.canonicalPreimage).toString("hex")}`).toBe( + vector.expected_preimage_hex, + ); + expect(evidence.contentFingerprint).toBe(vector.expected_keccak256); + }); + } + + it("rejects extra fields, mixed-case bytes and malformed UUIDs", () => { + const vector = fixture.vectors[0]!; + expect(() => + providerEvidenceV2("safe_head", { ...vector.input, extra: "field" }), + ).toThrow(); + expect(() => + providerEvidenceV2("safe_head", { + ...vector.input, + safe_block_hash_a: `0x${"AA".repeat(32)}`, + }), + ).toThrow(); + expect(() => + providerEvidenceV2("safe_head", { + ...vector.input, + epoch_id: "not-a-uuid", + }), + ).toThrow(); + }); +}); diff --git a/tests/data-pipeline/provider-evidence-v3.test.ts b/tests/data-pipeline/provider-evidence-v3.test.ts new file mode 100644 index 00000000..210ac096 --- /dev/null +++ b/tests/data-pipeline/provider-evidence-v3.test.ts @@ -0,0 +1,256 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + projectionExecutionTraceCommitmentV1, + projectionExecutionTracePreimageV1, + providerEvidenceV3, + providerEvidenceV3ContractCommitment, +} from "../../lib/data-pipeline/provider-evidence"; + +const bytes32 = (byte: string) => `0x${byte.repeat(64)}`; + +const executionInput = Object.freeze({ + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + source_group: "core", + epoch_id: "70000000-0000-4000-8000-000000000020", + pointer_generation: "1", + run_id: "80000000-0000-4000-8000-000000000001", + provider_a_id: "10000000-0000-4000-8000-000000000002", + provider_b_id: "10000000-0000-4000-8000-000000000003", + provider_a_identity: "alchemy-mainnet-11111111111111111111111111111111", + provider_b_identity: "quicknode-mainnet-55555555555555555555555555555555", + provider_a_vendor_group: "alchemy", + provider_b_vendor_group: "quicknode", + provider_a_endpoint_commitment: bytes32("3"), + provider_b_endpoint_commitment: bytes32("5"), + provider_a_origin_commitment: bytes32("4"), + provider_b_origin_commitment: bytes32("6"), + provider_a_call_count: 6, + provider_b_call_count: 6, + candidate_batch_size: 40, + hard_deadline_ms: 75_000, + maximum_calls_per_provider: 128, + elapsed_ms: 2, + execution_trace_commitment: bytes32("7"), +}); + +const rewardInput = Object.freeze({ + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + source_group: "core", + epoch_id: "70000000-0000-4000-8000-000000000020", + pointer_generation: "1", + run_id: "80000000-0000-4000-8000-000000000001", + projection_execution_evidence_id: + "81000000-0000-4000-8000-000000000001", + block_evidence_id: "82000000-0000-4000-8000-000000000001", + vault: `0x${"8".repeat(40)}`, + reward_model: "classic-v3", + block_number: "25639601", + block_hash: bytes32("9"), + provider_a_id: "10000000-0000-4000-8000-000000000002", + provider_b_id: "10000000-0000-4000-8000-000000000003", + provider_a_snapshot_commitment: bytes32("a"), + provider_b_snapshot_commitment: bytes32("a"), + provider_a_call_count: 14, + provider_b_call_count: 14, + verification_accounts: [ + `0x${"1".repeat(40)}`, + `0x${"2".repeat(40)}`, + ], + verification_account_chunk_end_offsets: [2], + provider_a_verification_chunk_commitments: [bytes32("d")], + provider_b_verification_chunk_commitments: [bytes32("d")], + provider_a_verification_chunk_call_counts: [14], + provider_b_verification_chunk_call_counts: [14], + folded_snapshot_commitment: bytes32("b"), + execution_trace_commitment: bytes32("c"), +}); + +describe("provider evidence v3 projection codecs", () => { + it("uses a new immutable version and subtype frame", () => { + const execution = providerEvidenceV3( + "projection_execution", + executionInput, + ); + const reward = providerEvidenceV3("reward_snapshot", rewardInput); + + expect(execution.encodingVersion).toBe(3); + expect(reward.encodingVersion).toBe(3); + expect(Buffer.from(execution.canonicalPreimage).subarray(0, 35)).toEqual( + Buffer.concat([ + Buffer.from("programmable:provider-evidence:v3\0", "utf8"), + Buffer.from([6]), + ]), + ); + expect(Buffer.from(reward.canonicalPreimage).subarray(0, 35)).toEqual( + Buffer.concat([ + Buffer.from("programmable:provider-evidence:v3\0", "utf8"), + Buffer.from([7]), + ]), + ); + expect(execution.contentFingerprint).toBe( + "0x9ce10c58b04e1d21bb51f78092e358ecc125aeef8ae597f88eae12dcf029cc4d", + ); + expect(reward.contentFingerprint).toBe( + "0x0bc3258a5ca6d74ac6710e5e2ba218d6c9f4ec46cda0e4fb4777e3799b5ddb54", + ); + expect(Buffer.from(reward.canonicalPreimage).toString("hex")).toBe( + "70726f6772616d6d61626c653a70726f76696465722d65766964656e63653a7633000700000000000000010000000a636c61737369632d763300000007636c617373696300000004636f726570000000000040008000000000000020000000000000000180000000000040008000000000000001810000000000400080000000000000018200000000004000800000000000000188888888888888888888888888888888888888880000000a636c61737369632d76330000000001873ab199999999999999999999999999999999999999999999999999999999999999991000000000004000800000000000000210000000000040008000000000000003aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000e0000000e0000000211111111111111111111111111111111111111112222222222222222222222222222222222222222000000010000000200000001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd00000001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd000000010000000e000000010000000ebbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + ); + expect(providerEvidenceV3ContractCommitment()).toBe( + "0x3234e87ac53489e1cfefafa865b053e9723945930d060265c0e8084669a1e955", + ); + }); + + it("rejects unknown fields and noncanonical account arrays", () => { + expect(() => + providerEvidenceV3("projection_execution", { + ...executionInput, + extra: true, + }), + ).toThrow(); + expect(() => + providerEvidenceV3("reward_snapshot", { + ...rewardInput, + verification_accounts: [`0x${"AA".repeat(20)}`], + }), + ).toThrow(); + }); + + it("freezes a structural execution trace independent of JSON key order", () => { + const call = { + providerIdentity: "alchemy-mainnet-11111111111111111111111111111111", + providerVendorGroup: "alchemy", + providerEndpointCommitment: bytes32("3"), + providerOriginCommitment: bytes32("4"), + operation: "getTransactionReceipt", + attempt: 1, + startedOffsetMs: 3, + durationMs: 2, + outcome: "success", + }; + const trace = { + startedAtMs: 1_775_000_000_000, + completedAtMs: 1_775_000_000_005, + candidateBatchSize: 1, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 128, + elapsedMs: 5, + providerCallCounts: [1, 0], + calls: [call], + }; + const reordered = { + calls: [{ + outcome: "success", + durationMs: 2, + startedOffsetMs: 3, + attempt: 1, + operation: "getTransactionReceipt", + providerOriginCommitment: bytes32("4"), + providerEndpointCommitment: bytes32("3"), + providerVendorGroup: "alchemy", + providerIdentity: "alchemy-mainnet-11111111111111111111111111111111", + }], + providerCallCounts: [1, 0], + elapsedMs: 5, + maxCallsPerProvider: 128, + hardDeadlineMs: 75_000, + candidateBatchSize: 1, + completedAtMs: 1_775_000_000_005, + startedAtMs: 1_775_000_000_000, + }; + + expect( + Buffer.from(projectionExecutionTracePreimageV1(trace)).toString("hex"), + ).toBe( + Buffer.from(projectionExecutionTracePreimageV1(reordered)).toString( + "hex", + ), + ); + expect(projectionExecutionTraceCommitmentV1(trace)).toBe( + "0x466d9059a360712fd7d40fc9a4fd326cf58ed7d8f4a7f93a31da4edd9bbdc620", + ); + }); + + it("rejects changed trace structure, call counts and enums", () => { + const trace = { + startedAtMs: 1, + completedAtMs: 2, + candidateBatchSize: 1, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 128, + elapsedMs: 1, + providerCallCounts: [0, 0], + calls: [], + }; + expect(() => projectionExecutionTracePreimageV1({ + ...trace, + extra: true, + })).toThrow(); + expect(() => projectionExecutionTracePreimageV1({ + ...trace, + providerCallCounts: [129, 0], + })).toThrow(); + expect(() => projectionExecutionTracePreimageV1({ + ...trace, + providerCallCounts: [1, 0], + calls: [{ + providerIdentity: "alchemy", + providerVendorGroup: "alchemy", + providerEndpointCommitment: bytes32("3"), + providerOriginCommitment: bytes32("4"), + operation: "unknown", + attempt: 1, + startedOffsetMs: 0, + durationMs: 1, + outcome: "success", + }], + })).toThrow(); + }); + + it("encodes reward traces whose logical calls summarize raw RPC counts", () => { + const providers = [ + { + identity: "alchemy-mainnet-11111111111111111111111111111111", + vendor: "alchemy", + endpoint: bytes32("3"), + origin: bytes32("4"), + }, + { + identity: "quicknode-mainnet-55555555555555555555555555555555", + vendor: "quicknode", + endpoint: bytes32("5"), + origin: bytes32("6"), + }, + ]; + const commitment = projectionExecutionTraceCommitmentV1({ + startedAtMs: 1_775_000_000_000, + completedAtMs: 1_775_000_000_005, + candidateBatchSize: 0, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 128, + elapsedMs: 5, + providerCallCounts: [14, 14], + calls: providers.map((provider) => ({ + providerIdentity: provider.identity, + providerVendorGroup: provider.vendor, + providerEndpointCommitment: provider.endpoint, + providerOriginCommitment: provider.origin, + operation: "readRewardSnapshot", + attempt: 1, + startedOffsetMs: 0, + durationMs: 5, + outcome: "success", + })), + }); + expect(commitment).toBe( + "0x387a035634613b9c1fcf9369aeea587e325815bef91d3d3945e56136d0472043", + ); + }); +}); diff --git a/tests/data-pipeline/public-route-queries.server.test.ts b/tests/data-pipeline/public-route-queries.server.test.ts new file mode 100644 index 00000000..55373be2 --- /dev/null +++ b/tests/data-pipeline/public-route-queries.server.test.ts @@ -0,0 +1,299 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { postgresPublicRouteQueries } from "../../lib/data-pipeline/public-route-queries.server"; +import type { PostgresTransaction } from "../../lib/data-pipeline/postgres"; + +const ADDRESS = "0x1111111111111111111111111111111111111111"; +const HASH = `0x${"22".repeat(32)}`; +const COMMITMENT = `0x${"33".repeat(32)}`; +const CLASSIC = [ + { model: "classic", releaseVersion: "classic-v3" }, +] as const; +const STOCK = [ + { model: "stock-paired", releaseVersion: "stock-paired-v1" }, + { model: "stock-paired", releaseVersion: "stock-paired-v2" }, + { model: "stock-paired", releaseVersion: "stock-paired-v3" }, +] as const; +const DISCOVERY = [ + { model: "classic", releaseVersion: "classic-v2" }, + ...CLASSIC, + ...STOCK, +] as const; + +function routeKeyFor(kind: string) { + if (kind === "explore") return "explore-list"; + if (kind === "token") return "explore-token"; + if (kind === "chart") return "explore-chart"; + if (kind === "classic-profile") return "classic-v3-profile"; + if (kind === "launch") return "launch-lookup"; + return "creator-profile"; +} + +function snapshot(kind: string, scope: readonly (typeof DISCOVERY)[number][]) { + const routeKey = routeKeyFor(kind); + return { + adapterVersion: "indexed-route-adapters-v2", + snapshotCommitment: COMMITMENT, + chainId: 1, + blockNumber: "100", + blockHash: HASH, + confirmations: 12, + capturedAt: "2026-07-31T10:00:00.000Z", + releasePointers: scope.map((member, index) => ({ + routeKey, + chainId: 1, + releaseVersion: member.releaseVersion, + modelVersion: member.model, + sourceGroup: `source-${index}`, + projectorVersion: "projector-v2", + epochId: `00000000-0000-4000-8000-00000000000${index + 1}`, + pointerGeneration: String(index + 1), + checkpointId: `10000000-0000-4000-8000-00000000000${index + 1}`, + checkpointGeneration: "2", + reorgGeneration: "0", + checkpointBlockNumber: "100", + checkpointBlockHash: HASH, + })), + }; +} + +function evidence(scope: readonly (typeof DISCOVERY)[number][]) { + return scope.map((member, index) => ({ + modelVersion: member.model, + releaseVersion: member.releaseVersion, + parityRecordId: `20000000-0000-4000-8000-00000000000${index + 1}`, + reconciliationId: `30000000-0000-4000-8000-00000000000${index + 1}`, + parityEvidenceCommitment: COMMITMENT, + parityBindingId: `40000000-0000-4000-8000-00000000000${index + 1}`, + parityBindingCommitment: COMMITMENT, + })); +} + +function row(input: { + kind: string; + scope: readonly (typeof DISCOVERY)[number][]; + data: Record; + status?: number; + recordScopes?: readonly Record[]; +}) { + return { + http_status: input.status ?? 200, + payload: { + status: "ready", + snapshot: snapshot(input.kind, input.scope), + data: input.data, + }, + payload_complete: true, + record_count: input.recordScopes?.length ?? 0, + record_scopes: input.recordScopes ?? [], + comparison_checkpoint_block_number: "100", + comparison_checkpoint_block_hash: HASH, + route_evidence: evidence(input.scope), + }; +} + +function transaction(rows: readonly Record[]) { + const query = vi.fn(async () => rows); + return { query, transaction: { query } as PostgresTransaction }; +} + +describe("atomic public route queries", () => { + it.each([ + { + name: "Explore", + call: (tx: PostgresTransaction) => + postgresPublicRouteQueries.explore(tx, { + chainId: 1, + query: "v4", + sort: "newest", + page: 2, + pageSize: 12, + }), + functionName: "get_public_explore_page_v1", + values: [1, "v4", "newest", 2, 12], + kind: "explore", + scope: DISCOVERY, + data: { + request: { query: "v4", sort: "newest", requestedPage: 2, pageSize: 12 }, + page: { + resolvedPage: 1, + totalCount: "0", + valuationUnit: null, + startAfter: null, + endAt: null, + }, + launcherFeesAccruedWei: "0", + tokens: [], + }, + }, + { + name: "token detail", + call: (tx: PostgresTransaction) => + postgresPublicRouteQueries.tokenDetail(tx, { chainId: 1, address: ADDRESS }), + functionName: "get_public_explore_token_v1", + values: [1, ADDRESS], + kind: "token", + scope: DISCOVERY, + status: 404, + data: { address: ADDRESS, token: null }, + }, + { + name: "creator profile", + call: (tx: PostgresTransaction) => + postgresPublicRouteQueries.creatorProfile(tx, { chainId: 1, account: ADDRESS }), + functionName: "get_public_creator_profile_v1", + values: [1, ADDRESS], + kind: "creator", + scope: DISCOVERY, + data: { account: ADDRESS, tokens: [], claims: [] }, + }, + { + name: "Classic profile", + call: (tx: PostgresTransaction) => + postgresPublicRouteQueries.classicV3Profile(tx, { chainId: 1, account: ADDRESS }), + functionName: "get_public_classic_v3_profile_v1", + values: [1, ADDRESS], + kind: "classic-profile", + scope: CLASSIC, + data: { account: ADDRESS, chainId: 1, rewards: [] }, + }, + { + name: "Stock profile", + call: (tx: PostgresTransaction) => + postgresPublicRouteQueries.stockPairedProfile(tx, { chainId: 1, account: ADDRESS }), + functionName: "get_public_stock_paired_profile_v1", + values: [1, ADDRESS], + kind: "stock-profile", + scope: STOCK, + data: { account: ADDRESS, chainId: 1, rewards: [] }, + }, + { + name: "Classic lookup", + call: (tx: PostgresTransaction) => + postgresPublicRouteQueries.launchLookup(tx, { + chainId: 1, + surface: "classic-v3", + account: ADDRESS, + transactionHash: HASH, + }), + functionName: "get_public_launch_lookup_v1", + values: [1, "classic-v3", ADDRESS, HASH], + kind: "launch", + scope: CLASSIC, + data: { + surface: "classic-v3", + account: ADDRESS, + transactionHash: HASH, + resolution: "not-found", + token: null, + }, + }, + { + name: "Stock lookup pending", + call: (tx: PostgresTransaction) => + postgresPublicRouteQueries.launchLookup(tx, { + chainId: 1, + surface: "stock-paired", + account: ADDRESS, + transactionHash: HASH, + }), + functionName: "get_public_launch_lookup_v1", + values: [1, "stock-paired", ADDRESS, HASH], + kind: "launch", + scope: STOCK, + status: 202, + data: { + surface: "stock-paired", + account: ADDRESS, + transactionHash: HASH, + resolution: "pending", + token: null, + }, + }, + ])("binds the exact $name reader", async (fixture) => { + const mock = transaction([ + row({ + kind: fixture.kind, + scope: fixture.scope, + data: fixture.data, + ...(fixture.status ? { status: fixture.status } : {}), + }), + ]); + + const result = await fixture.call(mock.transaction); + + expect(result.status).toBe("ready"); + expect(mock.query).toHaveBeenCalledWith( + expect.stringContaining(fixture.functionName), + fixture.values, + ); + }); + + it("binds chart evidence to its one returned source", async () => { + const source = { + ...snapshot("chart", DISCOVERY).releasePointers[0], + snapshotCommitment: COMMITMENT, + projectionRunId: "50000000-0000-4000-8000-000000000001", + publicationCommitment: COMMITMENT, + promotedBlockNumber: "100", + promotedBlockHash: HASH, + }; + const mock = transaction([ + row({ + kind: "chart", + scope: DISCOVERY, + recordScopes: [ + { model: "classic", releaseVersion: "classic-v2" }, + ], + data: { + address: ADDRESS, + range: "1d", + source, + poolId: HASH, + points: [], + swapCount: "0", + volumeNativeWei: "0", + volumeUsdWad: null, + }, + }), + ]); + + await expect( + postgresPublicRouteQueries.tokenChart(mock.transaction, { + chainId: 1, + address: ADDRESS, + range: "1d", + }), + ).resolves.toMatchObject({ status: "ready" }); + }); + + it("fails closed on absent, incomplete or inconsistent evidence", async () => { + const absent = transaction([]); + await expect( + postgresPublicRouteQueries.tokenDetail(absent.transaction, { + chainId: 1, + address: ADDRESS, + }), + ).resolves.toEqual({ + status: "not-ready", + reason: "reconciliation-incomplete", + }); + + const mismatched = row({ + kind: "token", + scope: DISCOVERY, + status: 404, + data: { address: ADDRESS, token: null }, + }); + mismatched.record_count = 1; + const invalid = transaction([mismatched]); + await expect( + postgresPublicRouteQueries.tokenDetail(invalid.transaction, { + chainId: 1, + address: ADDRESS, + }), + ).rejects.toThrow("record count evidence"); + }); +}); diff --git a/tests/data-pipeline/quicknode-stream-wake.test.ts b/tests/data-pipeline/quicknode-stream-wake.test.ts new file mode 100644 index 00000000..f02518e9 --- /dev/null +++ b/tests/data-pipeline/quicknode-stream-wake.test.ts @@ -0,0 +1,127 @@ +import { createHmac } from "node:crypto"; +import { gzipSync } from "node:zlib"; + +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { verifyQuickNodeStreamWake } from "../../lib/data-pipeline/quicknode-stream-wake.server"; + +const SECRET = "quicknode-stream-secret-at-least-32-bytes"; +const NOW_MS = Date.parse("2026-08-02T12:00:00.000Z"); +const TIMESTAMP = String(Math.floor(NOW_MS / 1_000)); +const NONCE = "0123456789abcdef0123456789abcdef"; + +function signedRequest( + payload: string, + input: Readonly<{ + secret?: string; + timestamp?: string; + gzip?: boolean; + signature?: string; + }> = {}, +) { + const timestamp = input.timestamp ?? TIMESTAMP; + const signature = + input.signature ?? + createHmac("sha256", input.secret ?? SECRET) + .update(NONCE) + .update(timestamp) + .update(payload) + .digest("hex"); + const body = input.gzip ? gzipSync(payload) : payload; + return new Request("https://programmable.family/api/ops/projector-wake", { + method: "POST", + headers: { + "content-type": "application/json", + ...(input.gzip ? { "content-encoding": "gzip" } : {}), + "x-qn-nonce": NONCE, + "x-qn-timestamp": timestamp, + "x-qn-signature": signature, + }, + body, + }); +} + +function expectWakeError(status: number) { + return expect.objectContaining({ + name: "QuickNodeStreamWakeError", + status, + }); +} + +describe("QuickNode stream wake verification", () => { + it("accepts a fresh signed JSON payload", async () => { + const payload = JSON.stringify({ block: { number: "0x123" } }); + await expect( + verifyQuickNodeStreamWake(signedRequest(payload), { + env: { PROGRAMMABLE_QUICKNODE_STREAM_SECRET: SECRET }, + nowMs: NOW_MS, + }), + ).resolves.toEqual({ + timestamp: TIMESTAMP, + payloadBytes: Buffer.byteLength(payload), + }); + }); + + it("verifies signatures over the decoded gzip payload", async () => { + const payload = JSON.stringify([{ number: "0x123" }]); + await expect( + verifyQuickNodeStreamWake(signedRequest(payload, { gzip: true }), { + env: { PROGRAMMABLE_QUICKNODE_STREAM_SECRET: SECRET }, + nowMs: NOW_MS, + }), + ).resolves.toMatchObject({ payloadBytes: Buffer.byteLength(payload) }); + }); + + it("rejects invalid signatures and stale timestamps", async () => { + await expect( + verifyQuickNodeStreamWake( + signedRequest("{}", { signature: "00".repeat(32) }), + { + env: { PROGRAMMABLE_QUICKNODE_STREAM_SECRET: SECRET }, + nowMs: NOW_MS, + }, + ), + ).rejects.toEqual(expectWakeError(401)); + + const staleTimestamp = String(Number(TIMESTAMP) - 301); + await expect( + verifyQuickNodeStreamWake( + signedRequest("{}", { timestamp: staleTimestamp }), + { + env: { PROGRAMMABLE_QUICKNODE_STREAM_SECRET: SECRET }, + nowMs: NOW_MS, + }, + ), + ).rejects.toEqual(expectWakeError(401)); + }); + + it("fails closed when the stream secret is absent or too short", async () => { + for (const secret of [undefined, "too-short"]) { + await expect( + verifyQuickNodeStreamWake(signedRequest("{}"), { + env: { PROGRAMMABLE_QUICKNODE_STREAM_SECRET: secret }, + nowMs: NOW_MS, + }), + ).rejects.toEqual(expectWakeError(503)); + } + }); + + it("rejects malformed JSON and oversized bodies", async () => { + await expect( + verifyQuickNodeStreamWake(signedRequest("not-json"), { + env: { PROGRAMMABLE_QUICKNODE_STREAM_SECRET: SECRET }, + nowMs: NOW_MS, + }), + ).rejects.toEqual(expectWakeError(400)); + + const oversized = JSON.stringify({ value: "x".repeat(64 * 1024) }); + await expect( + verifyQuickNodeStreamWake(signedRequest(oversized), { + env: { PROGRAMMABLE_QUICKNODE_STREAM_SECRET: SECRET }, + nowMs: NOW_MS, + }), + ).rejects.toEqual(expectWakeError(413)); + }); +}); diff --git a/tests/data-pipeline/read-model-deploy-policy.test.ts b/tests/data-pipeline/read-model-deploy-policy.test.ts new file mode 100644 index 00000000..0feb66d2 --- /dev/null +++ b/tests/data-pipeline/read-model-deploy-policy.test.ts @@ -0,0 +1,363 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as deployPolicy from "../../scripts/perf/read-model-deploy-policy.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import { runtimeProductionProviderBindingsFromUrls } from "../../scripts/perf/read-model-provider-binding.mjs"; + +const { + createStagedReleaseAttestation, + evaluateReadModelDeployPolicy, + readReleasePolicyExpectations, + validateStagedReleaseAttestation, + RELEASE_GATED_FLAG_NAMES, + REQUIRED_NON_SECRET_RUNTIME_ENV_NAMES, + WORKER_ACTIVATION_FLAG_NAMES, +} = deployPolicy; + +const ROOT = process.cwd(); +const ALCHEMY_URL = "https://eth-mainnet.g.alchemy.com/v2/abcdefgh"; +const QUICKNODE_URL = "https://programmable.quiknode.pro/abcdefgh"; +const EXPECTATIONS = readReleasePolicyExpectations(ROOT); +const PROVIDER_BINDINGS = runtimeProductionProviderBindingsFromUrls({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY_URL, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE_URL, +}); +const COMMITMENTS = Object.freeze({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_ENDPOINT_COMMITMENT: + PROVIDER_BINDINGS.find( + ({ vendorGroup }: { vendorGroup: string }) => vendorGroup === "alchemy", + ).endpointCommitment, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_ENDPOINT_COMMITMENT: + PROVIDER_BINDINGS.find( + ({ vendorGroup }: { vendorGroup: string }) => vendorGroup === "quicknode", + ).endpointCommitment, +}); + +function environmentFile(input: { + indexed?: Partial>; + workers?: Partial>; + nonSecret?: Partial>; + includeRuntimeProviders?: boolean; +} = {}) { + const values: Record = { + ...Object.fromEntries( + RELEASE_GATED_FLAG_NAMES.map((name: string) => [name, "false"]), + ), + ...Object.fromEntries( + WORKER_ACTIVATION_FLAG_NAMES.map((name: string) => [name, "false"]), + ), + ...EXPECTATIONS, + ...(input.includeRuntimeProviders + ? { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY_URL, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE_URL, + } + : {}), + ...input.indexed, + ...input.workers, + ...input.nonSecret, + }; + return Object.entries(values) + .filter(([, value]) => value !== undefined) + .map(([name, value]) => `${name}=${value}`) + .join("\n"); +} + +describe("read-model production deploy policy", () => { + it("binds legacy-only to exact false indexed flags and disabled workers", () => { + const policy = evaluateReadModelDeployPolicy( + environmentFile({ + workers: { PROGRAMMABLE_PROJECTOR_ACTIVE: undefined }, + }), + {}, + EXPECTATIONS, + ); + expect(policy).toMatchObject({ + mode: "legacy-only", + evidenceRequired: false, + policyReady: true, + commitmentsReady: true, + indexedFlags: Object.fromEntries( + RELEASE_GATED_FLAG_NAMES.map((name: string) => [name, false]), + ), + workerActivationFlags: { + PROGRAMMABLE_PROJECTOR_ACTIVE: false, + PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE: false, + }, + }); + }); + + it("treats either active worker as an evidence-gated runtime", () => { + for (const worker of WORKER_ACTIVATION_FLAG_NAMES) { + const policy = evaluateReadModelDeployPolicy( + environmentFile({ + workers: { [worker]: "true" }, + includeRuntimeProviders: true, + }), + COMMITMENTS, + EXPECTATIONS, + ); + expect(policy).toMatchObject({ + mode: "indexed-or-shadow", + evidenceRequired: true, + policyReady: true, + commitmentsReady: true, + runtimeProviderBinding: "verified", + }); + expect(policy.nonLegacyFlags).toContain(worker); + } + }); + + it("fails closed on ambiguous flags and missing or drifted provenance", () => { + const ambiguous = evaluateReadModelDeployPolicy( + environmentFile({ + workers: { PROGRAMMABLE_PROJECTOR_ACTIVE: "TRUE" }, + includeRuntimeProviders: true, + }), + COMMITMENTS, + EXPECTATIONS, + ); + expect(ambiguous.policyReady).toBe(false); + expect(ambiguous.invalidFlagNames).toEqual([ + "PROGRAMMABLE_PROJECTOR_ACTIVE", + ]); + + const missingIndexedFlag = environmentFile() + .split("\n") + .filter((line) => !line.startsWith(`${RELEASE_GATED_FLAG_NAMES[0]}=`)) + .join("\n"); + const missing = evaluateReadModelDeployPolicy( + missingIndexedFlag, + {}, + EXPECTATIONS, + ); + expect(missing.policyReady).toBe(false); + expect(missing.invalidFlagNames).toContain(RELEASE_GATED_FLAG_NAMES[0]); + + for (const name of REQUIRED_NON_SECRET_RUNTIME_ENV_NAMES) { + const drifted = evaluateReadModelDeployPolicy( + environmentFile({ nonSecret: { [name]: undefined } }), + {}, + EXPECTATIONS, + ); + expect(drifted.policyReady).toBe(false); + expect(drifted.invalidNonSecretEnvironmentNames).toContain(name); + } + const wrongGraph = evaluateReadModelDeployPolicy( + environmentFile({ + nonSecret: { + PROGRAMMABLE_UNISWAP_GRAPH_SCHEMA_COMMITMENT: `0x${"00".repeat(32)}`, + }, + }), + {}, + EXPECTATIONS, + ); + expect(wrongGraph.policyReady).toBe(false); + expect(wrongGraph.invalidNonSecretEnvironmentNames).toContain( + "PROGRAMMABLE_UNISWAP_GRAPH_SCHEMA_COMMITMENT", + ); + }); + + it("creates a canonical non-secret attestation for the exact staged target", () => { + const policy = evaluateReadModelDeployPolicy( + environmentFile(), + {}, + EXPECTATIONS, + ); + const result = createStagedReleaseAttestation({ + policy, + verifiedSha: "a".repeat(40), + vercelProjectId: "prj_1234567890abcdef", + stagedDeploymentId: `dpl_${"b".repeat(24)}`, + stagedDeploymentUrl: "https://programmable-stage-abc.vercel.app", + productionOrigin: "https://programmable.family", + expectedMode: "legacy-only", + timestamp: "2026-08-01T12:34:56.000Z", + }); + expect(JSON.parse(result.json)).toEqual({ + schemaVersion: 1, + verifiedSha: "a".repeat(40), + vercelProjectId: "prj_1234567890abcdef", + stagedDeploymentId: `dpl_${"b".repeat(24)}`, + stagedDeploymentUrl: "https://programmable-stage-abc.vercel.app", + productionOrigin: "https://programmable.family", + policyMode: "legacy-only", + indexedFlags: policy.indexedFlags, + workerActivationFlags: policy.workerActivationFlags, + timestamp: "2026-08-01T12:34:56.000Z", + }); + expect(result.sha256).toBe( + createHash("sha256").update(result.json, "utf8").digest("hex"), + ); + expect(result.json).not.toMatch(/(?:password|postgresql:\/\/)/iu); + expect( + validateStagedReleaseAttestation(JSON.parse(result.json), { + verifiedSha: "a".repeat(40), + vercelProjectId: "prj_1234567890abcdef", + stagedDeploymentId: `dpl_${"b".repeat(24)}`, + stagedDeploymentUrl: "https://programmable-stage-abc.vercel.app", + productionOrigin: "https://programmable.family", + nowMs: Date.parse("2026-08-01T12:35:00.000Z"), + }), + ).toEqual(JSON.parse(result.json)); + }); + + it("rejects stale, mutated or publicly exposed cutover attestations", () => { + const policy = evaluateReadModelDeployPolicy( + environmentFile({ + indexed: Object.fromEntries( + RELEASE_GATED_FLAG_NAMES.map((name: string) => [ + name, + name === "INDEXED_READ_SHADOW_COMPARE_ENABLED" ? "false" : "true", + ]), + ), + workers: Object.fromEntries( + WORKER_ACTIVATION_FLAG_NAMES.map((name: string) => [name, "true"]), + ), + includeRuntimeProviders: true, + }), + COMMITMENTS, + EXPECTATIONS, + ); + const value = JSON.parse( + createStagedReleaseAttestation({ + policy, + verifiedSha: "a".repeat(40), + vercelProjectId: "prj_1234567890abcdef", + stagedDeploymentId: `dpl_${"b".repeat(24)}`, + stagedDeploymentUrl: "https://programmable-stage-abc.vercel.app", + productionOrigin: "https://programmable.family", + expectedMode: "indexed-or-shadow", + timestamp: "2026-08-01T12:34:56.000Z", + }).json, + ); + const expected = { + verifiedSha: value.verifiedSha, + vercelProjectId: value.vercelProjectId, + stagedDeploymentId: value.stagedDeploymentId, + stagedDeploymentUrl: value.stagedDeploymentUrl, + productionOrigin: value.productionOrigin, + requireWorkersActive: true, + requireIndexedRoutesActive: true, + nowMs: Date.parse("2026-08-01T12:35:00.000Z"), + }; + expect(() => + validateStagedReleaseAttestation( + { ...value, verifiedSha: "c".repeat(40) }, + expected, + ), + ).toThrow("verifiedSha does not match"); + expect(() => + validateStagedReleaseAttestation( + { + ...value, + indexedFlags: { + ...value.indexedFlags, + INDEXED_EXPLORE_LIST_READS_ENABLED: false, + }, + }, + expected, + ), + ).toThrow("does not activate exact indexed routes"); + expect(() => + validateStagedReleaseAttestation(value, { + ...expected, + nowMs: Date.parse("2026-08-02T12:35:00.000Z"), + }), + ).toThrow("timestamp is invalid"); + }); + + it("rejects an attestation for a different mode, target or project", () => { + const policy = evaluateReadModelDeployPolicy( + environmentFile(), + {}, + EXPECTATIONS, + ); + const valid = { + policy, + verifiedSha: "a".repeat(40), + vercelProjectId: "prj_1234567890abcdef", + stagedDeploymentId: `dpl_${"b".repeat(24)}`, + stagedDeploymentUrl: "https://programmable-stage-abc.vercel.app", + productionOrigin: "https://programmable.family", + expectedMode: "legacy-only", + timestamp: "2026-08-01T12:34:56.000Z", + }; + expect(() => + createStagedReleaseAttestation({ + ...valid, + expectedMode: "indexed-or-shadow", + }), + ).toThrow("runtime mode"); + expect(() => + createStagedReleaseAttestation({ + ...valid, + stagedDeploymentUrl: "https://programmable.family", + }), + ).toThrow("deployment-specific Vercel host"); + expect(() => + createStagedReleaseAttestation({ + ...valid, + productionOrigin: "https://programmable.family/", + }), + ).toThrow("canonical Programmable domain"); + expect(() => + createStagedReleaseAttestation({ + ...valid, + vercelProjectId: "other-project", + }), + ).toThrow("project ID"); + }); + + it("publishes every exact non-secret runtime name in the env schema", () => { + const example = readFileSync(resolve(ROOT, ".env.example"), "utf8"); + for (const name of [ + ...WORKER_ACTIVATION_FLAG_NAMES, + ...REQUIRED_NON_SECRET_RUNTIME_ENV_NAMES, + ]) { + expect(example.match(new RegExp(`^${name}=`, "gmu"))).toHaveLength(1); + expect(example).not.toContain(`NEXT_PUBLIC_${name}`); + } + for (const [name, value] of Object.entries(EXPECTATIONS)) { + expect(example).toContain(`${name}=${value}`); + } + }); + + it("smokes the legacy release corpus and reconciles uncertain promotion outcomes", () => { + const workflow = readFileSync( + resolve(ROOT, ".github/workflows/deploy-production.yml"), + "utf8", + ); + expect(workflow).toContain("Attest exact staged release policy"); + expect(workflow).toContain("staged-release-attestation.json"); + expect(workflow).toContain("attestation_sha256"); + expect(workflow).toContain("Smoke legacy staged public APIs"); + expect(workflow).toContain('"/api/ops/health"'); + expect(workflow).toContain('"/api/indexers/v1/token-list"'); + expect(workflow).toContain("/api/explore/token?address="); + expect(workflow).toContain("/api/explore/profile?account="); + expect(workflow).toContain("releaseToken.creatorAddress"); + expect(workflow).toContain( + "Reverify staged binding immediately before promotion", + ); + expect(workflow.indexOf("Reverify staged binding immediately before promotion")) + .toBeLessThan(workflow.indexOf("vercel promote")); + expect(workflow).toContain("continue-on-error: true"); + expect(workflow).toContain("Reconcile an unsuccessful promotion attempt"); + expect(workflow).toContain( + 'current_deployment_id" = "$CANDIDATE_DEPLOYMENT_ID', + ); + expect(workflow).toContain('vercel rollback "$ROLLBACK_DEPLOYMENT_URL"'); + expect(workflow).toContain( + '--expected-deployment-id "$PREVIOUS_DEPLOYMENT_ID"', + ); + expect(workflow).not.toContain( + "failure() && steps.promote.outcome == 'success'", + ); + }); +}); diff --git a/tests/data-pipeline/read-model-ops-contract.test.ts b/tests/data-pipeline/read-model-ops-contract.test.ts new file mode 100644 index 00000000..b39be279 --- /dev/null +++ b/tests/data-pipeline/read-model-ops-contract.test.ts @@ -0,0 +1,340 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import { evaluateReadModelOperationsSourceContracts } from "../../scripts/perf/read-model-ops-source-contracts.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import { verifyPostPromotion } from "../../scripts/perf/read-model-post-promotion.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import { resolveProductionBinding } from "../../scripts/perf/read-model-production-binding.mjs"; + +const ROOT = process.cwd(); +const DEPLOYMENT_ID = "dpl_aaaaaaaaaaaaaaaaaaaaaaaa"; +const GIT_HEAD = "b".repeat(40); +const PROJECT_ID = "prj_programmable_test"; + +const AUTHENTICATED_ROUTE = ` + import { timingSafeEqual } from "node:crypto"; + function matchesBearer(request, secret) { + const authorization = request.headers.get("authorization"); + if (!secret || Buffer.byteLength(secret, "utf8") < 32 || Buffer.byteLength(secret, "utf8") > 1_024 || !authorization?.startsWith("Bearer ")) return false; + const provided = Buffer.from(authorization.slice(7), "utf8"); + const expected = Buffer.from(secret, "utf8"); + return provided.length === expected.length && timingSafeEqual(provided, expected); + } + function authorizationMode(request) { + const requestedMode = request.headers.get("x-programmable-cutover-mode"); + if (requestedMode !== null) { + return requestedMode === "raw-backfill-v1" && + process.env.PROGRAMMABLE_CUTOVER_BACKFILL_ACTIVE === "true" && + matchesBearer(request, process.env.PROGRAMMABLE_CUTOVER_OPERATOR_SECRET) + ? "cutover" : null; + } + return matchesBearer(request, process.env.CRON_SECRET) ? "standard" : null; + } + export async function GET(request) { + const mode = authorizationMode(request); + if (mode === null) return { status: 401, headers: { "Cache-Control": "no-store" } }; + if (mode === "cutover") runCutover(); + try { return { status: 200, headers: { "Cache-Control": "no-store" } }; } + catch { return { status: 503, headers: { "Cache-Control": "no-store" } }; } + } +`; + +const SAFE_SOURCE_ACTIVATION = ` + export function projectorRuntimeActivationState(env) { + const value = env.PROGRAMMABLE_PROJECTOR_ACTIVE; + if (value === "false" || value === undefined) return "disabled"; + if (value === "true") return "active"; + return invalidRuntimeConfig(); + } + export async function runConfiguredProjectorCycle(env) { + if (projectorRuntimeActivationState(env) === "disabled") return { status: "disabled" }; + const leaseController = createProjectorRuntimeLeaseController(); + const acquisition = await leaseController.tryAcquire(); + if (acquisition.status === "busy") return { status: "busy" }; + } +`; + +const SAFE_MARKET_ACTIVATION = ` + export async function runConfiguredMarketProjectorCycle(env, store) { + const value = env.PROGRAMMABLE_MARKET_PROJECTOR_ACTIVE; + if (value === "false" || value === undefined) return { status: "disabled" }; + if (value !== "true") throw invalidInput("config", "activation"); + const sourceCheckpointGeneration = "1"; + const lease = await store.tryAcquireLease(); + if (!lease) return { status: "busy" }; + try { return { sourceCheckpointGeneration }; } + finally { await store.releaseLease(lease); } + } +`; + +const PROVIDER_EVIDENCE_MIGRATION = ` + create table programmable_private.projection_provider_execution_evidence(); + create table programmable_private.reward_snapshot_provider_evidence(); + create table programmable_private.projection_publication_provider_bindings(); + alter table programmable_private.projection_provider_execution_evidence force row level security; +`; + +const MARKET_MIGRATION = ` + create table programmable_private.market_projector_cursor_history(); + create table programmable_private.market_snapshot_lineage_memberships(); + create table programmable_private.market_candle_lineage_memberships(); + create function programmable_private.try_acquire_market_projector_runtime_lease_v1(); + create function programmable_private.assert_market_projector_runtime_lease_v1(); + create function programmable_private.release_market_projector_runtime_lease_v1(); + select * from programmable_private.projector_checkpoint_current; + if cursor_block_global_log_index <> 4294967295 then raise exception 'partial'; end if; + if cursor_candidate_id <> 'empty-page' then raise exception 'partial'; end if; + alter table programmable_private.market_projector_cursor_history force row level security; +`; + +function integratedOverrides() { + return { + "app/api/ops/projector/route.ts": AUTHENTICATED_ROUTE, + "lib/data-pipeline/projector-runtime-config.server.ts": + SAFE_SOURCE_ACTIVATION, + "supabase/migrations/20260731224000_projector_provider_evidence_binding.sql": + PROVIDER_EVIDENCE_MIGRATION, + "app/api/ops/market-projector/route.ts": AUTHENTICATED_ROUTE, + "lib/data-pipeline/market-projector-runtime.server.ts": + SAFE_MARKET_ACTIVATION, + "supabase/migrations/20260731223000_market_projector_contract.sql": + MARKET_MIGRATION, + }; +} + +function fixtureDigests() { + return Object.fromEntries( + Object.entries(integratedOverrides()).map(([path, source]) => [ + path, + createHash("sha256").update(source).digest("hex"), + ]), + ); +} + +describe("read-model operations source contract", () => { + it("binds the per-minute schedulers, activation gates and release workflow", () => { + const result = evaluateReadModelOperationsSourceContracts(ROOT, { + sourceOverrides: integratedOverrides(), + expectedSha256Overrides: fixtureDigests(), + }); + expect(result.failures).toEqual([]); + expect(result.ok).toBe(true); + }); + + it("rejects scheduler, authorization and activation drift", () => { + const vercelPath = resolve(ROOT, "vercel.json"); + const drift = evaluateReadModelOperationsSourceContracts(ROOT, { + sourceOverrides: { + ...integratedOverrides(), + "app/api/ops/index/route.ts": + 'export { GET } from "../index-v2/route";', + "vercel.json": readFileSync(vercelPath, "utf8") + .replace('"* * * * *"', '"*/2 * * * *"') + .replace( + '"path": "/api/ops/market-projector"', + '"path": "/api/ops/reconcile-preparity"', + ), + "app/api/ops/projector/route.ts": AUTHENTICATED_ROUTE.replace( + "process.env.CRON_SECRET", + "process.env.AUTOMATION_SECRET", + ), + "lib/data-pipeline/market-projector-runtime.server.ts": + SAFE_MARKET_ACTIVATION.replace('value !== "true"', "false"), + }, + expectedSha256Overrides: fixtureDigests(), + }); + expect(drift.failures.map(({ id }: { id: string }) => id)).toEqual( + expect.arrayContaining([ + "ops-cron-exact-set", + "ops-legacy-alias-closed", + "ops-source-projector-schedule", + "ops-source-projector-route-auth", + "ops-market-projector-activation", + "ops-reconciler-unscheduled", + ]), + ); + }); + + it("rejects comment-only controls and jointly drifted manifests", () => { + const operations = JSON.parse( + readFileSync(resolve(ROOT, "config/read-model-operations.v1.json"), "utf8"), + ); + operations.legacyIndexer.schedule = "0 0 * * *"; + operations.workers.forEach((worker: { schedule: string }) => { + worker.schedule = "0 0 * * *"; + }); + const vercel = JSON.parse( + readFileSync(resolve(ROOT, "vercel.json"), "utf8"), + ); + vercel.crons.forEach((cron: { schedule: string }) => { + cron.schedule = "0 0 * * *"; + }); + const commentsOnly = ` + // process.env.CRON_SECRET request.headers.get("authorization") + // Buffer.byteLength(secret, "utf8") < 32; Buffer.byteLength(secret, "utf8") > 1_024 + // authorization.startsWith("Bearer "); provided.length === expected.length + // timingSafeEqual(provided, expected); if (!isAuthorized(request)) {} + // status: 401; status: 503; "Cache-Control": "no-store" + `; + const result = evaluateReadModelOperationsSourceContracts(ROOT, { + sourceOverrides: { + ...integratedOverrides(), + "config/read-model-operations.v1.json": JSON.stringify(operations), + "vercel.json": JSON.stringify(vercel), + "app/api/ops/projector/route.ts": commentsOnly, + }, + expectedSha256Overrides: { + ...fixtureDigests(), + "app/api/ops/projector/route.ts": createHash("sha256") + .update(commentsOnly) + .digest("hex"), + }, + }); + expect(result.failures.map(({ id }: { id: string }) => id)).toEqual( + expect.arrayContaining([ + "ops-config-schema", + "ops-cron-exact-set", + "ops-legacy-cron-preserved", + "ops-source-projector-schedule", + "ops-source-projector-route-auth", + ]), + ); + }); +}); + +function publicFetch(healthStatus = "healthy") { + return async (input: URL | RequestInfo) => { + const url = new URL(String(input)); + if (url.hostname === "api.vercel.com") { + return Response.json({ + id: DEPLOYMENT_ID, + url: "programmable-tested.vercel.app", + readyState: "READY", + projectId: PROJECT_ID, + meta: { githubCommitSha: GIT_HEAD }, + }); + } + if (url.pathname === "/") { + return new Response("Programmable", { + status: 200, + headers: { "content-type": "text/html" }, + }); + } + if (url.pathname === "/api/ops/health") { + return Response.json({ status: healthStatus }, { + status: healthStatus === "healthy" ? 200 : 503, + }); + } + if (url.pathname === "/api/explore") { + return Response.json({ status: "ready", tokens: [{}] }); + } + if (url.pathname === "/api/indexers/v1/token-list") { + return Response.json({ tokens: [{}] }); + } + return Response.json({ error: "not found" }, { status: 404 }); + }; +} + +function postPromotionInput(fetchImpl = publicFetch()) { + return { + rootDirectory: ROOT, + targetUrl: "https://programmable.family", + expectedDeploymentId: DEPLOYMENT_ID, + expectedGitHead: GIT_HEAD, + token: "vercel-test-token", + teamId: "team_programmable_test", + projectId: PROJECT_ID, + fetchImpl, + }; +} + +describe("post-promotion route verification", () => { + it("accepts a healthy public production surface", async () => { + const result = await verifyPostPromotion(postPromotionInput()); + expect(result.ok).toBe(true); + expect(result.checks.map(({ id }: { id: string }) => id)).toEqual([ + "production-deployment-id", + "production-deployment-project", + "production-deployment-ready", + "production-deployment-commit", + "production-root", + "production-health", + "production-explore", + "production-token-list", + ]); + }); + + it("fails closed when production health is not healthy", async () => { + const result = await verifyPostPromotion( + postPromotionInput(publicFetch("unhealthy")), + ); + expect(result.ok).toBe(false); + expect(result.failures).toContainEqual( + expect.objectContaining({ id: "production-health" }), + ); + }); + + it("rejects a target that is not an exact HTTPS origin", async () => { + await expect( + verifyPostPromotion({ + ...postPromotionInput(), + targetUrl: "https://programmable.family/untrusted", + }), + ).rejects.toThrow("HTTPS origin"); + }); + + it("fails if production does not resolve to the staged deployment", async () => { + const result = await verifyPostPromotion({ + ...postPromotionInput(), + expectedDeploymentId: "dpl_cccccccccccccccccccccccc", + }); + expect(result.ok).toBe(false); + expect(result.failures).toContainEqual( + expect.objectContaining({ id: "production-deployment-id" }), + ); + }); + + it("rejects an empty Explore response", async () => { + const base = publicFetch(); + const fetchImpl = async (input: URL | RequestInfo) => { + const url = new URL(String(input)); + if (url.pathname === "/api/explore") { + return Response.json({ status: "ready", tokens: [] }); + } + return base(input); + }; + const result = await verifyPostPromotion(postPromotionInput(fetchImpl)); + expect(result.ok).toBe(false); + expect(result.failures).toContainEqual( + expect.objectContaining({ id: "production-explore" }), + ); + }); + + it("captures a rollback binding and detects prior auto-promotion", async () => { + const binding = await resolveProductionBinding({ + targetUrl: "https://programmable.family", + token: "vercel-test-token", + teamId: "team_programmable_test", + projectId: PROJECT_ID, + fetchImpl: publicFetch(), + }); + expect(binding).toEqual( + expect.objectContaining({ deploymentId: DEPLOYMENT_ID, gitHead: GIT_HEAD }), + ); + await expect( + resolveProductionBinding({ + targetUrl: "https://programmable.family", + rejectGitHead: GIT_HEAD, + token: "vercel-test-token", + teamId: "team_programmable_test", + projectId: PROJECT_ID, + fetchImpl: publicFetch(), + }), + ).rejects.toThrow("automatic production-domain assignment"); + }); +}); diff --git a/tests/data-pipeline/read-model-performance-capture-route.test.ts b/tests/data-pipeline/read-model-performance-capture-route.test.ts new file mode 100644 index 00000000..c56dee54 --- /dev/null +++ b/tests/data-pipeline/read-model-performance-capture-route.test.ts @@ -0,0 +1,246 @@ +import { createHmac } from "node:crypto"; + +import { NextRequest } from "next/server"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + captureReadModelPerformance: vi.fn(), +})); + +vi.mock("server-only", () => ({})); + +vi.mock( + "../../lib/data-pipeline/read-model-performance-capture.server", + () => ({ + captureReadModelPerformance: mocks.captureReadModelPerformance, + }), +); + +import { + POST, + dynamic, + maxDuration, + runtime, +} from "../../app/api/ops/read-model-performance-capture/route"; + +const TOKEN = "performance-probe-token-at-least-32-bytes"; +const requestBody = { + schemaVersion: 1, + profileId: "read-model-smoke-v1", + gitHead: "a".repeat(40), + targetUrl: "https://programmable-git-codex.vercel.app/", + vercelDeploymentId: `dpl_${"A".repeat(24)}`, + captureNonce: `0x${"12".repeat(32)}`, +}; + +function request(input: { + token?: string; + probe?: string; + body?: string; + contentType?: string; + releaseSignature?: string; +} = {}) { + const headers = new Headers(); + if (input.token !== undefined) { + headers.set("x-programmable-performance-probe-token", input.token); + } + if (input.probe !== undefined) { + headers.set("x-programmable-performance-probe", input.probe); + } + if (input.contentType !== undefined) { + headers.set("content-type", input.contentType); + } + if (input.releaseSignature !== undefined) { + headers.set( + "x-programmable-release-capture-signature", + input.releaseSignature, + ); + } + return new NextRequest( + "https://programmable-git-codex.vercel.app/api/ops/read-model-performance-capture", + { + method: "POST", + headers, + body: input.body ?? JSON.stringify(requestBody), + }, + ); +} + +describe("read-model performance capture route", () => { + beforeEach(() => { + vi.stubEnv("PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN", TOKEN); + mocks.captureReadModelPerformance.mockReset(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it("pins the protected capture route to Node and disables caching", () => { + expect(dynamic).toBe("force-dynamic"); + expect(maxDuration).toBe(90); + expect(runtime).toBe("nodejs"); + }); + + it.each([ + {}, + { probe: "1", token: "wrong" }, + { probe: "0", token: TOKEN }, + ])("rejects unauthorized probes before reading the body", async (headers) => { + const response = await POST(request(headers)); + + expect(response.status).toBe(401); + expect(response.headers.get("cache-control")).toBe("private, no-store"); + await expect(response.json()).resolves.toEqual({ error: "Unauthorized" }); + expect(mocks.captureReadModelPerformance).not.toHaveBeenCalled(); + }); + + it("rejects non-JSON and oversized bodies before capture", async () => { + const authenticated = { probe: "1", token: TOKEN }; + const wrongType = await POST( + request({ ...authenticated, contentType: "text/plain" }), + ); + expect(wrongType.status).toBe(415); + + const oversized = await POST( + request({ + ...authenticated, + contentType: "application/json", + body: JSON.stringify({ value: "x".repeat(5_000) }), + }), + ); + expect(oversized.status).toBe(413); + expect(mocks.captureReadModelPerformance).not.toHaveBeenCalled(); + }); + + it("enforces the probe secret limits in UTF-8 bytes", async () => { + const minimumUtf8Token = "é".repeat(16); + vi.stubEnv("PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN", minimumUtf8Token); + mocks.captureReadModelPerformance.mockResolvedValue({ + schemaVersion: 1, + captureNonce: requestBody.captureNonce, + datasetManifest: { schemaVersion: 1 }, + rpcTrace: { schemaVersion: 1 }, + }); + + const accepted = await POST( + request({ + probe: "1", + token: minimumUtf8Token, + contentType: "application/json", + }), + ); + expect(accepted.status).toBe(200); + + const overlongUtf8Token = "é".repeat(600); + vi.stubEnv("PROGRAMMABLE_PERFORMANCE_PROBE_TOKEN", overlongUtf8Token); + const rejected = await POST( + request({ + probe: "1", + token: overlongUtf8Token, + contentType: "application/json", + }), + ); + expect(rejected.status).toBe(401); + }); + + it("returns only the exact private capture payload", async () => { + const payload = { + schemaVersion: 1, + captureNonce: requestBody.captureNonce, + datasetManifest: { schemaVersion: 1 }, + rpcTrace: { schemaVersion: 1 }, + }; + mocks.captureReadModelPerformance.mockResolvedValue(payload); + + const response = await POST( + request({ + probe: "1", + token: TOKEN, + contentType: "application/json", + }), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("private, no-store"); + await expect(response.json()).resolves.toEqual(payload); + expect(mocks.captureReadModelPerformance).toHaveBeenCalledWith(requestBody); + }); + + it("requires a body-bound HMAC and rate-limits the release capture", async () => { + const releaseBody = { + ...requestBody, + schemaVersion: 2, + profileId: "read-model-release-v1", + captureNonce: `0x${"34".repeat(32)}`, + issuedAtMs: Date.now(), + }; + const rawBody = JSON.stringify(releaseBody); + const signature = `v1=${createHmac("sha256", TOKEN) + .update(rawBody, "utf8") + .digest("hex")}`; + mocks.captureReadModelPerformance.mockResolvedValue({ + schemaVersion: 1, + captureNonce: releaseBody.captureNonce, + datasetManifest: { profileId: releaseBody.profileId }, + rpcTrace: { candidateBatchSize: 32 }, + }); + + const unsigned = await POST( + request({ + probe: "1", + token: TOKEN, + contentType: "application/json", + body: rawBody, + }), + ); + expect(unsigned.status).toBe(401); + expect(mocks.captureReadModelPerformance).not.toHaveBeenCalled(); + + const accepted = await POST( + request({ + probe: "1", + token: TOKEN, + contentType: "application/json", + body: rawBody, + releaseSignature: signature, + }), + ); + expect(accepted.status).toBe(200); + expect(mocks.captureReadModelPerformance).toHaveBeenCalledWith(releaseBody); + + const replay = await POST( + request({ + probe: "1", + token: TOKEN, + contentType: "application/json", + body: rawBody, + releaseSignature: signature, + }), + ); + expect(replay.status).toBe(429); + expect(replay.headers.get("retry-after")).toBe("30"); + }); + + it("fails closed without reflecting database, Envio or RPC details", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.captureReadModelPerformance.mockRejectedValue( + new Error("postgres://secret and https://rpc.example/key"), + ); + + const response = await POST( + request({ + probe: "1", + token: TOKEN, + contentType: "application/json", + }), + ); + const body = await response.text(); + + expect(response.status).toBe(503); + expect(body).toBe('{"error":"Performance capture unavailable"}'); + expect(body).not.toContain("secret"); + expect(body).not.toContain("rpc.example"); + }); +}); diff --git a/tests/data-pipeline/read-model-performance-capture.test.ts b/tests/data-pipeline/read-model-performance-capture.test.ts new file mode 100644 index 00000000..60f79e27 --- /dev/null +++ b/tests/data-pipeline/read-model-performance-capture.test.ts @@ -0,0 +1,692 @@ +import { rootCertificates } from "node:tls"; + +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + captureReadModelPerformance, + parseReadModelPerformanceCaptureRequest, + readPerformanceDataset, +} from "../../lib/data-pipeline/read-model-performance-capture.server"; + +const gitHead = "a".repeat(40); +const targetUrl = "https://programmable-git-codex.vercel.app/"; +const vercelDeploymentId = `dpl_${"A".repeat(24)}`; +const captureNonce = `0x${"12".repeat(32)}`; +const TEST_CA = rootCertificates[0]!; +const bytes32 = (byte: string) => `0x${byte.repeat(64)}`; +const address = (index: number) => + `0x${index.toString(16).padStart(40, "0")}` as `0x${string}`; +const transactionHash = (index: number) => + `0x${index.toString(16).padStart(64, "0")}` as `0x${string}`; +const candidateId = (index: number) => + `1:${bytes32((index + 1).toString(16))}:${bytes32((index + 8).toString(16))}:${index}`; +const accessEvidence = Object.freeze({ + projectorSessionUser: "programmable_projector_login", + projectorCurrentRole: "programmable_projector", + projectorCurrentSettingRole: "programmable_projector", + apiReaderSessionUser: "programmable_api_reader_login", + apiReaderCurrentRole: "programmable_api_reader", + apiReaderCurrentSettingRole: "programmable_api_reader", + apiReaderDeniedSqlstate: "42501", + apiReaderFunctionExecute: false, + apiReaderViewSelect: false, +}); + +function environment( + overrides: Record = {}, +): Record { + return { + VERCEL_GIT_COMMIT_SHA: gitHead, + VERCEL_URL: "programmable-git-codex.vercel.app", + VERCEL_DEPLOYMENT_ID: vercelDeploymentId, + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT: + "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY: + "envio:production-7f24e63", + ...overrides, + }; +} + +function databaseEnvironment( + overrides: Record = {}, +) { + return environment({ + PROGRAMMABLE_PROJECTOR_DATABASE_URL: + "postgresql://programmable_projector_login:projector-password@db.example:5432/postgres?sslmode=verify-full", + PROGRAMMABLE_API_READER_DATABASE_URL: + "postgresql://programmable_api_reader_login:reader-password@db.example:5432/postgres?sslmode=verify-full", + PROGRAMMABLE_POSTGRES_SSL_CA_PEM: TEST_CA, + ...overrides, + }); +} + +function candidateCaptureEnvironment() { + return environment({ + PROGRAMMABLE_PROJECTOR_BINDING_MODE: "candidate-backfill", + PROGRAMMABLE_PROJECTOR_ENVIO_MIRROR_COMMIT: + "7ffd15c2a28c481a2d3632e30b315262c2471b2e", + PROGRAMMABLE_ENVIO_GRAPHQL_URL: + "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + PROGRAMMABLE_PROJECTOR_ENVIO_REDACTED_IDENTITY: + "envio:production-7f24e63", + INDEXED_EXPLORE_LIST_READS_ENABLED: "false", + INDEXED_EXPLORE_TOKEN_READS_ENABLED: "false", + INDEXED_EXPLORE_CHART_READS_ENABLED: "false", + INDEXED_CREATOR_PROFILE_READS_ENABLED: "false", + INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED: "false", + INDEXED_LAUNCH_LOOKUP_ENABLED: "false", + INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED: "false", + INDEXED_READ_SHADOW_COMPARE_ENABLED: "false", + }); +} + +function body(overrides: Record = {}) { + return { + schemaVersion: 1, + profileId: "read-model-smoke-v1", + gitHead, + targetUrl, + vercelDeploymentId, + captureNonce, + ...overrides, + }; +} + +function releaseBody(overrides: Record = {}) { + return { + schemaVersion: 2, + profileId: "read-model-release-v1", + gitHead, + targetUrl, + vercelDeploymentId, + captureNonce: `0x${"34".repeat(32)}`, + issuedAtMs: Date.now(), + ...overrides, + }; +} + +function dataset() { + const releaseCounts = { + "classic-v2": 50, + "classic-v3": 62, + "stock-paired-v1": 32, + "stock-paired-v2": 32, + "stock-paired-v3": 32, + }; + const eligibleLaunches = Object.entries(releaseCounts).flatMap( + ([releaseVersion, count], releaseIndex) => + Array.from({ length: count }, (_, index) => { + const identity = releaseIndex * 64 + index + 1; + return { + account: address(identity + 1_000), + transactionHash: transactionHash(identity), + tokenAddress: address(identity), + releaseVersion, + }; + }), + ); + const accountAddresses = Array.from({ length: 100 }, (_, index) => + address(index + 201), + ); + return { + generatedAt: "2026-07-31T19:00:00.000Z", + counts: { + launches: 208, + chainEvents: 700, + marketSnapshots: 250, + marketCandles: 250, + accounts: 300, + rewardRows: 300, + }, + releaseCounts, + eligibleLaunches, + accountEvidence: accountAddresses.map((account) => ({ + account, + profileRows: 1, + rewardRows: 0, + })), + keys: { + tokenAddresses: Array.from({ length: 100 }, (_, index) => + address(index + 1), + ), + accountAddresses, + classicLaunches: Array.from({ length: 32 }, (_, index) => ({ + account: address(index + 401), + transactionHash: transactionHash(index + 1), + })), + stockLaunches: Array.from({ length: 32 }, (_, index) => ({ + account: address(index + 501), + transactionHash: transactionHash(index + 101), + })), + candidateIds: Array.from({ length: 8 }, (_, index) => candidateId(index)), + }, + }; +} + +function releaseDataset() { + const seed = dataset(); + const releaseCounts = { + "classic-v2": 60, + "classic-v3": 104, + "stock-paired-v1": 34, + "stock-paired-v2": 33, + "stock-paired-v3": 33, + }; + let identityCursor = 0; + const eligibleLaunches = Object.entries(releaseCounts).flatMap( + ([releaseVersion, count]) => + Array.from({ length: count }, (_, index) => { + const identity = identityCursor + index + 1; + return { + account: address(identity + 1_000), + transactionHash: transactionHash(identity), + tokenAddress: address(identity), + releaseVersion, + }; + }).map((launch, index, launches) => { + if (index === launches.length - 1) identityCursor += launches.length; + return launch; + }), + ); + return { + ...seed, + counts: { + ...seed.counts, + launches: 264, + chainEvents: 792, + marketSnapshots: 264, + marketCandles: 264, + rewardRows: 300, + }, + releaseCounts, + eligibleLaunches, + }; +} + +function releaseCandidate(index: number) { + const candidateBlockHash = `0x${(index + 1) + .toString(16) + .padStart(64, "0")}` as `0x${string}`; + const candidateTransactionHash = `0x${(index + 101) + .toString(16) + .padStart(64, "0")}` as `0x${string}`; + return { + candidateId: `1:${candidateBlockHash}:${candidateTransactionHash}:0`, + blockNumber: String(1_000 + index), + blockHash: candidateBlockHash, + transactionHash: candidateTransactionHash, + blockGlobalLogIndex: 0, + sourceAddress: address(index + 1), + }; +} + +describe("read-model performance capture binding", () => { + it("binds the request to the exact staged Vercel deployment", () => { + expect( + parseReadModelPerformanceCaptureRequest(body(), environment()), + ).toEqual(body()); + }); + + it("binds a fresh release request to the exact staged deployment", () => { + const request = releaseBody(); + expect( + parseReadModelPerformanceCaptureRequest(request, environment()), + ).toEqual(request); + expect(() => + parseReadModelPerformanceCaptureRequest( + { ...request, issuedAtMs: Date.now() - 60_001 }, + environment(), + ), + ).toThrow(); + }); + + it("captures the real 32-candidate release ceiling with a 128-call provider budget", async () => { + const seed = releaseDataset(); + const candidates = Array.from({ length: 32 }, (_, index) => + releaseCandidate(index), + ); + const readCandidatesAfter = vi.fn(async () => candidates); + const providers = [ + { + identity: "alchemy-provider", + vendorGroup: "alchemy", + endpointCommitment: bytes32("a"), + endpointOriginCommitment: bytes32("b"), + }, + { + identity: "quicknode-provider", + vendorGroup: "quicknode", + endpointCommitment: bytes32("c"), + endpointOriginCommitment: bytes32("d"), + }, + ]; + const calls = providers.flatMap((provider, providerIndex) => + [ + ["getChainId", 1], + ["getBlockNumber", 1], + ["getBlock", 33], + ["getTransactionReceipt", 32], + ["getBytecode", 32], + ].flatMap(([operation, count]) => + Array.from({ length: count as number }, (_, index) => ({ + providerIdentity: provider.identity, + providerVendorGroup: provider.vendorGroup, + providerEndpointCommitment: provider.endpointCommitment, + providerOriginCommitment: provider.endpointOriginCommitment, + operation, + attempt: 1, + startedOffsetMs: providerIndex * 100 + index, + durationMs: 1, + outcome: "success" as const, + })), + ), + ); + const runRpcTrace = vi.fn(async ({ work }) => { + await work(); + return { + startedAtMs: 2_000, + completedAtMs: 2_250, + candidateBatchSize: 32, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 128, + elapsedMs: 250, + calls, + providerCallCounts: [99, 99], + candidateEvidence: candidates.map((candidate) => ({ + candidateId: candidate.candidateId, + candidateBlockNumber: candidate.blockNumber, + candidateBlockHash: candidate.blockHash, + transactionHash: candidate.transactionHash, + sourceAddress: candidate.sourceAddress, + })), + }; + }); + const verifyBatch = vi.fn(async () => ({ candidates })); + const createEnvio = vi.fn(() => ({ readCandidatesAfter })); + const dependencies = { + readDataset: vi.fn(async () => ({ dataset: seed, accessEvidence })), + createEnvio, + createProviders: vi.fn(() => providers), + verifyBatch, + runRpcTrace, + } as never; + const request = releaseBody(); + + const result = await captureReadModelPerformance(request, { + env: environment(), + dependencies, + }); + + expect(readCandidatesAfter).toHaveBeenCalledWith({ + cursor: { blockNumber: "0", blockGlobalLogIndex: -1, candidateId: "" }, + limit: 32, + }); + expect(verifyBatch).toHaveBeenCalledWith( + expect.objectContaining({ + candidates, + rpcPolicy: { + hardDeadlineMs: expect.any(Number), + maxCallsPerProvider: 128, + }, + }), + ); + expect(result.rpcTrace).toMatchObject({ + profileId: "read-model-release-v1", + candidateBatchSize: 32, + maxCallsPerProvider: 128, + providerCallCounts: [99, 99], + }); + expect(result.datasetManifest.keys.tokenAddresses).toHaveLength(264); + expect(result.datasetManifest.keys.candidateIds).toHaveLength(32); + expect(createEnvio).toHaveBeenCalledWith(expect.objectContaining({ + releaseBinding: expect.objectContaining({ + envio: expect.objectContaining({ + deploymentLabel: "production-7f24e63", + }), + }), + })); + }); + + it.each([ + { gitHead: "b".repeat(40) }, + { targetUrl: "https://programmable.family" }, + { targetUrl: `${targetUrl}/path` }, + { vercelDeploymentId: `dpl_${"B".repeat(24)}` }, + { captureNonce: "0x1234" }, + { extra: true }, + ])("rejects unbound or non-exact capture input", (override) => { + expect(() => + parseReadModelPerformanceCaptureRequest( + body(override), + environment(), + ), + ).toThrow(); + }); + + it("performs one fresh 8-candidate Envio read and dual-RPC verification", async () => { + const seed = dataset(); + const readCandidate = vi.fn(async (id: string) => ({ candidateId: id })); + const providers = [ + { + identity: "alchemy-provider", + vendorGroup: "alchemy", + endpointCommitment: bytes32("a"), + endpointOriginCommitment: bytes32("b"), + }, + { + identity: "quicknode-provider", + vendorGroup: "quicknode", + endpointCommitment: bytes32("c"), + endpointOriginCommitment: bytes32("d"), + }, + ]; + const operationCounts = [ + ["getChainId", 1], + ["getBlockNumber", 1], + ["getBlock", 9], + ["getTransactionReceipt", 8], + ["getBytecode", 8], + ] as const; + const calls = providers.flatMap((provider, providerIndex) => + operationCounts.flatMap(([operation, count]) => + Array.from({ length: count }, (_, index) => ({ + providerIdentity: provider.identity, + providerVendorGroup: provider.vendorGroup, + providerEndpointCommitment: provider.endpointCommitment, + providerOriginCommitment: provider.endpointOriginCommitment, + operation, + attempt: 1, + startedOffsetMs: providerIndex * 25 + index, + durationMs: 1, + outcome: "success" as const, + })), + ), + ); + const candidateEvidence = Array.from({ length: 8 }, (_, index) => ({ + candidateId: candidateId(index), + candidateBlockNumber: String(1_000 + index), + candidateBlockHash: bytes32((index + 1).toString(16)), + transactionHash: bytes32((index + 8).toString(16)), + sourceAddress: address(index + 1), + })); + const runRpcTrace = vi.fn(async ({ work }) => { + await work(); + return { + startedAtMs: 1_000, + completedAtMs: 1_050, + candidateBatchSize: 8, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 42, + elapsedMs: 50, + calls, + providerCallCounts: [27, 27], + candidateEvidence, + }; + }); + const verifyBatch = vi.fn(async () => ({ candidates: Array(8).fill({}) })); + const createEnvio = vi.fn(() => ({ readCandidate })); + const dependencies = { + readDataset: vi.fn(async () => ({ dataset: seed, accessEvidence })), + createEnvio, + createProviders: vi.fn(() => providers), + verifyBatch, + runRpcTrace, + } as never; + + const result = await captureReadModelPerformance(body(), { + env: candidateCaptureEnvironment(), + dependencies, + }); + + expect(readCandidate).toHaveBeenCalledTimes(8); + expect(readCandidate.mock.calls.map(([id]) => id)).toEqual( + seed.keys.candidateIds, + ); + expect(verifyBatch).toHaveBeenCalledWith( + expect.objectContaining({ + candidates: seed.keys.candidateIds.map((id) => ({ candidateId: id })), + providers, + rpcPolicy: { + hardDeadlineMs: expect.any(Number), + maxCallsPerProvider: 42, + }, + }), + ); + expect(result).toEqual({ + schemaVersion: 1, + captureNonce, + datasetManifest: { + schemaVersion: 1, + profileId: "read-model-smoke-v1", + generatedAt: seed.generatedAt, + counts: seed.counts, + releaseCounts: seed.releaseCounts, + eligibleLaunches: seed.eligibleLaunches, + accountEvidence: seed.accountEvidence, + keys: seed.keys, + accessEvidence, + }, + rpcTrace: { + schemaVersion: 1, + profileId: "read-model-smoke-v1", + gitHead, + targetUrl, + vercelDeploymentId, + captureNonce, + startedAtMs: 1_000, + completedAtMs: 1_050, + candidateBatchSize: 8, + hardDeadlineMs: 75_000, + maxCallsPerProvider: 42, + elapsedMs: 50, + providerCallCounts: [27, 27], + calls, + candidateEvidence, + }, + }); + expect(JSON.stringify(result)).not.toContain("https://rpc"); + expect(createEnvio).toHaveBeenCalledWith(expect.objectContaining({ + endpoint: "https://indexer.hyperindex.xyz/d7a39a2/v1/graphql", + releaseBinding: expect.objectContaining({ + envio: expect.objectContaining({ + deploymentLabel: "production-7f24e63", + }), + }), + })); + }); + + it("captures projector identity with the dataset and proves the reader denial live", async () => { + const seed = dataset(); + const projectorStatements: string[] = []; + const readerStatements: string[] = []; + const datasetRow = { + generated_at: new Date(seed.generatedAt), + launch_count: seed.counts.launches, + eligible_launch_count: seed.counts.launches, + candidate_count: 8, + chain_event_count: seed.counts.chainEvents, + market_snapshot_count: seed.counts.marketSnapshots, + market_candle_count: seed.counts.marketCandles, + account_count: seed.counts.accounts, + reward_row_count: seed.counts.rewardRows, + release_coverage: seed.releaseCounts, + eligible_launches: seed.eligibleLaunches, + account_evidence: seed.accountEvidence, + token_addresses: seed.keys.tokenAddresses, + account_addresses: seed.keys.accountAddresses, + classic_launches: seed.keys.classicLaunches, + stock_launches: seed.keys.stockLaunches, + candidate_ids: seed.keys.candidateIds, + }; + const executor = (kind: "projector" | "reader") => ({ + transaction: async (work: (transaction: { query: (sql: string) => Promise }) => Promise) => + work({ + query: async (sql: string) => { + const statements = kind === "projector" + ? projectorStatements + : readerStatements; + statements.push(sql); + if (sql.includes("current_setting('role'")) { + return kind === "projector" + ? [{ + session_user: "programmable_projector_login", + current_role: "programmable_projector", + current_setting_role: "programmable_projector", + }] + : [{ + session_user: "programmable_api_reader_login", + current_role: "programmable_api_reader", + current_setting_role: "programmable_api_reader", + }]; + } + if (sql.includes("has_function_privilege")) { + return [{ function_execute: false, view_select: false }]; + } + if (sql.includes("get_read_model_performance_dataset_v1")) { + if (kind === "reader") { + throw Object.assign(new Error("permission denied"), { + code: "42501", + }); + } + return [datasetRow]; + } + return []; + }, + }), + close: vi.fn(async () => undefined), + }); + const projectorExecutor = executor("projector"); + const readerExecutor = executor("reader"); + const createExecutor = vi + .fn() + .mockReturnValueOnce(projectorExecutor) + .mockReturnValueOnce(readerExecutor); + + const captured = await readPerformanceDataset(databaseEnvironment(), { + createExecutor, + }); + + expect(captured.dataset).toEqual(seed); + expect(captured.accessEvidence).toEqual(accessEvidence); + expect(projectorStatements.findIndex((sql) => + sql.includes("current_setting('role'"), + )).toBeLessThan(projectorStatements.findIndex((sql) => + sql.includes("get_read_model_performance_dataset_v1"), + )); + expect(readerStatements).toEqual(expect.arrayContaining([ + expect.stringContaining("has_function_privilege"), + "savepoint performance_api_reader_denial", + expect.stringContaining("get_read_model_performance_dataset_v1"), + "rollback to savepoint performance_api_reader_denial", + "release savepoint performance_api_reader_denial", + ])); + expect(projectorExecutor.close).toHaveBeenCalledOnce(); + expect(readerExecutor.close).toHaveBeenCalledOnce(); + }); + + it("fails closed if the API reader can execute the projector dataset function", async () => { + const seed = dataset(); + const datasetRow = { + generated_at: new Date(seed.generatedAt), + launch_count: seed.counts.launches, + eligible_launch_count: seed.counts.launches, + candidate_count: 8, + chain_event_count: seed.counts.chainEvents, + market_snapshot_count: seed.counts.marketSnapshots, + market_candle_count: seed.counts.marketCandles, + account_count: seed.counts.accounts, + reward_row_count: seed.counts.rewardRows, + release_coverage: seed.releaseCounts, + eligible_launches: seed.eligibleLaunches, + account_evidence: seed.accountEvidence, + token_addresses: seed.keys.tokenAddresses, + account_addresses: seed.keys.accountAddresses, + classic_launches: seed.keys.classicLaunches, + stock_launches: seed.keys.stockLaunches, + candidate_ids: seed.keys.candidateIds, + }; + const createExecutor = vi + .fn() + .mockReturnValueOnce({ + transaction: async (work: (transaction: { query: (sql: string) => Promise }) => Promise) => + work({ query: async (sql: string) => + sql.includes("current_setting('role'") + ? [{ + session_user: "programmable_projector_login", + current_role: "programmable_projector", + current_setting_role: "programmable_projector", + }] + : sql.includes("get_read_model_performance_dataset_v1") + ? [datasetRow] + : [] }), + close: vi.fn(async () => undefined), + }) + .mockReturnValueOnce({ + transaction: async (work: (transaction: { query: (sql: string) => Promise }) => Promise) => + work({ query: async (sql: string) => + sql.includes("current_setting('role'") + ? [{ + session_user: "programmable_api_reader_login", + current_role: "programmable_api_reader", + current_setting_role: "programmable_api_reader", + }] + : sql.includes("has_function_privilege") + ? [{ function_execute: true, view_select: false }] + : [] }), + close: vi.fn(async () => undefined), + }); + + await expect( + readPerformanceDataset(databaseEnvironment(), { createExecutor }), + ).rejects.toThrow(); + }); + + it("refuses undersized, duplicated or stale evidence instead of padding it", async () => { + const undersized = dataset(); + undersized.keys.tokenAddresses = undersized.keys.tokenAddresses.slice(0, 99); + const createEnvio = vi.fn(); + const dependencies = { + readDataset: vi.fn(async () => ({ + dataset: undersized, + accessEvidence, + })), + createEnvio, + createProviders: vi.fn(), + verifyBatch: vi.fn(), + runRpcTrace: vi.fn(), + } as never; + + await expect( + captureReadModelPerformance(body(), { + env: environment(), + dependencies, + }), + ).rejects.toThrow(); + expect(createEnvio).not.toHaveBeenCalled(); + }); + + it("requires independent deterministic Classic and Stock launch paths", async () => { + const invalid = dataset(); + invalid.keys.stockLaunches[1] = invalid.keys.stockLaunches[0]!; + const createEnvio = vi.fn(); + const dependencies = { + readDataset: vi.fn(async () => ({ dataset: invalid, accessEvidence })), + createEnvio, + createProviders: vi.fn(), + verifyBatch: vi.fn(), + runRpcTrace: vi.fn(), + } as never; + + await expect( + captureReadModelPerformance(body(), { + env: environment(), + dependencies, + }), + ).rejects.toThrow(); + expect(createEnvio).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/data-pipeline/read-model-performance-explore-matrix.test.ts b/tests/data-pipeline/read-model-performance-explore-matrix.test.ts new file mode 100644 index 00000000..ee29c086 --- /dev/null +++ b/tests/data-pipeline/read-model-performance-explore-matrix.test.ts @@ -0,0 +1,409 @@ +import { createHash } from "node:crypto"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as captureModule from "../../scripts/perf/read-model-capture.mjs"; +// @ts-expect-error Operational JavaScript modules intentionally have no declarations. +import * as gateModule from "../../scripts/perf/read-model-gate.mjs"; + +const { + captureExploreMatrix, + commitExploreMatrixPage, + exploreMatrixCorpusCommitment, + normalizeExploreMatrixQuery, + serializeExploreMatrixPages, +} = captureModule; +const { + evaluateExploreMatrixReleaseEvidence, + loadExploreMatrixReleaseEvidence, +} = gateModule; + +const GIT_HEAD = "a".repeat(40); +const CAPTURE_NONCE = `0x${"12".repeat(32)}`; +const DEPLOYMENT_ID = `dpl_${"A".repeat(24)}`; +const TARGET_URL = new URL("https://programmable-matrix.vercel.app/"); +const DATASET_SHA256 = "d".repeat(64); +const RELEASES = [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const; +const SNAPSHOT = { + chainId: 1, + blockNumber: "25700000", + blockHash: `0x${"ab".repeat(32)}`, + confirmations: 12, +}; + +type MatrixToken = { + tokenAddress: string; + name: string; + symbol: string; + releaseVersion: string; +}; + +type MatrixPage = { + pageCommitment: string; + startedAtMs: number; + tokens: MatrixToken[]; + caseId: string; + sort: string; + vercelCache: string; + fallback: boolean | null; + parity: string; + checkpointSha256: string; + [key: string]: unknown; +}; + +type MatrixCase = { + caseId: string; + kind: string; +}; + +type MatrixManifest = { + captureNonce: string; + capturedAt: string; + target: Record; + dataset: { + manifestSha256: string; + inventorySha256: string; + eligibleLaunchCount: number; + }; + checkpoint: { snapshotSha256: string }; + matrix: { + cases: MatrixCase[]; + casesSha256: string; + caseCounts: Record; + caseCount: number; + pagesSha256: string; + pageCount: number; + tokenObservationCount: number; + corpusSha256: string; + }; + [key: string]: unknown; +}; + +type MatrixBundle = { + manifest: MatrixManifest; + pages: MatrixPage[]; + artifacts: { + exploreMatrixManifest: { sha256: string }; + exploreMatrixPages: { sha256: string }; + }; +}; + +function address(index: number) { + return `0x${index.toString(16).padStart(40, "0")}`; +} + +function transactionHash(index: number) { + return `0x${index.toString(16).padStart(64, "0")}`; +} + +function fixture() { + const tokens = Array.from({ length: 265 }, (_, index) => ({ + tokenAddress: address(index + 1), + name: `Matrix Alpha ${String(index + 1).padStart(3, "0")}`, + symbol: `MX${String(index + 1).padStart(3, "0")}`, + releaseVersion: RELEASES[index % RELEASES.length]!, + })); + const eligibleLaunches = tokens.map((token, index) => ({ + account: address(index + 1_000), + transactionHash: transactionHash(index + 1), + tokenAddress: token.tokenAddress, + releaseVersion: token.releaseVersion, + })); + const releaseCounts = Object.fromEntries( + RELEASES.map((releaseVersion) => [ + releaseVersion, + eligibleLaunches.filter( + (launch) => launch.releaseVersion === releaseVersion, + ).length, + ]), + ); + const datasetManifest = { + generatedAt: "2026-08-01T08:00:00.000Z", + eligibleLaunches, + releaseCounts, + }; + let clock = 1_800_000_000_000; + const now = () => clock++; + const observedHeaders: Headers[] = []; + const fetchImpl = async (request: URL | RequestInfo, init?: RequestInit) => { + observedHeaders.push(new Headers(init?.headers)); + const url = new URL( + request instanceof URL + ? request.toString() + : typeof request === "string" + ? request + : request.url, + ); + const query = url.searchParams.get("q") ?? ""; + const normalizedQuery = normalizeExploreMatrixQuery(query); + const sort = url.searchParams.get("sort") ?? "market-cap"; + const pageSize = Number(url.searchParams.get("limit")); + const requestedPage = Number(url.searchParams.get("page")); + const filtered = tokens.filter( + (token) => + normalizedQuery === "" || + token.name.toLowerCase().includes(normalizedQuery) || + token.symbol.toLowerCase().includes(normalizedQuery) || + token.tokenAddress.includes(normalizedQuery), + ); + const ordered = ["newest", "market-cap"].includes(sort) + ? [...filtered].reverse() + : [...filtered]; + const totalPages = Math.ceil(ordered.length / pageSize); + const resolvedPage = + totalPages === 0 ? 1 : Math.min(requestedPage, totalPages); + const offset = (resolvedPage - 1) * pageSize; + const body = { + status: "ready", + tokens: ordered.slice(offset, offset + pageSize), + page: resolvedPage, + pageSize, + total: ordered.length, + totalPages, + sort, + query: query.trim(), + snapshot: SNAPSHOT, + launcherFeesAccruedWei: "0", + launcherFeesAccruedEth: "0", + }; + return new Response(JSON.stringify(body), { + status: 200, + headers: { + "cache-control": "private, no-store", + "x-vercel-cache": "BYPASS", + "x-programmable-shadow-overhead-ms": "1", + "x-programmable-shadow-parity": "match", + "x-programmable-read-source": "indexed", + "x-programmable-live-fallback": "false", + }, + }); + }; + return { tokens, datasetManifest, now, fetchImpl, observedHeaders }; +} + +function sha256Bytes(value: string | Uint8Array) { + return createHash("sha256").update(value).digest("hex"); +} + +async function acceptedBundle() { + const source = fixture(); + const captured = (await captureExploreMatrix({ + targetUrl: TARGET_URL, + deploymentId: DEPLOYMENT_ID, + profileId: "read-model-release-v1", + gitHead: GIT_HEAD, + captureNonce: CAPTURE_NONCE, + shadowProbeToken: "shadow-probe-secret-that-is-32-bytes", // gitleaks:allow + automationBypassSecret: "vercel-bypass-secret-that-is-32-bytes", // gitleaks:allow + probeTimeoutMs: 30_000, + concurrency: 20, + datasetManifest: source.datasetManifest, + datasetManifestSha256: DATASET_SHA256, + fetchImpl: source.fetchImpl, + now: source.now, + })) as { manifest: MatrixManifest; pages: MatrixPage[] }; + expect(source.observedHeaders.length).toBeGreaterThan(0); + for (const headers of source.observedHeaders) { + expect(headers.get("x-vercel-protection-bypass")).toBe( + "vercel-bypass-secret-that-is-32-bytes", + ); + } + const matrixBundle: MatrixBundle = { + manifest: structuredClone(captured.manifest), + pages: structuredClone(captured.pages), + artifacts: { + exploreMatrixManifest: { + sha256: sha256Bytes( + `${JSON.stringify(captured.manifest, null, 2)}\n`, + ), + }, + exploreMatrixPages: { sha256: captured.manifest.matrix.pagesSha256 }, + }, + }; + const releaseCapturedAt = new Date( + Date.parse(captured.manifest.capturedAt) + 1, + ).toISOString(); + const releaseBundle = { + evidence: { + profileId: "read-model-release-v1", + captureNonce: CAPTURE_NONCE, + capturedAt: releaseCapturedAt, + target: { + url: TARGET_URL.toString(), + vercelDeploymentId: DEPLOYMENT_ID, + gitHead: GIT_HEAD, + }, + }, + datasetManifest: source.datasetManifest, + artifacts: { datasetManifest: { sha256: DATASET_SHA256 } }, + httpSamples: [ + { completedAtMs: Math.min(...captured.pages.map((page) => page.startedAtMs)) - 1 }, + ], + }; + return { matrixBundle, releaseBundle }; +} + +function rebindMatrix(bundle: MatrixBundle) { + bundle.pages.forEach((page) => { + page.pageCommitment = commitExploreMatrixPage(page); + }); + const pagesBytes = serializeExploreMatrixPages(bundle.pages); + const pagesSha256 = sha256Bytes(pagesBytes); + const tokenObservationCount = bundle.pages.reduce( + (total, page) => total + page.tokens.length, + 0, + ); + bundle.manifest.matrix.pagesSha256 = pagesSha256; + bundle.manifest.matrix.pageCount = bundle.pages.length; + bundle.manifest.matrix.tokenObservationCount = tokenObservationCount; + bundle.manifest.matrix.corpusSha256 = exploreMatrixCorpusCommitment({ + captureNonce: bundle.manifest.captureNonce, + target: bundle.manifest.target, + datasetManifestSha256: bundle.manifest.dataset.manifestSha256, + inventorySha256: bundle.manifest.dataset.inventorySha256, + casesSha256: bundle.manifest.matrix.casesSha256, + pagesSha256, + checkpointSha256: bundle.manifest.checkpoint.snapshotSha256, + eligibleLaunchCount: bundle.manifest.dataset.eligibleLaunchCount, + caseCount: bundle.manifest.matrix.caseCount, + pageCount: bundle.manifest.matrix.pageCount, + tokenObservationCount, + }); + bundle.artifacts.exploreMatrixPages.sha256 = pagesSha256; + bundle.artifacts.exploreMatrixManifest.sha256 = sha256Bytes( + `${JSON.stringify(bundle.manifest, null, 2)}\n`, + ); +} + +describe("complete aggregate Explore activation matrix", () => { + it("accepts all real pages, clamps, four sorts and real bounded queries for all releases", async () => { + const { matrixBundle, releaseBundle } = await acceptedBundle(); + + const result = evaluateExploreMatrixReleaseEvidence( + matrixBundle, + releaseBundle, + ); + + expect(result.releaseEvidenceAccepted).toBe(true); + expect(result.failures).toEqual([]); + expect(matrixBundle.manifest.matrix.caseCounts).toMatchObject({ + empty: 1, + name: 8, + symbol: 8, + address: 8, + }); + expect(matrixBundle.manifest.matrix.pageCount).toBe(376); + expect( + result.checks.find( + (check: { id: string }) => + check.id === "explore-matrix-page-and-cursor-coverage", + )?.status, + ).toBe("pass"); + for (const releaseVersion of RELEASES) { + for (const sort of [ + "newest", + "oldest", + "market-cap", + "market-cap-asc", + ]) { + expect( + result.checks.find( + (check: { id: string }) => + check.id === + `explore-matrix-release-${releaseVersion}-${sort}`, + )?.status, + ).toBe("pass"); + } + } + }); + + it("loads the fixed manifest and JSONL page sidecars and fails if either is missing", async () => { + const { matrixBundle } = await acceptedBundle(); + const directory = mkdtempSync(join(tmpdir(), "programmable-explore-matrix-")); + const evidencePath = join(directory, "read-model-release-evidence.v1.json"); + try { + writeFileSync( + join(directory, "explore-matrix-evidence.v1.json"), + `${JSON.stringify(matrixBundle.manifest, null, 2)}\n`, + ); + writeFileSync( + join(directory, "explore-matrix-pages.v1.jsonl"), + serializeExploreMatrixPages(matrixBundle.pages), + ); + + const loaded = loadExploreMatrixReleaseEvidence({ evidencePath }); + expect(loaded.pages).toHaveLength(matrixBundle.pages.length); + expect(loaded.manifest.matrix.corpusSha256).toBe( + matrixBundle.manifest.matrix.corpusSha256, + ); + + rmSync(join(directory, "explore-matrix-pages.v1.jsonl")); + expect(() => + loadExploreMatrixReleaseEvidence({ evidencePath }), + ).toThrow(); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it.each([ + ["cache", "explore-matrix-cache"], + ["fallback", "explore-matrix-fallback"], + ["parity", "explore-matrix-parity"], + ["checkpoint", "explore-matrix-checkpoint-binding"], + ["missing-case", "explore-matrix-case-coverage"], + ] as const)("rejects %s evidence", async (mutation, expectedFailure) => { + const { matrixBundle, releaseBundle } = await acceptedBundle(); + if (mutation === "cache") matrixBundle.pages[0]!.vercelCache = "HIT"; + if (mutation === "fallback") matrixBundle.pages[0]!.fallback = true; + if (mutation === "parity") matrixBundle.pages[0]!.parity = "mismatch"; + if (mutation === "checkpoint") { + matrixBundle.pages[0]!.checkpointSha256 = "f".repeat(64); + } + if (mutation === "missing-case") { + const removedCase = matrixBundle.manifest.matrix.cases.find( + (queryCase) => queryCase.kind === "address", + )!; + matrixBundle.pages = matrixBundle.pages.filter( + (page) => + !(page.caseId === removedCase.caseId && page.sort === "newest"), + ); + } + rebindMatrix(matrixBundle); + + const result = evaluateExploreMatrixReleaseEvidence( + matrixBundle, + releaseBundle, + ); + + expect(result.releaseEvidenceAccepted).toBe(false); + expect( + result.failures.map((failure: { id: string }) => failure.id), + ).toContain(expectedFailure); + }); + + it("rejects pages whose digest is not the committed matrix artifact", async () => { + const { matrixBundle, releaseBundle } = await acceptedBundle(); + matrixBundle.artifacts.exploreMatrixPages.sha256 = "e".repeat(64); + + const result = evaluateExploreMatrixReleaseEvidence( + matrixBundle, + releaseBundle, + ); + + expect(result.releaseEvidenceAccepted).toBe(false); + expect( + result.failures.map((failure: { id: string }) => failure.id), + ).toContain("explore-matrix-page-digest"); + }); +}); diff --git a/tests/data-pipeline/read-model.server.test.ts b/tests/data-pipeline/read-model.server.test.ts new file mode 100644 index 00000000..22411cb0 --- /dev/null +++ b/tests/data-pipeline/read-model.server.test.ts @@ -0,0 +1,318 @@ +import { rootCertificates } from "node:tls"; + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const postgresMocks = vi.hoisted(() => ({ + close: vi.fn(async () => undefined), + query: vi.fn(), + transaction: vi.fn(), + createPostgresExecutor: vi.fn(), + createPostgresReadModel: vi.fn(), +})); + +vi.mock("../../lib/data-pipeline/postgres", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + createPostgresExecutor: postgresMocks.createPostgresExecutor, + createPostgresReadModel: postgresMocks.createPostgresReadModel, + }; +}); + +import { DataPipelineError } from "../../lib/data-pipeline/errors"; +import { + getServerReadModel, + resetServerReadModelForTests, +} from "../../lib/data-pipeline/read-model.server"; + +const REMOTE_DATABASE_URL = [ + "postgres://postgres.project:", + "test-only", + "@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?sslmode=verify-full", +].join(""); + +const FLAG_NAMES = [ + "INDEXED_EXPLORE_LIST_READS_ENABLED", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED", + "INDEXED_EXPLORE_CHART_READS_ENABLED", + "INDEXED_CREATOR_PROFILE_READS_ENABLED", + "INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED", + "INDEXED_LAUNCH_LOOKUP_ENABLED", + "INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", + "INDEXED_READ_SHADOW_COMPARE_ENABLED", +] as const; + +function clearReadModelEnvironment() { + for (const name of FLAG_NAMES) vi.stubEnv(name, "false"); + vi.stubEnv("PROGRAMMABLE_API_READER_DATABASE_URL", ""); + vi.stubEnv("PROGRAMMABLE_POSTGRES_SSL_CA_PEM", ""); +} + +function enableRemoteReadModel() { + vi.stubEnv("INDEXED_EXPLORE_LIST_READS_ENABLED", "true"); + vi.stubEnv("PROGRAMMABLE_API_READER_DATABASE_URL", REMOTE_DATABASE_URL); + vi.stubEnv("PROGRAMMABLE_POSTGRES_SSL_CA_PEM", rootCertificates[0]!); +} + +describe("server read-model singleton", () => { + beforeEach(async () => { + vi.unstubAllEnvs(); + clearReadModelEnvironment(); + await resetServerReadModelForTests(); + postgresMocks.close.mockClear(); + postgresMocks.createPostgresExecutor.mockReset(); + postgresMocks.createPostgresReadModel.mockReset(); + postgresMocks.query.mockReset(); + postgresMocks.transaction.mockReset(); + postgresMocks.query.mockImplementation(async (text: string) => { + if (text === "select session_user::text as session_user") { + return [{ session_user: "programmable_api_reader_login" }]; + } + if ( + text === + "select session_user::text as session_user, current_role::text as current_role" + ) { + return [ + { + session_user: "programmable_api_reader_login", + current_role: "programmable_api_reader", + }, + ]; + } + return []; + }); + postgresMocks.transaction.mockImplementation( + async ( + work: (transaction: { + query: typeof postgresMocks.query; + }) => Promise, + ) => work({ query: postgresMocks.query }), + ); + postgresMocks.createPostgresExecutor.mockReturnValue({ + transaction: postgresMocks.transaction, + close: postgresMocks.close, + }); + postgresMocks.createPostgresReadModel.mockReturnValue({ + close: postgresMocks.close, + }); + }); + + it("does not construct a database client when every indexed route and shadow mode are off", async () => { + await expect(getServerReadModel()).resolves.toBeNull(); + expect(postgresMocks.createPostgresExecutor).not.toHaveBeenCalled(); + expect(postgresMocks.createPostgresReadModel).not.toHaveBeenCalled(); + }); + + it("fails closed without credentials when an indexed route requires Postgres", async () => { + vi.stubEnv("INDEXED_EXPLORE_LIST_READS_ENABLED", "true"); + + const result = getServerReadModel(); + await expect(result).rejects.toMatchObject({ + dependency: "config", + code: "invalid_config", + } satisfies Partial); + expect(postgresMocks.createPostgresExecutor).not.toHaveBeenCalled(); + }); + + it("fails closed without credentials when shadow comparison requires Postgres", async () => { + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "true"); + + await expect(getServerReadModel()).rejects.toBeInstanceOf( + DataPipelineError, + ); + expect(postgresMocks.createPostgresExecutor).not.toHaveBeenCalled(); + }); + + it("constructs the private reader for an authenticated probe while route flags stay off", async () => { + await expect(getServerReadModel()).resolves.toBeNull(); + vi.stubEnv("PROGRAMMABLE_API_READER_DATABASE_URL", REMOTE_DATABASE_URL); + vi.stubEnv("PROGRAMMABLE_POSTGRES_SSL_CA_PEM", rootCertificates[0]!); + + const model = await getServerReadModel({ required: true }); + + expect(model).not.toBeNull(); + expect(postgresMocks.createPostgresExecutor).toHaveBeenCalledTimes(1); + expect(postgresMocks.createPostgresReadModel).toHaveBeenCalledTimes(1); + }); + + it("shares one global promise and one bounded pool across concurrent callers", async () => { + enableRemoteReadModel(); + + const processOn = vi.spyOn(process, "on"); + try { + const first = getServerReadModel(); + const second = getServerReadModel(); + + expect(first).toBe(second); + const [firstModel, secondModel] = await Promise.all([first, second]); + expect(firstModel).toBe(secondModel); + expect(postgresMocks.createPostgresExecutor).toHaveBeenCalledTimes(1); + expect(postgresMocks.createPostgresReadModel).toHaveBeenCalledTimes(1); + expect(postgresMocks.createPostgresExecutor).toHaveBeenCalledWith( + expect.objectContaining({ + connectionString: REMOTE_DATABASE_URL, + sslCaPem: rootCertificates[0], + maxConnections: 2, + connectTimeoutMs: 1_000, + idleTimeoutMs: 5_000, + }), + ); + expect(processOn).not.toHaveBeenCalled(); + } finally { + processOn.mockRestore(); + } + }); + + it("runs route reads inside one explicit repeatable-read, read-only transaction", async () => { + enableRemoteReadModel(); + const model = await getServerReadModel(); + if (!model) throw new Error("expected read model"); + + const result = await model.repeatableReadSnapshot(async (transaction) => { + await transaction.query("select 'payload'::text as value"); + return "complete"; + }); + + expect(result).toBe("complete"); + expect(postgresMocks.transaction).toHaveBeenCalledTimes(1); + expect(postgresMocks.query.mock.calls.map((call) => call[0])).toEqual([ + "set transaction isolation level repeatable read, read only", + "select session_user::text as session_user", + "set local role programmable_api_reader", + "select session_user::text as session_user, current_role::text as current_role", + "set local statement_timeout = '1000ms'", + "set local lock_timeout = '250ms'", + "set local idle_in_transaction_session_timeout = '2000ms'", + "select 'payload'::text as value", + ]); + }); + + it.each([ + "postgres", + "service_role", + "programmable_projector_login", + "arbitrary_reader_member", + ])( + "rejects the %s login even if it could assume the reader capability", + async (sessionUser) => { + enableRemoteReadModel(); + postgresMocks.query.mockImplementation(async (text: string) => { + if (text === "select session_user::text as session_user") { + return [{ session_user: sessionUser }]; + } + if ( + text === + "select session_user::text as session_user, current_role::text as current_role" + ) { + return [ + { + session_user: sessionUser, + current_role: "programmable_api_reader", + }, + ]; + } + return []; + }); + + const model = await getServerReadModel(); + if (!model) throw new Error("expected read model"); + + await expect( + model.repeatableReadSnapshot(async () => "unreachable"), + ).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + safeMetadata: { operation: "runtime-login-role" }, + }); + expect(postgresMocks.query).not.toHaveBeenCalledWith( + "set local role programmable_api_reader", + ); + }, + ); + + it("keeps readiness and payload on the same snapshot when committed state changes between reads", async () => { + enableRemoteReadModel(); + let committedParity = "current"; + let snapshotParity: string | undefined; + postgresMocks.query.mockImplementation(async (text: string) => { + if ( + text === "set transaction isolation level repeatable read, read only" + ) { + snapshotParity = committedParity; + return []; + } + if (text === "select session_user::text as session_user") { + return [{ session_user: "programmable_api_reader_login" }]; + } + if ( + text === + "select session_user::text as session_user, current_role::text as current_role" + ) { + return [ + { + session_user: "programmable_api_reader_login", + current_role: "programmable_api_reader", + }, + ]; + } + if (text === "select parity") return [{ parity: snapshotParity }]; + return []; + }); + const model = await getServerReadModel(); + if (!model) throw new Error("expected read model"); + + const observed = await model.repeatableReadSnapshot(async (transaction) => { + const readiness = await transaction.query<{ parity: string }>( + "select parity", + ); + committedParity = "mismatch"; + const payloadVersion = await transaction.query<{ parity: string }>( + "select parity", + ); + return [readiness[0]?.parity, payloadVersion[0]?.parity]; + }); + + expect(observed).toEqual(["current", "current"]); + expect(committedParity).toBe("mismatch"); + expect(postgresMocks.transaction).toHaveBeenCalledTimes(1); + }); + + it("keeps a rejected construction promise sticky until the test-only reset", async () => { + enableRemoteReadModel(); + postgresMocks.createPostgresExecutor.mockImplementationOnce(() => { + throw new Error("synthetic construction failure"); + }); + + const first = getServerReadModel(); + const second = getServerReadModel(); + expect(first).toBe(second); + await expect(first).rejects.toThrow("synthetic construction failure"); + await expect(second).rejects.toThrow("synthetic construction failure"); + expect(postgresMocks.createPostgresExecutor).toHaveBeenCalledTimes(1); + + await resetServerReadModelForTests(); + await expect(getServerReadModel()).resolves.not.toBeNull(); + expect(postgresMocks.createPostgresExecutor).toHaveBeenCalledTimes(2); + }); + + it("closes the shared pool only through the test-only reset", async () => { + enableRemoteReadModel(); + await getServerReadModel(); + + await resetServerReadModelForTests(); + expect(postgresMocks.close).toHaveBeenCalledTimes(1); + + await resetServerReadModelForTests(); + expect(postgresMocks.close).toHaveBeenCalledTimes(1); + }); + + it("rejects reset access outside the test runtime", async () => { + vi.stubEnv("NODE_ENV", "production"); + await expect(resetServerReadModelForTests()).rejects.toBeInstanceOf( + DataPipelineError, + ); + }); +}); diff --git a/tests/data-pipeline/reconciler-corpus-partitions.test.ts b/tests/data-pipeline/reconciler-corpus-partitions.test.ts new file mode 100644 index 00000000..5029daf4 --- /dev/null +++ b/tests/data-pipeline/reconciler-corpus-partitions.test.ts @@ -0,0 +1,204 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + assembleReconcilerCorpusPages, + assembleReconcilerEntitlementPages, + createReconcilerCorpusManifest, + createReconcilerEntitlementManifest, + RECONCILER_CORPUS_MAXIMUM_TOTAL_COUNT, + RECONCILER_CORPUS_PARTITION_SIZE, +} from "../../lib/data-pipeline/reconciler-corpus-partitions"; +import type { HexBytes32 } from "../../lib/data-pipeline/codecs"; +import type { ReconcilerPreParityContract } from "../../lib/data-pipeline/reconciler-preparity"; + +function hex(value: number, bytes: number): `0x${string}` { + return `0x${value.toString(16).padStart(bytes * 2, "0")}`; +} + +function corpus(count: number) { + return Array.from({ length: count }, (_, index) => Object.freeze({ + tokenAddress: hex(index + 1, 20), + poolId: hex(index + 1, 32) as HexBytes32, + launchTransactionHash: hex(index + 10_001, 32) as HexBytes32, + launchBlockNumber: (25_000_000 + Math.floor(index / 8)).toString(), + launchTransactionIndex: index % 8, + launchLogIndex: index, + })); +} + +function contract(count: number): ReconcilerPreParityContract { + return Object.freeze({ + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "core", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "1", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "1", + reorgGeneration: "0", + checkpointBlockNumber: "25650000", + checkpointBlockHash: hex(999, 32) as HexBytes32, + routeKeys: Object.freeze([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "classic-v3-profile", + "launch-lookup", + ] as const), + routeContract: { version: "route-v1" }, + projectionContract: { + resultCommitment: hex(998, 32), + projectionRowCount: count.toString(), + }, + currentEntities: Array.from({ length: count }, (_, index) => ({ + entityKind: "launch", + entityKey: hex(index + 1, 20), + })), + }); +} + +describe("reconciler corpus partitions", () => { + it.each([ + [186, 2], + [256, 2], + [257, 3], + [513, 5], + ] as const)( + "partitions and reassembles the complete %i-launch manifest", + (count, expectedPages) => { + const identities = corpus(count); + const manifest = createReconcilerCorpusManifest({ + contract: contract(count), + identities, + }); + + expect(RECONCILER_CORPUS_PARTITION_SIZE).toBe(128); + expect(manifest.totalCount).toBe(count); + expect(manifest.pageCount).toBe(expectedPages); + expect(manifest.pages.at(-1)?.continuation).toBeNull(); + expect(assembleReconcilerCorpusPages(manifest, manifest.pages)) + .toEqual(identities); + }, + ); + + it("binds every page and continuation to the complete manifest", () => { + const identities = corpus(257); + const first = createReconcilerCorpusManifest({ + contract: contract(257), + identities, + }); + const reordered = [first.pages[1]!, first.pages[0]!, first.pages[2]!]; + const missing = first.pages.slice(0, -1); + + expect(() => assembleReconcilerCorpusPages(first, reordered)).toThrow(); + expect(() => assembleReconcilerCorpusPages(first, missing)).toThrow(); + + const changed = [...identities]; + changed[256] = Object.freeze({ + ...changed[256]!, + launchTransactionHash: hex(99_999, 32) as HexBytes32, + }); + const second = createReconcilerCorpusManifest({ + contract: contract(257), + identities: changed, + }); + expect(second.manifestCommitment).not.toBe(first.manifestCommitment); + expect(() => assembleReconcilerCorpusPages(first, second.pages)).toThrow(); + }); + + it("rejects a live corpus that omits an indexed launch", () => { + expect(() => createReconcilerCorpusManifest({ + contract: contract(257), + identities: corpus(256), + })).toThrow(); + }); + + it("partitions a large reward-entitlement set under its exact corpus page", () => { + const parent = createReconcilerCorpusManifest({ + contract: contract(128), + identities: corpus(128), + }); + const identities = Array.from({ length: 257 }, (_, index) => ({ + tokenAddress: parent.pages[0]!.identities[index % 128]!.tokenAddress, + vaultAddress: hex(index + 20_001, 20), + account: hex(index + 30_001, 20), + })); + const manifest = createReconcilerEntitlementManifest({ + contract: contract(128), + parentPage: parent.pages[0]!, + identities, + }); + + expect(manifest.pageCount).toBe(3); + expect(assembleReconcilerEntitlementPages(manifest, manifest.pages)) + .toEqual(identities); + expect(() => createReconcilerEntitlementManifest({ + contract: contract(128), + parentPage: parent.pages[0]!, + identities: [{ + tokenAddress: hex(999, 20), + vaultAddress: hex(998, 20), + account: hex(997, 20), + }], + })).toThrow(); + }); + + it("binds projector and route contract changes into the manifest", () => { + const identities = corpus(1); + const base = contract(1); + const commitment = createReconcilerCorpusManifest({ + contract: base, + identities, + }).manifestCommitment; + const variants: ReconcilerPreParityContract[] = [ + Object.freeze({ ...base, projectorVersion: "projector-v2" }), + Object.freeze({ ...base, routeKeys: base.routeKeys.slice(0, -1) }), + Object.freeze({ ...base, routeContract: { version: "route-v2" } }), + ]; + for (const variant of variants) { + expect(createReconcilerCorpusManifest({ + contract: variant, + identities, + }).manifestCommitment).not.toBe(commitment); + } + }); + + it("requires the exact indexed launch entity-key set", () => { + const identities = corpus(2); + const wrongKey = Object.freeze({ + ...contract(2), + currentEntities: [ + { entityKind: "launch", entityKey: identities[0]!.tokenAddress }, + { entityKind: "launch", entityKey: hex(999, 20) }, + ], + }); + const duplicateKey = Object.freeze({ + ...contract(2), + currentEntities: [ + { entityKind: "launch", entityKey: identities[0]!.tokenAddress }, + { entityKind: "launch", entityKey: identities[0]!.tokenAddress }, + ], + }); + expect(() => createReconcilerCorpusManifest({ + contract: wrongKey, + identities, + })).toThrow(); + expect(() => createReconcilerCorpusManifest({ + contract: duplicateKey, + identities, + })).toThrow(); + }); + + it("shares the explicit 10,000-item operational ceiling", () => { + expect(RECONCILER_CORPUS_MAXIMUM_TOTAL_COUNT).toBe(10_000); + expect(() => createReconcilerCorpusManifest({ + contract: contract(10_001), + identities: corpus(10_001), + })).toThrow(); + }); +}); diff --git a/tests/data-pipeline/reconciler-exact-block-log-bisection.test.ts b/tests/data-pipeline/reconciler-exact-block-log-bisection.test.ts new file mode 100644 index 00000000..c6cb1be9 --- /dev/null +++ b/tests/data-pipeline/reconciler-exact-block-log-bisection.test.ts @@ -0,0 +1,243 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { createExactBlockRpcClient } from "../../lib/data-pipeline/reconciler-exact-block-reader.server"; +import { projectorRpcDeploymentCommitment } from "../../lib/data-pipeline/projector-provider-commitments"; +import { rpcProviderCommitment } from "../../lib/data-pipeline/rpc-provider-commitments"; + +const ENDPOINT = "https://eth-mainnet.g.alchemy.com/v2/abcdefgh12345678"; +const ADDRESS = `0x${"11".repeat(20)}` as const; +const BLOCK_HASH = `0x${"22".repeat(32)}` as const; +const TOPIC = `0x${"33".repeat(32)}` as const; + +type RpcRequest = Readonly<{ + id: number; + method: string; + params: readonly [Readonly<{ + fromBlock: `0x${string}`; + toBlock: `0x${string}`; + }>]; +}>; + +function range(request: RpcRequest): readonly [bigint, bigint] { + return [ + BigInt(request.params[0].fromBlock), + BigInt(request.params[0].toBlock), + ]; +} + +function rpcResult(id: number, result: unknown): Response { + return new Response(JSON.stringify({ jsonrpc: "2.0", id, result }), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function rpcRangeError(id: number): Response { + return new Response(JSON.stringify({ + jsonrpc: "2.0", + id, + error: { + code: -32_005, + message: "query returned more than 10000 results; try a smaller block range", + }, + }), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function rawLog(blockNumber: bigint, logIndex = 0) { + const suffix = blockNumber.toString(16).padStart(64, "0"); + return { + address: ADDRESS, + blockNumber: `0x${blockNumber.toString(16)}`, + blockHash: BLOCK_HASH, + transactionHash: `0x${suffix}`, + transactionIndex: "0x0", + logIndex: `0x${logIndex.toString(16)}`, + removed: false, + topics: [TOPIC], + data: "0x", + }; +} + +function client(fetchMock: typeof fetch) { + return createExactBlockRpcClient({ + endpoint: ENDPOINT, + endpointCommitment: projectorRpcDeploymentCommitment(ENDPOINT), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ENDPOINT).origin, + ), + fetch: fetchMock, + }); +} + +async function getLogs( + rpc: ReturnType, + fromBlock: bigint, + toBlock: bigint, +) { + return rpc.getLogs({ + addresses: ADDRESS, + topics: [TOPIC], + fromBlock, + toBlock, + maximumLogs: 100, + signal: new AbortController().signal, + }); +} + +describe("exact-block eth_getLogs range bisection", () => { + it("bisects an explicit HTTP response-size rejection and preserves order", async () => { + const requested: Array = []; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as RpcRequest; + const [fromBlock, toBlock] = range(request); + requested.push([fromBlock, toBlock]); + if (toBlock - fromBlock + 1n > 2n) { + return new Response("payload too large", { status: 413 }); + } + return rpcResult(request.id, [rawLog(fromBlock)]); + }); + const rpc = client(fetchMock as typeof fetch); + + await expect(getLogs(rpc, 1n, 4n)).resolves.toMatchObject([ + { blockNumber: 1n, logIndex: 0 }, + { blockNumber: 3n, logIndex: 0 }, + ]); + expect(requested).toEqual([ + [1n, 4n], + [1n, 2n], + [3n, 4n], + ]); + expect(rpc.requestCount()).toBe(3); + expect(rpc.logicalRequestCount()).toBe(3); + }); + + it("performs deterministic nested bisections for provider range errors", async () => { + const requested: Array = []; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as RpcRequest; + const [fromBlock, toBlock] = range(request); + requested.push([fromBlock, toBlock]); + if (toBlock - fromBlock + 1n > 2n) { + return rpcRangeError(request.id); + } + return rpcResult(request.id, [rawLog(fromBlock)]); + }); + const rpc = client(fetchMock as typeof fetch); + + const logs = await getLogs(rpc, 1n, 8n); + expect(logs.map((log) => log.blockNumber)).toEqual([1n, 3n, 5n, 7n]); + expect(requested).toEqual([ + [1n, 8n], + [1n, 4n], + [1n, 2n], + [3n, 4n], + [5n, 8n], + [5n, 6n], + [7n, 8n], + ]); + }); + + it("returns the complete merged corpus for builder-level count bisection", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as RpcRequest; + const [fromBlock, toBlock] = range(request); + if (fromBlock === 1n && toBlock === 4n) { + return rpcRangeError(request.id); + } + return rpcResult(request.id, [rawLog(fromBlock)]); + }); + const rpc = client(fetchMock as typeof fetch); + + await expect(rpc.getLogs({ + addresses: ADDRESS, + fromBlock: 1n, + toBlock: 4n, + maximumLogs: 1, + signal: new AbortController().signal, + })).resolves.toHaveLength(2); + }); + + it("rejects a provider response that overlaps an adjacent split", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as RpcRequest; + const [fromBlock, toBlock] = range(request); + if (fromBlock === 1n && toBlock === 4n) { + return rpcRangeError(request.id); + } + return rpcResult(request.id, [rawLog(2n)]); + }); + const rpc = client(fetchMock as typeof fetch); + + await expect(getLogs(rpc, 1n, 4n)).rejects.toMatchObject({ + code: "validation_failed", + safeMetadata: { operation: "reconciler-rpc-log-block-range" }, + }); + }); + + it("rejects duplicate canonical log ordinals inside a split response", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as RpcRequest; + const [fromBlock, toBlock] = range(request); + if (fromBlock === 1n && toBlock === 4n) { + return rpcRangeError(request.id); + } + const log = rawLog(fromBlock); + return rpcResult(request.id, [log, log]); + }); + const rpc = client(fetchMock as typeof fetch); + + await expect(getLogs(rpc, 1n, 4n)).rejects.toMatchObject({ + code: "validation_failed", + safeMetadata: { operation: "reconciler-rpc-log-order" }, + }); + }); + + it("fails closed when a single block still exceeds the provider limit", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as RpcRequest; + return rpcRangeError(request.id); + }); + const rpc = client(fetchMock as typeof fetch); + + await expect(getLogs(rpc, 7n, 7n)).rejects.toMatchObject({ + code: "response_oversize", + retryable: false, + safeMetadata: { + operation: "reconciler-rpc-log-single-block-oversize", + }, + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("returns every log from a busy 10,000-block range without truncation", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as RpcRequest; + const [fromBlock, toBlock] = range(request); + if (toBlock - fromBlock + 1n > 100n) { + return rpcRangeError(request.id); + } + const logs = Array.from( + { length: Number(toBlock - fromBlock + 1n) }, + (_, offset) => rawLog(fromBlock + BigInt(offset)), + ); + return rpcResult(request.id, logs); + }); + const rpc = client(fetchMock as typeof fetch); + + const logs = await getLogs(rpc, 1n, 10_000n); + expect(logs).toHaveLength(10_000); + expect(logs[0]?.blockNumber).toBe(1n); + expect(logs.at(-1)?.blockNumber).toBe(10_000n); + expect(new Set(logs.map((log) => log.transactionHash))).toHaveLength( + 10_000, + ); + expect(rpc.requestCount()).toBe(255); + expect(rpc.logicalRequestCount()).toBe(255); + }); +}); diff --git a/tests/data-pipeline/reconciler-exact-block-reader.test.ts b/tests/data-pipeline/reconciler-exact-block-reader.test.ts new file mode 100644 index 00000000..9629778e --- /dev/null +++ b/tests/data-pipeline/reconciler-exact-block-reader.test.ts @@ -0,0 +1,1366 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { encodeFunctionData, keccak256, parseAbi, type Hex } from "viem"; + +vi.mock("server-only", () => ({})); + +import { + createExactBlockReconcilerRouteDtoReader, + createExactBlockRpcClient, +} from "../../lib/data-pipeline/reconciler-exact-block-reader.server"; +import { projectorRpcDeploymentCommitment } from "../../lib/data-pipeline/projector-provider-commitments"; +import { + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + RECONCILER_ROUTE_KEYS, + type ReconcilerPreParityContract, + type ReconcilerRouteDto, +} from "../../lib/data-pipeline/reconciler-preparity"; +import { rpcProviderCommitment } from "../../lib/data-pipeline/rpc-provider-commitments"; + +const BLOCK_HASH = `0x${"11".repeat(32)}` as const; +const ALTERNATE_HASH = `0x${"22".repeat(32)}` as const; +const ADDRESS = `0x${"33".repeat(20)}` as const; +const ALTERNATE_ADDRESS = `0x${"44".repeat(20)}` as const; +const BLOCK_NUMBER = 25_700_000n; +const ALCHEMY = "https://eth-mainnet.g.alchemy.com/v2/abcdefgh12345678"; +const QUICKNODE = "https://example.quiknode.pro/abcdefgh12345678/"; + +const contract: ReconcilerPreParityContract = { + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "ethereum-mainnet", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "8", + reorgGeneration: "2", + checkpointBlockNumber: "25700000", + checkpointBlockHash: BLOCK_HASH, + routeKeys: RECONCILER_ROUTE_KEYS, + routeContract: { exact: true }, + projectionContract: { exact: true }, + currentEntities: [], +}; + +function routes(label: string): readonly ReconcilerRouteDto[] { + return RECONCILER_ROUTE_KEYS.map((routeKey) => ({ + routeKey, + comparedCount: 1, + dto: { label, routeKey }, + })); +} + +function rpcResponse(id: number, result: unknown) { + return new Response(JSON.stringify({ jsonrpc: "2.0", id, result }), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function rpcBatchResponse( + items: readonly Readonly>[], + headers?: HeadersInit, +) { + return new Response(JSON.stringify(items), { + status: 200, + headers: { "content-type": "application/json", ...headers }, + }); +} + +const totalSupplyCall = encodeFunctionData({ + abi: parseAbi(["function totalSupply() view returns (uint256)"]), + functionName: "totalSupply", +}); + +function block(hash = BLOCK_HASH) { + return { + number: "0x18826a0", + hash, + timestamp: "0x64", + }; +} + +function blockAt(blockNumber: bigint, hash: Hex, timestamp: bigint) { + return { + number: `0x${blockNumber.toString(16)}`, + hash, + timestamp: `0x${timestamp.toString(16)}`, + }; +} + +describe("exact-block reconciler RPC", () => { + beforeEach(() => vi.restoreAllMocks()); + + it("uses an EIP-1898 canonical block hash for every eth_call", async () => { + const requests: Record[] = []; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + requests.push(body); + return rpcResponse(Number(body.id), "0x"); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + await rpc.call({ + to: ADDRESS, + data: totalSupplyCall, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + }); + + expect(requests).toHaveLength(1); + expect(requests[0]).toMatchObject({ + method: "eth_call", + params: [ + { to: ADDRESS, data: totalSupplyCall }, + { blockHash: BLOCK_HASH, requireCanonical: true }, + ], + }); + expect(JSON.stringify(requests[0])).not.toContain("latest"); + }); + + it("reads runtime code by canonical EIP-1898 block hash and returns its hash", async () => { + const runtime = "0x6001600055" as const; + const requests: Record[] = []; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + requests.push(body); + return rpcResponse(Number(body.id), runtime); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getCodeHash({ + address: ADDRESS, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).resolves.toBe(keccak256(runtime)); + + expect(requests).toEqual([expect.objectContaining({ + method: "eth_getCode", + params: [ + ADDRESS, + { blockHash: BLOCK_HASH, requireCanonical: true }, + ], + })]); + expect(JSON.stringify(requests)).not.toContain("latest"); + }); + + it("fails closed when exact-block runtime code is empty", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + return rpcResponse(Number(body.id), "0x"); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getCodeHash({ + address: ADDRESS, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "reconciler-rpc-code-empty" }, + }); + }); + + it("chunks eth_call batches and reconstructs results in request order", async () => { + const batches: Array = []; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Array<{ + id: number; + method: string; + params: readonly unknown[]; + }>; + batches.push(body); + return rpcBatchResponse(body.map((request) => ({ + jsonrpc: "2.0", + id: request.id, + result: `0x${request.id.toString(16).padStart(2, "0")}`, + })).reverse()); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumBatchSize: 2, + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.callMany({ + calls: [ + { to: ADDRESS, data: totalSupplyCall }, + { to: ALTERNATE_ADDRESS, data: totalSupplyCall }, + { to: ADDRESS, data: "0x" }, + ], + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).resolves.toEqual(["0x01", "0x02", "0x03"]); + + expect(batches.map((batch) => batch.length)).toEqual([2, 1]); + expect(batches.flat().every((request) => + request.method === "eth_call" && + JSON.stringify(request.params).includes( + `\"blockHash\":\"${BLOCK_HASH}\",\"requireCanonical\":true`, + ) && + !JSON.stringify(request.params).includes("latest") + )).toBe(true); + expect(rpc.requestCount()).toBe(2); + expect(rpc.logicalRequestCount()).toBe(3); + }); + + it("reads 257 exact-hash-bound block timestamps in nine physical requests", async () => { + const firstBlock = 30_000_000n; + const bindings = Array.from({ length: 257 }, (_, index) => { + const blockNumber = firstBlock + BigInt(index); + const expectedHash = `0x${(index + 1).toString(16).padStart(64, "0")}` as const; + return Object.freeze({ blockNumber, expectedHash }); + }); + const batches: Array = []; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Array<{ + id: number; + method: string; + params: readonly unknown[]; + }>; + batches.push(body); + return rpcBatchResponse(body.map((request) => { + const blockNumber = BigInt(request.params[0] as string); + const index = Number(blockNumber - firstBlock); + return { + jsonrpc: "2.0", + id: request.id, + result: blockAt( + blockNumber, + bindings[index]!.expectedHash, + 1_700_000_000n + BigInt(index), + ), + }; + }).reverse()); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumBatchSize: 32, + maximumRequests: 9, + maximumLogicalRequests: 257, + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getBlockTimestamps({ + blocks: bindings, + signal: new AbortController().signal, + })).resolves.toEqual(Array.from( + { length: 257 }, + (_, index) => 1_700_000_000n + BigInt(index), + )); + + expect(batches.map((batch) => batch.length)).toEqual([ + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 1, + ]); + expect(batches.flat().every((request) => + request.method === "eth_getBlockByNumber" && + request.params[1] === false + )).toBe(true); + expect(rpc.requestCount()).toBe(9); + expect(rpc.logicalRequestCount()).toBe(257); + }); + + it("fails closed when a batched timestamp block hash does not match its binding", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Array<{ id: number }>; + return rpcBatchResponse(body.map((request) => ({ + jsonrpc: "2.0", + id: request.id, + result: blockAt(BLOCK_NUMBER, ALTERNATE_HASH, 100n), + }))); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getBlockTimestamps({ + blocks: [{ blockNumber: BLOCK_NUMBER, expectedHash: BLOCK_HASH }], + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "reconciler-rpc-block-hash-mismatch" }, + }); + }); + + it("issues corpus clients in exact page order under one shared root budget", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + return rpcResponse(Number(body.id), "0x"); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumLogicalRequests: 1, + fetch: fetchMock as typeof fetch, + }); + const manifestCommitment = `0x${"51".repeat(32)}` as const; + const first = rpc.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"52".repeat(32)}`, + pageIndex: 0, + pageCount: 2, + pageSize: 128, + totalCount: 256, + startIndex: 0, + endIndexExclusive: 128, + }); + await first.call({ + to: ADDRESS, + data: totalSupplyCall, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + }); + const nested = first.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"55".repeat(32)}`, + pageIndex: 0, + pageCount: 1, + pageSize: 1, + totalCount: 1, + startIndex: 0, + endIndexExclusive: 1, + }); + expect(() => nested.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"5a".repeat(32)}`, + pageIndex: 0, + pageCount: 1, + pageSize: 1, + totalCount: 1, + startIndex: 0, + endIndexExclusive: 1, + })).toThrow(); + expect(() => rpc.createPartitionClient({ + manifestCommitment: `0x${"56".repeat(32)}`, + pageCommitment: `0x${"57".repeat(32)}`, + pageIndex: 1, + pageCount: 2, + pageSize: 128, + totalCount: 256, + startIndex: 128, + endIndexExclusive: 256, + })).toThrow(); + expect(() => rpc.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"58".repeat(32)}`, + pageIndex: 1, + pageCount: 3, + pageSize: 128, + totalCount: 256, + startIndex: 128, + endIndexExclusive: 256, + })).toThrow(); + expect(() => rpc.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"59".repeat(32)}`, + pageIndex: 1, + pageCount: 2, + pageSize: 128, + totalCount: 256, + startIndex: 129, + endIndexExclusive: 256, + })).toThrow(); + const second = rpc.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"53".repeat(32)}`, + pageIndex: 1, + pageCount: 2, + pageSize: 128, + totalCount: 256, + startIndex: 128, + endIndexExclusive: 256, + }); + await expect(second.call({ + to: ADDRESS, + data: totalSupplyCall, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "response_oversize", + retryable: false, + safeMetadata: { operation: "reconciler-rpc-logical-budget" }, + }); + + expect(rpc.requestCount()).toBe(2); + expect(rpc.logicalRequestCount()).toBe(2); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(() => rpc.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"54".repeat(32)}`, + pageIndex: 1, + pageCount: 2, + pageSize: 128, + totalCount: 256, + startIndex: 128, + endIndexExclusive: 256, + })).toThrow(); + }); + + it("rejects a partition sequence that does not start at page zero and index zero", () => { + const fetchMock = vi.fn(); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + const manifestCommitment = `0x${"61".repeat(32)}` as const; + expect(() => rpc.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"62".repeat(32)}`, + pageIndex: 1, + pageCount: 2, + pageSize: 128, + totalCount: 256, + startIndex: 128, + endIndexExclusive: 256, + })).toThrow(); + expect(() => rpc.createPartitionClient({ + manifestCommitment, + pageCommitment: `0x${"63".repeat(32)}`, + pageIndex: 0, + pageCount: 2, + pageSize: 128, + totalCount: 256, + startIndex: 1, + endIndexExclusive: 128, + })).toThrow(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it.each([ + { + label: "missing IDs", + operation: "reconciler-rpc-batch-id-missing", + response: (ids: readonly number[]) => [ + { jsonrpc: "2.0", id: ids[0], result: "0x01" }, + ], + }, + { + label: "duplicate IDs", + operation: "reconciler-rpc-batch-id-duplicate", + response: (ids: readonly number[]) => [ + { jsonrpc: "2.0", id: ids[0], result: "0x01" }, + { jsonrpc: "2.0", id: ids[0], result: "0x02" }, + ], + }, + { + label: "unknown IDs", + operation: "reconciler-rpc-batch-id-unknown", + response: (ids: readonly number[]) => [ + { jsonrpc: "2.0", id: ids[0], result: "0x01" }, + { jsonrpc: "2.0", id: 999_999, result: "0x02" }, + ], + }, + { + label: "string IDs", + operation: "reconciler-rpc-batch-item", + response: (ids: readonly number[]) => [ + { jsonrpc: "2.0", id: String(ids[0]), result: "0x01" }, + { jsonrpc: "2.0", id: ids[1], result: "0x02" }, + ], + }, + { + label: "per-item errors", + operation: "reconciler-rpc-batch-item-error", + response: (ids: readonly number[]) => [ + { jsonrpc: "2.0", id: ids[0], result: "0x01" }, + { jsonrpc: "2.0", id: ids[1], error: { code: -32_000 } }, + ], + }, + { + label: "both result and error", + operation: "reconciler-rpc-batch-item-shape", + response: (ids: readonly number[]) => [ + { jsonrpc: "2.0", id: ids[0], result: "0x01" }, + { + jsonrpc: "2.0", + id: ids[1], + result: "0x02", + error: { code: -32_000 }, + }, + ], + }, + ])("fails closed on $label", async ({ operation, response }) => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Array<{ id: number }>; + return rpcBatchResponse(response(body.map((request) => request.id))); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.callMany({ + calls: [ + { to: ADDRESS, data: totalSupplyCall }, + { to: ALTERNATE_ADDRESS, data: totalSupplyCall }, + ], + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation }, + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("checks the same checkpoint before and after a complete live build", async () => { + const methods: string[] = []; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { + id: number; + method: string; + }; + methods.push(body.method); + return rpcResponse(body.id, block()); + }); + const reader = createExactBlockReconcilerRouteDtoReader({ + env: { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + indexedStore: { + readExactIndexedRouteCorpus: vi.fn(async () => routes("indexed")), + }, + buildLiveRoutes: vi.fn(async () => routes("live")), + fetch: fetchMock as typeof fetch, + }); + const source = { + identity: "alchemy-mainnet-test", + vendorGroup: "alchemy", + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + }; + + await expect(reader.readLiveRoutes({ + source, + contract, + blockNumber: 25_700_000n, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).resolves.toEqual(routes("live")); + expect(methods).toEqual([ + "eth_getBlockByNumber", + "eth_getBlockByNumber", + ]); + }); + + it("selects only the routes applicable to the requested release", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { id: number }; + return rpcResponse(body.id, block()); + }); + const reader = createExactBlockReconcilerRouteDtoReader({ + env: { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + indexedStore: { + readExactIndexedRouteCorpus: vi.fn(async () => routes("indexed")), + }, + buildLiveRoutes: vi.fn(async () => routes("live")), + fetch: fetchMock as typeof fetch, + }); + const classicV2Contract: ReconcilerPreParityContract = { + ...contract, + releaseId: "classic-v2", + routeKeys: CLASSIC_V2_RECONCILER_ROUTE_KEYS, + }; + + const result = await reader.readLiveRoutes({ + source: { + identity: "alchemy-mainnet-test", + vendorGroup: "alchemy", + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + }, + contract: classicV2Contract, + blockNumber: BLOCK_NUMBER, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + }); + + expect(result.map(({ routeKey }) => routeKey)).toEqual( + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + ); + }); + + it("fails closed if the checkpoint changes after the route read", async () => { + let blockRead = 0; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { + id: number; + method: string; + }; + blockRead += 1; + return rpcResponse(body.id, block(blockRead === 1 ? BLOCK_HASH : ALTERNATE_HASH)); + }); + const reader = createExactBlockReconcilerRouteDtoReader({ + env: { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + indexedStore: { + readExactIndexedRouteCorpus: vi.fn(async () => routes("indexed")), + }, + buildLiveRoutes: vi.fn(async () => routes("live")), + fetch: fetchMock as typeof fetch, + }); + + await expect(reader.readLiveRoutes({ + source: { + identity: "alchemy-mainnet-test", + vendorGroup: "alchemy", + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + }, + contract, + blockNumber: 25_700_000n, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "reconciler-rpc-checkpoint-mismatch" }, + }); + }); + + it("counts physical requests and never retries past the provider budget", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { id: number }; + return rpcResponse(body.id, block()); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumRequests: 1, + fetch: fetchMock as typeof fetch, + }); + const signal = new AbortController().signal; + await rpc.assertCheckpoint({ + blockNumber: 25_700_000n, + blockHash: BLOCK_HASH, + signal, + }); + await expect(rpc.assertCheckpoint({ + blockNumber: 25_700_000n, + blockHash: BLOCK_HASH, + signal, + })).rejects.toMatchObject({ + code: "response_oversize", + retryable: false, + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(rpc.requestCount()).toBe(2); + }); + + it("rejects an over-budget batch before sending a partial prefix", async () => { + const fetchMock = vi.fn(); + const physicallyBounded = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumRequests: 1, + maximumLogicalRequests: 10, + maximumBatchSize: 2, + fetch: fetchMock as typeof fetch, + }); + const calls = [ + { to: ADDRESS, data: totalSupplyCall }, + { to: ALTERNATE_ADDRESS, data: totalSupplyCall }, + { to: ADDRESS, data: "0x" as Hex }, + ]; + + await expect(physicallyBounded.callMany({ + calls, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "response_oversize", + retryable: false, + safeMetadata: { operation: "reconciler-rpc-request-budget" }, + }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(physicallyBounded.requestCount()).toBe(2); + expect(physicallyBounded.logicalRequestCount()).toBe(3); + + const logicallyBounded = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumRequests: 10, + maximumLogicalRequests: 2, + maximumBatchSize: 100, + fetch: fetchMock as typeof fetch, + }); + await expect(logicallyBounded.callMany({ + calls, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "response_oversize", + retryable: false, + safeMetadata: { operation: "reconciler-rpc-logical-budget" }, + }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(logicallyBounded.requestCount()).toBe(1); + expect(logicallyBounded.logicalRequestCount()).toBe(3); + }); + + it("enforces the response-byte limit for a batch without retrying", async () => { + const fetchMock = vi.fn(async () => new Response("[]", { + status: 200, + headers: { "content-length": String(8 * 1024 * 1024 + 1) }, + })); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.callMany({ + calls: [{ to: ADDRESS, data: totalSupplyCall }], + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "response_oversize", + safeMetadata: { operation: "reconciler-rpc-response" }, + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("cancels an oversized streamed response before decoding it", async () => { + let cancelled = false; + const fetchMock = vi.fn(async () => new Response(new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(8 * 1024 * 1024)); + controller.enqueue(new Uint8Array(1)); + }, + cancel() { + cancelled = true; + }, + }), { status: 200 })); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.callMany({ + calls: [{ to: ADDRESS, data: totalSupplyCall }], + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ code: "response_oversize" }); + expect(cancelled).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("times out a batch once and never retries it", async () => { + vi.useFakeTimers(); + try { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => + await new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => { + reject(new DOMException("aborted", "AbortError")); + }, { once: true }); + }) + ); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + timeoutMs: 100, + fetch: fetchMock as typeof fetch, + }); + const pending = expect(rpc.callMany({ + calls: [{ to: ADDRESS, data: totalSupplyCall }], + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ code: "timeout", retryable: true }); + + await vi.advanceTimersByTimeAsync(100); + await pending; + expect(fetchMock).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); + + it("does not retry provider rate limits", async () => { + const fetchMock = vi.fn(async () => new Response("rate limited", { + status: 429, + })); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.callMany({ + calls: [{ to: ADDRESS, data: totalSupplyCall }], + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "dependency_unavailable", + retryable: true, + safeMetadata: { status: 429 }, + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("rejects truncated log corpora instead of accepting a prefix", async () => { + const rawLog = { + address: ADDRESS, + blockNumber: "0x1", + blockHash: BLOCK_HASH, + transactionHash: ALTERNATE_HASH, + transactionIndex: "0x0", + logIndex: "0x0", + topics: [`0x${"44".repeat(32)}` as Hex], + data: "0x", + }; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { id: number }; + return rpcResponse(body.id, [rawLog, { ...rawLog, logIndex: "0x1" }]); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getLogs({ + addresses: ADDRESS, + fromBlock: 1n, + toBlock: 1n, + maximumLogs: 1, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + code: "response_oversize", + retryable: false, + }); + }); + + it("returns a block-bound successful receipt with explicit log ordinals", async () => { + const transactionIndex = "0x2"; + const receipt = { + transactionHash: ALTERNATE_HASH, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionIndex, + status: "0x1", + logs: [4, 7].map((logIndex) => ({ + address: ADDRESS, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionHash: ALTERNATE_HASH, + transactionIndex, + logIndex: `0x${logIndex.toString(16)}`, + removed: false, + topics: [`0x${"55".repeat(32)}`], + data: "0x1234", + })), + }; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { id: number }; + return rpcResponse(body.id, receipt); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getTransactionReceipt({ + transactionHash: ALTERNATE_HASH, + expectedBlockNumber: BLOCK_NUMBER, + expectedBlockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).resolves.toMatchObject({ + transactionHash: ALTERNATE_HASH, + blockNumber: BLOCK_NUMBER, + blockHash: BLOCK_HASH, + transactionIndex: 2, + status: 1n, + logs: [ + { receiptLogIndex: 0, logIndex: 4 }, + { receiptLogIndex: 1, logIndex: 7 }, + ], + }); + }); + + it("batches receipts in input order with exact bindings and log ordinals", async () => { + const thirdHash = `0x${"66".repeat(32)}` as const; + const transactionHashes = [BLOCK_HASH, ALTERNATE_HASH, thirdHash] as const; + const transactionIndexes = new Map(transactionHashes.map( + (transactionHash, index) => [transactionHash, index + 1] as const, + )); + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Array<{ + id: number; + method: string; + params: [typeof BLOCK_HASH]; + }>; + return rpcBatchResponse(body.map((request) => { + const transactionHash = request.params[0]; + const transactionIndex = transactionIndexes.get(transactionHash)!; + return { + jsonrpc: "2.0", + id: request.id, + result: { + transactionHash, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionIndex: `0x${transactionIndex.toString(16)}`, + status: "0x1", + logs: [0, 1].map((receiptLogIndex) => ({ + address: receiptLogIndex === 0 ? ADDRESS : ALTERNATE_ADDRESS, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionHash, + transactionIndex: `0x${transactionIndex.toString(16)}`, + logIndex: `0x${( + transactionIndex * 10 + receiptLogIndex + ).toString(16)}`, + topics: [`0x${"55".repeat(32)}`], + data: "0x", + })), + }, + }; + }).reverse()); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumBatchSize: 2, + fetch: fetchMock as typeof fetch, + }); + + const result = await rpc.getTransactionReceipts({ + receipts: transactionHashes.map((transactionHash) => ({ + transactionHash, + expectedBlockNumber: BLOCK_NUMBER, + expectedBlockHash: BLOCK_HASH, + })), + signal: new AbortController().signal, + }); + + expect(result.map((receipt) => receipt.transactionHash)).toEqual( + transactionHashes, + ); + expect(result.map((receipt) => + receipt.logs.map((log) => log.receiptLogIndex) + )).toEqual([[0, 1], [0, 1], [0, 1]]); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock.mock.calls.every(([, init]) => + (JSON.parse(String(init?.body)) as Array<{ method: string }>).every( + (request) => request.method === "eth_getTransactionReceipt", + ) + )).toBe(true); + expect(rpc.requestCount()).toBe(2); + expect(rpc.logicalRequestCount()).toBe(3); + }); + + it("fails the complete receipt batch if one item is not block-bound", async () => { + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Array<{ + id: number; + params: [typeof BLOCK_HASH]; + }>; + return rpcBatchResponse(body.map((request, index) => ({ + jsonrpc: "2.0", + id: request.id, + result: { + transactionHash: request.params[0], + blockNumber: index === 0 ? "0x18826a0" : "0x188269f", + blockHash: BLOCK_HASH, + transactionIndex: `0x${index.toString(16)}`, + status: "0x1", + logs: [], + }, + }))); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getTransactionReceipts({ + receipts: [BLOCK_HASH, ALTERNATE_HASH].map((transactionHash) => ({ + transactionHash, + expectedBlockNumber: BLOCK_NUMBER, + expectedBlockHash: BLOCK_HASH, + })), + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "reconciler-rpc-receipt-binding" }, + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it.each([ + { + label: "a reverted status", + mutate: (receipt: Record) => ({ ...receipt, status: "0x0" }), + operation: "reconciler-rpc-receipt-binding", + }, + { + label: "a different block hash", + mutate: (receipt: Record) => ({ + ...receipt, + blockHash: ALTERNATE_HASH, + }), + operation: "reconciler-rpc-receipt-binding", + }, + { + label: "a log from another transaction", + mutate: (receipt: Record) => ({ + ...receipt, + logs: [{ + ...((receipt.logs as Array>)[0]!), + transactionHash: BLOCK_HASH, + }], + }), + operation: "reconciler-rpc-receipt-log-binding", + }, + { + label: "non-increasing log indexes", + mutate: (receipt: Record) => ({ + ...receipt, + logs: [ + (receipt.logs as Array>)[0], + { + ...((receipt.logs as Array>)[0]!), + address: ALTERNATE_ADDRESS, + }, + ], + }), + operation: "reconciler-rpc-receipt-log-order", + }, + ])("rejects receipt responses with $label", async ({ mutate, operation }) => { + const baseReceipt: Record = { + transactionHash: ALTERNATE_HASH, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionIndex: "0x2", + status: "0x1", + logs: [{ + address: ADDRESS, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionHash: ALTERNATE_HASH, + transactionIndex: "0x2", + logIndex: "0x4", + topics: [`0x${"55".repeat(32)}`], + data: "0x", + }], + }; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { id: number }; + return rpcResponse(body.id, mutate(baseReceipt)); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getTransactionReceipt({ + transactionHash: ALTERNATE_HASH, + expectedBlockNumber: BLOCK_NUMBER, + expectedBlockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation }, + }); + }); + + it("returns only a transaction bound to the expected hash, block and recipient", async () => { + const transaction = { + hash: ALTERNATE_HASH, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionIndex: "0x2", + from: ALTERNATE_ADDRESS, + to: ADDRESS, + input: "0x1234", + value: "0x5", + }; + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as { id: number }; + return rpcResponse(body.id, transaction); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + fetch: fetchMock as typeof fetch, + }); + + await expect(rpc.getTransaction({ + transactionHash: ALTERNATE_HASH, + expectedBlockNumber: BLOCK_NUMBER, + expectedBlockHash: BLOCK_HASH, + expectedTo: ADDRESS, + signal: new AbortController().signal, + })).resolves.toEqual({ + transactionHash: ALTERNATE_HASH, + blockNumber: BLOCK_NUMBER, + blockHash: BLOCK_HASH, + transactionIndex: 2, + from: ALTERNATE_ADDRESS, + to: ADDRESS, + input: "0x1234", + value: 5n, + }); + + await expect(rpc.getTransaction({ + transactionHash: ALTERNATE_HASH, + expectedBlockNumber: BLOCK_NUMBER, + expectedBlockHash: BLOCK_HASH, + expectedTo: ALTERNATE_ADDRESS, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + safeMetadata: { operation: "reconciler-rpc-transaction-binding" }, + }); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it("batches transactions while preserving each transaction's exact binding", async () => { + const thirdHash = `0x${"66".repeat(32)}` as const; + const byHash = new Map([ + [BLOCK_HASH, { from: ALTERNATE_ADDRESS, input: "0x01", value: "0x1" }], + [ALTERNATE_HASH, { from: ADDRESS, input: "0x02", value: "0x2" }], + [thirdHash, { from: ALTERNATE_ADDRESS, input: "0x03", value: "0x3" }], + ]); + const fetchMock = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Array<{ + id: number; + params: [typeof BLOCK_HASH]; + }>; + return rpcBatchResponse(body.map((request, index) => { + const transactionHash = request.params[0]; + const values = byHash.get(transactionHash)!; + return { + jsonrpc: "2.0", + id: request.id, + result: { + hash: transactionHash, + blockNumber: "0x18826a0", + blockHash: BLOCK_HASH, + transactionIndex: `0x${index.toString(16)}`, + to: ADDRESS, + ...values, + }, + }; + }).reverse()); + }); + const rpc = createExactBlockRpcClient({ + endpoint: ALCHEMY, + endpointCommitment: projectorRpcDeploymentCommitment(ALCHEMY), + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + maximumBatchSize: 2, + fetch: fetchMock as typeof fetch, + }); + + const result = await rpc.getTransactions({ + transactions: [BLOCK_HASH, ALTERNATE_HASH, thirdHash].map( + (transactionHash) => ({ + transactionHash, + expectedBlockNumber: BLOCK_NUMBER, + expectedBlockHash: BLOCK_HASH, + expectedTo: ADDRESS, + }), + ), + signal: new AbortController().signal, + }); + + expect(result.map((transaction) => transaction.transactionHash)).toEqual([ + BLOCK_HASH, + ALTERNATE_HASH, + thirdHash, + ]); + expect(result.map((transaction) => transaction.input)).toEqual([ + "0x01", + "0x02", + "0x03", + ]); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(rpc.requestCount()).toBe(2); + expect(rpc.logicalRequestCount()).toBe(3); + }); + + it("rejects an unbound provider identity before making a request", async () => { + const fetchMock = vi.fn(); + const reader = createExactBlockReconcilerRouteDtoReader({ + env: { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + indexedStore: { + readExactIndexedRouteCorpus: vi.fn(async () => routes("indexed")), + }, + buildLiveRoutes: vi.fn(async () => routes("live")), + fetch: fetchMock as typeof fetch, + }); + + await expect(reader.readLiveRoutes({ + source: { + identity: "alchemy-mainnet-test", + vendorGroup: "alchemy", + endpointCommitment: ALTERNATE_HASH, + endpointOriginCommitment: rpcProviderCommitment( + "origin", + new URL(ALCHEMY).origin, + ), + }, + contract, + blockNumber: 25_700_000n, + blockHash: BLOCK_HASH, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "rpc", + code: "invalid_input", + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/data-pipeline/reconciler-preparity-route.test.ts b/tests/data-pipeline/reconciler-preparity-route.test.ts new file mode 100644 index 00000000..ff50bf59 --- /dev/null +++ b/tests/data-pipeline/reconciler-preparity-route.test.ts @@ -0,0 +1,158 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const { runConfigured } = vi.hoisted(() => ({ + runConfigured: vi.fn(), +})); + +vi.mock( + "../../lib/data-pipeline/reconciler-preparity.server", + () => ({ runConfiguredReconcilerPreParity: runConfigured }), +); + +import { NextRequest } from "next/server"; + +import { POST } from "../../app/api/ops/reconcile-preparity/route"; + +const SECRET = "reconciler-test-secret-32-characters"; +const BODY = { + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "ethereum-mainnet", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointBlockNumber: "25700000", + checkpointBlockHash: `0x${"11".repeat(32)}`, + maximumEntityCount: 10_000, +} as const; + +function request(body: unknown = BODY, secret = SECRET) { + return new NextRequest("https://programmable.family/api/ops/reconcile-preparity", { + method: "POST", + headers: { + authorization: `Bearer ${secret}`, + "content-type": "application/json", + }, + body: JSON.stringify(body), + }); +} + +describe("reconciler pre-parity ops route", () => { + beforeEach(() => { + process.env.CRON_SECRET = SECRET; + runConfigured.mockReset(); + }); + + afterEach(() => { + delete process.env.CRON_SECRET; + vi.restoreAllMocks(); + }); + + it("rejects missing or wrong credentials before parsing or executing", async () => { + const missing = new NextRequest( + "https://programmable.family/api/ops/reconcile-preparity", + { method: "POST", body: "not-json" }, + ); + const missingResponse = await POST(missing); + expect(missingResponse.status).toBe(401); + expect(missingResponse.headers.get("cache-control")).toBe("no-store"); + + const wrongResponse = await POST(request(BODY, `${SECRET}-wrong`)); + expect(wrongResponse.status).toBe(401); + + process.env.CRON_SECRET = "too-short"; + const weakSecretResponse = await POST(request(BODY, "too-short")); + expect(weakSecretResponse.status).toBe(401); + + const overlongUtf8Secret = "🌸".repeat(300); + process.env.CRON_SECRET = overlongUtf8Secret; + const overlongUtf8Response = await POST( + request(BODY, SECRET), + ); + expect(overlongUtf8Response.status).toBe(401); + + expect(runConfigured).not.toHaveBeenCalled(); + }); + + it("rejects unknown fields and incomplete checkpoint identities", async () => { + const response = await POST(request({ ...BODY, latest: true })); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + error: "Invalid checkpoint request", + }); + expect(runConfigured).not.toHaveBeenCalled(); + }); + + it("returns only bounded reconciliation evidence on success", async () => { + runConfigured.mockResolvedValue({ + runId: "20000000-0000-4000-8000-000000000001", + reconciliationId: "20000000-0000-4000-8000-000000000002", + checkpointId: BODY.checkpointId, + checkpointBlockNumber: BODY.checkpointBlockNumber, + checkpointBlockHash: BODY.checkpointBlockHash, + routeCount: 6, + mismatchCount: 0, + status: "succeeded", + }); + vi.spyOn(console, "info").mockImplementation(() => undefined); + + const response = await POST(request()); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ + ok: true, + status: "succeeded", + routeCount: 6, + mismatchCount: 0, + checkpointId: BODY.checkpointId, + checkpointBlockNumber: BODY.checkpointBlockNumber, + checkpointBlockHash: BODY.checkpointBlockHash, + }); + expect(runConfigured).toHaveBeenCalledWith({ request: BODY }); + }); + + it("surfaces a recorded mismatch as a conflict rather than success", async () => { + runConfigured.mockResolvedValue({ + runId: "20000000-0000-4000-8000-000000000001", + reconciliationId: "20000000-0000-4000-8000-000000000002", + checkpointId: BODY.checkpointId, + checkpointBlockNumber: BODY.checkpointBlockNumber, + checkpointBlockHash: BODY.checkpointBlockHash, + routeCount: 6, + mismatchCount: 1, + status: "failed", + }); + vi.spyOn(console, "info").mockImplementation(() => undefined); + + const response = await POST(request()); + + expect(response.status).toBe(409); + await expect(response.json()).resolves.toMatchObject({ + ok: false, + status: "failed", + mismatchCount: 1, + }); + }); + + it("fails closed without leaking an upstream error or secret", async () => { + const upstream = `https://rpc.invalid/${SECRET}`; + runConfigured.mockRejectedValue(new Error(upstream)); + const errorLog = vi.spyOn(console, "error").mockImplementation(() => undefined); + + const response = await POST(request()); + const text = await response.text(); + + expect(response.status).toBe(503); + expect(text).not.toContain(upstream); + expect(text).not.toContain(SECRET); + expect(errorLog).toHaveBeenCalledWith( + "Programmable reconciliation failed", + expect.not.objectContaining({ message: expect.anything() }), + ); + }); +}); diff --git a/tests/data-pipeline/reconciler-preparity-server.test.ts b/tests/data-pipeline/reconciler-preparity-server.test.ts new file mode 100644 index 00000000..2e5a81c1 --- /dev/null +++ b/tests/data-pipeline/reconciler-preparity-server.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { runConfiguredReconcilerPreParity } from "../../lib/data-pipeline/reconciler-preparity.server"; +import type { ReconcilerCheckpointRequest } from "../../lib/data-pipeline/reconciler-preparity"; + +const request: ReconcilerCheckpointRequest = { + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "ethereum-mainnet", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointBlockNumber: "25700000", + checkpointBlockHash: `0x${"11".repeat(32)}`, + maximumEntityCount: 10_000, +}; + +describe("configured reconciler bootstrap", () => { + it("wires the reviewed Classic V3 exact-block builder before database configuration", async () => { + await expect( + runConfiguredReconcilerPreParity({ request, env: {} }), + ).rejects.toMatchObject({ + dependency: "config", + code: "invalid_input", + retryable: false, + safeMetadata: { operation: "reconciler-database-url" }, + }); + }); + + it("wires the reviewed Classic V2 historical reader before database configuration", async () => { + await expect( + runConfiguredReconcilerPreParity({ + request: { + ...request, + releaseId: "classic-v2", + }, + env: {}, + }), + ).rejects.toMatchObject({ + dependency: "config", + code: "invalid_input", + retryable: false, + safeMetadata: { operation: "reconciler-database-url" }, + }); + }); + + it.each([ + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ])("wires the reviewed %s historical reader before database configuration", async ( + releaseId, + ) => { + await expect( + runConfiguredReconcilerPreParity({ + request: { + ...request, + releaseId, + modelId: "stock-paired", + }, + env: {}, + }), + ).rejects.toMatchObject({ + dependency: "config", + code: "invalid_input", + retryable: false, + safeMetadata: { operation: "reconciler-database-url" }, + }); + }); + + it("keeps unsupported releases fail closed", async () => { + await expect( + runConfiguredReconcilerPreParity({ + request: { + ...request, + releaseId: "deep-v3", + modelId: "deep", + }, + env: {}, + }), + ).rejects.toMatchObject({ + dependency: "config", + code: "invalid_input", + retryable: false, + safeMetadata: { operation: "reconciler-release-model" }, + }); + }); +}); diff --git a/tests/data-pipeline/reconciler-preparity.test.ts b/tests/data-pipeline/reconciler-preparity.test.ts new file mode 100644 index 00000000..f3c6112e --- /dev/null +++ b/tests/data-pipeline/reconciler-preparity.test.ts @@ -0,0 +1,417 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import type { CandidateRpcProvider } from "../../lib/data-pipeline/dual-rpc"; +import { + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + RECONCILER_ROUTE_KEYS, + STOCK_PAIRED_RECONCILER_ROUTE_KEYS, + reconcilerRouteKeysForScope, + runReconcilerPreParityCycle, + type ReconcilerCheckpointRequest, + type ReconcilerCommitInput, + type ReconcilerPreParityContract, + type ReconcilerPreParityStore, + type ReconcilerRouteDto, + type ReconcilerRouteDtoReader, +} from "../../lib/data-pipeline/reconciler-preparity"; + +const BLOCK_NUMBER = 25_700_000n; +const BLOCK_HASH = `0x${"11".repeat(32)}` as const; +const ENDPOINT_A = `0x${"21".repeat(32)}` as const; +const ENDPOINT_B = `0x${"22".repeat(32)}` as const; +const ORIGIN_A = `0x${"31".repeat(32)}` as const; +const ORIGIN_B = `0x${"32".repeat(32)}` as const; + +const request: ReconcilerCheckpointRequest = Object.freeze({ + chainId: "1", + releaseId: "classic-v3", + modelId: "classic", + sourceGroup: "ethereum-mainnet", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointBlockNumber: BLOCK_NUMBER.toString(), + checkpointBlockHash: BLOCK_HASH, + maximumEntityCount: 10_000, +}); + +const contract: ReconcilerPreParityContract = Object.freeze({ + chainId: "1", + releaseId: request.releaseId, + modelId: request.modelId, + sourceGroup: request.sourceGroup, + projectorVersion: "projector-v1", + epochId: request.epochId, + pointerGeneration: request.pointerGeneration, + checkpointId: request.checkpointId, + checkpointGeneration: "11", + reorgGeneration: "0", + checkpointBlockNumber: request.checkpointBlockNumber, + checkpointBlockHash: request.checkpointBlockHash, + routeKeys: RECONCILER_ROUTE_KEYS, + routeContract: { routes: [...RECONCILER_ROUTE_KEYS] }, + projectionContract: { resultCommitment: `0x${"44".repeat(32)}` }, + currentEntities: [{ entityKind: "token", entityKey: "0x01" }], +}); + +function routeDtos( + suffix = "same", + routeKeys = RECONCILER_ROUTE_KEYS as readonly typeof RECONCILER_ROUTE_KEYS[number][], +): ReconcilerRouteDto[] { + return routeKeys.map((routeKey, index) => ({ + routeKey, + comparedCount: index + 1, + dto: { + routeKey, + suffix, + records: [{ id: String(index + 1), amount: "1000000000000000000" }], + }, + })); +} + +function provider(input: { + identity: string; + vendorGroup: string; + endpointCommitment: typeof ENDPOINT_A; + endpointOriginCommitment: typeof ORIGIN_A; + blockHash?: typeof BLOCK_HASH; +}) { + const getBlockNumber = vi.fn(async () => BLOCK_NUMBER + 100n); + const getBlock = vi.fn(async ({ blockNumber }: { blockNumber: bigint }) => ({ + number: blockNumber, + hash: input.blockHash ?? BLOCK_HASH, + timestamp: 1_785_500_000n, + })); + const value: CandidateRpcProvider = { + identity: input.identity, + vendorGroup: input.vendorGroup, + endpointCommitment: input.endpointCommitment, + endpointOriginCommitment: input.endpointOriginCommitment, + client: { + getChainId: vi.fn(async () => 1), + getBlockNumber, + getBlock, + getTransactionReceipt: vi.fn(), + getBytecode: vi.fn(), + }, + }; + return { value, getBlockNumber, getBlock }; +} + +function runtime(overrides: { + firstLive?: ReconcilerRouteDto[]; + secondLive?: ReconcilerRouteDto[]; + indexed?: ReconcilerRouteDto[]; + contract?: ReconcilerPreParityContract; + request?: ReconcilerCheckpointRequest; + reader?: ReconcilerRouteDtoReader; + providers?: readonly CandidateRpcProvider[]; +} = {}) { + const first = provider({ + identity: "alchemy-mainnet-a", + vendorGroup: "alchemy", + endpointCommitment: ENDPOINT_A, + endpointOriginCommitment: ORIGIN_A, + }); + const second = provider({ + identity: "quicknode-mainnet-b", + vendorGroup: "quicknode", + endpointCommitment: ENDPOINT_B, + endpointOriginCommitment: ORIGIN_B, + }); + const firstLive = overrides.firstLive ?? routeDtos(); + const secondLive = overrides.secondLive ?? routeDtos(); + const indexed = overrides.indexed ?? routeDtos(); + const readLiveRoutes = vi.fn(async ({ source }) => + source.vendorGroup === "alchemy" ? firstLive : secondLive, + ); + const reader = + overrides.reader ?? + ({ + readLiveRoutes, + readIndexedRoutes: vi.fn(async () => indexed), + } satisfies ReconcilerRouteDtoReader); + const readExactContract = vi.fn(async () => overrides.contract ?? contract); + const commitResult = vi.fn(async (input: ReconcilerCommitInput) => { + const mismatchCount = input.legacyDtoHashes.filter( + (hash, index) => hash !== input.indexedDtoHashes[index], + ).length; + return { + runId: input.runId, + reconciliationId: input.reconciliationId, + checkpointId: input.contract.checkpointId, + checkpointBlockNumber: input.contract.checkpointBlockNumber, + checkpointBlockHash: input.contract.checkpointBlockHash, + routeCount: input.routeKeys.length, + mismatchCount, + status: mismatchCount === 0 ? ("succeeded" as const) : ("failed" as const), + }; + }); + const store: ReconcilerPreParityStore = { + readExactContract, + commitResult, + }; + let uuidCounter = 0; + const uuidFactory = () => { + uuidCounter += 1; + return `20000000-0000-4000-8000-${String(uuidCounter).padStart(12, "0")}`; + }; + let nowCounter = 0; + const now = () => new Date(1_785_500_000_000 + nowCounter++); + return { + first, + second, + reader, + readLiveRoutes, + readExactContract, + commitResult, + input: { + request: overrides.request ?? request, + store, + providers: overrides.providers ?? [first.value, second.value], + routeDtoReader: reader, + uuidFactory, + now, + deadlineMs: 2_000, + }, + }; +} + +describe("exact-checkpoint reconciler", () => { + it("uses the exact applicable-route matrix for every supported release", () => { + expect(reconcilerRouteKeysForScope("classic-v2", "classic")).toEqual( + CLASSIC_V2_RECONCILER_ROUTE_KEYS, + ); + expect(reconcilerRouteKeysForScope("classic-v3", "classic")).toEqual( + RECONCILER_ROUTE_KEYS, + ); + for (const releaseId of [ + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", + ]) { + expect(reconcilerRouteKeysForScope(releaseId, "stock-paired")).toEqual( + STOCK_PAIRED_RECONCILER_ROUTE_KEYS, + ); + } + expect(() => reconcilerRouteKeysForScope("classic-v2", "stock-paired")) + .toThrow(); + }); + + it("commits only four records for a Classic V2 checkpoint", async () => { + const classicV2Request = { + ...request, + releaseId: "classic-v2", + }; + const classicV2Contract = { + ...contract, + releaseId: "classic-v2", + routeKeys: CLASSIC_V2_RECONCILER_ROUTE_KEYS, + routeContract: { routes: [...CLASSIC_V2_RECONCILER_ROUTE_KEYS] }, + }; + const routes = routeDtos("same", CLASSIC_V2_RECONCILER_ROUTE_KEYS); + const fixture = runtime({ + request: classicV2Request, + contract: classicV2Contract, + firstLive: routes, + secondLive: routes, + indexed: routes, + }); + + await expect(runReconcilerPreParityCycle(fixture.input)).resolves + .toMatchObject({ status: "succeeded", routeCount: 4 }); + const commit = fixture.commitResult.mock.calls[0]![0]; + expect(commit.routeKeys).toEqual(CLASSIC_V2_RECONCILER_ROUTE_KEYS); + expect(commit.parityRecordIds).toHaveLength(4); + expect(commit.parityBindingIds).toHaveLength(4); + }); + + it("reads both independent providers at the same explicit checkpoint and atomically commits six routes", async () => { + const fixture = runtime(); + + await expect(runReconcilerPreParityCycle(fixture.input)).resolves.toMatchObject({ + status: "succeeded", + routeCount: 6, + mismatchCount: 0, + checkpointBlockNumber: BLOCK_NUMBER.toString(), + checkpointBlockHash: BLOCK_HASH, + }); + + expect(fixture.first.getBlock).toHaveBeenCalledWith({ + blockNumber: BLOCK_NUMBER, + }); + expect(fixture.second.getBlock).toHaveBeenCalledWith({ + blockNumber: BLOCK_NUMBER, + }); + expect(fixture.first.getBlockNumber).not.toHaveBeenCalled(); + expect(fixture.second.getBlockNumber).not.toHaveBeenCalled(); + expect(fixture.readLiveRoutes).toHaveBeenCalledTimes(2); + expect(fixture.commitResult).toHaveBeenCalledTimes(1); + + const commit = fixture.commitResult.mock.calls[0]![0]; + expect(commit.routeKeys).toEqual(RECONCILER_ROUTE_KEYS); + expect(commit.legacyDtoHashes).toHaveLength(6); + expect(commit.indexedDtoHashes).toHaveLength(6); + expect(commit.routeEvidenceCommitments).toHaveLength(6); + expect(commit.parityBindingCommitments).toHaveLength(6); + expect(new Set([...commit.parityRecordIds, ...commit.parityBindingIds]).size) + .toBe(12); + }); + + it("canonicalizes a shuffled complete DTO set into the fixed six-route order", async () => { + const fixture = runtime({ + firstLive: routeDtos().reverse(), + secondLive: routeDtos().slice(2).concat(routeDtos().slice(0, 2)), + indexed: routeDtos().slice(1).concat(routeDtos().slice(0, 1)), + }); + + await expect(runReconcilerPreParityCycle(fixture.input)).resolves.toMatchObject({ + status: "succeeded", + }); + expect(fixture.commitResult.mock.calls[0]![0].routeKeys).toEqual( + RECONCILER_ROUTE_KEYS, + ); + }); + + it("fails before commit when either provider omits or duplicates a route", async () => { + const omitted = runtime({ firstLive: routeDtos().slice(0, 5) }); + await expect(runReconcilerPreParityCycle(omitted.input)).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + expect(omitted.commitResult).not.toHaveBeenCalled(); + + const duplicate = routeDtos(); + duplicate[5] = { ...duplicate[4]! }; + const duplicated = runtime({ secondLive: duplicate }); + await expect(runReconcilerPreParityCycle(duplicated.input)).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + expect(duplicated.commitResult).not.toHaveBeenCalled(); + }); + + it("fails before commit when live providers disagree", async () => { + const changed = routeDtos(); + changed[2] = { + ...changed[2]!, + dto: { routeKey: "explore-chart", suffix: "provider-disagreement" }, + }; + const fixture = runtime({ secondLive: changed }); + + await expect(runReconcilerPreParityCycle(fixture.input)).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + expect(fixture.commitResult).not.toHaveBeenCalled(); + }); + + it("records an indexed mismatch as failed instead of activating false parity", async () => { + const changed = routeDtos(); + changed[4] = { + ...changed[4]!, + dto: { routeKey: "classic-v3-profile", suffix: "stale-index" }, + }; + const fixture = runtime({ indexed: changed }); + + await expect(runReconcilerPreParityCycle(fixture.input)).resolves.toMatchObject({ + status: "failed", + mismatchCount: 1, + }); + expect(fixture.commitResult).toHaveBeenCalledTimes(1); + const commit = fixture.commitResult.mock.calls[0]![0]; + expect(commit.legacyDtoHashes[4]).not.toBe(commit.indexedDtoHashes[4]); + }); + + it("rejects zero-work route claims", async () => { + const emptyClaim = routeDtos(); + emptyClaim[0] = { ...emptyClaim[0]!, comparedCount: 0 }; + const fixture = runtime({ indexed: emptyClaim }); + + await expect(runReconcilerPreParityCycle(fixture.input)).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + }); + expect(fixture.commitResult).not.toHaveBeenCalled(); + }); + + it("rejects a stale or substituted database contract before RPC work", async () => { + const fixture = runtime({ + contract: { ...contract, checkpointBlockHash: `0x${"99".repeat(32)}` }, + }); + + await expect(runReconcilerPreParityCycle(fixture.input)).rejects.toMatchObject({ + dependency: "postgres", + code: "validation_failed", + }); + expect(fixture.first.getBlock).not.toHaveBeenCalled(); + expect(fixture.commitResult).not.toHaveBeenCalled(); + }); + + it("rejects an RPC block that does not match the database checkpoint", async () => { + const first = provider({ + identity: "alchemy-mainnet-a", + vendorGroup: "alchemy", + endpointCommitment: ENDPOINT_A, + endpointOriginCommitment: ORIGIN_A, + blockHash: `0x${"98".repeat(32)}`, + }); + const second = provider({ + identity: "quicknode-mainnet-b", + vendorGroup: "quicknode", + endpointCommitment: ENDPOINT_B, + endpointOriginCommitment: ORIGIN_B, + }); + const fixture = runtime({ providers: [first.value, second.value] }); + + await expect(runReconcilerPreParityCycle(fixture.input)).rejects.toMatchObject({ + dependency: "rpc", + code: "validation_failed", + }); + expect(fixture.readLiveRoutes).not.toHaveBeenCalled(); + expect(fixture.commitResult).not.toHaveBeenCalled(); + }); + + it("does not commit after the overall deadline even if a reader resolves later", async () => { + const delayedRoutes = routeDtos(); + const reader: ReconcilerRouteDtoReader = { + readLiveRoutes: vi.fn( + () => + new Promise((resolve) => { + setTimeout(() => resolve(delayedRoutes), 150); + }), + ), + readIndexedRoutes: vi.fn( + () => + new Promise((resolve) => { + setTimeout(() => resolve(delayedRoutes), 150); + }), + ), + }; + const fixture = runtime({ reader }); + + await expect( + runReconcilerPreParityCycle({ ...fixture.input, deadlineMs: 100 }), + ).rejects.toMatchObject({ dependency: "rpc", code: "timeout" }); + await new Promise((resolve) => setTimeout(resolve, 80)); + expect(fixture.commitResult).not.toHaveBeenCalled(); + }); + + it("rejects provider pairs that are not independently identified", async () => { + const same = provider({ + identity: "same-mainnet", + vendorGroup: "same", + endpointCommitment: ENDPOINT_A, + endpointOriginCommitment: ORIGIN_A, + }); + const fixture = runtime({ providers: [same.value, same.value] }); + + await expect(runReconcilerPreParityCycle(fixture.input)).rejects.toMatchObject({ + dependency: "rpc", + code: "invalid_input", + }); + expect(fixture.readExactContract).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/data-pipeline/reconciler-route-sql-parity.test.ts b/tests/data-pipeline/reconciler-route-sql-parity.test.ts new file mode 100644 index 00000000..4d1f6340 --- /dev/null +++ b/tests/data-pipeline/reconciler-route-sql-parity.test.ts @@ -0,0 +1,229 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +import { PGlite } from "@electric-sql/pglite"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { canonicalizeFingerprintJson } from "../../lib/data-pipeline/canonical-fingerprint"; +import { + assembleReconcilerRoutesFromContributions, + CLASSIC_V3_RECONCILER_REWARD_ALLOCATION_FIELDS, + CLASSIC_V3_RECONCILER_REWARD_ENTITLEMENT_FIELDS, + CLASSIC_V3_RECONCILER_REWARD_FIELDS, + type ClassicV3ReconcilerRouteParts, +} from "../../lib/data-pipeline/classic-v3-reconciler-route-contract"; +import type { ReconcilerRouteDto } from "../../lib/data-pipeline/reconciler-preparity"; +import { + CLASSIC_V3_RECONCILER_ROUTE_FIXTURE_PARTS, + classicV3ReconcilerRouteFixture, +} from "./classic-v3-reconciler-route-fixture"; + +type RouteParts = Readonly<{ + tokens: readonly unknown[]; + charts: readonly unknown[]; + profiles: readonly unknown[]; + rewards: readonly unknown[]; + launches: readonly unknown[]; +}>; + +function routeCollection( + routes: readonly ReconcilerRouteDto[], + routeKey: ReconcilerRouteDto["routeKey"], + collectionKey: string, +): readonly unknown[] { + const route = routes.find((candidate) => candidate.routeKey === routeKey); + if (!route) return []; + return (route.dto as Record)[collectionKey] ?? []; +} + +function routeParts(routes: readonly ReconcilerRouteDto[]): RouteParts { + return { + tokens: routeCollection(routes, "explore-list", "tokens"), + charts: routeCollection(routes, "explore-chart", "charts"), + profiles: routeCollection(routes, "creator-profile", "profiles"), + rewards: routeCollection(routes, "classic-v3-profile", "rewards"), + launches: routeCollection(routes, "launch-lookup", "launches"), + }; +} + +describe("SQL and live route corpus contract", () => { + const database = new PGlite(); + + beforeAll(async () => { + await database.exec(` + create role programmable_migrator nologin; + create role programmable_reconciler nologin; + create role programmable_projector nologin; + create role programmable_api_reader nologin; + create role programmable_profile_binder nologin; + create role programmable_profile_recovery nologin; + create role programmable_profile_writer nologin; + create role programmable_maintenance nologin; + create role anon nologin; + create role authenticated nologin; + create role service_role nologin; + create schema programmable_private authorization programmable_migrator; + `); + await database.exec(await readFile(resolve( + "supabase/migrations/20260731225000_reconciler_route_corpus.sql", + ), "utf8")); + }, 30_000); + + afterAll(async () => database.close()); + + async function sqlRoutes( + parts: RouteParts, + ): Promise { + const result = await database.query<{ + route_key: ReconcilerRouteDto["routeKey"]; + compared_count: string | number; + dto: ReconcilerRouteDto["dto"]; + }>(` + select route_key, compared_count, dto + from programmable_private.assemble_reconciler_routes_v1( + $1::jsonb, $2::jsonb, $3::jsonb, $4::jsonb, $5::jsonb + ) + `, [ + JSON.stringify(parts.tokens), + JSON.stringify(parts.charts), + JSON.stringify(parts.profiles), + JSON.stringify(parts.rewards), + JSON.stringify(parts.launches), + ]); + return result.rows.map((row) => ({ + routeKey: row.route_key, + comparedCount: Number(row.compared_count), + dto: row.dto, + })); + } + + async function expectCanonicalParity( + liveRoutes: readonly ReconcilerRouteDto[], + parts: RouteParts = routeParts(liveRoutes), + ): Promise { + const indexedRoutes = await sqlRoutes(parts); + + expect(indexedRoutes.map((route) => + canonicalizeFingerprintJson(route) + )).toEqual(liveRoutes.map((route) => + canonicalizeFingerprintJson(route) + )); + } + + it("produces byte-identical Classic V3 DTOs for every route", async () => { + await expectCanonicalParity( + classicV3ReconcilerRouteFixture(), + CLASSIC_V3_RECONCILER_ROUTE_FIXTURE_PARTS, + ); + }); + + it("builds the SQL Classic V3 reward with the exact runtime field contract", async () => { + const expected = CLASSIC_V3_RECONCILER_ROUTE_FIXTURE_PARTS.rewards[0] as + Record; + const allocations = (expected.allocations as Array>) + .map((allocation) => ({ + ...allocation, + claimableWei: "legacy-field-must-not-escape", + claimedWei: "legacy-field-must-not-escape", + })); + const result = await database.query<{ reward: Record }>(` + select programmable_private.build_classic_v3_reconciler_reward_v1( + pg_catalog.decode($1::text, 'hex'), + pg_catalog.decode($2::text, 'hex'), + pg_catalog.decode($3::text, 'hex'), + $4::text, + $5::text, + pg_catalog.decode($6::text, 'hex'), + $7::integer, + $8::integer, + $9::integer, + pg_catalog.decode($10::text, 'hex'), + pg_catalog.decode($11::text, 'hex'), + $12::bigint, + $13::numeric, + $14::numeric, + $15::numeric, + $16::jsonb, + $17::jsonb, + $18::jsonb + ) as reward + `, [ + String(expected.vaultAddress).slice(2), + String(expected.poolId).slice(2), + String(expected.tokenAddress).slice(2), + expected.tokenName, + expected.tokenSymbol, + String(expected.launchTransactionHash).slice(2), + expected.buySwapFeeBps, + expected.sellSwapFeeBps, + expected.launcherFeeBps, + String(expected.configurationHash).slice(2), + String(expected.activeConfigurationHash).slice(2), + expected.configurationEpoch, + expected.totalCreatorFeesReceivedWei, + expected.totalCreatorFeesClaimedWei, + expected.pendingCreatorFeesWei, + JSON.stringify(allocations), + JSON.stringify(expected.entitlements), + JSON.stringify(expected.events), + ]); + const reward = result.rows[0]!.reward; + + expect(Object.keys(reward).sort()).toEqual( + [...CLASSIC_V3_RECONCILER_REWARD_FIELDS].sort(), + ); + expect(Object.keys( + (reward.allocations as Array>)[0]!, + ).sort()).toEqual( + [...CLASSIC_V3_RECONCILER_REWARD_ALLOCATION_FIELDS].sort(), + ); + expect(Object.keys( + (reward.entitlements as Array>)[0]!, + ).sort()).toEqual( + [...CLASSIC_V3_RECONCILER_REWARD_ENTITLEMENT_FIELDS].sort(), + ); + expect(canonicalizeFingerprintJson(reward as never)).toEqual( + canonicalizeFingerprintJson(expected as never), + ); + + const parts: ClassicV3ReconcilerRouteParts = { + ...CLASSIC_V3_RECONCILER_ROUTE_FIXTURE_PARTS, + rewards: [reward as never], + }; + await expectCanonicalParity(classicV3ReconcilerRouteFixture(), parts); + }); + + it.each([ + { releaseVersion: "classic-v2", modelId: "classic" }, + { releaseVersion: "stock-paired-v1", modelId: "stock-paired" }, + { releaseVersion: "stock-paired-v2", modelId: "stock-paired" }, + { releaseVersion: "stock-paired-v3", modelId: "stock-paired" }, + ] as const)( + "produces byte-identical $releaseVersion DTOs for every applicable route", + async ({ releaseVersion, modelId }) => { + const source = CLASSIC_V3_RECONCILER_ROUTE_FIXTURE_PARTS; + const token = structuredClone(source.tokens[0]!) as Record; + const chart = structuredClone(source.charts[0]!) as Record; + token.releaseVersion = releaseVersion; + token.modelId = modelId; + chart.releaseVersion = releaseVersion; + chart.modelId = modelId; + if (releaseVersion === "classic-v2") { + token.rewardVaultAddress = null; + } else { + const quoteAsset = `0x${"ab".repeat(20)}`; + token.quoteAssetAddress = quoteAsset; + chart.quoteAssetAddress = quoteAsset; + (chart.volume as Record).quoteAssetAddress = quoteAsset; + } + const liveRoutes = assembleReconcilerRoutesFromContributions([{ + tokens: [token] as never, + charts: [chart] as never, + }]); + + await expectCanonicalParity(liveRoutes); + }, + ); +}); diff --git a/tests/data-pipeline/release-probe-nonce.server.test.ts b/tests/data-pipeline/release-probe-nonce.server.test.ts new file mode 100644 index 00000000..d9c79907 --- /dev/null +++ b/tests/data-pipeline/release-probe-nonce.server.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { DataPipelineError } from "../../lib/data-pipeline/errors"; +import { + createReleaseProbeNonceConsumer, + type ReleaseProbeNonceInput, +} from "../../lib/data-pipeline/release-probe-nonce.server"; +import type { + PostgresExecutor, + PostgresParameter, + PostgresTransaction, +} from "../../lib/data-pipeline/postgres"; + +type RecordedQuery = Readonly<{ + text: string; + values: readonly PostgresParameter[]; +}>; + +class FakeExecutor implements PostgresExecutor { + readonly queries: RecordedQuery[] = []; + readonly close = vi.fn(async () => undefined); + + constructor( + private readonly responder: ( + text: string, + values: readonly PostgresParameter[], + ) => Promise[]>, + ) {} + + async transaction( + work: (transaction: PostgresTransaction) => Promise, + ): Promise { + return work({ + query: async >( + text: string, + values: readonly PostgresParameter[] = [], + ) => { + this.queries.push({ text, values }); + return (await this.responder(text, values)) as readonly Row[]; + }, + }); + } +} + +const issuedAt = new Date("2026-07-31T20:00:00.000Z"); +const expiresAt = new Date("2026-07-31T20:05:00.000Z"); +const candidate: ReleaseProbeNonceInput = { + route: "explore-token", + nonce: `${issuedAt.valueOf()}-${"ab".repeat(32)}-1`, + issuedAt, + expiresAt, +}; + +function identityRow() { + return { + session_user: "programmable_release_probe_nonce_login", + active_role: "programmable_release_probe_nonce", + }; +} + +describe("distributed release-probe nonce consumer", () => { + it("uses only the dedicated role and atomically consumes one SHA-256 digest", async () => { + const executor = new FakeExecutor(async (text) => { + if (/session_user::text/.test(text)) return [identityRow()]; + if (/consume_release_probe_nonce_v1/.test(text)) { + return [{ consumed: true }]; + } + return []; + }); + const consumer = createReleaseProbeNonceConsumer({ executor }); + + await expect(consumer.consume(candidate)).resolves.toBe(true); + + expect(executor.queries[0]?.text).toBe( + "set local role programmable_release_probe_nonce", + ); + const consume = executor.queries.find((query) => + query.text.includes("consume_release_probe_nonce_v1"), + ); + expect(consume?.values[0]).toBe("explore-token"); + expect(consume?.values[1]).toBeInstanceOf(Uint8Array); + expect((consume?.values[1] as Uint8Array).byteLength).toBe(32); + expect(consume?.values[2]).toEqual(issuedAt); + expect(consume?.values[3]).toEqual(expiresAt); + expect(consume?.text).not.toContain(candidate.nonce); + }); + + it("returns false when the database reports a globally consumed nonce", async () => { + const executor = new FakeExecutor(async (text) => { + if (/session_user::text/.test(text)) return [identityRow()]; + if (/consume_release_probe_nonce_v1/.test(text)) { + return [{ consumed: false }]; + } + return []; + }); + + await expect( + createReleaseProbeNonceConsumer({ executor }).consume(candidate), + ).resolves.toBe(false); + }); + + it("rejects the wrong session identity before calling the consume function", async () => { + const executor = new FakeExecutor(async (text) => + /session_user::text/.test(text) + ? [ + { + session_user: "programmable_api_reader_login", + active_role: "programmable_release_probe_nonce", + }, + ] + : [], + ); + + await expect( + createReleaseProbeNonceConsumer({ executor }).consume(candidate), + ).rejects.toBeInstanceOf(DataPipelineError); + expect( + executor.queries.some((query) => + query.text.includes("consume_release_probe_nonce_v1"), + ), + ).toBe(false); + }); + + it("rejects unsupported routes without opening a database transaction", async () => { + const executor = new FakeExecutor(async () => []); + await expect( + createReleaseProbeNonceConsumer({ executor }).consume({ + ...candidate, + route: "arbitrary-route", + }), + ).rejects.toBeInstanceOf(DataPipelineError); + expect(executor.queries).toEqual([]); + }); + + it("sanitizes database failures without serializing credentials or nonces", async () => { + const executor = new FakeExecutor(async () => { + throw new Error( + `postgres://probe:secret@example.invalid/db ${candidate.nonce}`, + ); + }); + + let failure: unknown; + try { + await createReleaseProbeNonceConsumer({ executor }).consume(candidate); + } catch (error) { + failure = error; + } + expect(failure).toBeInstanceOf(DataPipelineError); + expect(JSON.stringify(failure)).not.toContain("secret"); + expect(JSON.stringify(failure)).not.toContain(candidate.nonce); + }); +}); diff --git a/tests/data-pipeline/route-activation.server.test.ts b/tests/data-pipeline/route-activation.server.test.ts new file mode 100644 index 00000000..18039734 --- /dev/null +++ b/tests/data-pipeline/route-activation.server.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + indexedLaunchLookupEnabled, + indexedPublicIndexerFeedEnabled, +} from "../../lib/data-pipeline/route-activation.server"; + +describe("independent indexed route activation", () => { + it.each([ + { + explore: "false", + action: "false", + feed: "false", + expectedAction: false, + expectedFeed: false, + }, + { + explore: "true", + action: "false", + feed: "false", + expectedAction: false, + expectedFeed: false, + }, + { + explore: "false", + action: "true", + feed: "false", + expectedAction: true, + expectedFeed: false, + }, + { + explore: "true", + action: "false", + feed: "true", + expectedAction: false, + expectedFeed: true, + }, + { + explore: "false", + action: "true", + feed: "true", + expectedAction: true, + expectedFeed: true, + }, + ])( + "keeps Explore=$explore, actions=$action and feed=$feed independent", + ({ explore, action, feed, expectedAction, expectedFeed }) => { + const env = { + INDEXED_EXPLORE_LIST_READS_ENABLED: explore, + INDEXED_EXPLORE_TOKEN_READS_ENABLED: explore, + INDEXED_LAUNCH_LOOKUP_ENABLED: action, + INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED: feed, + }; + + expect(indexedLaunchLookupEnabled(env)).toBe(expectedAction); + expect(indexedPublicIndexerFeedEnabled(env)).toBe(expectedFeed); + }, + ); +}); diff --git a/tests/data-pipeline/route-adapters.test.ts b/tests/data-pipeline/route-adapters.test.ts new file mode 100644 index 00000000..ff9d506a --- /dev/null +++ b/tests/data-pipeline/route-adapters.test.ts @@ -0,0 +1,1169 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + IndexedRouteAdapterError, + adaptIndexedChartV2, + adaptIndexedClassicV3ProfileV2, + adaptIndexedCreatorProfileV2, + adaptIndexedExploreListV2, + adaptIndexedLaunchLookupV2, + adaptIndexedStockPairedProfileV2, + adaptIndexedTokenDetailV2, + assertSupportedIndexedReleaseV2, + indexedRouteCacheHeaders, + type IndexedExploreCursorV2, + type IndexedRouteEnvelopeV2, + type IndexedRouteKeyV2, + type IndexedRowSourceV2, + type IndexedSnapshotIdentityV2, + type IndexedTokenProjectionV2, +} from "../../lib/data-pipeline/route-adapters.server"; + +const TOKEN_A = "0x1111111111111111111111111111111111111111"; +const TOKEN_B = "0x2222222222222222222222222222222222222222"; +const TOKEN_C = "0x3333333333333333333333333333333333333333"; +const TOKEN_D = "0x4444444444444444444444444444444444444444"; +const TOKEN_E = "0x5555555555555555555555555555555555555555"; +const CREATOR = "0x6666666666666666666666666666666666666666"; +const OTHER = "0x7777777777777777777777777777777777777777"; +const HOOK = "0x8888888888888888888888888888888888888888"; +const VAULT = "0x9999999999999999999999999999999999999999"; +const QUOTE = "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa"; +const POSITION = "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"; +const POOL_ID = `0x${"aa".repeat(32)}` as const; +const LAUNCH_HASH = `0x${"bb".repeat(32)}` as const; +const TRANSACTION_HASH = `0x${"cc".repeat(32)}` as const; +const BLOCK_HASH = `0x${"dd".repeat(32)}` as const; +const SNAPSHOT_COMMITMENT = `0x${"ee".repeat(32)}` as const; +const PUBLICATION_COMMITMENT = `0x${"12".repeat(32)}` as const; + +type Release = + | "classic-v2" + | "classic-v3" + | "stock-paired-v1" + | "stock-paired-v2" + | "stock-paired-v3"; + +function modelFor(release: Release) { + return release.startsWith("stock-paired") + ? ("stock-paired" as const) + : ("classic" as const); +} + +function epochFor(release: Release) { + const suffix = { + "classic-v2": "0001", + "classic-v3": "0002", + "stock-paired-v1": "0003", + "stock-paired-v2": "0004", + "stock-paired-v3": "0005", + }[release]; + return `10000000-0000-4000-8000-00000000${suffix}`; +} + +const ALL_RELEASES = [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const; + +function pointer(release: Release, routeKey: IndexedRouteKeyV2) { + const index = ALL_RELEASES.indexOf(release); + return { + routeKey, + chainId: 1 as const, + releaseVersion: release, + modelVersion: modelFor(release), + sourceGroup: `source-${release}`, + projectorVersion: "public-route-projector-v2", + epochId: epochFor(release), + pointerGeneration: String(index + 1), + checkpointId: `20000000-0000-4000-8000-00000000000${index + 1}`, + checkpointGeneration: "7", + reorgGeneration: "0", + checkpointBlockNumber: "25660000", + checkpointBlockHash: BLOCK_HASH, + }; +} + +function snapshotFor( + routeKey: IndexedRouteKeyV2, + releases: readonly Release[] = ALL_RELEASES, +): IndexedSnapshotIdentityV2 { + return { + adapterVersion: "indexed-route-adapters-v2", + snapshotCommitment: SNAPSHOT_COMMITMENT, + chainId: 1, + blockNumber: "25660000", + blockHash: BLOCK_HASH, + confirmations: 12, + capturedAt: "2026-07-31T10:00:00.000Z", + releasePointers: releases.map((release) => pointer(release, routeKey)), + ethUsdQuote: { + feedAddress: QUOTE, + roundId: "200", + answer: "350000000000", + decimals: 8, + updatedAt: "2026-07-31T09:59:00.000Z", + }, + }; +} + +const snapshot = snapshotFor("explore-list"); + +function source( + release: Release, + routeKey: IndexedRouteKeyV2, + blockNumber = "25650000", +): IndexedRowSourceV2 { + return { + ...pointer(release, routeKey), + snapshotCommitment: SNAPSHOT_COMMITMENT, + projectionRunId: `30000000-0000-4000-8000-00000000000${ALL_RELEASES.indexOf(release) + 1}`, + publicationCommitment: PUBLICATION_COMMITMENT, + promotedBlockNumber: blockNumber, + promotedBlockHash: BLOCK_HASH, + }; +} + +function token( + release: Release, + tokenAddress: `0x${string}`, + routeKey: IndexedRouteKeyV2, + blockNumber = "25650000", +): IndexedTokenProjectionV2 { + const stock = release.startsWith("stock-paired"); + return { + source: source(release, routeKey, blockNumber), + tokenAddress, + hookAddress: HOOK, + poolId: POOL_ID, + creatorAddress: CREATOR, + positionRecipient: POSITION, + positionTokenId: "42", + rewardVaultAddress: release === "classic-v2" ? null : VAULT, + launchHash: LAUNCH_HASH, + launchBlockNumber: blockNumber, + launchTransactionHash: TRANSACTION_HASH, + launchTransactionIndex: 3, + launchLogIndex: 9, + launchedAt: "2026-07-31T08:00:00.000Z", + name: `Token ${release}`, + symbol: release === "classic-v2" ? "C2" : release.toUpperCase(), + decimals: 18, + totalSupplyRaw: "1000000000000000000000000000", + metadata: { + revision: "2", + createdAt: "2026-07-31T08:00:10.000Z", + description: " A verified indexed token ", + imageUrl: "https://programmable.family/token.png", + links: [ + { + kind: "website", + url: "https://programmable.family/", + displayOrder: 0, + }, + { + kind: "x", + url: "https://x.com/0xProgrammable", + displayOrder: 1, + }, + ], + extraData: "0x", + }, + liquidity: { + tokenLiquidityAmountRaw: "900000000000000000000000000", + lockedTokenDustRaw: "1", + currentTick: 120, + initialTick: 100, + tickLower: -887200, + tickUpper: 887200, + activeLiquidity: "123456789012345678901234567890", + }, + fees: stock + ? { + totalSwapFeeBps: 100, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + protocolFeePips: 0, + } + : { + totalSwapFeeBps: release === "classic-v3" ? 300 : 200, + buySwapFeeBps: release === "classic-v3" ? 300 : 200, + sellSwapFeeBps: release === "classic-v3" ? 100 : 200, + buyCreatorFeeBps: release === "classic-v3" ? 290 : 190, + sellCreatorFeeBps: release === "classic-v3" ? 90 : 190, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + protocolFeePips: 0, + }, + market: { + tokenPriceNativeWei: stock ? null : "2500000000000", + marketCapNativeWei: stock ? null : "2500000000000000000", + indexedMarketCapNativeWei: stock ? null : "2600000000000000000", + indexedMarketCapUsdWad: stock + ? "1200000000000000000000000" + : "9100000000000000000000", + indexedValuationBlockNumber: "25659999", + fdvUsdWad: stock ? "1200000000000000000000000" : null, + grossVolumeNativeWei: stock ? null : "1234500000000000000", + creatorFeesGeneratedNativeWei: stock ? null : "100000000000000000", + launcherFeesGeneratedNativeWei: stock ? null : "10000000000000000", + creatorFeesAccruedNativeWei: stock ? null : "40000000000000000", + swapCount: 123, + }, + quote: stock + ? { + address: QUOTE, + symbol: "SPYON", + name: "Tokenized S&P 500", + decimals: 18, + isCurrency0: true, + tokenPriceQuoteWad: "2500000000000000000", + marketCapQuoteWad: "2500000000000000000000000", + grossVolumeQuoteRaw: "700000000000000000000", + creatorFeesGeneratedQuoteRaw: "6300000000000000000", + programmableFeesGeneratedQuoteRaw: "700000000000000000", + creatorFeesAccruedQuoteRaw: "1100000000000000000", + } + : null, + initialBuy: stock + ? { + nativeWei: "600000000000000", + quoteRaw: "1500000000000000000", + tokenRaw: "240000000000000000000000", + } + : { + nativeWei: "600000000000000", + quoteRaw: null, + tokenRaw: "240000000000000000000000", + }, + uniswapV4Pool: { + source: "official-uniswap-v4-subgraph", + indexedBlockNumber: "25659999", + indexedBlockHash: BLOCK_HASH, + volumeUsdWad: "4321000000000000000000", + tvlUsdWad: "9876000000000000000000", + transactionCount: "123", + liquidity: "456", + sqrtPriceX96: "79228162514264337593543950336", + tick: 120, + feeTierPips: "0", + }, + }; +} + +function ready(data: T): IndexedRouteEnvelopeV2 { + const value = data as Record; + let routeKey: IndexedRouteKeyV2; + let releases: readonly Release[] = ALL_RELEASES; + if ("request" in value) { + routeKey = "explore-list"; + } else if ("range" in value) { + routeKey = "explore-chart"; + } else if ("address" in value && !("surface" in value)) { + routeKey = "explore-token"; + } else if ("surface" in value) { + routeKey = "launch-lookup"; + releases = value.surface === "classic-v3" + ? ["classic-v3"] + : ["stock-paired-v1", "stock-paired-v2", "stock-paired-v3"]; + } else if ("tokens" in value) { + routeKey = "creator-profile"; + } else { + const firstReward = (value.rewards as { source?: IndexedRowSourceV2 }[] | undefined)?.[0]; + routeKey = firstReward?.source?.routeKey ?? "classic-v3-profile"; + releases = routeKey === "classic-v3-profile" + ? ["classic-v3"] + : ["stock-paired-v1", "stock-paired-v2", "stock-paired-v3"]; + } + return { status: "ready", snapshot: snapshotFor(routeKey, releases), data }; +} + +function expectAdapterError( + operation: () => unknown, + code: IndexedRouteAdapterError["code"], +) { + try { + operation(); + throw new Error("expected adapter failure"); + } catch (error) { + expect(error).toBeInstanceOf(IndexedRouteAdapterError); + expect((error as IndexedRouteAdapterError).code).toBe(code); + } +} + +describe("indexed route adapter v2 release boundary", () => { + it.each([ + ["classic-v2", "classic", "classic", undefined], + ["classic-v3", "classic", "classic", "classic-v3"], + ["stock-paired-v1", "stock-paired", "stock-paired", "stock-paired-v1"], + ["stock-paired-v2", "stock-paired", "stock-paired", "stock-paired-v2"], + ["stock-paired-v3", "stock-paired", "stock-paired", "stock-paired-v3"], + ] as const)( + "maps %s only with its exact indexed model", + (releaseVersion, modelVersion, launchModel, launchModelVersion) => { + expect( + assertSupportedIndexedReleaseV2({ releaseVersion, modelVersion }), + ).toEqual({ + releaseVersion, + modelVersion, + launchModel, + ...(launchModelVersion ? { launchModelVersion } : {}), + }); + }, + ); + + it.each([ + ["classic-v3", "stock-paired"], + ["stock-paired-v2", "classic"], + ["deep-full-range-v3", "deep"], + ["unresolved", "unresolved"], + ])("rejects unsupported or relabelled source %s/%s", (releaseVersion, modelVersion) => { + expectAdapterError( + () => assertSupportedIndexedReleaseV2({ releaseVersion, modelVersion }), + "unsupported-release", + ); + }); + + it("rejects checkpoint, projector and reorg evidence that is not snapshot-bound", () => { + const wrongCheckpoint = snapshotFor("explore-token"); + wrongCheckpoint.releasePointers[0]!.checkpointBlockHash = + `0x${"01".repeat(32)}`; + expectAdapterError( + () => + adaptIndexedTokenDetailV2({ + status: "ready", + snapshot: wrongCheckpoint, + data: { address: TOKEN_A, token: null }, + }), + "snapshot-mismatch", + ); + + const wrongGeneration = token( + "classic-v3", + TOKEN_A, + "explore-token", + ); + wrongGeneration.source.reorgGeneration = "1"; + expectAdapterError( + () => + adaptIndexedTokenDetailV2( + ready({ address: TOKEN_A, token: wrongGeneration }), + ), + "snapshot-mismatch", + ); + }); +}); + +describe("indexed route cache policy", () => { + it("uses short shared cache windows only for public immutable snapshots", () => { + expect(indexedRouteCacheHeaders("explore-list")).toEqual({ + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }); + expect(indexedRouteCacheHeaders("token-detail")).toEqual({ + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }); + expect(indexedRouteCacheHeaders("token-chart")).toEqual({ + "Cache-Control": + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + }); + expect(indexedRouteCacheHeaders("creator-profile")).toEqual({ + "Cache-Control": "private, max-age=0, s-maxage=15", + }); + expect(indexedRouteCacheHeaders("launch-lookup")).toEqual({ + "Cache-Control": "no-store", + }); + expect(indexedRouteCacheHeaders("token-detail", "not-found")).toEqual({ + "Cache-Control": "no-store", + }); + }); +}); + +describe("indexed Explore list adapter v2", () => { + it("preserves the public page shape for all supported release generations", () => { + const tokens = [ + token("stock-paired-v3", TOKEN_E, "explore-list", "25650005"), + token("stock-paired-v2", TOKEN_D, "explore-list", "25650004"), + token("stock-paired-v1", TOKEN_C, "explore-list", "25650003"), + token("classic-v3", TOKEN_B, "explore-list", "25650002"), + token("classic-v2", TOKEN_A, "explore-list", "25650001"), + ]; + const endAt: IndexedExploreCursorV2 = { + adapterVersion: "indexed-route-adapters-v2", + snapshotCommitment: SNAPSHOT_COMMITMENT, + normalizedQuery: "", + sort: "newest", + pageSize: 5, + valuationUnit: null, + position: { + marketCapAtomic: null, + launchBlockNumber: "25650001", + launchTransactionIndex: 3, + launchLogIndex: 9, + launchTransactionHash: TRANSACTION_HASH, + tokenAddress: TOKEN_A, + }, + }; + + const response = adaptIndexedExploreListV2( + ready({ + request: { + query: "", + sort: "newest", + requestedPage: 1, + pageSize: 5, + }, + page: { + resolvedPage: 1, + totalCount: "5", + valuationUnit: null, + startAfter: null, + endAt, + }, + launcherFeesAccruedWei: "123456789012345678901234567890", + tokens, + }), + ); + + expect(response).toMatchObject({ + status: "ready", + page: 1, + pageSize: 5, + total: 5, + totalPages: 1, + sort: "newest", + query: "", + snapshot: { + chainId: 1, + blockNumber: snapshot.blockNumber, + blockHash: snapshot.blockHash, + confirmations: 12, + }, + launcherFeesAccruedWei: "123456789012345678901234567890", + launcherFeesAccruedEth: "123456789012.34567890123456789", + }); + expect(response.tokens.map((entry) => entry.launchModelVersion)).toEqual([ + "stock-paired-v3", + "stock-paired-v2", + "stock-paired-v1", + "classic-v3", + undefined, + ]); + expect(response.tokens[0]).toMatchObject({ + id: `1:${TOKEN_E}`, + description: "A verified indexed token", + imageUrl: "https://programmable.family/token.png", + links: [ + { kind: "website", url: "https://programmable.family/" }, + { kind: "x", url: "https://x.com/0xProgrammable" }, + ], + totalSupply: "1000000000", + totalSupplyRaw: "1000000000000000000000000000", + quoteAssetAddress: QUOTE, + grossVolumeQuote: "700", + grossVolumeQuoteRaw: "700000000000000000000", + marketCapQuote: "2500000", + marketCapQuoteWad: "2500000000000000000000000", + launchModel: "stock-paired", + liquidityPath: "meme", + }); + expect(response.tokens[3]).toMatchObject({ + totalSwapFeeBps: 300, + buyHookFeeBps: 300, + sellHookFeeBps: 100, + buyCreatorFeeBps: 290, + sellCreatorFeeBps: 90, + launcherFeeBps: 10, + launchModel: "classic", + launchModelVersion: "classic-v3", + }); + expect(JSON.stringify(response)).not.toContain("pointerGeneration"); + expect(JSON.stringify(response)).not.toContain("snapshotCommitment"); + expect(() => JSON.stringify(response)).not.toThrow(); + }); + + it("binds a later page to the complete market-cap cursor and immutable snapshot", () => { + const first = token("classic-v3", TOKEN_B, "explore-list", "25650002"); + first.market.indexedMarketCapUsdWad = "900"; + const second = token("classic-v2", TOKEN_A, "explore-list", "25650001"); + second.market.indexedMarketCapUsdWad = "800"; + const startAfter: IndexedExploreCursorV2 = { + adapterVersion: "indexed-route-adapters-v2", + snapshotCommitment: SNAPSHOT_COMMITMENT, + normalizedQuery: "token", + sort: "market-cap", + pageSize: 2, + valuationUnit: "usd-wad", + position: { + marketCapAtomic: "1000", + launchBlockNumber: "25650003", + launchTransactionIndex: 3, + launchLogIndex: 9, + launchTransactionHash: TRANSACTION_HASH, + tokenAddress: TOKEN_C, + }, + }; + const endAt: IndexedExploreCursorV2 = { + ...startAfter, + position: { + marketCapAtomic: "800", + launchBlockNumber: "25650001", + launchTransactionIndex: 3, + launchLogIndex: 9, + launchTransactionHash: TRANSACTION_HASH, + tokenAddress: TOKEN_A, + }, + }; + + const response = adaptIndexedExploreListV2( + ready({ + request: { + query: "$token", + sort: "market-cap", + requestedPage: 2, + pageSize: 2, + }, + page: { + resolvedPage: 2, + totalCount: "4", + valuationUnit: "usd-wad", + startAfter, + endAt, + }, + launcherFeesAccruedWei: "0", + tokens: [first, second], + }), + ); + + expect(response.page).toBe(2); + expect(response.tokens.map((entry) => entry.tokenAddress)).toEqual([ + TOKEN_B, + TOKEN_A, + ]); + + expectAdapterError( + () => + adaptIndexedExploreListV2( + ready({ + request: { + query: "$token", + sort: "market-cap", + requestedPage: 2, + pageSize: 2, + }, + page: { + resolvedPage: 2, + totalCount: "4", + valuationUnit: "usd-wad", + startAfter: { + ...startAfter, + snapshotCommitment: `0x${"ff".repeat(32)}`, + }, + endAt, + }, + launcherFeesAccruedWei: "0", + tokens: [first, second], + }), + ), + "cursor-mismatch", + ); + }); + + it("rejects rows from another route or unstable page order instead of partial output", () => { + const first = token("classic-v3", TOKEN_A, "explore-list", "25650001"); + const later = token("classic-v2", TOKEN_B, "explore-list", "25650002"); + const badRoute = token("classic-v2", TOKEN_B, "creator-profile", "25650000"); + const page = { + request: { query: "", sort: "newest" as const, requestedPage: 1, pageSize: 2 }, + page: { + resolvedPage: 1, + totalCount: "2", + valuationUnit: null, + startAfter: null, + endAt: { + adapterVersion: "indexed-route-adapters-v2" as const, + snapshotCommitment: SNAPSHOT_COMMITMENT, + normalizedQuery: "", + sort: "newest" as const, + pageSize: 2, + valuationUnit: null, + position: { + marketCapAtomic: null, + launchBlockNumber: "25650002", + launchTransactionIndex: 3, + launchLogIndex: 9, + launchTransactionHash: TRANSACTION_HASH, + tokenAddress: TOKEN_B, + }, + }, + }, + launcherFeesAccruedWei: "0", + }; + + expectAdapterError( + () => + adaptIndexedExploreListV2( + ready({ + ...page, + page: { + ...page.page, + endAt: page.page.endAt as IndexedExploreCursorV2, + }, + tokens: [first, later], + }), + ), + "cursor-mismatch", + ); + expectAdapterError( + () => + adaptIndexedExploreListV2( + ready({ + ...page, + page: { + ...page.page, + endAt: page.page.endAt as IndexedExploreCursorV2, + }, + tokens: [later, badRoute], + }), + ), + "scope-mismatch", + ); + }); +}); + +describe("indexed token-detail adapter v2", () => { + it("returns exactly the existing ready token-detail body", () => { + const projection = token("classic-v3", TOKEN_A, "explore-token"); + expect( + adaptIndexedTokenDetailV2( + ready({ address: TOKEN_A, token: projection }), + ), + ).toEqual({ + status: "ready", + token: expect.objectContaining({ + tokenAddress: TOKEN_A, + name: "Token classic-v3", + launchModelVersion: "classic-v3", + }), + snapshot: { + chainId: 1, + blockNumber: snapshot.blockNumber, + blockHash: snapshot.blockHash, + confirmations: 12, + ethUsdQuote: snapshot.ethUsdQuote, + }, + }); + }); + + it("keeps a verified empty lookup distinct from a not-ready index", () => { + expect( + adaptIndexedTokenDetailV2(ready({ address: TOKEN_A, token: null })), + ).toEqual({ + status: "ready", + token: null, + snapshot: expect.objectContaining({ blockNumber: snapshot.blockNumber }), + }); + expectAdapterError( + () => + adaptIndexedTokenDetailV2({ + status: "not-ready", + reason: "projection-lag", + }), + "not-ready", + ); + }); +}); + +describe("indexed chart adapter v2", () => { + it("maps exact atomic native and USD values without Number conversion", () => { + const response = adaptIndexedChartV2( + ready({ + address: TOKEN_A, + range: "1d", + source: source("classic-v3", "explore-chart"), + poolId: POOL_ID, + points: [ + { + blockNumber: "25650001", + priceNativeWei: "123456789012345678901234567890", + priceUsdWad: "432109876543210987654321098765", + }, + { + blockNumber: "25650002", + priceNativeWei: "223456789012345678901234567890", + priceUsdWad: null, + }, + ], + swapCount: "9007199254740991", + volumeNativeWei: "987654321098765432109876543210", + volumeUsdWad: "345678901234567890123456789012", + }), + ); + + expect(response).toEqual({ + status: "ready", + address: TOKEN_A, + points: [ + { + blockNumber: "25650001", + priceEth: "123456789012.34567890123456789", + priceUsd: "432109876543.210987654321098765", + }, + { + blockNumber: "25650002", + priceEth: "223456789012.34567890123456789", + }, + ], + swapCount: 9_007_199_254_740_991, + volumeWei: "987654321098765432109876543210", + volumeEth: "987654321098.76543210987654321", + volumeUsdWad: "345678901234567890123456789012", + range: "1d", + snapshotBlock: snapshot.blockNumber, + }); + }); + + it.each(["stock-paired-v1", "stock-paired-v2", "stock-paired-v3"] as const)( + "preserves the current insufficient-history behavior for %s", + (release) => { + expect( + adaptIndexedChartV2( + ready({ + address: TOKEN_A, + range: "all", + source: source(release, "explore-chart"), + poolId: POOL_ID, + points: [], + swapCount: "0", + volumeNativeWei: "0", + volumeUsdWad: null, + }), + ), + ).toEqual({ + status: "insufficient-history", + address: TOKEN_A, + points: [], + swapCount: 0, + volumeWei: "0", + volumeEth: "0", + range: "all", + snapshotBlock: snapshot.blockNumber, + }); + }, + ); + + it("rejects an unsafe count instead of losing precision", () => { + expectAdapterError( + () => + adaptIndexedChartV2( + ready({ + address: TOKEN_A, + range: "all", + source: source("classic-v2", "explore-chart"), + poolId: POOL_ID, + points: [], + swapCount: "9007199254740992", + volumeNativeWei: "0", + volumeUsdWad: null, + }), + ), + "precision-loss", + ); + }); +}); + +describe("indexed creator-profile adapter v2", () => { + it("keeps Stock launches in tokens, excludes them from native pools, and sums bigint totals", () => { + const classicV2 = token("classic-v2", TOKEN_A, "creator-profile", "25650001"); + const classicV3 = token("classic-v3", TOKEN_B, "creator-profile", "25650002"); + const stock = token("stock-paired-v3", TOKEN_C, "creator-profile", "25650003"); + classicV2.market.creatorFeesAccruedNativeWei = "100000000000000000000000000000"; + classicV2.market.creatorFeesGeneratedNativeWei = "300000000000000000000000000000"; + classicV3.market.creatorFeesAccruedNativeWei = "200000000000000000000000000000"; + classicV3.market.creatorFeesGeneratedNativeWei = "400000000000000000000000000000"; + + const response = adaptIndexedCreatorProfileV2( + ready({ + account: CREATOR, + tokens: [classicV2, classicV3, stock], + claims: [ + { + source: source("classic-v2", "creator-profile", "25650010"), + poolId: POOL_ID, + tokenAddress: TOKEN_A, + creatorAddress: CREATOR, + recipientAddress: CREATOR, + callerAddress: OTHER, + amountWei: "90000000000000000000000000000", + blockNumber: "25650010", + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logIndex: 4, + claimedAt: "2026-07-31T09:00:00.000Z", + }, + ], + }), + ); + + expect(response.tokens).toHaveLength(3); + expect(response.pools).toHaveLength(2); + expect(response.pools.map((entry) => entry.launchModel)).toEqual([ + "classic", + "classic", + ]); + expect(response.totals).toEqual({ + claimableWei: "300000000000000000000000000000", + claimableEth: "300000000000", + generatedWei: "700000000000000000000000000000", + generatedEth: "700000000000", + claimedWei: "90000000000000000000000000000", + claimedEth: "90000000000", + }); + expect(response.claims[0]).toMatchObject({ + amountWei: "90000000000000000000000000000", + amountEth: "90000000000", + creatorAddress: CREATOR, + }); + }); + + it("rejects a profile row scoped to another creator", () => { + const foreign = token("classic-v2", TOKEN_A, "creator-profile"); + foreign.creatorAddress = OTHER; + expectAdapterError( + () => + adaptIndexedCreatorProfileV2( + ready({ account: CREATOR, tokens: [foreign], claims: [] }), + ), + "scope-mismatch", + ); + }); +}); + +describe("indexed Classic V3 profile adapter v2", () => { + it("preserves the existing rewards DTO and exact ETH formatting", () => { + const response = adaptIndexedClassicV3ProfileV2( + ready({ + account: CREATOR, + chainId: 1, + rewards: [ + { + source: source("classic-v3", "classic-v3-profile"), + tokenAddress: TOKEN_A, + tokenName: "Classic reward", + tokenSymbol: "CRW", + poolId: POOL_ID, + vaultAddress: VAULT, + claimableWei: "123456789012345678901234567890", + claimedWei: "98765432109876543210987654321", + buySwapFeeBps: 300, + sellSwapFeeBps: 100, + platformFeeBps: 10, + allocations: [ + { + allocationIndex: 0, + beneficiary: CREATOR, + payoutAddress: CREATOR, + shareBps: 6000, + }, + { + allocationIndex: 1, + beneficiary: OTHER, + payoutAddress: OTHER, + shareBps: 4000, + }, + ], + launchTransactionHash: TRANSACTION_HASH, + }, + ], + }), + ); + + expect(response).toEqual({ + status: "ready", + account: CREATOR, + chainId: 1, + rewards: [ + { + tokenAddress: TOKEN_A, + tokenName: "Classic reward", + tokenSymbol: "CRW", + poolId: POOL_ID, + vaultAddress: VAULT, + beneficiary: CREATOR, + payoutAddress: CREATOR, + shareBps: 6000, + ownedAllocations: [ + { + allocationIndex: 0, + beneficiary: CREATOR, + payoutAddress: CREATOR, + shareBps: 6000, + }, + ], + claimableWei: "123456789012345678901234567890", + claimableEth: "123456789012.34567890123456789", + claimedWei: "98765432109876543210987654321", + claimedEth: "98765432109.876543210987654321", + buySwapFeeBps: 300, + sellSwapFeeBps: 100, + platformFeeBps: 10, + beneficiaries: [ + { + allocationIndex: 0, + beneficiary: CREATOR, + payoutAddress: CREATOR, + shareBps: 6000, + }, + { + allocationIndex: 1, + beneficiary: OTHER, + payoutAddress: OTHER, + shareBps: 4000, + }, + ], + launchTransactionHash: TRANSACTION_HASH, + }, + ], + }); + }); + + it("fails closed on payout semantics the current client cannot represent", () => { + expectAdapterError( + () => + adaptIndexedClassicV3ProfileV2( + ready({ + account: CREATOR, + chainId: 1, + rewards: [ + { + source: source("classic-v3", "classic-v3-profile"), + tokenAddress: TOKEN_A, + tokenName: "Classic reward", + tokenSymbol: "CRW", + poolId: POOL_ID, + vaultAddress: VAULT, + claimableWei: "1", + claimedWei: "0", + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + platformFeeBps: 10, + allocations: [ + { + allocationIndex: 0, + beneficiary: OTHER, + payoutAddress: CREATOR, + shareBps: 10_000, + }, + ], + launchTransactionHash: TRANSACTION_HASH, + }, + ], + }), + ), + "not-ready", + ); + }); +}); + +describe("indexed Stock-Paired profile adapter v2", () => { + it("preserves beneficiary rewards and quote-denominated estimates", () => { + const response = adaptIndexedStockPairedProfileV2( + ready({ + account: CREATOR, + chainId: 1, + rewards: [ + { + source: source("stock-paired-v3", "creator-profile"), + tokenAddress: TOKEN_A, + tokenName: "Stock reward", + tokenSymbol: "STK", + imageUrl: "https://programmable.family/token.png", + hookAddress: HOOK, + poolId: POOL_ID, + vaultAddress: VAULT, + quoteAsset: QUOTE, + quoteAssetSymbol: "SPYON", + beneficiary: CREATOR, + payoutAddress: CREATOR, + shareBps: 6000, + claimableRaw: "1000000000000000000", + claimedRaw: "2000000000000000000", + generatedRaw: "3000000000000000000", + creatorFeesPendingRaw: "4000000000000000000", + beneficiaries: [ + { + beneficiary: CREATOR, + payoutAddress: CREATOR, + shareBps: 6000, + }, + { + beneficiary: OTHER, + payoutAddress: OTHER, + shareBps: 4000, + }, + ], + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + programmableFeeBps: 10, + launchTransactionHash: TRANSACTION_HASH, + estimate: { + ethRaw: "5000000000000000000", + usdRaw: "17500000000", + }, + }, + ], + }), + ); + + expect(response).toEqual({ + status: "ready", + account: CREATOR, + chainId: 1, + snapshotBlock: "25660000", + rewards: [ + expect.objectContaining({ + model: "stock-paired", + tokenAddress: TOKEN_A, + poolId: POOL_ID, + beneficiary: CREATOR, + shareBps: 6000, + claimable: "1", + claimed: "2", + generated: "3", + estimatedEth: "5", + estimatedUsd: "17500", + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + programmableFeeBps: 10, + }), + ], + }); + }); +}); + +describe("indexed launch-lookup adapter v2", () => { + it("preserves the current compact Classic V3 launch body", () => { + expect( + adaptIndexedLaunchLookupV2( + ready({ + surface: "classic-v3", + account: CREATOR, + transactionHash: TRANSACTION_HASH, + resolution: "found", + token: token("classic-v3", TOKEN_A, "launch-lookup"), + }), + ), + ).toEqual({ + status: "ready", + launch: { + tokenAddress: TOKEN_A, + name: "Token classic-v3", + symbol: "CLASSIC-V3", + launchTransactionHash: TRANSACTION_HASH, + }, + }); + }); + + it.each(["stock-paired-v1", "stock-paired-v2", "stock-paired-v3"] as const)( + "preserves the current Stock-Paired launch body for %s", + (release) => { + expect( + adaptIndexedLaunchLookupV2( + ready({ + surface: "stock-paired", + account: CREATOR, + transactionHash: TRANSACTION_HASH, + resolution: "found", + token: token(release, TOKEN_A, "launch-lookup"), + }), + ), + ).toEqual({ + status: "ready", + launch: { + tokenAddress: TOKEN_A, + name: `Token ${release}`, + symbol: release.toUpperCase(), + quoteAsset: QUOTE, + poolId: POOL_ID, + rewardVault: VAULT, + positionRecipient: POSITION, + positionTokenId: "42", + creator: CREATOR, + initialBuyEthAmount: "600000000000000", + initialBuyQuoteAmount: "1500000000000000000", + initialBuyTokenAmount: "240000000000000000000000", + transactionHash: TRANSACTION_HASH, + }, + }); + }, + ); + + it("keeps pending Stock indexing distinct from a verified empty Classic lookup", () => { + expect( + adaptIndexedLaunchLookupV2( + ready({ + surface: "stock-paired", + account: CREATOR, + transactionHash: TRANSACTION_HASH, + resolution: "pending", + token: null, + }), + ), + ).toEqual({ status: "pending", launch: null }); + expect( + adaptIndexedLaunchLookupV2( + ready({ + surface: "classic-v3", + account: CREATOR, + transactionHash: TRANSACTION_HASH, + resolution: "not-found", + token: null, + }), + ), + ).toEqual({ status: "ready", launch: null }); + }); + + it("rejects account, transaction, or release relabelling", () => { + const foreign = token("stock-paired-v3", TOKEN_A, "launch-lookup"); + foreign.creatorAddress = OTHER; + expectAdapterError( + () => + adaptIndexedLaunchLookupV2( + ready({ + surface: "stock-paired", + account: CREATOR, + transactionHash: TRANSACTION_HASH, + resolution: "found", + token: foreign, + }), + ), + "scope-mismatch", + ); + expectAdapterError( + () => + adaptIndexedLaunchLookupV2( + ready({ + surface: "classic-v3", + account: CREATOR, + transactionHash: TRANSACTION_HASH, + resolution: "found", + token: token("classic-v2", TOKEN_A, "launch-lookup"), + }), + ), + "snapshot-mismatch", + ); + }); +}); diff --git a/tests/data-pipeline/route-coordinator.server.test.ts b/tests/data-pipeline/route-coordinator.server.test.ts new file mode 100644 index 00000000..7b220818 --- /dev/null +++ b/tests/data-pipeline/route-coordinator.server.test.ts @@ -0,0 +1,1288 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const readModelMocks = vi.hoisted(() => ({ + getServerReadModel: vi.fn(), + repeatableReadSnapshot: vi.fn(), + transactionQuery: vi.fn(), +})); + +const nonceConsumerMocks = vi.hoisted(() => { + const consumed = new Set(); + return { + consumed, + consumeReleaseProbeNonce: vi.fn(async (input: { route: string; nonce: string }) => { + const key = `${input.route}:${input.nonce}`; + if (consumed.has(key)) return false; + consumed.add(key); + return true; + }), + }; +}); + +vi.mock("../../lib/data-pipeline/read-model.server", () => ({ + getServerReadModel: readModelMocks.getServerReadModel, +})); + +vi.mock("../../lib/data-pipeline/release-probe-nonce.server", () => ({ + consumeReleaseProbeNonce: nonceConsumerMocks.consumeReleaseProbeNonce, +})); + +import { DataPipelineError } from "../../lib/data-pipeline/errors"; +import { + ALL_REVIEWED_ROUTE_SCOPES, + authorizeRouteReleaseProbe, + canonicalizeRouteResponse, + compareRouteResponses, + coordinateRouteRead, + hashCanonicalRouteResponse, + signRouteReleaseProbe, + validatedRecordScopeEvidence, + type CoordinatedRouteRead, + type IndexedRouteResult, + type IndexedProjectionVersion, + type ReviewedRouteScope, + type RouteReadiness, + type RouteScopeProjectionVersion, +} from "../../lib/data-pipeline/route-coordinator.server"; + +const BLOCK_HASH = `0x${"aB".repeat(32)}`; +const OTHER_BLOCK_HASH = `0x${"cd".repeat(32)}`; +const ADDRESS = `0x${"aB".repeat(20)}`; +const TRANSACTION_HASH = `0x${"Cd".repeat(32)}`; +const RELEASE_PROBE_TOKEN = "p".repeat(48); +let releaseProbeSequence = 0; +const CLASSIC_SCOPE = [ + { model: "classic" as const, releaseVersion: "classic-v3" as const }, +] as const; +const PROJECTION_VERSION = { + checkpointId: "00000000-0000-4000-8000-000000000001", + blockNumber: "100", + blockHash: BLOCK_HASH.toLowerCase(), + sourceGroup: "envio-primary", + projectorVersion: "read-model-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "3", + checkpointGeneration: "7", + reorgGeneration: "1", +} as const; + +const ROUTE_FLAGS = [ + "INDEXED_EXPLORE_LIST_READS_ENABLED", + "INDEXED_EXPLORE_TOKEN_READS_ENABLED", + "INDEXED_EXPLORE_CHART_READS_ENABLED", + "INDEXED_CREATOR_PROFILE_READS_ENABLED", + "INDEXED_CLASSIC_V3_PROFILE_READS_ENABLED", + "INDEXED_LAUNCH_LOOKUP_ENABLED", +] as const; + +function clearCoordinatorEnvironment() { + for (const name of ROUTE_FLAGS) vi.stubEnv(name, "false"); + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "false"); + vi.stubEnv("INDEXED_READ_REQUIRE_PARITY_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_LIVE_FALLBACK_ENABLED", "true"); + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", ""); +} + +function releaseProbeHeaders( + nonce: string, + token = RELEASE_PROBE_TOKEN, + marker = "1", + route: "explore-token" | "explore-list" = "explore-token", +): Headers { + return new Headers({ + "x-programmable-shadow-probe": marker, + "x-programmable-shadow-probe-signature": signRouteReleaseProbe({ + route, + nonce, + secret: token, + }), + }); +} + +function releaseProbeNonce(issuedAt = Date.now()) { + releaseProbeSequence += 1; + return `${issuedAt}-${releaseProbeSequence.toString(16).padStart(64, "0")}-${releaseProbeSequence}`; +} + +async function authorizedTestReleaseProbe( + nonce = releaseProbeNonce(), + route: "explore-token" | "explore-list" = "explore-token", +) { + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", RELEASE_PROBE_TOKEN); + return authorizeRouteReleaseProbe( + releaseProbeHeaders(nonce, RELEASE_PROBE_TOKEN, "1", route), + nonce, + route, + ); +} + +function jsonResponse(body: unknown, init: ResponseInit = {}): Response { + const headers = new Headers(init.headers); + headers.set("Content-Type", "application/json; charset=utf-8"); + return new Response(JSON.stringify(body), { ...init, headers }); +} + +function legacyResult(response: Response, source: "rpc" | "blob" = "rpc") { + return { + response, + source, + checkpoint: { blockNumber: "100", blockHash: BLOCK_HASH }, + } as const; +} + +function projectionVersion( + index: number, + overrides: Partial = {}, +): IndexedProjectionVersion { + const suffix = String(index + 1).padStart(12, "0"); + const byte = (index + 1).toString(16).padStart(2, "0"); + return { + ...PROJECTION_VERSION, + checkpointId: `00000000-0000-4000-8000-${suffix}`, + epochId: `10000000-0000-4000-8000-${suffix}`, + blockNumber: String(100 + index), + blockHash: `0x${byte.repeat(32)}`, + sourceGroup: `envio-primary-${index + 1}`, + projectorVersion: `read-model-v${index + 1}`, + pointerGeneration: String(3 + index), + checkpointGeneration: String(7 + index), + reorgGeneration: String(index), + ...overrides, + }; +} + +function scopedVersions( + scope: readonly ReviewedRouteScope[], + versions: readonly IndexedProjectionVersion[] = scope.map((_, index) => + projectionVersion(index), + ), +): readonly RouteScopeProjectionVersion[] { + return scope.map((member, index) => ({ + ...member, + version: versions[index]!, + })); +} + +function scopeEvidence(scopes: readonly ReviewedRouteScope[]) { + return validatedRecordScopeEvidence( + scopes.map((scope) => ({ scope })), + (record) => record.scope, + ); +} + +function indexedResult( + response: Response, + options: { + scope?: readonly ReviewedRouteScope[]; + recordScopes?: readonly ReviewedRouteScope[]; + versions?: readonly RouteScopeProjectionVersion[]; + comparisonCheckpoint?: { blockNumber: string; blockHash: string }; + } = {}, +) { + const scope = options.scope ?? CLASSIC_SCOPE; + const versions = + options.versions ?? scopedVersions(scope, [PROJECTION_VERSION]); + return { + response, + source: "indexed" as const, + scope, + scopeEvidence: scopeEvidence(options.recordScopes ?? [scope[0]!]), + versions, + comparisonCheckpoint: options.comparisonCheckpoint ?? versions[0]!.version, + projectionLag: 0, + reconciledAt: "2026-07-31T08:00:00.000Z", + }; +} + +function baseInput(overrides: Record = {}) { + const input = { + route: "explore-token" as const, + scope: CLASSIC_SCOPE, + legacy: vi.fn(async () => legacyResult(jsonResponse({ ok: true }))), + readiness: vi.fn(async () => [ + { + ...CLASSIC_SCOPE[0], + eligibility: "eligible" as const, + parity: "current" as const, + version: PROJECTION_VERSION, + }, + ]), + comparisonSchema: { + addressFields: ["address"], + hashFields: ["transactionHash"], + integerFields: ["amount"], + }, + indexed: vi.fn(async () => indexedResult(jsonResponse({ ok: true }))), + ...overrides, + }; + const readiness = input.readiness as ( + readModel: Parameters[0], + ) => Promise; + const indexed = input.indexed as ( + readModel: Parameters[0], + ) => Promise; + const snapshotOverride = overrides.indexedSnapshot; + const indexedSnapshot = + typeof snapshotOverride === "function" + ? (snapshotOverride as CoordinatedRouteRead["indexedSnapshot"]) + : vi.fn(async (readModel) => { + const snapshotReadiness = await readiness(readModel); + const scopeLength = Array.isArray(input.scope) + ? input.scope.length + : 0; + const mayServe = + snapshotReadiness.length === scopeLength && + snapshotReadiness.every( + (member) => + member.eligibility === "eligible" && + member.parity === "current" && + member.version !== undefined, + ); + return { + readiness: snapshotReadiness, + ...(mayServe ? { indexed: await indexed(readModel) } : {}), + }; + }); + return { ...input, indexedSnapshot }; +} + +describe("release-probe authorization", () => { + beforeEach(() => { + nonceConsumerMocks.consumed.clear(); + nonceConsumerMocks.consumeReleaseProbeNonce.mockClear(); + }); + + it("creates a non-serializable capability only for the exact probe headers", async () => { + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", RELEASE_PROBE_TOKEN); + const nonce = releaseProbeNonce(); + const authorized = await authorizeRouteReleaseProbe( + releaseProbeHeaders(nonce), + nonce, + "explore-token", + ); + expect(authorized).not.toBeNull(); + expect(JSON.stringify(authorized)).toBe("{}"); + expect( + await authorizeRouteReleaseProbe( + releaseProbeHeaders(releaseProbeNonce(), "x".repeat(48)), + releaseProbeNonce(), + "explore-token", + ), + ).toBeNull(); + expect( + await authorizeRouteReleaseProbe( + new Headers(), + releaseProbeNonce(), + "explore-token", + ), + ).toBeNull(); + const wrongMarkerNonce = releaseProbeNonce(); + expect( + await authorizeRouteReleaseProbe( + releaseProbeHeaders( + wrongMarkerNonce, + RELEASE_PROBE_TOKEN, + "0", + ), + wrongMarkerNonce, + "explore-token", + ), + ).toBeNull(); + }); + + it("fails closed for an unsafe expected probe secret", async () => { + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", "short"); + await expect( + authorizeRouteReleaseProbe( + new Headers({ + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-signature": "a".repeat(64), + }), + releaseProbeNonce(), + "explore-token", + ), + ).rejects.toBeInstanceOf(DataPipelineError); + }); + + it("rejects stale and globally reused nonces before creating another capability", async () => { + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", RELEASE_PROBE_TOKEN); + const nonce = releaseProbeNonce(); + expect( + await authorizeRouteReleaseProbe( + releaseProbeHeaders(nonce), + nonce, + "explore-token", + ), + ).not.toBeNull(); + expect( + await authorizeRouteReleaseProbe( + releaseProbeHeaders(nonce), + nonce, + "explore-token", + ), + ).toBeNull(); + const staleNonce = releaseProbeNonce( + Date.now() - 5 * 60 * 1_000 - 1, + ); + expect( + await authorizeRouteReleaseProbe( + releaseProbeHeaders(staleNonce), + staleNonce, + "explore-token", + ), + ).toBeNull(); + }); + + it("binds signatures and capabilities to one route and rejects bearer-token auth", async () => { + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", RELEASE_PROBE_TOKEN); + const wrongRouteNonce = releaseProbeNonce(); + expect( + await authorizeRouteReleaseProbe( + releaseProbeHeaders(wrongRouteNonce), + wrongRouteNonce, + "explore-list", + ), + ).toBeNull(); + + const bearerNonce = releaseProbeNonce(); + expect( + await authorizeRouteReleaseProbe( + new Headers({ + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-token": RELEASE_PROBE_TOKEN, + "x-programmable-shadow-probe-signature": signRouteReleaseProbe({ + route: "explore-token", + nonce: bearerNonce, + secret: RELEASE_PROBE_TOKEN, + }), + }), + bearerNonce, + "explore-token", + ), + ).toBeNull(); + + const capability = await authorizedTestReleaseProbe(); + if (!capability) throw new Error("expected release probe"); + await expect( + coordinateRouteRead( + baseInput({ + route: "explore-list", + scope: ALL_REVIEWED_ROUTE_SCOPES, + releaseProbe: capability, + }), + ), + ).rejects.toBeInstanceOf(DataPipelineError); + }); +}); + +describe("canonical route response comparison", () => { + it("sorts keys, omits undefined object fields, canonicalizes integer strings, and normalizes only addresses and hashes", () => { + const canonical = canonicalizeRouteResponse( + { + z: undefined, + words: "AbC", + nested: { + transactionHash: TRANSACTION_HASH, + amount: "00042", + address: ADDRESS, + }, + array: ["02", "01"], + }, + { + addressFields: ["address"], + hashFields: ["transactionHash"], + integerFields: ["amount", "array"], + }, + ); + + expect(canonical).toBe( + `{"array":["2","1"],"nested":{"address":"${ADDRESS.toLowerCase()}","amount":"42","transactionHash":"${TRANSACTION_HASH.toLowerCase()}"},"words":"AbC"}`, + ); + expect(hashCanonicalRouteResponse({ a: 1, b: 2 })).toMatch( + /^0x[0-9a-f]{64}$/, + ); + expect(hashCanonicalRouteResponse({ a: 1, b: 2 })).toBe( + hashCanonicalRouteResponse({ b: 2, a: 1 }), + ); + }); + + it("preserves array order and caps sanitized mismatch paths", () => { + const left: Record = { + array: ["first", "second"], + "password=do-not-leak": "left-secret", + }; + const right: Record = { + array: ["second", "first"], + "password=do-not-leak": "right-secret", + }; + for (let index = 0; index < 20; index += 1) { + left[`field${index}`] = index; + right[`field${index}`] = index + 100; + } + + const outcome = compareRouteResponses(left, right); + expect(outcome.kind).toBe("mismatch"); + if (outcome.kind !== "mismatch") throw new Error("expected mismatch"); + expect(outcome.mismatchPaths).toHaveLength(8); + expect(outcome.mismatchPaths).toContain("$.array[0]"); + expect(JSON.stringify(outcome)).not.toContain("do-not-leak"); + expect(JSON.stringify(outcome)).not.toContain("left-secret"); + expect(JSON.stringify(outcome)).not.toContain("right-secret"); + }); + + it("preserves user strings by default and retains hostile object keys", () => { + expect(compareRouteResponses({ ticker: "01" }, { ticker: "1" }).kind).toBe( + "mismatch", + ); + expect( + compareRouteResponses( + { amount: "01" }, + { amount: "1" }, + { integerFields: ["amount"] }, + ).kind, + ).toBe("match"); + + const hostile = JSON.parse('{"__proto__":{"different":true}}'); + expect(canonicalizeRouteResponse(hostile)).toBe( + '{"__proto__":{"different":true}}', + ); + expect(compareRouteResponses(hostile, {}).kind).toBe("mismatch"); + }); +}); + +describe("route shadow and fallback coordinator", () => { + beforeEach(() => { + vi.unstubAllEnvs(); + clearCoordinatorEnvironment(); + readModelMocks.getServerReadModel.mockReset(); + readModelMocks.repeatableReadSnapshot.mockReset(); + readModelMocks.transactionQuery.mockReset(); + readModelMocks.repeatableReadSnapshot.mockImplementation( + async ( + work: (transaction: { + query: typeof readModelMocks.transactionQuery; + }) => Promise, + ) => work({ query: readModelMocks.transactionQuery }), + ); + readModelMocks.getServerReadModel.mockResolvedValue({ + health: vi.fn(), + repeatableReadSnapshot: readModelMocks.repeatableReadSnapshot, + }); + }); + + it("returns the exact legacy Response and does no database work while the route flag is off", async () => { + const legacyResponse = jsonResponse( + { exact: "legacy" }, + { status: 206, headers: { "X-Legacy": "exact" } }, + ); + const input = baseInput({ + legacy: vi.fn(async () => legacyResult(legacyResponse)), + }); + + const result = await coordinateRouteRead(input); + expect(result).toBe(legacyResponse); + expect(result.status).toBe(206); + expect(result.headers.get("X-Legacy")).toBe("exact"); + await expect(result.text()).resolves.toBe('{"exact":"legacy"}'); + expect(readModelMocks.getServerReadModel).not.toHaveBeenCalled(); + expect(readModelMocks.repeatableReadSnapshot).not.toHaveBeenCalled(); + expect(input.readiness).not.toHaveBeenCalled(); + expect(input.indexed).not.toHaveBeenCalled(); + }); + + it("measures a disabled route only for a private authenticated probe", async () => { + const releaseProbe = await authorizedTestReleaseProbe(); + if (!releaseProbe) throw new Error("expected release probe"); + const input = baseInput({ + releaseProbe, + legacy: vi.fn(async () => + legacyResult( + jsonResponse( + { exact: "legacy" }, + { + headers: { + "X-Vercel-Cache": "HIT", + "Vercel-CDN-Cache-Control": "s-maxage=600", + }, + }, + ), + ), + ), + }); + + const result = await coordinateRouteRead(input); + + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + expect(result.headers.get("X-Vercel-Cache")).toBeNull(); + expect(result.headers.get("Vercel-CDN-Cache-Control")).toBeNull(); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("x-programmable-shadow-overhead-ms")).toMatch( + /^\d+$/, + ); + expect(result.headers.get("x-programmable-shadow-parity")).toBe( + "mismatch", + ); + expect(result.headers.get("x-programmable-live-fallback")).toBe("false"); + expect(readModelMocks.getServerReadModel).toHaveBeenCalledWith({ + required: true, + }); + expect(readModelMocks.repeatableReadSnapshot).toHaveBeenCalledTimes(1); + }); + + it("runs both paths in shadow mode, records a normalized match, and returns legacy byte-for-byte", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "true"); + const legacyResponse = jsonResponse( + { address: ADDRESS, amount: "0009", word: "KeepCase" }, + { status: 202, headers: { "X-Legacy": "untouched" } }, + ); + const recordComparison = vi.fn(async () => undefined); + const backgroundTasks: Array<() => Promise> = []; + const input = baseInput({ + legacy: vi.fn(async () => legacyResult(legacyResponse)), + indexed: vi.fn(async () => + indexedResult( + jsonResponse( + { + word: "KeepCase", + amount: "9", + address: ADDRESS.toLowerCase(), + }, + { status: 202 }, + ), + ), + ), + recordComparison, + scheduleShadowComparison: vi.fn((task: () => Promise) => { + backgroundTasks.push(task); + }), + }); + + const result = await coordinateRouteRead(input); + expect(result).toBe(legacyResponse); + expect(result.headers.get("X-Legacy")).toBe("untouched"); + expect(result.headers.get("X-Programmable-Read-Source")).toBeNull(); + expect(result.headers.get("x-programmable-shadow-overhead-ms")).toBeNull(); + expect(result.headers.get("x-programmable-shadow-parity")).toBeNull(); + expect(result.headers.get("x-programmable-live-fallback")).toBeNull(); + expect(input.readiness).not.toHaveBeenCalled(); + expect(input.indexed).not.toHaveBeenCalled(); + expect(recordComparison).not.toHaveBeenCalled(); + expect(backgroundTasks).toHaveLength(1); + + await backgroundTasks[0]!(); + expect(recordComparison).toHaveBeenCalledWith( + expect.objectContaining({ + kind: "match", + route: "explore-token", + scope: CLASSIC_SCOPE, + legacyHash: expect.stringMatching(/^0x[0-9a-f]{64}$/), + indexedHash: expect.stringMatching(/^0x[0-9a-f]{64}$/), + }), + ); + }); + + it("runs a synchronous authorized shadow probe and reports measured parity without using the scheduler", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "true"); + const releaseProbe = await authorizedTestReleaseProbe(); + if (!releaseProbe) throw new Error("expected release probe"); + const legacyResponse = jsonResponse( + { address: ADDRESS, amount: "09" }, + { status: 202, headers: { "X-Legacy": "preserved" } }, + ); + const input = baseInput({ + releaseProbe, + legacy: vi.fn(async () => legacyResult(legacyResponse)), + indexed: vi.fn(async () => + indexedResult( + jsonResponse( + { amount: "9", address: ADDRESS.toLowerCase() }, + { status: 202 }, + ), + ), + ), + }); + + const result = await coordinateRouteRead(input); + expect(result).not.toBe(legacyResponse); + expect(result.status).toBe(202); + expect(result.headers.get("X-Legacy")).toBe("preserved"); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + expect(result.headers.get("x-programmable-shadow-overhead-ms")).toMatch( + /^\d+$/, + ); + expect(result.headers.get("x-programmable-shadow-parity")).toBe("match"); + expect(result.headers.get("x-programmable-live-fallback")).toBe("false"); + await expect(result.json()).resolves.toEqual({ + address: ADDRESS, + amount: "09", + }); + expect(readModelMocks.repeatableReadSnapshot).toHaveBeenCalledTimes(1); + expect(input.readiness).toHaveBeenCalledTimes(1); + expect(input.indexed).toHaveBeenCalledTimes(1); + }); + + it("reports incomparable when an authorized shadow probe cannot establish parity", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "true"); + const releaseProbe = await authorizedTestReleaseProbe(); + if (!releaseProbe) throw new Error("expected release probe"); + const input = baseInput({ + releaseProbe, + indexed: vi.fn(async () => ({ + ...indexedResult(jsonResponse({ ok: false })), + versions: scopedVersions(CLASSIC_SCOPE, [ + { ...PROJECTION_VERSION, blockHash: OTHER_BLOCK_HASH }, + ]), + comparisonCheckpoint: { + blockNumber: PROJECTION_VERSION.blockNumber, + blockHash: OTHER_BLOCK_HASH, + }, + })), + }); + + const result = await coordinateRouteRead(input); + expect(result.headers.get("x-programmable-shadow-overhead-ms")).toMatch( + /^\d+$/, + ); + expect(result.headers.get("x-programmable-shadow-parity")).toBe( + "incomparable", + ); + }); + + it("marks different checkpoints incomparable without exposing response bodies", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "true"); + const secret = "never-log-this-body"; + const recordComparison = vi.fn(async () => undefined); + const backgroundTasks: Array<() => Promise> = []; + const input = baseInput({ + legacy: vi.fn(async () => legacyResult(jsonResponse({ secret }))), + indexed: vi.fn(async () => ({ + ...indexedResult(jsonResponse({ secret: "different" })), + versions: scopedVersions(CLASSIC_SCOPE, [ + { + ...PROJECTION_VERSION, + blockHash: OTHER_BLOCK_HASH, + }, + ]), + comparisonCheckpoint: { + blockNumber: PROJECTION_VERSION.blockNumber, + blockHash: OTHER_BLOCK_HASH, + }, + })), + recordComparison, + scheduleShadowComparison: vi.fn((task: () => Promise) => { + backgroundTasks.push(task); + }), + }); + + const result = await coordinateRouteRead(input); + expect(result.status).toBe(200); + expect(input.readiness).not.toHaveBeenCalled(); + expect(input.indexed).not.toHaveBeenCalled(); + + await backgroundTasks[0]!(); + const event = (recordComparison.mock.calls as unknown[][])[0]?.[0]; + expect(event).toMatchObject({ + kind: "incomparable", + reason: "checkpoint-mismatch", + }); + expect(JSON.stringify(event)).not.toContain(secret); + expect(JSON.stringify(event)).not.toContain("different"); + }); + + it("contains synchronous recorder failures inside the background task", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "true"); + const legacyResponse = jsonResponse({ ok: true }); + const backgroundTasks: Array<() => Promise> = []; + const input = baseInput({ + legacy: vi.fn(async () => legacyResult(legacyResponse)), + recordComparison: vi.fn(() => { + throw new Error("telemetry unavailable"); + }), + scheduleShadowComparison: vi.fn((task: () => Promise) => { + backgroundTasks.push(task); + }), + }); + + const result = await coordinateRouteRead(input); + expect(result).toBe(legacyResponse); + await expect(backgroundTasks[0]!()).resolves.toBeUndefined(); + }); + + it("rejects shadow activation without a platform background scheduler", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_SHADOW_COMPARE_ENABLED", "true"); + const input = baseInput(); + + await expect(coordinateRouteRead(input)).rejects.toBeInstanceOf( + DataPipelineError, + ); + expect(input.legacy).not.toHaveBeenCalled(); + expect(input.readiness).not.toHaveBeenCalled(); + expect(input.indexed).not.toHaveBeenCalled(); + }); + + it("serves indexed data only after explicit model eligibility and current parity", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const input = baseInput(); + + const result = await coordinateRouteRead(input); + expect(readModelMocks.repeatableReadSnapshot).toHaveBeenCalledTimes(1); + expect(input.readiness).toHaveBeenCalledTimes(1); + expect(input.indexed).toHaveBeenCalledTimes(1); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("indexed"); + expect(result.headers.get("X-Programmable-Projection-Block")).toBe("100"); + expect(result.headers.get("X-Programmable-Projection-Hash")).toBe( + BLOCK_HASH.toLowerCase(), + ); + expect(result.headers.get("X-Programmable-Release-Version")).toBe( + "classic-v3", + ); + expect(result.headers.get("x-programmable-live-fallback")).toBeNull(); + }); + + it("reports indexed serving and fallback only to an authorized live probe", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const releaseProbe = await authorizedTestReleaseProbe(); + if (!releaseProbe) throw new Error("expected release probe"); + + const indexedInput = baseInput({ releaseProbe }); + const indexedResponse = await coordinateRouteRead(indexedInput); + expect(indexedResponse.headers.get("x-programmable-live-fallback")).toBe( + "false", + ); + expect(indexedResponse.headers.get("Cache-Control")).toBe( + "private, no-store", + ); + expect( + indexedResponse.headers.get("x-programmable-shadow-overhead-ms"), + ).toMatch(/^\d+$/); + expect(indexedResponse.headers.get("x-programmable-shadow-parity")).toBe( + "match", + ); + + const fallbackProbe = await authorizedTestReleaseProbe(); + if (!fallbackProbe) throw new Error("expected fallback release probe"); + const fallbackInput = baseInput({ + releaseProbe: fallbackProbe, + readiness: vi.fn(async () => [ + { + ...CLASSIC_SCOPE[0], + eligibility: "eligible" as const, + parity: "stale" as const, + }, + ]), + }); + const fallbackResponse = await coordinateRouteRead(fallbackInput); + expect(fallbackResponse.headers.get("x-programmable-live-fallback")).toBe( + "true", + ); + expect(fallbackResponse.headers.get("Cache-Control")).toBe( + "private, no-store", + ); + + vi.stubEnv("INDEXED_READ_LIVE_FALLBACK_ENABLED", "false"); + const unavailableProbe = await authorizedTestReleaseProbe(); + if (!unavailableProbe) { + throw new Error("expected unavailable release probe"); + } + const unavailableResponse = await coordinateRouteRead( + baseInput({ + releaseProbe: unavailableProbe, + readiness: vi.fn(async () => [ + { + ...CLASSIC_SCOPE[0], + eligibility: "eligible" as const, + parity: "stale" as const, + }, + ]), + }), + ); + expect(unavailableResponse.status).toBe(503); + expect( + unavailableResponse.headers.get("x-programmable-live-fallback"), + ).toBe("false"); + expect( + unavailableResponse.headers.get("x-programmable-shadow-parity"), + ).toBe("incomparable"); + expect( + unavailableResponse.headers.get("x-programmable-shadow-overhead-ms"), + ).toMatch(/^\d+$/); + }); + + it("keeps the indexed response selected when a live probe detects a mismatch", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const releaseProbe = await authorizedTestReleaseProbe(); + if (!releaseProbe) throw new Error("expected release probe"); + const input = baseInput({ + releaseProbe, + legacy: vi.fn(async () => + legacyResult(jsonResponse({ source: "legacy" }, { status: 202 })), + ), + indexed: vi.fn(async () => + indexedResult(jsonResponse({ source: "indexed" }, { status: 201 })), + ), + }); + + const result = await coordinateRouteRead(input); + + expect(result.status).toBe(201); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("indexed"); + expect(result.headers.get("x-programmable-shadow-parity")).toBe("mismatch"); + expect(result.headers.get("x-programmable-live-fallback")).toBe("false"); + await expect(result.json()).resolves.toEqual({ source: "indexed" }); + }); + + it("keeps the indexed response selected and reports incomparable when live comparison fails", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const releaseProbe = await authorizedTestReleaseProbe(); + if (!releaseProbe) throw new Error("expected release probe"); + const input = baseInput({ + releaseProbe, + legacy: vi.fn(async () => { + throw new Error("legacy provider unavailable"); + }), + indexed: vi.fn(async () => + indexedResult(jsonResponse({ source: "indexed" }, { status: 201 })), + ), + }); + + const result = await coordinateRouteRead(input); + + expect(result.status).toBe(201); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("indexed"); + expect(result.headers.get("x-programmable-shadow-overhead-ms")).toMatch( + /^\d+$/, + ); + expect(result.headers.get("x-programmable-shadow-parity")).toBe( + "incomparable", + ); + expect(result.headers.get("x-programmable-live-fallback")).toBe("false"); + await expect(result.json()).resolves.toEqual({ source: "indexed" }); + }); + + it("rejects forged probe capabilities and upstream probe headers", async () => { + const forged = baseInput({ + releaseProbe: Object.freeze({}), + }); + await expect( + coordinateRouteRead( + forged as unknown as Parameters[0], + ), + ).rejects.toBeInstanceOf(DataPipelineError); + expect(forged.legacy).not.toHaveBeenCalled(); + + const upstream = baseInput({ + legacy: vi.fn(async () => + legacyResult( + jsonResponse( + { ok: true }, + { + headers: { + "x-programmable-live-fallback": "false", + }, + }, + ), + ), + ), + }); + await expect(coordinateRouteRead(upstream)).rejects.toThrow( + "Route response is not comparable", + ); + }); + + it("does not serve when the atomic snapshot observes a same-checkpoint parity flip", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const stalePreflight = vi.fn(async () => [ + { + ...CLASSIC_SCOPE[0], + eligibility: "eligible" as const, + parity: "current" as const, + version: PROJECTION_VERSION, + }, + ]); + const indexedSnapshot = vi.fn(async () => ({ + readiness: [ + { + ...CLASSIC_SCOPE[0], + eligibility: "eligible" as const, + parity: "mismatch" as const, + version: PROJECTION_VERSION, + }, + ], + indexed: indexedResult(jsonResponse({ ok: true })), + })); + const input = baseInput({ + readiness: stalePreflight, + indexedSnapshot, + }); + + const result = await coordinateRouteRead(input); + expect(indexedSnapshot).toHaveBeenCalledTimes(1); + expect(stalePreflight).not.toHaveBeenCalled(); + expect(input.indexed).not.toHaveBeenCalled(); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + }); + + it.each([ + { eligibility: "ineligible", parity: "current" }, + { eligibility: "eligible", parity: "pending" }, + { eligibility: "eligible", parity: "stale" }, + { eligibility: "eligible", parity: "mismatch" }, + { eligibility: "eligible", parity: "missing" }, + ] as const)( + "falls back with no-store when readiness is $eligibility/$parity", + async (readiness) => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const legacyResponse = jsonResponse( + { legacy: true }, + { + headers: { + "X-Programmable-Read-Source": "indexed", + "X-Programmable-Projection-Block": "999", + "X-Programmable-Projection-Hash": OTHER_BLOCK_HASH, + "Vercel-CDN-Cache-Control": "public, max-age=3600", + "CDN-Cache-Control": "public, max-age=3600", + "Surrogate-Control": "max-age=3600", + }, + }, + ); + const input = baseInput({ + readiness: vi.fn(async () => [ + { + ...CLASSIC_SCOPE[0], + ...readiness, + ...(readiness.parity === "current" + ? { + version: PROJECTION_VERSION, + } + : {}), + }, + ]), + legacy: vi.fn(async () => legacyResult(legacyResponse, "rpc")), + }); + + const result = await coordinateRouteRead(input); + expect(input.indexed).not.toHaveBeenCalled(); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("X-Programmable-Projection-Block")).toBeNull(); + expect(result.headers.get("X-Programmable-Projection-Hash")).toBeNull(); + expect(result.headers.get("Vercel-CDN-Cache-Control")).toBeNull(); + expect(result.headers.get("CDN-Cache-Control")).toBeNull(); + expect(result.headers.get("Surrogate-Control")).toBeNull(); + await expect(result.json()).resolves.toEqual({ legacy: true }); + }, + ); + + it("returns a generic no-store 503 without calling legacy when fallback is disabled", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_READ_LIVE_FALLBACK_ENABLED", "false"); + const input = baseInput({ + readiness: vi.fn(async () => [ + { + ...CLASSIC_SCOPE[0], + eligibility: "eligible" as const, + parity: "stale" as const, + }, + ]), + }); + + const result = await coordinateRouteRead(input); + expect(result.status).toBe(503); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + expect(input.legacy).not.toHaveBeenCalled(); + expect(await result.json()).toEqual({ + error: "read_temporarily_unavailable", + }); + }); + + it("falls back after an indexed dependency failure and returns 503 if fallback also fails", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const first = baseInput({ + indexed: vi.fn(async () => { + throw new Error("database unavailable"); + }), + }); + const fallback = await coordinateRouteRead(first); + expect(fallback.headers.get("Cache-Control")).toBe("private, no-store"); + expect(fallback.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + + const second = baseInput({ + indexed: vi.fn(async () => { + throw new Error("database unavailable"); + }), + legacy: vi.fn(async () => { + throw new Error("rpc unavailable"); + }), + }); + const unavailable = await coordinateRouteRead(second); + expect(unavailable.status).toBe(503); + expect(unavailable.headers.get("Cache-Control")).toBe("private, no-store"); + }); + + it("never promotes Deep, unknown models, or an incompatible release", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const cases = [ + baseInput({ + scope: [{ model: "deep", releaseVersion: "deep-v3" }], + }), + baseInput({ + scope: [{ model: "unknown", releaseVersion: "unknown-v1" }], + }), + baseInput({ + route: "classic-v3-profile", + scope: [{ model: "classic", releaseVersion: "classic-v2" }], + }), + ]; + + for (const input of cases) { + await expect( + coordinateRouteRead(input as Parameters[0]), + ).rejects.toBeInstanceOf(DataPipelineError); + expect(input.legacy).not.toHaveBeenCalled(); + expect(input.indexed).not.toHaveBeenCalled(); + } + }); + + it("requires every member of an aggregate allowlisted scope to be current", async () => { + vi.stubEnv("INDEXED_EXPLORE_LIST_READS_ENABLED", "true"); + const scope = ALL_REVIEWED_ROUTE_SCOPES; + const versions = scopedVersions(scope); + const currentReadiness = scope.map((member, index) => ({ + ...member, + eligibility: "eligible" as const, + parity: "current" as const, + version: versions[index]!.version, + })); + const indexed = vi.fn(async () => + indexedResult(jsonResponse({ aggregate: true }), { + scope, + recordScopes: scope, + versions, + }), + ); + const current = baseInput({ + route: "explore-list", + scope, + readiness: vi.fn(async () => currentReadiness), + indexed, + }); + + const result = await coordinateRouteRead(current); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("indexed"); + expect(indexed).toHaveBeenCalledTimes(1); + + const missing = baseInput({ + route: "explore-list", + scope, + readiness: vi.fn(async () => currentReadiness.slice(0, -1)), + }); + const fallback = await coordinateRouteRead(missing); + expect(fallback.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(missing.indexed).not.toHaveBeenCalled(); + + const staleReadiness = currentReadiness.map((member, index) => + index === 2 + ? { + ...member, + parity: "stale" as const, + version: undefined, + } + : member, + ); + const stale = baseInput({ + route: "explore-list", + scope, + readiness: vi.fn(async () => staleReadiness), + }); + const staleFallback = await coordinateRouteRead(stale); + expect(staleFallback.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(stale.indexed).not.toHaveBeenCalled(); + }); + + it("discovers a token across reviewed releases and permits zero or one returned scope", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const scope = ALL_REVIEWED_ROUTE_SCOPES; + const versions = scopedVersions(scope); + const readiness = scope.map((member, index) => ({ + ...member, + eligibility: "eligible" as const, + parity: "current" as const, + version: versions[index]!.version, + })); + + const found = baseInput({ + route: "explore-token", + scope, + readiness: vi.fn(async () => readiness), + indexed: vi.fn(async () => + indexedResult(jsonResponse({ token: { address: ADDRESS } }), { + scope, + recordScopes: [scope[3]!], + versions, + }), + ), + }); + const foundResponse = await coordinateRouteRead(found); + expect(foundResponse.headers.get("X-Programmable-Read-Source")).toBe( + "indexed", + ); + expect( + foundResponse.headers.get("X-Programmable-Projection-Block"), + ).toBeNull(); + + const missing = baseInput({ + route: "explore-token", + scope, + readiness: vi.fn(async () => readiness), + indexed: vi.fn(async () => + indexedResult(jsonResponse({ token: null }), { + scope, + recordScopes: [], + versions, + }), + ), + }); + const missingResponse = await coordinateRouteRead(missing); + expect(missingResponse.headers.get("X-Programmable-Read-Source")).toBe( + "indexed", + ); + + const ambiguous = baseInput({ + route: "explore-token", + scope, + readiness: vi.fn(async () => readiness), + indexed: vi.fn(async () => + indexedResult(jsonResponse({ token: { address: ADDRESS } }), { + scope, + recordScopes: [scope[0]!, scope[1]!], + versions, + }), + ), + }); + const ambiguousResponse = await coordinateRouteRead(ambiguous); + expect(ambiguousResponse.headers.get("X-Programmable-Read-Source")).toBe( + "rpc", + ); + expect(ambiguousResponse.headers.get("Cache-Control")).toBe( + "private, no-store", + ); + }); + + it("requires branded scope evidence and rejects Deep before response construction", async () => { + vi.stubEnv("INDEXED_EXPLORE_LIST_READS_ENABLED", "true"); + const scope = ALL_REVIEWED_ROUTE_SCOPES; + const versions = scopedVersions(scope); + const readiness = scope.map((member, index) => ({ + ...member, + eligibility: "eligible" as const, + parity: "current" as const, + version: versions[index]!.version, + })); + expect(() => + scopeEvidence([ + { model: "deep", releaseVersion: "deep-v3" }, + ] as unknown as readonly ReviewedRouteScope[]), + ).toThrow(DataPipelineError); + + const input = baseInput({ + route: "explore-list", + scope, + readiness: vi.fn(async () => readiness), + indexed: vi.fn(async () => ({ + ...indexedResult(jsonResponse({ tokens: [] }), { + scope, + recordScopes: scope, + versions, + }), + scopeEvidence: Object.freeze({ + recordCount: 1, + recordScopes: Object.freeze([scope[0]!]), + }), + })), + }); + + const result = await coordinateRouteRead(input); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + }); + + it("refuses an indexed response that advanced beyond its parity checkpoint", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const input = baseInput({ + readiness: vi.fn(async () => [ + { + ...CLASSIC_SCOPE[0], + eligibility: "eligible" as const, + parity: "current" as const, + version: { + ...PROJECTION_VERSION, + blockNumber: "99", + blockHash: OTHER_BLOCK_HASH, + }, + }, + ]), + }); + + const result = await coordinateRouteRead(input); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + }); + + it("refuses a generation change even when block number and hash are unchanged", async () => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const input = baseInput({ + indexed: vi.fn(async () => ({ + ...indexedResult(jsonResponse({ ok: true })), + versions: scopedVersions(CLASSIC_SCOPE, [ + { + ...PROJECTION_VERSION, + reorgGeneration: "2", + }, + ]), + })), + }); + + const result = await coordinateRouteRead(input); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + }); + + it.each([ + { + field: "checkpointId", + value: "00000000-0000-4000-8000-000000000099", + }, + { field: "projectorVersion", value: "read-model-v2" }, + { field: "sourceGroup", value: "envio-secondary" }, + ] as const)( + "refuses a $field change at the same block and generation", + async ({ field, value }) => { + vi.stubEnv("INDEXED_EXPLORE_TOKEN_READS_ENABLED", "true"); + const input = baseInput({ + indexed: vi.fn(async () => ({ + ...indexedResult(jsonResponse({ ok: true })), + versions: scopedVersions(CLASSIC_SCOPE, [ + { + ...PROJECTION_VERSION, + [field]: value, + }, + ]), + })), + }); + + const result = await coordinateRouteRead(input); + expect(result.headers.get("X-Programmable-Read-Source")).toBe("rpc"); + expect(result.headers.get("Cache-Control")).toBe("private, no-store"); + }, + ); +}); diff --git a/tests/data-pipeline/rpc-providers.test.ts b/tests/data-pipeline/rpc-providers.test.ts new file mode 100644 index 00000000..9105e9aa --- /dev/null +++ b/tests/data-pipeline/rpc-providers.test.ts @@ -0,0 +1,208 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + assertProductionDualRpcProviders, + boundedRpcExecutor, + createProductionDualRpcProviders, +} from "../../lib/data-pipeline/rpc-providers.server"; + +const ALCHEMY = + "https://eth-mainnet.g.alchemy.com/v2/alchemy-test-key"; +const QUICKNODE = + "https://programmable.ethereum.quiknode.pro/quicknode-test-token/"; + +describe("production dual-RPC providers", () => { + it("paces provider calls below the sustained request ceiling", async () => { + vi.useFakeTimers(); + try { + const execute = boundedRpcExecutor(20); + const starts: number[] = []; + const calls = Array.from({ length: 21 }, () => + execute(async () => { + starts.push(Date.now()); + }) + ); + + await vi.advanceTimersByTimeAsync(0); + expect(starts).toHaveLength(20); + await vi.advanceTimersByTimeAsync(999); + expect(starts).toHaveLength(20); + await vi.advanceTimersByTimeAsync(1); + await Promise.all(calls); + expect(starts).toHaveLength(21); + expect(starts[20]! - starts[0]!).toBeGreaterThanOrEqual(1_000); + } finally { + vi.useRealTimers(); + } + }); + + it("derives fixed independent identities from exact paid-provider URLs", () => { + const providers = createProductionDualRpcProviders({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }); + + expect(providers[0].identity).toMatch( + /^alchemy-mainnet-[0-9a-f]{32}$/u, + ); + expect(providers[1].identity).toMatch( + /^quicknode-mainnet-[0-9a-f]{32}$/u, + ); + expect(providers.map(({ vendorGroup }) => vendorGroup)).toEqual([ + "alchemy", + "quicknode", + ]); + expect(providers[0].client).not.toBe(providers[1].client); + expect(providers[0].endpointCommitment).toMatch(/^0x[0-9a-f]{64}$/u); + expect(providers[1].endpointCommitment).toMatch(/^0x[0-9a-f]{64}$/u); + expect(providers[0].endpointCommitment).not.toBe( + providers[1].endpointCommitment, + ); + expect(providers[0].endpointOriginCommitment).toMatch( + /^0x[0-9a-f]{64}$/u, + ); + expect(providers[1].endpointOriginCommitment).toMatch( + /^0x[0-9a-f]{64}$/u, + ); + expect(Object.isFrozen(providers)).toBe(true); + expect(Object.isFrozen(providers[0])).toBe(true); + expect(Object.isFrozen(providers[0].client)).toBe(true); + expect(Object.isFrozen(providers[1].client)).toBe(true); + expect(() => + Object.defineProperty(providers[0].client, "getChainId", { + value: async () => 11155111, + }), + ).toThrow(TypeError); + expect(JSON.stringify(providers)).not.toContain("alchemy-test-key"); + expect(JSON.stringify(providers)).not.toContain("quicknode-test-token"); + expect(JSON.stringify(providers)).not.toContain( + "programmable.ethereum.quiknode.pro", + ); + }); + + it("fails closed when a production platform marker conflicts with NODE_ENV=test", () => { + const providers = createProductionDualRpcProviders({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }); + vi.stubEnv("NODE_ENV", "test"); + vi.stubEnv("VERCEL_ENV", "production"); + try { + expect(() => assertProductionDualRpcProviders(providers)).not.toThrow(); + expect(() => + assertProductionDualRpcProviders([ + { ...providers[0] }, + { ...providers[1] }, + ]), + ).toThrowError( + expect.objectContaining({ + dependency: "rpc", + code: "invalid_input", + }), + ); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("rejects a structurally identical but unregistered pair in production", () => { + const providers = createProductionDualRpcProviders({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }); + vi.stubEnv("NODE_ENV", "production"); + try { + expect(() => assertProductionDualRpcProviders(providers)).not.toThrow(); + expect(() => + assertProductionDualRpcProviders([ + { ...providers[0] }, + { ...providers[1] }, + ]), + ).toThrowError( + expect.objectContaining({ + dependency: "rpc", + code: "invalid_input", + }), + ); + } finally { + vi.unstubAllEnvs(); + } + }); + + it.each([ + [{}, "missing URLs"], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: + "https://eth-sepolia.g.alchemy.com/v2/alchemy-test-key", + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + "wrong Alchemy network", + ], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: + "https://example.com/quicknode-test-token/", + }, + "non-QuickNode secondary", + ], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: `${ALCHEMY}?leak=true`, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + "query-bearing endpoint", + ], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: + "https://user:password@programmable.ethereum.quiknode.pro/quicknode-test-token/", + }, + "embedded basic auth", + ], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: + "http://eth-mainnet.g.alchemy.com/v2/alchemy-test-key", + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + "non-TLS endpoint", + ], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: + "https://eth-mainnet.g.alchemy.com/v2/docs-demo", + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }, + "public Alchemy demo credential", + ], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: + "https://docs-demo.quiknode.pro/docs-demo/", + }, + "public QuickNode demo credential", + ], + [ + { + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + NEXT_PUBLIC_PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + }, + "browser-exposed Alchemy endpoint", + ], + ] as const)("rejects %s (%s)", (environment, label) => { + expect(label.length).toBeGreaterThan(0); + expect(() => createProductionDualRpcProviders(environment)).toThrowError( + expect.objectContaining({ + dependency: "config", + code: "invalid_input", + }), + ); + }); +}); diff --git a/tests/data-pipeline/rpc-reward-providers.test.ts b/tests/data-pipeline/rpc-reward-providers.test.ts new file mode 100644 index 00000000..5a6a007a --- /dev/null +++ b/tests/data-pipeline/rpc-reward-providers.test.ts @@ -0,0 +1,301 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + readContract: vi.fn(), + createPublicClient: vi.fn(), +})); + +vi.mock("server-only", () => ({})); +vi.mock("viem", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createPublicClient: mocks.createPublicClient, + }; +}); + +import { createProductionDualRpcProviders } from "../../lib/data-pipeline/rpc-providers.server"; + +const ALCHEMY = "https://eth-mainnet.g.alchemy.com/v2/alchemy-test-key"; +const QUICKNODE = + "https://programmable.ethereum.quiknode.pro/quicknode-test-token/"; +const address = (digit: string) => + `0x${digit.repeat(40)}` as `0x${string}`; +const bytes32 = (digit: string) => + `0x${digit.repeat(64)}` as `0x${string}`; + +describe("production reward-vault RPC reader", () => { + beforeEach(() => { + mocks.readContract.mockReset(); + mocks.createPublicClient.mockReset(); + mocks.createPublicClient.mockReturnValue({ + readContract: mocks.readContract, + }); + }); + + it("uses only committed call shapes at the requested historical block", async () => { + const vault = address("7"); + const alice = address("1"); + const bob = address("2"); + const blockHash = bytes32("9"); + mocks.readContract.mockImplementation(async ({ functionName, args }) => { + if (functionName === "poolId") return bytes32("3"); + if (functionName === "configurationEpoch") return 2n; + if (functionName === "activeConfigurationHash") return bytes32("4"); + if (functionName === "totalCreatorFeesReceived") return 13n; + if (functionName === "totalCreatorFeesClaimed") return 4n; + if (functionName === "beneficiaryCount") return 2n; + if (functionName === "beneficiaryAt") return bob; + if (functionName === "shareBpsAt") { + return args[0] === 0n ? 4_000 : 6_000; + } + if (functionName === "claimable") { + return args[0] === alice ? 0n : 9n; + } + if (functionName === "claimedBy") { + return args[0] === alice ? 4n : 0n; + } + throw new Error("unexpected function"); + }); + const providers = createProductionDualRpcProviders({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }); + + const snapshot = await providers[0].client.readRewardSnapshot!({ + model: "classic-v3", + vault, + blockNumber: 100n, + blockHash, + balanceAccounts: [alice, bob], + }); + + expect(snapshot).toMatchObject({ + model: "classic-v3", + vault, + blockNumber: "100", + blockHash, + beneficiaryCount: "2", + rpcCallCount: 14, + balances: [ + { account: alice, claimableAccrued: "0", claimedTotal: "4" }, + { account: bob, claimableAccrued: "9", claimedTotal: "0" }, + ], + }); + expect(mocks.readContract).toHaveBeenCalledTimes(14); + for (const [request] of mocks.readContract.mock.calls) { + expect(request).toMatchObject({ + address: vault, + blockHash, + requireCanonical: true, + }); + expect(request).not.toHaveProperty("blockNumber"); + } + expect( + [...new Set( + mocks.readContract.mock.calls.map( + ([request]) => request.functionName, + ), + )].sort(), + ).toEqual([ + "activeConfigurationHash", + "beneficiaryAt", + "beneficiaryCount", + "claimable", + "claimedBy", + "configurationEpoch", + "poolId", + "shareBpsAt", + "totalCreatorFeesClaimed", + "totalCreatorFeesReceived", + ]); + }); + + it("caps physical reward reads at eight in flight per provider", async () => { + const vault = address("7"); + const beneficiary = address("1"); + const blockHash = bytes32("9"); + const accounts = Array.from({ length: 48 }, (_value, index) => + `0x${(index + 1).toString(16).padStart(40, "0")}` as `0x${string}` + ); + let inFlight = 0; + let maximumInFlight = 0; + mocks.readContract.mockImplementation(async ({ functionName }) => { + inFlight += 1; + maximumInFlight = Math.max(maximumInFlight, inFlight); + await new Promise((resolve) => setTimeout(resolve, 1)); + inFlight -= 1; + if (functionName === "poolId") return bytes32("3"); + if (functionName === "configurationEpoch") return 1n; + if (functionName === "activeConfigurationHash") return bytes32("4"); + if (functionName === "totalCreatorFeesReceived") return 0n; + if (functionName === "totalCreatorFeesClaimed") return 0n; + if (functionName === "beneficiaryCount") return 1n; + if (functionName === "beneficiaryAt") return beneficiary; + if (functionName === "shareBpsAt") return 10_000n; + if (functionName === "claimable" || functionName === "claimedBy") { + return 0n; + } + throw new Error("unexpected function"); + }); + const providers = createProductionDualRpcProviders({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }); + + const snapshot = await providers[0].client.readRewardSnapshot!({ + model: "classic-v3", + vault, + blockNumber: 100n, + blockHash, + balanceAccounts: accounts, + }); + + expect(snapshot.rpcCallCount).toBe(104); + expect(mocks.readContract).toHaveBeenCalledTimes(104); + expect(maximumInFlight).toBeGreaterThan(1); + expect(maximumInFlight).toBeLessThanOrEqual(8); + }, 10_000); + + it("shares one eight-call limit across mixed single and batch reads", async () => { + const blockHash = bytes32("9"); + const transactionHash = bytes32("8"); + const token = address("7"); + let inFlight = 0; + let maximumInFlight = 0; + const physical = async (value: T): Promise => { + inFlight += 1; + maximumInFlight = Math.max(maximumInFlight, inFlight); + await new Promise((resolve) => setTimeout(resolve, 1)); + inFlight -= 1; + return value; + }; + const block = { number: 100n, hash: blockHash, timestamp: 1_000n }; + const receipt = { + status: "success" as const, + blockNumber: 100n, + blockHash, + transactionHash, + transactionIndex: 0, + logs: [], + }; + const singleClient = { + getBlock: vi.fn(async () => physical(block)), + getTransactionReceipt: vi.fn(async () => physical(receipt)), + getBytecode: vi.fn(async () => physical("0x60" as const)), + request: vi.fn(async () => physical([])), + readContract: mocks.readContract, + }; + const batchClient = { + getBlock: vi.fn(async () => physical(block)), + getTransactionReceipt: vi.fn(async () => physical(receipt)), + getBytecode: vi.fn(async () => physical("0x60" as const)), + request: vi.fn(async () => physical([])), + readContract: mocks.readContract, + }; + mocks.createPublicClient + .mockReset() + .mockReturnValueOnce(singleClient) + .mockReturnValueOnce(batchClient) + .mockReturnValueOnce(singleClient) + .mockReturnValueOnce(batchClient); + const providers = createProductionDualRpcProviders({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }); + const blockNumbers = Array.from({ length: 20 }, (_value, index) => + BigInt(101 + index) + ); + const hashes = Array.from({ length: 20 }, (_value, index) => + `0x${(index + 1).toString(16).padStart(64, "0")}` as `0x${string}` + ); + const bytecodeRequests = blockNumbers.map(() => ({ + address: token, + blockHash, + requireCanonical: true as const, + })); + const logFilter = { + addresses: [token], + topic0: [bytes32("6")], + fromBlock: 100n, + toBlock: 100n, + }; + + await Promise.all([ + providers[0].client.getBlock({ blockNumber: 100n }), + providers[0].client.getBlocks!({ blockNumbers }), + providers[0].client.getTransactionReceipt({ hash: transactionHash }), + providers[0].client.getTransactionReceipts!({ hashes }), + providers[0].client.getBytecode({ address: token, blockNumber: 100n }), + providers[0].client.getBytecodes!({ requests: bytecodeRequests }), + providers[0].client.getLogs!(logFilter), + providers[0].client.getLogsBatch!({ + requests: Array.from({ length: 20 }, () => logFilter), + }), + ]); + + expect(maximumInFlight).toBeGreaterThan(1); + expect(maximumInFlight).toBeLessThanOrEqual(8); + expect(batchClient.getBlock).toHaveBeenCalledTimes(20); + expect(batchClient.getTransactionReceipt).toHaveBeenCalledTimes(20); + expect(batchClient.getBytecode).toHaveBeenCalledTimes(20); + expect(batchClient.request).toHaveBeenCalledTimes(20); + }, 10_000); + + it("reads factory authentication and CREATE2 helpers at the exact block", async () => { + const factory = address("6"); + const vault = address("7"); + const feeHook = address("8"); + const alice = address("1"); + const blockHash = bytes32("9"); + const salt = bytes32("a"); + const poolId = bytes32("3"); + const configurationHash = bytes32("4"); + const initCodeHash = bytes32("5"); + const ctoAuthority = address("a"); + mocks.readContract.mockImplementation(async ({ functionName }) => { + if (functionName === "configurationHashOf") return configurationHash; + if (functionName === "ctoAuthority") return ctoAuthority; + if (functionName === "initCodeHash") return initCodeHash; + if (functionName === "predict") return vault; + throw new Error("unexpected function"); + }); + const providers = createProductionDualRpcProviders({ + PROGRAMMABLE_ALCHEMY_MAINNET_RPC_URL: ALCHEMY, + PROGRAMMABLE_QUICKNODE_MAINNET_RPC_URL: QUICKNODE, + }); + + await expect( + providers[0].client.readClassicRewardFactorySnapshot!({ + factory, + vault, + blockNumber: 100n, + blockHash, + salt, + feeHook, + poolId, + beneficiaries: [alice], + sharesBps: [10_000], + }), + ).resolves.toEqual({ + factory, + vault, + blockNumber: "100", + blockHash, + configurationHash, + ctoAuthority, + initCodeHash, + predictedVault: vault, + rpcCallCount: 4, + }); + expect(mocks.readContract).toHaveBeenCalledTimes(4); + for (const [request] of mocks.readContract.mock.calls) { + expect(request).toMatchObject({ + address: factory, + blockHash, + requireCanonical: true, + }); + expect(request).not.toHaveProperty("blockNumber"); + } + }); +}); diff --git a/tests/data-pipeline/runtime-bytecode.test.ts b/tests/data-pipeline/runtime-bytecode.test.ts new file mode 100644 index 00000000..4486cafc --- /dev/null +++ b/tests/data-pipeline/runtime-bytecode.test.ts @@ -0,0 +1,219 @@ +import { describe, expect, it, vi } from "vitest"; +import { keccak256 } from "viem"; + +vi.mock("server-only", () => ({})); + +import { + immutableReferencesCommitment, + normalizeRuntimeBytecode, + runtimeBytecodeEvidence, +} from "../../lib/data-pipeline/runtime-bytecode"; + +const REFERENCES = [ + { start: 2, length: 2 }, + { start: 7, length: 1 }, +] as const; + +describe("constructor-immutable runtime normalization", () => { + it("retains exact instance hashes while producing one template hash", () => { + const first = runtimeBytecodeEvidence({ + runtimeBytecode: "0x6001aaaabbccdd11", + expectedByteLength: 8, + immutableReferences: REFERENCES, + }); + const second = runtimeBytecodeEvidence({ + runtimeBytecode: "0x6001ffffbbccdd22", + expectedByteLength: 8, + immutableReferences: REFERENCES, + }); + + expect(first.exactRuntimeCodeHash).not.toBe(second.exactRuntimeCodeHash); + expect(first.normalizedRuntimeCodeHash).toBe( + second.normalizedRuntimeCodeHash, + ); + expect(first.immutableReferencesCommitment).toBe( + second.immutableReferencesCommitment, + ); + expect(normalizeRuntimeBytecode({ + runtimeBytecode: "0x6001aaaabbccdd11", + expectedByteLength: 8, + immutableReferences: REFERENCES, + })).toBe("0x60010000bbccdd00"); + }); + + it("does not hide mutations outside compiler-reviewed ranges", () => { + const first = runtimeBytecodeEvidence({ + runtimeBytecode: "0x6001aaaabbccdd11", + expectedByteLength: 8, + immutableReferences: REFERENCES, + }); + const mutated = runtimeBytecodeEvidence({ + runtimeBytecode: "0x6001aaaabbccfe11", + expectedByteLength: 8, + immutableReferences: REFERENCES, + }); + + expect(mutated.normalizedRuntimeCodeHash).not.toBe( + first.normalizedRuntimeCodeHash, + ); + }); + + it("commits to ordering, offsets, lengths, and runtime length", () => { + const baseline = immutableReferencesCommitment(REFERENCES, 8); + expect(baseline).toMatch(/^0x[0-9a-f]{64}$/u); + expect( + immutableReferencesCommitment( + [ + { start: 2, length: 1 }, + { start: 7, length: 1 }, + ], + 8, + ), + ).not.toBe(baseline); + expect(immutableReferencesCommitment(REFERENCES, 9)).not.toBe(baseline); + }); + + it("derives every field from one immutable input snapshot", () => { + let runtimeReads = 0; + let lengthReads = 0; + let referencesReads = 0; + let firstStartReads = 0; + let firstLengthReads = 0; + const approvedRuntime = "0x6001aaaabbccdd11" as const; + const input = { + get runtimeBytecode() { + runtimeReads += 1; + return runtimeReads === 1 + ? approvedRuntime + : ("0x6001aaaabbccfe11" as const); + }, + get expectedByteLength() { + lengthReads += 1; + return lengthReads === 1 ? 8 : 7; + }, + get immutableReferences() { + referencesReads += 1; + return [ + { + get start() { + firstStartReads += 1; + return firstStartReads === 1 ? 2 : 4; + }, + get length() { + firstLengthReads += 1; + return firstLengthReads === 1 ? 2 : 1; + }, + }, + { start: 7, length: 1 }, + ]; + }, + }; + + const evidence = runtimeBytecodeEvidence(input); + + expect(runtimeReads).toBe(1); + expect(lengthReads).toBe(1); + expect(referencesReads).toBe(1); + expect(firstStartReads).toBe(1); + expect(firstLengthReads).toBe(1); + expect(evidence.exactRuntimeCodeHash).toBe( + keccak256(approvedRuntime), + ); + expect(evidence.normalizedRuntimeCodeHash).toBe( + keccak256("0x60010000bbccdd00"), + ); + expect(evidence.runtimeByteLength).toBe(8); + }); + + it("reads each proxied immutable-reference element once", () => { + let elementReads = 0; + const references = new Proxy([...REFERENCES], { + get(target, property, receiver) { + if (property === "0" || property === "1") elementReads += 1; + return Reflect.get(target, property, receiver); + }, + }); + + runtimeBytecodeEvidence({ + runtimeBytecode: "0x6001aaaabbccdd11", + expectedByteLength: 8, + immutableReferences: references, + }); + + expect(elementReads).toBe(2); + }); + + it("pins the compiler-reviewed Classic and Stock reference maps", () => { + const classicReferences = [ + 362, 510, 664, 884, 1092, 1195, 1941, 2469, 3048, 3601, 3645, + 3910, 4012, + ].map((start) => ({ start, length: 32 })); + const stockReferences = [ + 286, 404, 479, 610, 712, 751, 1258, 1411, 1462, 1604, 2018, 2059, + 2441, + ].map((start) => ({ start, length: 32 })); + + expect(immutableReferencesCommitment(classicReferences, 6543)).toBe( + "0x907697f82d7893c5208d35481bff0be82526809b29f52b3e83a1bcda15be583d", + ); + expect(immutableReferencesCommitment(stockReferences, 3352)).toBe( + "0xb7a44c4e10798e9027247e4d5e7ac191d3f7b50b8d81a4746ffbd6a337a42ec0", + ); + expect(() => + immutableReferencesCommitment( + [classicReferences[1]!, classicReferences[0]!], + 6543, + ), + ).toThrowError(expect.objectContaining({ code: "invalid_input" })); + }); + + it("accepts the EIP-170 ceiling and rejects one byte above it", () => { + expect(() => + runtimeBytecodeEvidence({ + runtimeBytecode: `0x${"00".repeat(24_576)}`, + expectedByteLength: 24_576, + immutableReferences: [{ start: 0, length: 1 }], + }), + ).not.toThrow(); + expect(() => + runtimeBytecodeEvidence({ + runtimeBytecode: `0x${"00".repeat(24_577)}`, + expectedByteLength: 24_577, + immutableReferences: [{ start: 0, length: 1 }], + }), + ).toThrowError(expect.objectContaining({ code: "invalid_input" })); + }); + + it.each([ + { + runtimeBytecode: "0x", + expectedByteLength: 0, + immutableReferences: REFERENCES, + }, + { + runtimeBytecode: "0x6001aa", + expectedByteLength: 4, + immutableReferences: REFERENCES, + }, + { + runtimeBytecode: "0x6001aaaabbccdd11", + expectedByteLength: 8, + immutableReferences: [ + { start: 2, length: 3 }, + { start: 4, length: 1 }, + ], + }, + { + runtimeBytecode: "0x6001aaaabbccdd11", + expectedByteLength: 8, + immutableReferences: [{ start: 7, length: 2 }], + }, + ])("rejects malformed runtime evidence", (input) => { + expect(() => runtimeBytecodeEvidence({ + ...input, + runtimeBytecode: input.runtimeBytecode as `0x${string}`, + })).toThrowError( + expect.objectContaining({ code: "invalid_input" }), + ); + }); +}); diff --git a/tests/data-pipeline/stock-paired-reconciler-route-builder.test.ts b/tests/data-pipeline/stock-paired-reconciler-route-builder.test.ts new file mode 100644 index 00000000..c6c85088 --- /dev/null +++ b/tests/data-pipeline/stock-paired-reconciler-route-builder.test.ts @@ -0,0 +1,305 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { + buildStockPairedV1ExactBlockContribution, + buildStockPairedV2ExactBlockContribution, + buildStockPairedV3ExactBlockContribution, + STOCK_PAIRED_RECONCILER_ROUTE_KEYS, + type StockPairedExactBlockContributionBuilder, +} from "../../lib/data-pipeline/stock-paired-reconciler-route-builder.server"; +import { + assertStockPairedReconcilerContribution, + type StockPairedReconcilerRelease, +} from "../../lib/data-pipeline/stock-paired-reconciler-contribution"; +import { + stockPairedLargeCorpusFixture, + stockPairedReconcilerRouteFixture, + type StockPairedReconcilerFixture, + type StockPairedReconcilerFixtureMutation, +} from "./stock-paired-reconciler-route-fixture"; + +const builders: Readonly> = Object.freeze({ + "stock-paired-v1": buildStockPairedV1ExactBlockContribution, + "stock-paired-v2": buildStockPairedV2ExactBlockContribution, + "stock-paired-v3": buildStockPairedV3ExactBlockContribution, +}); + +const releaseVersions = Object.freeze([ + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const); + +async function build( + releaseVersion: StockPairedReconcilerRelease, + fixture: StockPairedReconcilerFixture, +) { + return builders[releaseVersion]({ + rpc: fixture.rpc, + contract: fixture.contract, + blockNumber: fixture.blockNumber, + blockHash: fixture.blockHash, + signal: new AbortController().signal, + }); +} + +describe("Stock-Paired exact-block reconciler contribution", () => { + it("builds a real 257-launch three-page corpus inside both global RPC caps", async () => { + const fixture = stockPairedLargeCorpusFixture("stock-paired-v3", 257); + const contribution = await builders["stock-paired-v3"]({ + rpc: fixture.rpc, + contract: fixture.contract, + blockNumber: fixture.blockNumber, + blockHash: fixture.blockHash, + signal: new AbortController().signal, + }); + + expect(contribution.tokens).toHaveLength(257); + expect(contribution.charts).toHaveLength(257); + expect(contribution.launches).toHaveLength(257); + expect(fixture.corpusPageSizes).toEqual([128, 128, 1]); + expect(fixture.timestampBatchSizes).toEqual([128, 128, 1]); + expect(fixture.budget).toEqual({ physical: 283, logical: 7_749 }); + expect(fixture.budget.physical).toBeLessThanOrEqual(512); + expect(fixture.budget.logical).toBeLessThanOrEqual(512 * 32); + expect(fixture.rpc.requestCount()).toBe(283); + expect(fixture.rpc.logicalRequestCount()).toBe(7_749); + }); + + it.each(releaseVersions)( + "builds exact, release-labelled %s route parts", + async (releaseVersion) => { + const fixture = stockPairedReconcilerRouteFixture(releaseVersion); + const contribution = await build(releaseVersion, fixture); + + expect(contribution).toMatchObject({ + contractVersion: "stock-paired-route-contribution-v1", + releaseVersion, + modelId: "stock-paired", + }); + expect(contribution.tokens).toHaveLength(1); + expect(contribution.charts).toHaveLength(1); + expect(contribution.profiles).toHaveLength(1); + expect(contribution.launches).toHaveLength(1); + expect(contribution).not.toHaveProperty("rewards"); + expect(contribution.tokens[0]).toMatchObject({ + releaseVersion, + modelId: "stock-paired", + tokenAddress: fixture.expected.token.toLowerCase(), + creatorAddress: fixture.expected.creator.toLowerCase(), + quoteAssetAddress: fixture.expected.quoteAsset.toLowerCase(), + poolId: fixture.expected.poolId, + }); + expect(contribution.charts[0]).toMatchObject({ + releaseVersion, + modelId: "stock-paired", + tokenAddress: fixture.expected.token.toLowerCase(), + quoteAssetAddress: fixture.expected.quoteAsset.toLowerCase(), + volume: { + quoteAssetAddress: fixture.expected.quoteAsset.toLowerCase(), + grossQuoteRaw: fixture.expected.grossQuoteRaw, + creatorFeeQuoteRaw: "9000", + launcherFeeQuoteRaw: "1000", + }, + }); + expect(contribution.profiles[0]).toEqual({ + account: fixture.expected.creator.toLowerCase(), + tokens: [{ + releaseVersion, + modelId: "stock-paired", + tokenAddress: fixture.expected.token.toLowerCase(), + launchTransactionHash: + (contribution.tokens[0] as { launchTransactionHash: string }) + .launchTransactionHash, + }], + }); + expect(contribution.launches[0]).toMatchObject({ + releaseVersion, + modelId: "stock-paired", + account: fixture.expected.creator.toLowerCase(), + tokenAddress: fixture.expected.token.toLowerCase(), + }); + expect(STOCK_PAIRED_RECONCILER_ROUTE_KEYS).toEqual([ + "explore-list", + "explore-token", + "explore-chart", + "creator-profile", + "launch-lookup", + ]); + expect(STOCK_PAIRED_RECONCILER_ROUTE_KEYS).not.toContain( + "classic-v3-profile", + ); + + expect(fixture.observations.codeBlockHashes.length).toBeGreaterThan(20); + expect(fixture.observations.codeBlockHashes.every( + (hash) => hash === fixture.blockHash, + )).toBe(true); + expect(fixture.observations.callBlockHashes).toEqual([ + fixture.blockHash, + ]); + expect(fixture.observations.timestampExpectedHashes).toEqual([ + fixture.blockHash, + ]); + }, + ); + + it.each(releaseVersions)( + "produces the same %s contribution from two independent exact providers", + async (releaseVersion) => { + const first = stockPairedReconcilerRouteFixture(releaseVersion); + const second = stockPairedReconcilerRouteFixture(releaseVersion); + const [left, right] = await Promise.all([ + build(releaseVersion, first), + build(releaseVersion, second), + ]); + + expect(left).toEqual(right); + }, + ); + + it.each(releaseVersions)( + "accepts the exact-output ceiling fee envelope for a tiny %s quote swap", + async (releaseVersion) => { + const fixture = stockPairedReconcilerRouteFixture(releaseVersion, { + feeGrossQuote: 2n, + feeTotalQuote: 1n, + }); + const contribution = await build(releaseVersion, fixture); + + expect(contribution.charts[0]).toMatchObject({ + volume: { + grossQuoteRaw: "2", + creatorFeeQuoteRaw: "1", + launcherFeeQuoteRaw: "0", + }, + }); + }, + ); + + it.each(releaseVersions)( + "rejects a %s quote fee outside the exact-input floor/exact-output ceiling envelope", + async (releaseVersion) => { + const fixture = stockPairedReconcilerRouteFixture(releaseVersion, { + feeGrossQuote: 2n, + feeTotalQuote: 2n, + }); + + await expect(build(releaseVersion, fixture)).rejects.toMatchObject({ + dependency: "uniswap", + code: "validation_failed", + safeMetadata: { operation: "stock-reconciler-fee-conservation" }, + }); + }, + ); + + it("exposes provider disagreement instead of normalizing quote volume", async () => { + const first = stockPairedReconcilerRouteFixture("stock-paired-v3"); + const second = stockPairedReconcilerRouteFixture("stock-paired-v3", { + feeGrossQuote: 2_000_000n, + }); + const [left, right] = await Promise.all([ + build("stock-paired-v3", first), + build("stock-paired-v3", second), + ]); + + expect(left).not.toEqual(right); + expect(right.charts[0]).toMatchObject({ + volume: { + grossQuoteRaw: "2000000", + creatorFeeQuoteRaw: "18000", + launcherFeeQuoteRaw: "2000", + }, + }); + }); + + it.each(releaseVersions)( + "accepts normal %s exact-output rounding without losing a fee unit", + async (releaseVersion) => { + const fixture = stockPairedReconcilerRouteFixture(releaseVersion, { + feeGrossQuote: 10_102n, + feeTotalQuote: 102n, + }); + const contribution = await build(releaseVersion, fixture); + + expect(contribution.charts[0]).toMatchObject({ + volume: { + grossQuoteRaw: "10102", + creatorFeeQuoteRaw: "92", + launcherFeeQuoteRaw: "10", + }, + }); + }, + ); + + it.each([ + ["runtime", "stock-reconciler-runtime-launcher"], + ["quote-configuration", "stock-reconciler-current-provenance"], + ["receipt-provenance", "stock-reconciler-receipt-provenance"], + ["transaction-provenance", "stock-reconciler-calldata-provenance"], + ["companion-launch-hash", "stock-reconciler-companion-launch-hash"], + ["forwarder-provenance", "stock-reconciler-current-provenance"], + ] as const)( + "fails closed for the %s mutation", + async (mutation, operation) => { + const fixture = stockPairedReconcilerRouteFixture("stock-paired-v3", { + mutation: mutation as StockPairedReconcilerFixtureMutation, + }); + await expect(build("stock-paired-v3", fixture)).rejects.toMatchObject({ + dependency: "uniswap", + code: "validation_failed", + safeMetadata: { operation }, + }); + }, + ); + + it("rejects a route matrix that includes the Classic-only profile", async () => { + const fixture = stockPairedReconcilerRouteFixture("stock-paired-v3"); + await expect(build("stock-paired-v3", { + ...fixture, + contract: { + ...fixture.contract, + routeKeys: [...fixture.contract.routeKeys, "classic-v3-profile"], + }, + })).rejects.toMatchObject({ + dependency: "config", + code: "invalid_input", + safeMetadata: { operation: "stock-reconciler-release" }, + }); + }); + + it("rejects a checkpoint that is not the contract checkpoint", async () => { + const fixture = stockPairedReconcilerRouteFixture("stock-paired-v3"); + await expect(builders["stock-paired-v3"]({ + rpc: fixture.rpc, + contract: fixture.contract, + blockNumber: fixture.blockNumber + 1n, + blockHash: fixture.blockHash, + signal: new AbortController().signal, + })).rejects.toMatchObject({ + dependency: "config", + code: "invalid_input", + safeMetadata: { operation: "stock-reconciler-checkpoint-binding" }, + }); + }); + + it("rejects extra or renamed contribution fields", async () => { + const fixture = stockPairedReconcilerRouteFixture("stock-paired-v3"); + const contribution = await build("stock-paired-v3", fixture); + const malformed = structuredClone(contribution) as unknown as { + tokens: Array>; + }; + malformed.tokens[0]!.nativeVolumeWei = "1000000"; + + expect(() => assertStockPairedReconcilerContribution( + malformed as never, + )).toThrowError(expect.objectContaining({ + dependency: "postgres", + code: "validation_failed", + })); + }); +}); diff --git a/tests/data-pipeline/stock-paired-reconciler-route-fixture.ts b/tests/data-pipeline/stock-paired-reconciler-route-fixture.ts new file mode 100644 index 00000000..1fc32d0f --- /dev/null +++ b/tests/data-pipeline/stock-paired-reconciler-route-fixture.ts @@ -0,0 +1,1410 @@ +import { + encodeAbiParameters, + encodeEventTopics, + encodeFunctionData, + encodeFunctionResult, + getAddress, + keccak256, + parseAbi, + parseAbiItem, + parseAbiParameters, + type Abi, + type AbiEvent, + type AbiParameter, + type Address, + type Hex, +} from "viem"; + +import { stateViewReadAbi, uerc20ReadAbi } from "../../lib/onchain/abis"; +import { + getStockPairedExpectedInitialTickForRelease, + getStockPairedQuoteAssetsForRelease, + stockFeeSplitVaultAbi, + stockPairedEthLaunchCoordinatorAbi, + stockPairedHookAbi, + stockQuoteRegistryAbi, + STOCK_PAIRED_CREATOR_FEE_BPS, + STOCK_PAIRED_PROGRAMMABLE_FEE_BPS, + STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, +} from "../../lib/stock-paired"; +import { + resolveVerifiedStockPairedRelease, + resolveVerifiedStockPairedV2Release, + resolveVerifiedStockPairedV3Release, + type VerifiedStockPairedRelease, +} from "../../lib/stock-paired-release"; +import type { + ExactBlockRpcCall, + ExactBlockRpcClient, + ExactBlockRpcLog, + ExactBlockRpcReceipt, + ExactBlockRpcTransaction, +} from "../../lib/data-pipeline/reconciler-exact-block-reader.server"; +import type { HexBytes32 } from "../../lib/data-pipeline/codecs"; +import type { ReconcilerPreParityContract } from "../../lib/data-pipeline/reconciler-preparity"; +import { + STOCK_PAIRED_RECONCILER_ROUTE_KEYS, +} from "../../lib/data-pipeline/stock-paired-reconciler-route-builder.server"; +import type { StockPairedReconcilerRelease } from "../../lib/data-pipeline/stock-paired-reconciler-contribution"; + +const TOKEN_SUPPLY = 1_000_000_000n * 10n ** 18n; +const TOKEN = getAddress("0xf111111111111111111111111111111111111111"); +const CREATOR = getAddress("0xc111111111111111111111111111111111111111"); +const REWARD_VAULT = getAddress("0xd111111111111111111111111111111111111111"); +const POSITION_RECIPIENT = getAddress( + "0xe111111111111111111111111111111111111111", +); +const TRANSACTION_HASH = `0x${"71".repeat(32)}` as HexBytes32; +const BLOCK_HASH = `0x${"72".repeat(32)}` as HexBytes32; +const LAUNCH_HASH = `0x${"73".repeat(32)}` as HexBytes32; +const CREATOR_SALT = `0x${"74".repeat(32)}` as HexBytes32; +const EFFECTIVE_GRAFFITI = `0x${"75".repeat(32)}` as HexBytes32; +const QUOTE_CONFIGURATION_HASH = `0x${"76".repeat(32)}` as HexBytes32; +const FORWARDER_CONFIGURATION_HASH = + `0x${"77".repeat(32)}` as HexBytes32; +const ENDPOINT_COMMITMENT = `0x${"78".repeat(32)}` as HexBytes32; +const ENDPOINT_ORIGIN_COMMITMENT = + `0x${"79".repeat(32)}` as HexBytes32; +const ZERO_BYTES32 = `0x${"00".repeat(32)}` as HexBytes32; +const TOTAL_SUPPLY = TOKEN_SUPPLY; +const LOCKED_DUST = 1n; +const TOKEN_LIQUIDITY = TOTAL_SUPPLY - LOCKED_DUST; +const INITIAL_BUY_ETH = 5_000_000_000_000_000n; +const INITIAL_BUY_QUOTE = 1_000_000n; +const INITIAL_BUY_TOKEN = 1_000n * 10n ** 18n; +const SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336n; +const ACTIVE_LIQUIDITY = 123_456_789n; +const FEE_GROSS_QUOTE = 1_000_000n; + +const launchedEvent = parseAbiItem( + "event StockPairedTokenLaunched(address indexed deployer,address indexed token,address indexed quoteAsset,bytes32 poolId,address rewardVault,address positionRecipient,uint256 positionTokenId,bytes32 launchHash)", +); +const liquidityEvent = parseAbiItem( + "event StockPairedLiquidityConfigured(address indexed token,address indexed quoteAsset,uint256 totalSupply,uint256 tokenLiquidityAmount,uint256 lockedTokenDust,int24 initialTick,int24 tickLower,int24 tickUpper,uint24 lpFeePips,bytes32 launchHash)", +); +const initialBuyEvent = parseAbiItem( + "event StockPairedCreatorInitialBuy(address indexed deployer,address indexed token,address indexed quoteAsset,bytes32 poolId,uint256 quoteAmount,uint256 tokenAmount,bytes32 launchHash)", +); +const ethLaunchEvent = parseAbiItem( + "event StockPairedEthTokenLaunched(address indexed creator,address indexed token,address indexed quoteAsset,uint256 initialBuyEthAmount,uint256 initialBuyQuoteAmount,uint256 initialBuyTokenAmount,bytes32 launchHash)", +); +const poolRegisteredEvent = parseAbiItem( + "event PoolRegistered(bytes32 indexed poolId,address indexed token,address indexed quoteAsset,address rewardVault,address registrar,bool quoteIsCurrency0,bytes32 rewardConfigurationHash,bytes32 quoteConfigurationHash)", +); +const feeDisclosureEvent = parseAbiItem( + "event PoolFeeDisclosure(bytes32 indexed poolId,address indexed token,address indexed quoteAsset,address rewardVault,uint16 buySwapFeeBps,uint16 sellSwapFeeBps,uint16 creatorFeeBps,uint16 launcherFeeBps,uint16 transferTaxBps,uint24 lpFeePips)", +); +const feeAccruedEvent = parseAbiItem( + "event QuoteSwapFeesAccrued(bytes32 indexed poolId,address indexed swapSender,address indexed quoteAsset,bool isBuy,uint256 grossQuoteAmount,uint256 creatorFee,uint256 launcherFee)", +); +const vaultDeployedEvent = parseAbiItem( + "event QuoteAssetFeeSplitVaultDeployed(address indexed vault,address indexed feeHook,bytes32 indexed poolId,address quoteAsset)", +); +const swapEvent = parseAbiItem( + "event Swap(bytes32 indexed id,address indexed sender,int128 amount0,int128 amount1,uint160 sqrtPriceX96,uint128 liquidity,int24 tick,uint24 fee)", +); + +const launcherStateAbi = parseAbi([ + "function launchHashOf(address token) view returns (bytes32)", + "function rewardVaultOf(address token) view returns (address)", + "function quoteAssetOf(address token) view returns (address)", +]); +const rewardVaultFactoryStateAbi = parseAbi([ + "function isFactoryVault(address vault) view returns (bool)", + "function configurationHashOf(address vault) view returns (bytes32)", +]); +const positionForwarderFactoryStateAbi = parseAbi([ + "function isFactoryForwarder(address forwarder) view returns (bool)", + "function configurationHashOf(address forwarder) view returns (bytes32)", +]); +const poolKeyParameters = parseAbiParameters( + "address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks", +); +const rewardConfigurationParameters = parseAbiParameters( + "uint256 chainId,address vault,address feeHook,address poolManager,address quoteAsset,bytes32 poolId,address[] beneficiaries,uint16[] sharesBps", +); + +export type StockPairedReconcilerFixtureMutation = + | "runtime" + | "quote-configuration" + | "receipt-provenance" + | "transaction-provenance" + | "companion-launch-hash" + | "forwarder-provenance"; + +export type StockPairedReconcilerFixture = Readonly<{ + release: VerifiedStockPairedRelease; + contract: ReconcilerPreParityContract; + blockNumber: bigint; + blockHash: HexBytes32; + rpc: ExactBlockRpcClient; + expected: Readonly<{ + token: Address; + creator: Address; + quoteAsset: Address; + poolId: HexBytes32; + grossQuoteRaw: string; + }>; + observations: Readonly<{ + codeBlockHashes: HexBytes32[]; + callBlockHashes: HexBytes32[]; + timestampExpectedHashes: Array; + }>; +}>; + +export type StockPairedLargeCorpusFixture = Readonly<{ + release: VerifiedStockPairedRelease; + contract: ReconcilerPreParityContract; + blockNumber: bigint; + blockHash: HexBytes32; + rpc: ExactBlockRpcClient; + budget: Readonly<{ physical: number; logical: number }>; + corpusPageSizes: readonly number[]; + timestampBatchSizes: readonly number[]; +}>; + +function configuredRelease( + releaseVersion: StockPairedReconcilerRelease, +): VerifiedStockPairedRelease { + const release = releaseVersion === "stock-paired-v1" + ? resolveVerifiedStockPairedRelease() + : releaseVersion === "stock-paired-v2" + ? resolveVerifiedStockPairedV2Release() + : resolveVerifiedStockPairedV3Release(); + if (!release) throw new Error(`${releaseVersion} fixture is unavailable`); + return release; +} + +function encodedEvent( + event: AbiEvent, + args: Readonly>, +): Readonly<{ topics: readonly Hex[]; data: Hex }> { + const topics = encodeEventTopics({ + abi: [event], + eventName: event.name, + args, + }) as readonly Hex[]; + const nonIndexed = event.inputs.filter( + (input) => !("indexed" in input) || input.indexed !== true, + ) as readonly AbiParameter[]; + return Object.freeze({ + topics: Object.freeze(topics), + data: encodeAbiParameters( + nonIndexed, + nonIndexed.map((input) => args[input.name!]), + ), + }); +} + +function log(input: Readonly<{ + address: Address; + event: AbiEvent; + args: Readonly>; + blockNumber: bigint; + logIndex: number; +}>): ExactBlockRpcLog { + const encoded = encodedEvent(input.event, input.args); + return Object.freeze({ + address: input.address, + blockNumber: input.blockNumber, + blockHash: BLOCK_HASH, + transactionHash: TRANSACTION_HASH, + transactionIndex: 2, + logIndex: input.logIndex, + topics: encoded.topics, + data: encoded.data, + }); +} + +function result( + abi: Abi, + functionName: string, + value: unknown, +): Hex { + return encodeFunctionResult({ + abi, + functionName, + result: value, + } as never); +} + +function poolIdentity( + token: Address, + quoteAsset: Address, + hook: Address, +) { + const quoteIsCurrency0 = BigInt(quoteAsset) < BigInt(token); + const currency0 = quoteIsCurrency0 ? quoteAsset : token; + const currency1 = quoteIsCurrency0 ? token : quoteAsset; + return Object.freeze({ + quoteIsCurrency0, + poolId: keccak256(encodeAbiParameters(poolKeyParameters, [ + currency0, + currency1, + 0, + 200, + hook, + ])) as HexBytes32, + }); +} + +function topicsMatch( + logTopics: readonly Hex[], + requested: readonly (Hex | readonly Hex[] | null)[] | undefined, +): boolean { + if (!requested) return true; + return requested.every((filter, index) => { + if (filter === null) return true; + const actual = (logTopics[index] ?? "").toLowerCase(); + return Array.isArray(filter) + ? filter.some((candidate) => candidate.toLowerCase() === actual) + : (filter as Hex).toLowerCase() === actual; + }); +} + +export function stockPairedReconcilerRouteFixture( + releaseVersion: StockPairedReconcilerRelease, + options: Readonly<{ + mutation?: StockPairedReconcilerFixtureMutation; + feeGrossQuote?: bigint; + feeTotalQuote?: bigint; + }> = {}, +): StockPairedReconcilerFixture { + const release = configuredRelease(releaseVersion); + const blockNumber = BigInt(release.startBlock); + const quoteAsset = getStockPairedQuoteAssetsForRelease(release)[0]!.address; + const pool = poolIdentity(TOKEN, quoteAsset, release.addresses.feeHook); + const initialTick = getStockPairedExpectedInitialTickForRelease( + release, + quoteAsset, + pool.quoteIsCurrency0, + ); + if (initialTick === null) throw new Error("Fixture tick is unavailable"); + const tickLower = pool.quoteIsCurrency0 ? -887_200 : initialTick; + const tickUpper = pool.quoteIsCurrency0 ? initialTick : 887_200; + const rewardConfigurationHash = keccak256(encodeAbiParameters( + rewardConfigurationParameters, + [ + 1n, + REWARD_VAULT, + release.addresses.feeHook, + release.officialDependencies.poolManager.address, + quoteAsset, + pool.poolId, + [CREATOR], + [10_000], + ], + )) as HexBytes32; + const grossQuote = options.feeGrossQuote ?? FEE_GROSS_QUOTE; + const totalFee = options.feeTotalQuote ?? ( + grossQuote * BigInt(STOCK_PAIRED_TOTAL_SWAP_FEE_BPS) / 10_000n + ); + const expectedLauncherFee = grossQuote * + BigInt(STOCK_PAIRED_PROGRAMMABLE_FEE_BPS) / 10_000n; + const launcherFee = expectedLauncherFee > totalFee + ? totalFee + : expectedLauncherFee; + const creatorFee = totalFee - launcherFee; + const companionLaunchHash = options.mutation === "companion-launch-hash" + ? ZERO_BYTES32 + : LAUNCH_HASH; + + const logs = [ + log({ + address: release.addresses.feeSplitVaultFactory, + event: vaultDeployedEvent, + args: { + vault: REWARD_VAULT, + feeHook: release.addresses.feeHook, + poolId: pool.poolId, + quoteAsset, + }, + blockNumber, + logIndex: 0, + }), + log({ + address: release.addresses.feeHook, + event: poolRegisteredEvent, + args: { + poolId: pool.poolId, + token: TOKEN, + quoteAsset, + rewardVault: REWARD_VAULT, + registrar: release.addresses.launcher, + quoteIsCurrency0: pool.quoteIsCurrency0, + rewardConfigurationHash, + quoteConfigurationHash: QUOTE_CONFIGURATION_HASH, + }, + blockNumber, + logIndex: 1, + }), + log({ + address: release.addresses.feeHook, + event: feeDisclosureEvent, + args: { + poolId: pool.poolId, + token: TOKEN, + quoteAsset, + rewardVault: REWARD_VAULT, + buySwapFeeBps: STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + sellSwapFeeBps: STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + creatorFeeBps: STOCK_PAIRED_CREATOR_FEE_BPS, + launcherFeeBps: STOCK_PAIRED_PROGRAMMABLE_FEE_BPS, + transferTaxBps: 0, + lpFeePips: 0, + }, + blockNumber, + logIndex: 2, + }), + log({ + address: release.addresses.launcher, + event: liquidityEvent, + args: { + token: TOKEN, + quoteAsset, + totalSupply: TOTAL_SUPPLY, + tokenLiquidityAmount: TOKEN_LIQUIDITY, + lockedTokenDust: LOCKED_DUST, + initialTick, + tickLower, + tickUpper, + lpFeePips: 0, + launchHash: companionLaunchHash, + }, + blockNumber, + logIndex: 3, + }), + log({ + address: release.addresses.launcher, + event: initialBuyEvent, + args: { + deployer: release.addresses.ethLaunchCoordinator, + token: TOKEN, + quoteAsset, + poolId: pool.poolId, + quoteAmount: INITIAL_BUY_QUOTE, + tokenAmount: INITIAL_BUY_TOKEN, + launchHash: LAUNCH_HASH, + }, + blockNumber, + logIndex: 4, + }), + log({ + address: release.addresses.launcher, + event: launchedEvent, + args: { + deployer: release.addresses.ethLaunchCoordinator, + token: TOKEN, + quoteAsset, + poolId: pool.poolId, + rewardVault: REWARD_VAULT, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: 42n, + launchHash: LAUNCH_HASH, + }, + blockNumber, + logIndex: 5, + }), + log({ + address: release.addresses.ethLaunchCoordinator, + event: ethLaunchEvent, + args: { + creator: CREATOR, + token: TOKEN, + quoteAsset, + initialBuyEthAmount: INITIAL_BUY_ETH, + initialBuyQuoteAmount: INITIAL_BUY_QUOTE, + initialBuyTokenAmount: INITIAL_BUY_TOKEN, + launchHash: LAUNCH_HASH, + }, + blockNumber, + logIndex: 6, + }), + log({ + address: release.addresses.feeHook, + event: feeAccruedEvent, + args: { + poolId: pool.poolId, + swapSender: CREATOR, + quoteAsset, + isBuy: true, + grossQuoteAmount: grossQuote, + creatorFee, + launcherFee, + }, + blockNumber, + logIndex: 7, + }), + log({ + address: release.officialDependencies.poolManager.address, + event: swapEvent, + args: { + id: pool.poolId, + sender: release.addresses.launcher, + amount0: pool.quoteIsCurrency0 ? -1_000_000n : 1_000n, + amount1: pool.quoteIsCurrency0 ? 1_000n : -1_000_000n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: ACTIVE_LIQUIDITY, + tick: initialTick, + fee: 0, + }, + blockNumber, + logIndex: 8, + }), + ] as const; + + const transactionInput = encodeFunctionData({ + abi: stockPairedEthLaunchCoordinatorAbi, + functionName: "launch", + args: [{ + minimumQuoteAmountOut: INITIAL_BUY_QUOTE - 1n, + minimumInitialTokenOut: INITIAL_BUY_TOKEN - 1n, + deadline: 2_000_000_000n, + launch: { + name: "Stock Fixture", + symbol: "STOCK", + quoteAsset, + initialBuyQuoteAmount: 0n, + creatorSalt: CREATOR_SALT, + metadata: { + description: "Exact block Stock-Paired fixture", + website: "https://programmable.family", + image: "https://programmable.family/fixture.png", + extraData: "0x1234", + }, + rewardBeneficiaries: [CREATOR], + rewardSharesBps: [10_000], + }, + }], + }); + const transaction: ExactBlockRpcTransaction = Object.freeze({ + transactionHash: TRANSACTION_HASH, + blockNumber, + blockHash: BLOCK_HASH, + transactionIndex: 2, + from: options.mutation === "transaction-provenance" + ? getAddress("0xa111111111111111111111111111111111111111") + : CREATOR, + to: release.addresses.ethLaunchCoordinator, + input: transactionInput, + value: INITIAL_BUY_ETH, + }); + const receiptLogs = logs.map((entry, receiptLogIndex) => Object.freeze({ + ...entry, + receiptLogIndex, + })).filter((_entry, index) => + options.mutation !== "receipt-provenance" || index !== 1 + ); + const receipt: ExactBlockRpcReceipt = Object.freeze({ + transactionHash: TRANSACTION_HASH, + blockNumber, + blockHash: BLOCK_HASH, + transactionIndex: 2, + status: 1n, + logs: Object.freeze(receiptLogs), + }); + + const quoteConfigurationResult = options.mutation === "quote-configuration" + ? ZERO_BYTES32 + : QUOTE_CONFIGURATION_HASH; + const forwarderConfigurationResult = + options.mutation === "forwarder-provenance" + ? ZERO_BYTES32 + : FORWARDER_CONFIGURATION_HASH; + const callResults = Object.freeze([ + result(uerc20ReadAbi, "name", "Stock Fixture"), + result(uerc20ReadAbi, "symbol", "STOCK"), + result(uerc20ReadAbi, "decimals", 18), + result(uerc20ReadAbi, "totalSupply", TOTAL_SUPPLY), + result(uerc20ReadAbi, "creator", release.addresses.launcher), + result(uerc20ReadAbi, "metadata", [ + "Exact block Stock-Paired fixture", + "https://programmable.family", + "https://programmable.family/fixture.png", + "0x1234", + ]), + result(stateViewReadAbi, "getSlot0", [ + SQRT_PRICE_X96, + initialTick, + 0, + 0, + ]), + result(stateViewReadAbi, "getLiquidity", ACTIVE_LIQUIDITY), + result(stockPairedHookAbi, "feeDisclosure", [ + quoteAsset, + TOKEN, + STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + STOCK_PAIRED_CREATOR_FEE_BPS, + STOCK_PAIRED_PROGRAMMABLE_FEE_BPS, + 0, + 0, + REWARD_VAULT, + ]), + result(stockPairedHookAbi, "poolFeeConfig", [ + quoteAsset, + TOKEN, + REWARD_VAULT, + release.addresses.launcher, + pool.quoteIsCurrency0, + true, + creatorFee, + ]), + result(stockPairedEthLaunchCoordinatorAbi, "predictTokenAddress", [ + TOKEN, + EFFECTIVE_GRAFFITI, + ]), + result(launcherStateAbi, "launchHashOf", LAUNCH_HASH), + result(launcherStateAbi, "rewardVaultOf", REWARD_VAULT), + result(launcherStateAbi, "quoteAssetOf", quoteAsset), + result(rewardVaultFactoryStateAbi, "isFactoryVault", true), + result( + rewardVaultFactoryStateAbi, + "configurationHashOf", + rewardConfigurationHash, + ), + result(stockFeeSplitVaultAbi, "feeHook", release.addresses.feeHook), + result(stockFeeSplitVaultAbi, "poolId", pool.poolId), + result(stockFeeSplitVaultAbi, "quoteAsset", quoteAsset), + result(stockFeeSplitVaultAbi, "configurationHash", rewardConfigurationHash), + result(stockFeeSplitVaultAbi, "beneficiaryCount", 1n), + result(stockFeeSplitVaultAbi, "totalCreatorFeesReceived", 0n), + result(stockFeeSplitVaultAbi, "totalCreatorFeesClaimed", 0n), + result(stockQuoteRegistryAbi, "isSupported", true), + result( + stockQuoteRegistryAbi, + "assertAssetReady", + quoteConfigurationResult, + ), + result(positionForwarderFactoryStateAbi, "isFactoryForwarder", true), + result( + positionForwarderFactoryStateAbi, + "configurationHashOf", + forwarderConfigurationResult, + ), + ]); + + const runtimeHashes = new Map(); + for (const [label, expectedHash] of Object.entries( + release.runtimeCodeHashes, + )) { + runtimeHashes.set( + release.addresses[label as keyof typeof release.addresses].toLowerCase(), + expectedHash as HexBytes32, + ); + } + for (const dependency of Object.values(release.officialDependencies)) { + runtimeHashes.set( + dependency.address.toLowerCase(), + dependency.runtimeCodeHash as HexBytes32, + ); + } + runtimeHashes.set( + release.issuerRuntime.beacon.toLowerCase(), + release.issuerRuntime.beaconRuntimeCodeHash as HexBytes32, + ); + runtimeHashes.set( + release.issuerRuntime.implementation.toLowerCase(), + release.issuerRuntime.implementationRuntimeCodeHash as HexBytes32, + ); + if ( + release.issuerRuntime.gmTokenManager && + release.issuerRuntime.gmTokenManagerRuntimeCodeHash + ) { + runtimeHashes.set( + release.issuerRuntime.gmTokenManager.toLowerCase(), + release.issuerRuntime.gmTokenManagerRuntimeCodeHash as HexBytes32, + ); + } + runtimeHashes.set( + quoteAsset.toLowerCase(), + release.issuerRuntime.tokenRuntimeCodeHash as HexBytes32, + ); + if (options.mutation === "runtime") { + runtimeHashes.set( + release.addresses.launcher.toLowerCase(), + ZERO_BYTES32, + ); + } + + const codeBlockHashes: HexBytes32[] = []; + const callBlockHashes: HexBytes32[] = []; + const timestampExpectedHashes: Array = []; + const rpc: ExactBlockRpcClient = Object.freeze({ + endpointCommitment: ENDPOINT_COMMITMENT, + endpointOriginCommitment: ENDPOINT_ORIGIN_COMMITMENT, + requestCount: () => 0, + logicalRequestCount: () => 0, + createPartitionClient: () => rpc, + assertCheckpoint: async ({ blockHash }) => { + if (blockHash !== BLOCK_HASH) throw new Error("checkpoint hash mismatch"); + return 1_700_000_000n; + }, + call: async () => { + throw new Error("Unexpected single call"); + }, + callMany: async (input: { + calls: readonly ExactBlockRpcCall[]; + blockHash: HexBytes32; + }) => { + callBlockHashes.push(input.blockHash); + if (input.calls.length !== callResults.length) { + throw new Error("Unexpected call cardinality"); + } + return callResults; + }, + getCodeHash: async ({ address, blockHash }) => { + codeBlockHashes.push(blockHash); + const runtimeHash = runtimeHashes.get(address.toLowerCase()); + if (!runtimeHash) throw new Error(`Missing runtime ${address}`); + return runtimeHash; + }, + getLogs: async (input) => { + const requestedAddresses = new Set( + (Array.isArray(input.addresses) ? input.addresses : [input.addresses]) + .map((address) => address.toLowerCase()), + ); + return Object.freeze(logs.filter((entry) => + requestedAddresses.has(entry.address.toLowerCase()) && + entry.blockNumber >= input.fromBlock && + entry.blockNumber <= input.toBlock && + topicsMatch(entry.topics, input.topics) + )); + }, + getBlockTimestamp: async ({ blockNumber: candidate, expectedHash }) => { + timestampExpectedHashes.push(expectedHash); + if (candidate !== blockNumber) throw new Error("block number mismatch"); + return 1_700_000_000n; + }, + getBlockTimestamps: async ({ blocks }) => Object.freeze(blocks.map((block) => { + timestampExpectedHashes.push(block.expectedHash); + if (block.blockNumber !== blockNumber) { + throw new Error("block number mismatch"); + } + return 1_700_000_000n; + })), + getTransactionReceipt: async () => receipt, + getTransactionReceipts: async ({ receipts: bindings }) => { + if (bindings.length !== 1) throw new Error("receipt binding mismatch"); + return Object.freeze([receipt]); + }, + getTransaction: async () => transaction, + getTransactions: async ({ transactions: bindings }) => { + if (bindings.length !== 1) throw new Error("transaction binding mismatch"); + return Object.freeze([transaction]); + }, + }); + const contract: ReconcilerPreParityContract = Object.freeze({ + chainId: "1", + releaseId: releaseVersion, + modelId: "stock-paired", + sourceGroup: "ethereum-mainnet", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "8", + reorgGeneration: "2", + checkpointBlockNumber: blockNumber.toString(), + checkpointBlockHash: BLOCK_HASH, + routeKeys: STOCK_PAIRED_RECONCILER_ROUTE_KEYS, + routeContract: { exact: true }, + projectionContract: { exact: true }, + currentEntities: [{ + entityKind: "launch", + entityKey: TOKEN.toLowerCase(), + }], + }); + + return Object.freeze({ + release, + contract, + blockNumber, + blockHash: BLOCK_HASH, + rpc, + expected: Object.freeze({ + token: TOKEN, + creator: CREATOR, + quoteAsset, + poolId: pool.poolId, + grossQuoteRaw: grossQuote.toString(), + }), + observations: Object.freeze({ + codeBlockHashes, + callBlockHashes, + timestampExpectedHashes, + }), + }); +} + +export function stockPairedLargeCorpusFixture( + releaseVersion: StockPairedReconcilerRelease, + launchCount: number, +): StockPairedLargeCorpusFixture { + if (!Number.isSafeInteger(launchCount) || launchCount < 1) { + throw new Error("invalid launch count"); + } + const release = configuredRelease(releaseVersion); + const quoteAsset = getStockPairedQuoteAssetsForRelease(release)[0]!.address; + const startBlock = BigInt(release.startBlock); + const blockNumber = startBlock + BigInt(launchCount - 1); + const checkpointHash = `0x${"fa".repeat(32)}` as HexBytes32; + const indexedAddress = (domain: number, index: number) => getAddress( + `0x${((BigInt(domain) << 152n) | BigInt(index + 1)).toString(16).padStart(40, "0")}`, + ); + const indexedBytes32 = (domain: number, index: number) => + `0x${((BigInt(domain) << 248n) | BigInt(index + 1)).toString(16).padStart(64, "0")}` as HexBytes32; + const allLogs: ExactBlockRpcLog[] = []; + const transactions = new Map(); + const receipts = new Map(); + const blockHashes = new Map(); + const callResults = new Map(); + const tokens: Address[] = []; + + const registerCallResult = ( + to: Address, + abi: Abi, + functionName: string, + args: readonly unknown[], + value: unknown, + ) => { + const callData = encodeFunctionData({ abi, functionName, args } as never); + callResults.set( + `${to.toLowerCase()}:${callData.toLowerCase()}`, + result(abi, functionName, value), + ); + }; + const dynamicLog = (input: Readonly<{ + address: Address; + event: AbiEvent; + args: Readonly>; + launchBlock: bigint; + blockHash: HexBytes32; + transactionHash: HexBytes32; + logIndex: number; + }>): ExactBlockRpcLog => { + const encoded = encodedEvent(input.event, input.args); + return Object.freeze({ + address: input.address, + blockNumber: input.launchBlock, + blockHash: input.blockHash, + transactionHash: input.transactionHash, + transactionIndex: 2, + logIndex: input.logIndex, + topics: encoded.topics, + data: encoded.data, + }); + }; + + for (let index = 0; index < launchCount; index += 1) { + const token = indexedAddress(0xf1, index); + const rewardVault = indexedAddress(0xe1, index); + const launchBlock = startBlock + BigInt(index); + const launchBlockHash = indexedBytes32(0x72, index); + const transactionHash = indexedBytes32(0x71, index); + const launchHash = indexedBytes32(0x73, index); + const creatorSalt = indexedBytes32(0x74, index); + const name = `Stock Fixture ${index + 1}`; + const symbol = `S${index + 1}`; + const pool = poolIdentity(token, quoteAsset, release.addresses.feeHook); + const initialTick = getStockPairedExpectedInitialTickForRelease( + release, + quoteAsset, + pool.quoteIsCurrency0, + ); + if (initialTick === null) throw new Error("fixture tick unavailable"); + const tickLower = pool.quoteIsCurrency0 ? -887_200 : initialTick; + const tickUpper = pool.quoteIsCurrency0 ? initialTick : 887_200; + const rewardConfigurationHash = keccak256(encodeAbiParameters( + rewardConfigurationParameters, + [ + 1n, + rewardVault, + release.addresses.feeHook, + release.officialDependencies.poolManager.address, + quoteAsset, + pool.poolId, + [CREATOR], + [10_000], + ], + )) as HexBytes32; + const totalFee = FEE_GROSS_QUOTE * + BigInt(STOCK_PAIRED_TOTAL_SWAP_FEE_BPS) / 10_000n; + const expectedLauncherFee = FEE_GROSS_QUOTE * + BigInt(STOCK_PAIRED_PROGRAMMABLE_FEE_BPS) / 10_000n; + const launcherFee = expectedLauncherFee > totalFee + ? totalFee + : expectedLauncherFee; + const creatorFee = totalFee - launcherFee; + tokens.push(token); + blockHashes.set(launchBlock.toString(), launchBlockHash); + + const launchLogs = [ + dynamicLog({ + address: release.addresses.feeSplitVaultFactory, + event: vaultDeployedEvent, + args: { + vault: rewardVault, + feeHook: release.addresses.feeHook, + poolId: pool.poolId, + quoteAsset, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 0, + }), + dynamicLog({ + address: release.addresses.feeHook, + event: poolRegisteredEvent, + args: { + poolId: pool.poolId, + token, + quoteAsset, + rewardVault, + registrar: release.addresses.launcher, + quoteIsCurrency0: pool.quoteIsCurrency0, + rewardConfigurationHash, + quoteConfigurationHash: QUOTE_CONFIGURATION_HASH, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 1, + }), + dynamicLog({ + address: release.addresses.feeHook, + event: feeDisclosureEvent, + args: { + poolId: pool.poolId, + token, + quoteAsset, + rewardVault, + buySwapFeeBps: STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + sellSwapFeeBps: STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + creatorFeeBps: STOCK_PAIRED_CREATOR_FEE_BPS, + launcherFeeBps: STOCK_PAIRED_PROGRAMMABLE_FEE_BPS, + transferTaxBps: 0, + lpFeePips: 0, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 2, + }), + dynamicLog({ + address: release.addresses.launcher, + event: liquidityEvent, + args: { + token, + quoteAsset, + totalSupply: TOTAL_SUPPLY, + tokenLiquidityAmount: TOKEN_LIQUIDITY, + lockedTokenDust: LOCKED_DUST, + initialTick, + tickLower, + tickUpper, + lpFeePips: 0, + launchHash, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 3, + }), + dynamicLog({ + address: release.addresses.launcher, + event: initialBuyEvent, + args: { + deployer: release.addresses.ethLaunchCoordinator, + token, + quoteAsset, + poolId: pool.poolId, + quoteAmount: INITIAL_BUY_QUOTE, + tokenAmount: INITIAL_BUY_TOKEN, + launchHash, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 4, + }), + dynamicLog({ + address: release.addresses.launcher, + event: launchedEvent, + args: { + deployer: release.addresses.ethLaunchCoordinator, + token, + quoteAsset, + poolId: pool.poolId, + rewardVault, + positionRecipient: POSITION_RECIPIENT, + positionTokenId: BigInt(index + 1), + launchHash, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 5, + }), + dynamicLog({ + address: release.addresses.ethLaunchCoordinator, + event: ethLaunchEvent, + args: { + creator: CREATOR, + token, + quoteAsset, + initialBuyEthAmount: INITIAL_BUY_ETH, + initialBuyQuoteAmount: INITIAL_BUY_QUOTE, + initialBuyTokenAmount: INITIAL_BUY_TOKEN, + launchHash, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 6, + }), + dynamicLog({ + address: release.addresses.feeHook, + event: feeAccruedEvent, + args: { + poolId: pool.poolId, + swapSender: CREATOR, + quoteAsset, + isBuy: true, + grossQuoteAmount: FEE_GROSS_QUOTE, + creatorFee, + launcherFee, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 7, + }), + dynamicLog({ + address: release.officialDependencies.poolManager.address, + event: swapEvent, + args: { + id: pool.poolId, + sender: release.addresses.launcher, + amount0: pool.quoteIsCurrency0 ? -1_000_000n : 1_000n, + amount1: pool.quoteIsCurrency0 ? 1_000n : -1_000_000n, + sqrtPriceX96: SQRT_PRICE_X96, + liquidity: ACTIVE_LIQUIDITY, + tick: initialTick, + fee: 0, + }, + launchBlock, + blockHash: launchBlockHash, + transactionHash, + logIndex: 8, + }), + ] as const; + allLogs.push(...launchLogs); + + const transactionInput = encodeFunctionData({ + abi: stockPairedEthLaunchCoordinatorAbi, + functionName: "launch", + args: [{ + minimumQuoteAmountOut: INITIAL_BUY_QUOTE - 1n, + minimumInitialTokenOut: INITIAL_BUY_TOKEN - 1n, + deadline: 2_000_000_000n, + launch: { + name, + symbol, + quoteAsset, + initialBuyQuoteAmount: 0n, + creatorSalt, + metadata: { + description: "Exact block Stock-Paired fixture", + website: "https://programmable.family", + image: "https://programmable.family/fixture.png", + extraData: "0x1234", + }, + rewardBeneficiaries: [CREATOR], + rewardSharesBps: [10_000], + }, + }], + }); + transactions.set(transactionHash.toLowerCase(), Object.freeze({ + transactionHash, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionIndex: 2, + from: CREATOR, + to: release.addresses.ethLaunchCoordinator, + input: transactionInput, + value: INITIAL_BUY_ETH, + })); + receipts.set(transactionHash.toLowerCase(), Object.freeze({ + transactionHash, + blockNumber: launchBlock, + blockHash: launchBlockHash, + transactionIndex: 2, + status: 1n, + logs: Object.freeze(launchLogs.map((entry, receiptLogIndex) => + Object.freeze({ ...entry, receiptLogIndex }) + )), + })); + + registerCallResult(token, uerc20ReadAbi, "name", [], name); + registerCallResult(token, uerc20ReadAbi, "symbol", [], symbol); + registerCallResult(token, uerc20ReadAbi, "decimals", [], 18); + registerCallResult(token, uerc20ReadAbi, "totalSupply", [], TOTAL_SUPPLY); + registerCallResult( + token, + uerc20ReadAbi, + "creator", + [], + release.addresses.launcher, + ); + registerCallResult(token, uerc20ReadAbi, "metadata", [], [ + "Exact block Stock-Paired fixture", + "https://programmable.family", + "https://programmable.family/fixture.png", + "0x1234", + ]); + registerCallResult( + release.officialDependencies.stateView.address, + stateViewReadAbi, + "getSlot0", + [pool.poolId], + [SQRT_PRICE_X96, initialTick, 0, 0], + ); + registerCallResult( + release.officialDependencies.stateView.address, + stateViewReadAbi, + "getLiquidity", + [pool.poolId], + ACTIVE_LIQUIDITY, + ); + registerCallResult( + release.addresses.feeHook, + stockPairedHookAbi, + "feeDisclosure", + [pool.poolId], + [ + quoteAsset, + token, + STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + STOCK_PAIRED_TOTAL_SWAP_FEE_BPS, + STOCK_PAIRED_CREATOR_FEE_BPS, + STOCK_PAIRED_PROGRAMMABLE_FEE_BPS, + 0, + 0, + rewardVault, + ], + ); + registerCallResult( + release.addresses.feeHook, + stockPairedHookAbi, + "poolFeeConfig", + [pool.poolId], + [ + quoteAsset, + token, + rewardVault, + release.addresses.launcher, + pool.quoteIsCurrency0, + true, + creatorFee, + ], + ); + registerCallResult( + release.addresses.ethLaunchCoordinator, + stockPairedEthLaunchCoordinatorAbi, + "predictTokenAddress", + [name, symbol, CREATOR, creatorSalt], + [token, EFFECTIVE_GRAFFITI], + ); + registerCallResult( + release.addresses.launcher, + launcherStateAbi, + "launchHashOf", + [token], + launchHash, + ); + registerCallResult( + release.addresses.launcher, + launcherStateAbi, + "rewardVaultOf", + [token], + rewardVault, + ); + registerCallResult( + release.addresses.launcher, + launcherStateAbi, + "quoteAssetOf", + [token], + quoteAsset, + ); + registerCallResult( + release.addresses.feeSplitVaultFactory, + rewardVaultFactoryStateAbi, + "isFactoryVault", + [rewardVault], + true, + ); + registerCallResult( + release.addresses.feeSplitVaultFactory, + rewardVaultFactoryStateAbi, + "configurationHashOf", + [rewardVault], + rewardConfigurationHash, + ); + registerCallResult( + rewardVault, + stockFeeSplitVaultAbi, + "feeHook", + [], + release.addresses.feeHook, + ); + registerCallResult( + rewardVault, + stockFeeSplitVaultAbi, + "poolId", + [], + pool.poolId, + ); + registerCallResult( + rewardVault, + stockFeeSplitVaultAbi, + "quoteAsset", + [], + quoteAsset, + ); + registerCallResult( + rewardVault, + stockFeeSplitVaultAbi, + "configurationHash", + [], + rewardConfigurationHash, + ); + registerCallResult( + rewardVault, + stockFeeSplitVaultAbi, + "beneficiaryCount", + [], + 1n, + ); + registerCallResult( + rewardVault, + stockFeeSplitVaultAbi, + "totalCreatorFeesReceived", + [], + 0n, + ); + registerCallResult( + rewardVault, + stockFeeSplitVaultAbi, + "totalCreatorFeesClaimed", + [], + 0n, + ); + registerCallResult( + release.addresses.quoteRegistry, + stockQuoteRegistryAbi, + "isSupported", + [quoteAsset], + true, + ); + registerCallResult( + release.addresses.quoteRegistry, + stockQuoteRegistryAbi, + "assertAssetReady", + [quoteAsset], + QUOTE_CONFIGURATION_HASH, + ); + registerCallResult( + release.addresses.positionForwarderFactory, + positionForwarderFactoryStateAbi, + "isFactoryForwarder", + [POSITION_RECIPIENT], + true, + ); + registerCallResult( + release.addresses.positionForwarderFactory, + positionForwarderFactoryStateAbi, + "configurationHashOf", + [POSITION_RECIPIENT], + FORWARDER_CONFIGURATION_HASH, + ); + } + + const runtimeHashes = new Map(); + for (const [label, expectedHash] of Object.entries(release.runtimeCodeHashes)) { + runtimeHashes.set( + release.addresses[label as keyof typeof release.addresses].toLowerCase(), + expectedHash as HexBytes32, + ); + } + for (const dependency of Object.values(release.officialDependencies)) { + runtimeHashes.set( + dependency.address.toLowerCase(), + dependency.runtimeCodeHash as HexBytes32, + ); + } + runtimeHashes.set( + release.issuerRuntime.beacon.toLowerCase(), + release.issuerRuntime.beaconRuntimeCodeHash as HexBytes32, + ); + runtimeHashes.set( + release.issuerRuntime.implementation.toLowerCase(), + release.issuerRuntime.implementationRuntimeCodeHash as HexBytes32, + ); + if ( + release.issuerRuntime.gmTokenManager && + release.issuerRuntime.gmTokenManagerRuntimeCodeHash + ) { + runtimeHashes.set( + release.issuerRuntime.gmTokenManager.toLowerCase(), + release.issuerRuntime.gmTokenManagerRuntimeCodeHash as HexBytes32, + ); + } + runtimeHashes.set( + quoteAsset.toLowerCase(), + release.issuerRuntime.tokenRuntimeCodeHash as HexBytes32, + ); + + const budget = { physical: 0, logical: 0 }; + const corpusPageSizes: number[] = []; + const timestampBatchSizes: number[] = []; + const charge = (physical: number, logical: number) => { + budget.physical += physical; + budget.logical += logical; + if (budget.physical > 512 || budget.logical > 512 * 32) { + throw new Error( + `global request budget exceeded: ${budget.physical}/${budget.logical}`, + ); + } + }; + const batchCharge = (logical: number) => + charge(Math.ceil(logical / 32), logical); + const rpcAtDepth = (depth: number): ExactBlockRpcClient => Object.freeze({ + endpointCommitment: ENDPOINT_COMMITMENT, + endpointOriginCommitment: ENDPOINT_ORIGIN_COMMITMENT, + requestCount: () => budget.physical, + logicalRequestCount: () => budget.logical, + createPartitionClient: (binding) => { + if (depth === 0) { + corpusPageSizes.push(binding.endIndexExclusive - binding.startIndex); + } + return rpcAtDepth(depth + 1); + }, + assertCheckpoint: async ({ blockHash }) => { + charge(1, 1); + if (blockHash !== checkpointHash) throw new Error("checkpoint mismatch"); + return 1_700_000_000n; + }, + call: async () => { + throw new Error("unexpected single call"); + }, + callMany: async ({ calls, blockHash }) => { + if (blockHash !== checkpointHash) throw new Error("call hash mismatch"); + batchCharge(calls.length); + return Object.freeze(calls.map((call) => { + const resolved = callResults.get( + `${call.to.toLowerCase()}:${call.data.toLowerCase()}`, + ); + if (!resolved) throw new Error(`missing call result ${call.to}:${call.data}`); + return resolved; + })); + }, + getCodeHash: async ({ address, blockHash }) => { + if (blockHash !== checkpointHash) throw new Error("code hash mismatch"); + charge(1, 1); + const resolved = runtimeHashes.get(address.toLowerCase()); + if (!resolved) throw new Error(`missing runtime ${address}`); + return resolved; + }, + getLogs: async ({ addresses, topics, fromBlock, toBlock }) => { + charge(1, 1); + const requestedAddresses = new Set( + (Array.isArray(addresses) ? addresses : [addresses]) + .map((address) => address.toLowerCase()), + ); + return Object.freeze(allLogs.filter((entry) => + requestedAddresses.has(entry.address.toLowerCase()) && + entry.blockNumber >= fromBlock && + entry.blockNumber <= toBlock && + topicsMatch(entry.topics, topics) + )); + }, + getBlockTimestamp: async ({ blockNumber: candidate, expectedHash }) => { + charge(1, 1); + if (blockHashes.get(candidate.toString()) !== expectedHash) { + throw new Error("timestamp binding mismatch"); + } + return 1_700_000_000n + candidate - startBlock; + }, + getBlockTimestamps: async ({ blocks }) => { + batchCharge(blocks.length); + timestampBatchSizes.push(blocks.length); + return Object.freeze(blocks.map(({ blockNumber: candidate, expectedHash }) => { + if (blockHashes.get(candidate.toString()) !== expectedHash) { + throw new Error("timestamp binding mismatch"); + } + return 1_700_000_000n + candidate - startBlock; + })); + }, + getTransactionReceipt: async ({ transactionHash }) => { + charge(1, 1); + const resolved = receipts.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing receipt"); + return resolved; + }, + getTransactionReceipts: async ({ receipts: bindings }) => { + batchCharge(bindings.length); + return Object.freeze(bindings.map(({ transactionHash }) => { + const resolved = receipts.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing receipt"); + return resolved; + })); + }, + getTransaction: async ({ transactionHash }) => { + charge(1, 1); + const resolved = transactions.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing transaction"); + return resolved; + }, + getTransactions: async ({ transactions: bindings }) => { + batchCharge(bindings.length); + return Object.freeze(bindings.map(({ transactionHash }) => { + const resolved = transactions.get(transactionHash.toLowerCase()); + if (!resolved) throw new Error("missing transaction"); + return resolved; + })); + }, + }); + const contract: ReconcilerPreParityContract = Object.freeze({ + chainId: "1", + releaseId: releaseVersion, + modelId: "stock-paired", + sourceGroup: "ethereum-mainnet", + projectorVersion: "projector-v1", + epochId: "10000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "10000000-0000-4000-8000-000000000002", + checkpointGeneration: "8", + reorgGeneration: "2", + checkpointBlockNumber: blockNumber.toString(), + checkpointBlockHash: checkpointHash, + routeKeys: STOCK_PAIRED_RECONCILER_ROUTE_KEYS, + routeContract: { exact: true }, + projectionContract: { exact: true }, + currentEntities: tokens.map((token) => ({ + entityKind: "launch", + entityKey: token.toLowerCase(), + })), + }); + return Object.freeze({ + release, + contract, + blockNumber, + blockHash: checkpointHash, + rpc: rpcAtDepth(0), + budget, + corpusPageSizes, + timestampBatchSizes, + }); +} diff --git a/tests/data-pipeline/uniswap.test.ts b/tests/data-pipeline/uniswap.test.ts new file mode 100644 index 00000000..ce7c1290 --- /dev/null +++ b/tests/data-pipeline/uniswap.test.ts @@ -0,0 +1,788 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import * as dataPipelineConfig from "../../lib/data-pipeline/config"; +import { + OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + OFFICIAL_V4_SUBGRAPH_ID, + createUniswapAnalyticsClient, + priceRatiosFromSqrtPriceX96, +} from "../../lib/data-pipeline/uniswap"; + +const POOL_ID = + "0x1a1d489ab64459031dd616d24b823600e804f04164fd47f3c158a6338c77fc42"; +const TOKEN = "0x2222222222222222222222222222222222222222"; +const HOOK = "0x3333333333333333333333333333333333333333"; +const NATIVE = "0x0000000000000000000000000000000000000000"; +const BLOCK_HASH = `0x${"44".repeat(32)}`; + +const POOL_KEY = { + poolId: POOL_ID, + currency0: NATIVE, + currency1: TOKEN, + fee: 0, + tickSpacing: 60, + hooks: HOOK, + token0Decimals: 6, + token1Decimals: 18, +} as const; + +const BLOCK = { + number: "25650000", + hash: BLOCK_HASH, +} as const; + +function meta(overrides: Record = {}) { + return { + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + hasIndexingErrors: false, + block: { + number: BLOCK.number, + hash: BLOCK.hash, + }, + ...overrides, + }; +} + +function pool(overrides: Record = {}) { + return { + id: POOL_ID, + createdAtTimestamp: "1785480000", + createdAtBlockNumber: "25640000", + token0: { id: NATIVE, decimals: "6" }, + token1: { id: TOKEN, decimals: "18" }, + hooks: HOOK, + feeTier: "0", + tickSpacing: "60", + liquidity: "340282366920938463463374607431768211455", + sqrtPrice: "79228162514264337593543950336", + tick: "0", + txCount: "42", + volumeToken0: "12345678901234567890.123456", + volumeToken1: "0.000000000000000001", + volumeUSD: "999999999999999999999999.999999999999999999", + totalValueLockedToken0: "1000000.1", + totalValueLockedToken1: "2000000.2", + totalValueLockedUSD: "3000000.3", + ...overrides, + }; +} + +function json(body: unknown, init?: ResponseInit) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + ...init, + }); +} + +function swap(index: number, timestamp = 100) { + const id = `swap-${index.toString().padStart(4, "0")}`; + const firstPageBoundaryFixture = index === 0; + return { + id, + transaction: { + id: `0x${index.toString(16).padStart(64, "0")}`, + blockNumber: String(25_649_000 + index), + timestamp: String(timestamp), + }, + timestamp: String(timestamp), + pool: { id: POOL_ID }, + sender: NATIVE, + origin: TOKEN, + amount0: firstPageBoundaryFixture + ? "-12345678901234567890.123456" + : "-1", + amount1: firstPageBoundaryFixture ? "0.000000000000000001" : "1", + amountUSD: "1.25", + sqrtPriceX96: "79228162514264337593543950336", + tick: "0", + logIndex: String(index), + }; +} + +function candle(id: string, time: number) { + return { + id, + periodStartUnix: time, + pool: { id: POOL_ID }, + liquidity: "100000000000000000000000000000000000000", + sqrtPrice: "79228162514264337593543950336", + token0Price: "1000000000000", + token1Price: "0.000000000001", + tick: "0", + tvlUSD: "30.5", + volumeToken0: "10.1", + volumeToken1: "20.2", + volumeUSD: "30.3", + feesUSD: "0.1", + txCount: "2", + open: "1", + high: "2", + low: "0.5", + close: "1.5", + }; +} + +function dayCandle(id: string, time: number) { + const value: Record = { ...candle(id, time) }; + delete value.periodStartUnix; + return { ...value, date: time }; +} + +describe("pinned Uniswap v4 analytics adapter", () => { + it("rejects a custom production gateway before it can receive the API key", () => { + const fetcher = vi.fn(async () => json({})); + let thrown: unknown; + vi.stubEnv("VERCEL_ENV", "production"); + try { + createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://graph-gateway.example", + apiKey: "graph-secret", + fetcher, + }); + } catch (error) { + thrown = error; + } finally { + vi.unstubAllEnvs(); + } + + expect(thrown).toMatchObject({ + dependency: "config", + code: "invalid_config", + }); + expect(fetcher).not.toHaveBeenCalled(); + }); + + it("enforces the production gateway boundary even if config policy becomes permissive", () => { + const permissiveConfig = dataPipelineConfig.loadDataPipelineConfig({ + PROGRAMMABLE_UNISWAP_GRAPH_BASE_URL: + "https://graph-gateway.example", + PROGRAMMABLE_UNISWAP_GRAPH_API_KEY: "graph-secret", + }); + const configSpy = vi + .spyOn(dataPipelineConfig, "loadDataPipelineConfig") + .mockReturnValue(permissiveConfig); + const fetcher = vi.fn(async () => json({})); + let thrown: unknown; + vi.stubEnv("VERCEL_ENV", "production"); + try { + createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://graph-gateway.example", + apiKey: "graph-secret", + fetcher, + }); + } catch (error) { + thrown = error; + } finally { + configSpy.mockRestore(); + vi.unstubAllEnvs(); + } + + expect(thrown).toMatchObject({ + dependency: "config", + code: "invalid_config", + }); + expect(fetcher).not.toHaveBeenCalled(); + }); + + it("allows the exact official gateway in production", async () => { + const fetcher = vi.fn(async () => + json({ data: { _meta: meta(), pool: pool() } }), + ); + vi.stubEnv("VERCEL_ENV", "production"); + try { + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher, + }); + await expect( + client.readPoolSnapshot({ poolKey: POOL_KEY, block: BLOCK }), + ).resolves.toMatchObject({ status: "ready" }); + } finally { + vi.unstubAllEnvs(); + } + expect(fetcher).toHaveBeenCalledOnce(); + }); + + it("keeps custom gateways available outside production", async () => { + const fetcher = vi.fn(async (url: string) => { + expect(url).toBe( + `https://graph-gateway.example/api/subgraphs/id/${OFFICIAL_V4_SUBGRAPH_ID}`, + ); + return json({ data: { _meta: meta(), pool: pool() } }); + }); + vi.stubEnv("VERCEL_ENV", "preview"); + try { + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://graph-gateway.example", + apiKey: "graph-secret", + fetcher, + }); + await expect( + client.readPoolSnapshot({ poolKey: POOL_KEY, block: BLOCK }), + ).resolves.toMatchObject({ status: "ready" }); + } finally { + vi.unstubAllEnvs(); + } + expect(fetcher).toHaveBeenCalledOnce(); + }); + + it("uses the fixed subgraph and exact block Pool contract", async () => { + const fetcher = vi.fn(async (url: string, init?: RequestInit) => { + expect(url).toBe( + `https://gateway.thegraph.com/api/subgraphs/id/${OFFICIAL_V4_SUBGRAPH_ID}`, + ); + expect(url).not.toContain("graph-secret"); + expect(init?.headers).toMatchObject({ + authorization: "Bearer graph-secret", + "content-type": "application/json", + }); + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(request.query).toContain("query ProgrammablePoolSnapshot"); + expect(request.query).toContain("subgraphError: deny"); + expect(request.query).toContain("block: { number: $block }"); + expect(request.variables).toEqual({ + poolId: POOL_ID, + block: 25_650_000, + }); + return json({ data: { _meta: meta(), pool: pool() } }); + }); + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher, + }); + + const result = await client.readPoolSnapshot({ + poolKey: POOL_KEY, + block: BLOCK, + }); + + expect(result).toMatchObject({ + status: "ready", + provenance: { + deployment: OFFICIAL_V4_SUBGRAPH_DEPLOYMENT, + blockNumber: BLOCK.number, + blockHash: BLOCK.hash, + }, + data: { + id: POOL_ID, + token0: { id: NATIVE, decimals: 6 }, + token1: { id: TOKEN, decimals: 18 }, + liquidity: "340282366920938463463374607431768211455", + sqrtPriceX96: "79228162514264337593543950336", + marketVolumeToken0: "12345678901234567890.123456", + marketVolumeToken1: "0.000000000000000001", + marketVolumeUsd: "999999999999999999999999.999999999999999999", + }, + }); + expect(result).not.toHaveProperty("data.hookGrossVolume"); + expect(fetcher).toHaveBeenCalledOnce(); + }); + + it.each([ + ["deployment", { deployment: "QmWrong" }], + ["indexing status", { hasIndexingErrors: true }], + [ + "block number", + { block: { number: "25649999", hash: BLOCK.hash } }, + ], + [ + "block hash", + { block: { number: BLOCK.number, hash: `0x${"99".repeat(32)}` } }, + ], + ])("returns market pending on %s mismatch", async (_name, override) => { + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher: async () => + json({ data: { _meta: meta(override), pool: pool() } }), + }); + + await expect( + client.readPoolSnapshot({ poolKey: POOL_KEY, block: BLOCK }), + ).resolves.toEqual({ + status: "pending", + reason: "validation_failed", + }); + }); + + it("rejects PoolKey and returned pool-id mismatches without substituting a pool", async () => { + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher: async () => + json({ + data: { + _meta: meta(), + pool: pool({ id: `0x${"99".repeat(32)}` }), + }, + }), + }); + await expect( + client.readPoolSnapshot({ poolKey: POOL_KEY, block: BLOCK }), + ).resolves.toEqual({ + status: "pending", + reason: "validation_failed", + }); + + await expect( + client.readPoolSnapshot({ + poolKey: { ...POOL_KEY, tickSpacing: 61 }, + block: BLOCK, + }), + ).rejects.toMatchObject({ + code: "invalid_input", + countsTowardCircuit: false, + }); + }); + + it("paginates swaps in exact 250-row pages with id_gt and half-open windows", async () => { + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + expect(request.query).toContain("query ProgrammableSwapPage"); + expect(request.query).toContain("first: 250"); + expect(request.query).toContain("orderBy: id"); + expect(request.query).toContain("id_gt: $cursor"); + expect(request.query).toContain("timestamp_gte: $from"); + expect(request.query).toContain("timestamp_lt: $toExclusive"); + expect(request.query).toContain("block: { hash: $blockHash }"); + expect(request.query).toContain("subgraphError: deny"); + const cursor = String(request.variables.cursor); + if (cursor === "") { + expect(request.variables).toMatchObject({ + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: "100", + toExclusive: "200", + }); + return json({ + data: { + _meta: meta(), + swaps: Array.from({ length: 250 }, (_, index) => swap(index, 100)), + }, + }); + } + expect(cursor).toBe("swap-0249"); + return json({ + data: { _meta: meta(), swaps: [swap(250, 199)] }, + }); + }); + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher, + }); + + const result = await client.readSwaps({ + poolKey: POOL_KEY, + block: BLOCK, + from: "100", + toExclusive: "200", + }); + + expect(result).toMatchObject({ status: "ready" }); + if (result.status !== "ready") throw new Error("expected ready"); + expect(result.data).toHaveLength(251); + expect(result.data[0]).toMatchObject({ + amount0: "-12345678901234567890.123456", + amount1: "0.000000000000000001", + sqrtPriceX96: "79228162514264337593543950336", + marketAmountUsd: "1.25", + }); + expect(fetcher).toHaveBeenCalledTimes(2); + }); + + it("queries hour and day series with the pinned fields and half-open windows", async () => { + const queries: string[] = []; + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + queries.push(request.query); + expect(request.variables).toMatchObject({ + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: 100, + toExclusive: 200, + cursor: "", + }); + if (request.query.includes("PoolHourSeries")) { + return json({ + data: { + _meta: meta(), + poolHourDatas: [candle("hour-1", 100)], + }, + }); + } + return json({ + data: { + _meta: meta(), + poolDayDatas: [ + { ...candle("day-1", 100), date: 100, periodStartUnix: undefined }, + ], + }, + }); + }); + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher, + }); + + await expect( + client.readHourSeries({ + poolKey: POOL_KEY, + block: BLOCK, + from: 100, + toExclusive: 200, + }), + ).resolves.toMatchObject({ status: "ready", data: [{ feesUsd: "0.1" }] }); + await expect( + client.readDaySeries({ + poolKey: POOL_KEY, + block: BLOCK, + from: 100, + toExclusive: 200, + }), + ).resolves.toMatchObject({ status: "ready", data: [{ feesUsd: "0.1" }] }); + expect(queries[0]).toContain("periodStartUnix_lt: $toExclusive"); + expect(queries[1]).toContain("date_lt: $toExclusive"); + expect(queries.every((query) => query.includes("feesUSD"))).toBe(true); + }); + + it("splits swap ranges into contiguous six-hour windows before paginating each window", async () => { + const requests: Array> = []; + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + variables: Record; + }; + requests.push(request.variables); + + if (requests.length === 1) { + return json({ + data: { + _meta: meta(), + swaps: Array.from({ length: 250 }, (_, index) => + swap(index, 100), + ), + }, + }); + } + if (requests.length === 2) { + return json({ data: { _meta: meta(), swaps: [] } }); + } + return json({ data: { _meta: meta(), swaps: [swap(300, 21_700)] } }); + }); + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher, + }); + + const result = await client.readSwaps({ + poolKey: POOL_KEY, + block: BLOCK, + from: "100", + toExclusive: "21705", + }); + + expect(result).toMatchObject({ status: "ready" }); + if (result.status !== "ready") throw new Error("expected ready"); + expect(result.data).toHaveLength(251); + expect(requests).toEqual([ + { + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: "100", + toExclusive: "21700", + cursor: "", + }, + { + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: "100", + toExclusive: "21700", + cursor: "swap-0249", + }, + { + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: "21700", + toExclusive: "21705", + cursor: "", + }, + ]); + }); + + it("splits hour and day ranges at deterministic contiguous half-open boundaries", async () => { + const hourVariables: Array> = []; + const dayVariables: Array> = []; + const fetcher = vi.fn(async (_url: string, init?: RequestInit) => { + const request = JSON.parse(String(init?.body)) as { + query: string; + variables: Record; + }; + if (request.query.includes("PoolHourSeries")) { + hourVariables.push(request.variables); + if (hourVariables.length === 1) { + return json({ + data: { + _meta: meta(), + poolHourDatas: [candle("hour-a", 102), candle("hour-z", 101)], + }, + }); + } + return json({ + data: { + _meta: meta(), + poolHourDatas: [candle("hour-b", 2_678_500)], + }, + }); + } + + dayVariables.push(request.variables); + if (dayVariables.length === 1) { + return json({ + data: { + _meta: meta(), + poolDayDatas: [dayCandle("day-a", 102), dayCandle("day-z", 101)], + }, + }); + } + return json({ + data: { + _meta: meta(), + poolDayDatas: [dayCandle("day-b", 31_622_500)], + }, + }); + }); + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher, + }); + + const hours = await client.readHourSeries({ + poolKey: POOL_KEY, + block: BLOCK, + from: 100, + toExclusive: 2_678_505, + }); + const days = await client.readDaySeries({ + poolKey: POOL_KEY, + block: BLOCK, + from: 100, + toExclusive: 31_622_505, + }); + + expect(hours).toMatchObject({ status: "ready" }); + expect(days).toMatchObject({ status: "ready" }); + if (hours.status !== "ready" || days.status !== "ready") { + throw new Error("expected ready"); + } + expect(hours.data.map((item) => item.id)).toEqual([ + "hour-z", + "hour-a", + "hour-b", + ]); + expect(days.data.map((item) => item.id)).toEqual([ + "day-z", + "day-a", + "day-b", + ]); + expect(hourVariables).toEqual([ + { + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: 100, + toExclusive: 2_678_500, + cursor: "", + }, + { + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: 2_678_500, + toExclusive: 2_678_505, + cursor: "", + }, + ]); + expect(dayVariables).toEqual([ + { + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: 100, + toExclusive: 31_622_500, + cursor: "", + }, + { + poolId: POOL_ID, + blockHash: BLOCK.hash, + from: 31_622_500, + toExclusive: 31_622_505, + cursor: "", + }, + ]); + }); + + it("sorts swaps deterministically after combining all split windows", async () => { + let call = 0; + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher: async () => { + const fixtures = [swap(3, 100), swap(2, 21_700), swap(1, 43_300)]; + const entity = fixtures[call]; + call += 1; + return json({ data: { _meta: meta(), swaps: [entity] } }); + }, + }); + + const result = await client.readSwaps({ + poolKey: POOL_KEY, + block: BLOCK, + from: "100", + toExclusive: "43305", + }); + + expect(result).toMatchObject({ status: "ready" }); + if (result.status !== "ready") throw new Error("expected ready"); + expect(result.data.map((item) => item.id)).toEqual([ + "swap-0001", + "swap-0002", + "swap-0003", + ]); + }); + + it("enforces one global page budget across split windows", async () => { + const fetcher = vi.fn(async () => + json({ data: { _meta: meta(), swaps: [] } }), + ); + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + limits: { maximumPages: 2, maximumEntities: 500 }, + fetcher, + }); + + await expect( + client.readSwaps({ + poolKey: POOL_KEY, + block: BLOCK, + from: "100", + toExclusive: "43305", + }), + ).resolves.toEqual({ status: "pending", reason: "response_oversize" }); + expect(fetcher).toHaveBeenCalledTimes(2); + }); + + it("enforces one global entity budget across split windows", async () => { + let call = 0; + const fetcher = vi.fn(async () => { + call += 1; + return json({ + data: { + _meta: meta(), + swaps: + call === 1 + ? [swap(1, 100), swap(2, 101)] + : [swap(3, 21_700)], + }, + }); + }); + const client = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + limits: { maximumPages: 4, maximumEntities: 2 }, + fetcher, + }); + + await expect( + client.readSwaps({ + poolKey: POOL_KEY, + block: BLOCK, + from: "100", + toExclusive: "21705", + }), + ).resolves.toEqual({ status: "pending", reason: "response_oversize" }); + expect(fetcher).toHaveBeenCalledTimes(2); + }); + + it("returns pending for GraphQL, body, and bounded-page failures", async () => { + const graphqlClient = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher: async () => + json({ data: null, errors: [{ message: "indexing failed" }] }), + }); + await expect( + graphqlClient.readPoolSnapshot({ poolKey: POOL_KEY, block: BLOCK }), + ).resolves.toEqual({ status: "pending", reason: "graphql_error" }); + + const bodyClient = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + fetcher: async () => + new Response("{}", { + status: 200, + headers: { "content-length": String(128 * 1024 + 1) }, + }), + }); + await expect( + bodyClient.readPoolSnapshot({ poolKey: POOL_KEY, block: BLOCK }), + ).resolves.toEqual({ status: "pending", reason: "response_oversize" }); + + const pageClient = createUniswapAnalyticsClient({ + gatewayBaseUrl: "https://gateway.thegraph.com", + apiKey: "graph-secret", + limits: { maximumPages: 1, maximumEntities: 250 }, + fetcher: async () => + json({ + data: { + _meta: meta(), + swaps: Array.from({ length: 250 }, (_, index) => swap(index, 100)), + }, + }), + }); + await expect( + pageClient.readSwaps({ + poolKey: POOL_KEY, + block: BLOCK, + from: "100", + toExclusive: "200", + }), + ).resolves.toEqual({ + status: "pending", + reason: "response_oversize", + }); + }); + + it("handles token ordering and decimals with exact bigint price ratios", () => { + expect( + priceRatiosFromSqrtPriceX96({ + sqrtPriceX96: "79228162514264337593543950336", + token0Decimals: 6, + token1Decimals: 18, + }), + ).toEqual({ + token1PerToken0: { + numerator: "1", + denominator: "1000000000000", + }, + token0PerToken1: { + numerator: "1000000000000", + denominator: "1", + }, + }); + }); +}); diff --git a/tests/deep-preflight.test.ts b/tests/deep-preflight.test.ts index 49b937ea..7ffa6252 100644 --- a/tests/deep-preflight.test.ts +++ b/tests/deep-preflight.test.ts @@ -1,292 +1,33 @@ import { NextRequest } from "next/server"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { decodeFunctionData, getAddress } from "viem"; - -const mocks = vi.hoisted(() => { - const address = (index: number) => - `0x${index.toString(16).padStart(40, "0")}`; - const runtimeHash = `0x${"11".repeat(32)}`; - const addresses = { - treasury: address(1), - lockedPositionFactory: address(2), - zapPlanner: address(3), - growthVaultFactory: address(4), - growthVaultImplementation: address(5), - hookFactory: address(6), - feeHook: "0x0000000000000000000000000000000000003aec", - launcher: address(8), - positionPlanner: address(9), - automation: address(10), - keeperExecutor: address(11), - poolManager: address(12), - positionManager: address(13), - tokenFactory: address(14), - predictedToken: address(15), - }; - const release = { - schemaVersion: 3, - chainId: 1, - startBlock: 100, - addresses, - runtimeCodeHashes: { - lockedPositionFactory: runtimeHash, - zapPlanner: runtimeHash, - growthVaultFactory: runtimeHash, - growthVaultImplementation: runtimeHash, - hookFactory: runtimeHash, - feeHook: runtimeHash, - launcher: runtimeHash, - positionPlanner: runtimeHash, - automation: runtimeHash, - keeperExecutor: runtimeHash, - }, - officialDependencies: { - poolManager: { - address: addresses.poolManager, - runtimeCodeHash: runtimeHash, - }, - positionManager: { - address: addresses.positionManager, - runtimeCodeHash: runtimeHash, - }, - uerc20Factory: { - address: addresses.tokenFactory, - runtimeCodeHash: runtimeHash, - }, - }, - }; - const client = { - getCode: vi.fn( - async ({ address: target }: { address: string }) => - target.toLowerCase() === addresses.predictedToken.toLowerCase() - ? "0x" - : "0x6000", - ), - getBalance: vi.fn(async () => 1_000_000_000_000_000_000n), - call: vi.fn(async () => ({ data: "0x" })), - estimateGas: vi.fn(async () => 7_000_000n), - getGasPrice: vi.fn(async () => 1_000_000_000n), - getChainId: vi.fn(async () => 1), - getBlock: vi.fn( - async ({ - blockNumber, - }: { - blockTag?: "latest" | "finalized"; - blockNumber?: bigint; - }) => ({ - number: blockNumber ?? 500n, - hash: `0x${"22".repeat(32)}`, - timestamp: 2_000_000_000n, - }), - ), - readContract: vi.fn( - async ({ functionName }: { functionName: string }) => { - if (functionName === "predictTokenAddress") { - return [addresses.predictedToken, `0x${"33".repeat(32)}`]; - } - throw new Error(`Unhandled read ${functionName}`); - }, - ), - }; - return { - addresses, - client, - release, - releaseReady: true as boolean, - runtimeHash, - }; -}); - -vi.mock("viem", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - createPublicClient: () => mocks.client, - }; -}); - -vi.mock("@/lib/deep-v3-release", () => ({ - getConfiguredDeepV3Release: () => - mocks.releaseReady ? mocks.release : null, - isConfiguredDeepV3ReleaseReady: () => mocks.releaseReady, -})); - -vi.mock("@/lib/deep-v3-runtime-binding", () => ({ - assertDeepV3RuntimeBinding: vi.fn(async () => ({ - blockNumber: 500n, - blockHash: `0x${"22".repeat(32)}`, - })), - requireIndependentDeepV3RpcUrls: ( - primary: string | undefined, - secondary: string | undefined, - ) => { - if (!primary || !secondary) throw new Error("missing RPC"); - return [primary, secondary]; - }, -})); - -vi.mock("@/contracts/config/app-deployments.v1.json", () => ({ - default: { - production: { - chainId: 1, - status: "ready", - memeLaunchStatus: "not-deployed", - adaptiveLaunchStatus: "not-deployed", - runtimeCodeHashes: {}, - }, - rehearsal: { - chainId: 11_155_111, - status: "not-deployed", - memeLaunchStatus: "not-deployed", - adaptiveLaunchStatus: "not-deployed", - runtimeCodeHashes: {}, - }, - }, -})); - -vi.mock("@/contracts/config/deployment-inputs.v1.json", () => ({ - default: { - platform: { treasury: mocks.addresses.treasury }, - }, -})); - -vi.mock("@/contracts/dependencies/ethereum-mainnet.json", () => ({ - default: { - contracts: { - poolManager: { - address: mocks.addresses.poolManager, - runtimeCodeHash: mocks.runtimeHash, - }, - positionManager: { - address: mocks.addresses.positionManager, - runtimeCodeHash: mocks.runtimeHash, - }, - uerc20Factory: { - address: mocks.addresses.tokenFactory, - runtimeCodeHash: mocks.runtimeHash, - }, - }, - }, -})); - -vi.mock("@/contracts/dependencies/ethereum-sepolia.json", () => ({ - default: { - contracts: { - poolManager: { - address: mocks.addresses.poolManager, - runtimeCodeHash: mocks.runtimeHash, - }, - positionManager: { - address: mocks.addresses.positionManager, - runtimeCodeHash: mocks.runtimeHash, - }, - uerc20Factory: { - address: mocks.addresses.tokenFactory, - runtimeCodeHash: mocks.runtimeHash, - }, - }, - }, -})); +import { describe, expect, it } from "vitest"; import { POST } from "../app/api/launch/preflight/route"; -import { - DEEP_V3_FIXED_POLICY, - deepV3LaunchAbi, -} from "../lib/deep-v3"; -import { createDeepDraft } from "../lib/launch"; - -const account = "0x1111111111111111111111111111111111111111"; -const salt = `0x${"aa".repeat(32)}`; -function request(walletChainId: string | number = "0x1") { +function request(launchModel: string) { return new NextRequest("http://localhost/api/launch/preflight", { method: "POST", body: JSON.stringify({ - account, - walletChainId, - draft: { - ...createDeepDraft(), - tokenName: "Deep Token", - tokenSymbol: "DEEP", - tokenDescription: - "Trading fees deepen the original locked pool.", - initialBuyEth: "0.0006", - launchSalt: salt, - }, + account: "not-an-address", + draft: { launchModel }, }), }); } -describe("Deep V3 launch preflight", () => { - afterEach(() => { - mocks.releaseReady = true; - vi.clearAllMocks(); - vi.unstubAllEnvs(); - }); - - it("prepares one exact protected launch after the runtime checks pass", async () => { - vi.stubEnv("ETHEREUM_RPC_URL", "https://rpc-a.example/project"); - vi.stubEnv("ETHEREUM_RPC_URL_B", "https://rpc-b.example/project"); - - const response = await POST(request()); - expect(response.status).toBe(200); - const body = await response.json(); - expect(body).toMatchObject({ - status: "ready", - mode: "deep", - title: "Ready for wallet review", - predictedToken: mocks.addresses.predictedToken, - predictedHook: getAddress(mocks.addresses.feeHook), - checks: [ - { id: "token", status: "pass" }, - { id: "wallet", status: "pass" }, - { id: "contracts", status: "pass" }, - { id: "simulation", status: "pass" }, - ], - transaction: { - kind: "launch", - chainId: 1, - to: mocks.addresses.launcher, - value: "600000000000000", - gasLimit: "8400000", - }, - }); - expect(body.planHash).toMatch(/^0x[0-9a-f]{64}$/); - - const decoded = decodeFunctionData({ - abi: deepV3LaunchAbi, - data: body.transaction.data, - }); - expect(decoded.functionName).toBe("launch"); - if (decoded.functionName !== "launch") return; - expect(decoded.args[0].minimumInitialTokenOut).toBeGreaterThan(1n); - expect(decoded.args[0].initialBuySqrtPriceLimitX96).toBe( - DEEP_V3_FIXED_POLICY.minimumInitialBuySqrtPriceLimitX96, - ); - expect(decoded.args[0].deadline).toBe(2_000_001_200n); - expect(mocks.client.call).toHaveBeenCalledTimes(1); - }); - - it("blocks the wallet before simulation when it is on another chain", async () => { - const response = await POST(request("0xaa36a7")); - expect(response.status).toBe(200); - await expect(response.json()).resolves.toMatchObject({ - status: "blocked", - mode: "deep", - checks: [ - { id: "token", status: "pass" }, - { id: "wallet", status: "blocked" }, - ], - }); - expect(mocks.client.call).not.toHaveBeenCalled(); - }); - - it("keeps preflight disabled when the terminal V3 release gate is absent", async () => { - mocks.releaseReady = false; - const response = await POST(request()); - expect(response.status).toBe(400); +describe("closed Deep launch preflight", () => { + it.each([ + "deep", + " Deep ", + "deep-v3", + "liquidity-growth", + "liquidity-growth-v3", + ])("returns one stable closure response for %s", async (launchModel) => { + const response = await POST(request(launchModel)); + + expect(response.status).toBe(410); + expect(response.headers.get("Cache-Control")).toBe("no-store"); await expect(response.json()).resolves.toEqual({ - error: "Deep is not enabled by a verified release manifest", + code: "deep_launches_closed", + error: "New Deep launches are not available", }); }); }); diff --git a/tests/deep-v2-launch-api.test.ts b/tests/deep-v2-launch-api.test.ts index af96e12d..f13ab83a 100644 --- a/tests/deep-v2-launch-api.test.ts +++ b/tests/deep-v2-launch-api.test.ts @@ -1,78 +1,16 @@ -import { NextRequest } from "next/server"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mocks = vi.hoisted(() => ({ - findDeepV2LaunchByTransaction: vi.fn(), - readExploreModel: vi.fn(), -})); - -vi.mock("../lib/onchain", () => ({ - readExploreModel: mocks.readExploreModel, -})); - -vi.mock("../lib/onchain/deep-v2-read-model", () => ({ - findDeepV2LaunchByTransaction: mocks.findDeepV2LaunchByTransaction, -})); +import { describe, expect, it } from "vitest"; import { GET } from "../app/api/explore/launch/route"; -const transactionHash = `0x${"12".repeat(32)}`; - -describe("Deep V2 launch confirmation API", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it.each([ - "", - "transaction=invalid", - `transaction=${transactionHash}&transaction=${transactionHash}`, - `transaction=${transactionHash}&extra=1`, - ])("rejects a non-canonical query before reading Explore: %s", async (query) => { - const response = await GET( - new NextRequest(`http://localhost/api/explore/launch?${query}`), - ); - - expect(response.status).toBe(400); - expect(mocks.readExploreModel).not.toHaveBeenCalled(); - }); - - it("returns only the launch selected by the strict V2 provenance finder", async () => { - const model = { - status: "ready", - tokens: [], - snapshot: { - chainId: 1, - blockNumber: "123", - blockHash: `0x${"34".repeat(32)}`, - confirmations: 12, - }, - creatorClaims: [], - launcherFeesAccruedWei: "0", - launcherFeesAccruedEth: "0", - }; - const launch = { - tokenAddress: "0x1111111111111111111111111111111111111111", - deepReleaseVersion: "deep-full-range-v2", - }; - mocks.readExploreModel.mockResolvedValue(model); - mocks.findDeepV2LaunchByTransaction.mockReturnValue(launch); - - const response = await GET( - new NextRequest( - `http://localhost/api/explore/launch?transaction=${transactionHash}`, - ), - ); +describe("closed historical Deep launch confirmation route", () => { + it("fails closed without reading the historical launch registry", async () => { + const response = await GET(); - expect(response.status).toBe(200); + expect(response.status).toBe(410); + expect(response.headers.get("Cache-Control")).toBe("no-store"); await expect(response.json()).resolves.toEqual({ - status: "ready", - launch, - snapshot: model.snapshot, + code: "deep_launches_closed", + error: "New Deep launches are not available", }); - expect(mocks.findDeepV2LaunchByTransaction).toHaveBeenCalledWith( - model, - transactionHash, - ); }); }); diff --git a/tests/deep-v2-profile-api-route.test.ts b/tests/deep-v2-profile-api-route.test.ts index f5fffdce..13cc4c4f 100644 --- a/tests/deep-v2-profile-api-route.test.ts +++ b/tests/deep-v2-profile-api-route.test.ts @@ -1,79 +1,26 @@ -import { NextRequest } from "next/server"; -import { describe, expect, it, vi } from "vitest"; - -vi.mock("server-only", () => ({})); +import { describe, expect, it } from "vitest"; import { GET, POST } from "../app/api/profile/deep/route"; -const ACCOUNT = "0x1111111111111111111111111111111111111111"; -const VAULT = "0x2222222222222222222222222222222222222222"; - -function request(body: Record) { - return new NextRequest("http://localhost/api/profile/deep", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); -} +const expected = { + code: "deep_profile_closed", + error: "The Deep profile endpoint is not available", +}; -describe("Deep profile API release dispatch", () => { - it("keeps the Deep V3 creator profile fail-closed before a verified live release", async () => { - const response = await GET( - new NextRequest( - `http://localhost/api/profile/deep?account=${ACCOUNT}&deepReleaseVersion=deep-full-range-v3`, - ), - ); +describe("closed Deep profile API", () => { + it("closes profile reads", async () => { + const response = await GET(); - expect(response.status).toBe(200); - await expect(response.json()).resolves.toEqual({ - status: "not-deployed", - account: "0x1111111111111111111111111111111111111111", - chainId: 1, - tokens: [], - }); + expect(response.status).toBe(410); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual(expected); }); - it("rejects unknown Deep profile release versions", async () => { - const response = await GET( - new NextRequest( - `http://localhost/api/profile/deep?account=${ACCOUNT}&deepReleaseVersion=deep-full-range-v4`, - ), - ); - - expect(response.status).toBe(400); - await expect(response.json()).resolves.toEqual({ - error: "Unsupported Deep release version", - }); - }); - - it("fails closed before registry or RPC access when V2 has no reviewed eligible manifest", async () => { - const response = await POST( - request({ - action: "claim", - deepReleaseVersion: "deep-full-range-v2", - account: ACCOUNT, - vaultAddress: VAULT, - chainId: 1, - }), - ); - expect(response.status).toBe(409); - await expect(response.json()).resolves.toEqual({ - error: "Deep V2 is not enabled by a verified release", - }); - }); + it("closes reward actions", async () => { + const response = await POST(); - it("never guesses a release version for a reward action", async () => { - const response = await POST( - request({ - action: "claim", - account: ACCOUNT, - vaultAddress: VAULT, - chainId: 1, - }), - ); - expect(response.status).toBe(400); - await expect(response.json()).resolves.toEqual({ - error: "The reward request is invalid", - }); + expect(response.status).toBe(410); + expect(response.headers.get("Cache-Control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual(expected); }); }); diff --git a/tests/deep-v3-launch-route.test.ts b/tests/deep-v3-launch-route.test.ts index e236824b..7d363a4d 100644 --- a/tests/deep-v3-launch-route.test.ts +++ b/tests/deep-v3-launch-route.test.ts @@ -1,200 +1,16 @@ -import { NextRequest } from "next/server"; -import { describe, expect, it, vi } from "vitest"; - -const mocks = vi.hoisted(() => { - const account = "0x1111111111111111111111111111111111111111"; - const launcher = "0x2222222222222222222222222222222222222222"; - const hook = "0x3333333333333333333333333333333333333333"; - const token = "0x4444444444444444444444444444444444444444"; - const vault = "0x5555555555555555555555555555555555555555"; - const recipient = "0x6666666666666666666666666666666666666666"; - const transaction = `0x${"12".repeat(32)}`; - const blockHash = `0x${"34".repeat(32)}`; - const poolId = `0x${"56".repeat(32)}`; - const launchHash = `0x${"78".repeat(32)}`; - const configurationHash = `0x${"9a".repeat(32)}`; - const clients: unknown[] = []; - return { - account, - launcher, - hook, - token, - vault, - recipient, - transaction, - blockHash, - poolId, - launchHash, - configurationHash, - clients, - parseReceipts: vi.fn(), - readProfile: vi.fn(), - }; -}); - -vi.mock("viem", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - createPublicClient: vi.fn(() => mocks.clients.shift()), - }; -}); - -vi.mock("@/lib/deep-v3-release", () => ({ - configuredMainnetDeepV3Manifest: { schemaVersion: 3 }, -})); - -vi.mock("@/lib/deep-v3-runtime-binding", () => ({ - requireIndependentDeepV3RpcUrls: () => - ["https://rpc-a.example", "https://rpc-b.example"] as const, -})); - -vi.mock("@/lib/onchain/deep-v3-read-model", () => ({ - resolveVerifiedDeepV3ReadRelease: () => ({ - startBlock: 100, - addresses: { - launcher: mocks.launcher, - feeHook: mocks.hook, - }, - }), -})); - -vi.mock("@/lib/deep-v3-launch-confirmation", () => ({ - parseDeepV3LaunchReceipts: mocks.parseReceipts, -})); - -vi.mock("@/lib/profile/deep-v3-profile.server", () => ({ - readDeepV3ProfileToken: mocks.readProfile, -})); +import { describe, expect, it } from "vitest"; import { GET } from "../app/api/explore/launch/deep-v3/route"; -function request(query = "") { - return new NextRequest( - `https://programmable.family/api/explore/launch/deep-v3${query}`, - ); -} - -function receipt() { - return { - status: "success" as const, - from: mocks.account, - to: mocks.launcher, - blockNumber: 120n, - blockHash: mocks.blockHash, - transactionHash: mocks.transaction, - transactionIndex: 1, - logs: [], - }; -} - -describe("Deep V3 launch confirmation route", () => { - it("rejects an ambiguous query before RPC access", async () => { - const response = await GET( - request( - `?account=${mocks.account}&account=${mocks.account}&transaction=${mocks.transaction}`, - ), - ); - - expect(response.status).toBe(400); - await expect(response.json()).resolves.toEqual({ - error: "Unsupported query parameters", - }); - expect(mocks.clients).toHaveLength(0); - }); - - it("waits for twelve confirmations from both RPCs", async () => { - mocks.clients.push( - { - getTransactionReceipt: vi.fn().mockResolvedValue(receipt()), - getBlockNumber: vi.fn().mockResolvedValue(131n), - }, - { - getTransactionReceipt: vi.fn().mockResolvedValue(receipt()), - getBlockNumber: vi.fn().mockResolvedValue(140n), - }, - ); - - const response = await GET( - request( - `?account=${mocks.account}&transaction=${mocks.transaction}`, - ), - ); - - expect(response.status).toBe(202); - await expect(response.json()).resolves.toEqual({ - status: "pending", - launch: null, - }); - expect(mocks.parseReceipts).not.toHaveBeenCalled(); - }); - - it("returns a token only after receipt agreement and full profile validation", async () => { - const provenance = { - deepReleaseVersion: "deep-full-range-v3", - launchModel: "deep", - launcher: mocks.launcher, - creator: mocks.account, - tokenAddress: mocks.token, - vaultAddress: mocks.vault, - hookAddress: mocks.hook, - positionRecipient: mocks.recipient, - positionTokenId: "7", - poolId: mocks.poolId, - launchHash: mocks.launchHash, - vaultConfigurationHash: mocks.configurationHash, - blockNumber: "120", - blockHash: mocks.blockHash, - transactionHash: mocks.transaction, - transactionIndex: 1, - logIndex: 2, - }; - mocks.parseReceipts.mockReturnValueOnce(provenance); - mocks.readProfile.mockResolvedValueOnce({ - snapshot: { - blockNumber: "140", - blockHash: `0x${"ab".repeat(32)}`, - }, - token: { - tokenAddress: mocks.token, - tokenName: "Deep Test", - tokenSymbol: "DEEP", - deepReleaseVersion: "deep-full-range-v3", - }, - }); - mocks.clients.push( - { - getTransactionReceipt: vi.fn().mockResolvedValue(receipt()), - getBlockNumber: vi.fn().mockResolvedValue(140n), - }, - { - getTransactionReceipt: vi.fn().mockResolvedValue(receipt()), - getBlockNumber: vi.fn().mockResolvedValue(141n), - }, - ); - - const response = await GET( - request( - `?account=${mocks.account}&transaction=${mocks.transaction}`, - ), - ); +describe("closed Deep V3 launch confirmation route", () => { + it("returns the stable closure response for every query", async () => { + const response = await GET(); - expect(response.status).toBe(200); + expect(response.status).toBe(410); + expect(response.headers.get("Cache-Control")).toBe("no-store"); await expect(response.json()).resolves.toEqual({ - status: "ready", - launch: { - tokenAddress: mocks.token, - name: "Deep Test", - symbol: "DEEP", - deepReleaseVersion: "deep-full-range-v3", - deepV3Provenance: provenance, - }, - snapshot: { - blockNumber: "140", - blockHash: `0x${"ab".repeat(32)}`, - }, + code: "deep_launches_closed", + error: "New Deep launches are not available", }); - expect(mocks.parseReceipts).toHaveBeenCalledOnce(); - expect(mocks.readProfile).toHaveBeenCalledOnce(); }); }); diff --git a/tests/docs-navigation.test.ts b/tests/docs-navigation.test.ts index 8add4f28..c6b9833c 100644 --- a/tests/docs-navigation.test.ts +++ b/tests/docs-navigation.test.ts @@ -149,12 +149,13 @@ describe("Docs navigation state", () => { expect(getDocsSearchResults("")).toEqual([]); }); - it("keeps hidden models out of search and describes Stock-Paired accurately", () => { + it("keeps hidden models out of search and labels Stock-Paired as historical", () => { const deepResults = getDocsSearchResults("deep"); const stockResults = getDocsSearchResults("stock"); expect(deepResults).toEqual([]); - expect(stockResults[0]?.description).toContain("restricted"); + expect(stockResults[0]?.title).toBe("Stock-Paired history"); + expect(stockResults[0]?.description).toContain("Historical"); }); it("opens keyboard navigation on the first or last result", () => { diff --git a/tests/explore-api.test.ts b/tests/explore-api.test.ts index ded97b13..8315a614 100644 --- a/tests/explore-api.test.ts +++ b/tests/explore-api.test.ts @@ -1,6 +1,8 @@ import { NextRequest } from "next/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; +vi.mock("server-only", () => ({})); + const mocks = vi.hoisted(() => ({ enrichExplorePageWithOfficialV4Subgraph: vi.fn(), paginateExplore: vi.fn(), @@ -78,7 +80,7 @@ describe("Explore API query boundary", () => { expect(response.status).toBe(200); expect(response.headers.get("Cache-Control")).toBe( - "public, max-age=0, s-maxage=10, stale-while-revalidate=10", + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", ); }); }); diff --git a/tests/explore-view-state.test.ts b/tests/explore-view-state.test.ts index 89657b29..dd74ce3f 100644 --- a/tests/explore-view-state.test.ts +++ b/tests/explore-view-state.test.ts @@ -25,7 +25,7 @@ afterEach(() => { describe("Explore refresh state", () => { it("refreshes only visible Explore content after the freshness interval", () => { - expect(EXPLORE_REFRESH_INTERVAL_MS).toBe(10_000); + expect(EXPLORE_REFRESH_INTERVAL_MS).toBe(5_000); expect( shouldRefreshExplore({ visibilityState: "hidden", @@ -37,14 +37,14 @@ describe("Explore refresh state", () => { shouldRefreshExplore({ visibilityState: "visible", lastRefreshAt: 5_000, - now: 14_999, + now: 9_999, }), ).toBe(false); expect( shouldRefreshExplore({ visibilityState: "visible", lastRefreshAt: 5_000, - now: 15_000, + now: 10_000, }), ).toBe(true); }); diff --git a/tests/indexer-feed-reader.test.ts b/tests/indexer-feed-reader.test.ts new file mode 100644 index 00000000..f7dd5d85 --- /dev/null +++ b/tests/indexer-feed-reader.test.ts @@ -0,0 +1,321 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +import { readIndexedFeedSnapshotWithModel } from "../app/api/indexers/v1/read-indexed-feed.server"; +import type { PostgresTransaction } from "../lib/data-pipeline/postgres"; + +const NOW = Date.parse("2026-07-31T12:00:00.000Z"); +const RELEASES = [ + "classic-v2", + "classic-v3", + "stock-paired-v1", + "stock-paired-v2", + "stock-paired-v3", +] as const; +const BLOCK_HASH = `0x${"11".repeat(32)}`; +const BLOCK_HASH_BYTEA = `\\x${"11".repeat(32)}`; +const PUBLICATION = `0x${"22".repeat(32)}`; +const PARITY_EVIDENCE = `0x${"33".repeat(32)}`; +const PARITY_BINDING = `0x${"44".repeat(32)}`; + +function uuid(index: number) { + return `10000000-0000-4000-8000-${String(index).padStart(12, "0")}`; +} + +function modelFor(release: (typeof RELEASES)[number]) { + return release.startsWith("classic-") ? "classic" : "stock-paired"; +} + +function releasePointer( + release: (typeof RELEASES)[number], + index: number, +) { + return { + routeKey: "explore-list", + chainId: 1, + releaseVersion: release, + modelVersion: modelFor(release), + sourceGroup: "core", + projectorVersion: "projector-v1", + epochId: uuid(index + 1), + pointerGeneration: "1", + checkpointId: uuid(index + 20), + checkpointGeneration: "2", + reorgGeneration: "0", + checkpointBlockNumber: "100", + checkpointBlockHash: BLOCK_HASH, + }; +} + +function routeEvidence( + release: (typeof RELEASES)[number], + index: number, +) { + return { + releaseVersion: release, + modelVersion: modelFor(release), + parityRecordId: uuid(index + 40), + reconciliationId: uuid(index + 50), + parityEvidenceCommitment: PARITY_EVIDENCE, + parityBindingId: uuid(index + 60), + parityBindingCommitment: PARITY_BINDING, + parityBoundAt: "2026-07-31T11:59:30.000Z", + }; +} + +function tokenSource() { + return { + ...releasePointer("classic-v2", 0), + snapshotCommitment: BLOCK_HASH, + projectionRunId: uuid(70), + publicationCommitment: PUBLICATION, + promotedBlockNumber: "100", + promotedBlockHash: BLOCK_HASH, + }; +} + +function rawToken() { + return { + source: tokenSource(), + tokenAddress: "0x1111111111111111111111111111111111111111", + hookAddress: "0x2222222222222222222222222222222222222222", + poolId: `0x${"55".repeat(32)}`, + creatorAddress: "0x4444444444444444444444444444444444444444", + positionRecipient: "0x7777777777777777777777777777777777777777", + positionTokenId: "42", + rewardVaultAddress: null, + launchHash: `0x${"66".repeat(32)}`, + launchBlockNumber: "98", + launchTransactionHash: `0x${"77".repeat(32)}`, + launchTransactionIndex: 3, + launchLogIndex: 4, + launchedAt: "2026-07-31T11:55:00.000Z", + name: "Test", + symbol: "TEST", + decimals: 18, + totalSupplyRaw: "1000000000000000000000000000", + metadata: { + revision: "1", + createdAt: "2026-07-31T11:55:00.000Z", + description: "This is a test", + imageUrl: "https://programmable.family/test.png", + links: [ + { kind: "website", url: "https://programmable.family/" }, + { kind: "x", url: "https://x.com/0xProgrammable" }, + ], + extraData: "0x", + }, + liquidity: { + tokenLiquidityAmountRaw: "999999999999999999999999999", + lockedTokenDustRaw: "1", + currentTick: -10, + initialTick: 0, + tickLower: -887200, + tickUpper: 887200, + activeLiquidity: "999999999999999999999999", + }, + fees: { + totalSwapFeeBps: 100, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + protocolFeePips: 0, + }, + market: {}, + quote: null, + initialBuy: null, + uniswapV4Pool: null, + }; +} + +function feedRow() { + const pointers = RELEASES.map(releasePointer); + const evidence = RELEASES.map(routeEvidence); + const token = rawToken(); + const source = { + tokenAddress: token.tokenAddress, + source: token.source, + parity: evidence[0], + }; + const snapshot = { + adapterVersion: "indexed-route-adapters-v2", + snapshotCommitment: BLOCK_HASH, + chainId: 1, + blockNumber: "100", + blockHash: BLOCK_HASH, + confirmations: 12, + capturedAt: "2026-07-31T11:59:00.000Z", + releasePointers: pointers, + safeBlockNumber: "102", + reconciledAt: "2026-07-31T11:59:30.000Z", + }; + const tokens = [token]; + const recordSources = [source]; + return { + http_status: 200, + payload_complete: true, + record_count: "1", + record_scopes: [ + { model: "classic", releaseVersion: "classic-v2" }, + ], + comparison_checkpoint_block_number: "100", + comparison_checkpoint_block_hash: BLOCK_HASH_BYTEA, + route_evidence: evidence, + snapshot, + tokens, + record_sources: recordSources, + captured_at: "2026-07-31T11:59:00.000Z", + reconciled_at: "2026-07-31T11:59:30.000Z", + snapshot_commitment: BLOCK_HASH_BYTEA, + payload: { + status: "ready", + snapshot, + data: { tokens, recordSources }, + }, + }; +} + +function readModel(rows: readonly Record[] = [feedRow()]) { + const queryMock = vi.fn(async () => rows); + const transaction: PostgresTransaction = { + async query>() { + return (await queryMock()) as readonly Row[]; + }, + }; + const repeatableReadSnapshot = vi.fn(); + return { + model: { + async repeatableReadSnapshot( + work: (value: PostgresTransaction) => Promise, + ): Promise { + repeatableReadSnapshot(); + return work(transaction); + }, + }, + queryMock, + repeatableReadSnapshot, + }; +} + +describe("indexed GMGN feed reader", () => { + it("reads the complete feed through one API-reader snapshot", async () => { + const fixture = readModel(); + + const result = await readIndexedFeedSnapshotWithModel(fixture.model, NOW); + + expect(fixture.repeatableReadSnapshot).toHaveBeenCalledTimes(1); + expect(fixture.queryMock).toHaveBeenCalledTimes(1); + expect(result).toMatchObject({ + chainId: 1, + capturedAt: "2026-07-31T11:59:00.000Z", + reconciledAt: "2026-07-31T11:59:30.000Z", + projectionLag: 2, + releaseVersions: RELEASES, + snapshotCommitment: BLOCK_HASH, + model: { + status: "ready", + snapshot: { + blockNumber: "100", + blockHash: BLOCK_HASH, + }, + tokens: [ + { + tokenAddress: "0x1111111111111111111111111111111111111111", + launchModel: "classic", + }, + ], + }, + }); + expect(result.sourceCommitment).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it("fails closed when the aggregate function returns no row", async () => { + await expect( + readIndexedFeedSnapshotWithModel(readModel([]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); + + it("fails closed on a stale snapshot", async () => { + const row = feedRow(); + row.snapshot.capturedAt = "2026-07-31T11:40:00.000Z"; + row.captured_at = row.snapshot.capturedAt; + row.payload.snapshot = row.snapshot; + + await expect( + readIndexedFeedSnapshotWithModel(readModel([row]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); + + it("fails closed on incomplete materialization", async () => { + const row = feedRow(); + row.payload_complete = false; + + await expect( + readIndexedFeedSnapshotWithModel(readModel([row]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); + + it("fails closed on a reorg checkpoint mismatch", async () => { + const row = feedRow(); + row.comparison_checkpoint_block_hash = `\\x${"99".repeat(32)}`; + + await expect( + readIndexedFeedSnapshotWithModel(readModel([row]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); + + it("fails closed on feed-count drift", async () => { + const row = feedRow(); + row.record_count = "2"; + + await expect( + readIndexedFeedSnapshotWithModel(readModel([row]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); + + it("fails closed when publication evidence is missing", async () => { + const row = feedRow(); + delete (row.tokens[0]!.source as Record) + .publicationCommitment; + row.record_sources[0]!.source = row.tokens[0]!.source; + row.payload.data.tokens = row.tokens; + row.payload.data.recordSources = row.record_sources; + + await expect( + readIndexedFeedSnapshotWithModel(readModel([row]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); + + it("fails closed on duplicate token identities", async () => { + const row = feedRow(); + row.tokens = [row.tokens[0]!, structuredClone(row.tokens[0]!)]; + row.record_sources = [ + row.record_sources[0]!, + structuredClone(row.record_sources[0]!), + ]; + row.record_count = "2"; + row.payload.data.tokens = row.tokens; + row.payload.data.recordSources = row.record_sources; + + await expect( + readIndexedFeedSnapshotWithModel(readModel([row]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); + + it("does not admit an unsupported Deep source", async () => { + const row = feedRow(); + row.tokens[0]!.source.modelVersion = "deep"; + row.record_sources[0]!.source = row.tokens[0]!.source; + row.payload.data.tokens = row.tokens; + row.payload.data.recordSources = row.record_sources; + + await expect( + readIndexedFeedSnapshotWithModel(readModel([row]).model, NOW), + ).rejects.toThrow("Indexed feed is not ready"); + }); +}); diff --git a/tests/indexer-feed.test.ts b/tests/indexer-feed.test.ts index 50748ae8..d1cbb8c4 100644 --- a/tests/indexer-feed.test.ts +++ b/tests/indexer-feed.test.ts @@ -1,7 +1,29 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("server-only", () => ({})); +const routeMocks = vi.hoisted(() => ({ + readIndexedFeedSnapshot: vi.fn(), + readExploreModel: vi.fn(), + getPublicOnchainDeployment: vi.fn(() => ({ chainId: 1 })), +})); + +vi.mock("../lib/onchain", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readExploreModel: routeMocks.readExploreModel, + getPublicOnchainDeployment: routeMocks.getPublicOnchainDeployment, + }; +}); + +vi.mock( + "../app/api/indexers/v1/read-indexed-feed.server", + () => ({ + readIndexedFeedSnapshot: routeMocks.readIndexedFeedSnapshot, + }), +); + import { GET as getTokenList } from "../app/api/indexers/v1/token-list/route"; import { GET as getIndexerTokens } from "../app/api/indexers/v1/tokens/route"; import { @@ -60,6 +82,24 @@ const readyModel: ExploreReadModel = { launcherFeesAccruedEth: "0", }; +beforeEach(() => { + vi.clearAllMocks(); + vi.stubEnv("INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", "true"); +}); + +function indexedFeedSnapshot() { + return { + chainId: 1 as const, + model: readyModel as ExploreReadModel & { status: "ready" }, + capturedAt: "2026-07-27T12:00:00.000Z", + snapshotCommitment: `0x${"77".repeat(32)}` as const, + sourceCommitment: `0x${"88".repeat(32)}` as const, + projectionLag: 2, + reconciledAt: "2026-07-27T12:00:01.000Z", + releaseVersions: ["classic-v2"] as const, + }; +} + describe("public indexer fee disclosure", () => { it("declares zero transfer tax and deducts the launcher share", () => { const result = serializeIndexerToken(token, 1); @@ -377,7 +417,10 @@ describe("public indexer fee disclosure", () => { ).toThrow("missing onchain fee disclosure"); }); - it("keeps the production feed fail-closed before V2 is ready", async () => { + it("serves the unchanged feed ABI from the indexed snapshot", async () => { + routeMocks.readIndexedFeedSnapshot.mockResolvedValueOnce( + indexedFeedSnapshot(), + ); const response = await getIndexerTokens( new Request( "https://programmable.family/api/indexers/v1/tokens", @@ -388,13 +431,150 @@ describe("public indexer fee disclosure", () => { expect(response.headers.get("access-control-allow-origin")).toBe( "*", ); + expect(response.headers.get("x-programmable-read-source")).toBe( + "indexed", + ); + expect(response.headers.get("x-programmable-projection-block")).toBe( + readyModel.snapshot?.blockNumber, + ); + expect(response.headers.get("x-programmable-projection-hash")).toBe( + readyModel.snapshot?.blockHash, + ); + expect(response.headers.get("x-programmable-projection-lag")).toBe( + "2", + ); + expect( + response.headers.get("x-programmable-snapshot-commitment"), + ).toBe(`0x${"77".repeat(32)}`); + expect(response.headers.get("x-programmable-source-commitment")).toBe( + `0x${"88".repeat(32)}`, + ); expect(await response.json()).toMatchObject({ - status: "not-deployed", + schemaVersion: "programmable-indexer-v1", + status: "ready", chainId: 1, - tokens: [], + snapshot: readyModel.snapshot, + tokens: [ + { + schemaVersion: "programmable-token-v1", + address: token.tokenAddress, + }, + ], }); }); + it("keeps the public feed on the legacy reader while its dedicated flag is off", async () => { + vi.stubEnv("INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", "false"); + vi.stubEnv("INDEXED_EXPLORE_LIST_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_LAUNCH_LOOKUP_ENABLED", "true"); + routeMocks.readExploreModel.mockResolvedValueOnce(readyModel); + + const response = await getIndexerTokens( + new Request("https://programmable.family/api/indexers/v1/tokens"), + ); + + expect(response.status).toBe(200); + expect(routeMocks.readExploreModel).toHaveBeenCalledTimes(1); + expect(routeMocks.readIndexedFeedSnapshot).not.toHaveBeenCalled(); + expect(response.headers.get("x-programmable-read-source")).toBeNull(); + }); + + it("keeps the public token list on the legacy reader while its dedicated flag is off", async () => { + vi.stubEnv("INDEXED_PUBLIC_INDEXER_FEED_READS_ENABLED", "false"); + vi.stubEnv("INDEXED_EXPLORE_LIST_READS_ENABLED", "true"); + vi.stubEnv("INDEXED_LAUNCH_LOOKUP_ENABLED", "true"); + routeMocks.readExploreModel.mockResolvedValueOnce(readyModel); + + const response = await getTokenList(); + + expect(response.status).toBe(200); + expect(routeMocks.readExploreModel).toHaveBeenCalledTimes(1); + expect(routeMocks.readIndexedFeedSnapshot).not.toHaveBeenCalled(); + expect(response.headers.get("x-programmable-read-source")).toBeNull(); + }); + + it.each([ + "snapshot-unavailable", + "projection-lag", + "reconciliation-incomplete", + ])("fails closed with no-store when indexed data is %s", async () => { + routeMocks.readIndexedFeedSnapshot.mockRejectedValueOnce( + new Error("Indexed feed is not ready"), + ); + + const response = await getIndexerTokens( + new Request("https://programmable.family/api/indexers/v1/tokens"), + ); + + expect(response.status).toBe(503); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(response.headers.get("x-programmable-read-source")).toBeNull(); + expect(await response.json()).toEqual({ + error: "Indexer data is temporarily unavailable", + }); + }); + + it("serves direct token lookup from the same indexed snapshot", async () => { + routeMocks.readIndexedFeedSnapshot.mockResolvedValueOnce( + indexedFeedSnapshot(), + ); + + const response = await getIndexerTokens( + new Request( + `https://programmable.family/api/indexers/v1/token?address=${token.tokenAddress}`, + ), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("x-programmable-read-source")).toBe( + "indexed", + ); + expect(await response.json()).toMatchObject({ + schemaVersion: "programmable-token-v1", + address: token.tokenAddress, + name: token.name, + symbol: token.symbol, + }); + }); + + it("keeps the direct-token 404 cache contract after an exact indexed lookup", async () => { + routeMocks.readIndexedFeedSnapshot.mockResolvedValueOnce( + indexedFeedSnapshot(), + ); + + const response = await getIndexerTokens( + new Request( + "https://programmable.family/api/indexers/v1/token?address=0x9999999999999999999999999999999999999999", + ), + ); + + expect(response.status).toBe(404); + expect(response.headers.get("cache-control")).toBe( + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", + ); + expect(response.headers.get("x-programmable-read-source")).toBe( + "indexed", + ); + expect(await response.json()).toEqual({ + error: "Programmable token not found", + }); + }); + + it("fails closed when provenance headers are not canonical", async () => { + routeMocks.readIndexedFeedSnapshot.mockResolvedValueOnce({ + ...indexedFeedSnapshot(), + releaseVersions: ["stock-paired-v1", "classic-v2"], + }); + + const response = await getIndexerTokens( + new Request("https://programmable.family/api/indexers/v1/tokens"), + ); + + expect(response.status).toBe(503); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(response.headers.get("x-programmable-read-source")).toBeNull(); + }); + it("rejects an invalid direct token lookup without reading chain state", async () => { const response = await getIndexerTokens( new Request( @@ -411,17 +591,65 @@ describe("public indexer fee disclosure", () => { }); }); - it("does not expose an invalid empty production token list", async () => { + it("does not expose an unavailable indexed token list", async () => { + routeMocks.readIndexedFeedSnapshot.mockRejectedValueOnce( + new Error("Indexed feed is not ready"), + ); const response = await getTokenList(); expect(response.status).toBe(503); expect(response.headers.get("access-control-allow-origin")).toBe( "*", ); + expect(response.headers.get("cache-control")).toBe("no-store"); expect(await response.json()).toEqual({ - status: "not-deployed", + error: "Token list is temporarily unavailable", + }); + }); + + it("keeps the exact cached first-launch response for a ready empty snapshot", async () => { + routeMocks.readIndexedFeedSnapshot.mockResolvedValueOnce({ + ...indexedFeedSnapshot(), + model: { + ...readyModel, + tokens: [], + }, + }); + + const response = await getTokenList(); + + expect(response.status).toBe(503); + expect(response.headers.get("cache-control")).toBe( + "public, max-age=0, s-maxage=60", + ); + expect(response.headers.get("retry-after")).toBe("60"); + expect(response.headers.get("x-programmable-read-source")).toBe( + "indexed", + ); + expect(await response.json()).toEqual({ + status: "ready", error: "The token list will be available after the first verified launch", }); }); + + it("binds the token-list timestamp and provenance to the indexed snapshot", async () => { + routeMocks.readIndexedFeedSnapshot.mockResolvedValueOnce( + indexedFeedSnapshot(), + ); + + const response = await getTokenList(); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(response.headers.get("x-programmable-read-source")).toBe( + "indexed", + ); + expect(body.timestamp).toBe("2026-07-27T12:00:00.000Z"); + expect(body.tokens).toHaveLength(1); + expect(body.tokens[0]).toMatchObject({ + address: token.tokenAddress, + symbol: token.symbol, + }); + }); }); diff --git a/tests/interaction-accessibility.test.ts b/tests/interaction-accessibility.test.ts index 11b596e7..eebe1335 100644 --- a/tests/interaction-accessibility.test.ts +++ b/tests/interaction-accessibility.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { getChartPointIndex, getPriceHistoryEmptyMessage, + shouldRenderPriceHistory, } from "../components/token-price-chart"; const root = process.cwd(); @@ -19,7 +20,10 @@ function collectCssFiles(directory: string): string[] { describe("interaction accessibility", () => { it("keeps the default arrow cursor policy across app controls", () => { - const css = [...collectCssFiles(join(root, "app")), ...collectCssFiles(join(root, "components"))] + const css = [ + ...collectCssFiles(join(root, "app")), + ...collectCssFiles(join(root, "components")), + ] .map((path) => readFileSync(path, "utf8")) .join("\n"); @@ -47,7 +51,9 @@ describe("interaction accessibility", () => { expect(source).toContain('role="group"'); expect(source).toContain('aria-label="Wallet actions"'); expect(source).toContain("event.relatedTarget instanceof Node"); - expect(source).toContain("event.currentTarget.contains(event.relatedTarget)"); + expect(source).toContain( + "event.currentTarget.contains(event.relatedTarget)", + ); }); it("dismisses the wallet disclosure with Escape and outside pointer input", () => { @@ -66,9 +72,7 @@ describe("interaction accessibility", () => { it("keeps the sticky header and its wallet disclosure above page content", () => { const css = readFileSync(join(root, "app/interface.css"), "utf8"); - expect(css).not.toContain( - ".app-frame > main,\n.site-header,\n.mobile-nav", - ); + expect(css).not.toContain(".app-frame > main,\n.site-header,\n.mobile-nav"); expect(css).toMatch( /\.site-header\s*\{[^}]*position:\s*sticky;[^}]*z-index:\s*50;/s, ); @@ -125,13 +129,28 @@ describe("interaction accessibility", () => { }); it("does not promise unsupported Stock-Paired chart history", () => { - expect( - getPriceHistoryEmptyMessage("stock-paired", false), - ).toBe( + expect(getPriceHistoryEmptyMessage("stock-paired", false)).toBe( "Historical price data is not available for Stock-Paired tokens", ); + expect(getPriceHistoryEmptyMessage("classic", false)).toBe( + "Price history appears after confirmed trades", + ); + }); + + it("keeps range controls available after selecting an empty chart window", () => { expect( - getPriceHistoryEmptyMessage("classic", false), - ).toBe("Price history appears after confirmed trades"); + shouldRenderPriceHistory({ + loading: false, + hasChart: false, + range: "all", + }), + ).toBe(false); + expect( + shouldRenderPriceHistory({ + loading: false, + hasChart: false, + range: "1h", + }), + ).toBe(true); }); }); diff --git a/tests/launch-model-gating.test.ts b/tests/launch-model-gating.test.ts index 2119f60f..7b83cd0d 100644 --- a/tests/launch-model-gating.test.ts +++ b/tests/launch-model-gating.test.ts @@ -158,25 +158,19 @@ describe("unreleased launch model gating", () => { ).toEqual([-1, -1, 0, -1, -1, -1]); }); - it("shows only Classic and Stock-Paired in the public model picker", () => { + it("shows only Classic in the public model picker", () => { const html = renderToStaticMarkup( createElement(LaunchModelPicker, { onChoose: () => undefined, }), ); - expect(html.match(/data-launch-model-option=/g)).toHaveLength(2); + expect(html.match(/data-launch-model-option=/g)).toHaveLength(1); expect(html).toContain('data-launch-model-option="classic"'); - expect(html).toContain('data-launch-model-option="stock-paired"'); expect(html).toContain("Classic"); - expect(html).toContain("Stock-Paired"); - expect(html).toContain("Coming soon"); expect(html).not.toContain("launch-model-classic-details"); - expect(html).not.toContain("launch-model-stock-details"); - const stockButton = html.match( - /]*data-launch-model-option="stock-paired"[^>]*>/, - )?.[0]; - expect(stockButton).toContain("disabled"); + expect(html).not.toContain('data-launch-model-option="stock-paired"'); + expect(html).not.toContain("Stock-Paired"); expect(html).not.toContain('data-launch-model-option="deep"'); expect(html).not.toContain("Deep"); expect(html).not.toMatch(/adaptive/i); @@ -184,21 +178,6 @@ describe("unreleased launch model gating", () => { expect(html).not.toContain("Liquidity Growth"); }); - it("enables Stock-Paired when the server public-release gate is open", () => { - const html = renderToStaticMarkup( - createElement(LaunchModelPicker, { - onChoose: () => undefined, - stockPairedPublicLaunchEnabled: true, - }), - ); - const stockButton = html.match( - /]*data-launch-model-option="stock-paired"[^>]*>/, - )?.[0]; - - expect(stockButton).not.toContain("disabled"); - expect(html).toContain(">Launch<"); - }); - it("keeps the Deep preset concise while retaining its material limits", () => { const html = renderToStaticMarkup(createElement(DeepPresetStep)); @@ -267,7 +246,7 @@ describe("unreleased launch model gating", () => { expect(resolveImplementedLaunchModel("classic")).toBe("classic"); expect(resolveImplementedLaunchModel("classic-v3")).toBe("classic-v3"); expect(resolveImplementedLaunchModel("adaptive")).toBeNull(); - expect(resolveImplementedLaunchModel("deep")).toBe("deep"); + expect(resolveImplementedLaunchModel("deep")).toBeNull(); expect(resolveImplementedLaunchModel("liquidity-growth")).toBeNull(); expect(resolveImplementedLaunchModel("unknown")).toBeNull(); expect(resolveReservedLaunchModel("deep")).toBeNull(); @@ -387,9 +366,10 @@ describe("unreleased launch model gating", () => { }); const result = await POST(request); - expect(result.status).toBe(400); + expect(result.status).toBe(410); await expect(result.json()).resolves.toEqual({ - error: "Deep is not enabled by a verified release manifest", + code: "deep_launches_closed", + error: "New Deep launches are not available", }); }, ); @@ -432,7 +412,7 @@ describe("unreleased launch model gating", () => { }); }); - it("keeps an active Stock-Paired launch bound to Ethereum Mainnet", async () => { + it("rejects new Stock-Paired launches with a stable retired-model response", async () => { const request = new NextRequest("http://localhost/api/launch/preflight", { method: "POST", body: JSON.stringify({ @@ -450,15 +430,10 @@ describe("unreleased launch model gating", () => { }); const result = await POST(request); - expect(result.status).toBe(200); - await expect(result.json()).resolves.toMatchObject({ - status: "blocked", - mode: "stock-paired", - title: "Switch the wallet to Ethereum", - checks: [ - { id: "token", status: "pass" }, - { id: "wallet", status: "blocked" }, - ], + expect(result.status).toBe(410); + await expect(result.json()).resolves.toEqual({ + code: "stock_paired_launches_closed", + error: "New Stock-Paired launches are no longer available", }); }); }); diff --git a/tests/live-data-refresh.test.ts b/tests/live-data-refresh.test.ts new file mode 100644 index 00000000..4a6cc05a --- /dev/null +++ b/tests/live-data-refresh.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; + +import { + LIVE_DATA_REFRESH_INTERVAL_MS, + shouldRefreshLiveData, +} from "../components/use-live-data-refresh"; + +describe("live data refresh policy", () => { + it("refreshes visible data every five seconds", () => { + expect(LIVE_DATA_REFRESH_INTERVAL_MS).toBe(5_000); + expect( + shouldRefreshLiveData({ + visibilityState: "visible", + lastRefreshAt: 1_000, + now: 6_000, + }), + ).toBe(true); + }); + + it("does not refresh hidden tabs or early intervals", () => { + expect( + shouldRefreshLiveData({ + visibilityState: "hidden", + lastRefreshAt: 1_000, + now: 60_000, + }), + ).toBe(false); + expect( + shouldRefreshLiveData({ + visibilityState: "visible", + lastRefreshAt: 1_000, + now: 5_999, + }), + ).toBe(false); + }); +}); diff --git a/tests/onchain-durable-model.test.ts b/tests/onchain-durable-model.test.ts index 48655bba..a7268874 100644 --- a/tests/onchain-durable-model.test.ts +++ b/tests/onchain-durable-model.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { keccak256, toBytes } from "viem"; import { + selectFreshDurableExploreModel, shouldReplaceDurableSnapshot, validateDurableExploreEnvelope, type DeepExploreReleaseBinding, @@ -292,6 +293,34 @@ describe("durable onchain snapshot replacement", () => { expect(result.ageMs).toBeGreaterThanOrEqual(120_000); } }); + + it("never serves a durable snapshot after its verified freshness window", () => { + const freshValue = envelope("programmable-durable-index-v1"); + const fresh = validateDurableExploreEnvelope( + freshValue, + deployment, + 60_000, + ); + expect(selectFreshDurableExploreModel(fresh)).toBe( + freshValue.payload.model, + ); + + const staleValue = envelope("programmable-durable-index-v1"); + staleValue.payload.generatedAt = new Date( + Date.now() - 120_000, + ).toISOString(); + staleValue.contentHash = keccak256( + toBytes(JSON.stringify(staleValue.payload)), + ); + const stale = validateDurableExploreEnvelope( + staleValue, + deployment, + 60_000, + ); + + expect(stale).toMatchObject({ status: "unavailable", reason: "stale" }); + expect(selectFreshDurableExploreModel(stale)).toBeNull(); + }); }); describe("durable Deep release binding", () => { diff --git a/tests/onchain-explore-read-source.test.ts b/tests/onchain-explore-read-source.test.ts new file mode 100644 index 00000000..0e48c5b4 --- /dev/null +++ b/tests/onchain-explore-read-source.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it, vi } from "vitest"; + +import { resolveExploreReadSource } from "../lib/onchain/explore-read-source"; +import type { + ExploreReadModel, + ReadyOnchainDeployment, +} from "../lib/onchain/types"; + +const config = { + environment: "production", + releaseVersion: "classic-v2", + chainId: 1, + status: "ready", + launcher: "0x1111111111111111111111111111111111111111", + feeHook: "0x2222222222222222222222222222222222222222", + launcherRuntimeCodeHash: `0x${"11".repeat(32)}`, + feeHookRuntimeCodeHash: `0x${"22".repeat(32)}`, + deploymentBlock: 100n, + stateView: "0x3333333333333333333333333333333333333333", + stateViewRuntimeCodeHash: `0x${"33".repeat(32)}`, + rpcUrl: "https://primary.example", + rpcUrlSecondary: "https://secondary.example", + confirmations: 12n, + logBlockRange: 10_000n, +} satisfies ReadyOnchainDeployment; + +const liveModel = { + status: "ready", + tokens: [], + snapshot: { + chainId: 1, + blockNumber: "200", + blockHash: `0x${"44".repeat(32)}`, + confirmations: 12, + }, + creatorClaims: [], + launcherFeesAccruedWei: "0", + launcherFeesAccruedEth: "0", +} satisfies ExploreReadModel; + +describe("Explore read source", () => { + it("falls through a stale durable snapshot to the live RPC model", async () => { + const readLive = vi.fn().mockResolvedValue(liveModel); + const enrichWithUsd = vi.fn().mockResolvedValue(liveModel); + const warn = vi.fn(); + const staleEnvelope = { + schemaVersion: "programmable-durable-index-v1" as const, + contentHash: `0x${"55".repeat(32)}` as const, + payload: { + generatedAt: "2026-07-31T00:00:00.000Z", + deployment: { + chainId: 1, + releaseVersion: "classic-v2", + launcher: config.launcher, + feeHook: config.feeHook, + }, + model: liveModel, + }, + }; + + const result = await resolveExploreReadSource(config, { + readDurable: vi.fn().mockResolvedValue({ + status: "unavailable", + reason: "stale", + detail: "snapshot is older than the hard freshness limit", + envelope: staleEnvelope, + ageMs: 901_000, + }), + selectFreshDurable: vi.fn().mockReturnValue(null), + readLive, + enrichWithUsd, + warn, + error: vi.fn(), + }); + + expect(result).toBe(liveModel); + expect(readLive).toHaveBeenCalledOnce(); + expect(enrichWithUsd).toHaveBeenCalledWith(liveModel, config); + expect(warn).toHaveBeenCalledWith( + "Durable Explore index unavailable; using live RPCs", + { reason: "stale", ageSeconds: 901 }, + ); + }); + + it("uses a fresh durable model without touching live RPCs", async () => { + const readLive = vi.fn(); + const enrichWithUsd = vi.fn().mockResolvedValue(liveModel); + + const result = await resolveExploreReadSource(config, { + readDurable: vi.fn().mockResolvedValue({ + status: "ready", + envelope: {} as never, + ageMs: 1_000, + }), + selectFreshDurable: vi.fn().mockReturnValue(liveModel), + readLive, + enrichWithUsd, + warn: vi.fn(), + error: vi.fn(), + }); + + expect(result).toBe(liveModel); + expect(readLive).not.toHaveBeenCalled(); + expect(enrichWithUsd).toHaveBeenCalledWith(liveModel, config); + }); +}); diff --git a/tests/profile-view.test.ts b/tests/profile-view.test.ts index 2706e250..50614277 100644 --- a/tests/profile-view.test.ts +++ b/tests/profile-view.test.ts @@ -19,6 +19,7 @@ import { sortProfileTokensByMarketCap, upsertPendingProfileTransactionRecords, waitForTransaction, + withoutClosedDeepProfileData, type PendingProfileTransactionRecord, } from "../components/profile-view"; import type { ClassicV3Reward } from "../lib/profile/classic-v3-rewards"; @@ -142,6 +143,59 @@ const deepV3Token = { } satisfies DeepV3CreatorToken; describe("profile reward grouping", () => { + it("removes closed Deep data from the public profile surface", () => { + const deepToken: ProfileToken = { + address: thirdAddress, + name: "Historical Deep", + symbol: "DEEP", + launchedAt: "Jul 29, 2026", + href: `/token/${thirdAddress}`, + launchModel: "deep", + }; + const filtered = withoutClosedDeepProfileData({ + status: "ready", + account: firstAddress, + chainId: 1, + tokens: [...tokens, deepToken], + positions: [ + { + id: `0x${"31".repeat(32)}`, + tokenAddress: thirdAddress, + tokenName: deepToken.name, + tokenSymbol: deepToken.symbol, + positionRecipient: firstAddress, + positionTokenId: "7", + lockStatus: "permanently-locked", + href: deepToken.href, + }, + ], + claims: [ + { + ...claim, + id: `0x${"41".repeat(32)}`, + tokenAddress: thirdAddress, + tokenName: deepToken.name, + tokenSymbol: deepToken.symbol, + href: deepToken.href, + }, + ], + activity: [ + { + id: "deep-activity", + label: "Deep launch", + detail: "Historical launch", + occurredAt: "Jul 29, 2026", + href: deepToken.href, + }, + ], + }); + + expect(filtered.tokens).toEqual(tokens); + expect(filtered.positions).toEqual([]); + expect(filtered.claims).toEqual([]); + expect(filtered.activity).toEqual([]); + }); + it("keeps deployed-token order and attaches each reward to its token", () => { const grouped = groupProfileRewards(tokens, [claim]); diff --git a/tests/public-route-handler-wiring.test.ts b/tests/public-route-handler-wiring.test.ts new file mode 100644 index 00000000..3b13cc50 --- /dev/null +++ b/tests/public-route-handler-wiring.test.ts @@ -0,0 +1,179 @@ +import { NextRequest } from "next/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const mocks = vi.hoisted(() => ({ + buildCreatorProfile: vi.fn(), + coordinatePublicRouteRead: vi.fn(), + preparePublicRouteRequest: vi.fn( + async (value: URLSearchParams, headers: Headers, _route: string) => { + void _route; + const search = new URLSearchParams(value); + if (headers.get("x-programmable-shadow-probe") === "error") { + return { + searchParams: search, + probeFailure: Response.json( + { error: "release_probe_temporarily_unavailable" }, + { + status: 503, + headers: { "Cache-Control": "private, no-store" }, + }, + ), + }; + } + const authorized = + headers.get("x-programmable-shadow-probe") === "1"; + if (authorized) { + search.delete("__read_model_probe"); + } + return { + searchParams: search, + ...(authorized ? { releaseProbe: Object.freeze({}) } : {}), + }; + }, + ), + readExploreModel: vi.fn(), +})); + +const fixtures = vi.hoisted(() => { + const discoveryScope = [ + { model: "classic", releaseVersion: "classic-v2" }, + { model: "classic", releaseVersion: "classic-v3" }, + { model: "stock-paired", releaseVersion: "stock-paired-v1" }, + { model: "stock-paired", releaseVersion: "stock-paired-v2" }, + { model: "stock-paired", releaseVersion: "stock-paired-v3" }, + ] as const; + return { + discoveryScope, + stockScope: discoveryScope.filter( + (scope) => scope.model === "stock-paired", + ), + }; +}); + +vi.mock("../lib/data-pipeline/public-route-readiness.server", () => ({ + coordinatePublicRouteRead: mocks.coordinatePublicRouteRead, + PUBLIC_DISCOVERY_ROUTE_SCOPES: fixtures.discoveryScope, + STOCK_PAIRED_ROUTE_SCOPES: fixtures.stockScope, + preparePublicRouteRequest: mocks.preparePublicRouteRequest, + publicSnapshotCheckpoint: (value: unknown) => value ?? undefined, +})); + +vi.mock("../lib/onchain", () => ({ + buildCreatorProfile: mocks.buildCreatorProfile, + readExploreModel: mocks.readExploreModel, +})); + +import { GET as creatorProfile } from "../app/api/explore/profile/route"; +import { GET as stockLaunchLookup } from "../app/api/explore/launch/stock-paired/route"; + +const ACCOUNT = "0x1111111111111111111111111111111111111111"; +const TRANSACTION = `0x${"22".repeat(32)}`; + +describe("public route coordinator wiring", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("wires creator profile through the aggregate reviewed scope", async () => { + const snapshot = { + blockNumber: "25650000", + blockHash: `0x${"33".repeat(32)}`, + }; + mocks.readExploreModel.mockResolvedValue({ status: "ready", snapshot }); + mocks.buildCreatorProfile.mockReturnValue({ + status: "ready", + account: ACCOUNT, + tokens: [], + }); + mocks.coordinatePublicRouteRead.mockImplementation( + async (input: { legacy: () => Promise<{ response: Response }> }) => + (await input.legacy()).response, + ); + + const response = await creatorProfile( + new NextRequest( + `http://localhost/api/explore/profile?account=${ACCOUNT}`, + ), + ); + + expect(response.status).toBe(200); + expect(mocks.coordinatePublicRouteRead).toHaveBeenCalledWith( + expect.objectContaining({ + route: "creator-profile", + scope: fixtures.discoveryScope, + }), + ); + expect(response.headers.get("Cache-Control")).toBe( + "private, max-age=0, s-maxage=15", + ); + }); + + it("wires Stock-Paired confirmation through the shared launch lookup gate", async () => { + mocks.coordinatePublicRouteRead.mockResolvedValue( + Response.json({ status: "coordinated" }), + ); + + const response = await stockLaunchLookup( + new NextRequest( + `http://localhost/api/explore/launch/stock-paired?account=${ACCOUNT}&transaction=${TRANSACTION}`, + ), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ status: "coordinated" }); + expect(mocks.coordinatePublicRouteRead).toHaveBeenCalledWith( + expect.objectContaining({ + route: "launch-lookup", + scope: fixtures.stockScope, + }), + ); + }); + + it("strips an authorized probe nonce before validating Stock-Paired lookup input", async () => { + mocks.coordinatePublicRouteRead.mockResolvedValue( + Response.json({ status: "coordinated" }), + ); + + const response = await stockLaunchLookup( + new NextRequest( + `http://localhost/api/explore/launch/stock-paired?account=${ACCOUNT}&transaction=${TRANSACTION}&__read_model_probe=release-1`, + { headers: { "x-programmable-shadow-probe": "1" } }, + ), + ); + + expect(response.status).toBe(200); + expect(mocks.preparePublicRouteRequest).toHaveBeenCalledTimes(1); + expect(mocks.coordinatePublicRouteRead).toHaveBeenCalledWith( + expect.objectContaining({ + route: "launch-lookup", + releaseProbe: expect.any(Object), + }), + ); + }); + + it("rejects invalid Stock-Paired lookup input before coordination", async () => { + const response = await stockLaunchLookup( + new NextRequest( + "http://localhost/api/explore/launch/stock-paired?account=bad&transaction=bad", + ), + ); + + expect(response.status).toBe(400); + expect(mocks.coordinatePublicRouteRead).not.toHaveBeenCalled(); + }); + + it("returns the private probe failure before route validation or coordination", async () => { + const response = await stockLaunchLookup( + new NextRequest( + `http://localhost/api/explore/launch/stock-paired?__read_model_probe=release-1`, + { headers: { "x-programmable-shadow-probe": "error" } }, + ), + ); + + expect(response.status).toBe(503); + expect(response.headers.get("Cache-Control")).toBe("private, no-store"); + expect(mocks.coordinatePublicRouteRead).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/public-route-readiness.test.ts b/tests/public-route-readiness.test.ts new file mode 100644 index 00000000..ec263446 --- /dev/null +++ b/tests/public-route-readiness.test.ts @@ -0,0 +1,488 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const nonceConsumerMocks = vi.hoisted(() => { + const consumed = new Set(); + return { + consumed, + consumeReleaseProbeNonce: vi.fn(async (input: { route: string; nonce: string }) => { + const key = `${input.route}:${input.nonce}`; + if (consumed.has(key)) return false; + consumed.add(key); + return true; + }), + }; +}); + +vi.mock("@/lib/data-pipeline/release-probe-nonce.server", () => ({ + consumeReleaseProbeNonce: nonceConsumerMocks.consumeReleaseProbeNonce, +})); + +import { + CLASSIC_V3_ROUTE_SCOPE, + PUBLIC_DISCOVERY_ROUTE_SCOPES, + preparePublicRouteRequest, + readExactPublicRouteSnapshot, + readExactRouteSnapshotReadiness, +} from "@/lib/data-pipeline/public-route-readiness.server"; +import type { PostgresTransaction } from "@/lib/data-pipeline/postgres"; +import { signRouteReleaseProbe } from "@/lib/data-pipeline/route-coordinator.server"; + +const HASH_A = `0x${"11".repeat(32)}` as const; +const HASH_B = `0x${"22".repeat(32)}` as const; +const COMMITMENT = `0x${"33".repeat(32)}` as const; +const ADDRESS = "0x1111111111111111111111111111111111111111"; + +function readinessRow( + input: Partial> = {}, +): Record { + return { + route_key: "classic-v3-profile", + chain_id: "1", + release_id: "classic-v3", + model_id: "classic", + source_group: "classic-mainnet", + route_status: "eligible", + eligibility_status: "eligible", + route_mode: "indexed", + projector_version: "projection-v3.1", + epoch_id: "00000000-0000-4000-8000-000000000001", + pointer_generation: "7", + checkpoint_id: "00000000-0000-4000-8000-000000000002", + checkpoint_generation: "11", + reorg_generation: "2", + checkpoint_block_number: "25650000", + checkpoint_block_hash: HASH_A, + safe_block_number: "25650012", + checkpoint_confirmations: "12", + parity_status: "current", + parity_record_id: "00000000-0000-4000-8000-000000000003", + reconciliation_id: "00000000-0000-4000-8000-000000000004", + parity_is_match: true, + parity_source_from_block: "25649900", + parity_source_to_block: "25650000", + parity_evidence_commitment: COMMITMENT, + reconciliation_mismatch_count: "0", + parity_checkpoint_id: "00000000-0000-4000-8000-000000000002", + parity_checkpoint_generation: "11", + parity_reorg_generation: "2", + parity_block_number: "25650000", + parity_block_hash: HASH_A, + parity_binding_id: "00000000-0000-4000-8000-000000000005", + parity_binding_commitment: COMMITMENT, + ...input, + }; +} + +function transaction(rows: readonly Record[]) { + const query = vi.fn(async () => rows); + return { + query, + transaction: { query } as PostgresTransaction, + }; +} + +describe("exact public route readiness", () => { + beforeEach(() => { + nonceConsumerMocks.consumed.clear(); + nonceConsumerMocks.consumeReleaseProbeNonce.mockClear(); + }); + + it("removes an internal cache nonce only for the authorized probe", async () => { + const token = "p".repeat(48); + const nonce = `${Date.now()}-${"ab".repeat(32)}-1`; + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", token); + try { + const input = new URLSearchParams( + `address=${ADDRESS}&__read_model_probe=${nonce}`, + ); + const authorized = await preparePublicRouteRequest( + input, + new Headers({ + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-signature": signRouteReleaseProbe({ + route: "explore-token", + nonce, + secret: token, + }), + }), + "explore-token", + ); + const unauthenticated = await preparePublicRouteRequest( + input, + new Headers(), + "explore-token", + ); + + expect(authorized.searchParams.has("__read_model_probe")).toBe(false); + expect(authorized.searchParams.get("address")).toBe(ADDRESS); + expect(authorized.releaseProbe).toBeDefined(); + expect( + unauthenticated.searchParams.get("__read_model_probe"), + ).toBe(nonce); + expect(unauthenticated.releaseProbe).toBeUndefined(); + expect(input.get("__read_model_probe")).toBe(nonce); + expect(nonceConsumerMocks.consumeReleaseProbeNonce).toHaveBeenCalledTimes( + 1, + ); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("returns a private fail-closed response when distributed nonce consumption is unavailable", async () => { + const token = "r".repeat(48); + const nonce = `${Date.now()}-${"ef".repeat(32)}-5`; + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", token); + nonceConsumerMocks.consumeReleaseProbeNonce.mockRejectedValueOnce( + new Error("database unavailable"), + ); + + const prepared = await preparePublicRouteRequest( + new URLSearchParams(`__read_model_probe=${nonce}`), + new Headers({ + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-signature": signRouteReleaseProbe({ + route: "explore-token", + nonce, + secret: token, + }), + }), + "explore-token", + ); + + expect(prepared.releaseProbe).toBeUndefined(); + expect(prepared.searchParams.get("__read_model_probe")).toBe(nonce); + expect(prepared.probeFailure?.status).toBe(503); + expect(prepared.probeFailure?.headers.get("Cache-Control")).toBe( + "private, no-store", + ); + }); + + it("leaves malformed probe parameters to ordinary route validation without a database read", async () => { + const prepared = await preparePublicRouteRequest( + new URLSearchParams("__read_model_probe=malformed"), + new Headers({ + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-signature": "a".repeat(64), + }), + "explore-token", + ); + + expect(prepared.releaseProbe).toBeUndefined(); + expect(prepared.probeFailure).toBeUndefined(); + expect(prepared.searchParams.get("__read_model_probe")).toBe("malformed"); + expect(nonceConsumerMocks.consumeReleaseProbeNonce).not.toHaveBeenCalled(); + }); + + it("rejects duplicated reserved nonces without a database read", async () => { + const nonce = `${Date.now()}-${"fa".repeat(32)}-6`; + const prepared = await preparePublicRouteRequest( + new URLSearchParams( + `__read_model_probe=${nonce}&__read_model_probe=${nonce}`, + ), + new Headers({ + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-signature": "a".repeat(64), + }), + "explore-token", + ); + + expect(prepared.releaseProbe).toBeUndefined(); + expect(prepared.searchParams.getAll("__read_model_probe")).toEqual([ + nonce, + nonce, + ]); + expect(nonceConsumerMocks.consumeReleaseProbeNonce).not.toHaveBeenCalled(); + }); + + it("rejects stale, globally replayed and incorrectly authenticated probe nonces", async () => { + const token = "q".repeat(48); + const now = Date.now(); + const freshNonce = `${now}-${"bc".repeat(32)}-2`; + const staleNonce = `${now - 5 * 60 * 1_000 - 1}-${"cd".repeat(32)}-3`; + vi.stubEnv("PROGRAMMABLE_SHADOW_PROBE_TOKEN", token); + try { + const signedHeaders = (nonce: string, secret = token) => + new Headers({ + "x-programmable-shadow-probe": "1", + "x-programmable-shadow-probe-signature": signRouteReleaseProbe({ + route: "explore-token", + nonce, + secret, + }), + }); + const prepare = ( + nonce: string, + requestHeaders = signedHeaders(nonce), + ) => + preparePublicRouteRequest( + new URLSearchParams(`__read_model_probe=${nonce}`), + requestHeaders, + "explore-token", + ); + + expect((await prepare(freshNonce)).releaseProbe).toBeDefined(); + expect((await prepare(freshNonce)).releaseProbe).toBeUndefined(); + expect((await prepare(freshNonce)).searchParams.get("__read_model_probe")).toBe( + freshNonce, + ); + expect((await prepare(staleNonce)).releaseProbe).toBeUndefined(); + expect( + (await prepare( + `${now}-${"de".repeat(32)}-4`, + signedHeaders( + `${now}-${"de".repeat(32)}-4`, + "x".repeat(48), + ), + )).releaseProbe, + ).toBeUndefined(); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("binds one adapted payload to the exact readiness generation", async () => { + const mock = transaction([readinessRow()]); + const indexed = vi.fn(async () => ({ + status: "ready" as const, + routeKey: "classic-v3-profile" as const, + snapshot: { + adapterVersion: "indexed-route-adapters-v2" as const, + snapshotCommitment: COMMITMENT, + chainId: 1 as const, + blockNumber: "25650000", + blockHash: HASH_A, + confirmations: 12, + capturedAt: "2026-07-31T10:00:00.000Z", + releasePointers: [ + { + routeKey: "classic-v3-profile" as const, + chainId: 1 as const, + releaseVersion: "classic-v3" as const, + modelVersion: "classic" as const, + sourceGroup: "classic-mainnet", + projectorVersion: "projection-v3.1", + epochId: "00000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointId: "00000000-0000-4000-8000-000000000002", + checkpointGeneration: "11", + reorgGeneration: "2", + checkpointBlockNumber: "25650000", + checkpointBlockHash: HASH_A, + }, + ], + }, + recordSources: [], + response: { + status: 200, + body: { + status: "ready", + account: "0x1111111111111111111111111111111111111111", + chainId: 1, + snapshotBlock: "25650000", + rewards: [], + }, + headers: { "Cache-Control": "no-store" }, + }, + })); + + const result = await readExactPublicRouteSnapshot({ + transaction: mock.transaction, + route: "classic-v3-profile", + scope: CLASSIC_V3_ROUTE_SCOPE, + indexed, + }); + + expect(indexed).toHaveBeenCalledWith(mock.transaction); + expect(result.indexed).toMatchObject({ + source: "indexed", + scopeEvidence: { recordCount: 0, recordScopes: [] }, + comparisonCheckpoint: { + blockNumber: "25650000", + blockHash: HASH_A, + }, + versions: [ + { + model: "classic", + releaseVersion: "classic-v3", + version: { checkpointGeneration: "11", reorgGeneration: "2" }, + }, + ], + }); + expect(result.indexed?.response.status).toBe(200); + await expect(result.indexed?.response.json()).resolves.toMatchObject({ + status: "ready", + rewards: [], + }); + }); + + it("does not query a payload unless every exact scope has current parity", async () => { + const mock = transaction([readinessRow({ parity_status: "pending" })]); + const indexed = vi.fn(); + + const result = await readExactPublicRouteSnapshot({ + transaction: mock.transaction, + route: "classic-v3-profile", + scope: CLASSIC_V3_ROUTE_SCOPE, + indexed, + }); + + expect(indexed).not.toHaveBeenCalled(); + expect(result).not.toHaveProperty("indexed"); + expect(result.readiness[0]?.parity).toBe("pending"); + }); + + it("uses the API-reader-only exact readiness view and binds parameters", async () => { + const mock = transaction([readinessRow()]); + const result = await readExactRouteSnapshotReadiness({ + transaction: mock.transaction, + route: "classic-v3-profile", + chainId: 1, + scope: CLASSIC_V3_ROUTE_SCOPE, + }); + + expect(mock.query).toHaveBeenCalledTimes(1); + const [sql, values] = mock.query.mock.calls[0] as unknown as [ + string, + unknown[], + ]; + expect(sql).toContain( + "from programmable_private.route_snapshot_readiness_v1", + ); + expect(sql).toContain("checkpoint_confirmations"); + expect(sql).not.toMatch(/\b(insert|update|delete|call)\b/i); + expect(values).toEqual([ + "classic-v3-profile", + 1, + ["classic-v3"], + ["classic"], + ]); + expect(result).toEqual({ + readiness: [ + { + model: "classic", + releaseVersion: "classic-v3", + eligibility: "eligible", + parity: "current", + version: { + checkpointId: "00000000-0000-4000-8000-000000000002", + sourceGroup: "classic-mainnet", + projectorVersion: "projection-v3.1", + epochId: "00000000-0000-4000-8000-000000000001", + pointerGeneration: "7", + checkpointGeneration: "11", + reorgGeneration: "2", + blockNumber: "25650000", + blockHash: HASH_A, + }, + }, + ], + }); + expect(result).not.toHaveProperty("indexed"); + }); + + it("returns missing/ineligible members for absent reviewed releases", async () => { + const mock = transaction([]); + const result = await readExactRouteSnapshotReadiness({ + transaction: mock.transaction, + route: "explore-list", + chainId: 1, + scope: PUBLIC_DISCOVERY_ROUTE_SCOPES, + }); + + expect(result.readiness).toHaveLength(5); + expect(result.readiness).toEqual( + PUBLIC_DISCOVERY_ROUTE_SCOPES.map((scope) => ({ + ...scope, + eligibility: "ineligible", + parity: "missing", + })), + ); + }); + + it("treats parity bound to a different checkpoint as stale", async () => { + const mock = transaction([ + readinessRow({ parity_block_hash: HASH_B }), + ]); + const result = await readExactRouteSnapshotReadiness({ + transaction: mock.transaction, + route: "classic-v3-profile", + chainId: 1, + scope: CLASSIC_V3_ROUTE_SCOPE, + }); + + expect(result.readiness[0]).toEqual({ + model: "classic", + releaseVersion: "classic-v3", + eligibility: "eligible", + parity: "stale", + }); + }); + + it("keeps a recorded mismatch distinct from stale parity", async () => { + const mock = transaction([ + readinessRow({ + parity_status: "mismatch", + parity_is_match: false, + reconciliation_mismatch_count: "1", + }), + ]); + const result = await readExactRouteSnapshotReadiness({ + transaction: mock.transaction, + route: "classic-v3-profile", + chainId: 1, + scope: CLASSIC_V3_ROUTE_SCOPE, + }); + + expect(result.readiness[0]?.parity).toBe("mismatch"); + expect(result.readiness[0]).not.toHaveProperty("version"); + }); + + it("rejects ambiguous current source groups", async () => { + const mock = transaction([ + readinessRow(), + readinessRow({ source_group: "classic-mainnet-b" }), + ]); + await expect( + readExactRouteSnapshotReadiness({ + transaction: mock.transaction, + route: "classic-v3-profile", + chainId: 1, + scope: CLASSIC_V3_ROUTE_SCOPE, + }), + ).rejects.toThrow("Ambiguous route readiness source group"); + }); + + it("rejects unsupported Deep or Adaptive rows returned by the database", async () => { + const mock = transaction([ + readinessRow({ + release_id: "deep-v3", + model_id: "deep", + }), + ]); + await expect( + readExactRouteSnapshotReadiness({ + transaction: mock.transaction, + route: "classic-v3-profile", + chainId: 1, + scope: CLASSIC_V3_ROUTE_SCOPE, + }), + ).rejects.toThrow("Unsupported route readiness release"); + }); + + it("propagates database unavailability for coordinator fallback", async () => { + const query = vi.fn(async () => { + throw new Error("database unavailable"); + }); + await expect( + readExactRouteSnapshotReadiness({ + transaction: { query }, + route: "classic-v3-profile", + chainId: 1, + scope: CLASSIC_V3_ROUTE_SCOPE, + }), + ).rejects.toThrow("database unavailable"); + }); +}); diff --git a/tests/stock-paired-access.test.ts b/tests/stock-paired-access.test.ts index ab72f098..50871f76 100644 --- a/tests/stock-paired-access.test.ts +++ b/tests/stock-paired-access.test.ts @@ -7,14 +7,15 @@ import { } from "../lib/stock-paired-access"; describe("Stock-Paired access", () => { - it("binds the reviewed switch to V3 on Ethereum Mainnet only", () => { + it("keeps new launches closed for every environment and release", () => { const release = { internalContractRelease: "stock-paired-v3", chainId: 1, }; + expect(STOCK_PAIRED_NEW_LAUNCHES_ENABLED).toBe(false); expect( isStockPairedPublicLaunchEnabled("production", release), - ).toBe(STOCK_PAIRED_NEW_LAUNCHES_ENABLED); + ).toBe(false); expect( isStockPairedPublicLaunchEnabled("rehearsal", release), ).toBe(false); @@ -43,12 +44,12 @@ describe("Stock-Paired access", () => { } }); - it("never gives an unreviewed wallet privileged access", () => { + it("does not retain a privileged launch wallet", () => { expect( isStockPairedDevAccount( "0x2Bb333d48DFAF1596D9036671d2E43168994249E", ), - ).toBe(STOCK_PAIRED_NEW_LAUNCHES_ENABLED); + ).toBe(false); expect( isStockPairedDevAccount( "0x1111111111111111111111111111111111111111", diff --git a/tests/stock-paired-action-activation.test.ts b/tests/stock-paired-action-activation.test.ts new file mode 100644 index 00000000..4ed78be9 --- /dev/null +++ b/tests/stock-paired-action-activation.test.ts @@ -0,0 +1,316 @@ +import { NextRequest } from "next/server"; +import { + getAddress, + keccak256, + type Address, + type Hex, +} from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const mocks = vi.hoisted(() => ({ + indexedEnabled: true, + lookup: vi.fn(), + readLegacy: vi.fn(), + createPublicClient: vi.fn(), +})); + +const account = getAddress("0x1111111111111111111111111111111111111111"); +const token = getAddress("0x2222222222222222222222222222222222222222"); +const hook = getAddress("0x3333333333333333333333333333333333333333"); +const vault = getAddress("0x4444444444444444444444444444444444444444"); +const launcher = getAddress("0x5555555555555555555555555555555555555555"); +const factory = getAddress("0x6666666666666666666666666666666666666666"); +const quote = getAddress("0x7777777777777777777777777777777777777777"); +const hookCode = "0x6001600155" as Hex; +const factoryCode = "0x6002600255" as Hex; +const vaultCode = "0x6003600355" as Hex; +const poolId = `0x${"88".repeat(32)}` as Hex; +const blockHash = `0x${"99".repeat(32)}` as Hex; +const launchTransactionHash = `0x${"aa".repeat(32)}` as Hex; +const configurationHash = `0x${"bb".repeat(32)}` as Hex; + +const release = { + internalContractRelease: "stock-paired-v1" as const, + addresses: { + feeHook: hook, + feeSplitVaultFactory: factory, + launcher, + }, + runtimeCodeHashes: { + feeHook: keccak256(hookCode), + feeSplitVaultFactory: keccak256(factoryCode), + }, +}; + +const indexedToken = { + chainId: 1 as const, + releaseVersion: "stock-paired-v1" as const, + modelVersion: "stock-paired" as const, + tokenAddress: token, + creatorAddress: account, + launchTransactionHash, + poolId, + rewardVaultAddress: vault, + launchHash: `0x${"cc".repeat(32)}` as Hex, + tokenName: "Stock Token", + tokenSymbol: "STK", + totalSupplyRaw: "1000000000000000000000000000", + launchedAt: "2026-07-31T00:00:00.000Z", + hookAddress: hook, + quoteAssetAddress: quote, + totalSwapFeeBps: 100, + buySwapFeeBps: 100, + sellSwapFeeBps: 100, + buyCreatorFeeBps: 90, + sellCreatorFeeBps: 90, + creatorFeeBps: 90, + launcherFeeBps: 10, + transferTaxBps: 0, + lpFeePips: 0, + promotedBlockNumber: "100", + promotedBlockHash: blockHash, + verifiedAt: "2026-07-31T00:01:00.000Z", +}; + +const actionReward = { + chainId: 1 as const, + account, + vaultAddress: vault, + poolId, + hookAddress: hook, + quoteAssetAddress: quote, + claimableRaw: "1100000000000000000", + claimedRaw: "0", + entitledRaw: "1100000000000000000", + releaseVersion: "stock-paired-v1" as const, + modelVersion: "stock-paired" as const, + promotedBlockNumber: "100", + promotedBlockHash: blockHash, + verifiedAt: "2026-07-31T00:01:00.000Z", + token: indexedToken, +}; + +const launcherToken = { + id: `1:${token}`, + name: "Stock Token", + symbol: "STK", + tokenAddress: token, + hookAddress: hook, + poolId, + creatorAddress: account, + rewardVaultAddress: vault, + quoteAssetAddress: quote, + launchTransactionHash, + launchedAt: "2026-07-31T00:00:00.000Z", + totalSwapFeeBps: 100, + launchModel: "stock-paired" as const, + launchModelVersion: "stock-paired-v1" as const, + liquidityPath: "meme" as const, +}; + +const legacyModel = { + status: "ready" as const, + tokens: [launcherToken], + snapshot: { + chainId: 1, + blockNumber: "100", + blockHash, + confirmations: 12, + }, + creatorClaims: [], + launcherFeesAccruedWei: "0", + launcherFeesAccruedEth: "0", +}; + +function rpcClient(index: number) { + return { + getBlockNumber: vi.fn().mockResolvedValue(120n), + getBlock: vi.fn().mockResolvedValue({ hash: blockHash }), + getCode: vi.fn(({ address }: { address: Address }) => { + const normalized = address.toLowerCase(); + if (normalized === hook.toLowerCase()) return Promise.resolve(hookCode); + if (normalized === factory.toLowerCase()) { + return Promise.resolve(factoryCode); + } + if (normalized === vault.toLowerCase()) return Promise.resolve(vaultCode); + return Promise.resolve("0x" as Hex); + }), + readContract: vi.fn( + ({ functionName }: { functionName: string }) => { + switch (functionName) { + case "isFactoryVault": + return Promise.resolve(true); + case "feeHook": + return Promise.resolve(hook); + case "poolId": + return Promise.resolve(poolId); + case "quoteAsset": + return Promise.resolve(quote); + case "configurationHash": + return Promise.resolve(configurationHash); + case "beneficiaryCount": + return Promise.resolve(1n); + case "shareBpsOf": + return Promise.resolve(10_000n); + case "payoutAddressOf": + case "beneficiaryAt": + return Promise.resolve(account); + case "claimedBy": + return Promise.resolve(0n); + case "totalCreatorFeesReceived": + return Promise.resolve(10n ** 18n); + case "poolFeeConfig": + return Promise.resolve([ + quote, + token, + vault, + launcher, + true, + true, + 10n ** 17n, + ]); + default: + throw new Error(`Unexpected function ${functionName}`); + } + }, + ), + call: vi.fn().mockResolvedValue({ data: "0x" }), + estimateGas: vi.fn().mockResolvedValue(100_000n + BigInt(index)), + getGasPrice: vi.fn().mockResolvedValue(2_000_000_000n + BigInt(index)), + getBalance: vi.fn().mockResolvedValue(10n ** 18n), + }; +} + +vi.mock("../lib/data-pipeline/route-activation.server", () => ({ + indexedLaunchLookupEnabled: () => mocks.indexedEnabled, +})); + +vi.mock("../lib/data-pipeline/action-lookup", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + lookupActionReward: mocks.lookup, + }; +}); + +vi.mock("../lib/onchain", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getOnchainDeployment: () => ({ status: "ready", chainId: 1 }), + readExploreModel: mocks.readLegacy, + }; +}); + +vi.mock("../lib/stock-paired", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getStockPairedQuoteAssetForRelease: () => ({ + address: quote, + symbol: "USDY", + }), + }; +}); + +vi.mock("../lib/stock-paired-release", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + getConfiguredStockPairedReleaseByHookAndVersion: () => release, + }; +}); + +vi.mock("viem", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createPublicClient: mocks.createPublicClient, + }; +}); + +import { POST } from "../app/api/profile/stock-paired/route"; + +function request() { + return new NextRequest("http://localhost/api/profile/stock-paired", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + action: "claim", + account, + vaultAddress: vault, + chainId: 1, + }), + }); +} + +describe("Stock-Paired action identity activation", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubEnv( + "ETHEREUM_RPC_URL", + "https://eth-mainnet.g.alchemy.com/v2/alchemy-stock-key", + ); + vi.stubEnv( + "ETHEREUM_RPC_URL_B", + "https://stock-node.quiknode.pro/quicknode-stock-key/", + ); + mocks.lookup.mockResolvedValue(actionReward); + mocks.readLegacy.mockResolvedValue(legacyModel); + let clientIndex = 0; + mocks.createPublicClient.mockImplementation(() => + rpcClient(clientIndex++), + ); + }); + + it.each([true, false])( + "uses the same multi-provider state checks and simulations with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + + const response = await POST(request()); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + status: "ready", + action: "claim", + account, + vaultAddress: vault, + transaction: { + kind: "claim-stock-paired-rewards", + chainId: 1, + from: account, + to: vault, + }, + }); + expect(mocks.createPublicClient.mock.calls.length).toBeGreaterThanOrEqual( + 2, + ); + expect(mocks.lookup).toHaveBeenCalledTimes(indexedEnabled ? 1 : 0); + expect(mocks.readLegacy).toHaveBeenCalledTimes(indexedEnabled ? 0 : 1); + }, + ); + + it.each([true, false])( + "fails closed on same-provider aliases with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + vi.stubEnv( + "ETHEREUM_RPC_URL_B", + "https://eth-mainnet.g.alchemy.com/v2/second-stock-secret", + ); + + const response = await POST(request()); + const serialized = JSON.stringify(await response.json()); + + expect(response.status).toBe(503); + expect(mocks.createPublicClient).not.toHaveBeenCalled(); + expect(serialized).not.toContain("alchemy-stock-key"); + expect(serialized).not.toContain("second-stock-secret"); + }, + ); +}); diff --git a/tests/stock-paired-public-activation.test.ts b/tests/stock-paired-public-activation.test.ts index ad1a282a..1a12e06a 100644 --- a/tests/stock-paired-public-activation.test.ts +++ b/tests/stock-paired-public-activation.test.ts @@ -1,70 +1,26 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { NextRequest } from "next/server"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; +import LaunchPage from "../app/launch/page"; +import { POST } from "../app/api/launch/preflight/route"; import { createStockPairedDraft } from "../lib/launch"; import { STOCK_PAIRED_ETH_QUOTE_ASSETS } from "../lib/stock-paired"; const publicAccount = "0x1111111111111111111111111111111111111111"; -const readyV3Release = { - internalContractRelease: "stock-paired-v3", - chainId: 1, -}; - -async function loadPublicSurface( - release: typeof readyV3Release | null, - enabled = false, -) { - vi.resetModules(); - vi.doMock("@/lib/stock-paired-release", async (importOriginal) => { - const original = - await importOriginal< - typeof import("../lib/stock-paired-release") - >(); - return { - ...original, - getConfiguredStockPairedLaunchRelease: () => release, - }; - }); - vi.doMock("@/lib/stock-paired-access", async (importOriginal) => { - const original = - await importOriginal< - typeof import("../lib/stock-paired-access") - >(); - return { - ...original, - isStockPairedPublicLaunchEnabled: ( - environment: "production" | "rehearsal", - candidate: typeof readyV3Release | null, - ) => - Boolean( - enabled && - environment === "production" && - candidate?.internalContractRelease === "stock-paired-v3" && - candidate.chainId === 1, - ), - }; - }); - - const [{ default: LaunchPage }, { POST }] = await Promise.all([ - import("../app/launch/page"), - import("../app/api/launch/preflight/route"), - ]); - return { LaunchPage, POST }; -} function publicPreflightRequest() { return new NextRequest("http://localhost/api/launch/preflight", { method: "POST", body: JSON.stringify({ account: publicAccount, - walletChainId: "0xaa36a7", + walletChainId: "0x1", draft: { ...createStockPairedDraft(), - tokenName: "Public Stock Pair", - tokenSymbol: "PSP", - tokenDescription: "Public activation regression test", + tokenName: "Historical Stock Pair", + tokenSymbol: "HSP", + tokenDescription: "Closed launch model regression test", initialBuyEth: "0.01", stockQuoteAsset: STOCK_PAIRED_ETH_QUOTE_ASSETS[0].address, launchSalt: `0x${"42".repeat(32)}`, @@ -73,97 +29,23 @@ function publicPreflightRequest() { }); } -afterEach(() => { - vi.doUnmock("@/lib/stock-paired-release"); - vi.doUnmock("@/lib/stock-paired-access"); - vi.resetModules(); -}); - -describe.sequential("Stock-Paired public activation", () => { - it("keeps UI and preflight closed before the explicit V3 activation", async () => { - const { LaunchPage, POST } = await loadPublicSurface(readyV3Release); +describe("Stock-Paired launch closure", () => { + it("removes Stock-Paired from the public launch picker", () => { const html = renderToStaticMarkup(createElement(LaunchPage)); - const stockButton = html.match( - /]*data-launch-model-option="stock-paired"[^>]*>/, - )?.[0]; - - expect(stockButton).toContain("disabled"); - expect(html).toContain("Coming soon"); - const result = await POST(publicPreflightRequest()); - expect(result.status).toBe(403); - await expect(result.json()).resolves.toEqual({ - error: "Stock-Paired is coming soon", - }); + expect(html).toContain('data-launch-model-option="classic"'); + expect(html).not.toContain('data-launch-model-option="stock-paired"'); + expect(html).not.toContain("Stock-Paired"); }); - it("also stays closed without a verified V3 release", async () => { - const { LaunchPage, POST } = await loadPublicSurface(null); - const html = renderToStaticMarkup(createElement(LaunchPage)); - const stockButton = html.match( - /]*data-launch-model-option="stock-paired"[^>]*>/, - )?.[0]; - - expect(stockButton).toContain("disabled"); - expect(html).toContain("Coming soon"); - + it("rejects a valid direct launch request with a stable response", async () => { const result = await POST(publicPreflightRequest()); - expect(result.status).toBe(403); - await expect(result.json()).resolves.toEqual({ - error: "Stock-Paired is coming soon", - }); - }); - - it("opens the Stock-Paired UX only for the verified Mainnet release", async () => { - const { LaunchPage, POST } = await loadPublicSurface( - readyV3Release, - true, - ); - const html = renderToStaticMarkup(createElement(LaunchPage)); - const stockButton = html.match( - /]*data-launch-model-option="stock-paired"[^>]*>/, - )?.[0]; - - expect(stockButton).not.toContain("disabled"); - expect(html).not.toContain("Stock-PairedComing soon"); - - const result = await POST(publicPreflightRequest()); - expect(result.status).toBe(200); - await expect(result.json()).resolves.toMatchObject({ - status: "blocked", - mode: "stock-paired", - title: "Switch the wallet to Ethereum", - checks: [ - { id: "token", status: "pass" }, - { id: "wallet", status: "blocked" }, - ], - }); - }); - - it("ships the checked-in verified V3 release open on Ethereum Mainnet", async () => { - vi.resetModules(); - const [{ default: LaunchPage }, { POST }] = await Promise.all([ - import("../app/launch/page"), - import("../app/api/launch/preflight/route"), - ]); - const html = renderToStaticMarkup(createElement(LaunchPage)); - const stockButton = html.match( - /]*data-launch-model-option="stock-paired"[^>]*>/, - )?.[0]; - expect(stockButton).not.toContain("disabled"); - expect(html).not.toContain("Stock-PairedComing soon"); - - const result = await POST(publicPreflightRequest()); - expect(result.status).toBe(200); - await expect(result.json()).resolves.toMatchObject({ - status: "blocked", - mode: "stock-paired", - title: "Switch the wallet to Ethereum", - checks: [ - { id: "token", status: "pass" }, - { id: "wallet", status: "blocked" }, - ], + expect(result.status).toBe(410); + expect(result.headers.get("cache-control")).toBe("no-store"); + await expect(result.json()).resolves.toEqual({ + code: "stock_paired_launches_closed", + error: "New Stock-Paired launches are no longer available", }); }); }); diff --git a/tests/token-chart-api.test.ts b/tests/token-chart-api.test.ts index d47c9913..f8e2b6f5 100644 --- a/tests/token-chart-api.test.ts +++ b/tests/token-chart-api.test.ts @@ -1,6 +1,8 @@ import { NextRequest } from "next/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; +vi.mock("server-only", () => ({})); + const mocks = vi.hoisted(() => ({ getPublicOnchainDeployment: vi.fn(), isTokenChartRange: vi.fn(), @@ -76,6 +78,7 @@ describe("token chart API", () => { }), ); await expect(response.json()).resolves.toMatchObject({ + address: token.tokenAddress, range: "1h", swapCount: 2, volumeWei: "1250000000000000000", @@ -83,7 +86,7 @@ describe("token chart API", () => { volumeUsdWad: "4375000000000000000000", }); expect(response.headers.get("Cache-Control")).toBe( - "public, max-age=0, s-maxage=15, stale-while-revalidate=15", + "public, max-age=0, s-maxage=2, stale-while-revalidate=2", ); }); }); diff --git a/tests/token-detail-api.test.ts b/tests/token-detail-api.test.ts index b0c857c6..3d65caad 100644 --- a/tests/token-detail-api.test.ts +++ b/tests/token-detail-api.test.ts @@ -1,6 +1,8 @@ import { NextRequest } from "next/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; +vi.mock("server-only", () => ({})); + import type { ExploreReadModel } from "../lib/onchain/types"; import type { LauncherToken } from "../lib/tokens"; diff --git a/tests/trade-action-activation.test.ts b/tests/trade-action-activation.test.ts new file mode 100644 index 00000000..2a78aca6 --- /dev/null +++ b/tests/trade-action-activation.test.ts @@ -0,0 +1,164 @@ +import { NextRequest } from "next/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("server-only", () => ({})); + +const mocks = vi.hoisted(() => ({ + indexedEnabled: true, + lookup: vi.fn(), + readLegacy: vi.fn(), + prepare: vi.fn(), + createPublicClient: vi.fn(() => ({})), +})); + +const token = "0x1111111111111111111111111111111111111111"; +const registry = { + status: "ready" as const, + tokens: [ + { + id: `1:${token}`, + name: "Action Token", + symbol: "ACT", + tokenAddress: token, + hookAddress: "0x2222222222222222222222222222222222222222", + poolId: `0x${"11".repeat(32)}`, + launchedAt: "2026-07-31T00:00:00.000Z", + totalSwapFeeBps: 100, + launchModel: "classic" as const, + liquidityPath: "meme" as const, + }, + ], + snapshot: { + chainId: 1, + blockNumber: "100", + blockHash: `0x${"22".repeat(32)}`, + confirmations: 12, + }, + creatorClaims: [], + launcherFeesAccruedWei: "0", + launcherFeesAccruedEth: "0", +}; + +vi.mock("../lib/data-pipeline/route-activation.server", () => ({ + indexedLaunchLookupEnabled: () => mocks.indexedEnabled, +})); + +vi.mock("../lib/data-pipeline/action-lookup", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + lookupActionTokenByAddress: mocks.lookup, + actionTokenAsExploreModel: () => registry, + }; +}); + +vi.mock("../lib/onchain", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getOnchainDeployment: () => ({ status: "ready", chainId: 1 }), + readExploreModel: mocks.readLegacy, + }; +}); + +vi.mock("../lib/trade/server", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getPinnedOfficialTradeStack: vi.fn(), + resolveTradeDeployment: vi.fn(() => ({})), + prepareClassicTrade: mocks.prepare, + }; +}); + +vi.mock("viem", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createPublicClient: mocks.createPublicClient, + }; +}); + +import { POST } from "../app/api/trade/prepare/route"; + +const body = { + chainId: 1, + owner: "0x5555555555555555555555555555555555555555", + token, + side: "buy", + amountIn: "1000000000000000", + slippageBps: 100, + deadline: "2000000000", +}; + +function request() { + return new NextRequest("http://localhost/api/trade/prepare", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +describe("trade action identity activation", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubEnv( + "ETHEREUM_RPC_URL", + "https://eth-mainnet.g.alchemy.com/v2/alchemy-action-key", + ); + vi.stubEnv( + "ETHEREUM_RPC_URL_B", + "https://action-node.quiknode.pro/quicknode-action-key/", + ); + mocks.lookup.mockResolvedValue({}); + mocks.readLegacy.mockResolvedValue(registry); + mocks.prepare + .mockResolvedValueOnce({ + quote: { amountOut: "100" }, + transaction: { kind: "swap" }, + }) + .mockResolvedValueOnce({ + quote: { amountOut: "99" }, + transaction: { kind: "swap" }, + }); + }); + + it.each([true, false])( + "uses two independent RPC preparations with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + + const response = await POST(request()); + + expect(response.status).toBe(200); + expect(mocks.prepare).toHaveBeenCalledTimes(2); + expect(mocks.createPublicClient).toHaveBeenCalledTimes(2); + expect(mocks.lookup).toHaveBeenCalledTimes(indexedEnabled ? 1 : 0); + expect(mocks.readLegacy).toHaveBeenCalledTimes(indexedEnabled ? 0 : 1); + await expect(response.json()).resolves.toMatchObject({ + quote: { amountOut: "99" }, + }); + }, + ); + + it.each([true, false])( + "fails closed on same-provider key aliases with indexed lookup %s", + async (indexedEnabled) => { + mocks.indexedEnabled = indexedEnabled; + vi.stubEnv( + "ETHEREUM_RPC_URL_B", + "https://eth-mainnet.g.alchemy.com/v2/second-secret-key", + ); + + const response = await POST(request()); + const serialized = JSON.stringify(await response.json()); + + expect(response.status).toBe(502); + expect(mocks.prepare).not.toHaveBeenCalled(); + expect(mocks.createPublicClient).not.toHaveBeenCalled(); + expect(serialized).not.toContain("alchemy-action-key"); + expect(serialized).not.toContain("second-secret-key"); + }, + ); +}); diff --git a/tests/trade-api.test.ts b/tests/trade-api.test.ts index 33ea1522..9de6254e 100644 --- a/tests/trade-api.test.ts +++ b/tests/trade-api.test.ts @@ -1,6 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; +vi.mock("server-only", () => ({})); + import { POST } from "../app/api/trade/prepare/route"; const baseBody = { diff --git a/tsconfig.json b/tsconfig.json index c40d8ccb..6e136b96 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ "exclude": [ "node_modules", "work", + "indexer", "contracts/lib", "contracts/out", "contracts/cache", diff --git a/vercel.json b/vercel.json index f9bc04dd..f59d8dc6 100644 --- a/vercel.json +++ b/vercel.json @@ -4,6 +4,14 @@ { "path": "/api/ops/index-v2", "schedule": "*/5 * * * *" + }, + { + "path": "/api/ops/projector", + "schedule": "* * * * *" + }, + { + "path": "/api/ops/market-projector", + "schedule": "* * * * *" } ] }