From 7b668019e41c9d17d94d3cfc13860384f7a2d230 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 13 Sep 2026 15:20:34 +0300 Subject: [PATCH 1/2] fix: make payment deployment resolution fail closed --- ...il-closed-payment-deployment-resolution.md | 122 +++++ .../aifinp-223-224-payment-registry.md | 348 +++++++++++++ docs/business/aifinp-223-224-analysis.md | 153 ++++++ docs/business/aifinp-223-224-prd.md | 309 +++++++++++ ...NP-223-224_IMPLEMENTATION_2026-09-13_RU.md | 113 ++++ docs/stories/aifinp-223-224-tests.md | 363 +++++++++++++ mcp/package-lock.json | 27 +- node/package-lock.json | 4 +- node/package.json | 8 +- node/registry/payment-deployments.json | 442 ++++++++++++++++ node/registry/payment-deployments.schema.json | 67 +++ node/scripts/generate-payment-deployments.mjs | 188 +++++++ node/src/chains.ts | 17 +- node/src/deploymentResolver.ts | 63 ++- node/src/index.ts | 11 +- node/src/solanaDeploymentResolver.ts | 56 +- node/src/solanaV14Deployments.generated.ts | 106 ++-- node/src/v14Deployments.generated.ts | 482 +++++++++++++++--- node/tests/deploymentResolver.test.ts | 115 +++-- node/tests/paymentDeploymentRegistry.test.ts | 54 ++ node/tests/solanaDeploymentResolver.test.ts | 124 +++-- 21 files changed, 2866 insertions(+), 306 deletions(-) create mode 100644 docs/adr/0001-fail-closed-payment-deployment-resolution.md create mode 100644 docs/architecture/aifinp-223-224-payment-registry.md create mode 100644 docs/business/aifinp-223-224-analysis.md create mode 100644 docs/business/aifinp-223-224-prd.md create mode 100644 docs/reviews/AIFINP-223-224_IMPLEMENTATION_2026-09-13_RU.md create mode 100644 docs/stories/aifinp-223-224-tests.md create mode 100644 node/registry/payment-deployments.json create mode 100644 node/registry/payment-deployments.schema.json create mode 100644 node/scripts/generate-payment-deployments.mjs create mode 100644 node/tests/paymentDeploymentRegistry.test.ts diff --git a/docs/adr/0001-fail-closed-payment-deployment-resolution.md b/docs/adr/0001-fail-closed-payment-deployment-resolution.md new file mode 100644 index 0000000..bc6ddac --- /dev/null +++ b/docs/adr/0001-fail-closed-payment-deployment-resolution.md @@ -0,0 +1,122 @@ +# ADR-0001: Fail-closed payment deployment resolution + +**Date:** 2026-09-13 +**Status:** proposed +**Deciders:** AiFinPay CTO/protocol team +**Tickets:** AIFINP-223, AIFINP-224 + +## Context + +The SDK currently resolves EVM and Solana deployments from separate generated +tables. EVM `auto` selection prefers v1.4 but silently falls back to v1.2 when +v1.4 is unavailable. Solana already fails when v1.4 is absent because it has no +valid v1.2 fallback. + +Payment versions are not interchangeable. They may use different contracts, +quote formats, payout economics, governance, token allow-lists and backend +verification logic. Availability cannot safely choose those semantics. + +The reviewed deployment commits also contain networks that are deployed but +not yet safe to advertise as payable. Examples include an internally +inconsistent Base artifact, a Polygon asset-identity error, Robinhood assets +that do not fit the existing fixed USDC/USDT SDK model, and new Solana program +IDs that require complete executable/initialization evidence and backend +verification. + +## Decision + +AiFinPay will use one static canonical payment deployment registry for EVM and +Solana. + +- SDK deployment artifacts are generated deterministically from that registry. +- Contract-repository artifacts are imported offline from pinned commits and + recorded with hashes. +- Runtime code never fetches configuration from GitHub, Jira, or a mutable URL. +- Resolution uses an exact rail, environment, network and version tuple. +- Omitted version/`auto` means the single reviewed default for that exact + network. It does not mean a version search. +- Missing, disabled, mismatched or unverifiable entries return a typed error + before a wallet transaction is constructed. +- Assets are an explicitly identified array. A token symbol alone is never a + payment identity. +- Deployment existence and settlement readiness are separate registry states. +- BOT Chain has no production payment default and remains disabled in payment + resolution under contract ADR-0001. + +No resolver may silently downgrade or switch protocol version, environment, +network, rail, route or asset. + +## Consequences + +### Positive + +- A stale deployment fails before funds move. +- SDK releases are reproducible and traceable to exact contract artifacts. +- EVM and Solana follow the same selection policy. +- Robinhood can represent USDe/USDG without inventing USDC/USDT fields. +- Networks can be distributed as disabled metadata and enabled independently + after full verification. +- Rollback disables the affected target instead of redirecting money through a + legacy contract. + +### Negative + +- Clients that relied on EVM `auto` fallback will receive a typed error. +- Every deployment/configuration update requires registry regeneration and an + SDK release. +- Deprecated table exports must be maintained temporarily. +- The backend and SDK release processes become coupled at the settlement-ready + gate. + +## Alternatives considered + +### Keep automatic v1.4 → v1.2 fallback + +Rejected. It changes payment semantics because a preferred deployment is +unavailable. A warning is not enough once an autonomous client can submit +funds. + +### Fetch the latest deployment files from GitHub at runtime + +Rejected. Branch contents are mutable, availability becomes a payment +dependency, and the reviewed SDK package no longer determines its payout +target. + +### Keep separate hand-maintained EVM and Solana tables + +Rejected. The current tables have already drifted from their source commits and +use incompatible asset/readiness models. + +### Enable every address present in a deployment commit + +Rejected. A deploy transaction proves neither correct initialization nor +backend quote/receipt support. Deployment metadata may be imported, but it +starts disabled. + +## Migration + +1. Add the canonical registry and schema. +2. Import EVM `78240ec...` and Solana `e5df8f5...` as disabled records. +3. Generate the unified TypeScript artifact and deprecated compatibility views. +4. Move both resolvers to exact registry lookup and remove version fallback. +5. Remove BOT Chain from payment allow-lists and add Robinhood as disabled. +6. Correct Polygon asset identity and reject the current Base record. +7. Enable deployments individually only after verification, funded E2E, + security review and human approval. + +## Compliance and approval + +This changes payment routing and therefore carries a 10–30% AI autonomy cap +under the project SDLC. Architecture, security, code review and production +enablement require named human approval. Production deployment remains a +separate authorized action. + +## References + +- `docs/architecture/aifinp-223-224-payment-registry.md` +- `node/src/deploymentResolver.ts` +- `node/src/solanaDeploymentResolver.ts` +- `node/src/v14Deployments.generated.ts` +- `node/src/solanaV14Deployments.generated.ts` +- `AiFinPay/evm-contract` commit `78240eccf96dd078c9b40be068d635b14876364c` +- `AiFinPay/solana-contract` commit `e5df8f5436cf646ab495381eee04e0d1a10b4e2f` diff --git a/docs/architecture/aifinp-223-224-payment-registry.md b/docs/architecture/aifinp-223-224-payment-registry.md new file mode 100644 index 0000000..2816746 --- /dev/null +++ b/docs/architecture/aifinp-223-224-payment-registry.md @@ -0,0 +1,348 @@ +# AIFINP-223/224 — Canonical payment deployment registry + +**Status:** proposed +**Date:** 2026-09-13 +**Tickets:** AIFINP-223 (EVM), AIFINP-224 (Solana) +**Risk:** high — payment routing; AI autonomy cap 10–30% +**Decision:** one static registry, generated SDK artifacts, exact fail-closed +resolution, no runtime GitHub fetch, and no silent protocol-version fallback. + +## 1. Scope + +This design replaces the SDK's independent EVM and Solana deployment tables +with one reviewed, versioned registry. It covers: + +- EVM v1.4 artifacts from `AiFinPay/evm-contract` commit + `78240eccf96dd078c9b40be068d635b14876364c`; +- Solana v1.4 artifacts from `AiFinPay/solana-contract` commit + `e5df8f5436cf646ab495381eee04e0d1a10b4e2f`; +- retained legacy EVM deployments while they remain explicitly supported; +- environment, network, version and asset selection; +- provenance, deployment readiness, and safe disablement. + +The registry records where a deployment exists. It does not declare a payment +route usable merely because an address or program ID was published. + +## 2. Current architecture and failure modes + +The SDK currently has three payment-routing sources: + +| Source | Purpose | Problem | +|---|---|---| +| `node/src/v14Deployments.generated.ts` | EVM v1.4 | Contains only Amoy and Polygon and pins obsolete source commit `67b3f518...`. | +| `node/src/solanaV14Deployments.generated.ts` | Solana v1.4 | Contains the superseded devnet/mainnet program IDs from `8a13d10...`. | +| `SPLITTER_DEPLOYMENTS` / `splitterRoutes.generated.ts` | legacy EVM | Separate model and selection path; includes BOT Chain. | + +`node/src/deploymentResolver.ts` implements `auto` as “v1.4, otherwise v1.2”. +That is a change in payment semantics without payer consent. A missing or +disabled v1.4 record can therefore route money through a legacy contract. + +Other observed mismatches: + +- BOT Chain remains selectable in SDK modules even though contract ADR-0001 + forbids production settlement there. +- Robinhood (chain ID 4663) is absent from SDK payment-network definitions. +- the Polygon artifact labels `0x2791...` as USDT; this address must not be + exposed as USDT until the asset identity is corrected and reverified; +- the Base artifact assigns the same address to `splitter` and `tokenList`. + Those are different contract interfaces, so the artifact is not eligible for + payment resolution; +- the current fixed `usdc` / `usdt` fields cannot represent Robinhood's USDe + and USDG configuration; +- a contract deployment record does not prove that the backend can issue and + verify receipts for that rail. + +## 3. Target topology + +```mermaid +flowchart TD + E["EVM deployment artifacts"] --> S["Offline sync + verification"] + L["Solana deployment artifacts"] --> S + S --> R["Canonical static registry"] + R --> G["Deterministic generator"] + G --> T["Generated TypeScript"] + T --> V["Fail-closed resolver"] +``` + +There is no production-time request to GitHub, Jira, or a contract repository. +Updating an address requires a reviewed registry change and a new SDK package. + +## 4. Canonical files + +| File | Responsibility | +|---|---| +| `node/registry/payment-deployments.json` | Human-reviewable canonical registry for all rails and selectable protocol versions. | +| `node/registry/payment-deployments.schema.json` | Structural validation and enabled-record invariants. | +| `node/scripts/sync-payment-deployments.mjs` | Offline import from pinned local checkouts; never fetches at SDK runtime. | +| `node/scripts/check-payment-registry.mjs` | Provenance, uniqueness, address, state and generated-file drift checks. | +| `node/src/paymentDeployments.generated.ts` | Deterministic typed artifact; never edited by hand. | +| `node/src/deploymentResolver.ts` | EVM compatibility API backed only by the generated registry. | +| `node/src/solanaDeploymentResolver.ts` | Solana compatibility API backed only by the generated registry. | + +The legacy generated files may remain as deprecated views for one release, but +they must be produced from `payment-deployments.json`. They cannot remain +independent sources of truth. + +## 5. Registry schema + +The top-level format is intentionally small: + +```json +{ + "schemaVersion": 1, + "sources": { + "evm": { + "repo": "AiFinPay/evm-contract", + "commit": "78240eccf96dd078c9b40be068d635b14876364c" + }, + "solana": { + "repo": "AiFinPay/solana-contract", + "commit": "e5df8f5436cf646ab495381eee04e0d1a10b4e2f" + } + }, + "networkDefaults": {}, + "deployments": [] +} +``` + +Each deployment is a discriminated record: + +| Field | Required meaning | +|---|---| +| `id` | Stable unique key: `:::`. | +| `rail` | `evm` or `solana`. | +| `environment` | `dev` or `prod`; never inferred from a branch name. | +| `network` | Canonical SDK network name. | +| `aliases` | Closed reviewed list, for example `mainnet-beta` → `mainnet`. | +| `version` | Exact protocol version implemented by the target. | +| `state` | `enabled`, `disabled`, or `retired`. | +| `disabledReason` | Required unless `state` is `enabled`. | +| `provenance` | Source repo, commit, artifact path and SHA-256. | +| `verification` | Chain identity, timestamp, verifier version and hashes observed before enablement. | +| `assets` | Array of explicitly identified assets; no fixed USDC/USDT slots. | +| `evm` / `solana` | Rail-specific deployment payload. Exactly one is present. | + +`networkDefaults` maps an exact `rail + environment + network` to one default +version. It is a selection value, not a fallback list. + +### 5.1 EVM payload + +An EVM entry contains: + +- numeric `chainId`; +- splitter, TokenList and Profiles addresses; +- expected runtime code hash for each contract; +- signer, pauser, treasury and admin addresses; +- Safe address, owners and threshold when applicable; +- route profiles verified at the registry verification time. + +An enabled EVM record must have three distinct contract addresses, non-empty +runtime code, matching runtime hashes, expected roles, an allowed asset set, +and a backend verifier approved for that exact chain and version. + +### 5.2 Solana payload + +A Solana entry contains: + +- cluster and cluster genesis hash; +- program ID and executable program-data hash; +- program-data address and upgrade authority; +- IDL artifact path, version and SHA-256; +- initialized config PDA and the verified route/token state. + +An enabled Solana record must prove that the program is executable and +initialized and that the backend can verify its settlement transaction. A +successful deploy signature alone is insufficient. + +### 5.3 Assets + +Every asset uses an array element rather than a symbol-named property: + +```json +{ + "assetId": "circle:usdc", + "symbol": "USDC", + "kind": "erc20", + "identifier": "0x...", + "decimals": 6, + "settlementEnabled": true, + "provenance": "issuer registry or reviewed chain evidence" +} +``` + +`assetId` is the trusted identity. `symbol` is display metadata and cannot be +used to select a token. Native assets and Solana SPL mints use the same model +with `kind: native` or `kind: spl`. + +## 6. Resolution rules + +Payment selection takes the tuple: + +`rail + environment + network + requestedVersion + requestedAsset` + +Resolution is deterministic: + +1. Normalize only documented aliases. +2. If the caller requests a version, find that exact record. +3. If the version is omitted or `auto`, read the single exact version from + `networkDefaults`. +4. Never search another protocol version, environment, network, or rail. +5. Reject records whose state is not `enabled`. +6. If an asset is requested, require its exact `assetId` and identifier to be + enabled for the selected deployment. +7. Return an immutable deployment object or a typed error. + +Required errors: + +- `UnknownNetworkError` +- `EnvironmentMismatchError` +- `VersionUnavailableError` +- `DeploymentDisabledError` with the registry reason +- `AssetUnavailableError` +- `RegistryInvariantError` for an impossible generated state + +`auto` is retained only for API compatibility. It means “use the reviewed +default for this exact network”, not “try versions until one works”. + +## 7. Network-specific decisions + +| Network | Import state | Required handling | +|---|---|---| +| Amoy | disabled until verification refresh | Development only. Never resolve under `prod`. | +| Polygon | disabled during asset correction | Keep native USDC only after verification. Remove `0x2791...` from the USDT identity; do not silently rename a payment asset. | +| Base | disabled | Reject the current internally inconsistent deployment artifact. Re-enable only after a distinct verified splitter deployment and new artifact. | +| Arbitrum, Avalanche, BNB, Optimism, Unichain, XRPL EVM | imported disabled | Addresses may be shipped as metadata, but payment resolution remains off until contract-state, asset and backend-verifier gates pass. | +| Robinhood | imported disabled | Add chain ID 4663. Represent USDe and USDG through `assets[]`. Current zero USDC/USDT slots do not authorize stable settlement. | +| BOT Chain | excluded from payment defaults | Preserve a non-payment chain descriptor only if another SDK feature needs it. Resolver always throws `DeploymentDisabledError`; no production payment advertisement. | +| Solana devnet | disabled until full evidence | Use program ID `8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y`; require executable hash, initialized state and IDL hash before enablement. | +| Solana mainnet | disabled until full evidence | Use program ID `724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD`; require the same evidence plus backend settlement verification. | + +The `disabled` state prevents a repeated production failure mode: publishing a +contract address before the complete quote → settlement → receipt path works. + +## 8. Trust boundaries + +| Boundary | Threat | Control | +|---|---|---| +| Contract repo → SDK registry | Stale, malformed or forged artifact | Pinned commit and artifact SHA-256; offline import; human-reviewed diff. | +| RPC → verifier | Wrong chain or compromised provider | Verify chain identity and use provider quorum before enablement. | +| Registry → generated TS | Hand-edited payout target | Deterministic generation and byte-for-byte CI drift check. | +| SDK input → resolver | Alias, environment or version confusion | Closed aliases, exact tuple, typed fail-closed errors. | +| SDK → wallet/RPC | Correct address but changed bytecode/state | Preflight chain ID and code/program hash before first payment session; abort on mismatch. | +| Backend → SDK | Quote for an unsupported deployment | Bind quote to chain/program, contract, version, route, asset and amount; require exact registry match. | +| Package publication → consumer | Registry artifact substituted | npm provenance/package integrity, release tag, source commits in generated metadata. | + +Custom payment addresses are outside the safe resolver. If retained for local +development, they require an explicitly named unsafe API and are prohibited in +`prod` by default. + +## 9. Impacted modules + +### Direct + +- `node/src/deploymentResolver.ts` +- `node/src/solanaDeploymentResolver.ts` +- `node/src/v14Deployments.generated.ts` +- `node/src/solanaV14Deployments.generated.ts` +- `node/src/unifiedAgent.ts` +- `node/src/splitterRoutes.ts` and `splitterRoutes.generated.ts` +- `node/src/chains.ts` +- `node/src/settlement.ts` +- `node/src/index.ts` +- `node/scripts/generate-splitter-routes.mjs` +- `node/scripts/check-registry-provenance.mjs` +- `mcp/src/tools/production-control.ts` + +### Tests and release surfaces + +- `node/tests/deploymentResolver.test.ts` +- `node/tests/solanaDeploymentResolver.test.ts` +- splitter deployment/route/settlement regression tests +- MCP network allow-list tests +- package exports, README and changelog +- CI registry drift and provenance jobs + +The backend is a cross-repository dependency. A deployment cannot become +`enabled` until the matching backend quote and receipt verifier is available. + +## 10. Compatibility and migration + +1. Add the canonical JSON, schema, generator and tests with every imported + record disabled. +2. Generate a single TypeScript deployment table. +3. Keep `resolveDeployment` and `resolveSolanaDeployment` signatures. Adapt + their results from the new table so existing callers keep their shapes. +4. Retain `V14_DEPLOYMENTS`, `SOLANA_V14_DEPLOYMENTS` and source metadata as + deprecated generated views for one release cycle. +5. Change `auto` to exact default selection. Emit a development warning for + callers relying on `auto`; never emit a warning instead of blocking a + payment. +6. Legacy v1.2/v1.3 is reachable only by an explicit version and only while its + registry record is enabled. No new caller should default to it. +7. BOT Chain payment requests fail with a typed disabled error. Removing it + from all public TypeScript unions can wait for the next breaking release if + non-payment consumers still compile against the name. +8. Enable one deployment at a time after contract verification, backend + support, funded end-to-end testing, security review and human approval. + +Because the current SDK line is a release candidate, the resolver semantic +change should ship in the next RC. If published in a stable line, treat the +change as security-significant and document the behavior change prominently; +do not preserve unsafe fallback for semantic-version convenience. + +## 11. Required validation + +### Registry and generator + +- schema validation and unique deployment IDs; +- exact network/chain/cluster identity; +- artifact hash and pinned commit verification; +- deterministic generation with a clean-tree drift check; +- enabled-record invariants; +- rejection of zero addresses, duplicate contract addresses and unknown asset + identities. + +### Resolver + +- no v1.4 → v1.2 fallback; +- no dev → prod or prod → dev crossover; +- missing/disabled Base fails closed; +- BOT Chain always fails closed for payment; +- Robinhood cannot select zero USDC/USDT; +- Polygon cannot resolve `0x2791...` as USDT; +- Solana aliases resolve only to the exact cluster; +- corrupt generated data raises `RegistryInvariantError` before wallet/RPC use. + +### Integration + +- exact quote/contract/program/asset binding; +- one funded end-to-end test per enabled deployment; +- receipt verification and replay/idempotency checks; +- first-payment preflight rejects code or program hash drift. + +## 12. Rollout and rollback + +Rollout is registry-first and network-by-network. Disabled metadata can ship +without making a route payable. Enabling a route requires a reviewed registry +diff and a new SDK/backend release. + +Rollback never changes protocol version automatically. Publish a new registry +with the affected record `disabled`, pause the contract where appropriate, and +release the SDK/backend change. A caller receives `DeploymentDisabledError` and +does not submit funds. + +## 13. Architecture gate + +- [x] Existing resolver, generated tables, registries and upstream deployment + artifacts analyzed. +- [x] Impacted SDK modules and cross-repository backend dependency identified. +- [x] Data generation, runtime selection and trust dependencies documented. +- [x] ADR created for the non-trivial fail-closed decision. +- [ ] Repository bootstrap requirement satisfied: root `AGENTS.md` and + `ARCHITECTURE.md` are absent. +- [ ] CTO/human reviewer approves the detailed schema and migration plan. + +**Gate result: BLOCKED.** The architecture is ready for review, but the SDLC +Architecture Gate cannot hand off to implementation until the two repository +bootstrap files exist and a human engineering owner approves this payment-risk +design. diff --git a/docs/business/aifinp-223-224-analysis.md b/docs/business/aifinp-223-224-analysis.md new file mode 100644 index 0000000..e133ae2 --- /dev/null +++ b/docs/business/aifinp-223-224-analysis.md @@ -0,0 +1,153 @@ +# AIFINP-223 / AIFINP-224 Requirements Analysis + +**Traceability ID:** AIFINP-223-AIFINP-224 + +**Date:** 2026-09-13 + +**Repository:** AiFinPay/sdk + +**Upstream sources:** AiFinPay/evm-contract, AiFinPay/solana-contract + +**Risk class:** Critical payment configuration change + +**AI autonomy cap:** 10% (payments); 0–20% for smart-contract or production-infrastructure actions + +## Problem statement + +The SDK contains environment/version resolvers for EVM and Solana, but the bundled deployment data and the resolver policy no longer match the production deployment sources. + +- The EVM SDK table is pinned to an old `evm-contract` commit and contains only Amoy and Polygon v1.4. +- The Solana SDK table is pinned to an old `solana-contract` commit and contains superseded program IDs. +- The EVM `auto` selector silently falls back from v1.4 to legacy v1.2. This can change the settlement contract and payment semantics without the caller explicitly accepting the change. +- BOT Chain remains selectable in SDK and MCP payment surfaces despite accepted ADR-0001 prohibiting production settlement there. +- Robinhood Chain was added upstream, but the current EVM deployment artifact can only represent `usdc` and `usdt`; Robinhood is configured upstream with USDe and USDG. The produced deployment record therefore contains two zero token addresses and cannot prove that either configured Robinhood stablecoin is allowed. +- Some upstream records are unsafe to consume as valid deployments without quarantine and independent on-chain verification. + +The business need is to ship a deterministic SDK configuration update without allowing stale, malformed, unverified, or policy-disabled records to become payment routes. + +## Source request and Jira context + +### AIFINP-223 — EVM environment and protocol version switcher + +Jira currently requests: + +- `dev` and `prod` environments; +- explicit `v1.2` and `v1.4` selection; +- `auto` selection that prefers v1.4 and falls back to v1.2; +- Amoy-only development support; +- a centralized resolver using deployment records from `evm-contract`; +- an SDK refresh from `evm-contract` commit `78240eccf96dd078c9b40be068d635b14876364c`. + +The automatic downgrade requirement conflicts with fail-closed payment behavior and must be corrected before implementation approval. + +Jira: + +Deployment source: + +### AIFINP-224 — Solana environment and protocol version switcher + +Jira inherits AIFINP-223's environment/version objective and instructs the SDK to use redeployment artifacts from `solana-contract` commit `e5df8f5436cf646ab495381eee04e0d1a10b4e2f`. + +Jira: + +Deployment source: + +## Stakeholders and intended users + +| Stakeholder | Need | +|---|---| +| SDK integrator | Deterministic environment/network/version resolution and typed failures | +| Agent or wallet operator | Assurance that the selected deployment and asset are exactly the ones approved | +| Merchant | No settlement against a legacy, wrong-chain, wrong-token, or unverified contract | +| Backend/facilitator operator | SDK routes constrained to networks the backend can verify and receipt | +| Protocol/security team | Reproducible provenance, on-chain verification, quarantine, and human activation controls | +| Release/QA team | Testable network matrix and negative-path acceptance criteria | + +## Current-state evidence + +### SDK + +| Surface | Current state | Consequence | +|---|---|---| +| `node/src/deploymentResolver.ts` | `auto` returns v1.2 when v1.4 is absent | Silent money-path downgrade | +| `node/src/v14Deployments.generated.ts` | Source commit `67b3f518...`; Amoy and Polygon only | New EVM production records are absent | +| `node/src/solanaV14Deployments.generated.ts` | Source commit `8a13d10...`; devnet `Dg9v...`, mainnet `8dty...` | Both entries are stale after the redeploy | +| `mcp/src/tools/production-control.ts` | BOT Chain is accepted; Robinhood is absent | Policy conflict and incomplete network support | +| legacy chain/route tables | BOT Chain remains resolvable | ADR-0001 is not enforced at the settlement boundary | + +The current tests explicitly require v1.4-to-v1.2 fallback on Base and include BOT Chain among fallback networks. Those tests encode the unsafe behavior and must be replaced rather than preserved. + +### EVM upstream records at commit `78240ec` + +The commit adds v1.4 splitter records for Arbitrum, Avalanche, BNB Chain, Optimism, Robinhood, Unichain, and XRPL EVM. The branch also contains Amoy, Polygon, and Base records. + +| Network | Chain ID | Analysis status | Reason | +|---|---:|---|---| +| Amoy | 80002 | Candidate, dev only | Must pass independent code/roles/profile/token checks | +| Polygon | 137 | Quarantined | Record labels `0x2791...` as USDT; the address is the legacy bridged USDC asset, so the asset identity is unsafe | +| Base | 8453 | Invalid/quarantined | Record has the same address for `splitter.address` and `splitter.tokenList`; it cannot be accepted as a valid splitter deployment | +| Arbitrum | 42161 | Candidate pending verification | New record exists; availability is not proof of production eligibility | +| Avalanche | 43114 | Candidate pending verification | New record exists; availability is not proof of production eligibility | +| BNB Chain | 56 | Candidate pending verification | New record exists; availability is not proof of production eligibility | +| Optimism | 10 | Candidate pending verification | New record exists; availability is not proof of production eligibility | +| Unichain | 130 | Candidate pending verification | New record exists; availability is not proof of production eligibility | +| XRPL EVM | 1440000 | Candidate pending verification | No stablecoin is represented in the record; payment eligibility must stay off unless an explicitly supported asset is verified | +| Robinhood | 4663 | Quarantined | Upstream config lists USDe/USDG, but deploy/record/check code handles only USDC/USDT; record contains zero token addresses | +| BOT Chain | 677 | Disabled by policy | ADR-0001 says no production v1.4 deployment and no SDK `settlementEnabled` status | + +The current EVM checker also iterates fixed `USDC`/`USDT` fields and can call `isAllowed(address(0))`. A green result from that checker is insufficient for generalized stablecoin support. + +### Solana upstream records at commit `e5df8f5` + +| Environment | Cluster | New program ID | Artifact | +|---|---|---|---| +| `dev` | devnet | `8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y` | `splitter.devnet.20260911-195503.json` | +| `prod` | mainnet / mainnet-beta | `724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD` | `splitter.mainnet.20260911-200222.json` | + +Both IDLs declare version `1.4.1`. Both IDLs also encode the same `initialize.payer.address` (`Fz3jd...`) even though the deploy logs identify different deployers. This provenance inconsistency does not prove the deployed programs are wrong, but it blocks treating the IDL snapshot as independently trustworthy until a human verifies the build context and on-chain state. + +Solana has no supported v1.2 splitter deployment. `auto` therefore either resolves an eligible v1.4 deployment or fails with a typed error. + +## Constraints + +1. Deployment data must be bundled from immutable commit SHAs. The shipped SDK must not fetch a mutable GitHub branch at payment time. +2. Record presence means `known`, not `settlement enabled`. +3. Environment, network, version, asset, route economics, verifier support, and deployment eligibility must all agree before a payment route is returned as executable. +4. No version fallback may change a payment contract or economic behavior silently. +5. Existing direct legacy exports may remain for compatibility, but their presence must not authorize settlement and BOT Chain must not be selectable through payment APIs. +6. `dev` EVM is Amoy only; `dev` Solana is devnet only. Production records must never resolve under `dev`, and development records must never resolve under `prod`. +7. Mainnet writes, Safe transactions, contract redeployments, key/authority changes, and route activation require humans. This documentation work authorizes none of them. +8. AIFP-1's separate v1.3 production route behavior must not regress. Adding v1.3 to this version selector is outside these two tickets. + +## Assumptions + +- The two immutable commits named in Jira are the requested provenance anchors for this work. +- `v1.2` remains a request-side compatibility value, but it is only selected explicitly and never through `auto`. +- A resolved deployment is metadata. Executability requires an independent settlement-eligibility gate. +- Network aliases are normalized deterministically; Solana `mainnet-beta` maps to `mainnet`. +- Robinhood needs an asset-list representation rather than hard-coded USDC/USDT slots. + +## Unknowns requiring human or on-chain evidence + +- Whether every candidate EVM address has code matching the expected compiled artifact and recorded runtime hash. +- Whether splitter, TokenList, and Profiles roles are held by the intended Safe/Timelock and whether deployer privileges were renounced. +- Whether Safe owners, threshold, version, singleton, and chain-specific transaction service configuration match policy. +- Whether every advertised token has the intended issuer, symbol, decimals, code, and TokenList allowlist state. +- Whether AIFP-1 and AIFP-2 profiles are enabled with the exact approved economics and immutable quote binding. +- Whether backend verification and receipt issuance support each network/asset/program before SDK activation. +- Whether the Solana programs are executable, initialized, not paused, configured with the intended routes/tokens, and controlled by the approved upgrade authority. +- Whether the Solana IDL payer-address discrepancy is harmless generation metadata or a build/deployment inconsistency. +- Whether Base must be redeployed at new deterministic addresses or repaired through a corrected deployment process. + +## Corrected, testable requirement + +Update the SDK's centralized EVM and Solana deployment resolution so it consumes immutable, validated deployment metadata from the Jira-specified commits, returns only environment/network/version combinations that are explicitly eligible, and fails closed otherwise. `auto` may select an eligible v1.4 deployment but must never silently downgrade to v1.2. BOT Chain must be disabled at every production settlement boundary. Robinhood must use a generalized stablecoin list and remain quarantined until its supported assets are on-chain verified. Solana must use the redeployed devnet and mainnet program IDs and remain non-executable until its artifacts and on-chain configuration pass the activation gate. Invalid or incomplete records, including Base and the current Robinhood record, must be quarantined rather than exposed as usable payment routes. + +## Requirement Gate self-check + +- [x] Requirement is clearly defined. +- [x] Business requirements are understood. +- [x] Unknowns and assumptions are surfaced. +- [x] Missing AI-readable repository files are recorded: root `AGENTS.md`, root `ARCHITECTURE.md`, `CONTRIBUTING.md`, and `SECURITY.md` are absent. Architecture/implementation must not claim their gates pass until the minimum documentation precondition is repaired. + +**HANDOFF: sdlc-pm | artifact: `docs/business/aifinp-223-224-analysis.md` | gate: pass** diff --git a/docs/business/aifinp-223-224-prd.md b/docs/business/aifinp-223-224-prd.md new file mode 100644 index 0000000..237ca25 --- /dev/null +++ b/docs/business/aifinp-223-224-prd.md @@ -0,0 +1,309 @@ +# AIFINP-223 / AIFINP-224 Product Requirements Document + +**Traceability ID:** AIFINP-223-AIFINP-224 + +**Depends on:** `docs/business/aifinp-223-224-analysis.md` + +**Risk class:** Critical + +**Autonomy cap:** 10% for payment-path code; 0–20% for smart-contract and production-infrastructure actions + +**Human approvals required:** Architecture, Security, Code Review, and Production Deployment + +## Objective + +Bring the SDK deployment configuration in line with the Jira-specified EVM and Solana redeployments while enforcing fail-closed payment selection. + +Success means the SDK can identify known deployments, but only returns a deployment as executable after its environment, network, version, asset, policy status, upstream provenance, on-chain state, and backend verifier support have all been validated. + +## User stories + +### US-1 — Deterministic EVM selection + +As an SDK integrator, I want to request an EVM environment, network, and protocol version and receive the exact eligible deployment or a typed error, so that the SDK cannot silently change the payment contract. + +### US-2 — Fail-closed automatic mode + +As a payer, I want automatic selection to fail when v1.4 is unavailable or quarantined, so that my payment is not silently redirected to legacy v1.2. + +### US-3 — Deterministic Solana selection + +As an SDK integrator, I want devnet and mainnet to resolve to the redeployed Solana v1.4 program IDs, so that stale program IDs are never used. + +### US-4 — Network-policy enforcement + +As a protocol operator, I want BOT Chain disabled and Robinhood quarantined until its assets are verified, so that a known network cannot become a settlement route merely by appearing in a configuration file. + +### US-5 — Deployment provenance and quarantine + +As a release reviewer, I want every bundled record to identify its immutable source and eligibility state, so that invalid or incomplete deployments are rejected before funds can move. + +## Functional requirements + +### FR-1 — Environment isolation + +- Supported SDK environments are `dev` and `prod`. +- EVM `dev` supports Amoy only. +- Solana `dev` supports devnet only. +- EVM `prod` must not resolve Amoy. +- Solana `prod` supports `mainnet` and the normalized alias `mainnet-beta`; it must not resolve devnet. +- Any unknown environment or environment/network mismatch returns a typed configuration error before quote construction or wallet interaction. + +### FR-2 — Version-selection policy + +- EVM request values remain `v1.2`, `v1.4`, and `auto` for compatibility. +- Explicit `v1.4` returns only an eligible v1.4 record; otherwise it fails. +- Explicit `v1.2` may resolve only an explicitly known legacy record and must never be substituted for another version. Resolution alone does not authorize execution. +- `auto` returns an eligible v1.4 record or fails with a typed unavailable/quarantined error. +- `auto` never returns v1.2. +- Solana supports v1.4 only. Explicit v1.2 always fails, and `auto` never invents a fallback. +- Error messages must not instruct callers to enable a money-path downgrade. + +### FR-3 — Immutable deployment provenance + +- EVM generated data is pinned to `AiFinPay/evm-contract@78240eccf96dd078c9b40be068d635b14876364c` or to a later explicitly reviewed correction commit that references it. +- Solana generated data is pinned to `AiFinPay/solana-contract@e5df8f5436cf646ab495381eee04e0d1a10b4e2f` or to a later explicitly reviewed correction commit that references it. +- SDK runtime payment selection does not fetch a mutable GitHub branch. +- Generated records expose source repository, commit SHA, artifact path, and generation/validation status. +- A stale or unrecognized source SHA fails the generation/CI gate; it is not silently accepted. + +### FR-4 — Deployment lifecycle state + +Every known deployment has an explicit state with these product semantics: + +| State | Resolver behavior | Settlement behavior | +|---|---|---| +| `eligible` | May resolve for its exact environment/network/version | May proceed only if route/asset/backend gates also pass | +| `quarantined` | Typed quarantine error | Forbidden | +| `disabled` | Typed policy-disabled error | Forbidden | +| `invalid` | Validation error; excluded from normal output | Forbidden | + +Absence of a state is treated as `quarantined`, never `eligible`. + +### FR-5 — EVM network matrix + +The initial generated inventory must include the following records without implying that all are eligible: + +| Network | Chain ID | Required initial policy | +|---|---:|---| +| Amoy | 80002 | Candidate for `eligible` after dev validation | +| Polygon | 137 | `quarantined` until the `0x2791...` asset misclassification and TokenList state are corrected | +| Base | 8453 | `invalid`/`quarantined`; splitter/TokenList address collision must be corrected by a human-approved redeploy or replacement record | +| Arbitrum | 42161 | `quarantined` until independent on-chain and backend validation passes | +| Avalanche | 43114 | `quarantined` until independent on-chain and backend validation passes | +| BNB Chain | 56 | `quarantined` until independent on-chain and backend validation passes | +| Optimism | 10 | `quarantined` until independent on-chain and backend validation passes | +| Unichain | 130 | `quarantined` until independent on-chain and backend validation passes | +| XRPL EVM | 1440000 | `quarantined` until a supported asset and backend verifier are proven | +| Robinhood | 4663 | `quarantined` until generalized asset-list deployment and verification passes | +| BOT Chain | 677 | `disabled` by ADR-0001; never settlement-enabled | + +### FR-6 — BOT Chain enforcement + +- BOT Chain may remain as historical or monitoring metadata. +- It is removed from all advertised/selectable production settlement enums, MCP schemas, automatic resolver candidates, and executable chain maps. +- Any attempt to construct a BOT Chain settlement invoice or execute a payment fails with a typed policy-disabled error before quote or signing. +- Tests must prove that v1.2, v1.4, and `auto` cannot make BOT Chain executable. + +### FR-7 — Robinhood generalized asset handling + +- Deployment data represents stablecoins as an address-bearing list, not only fixed `usdc`/`usdt` fields. +- Each asset entry includes at least canonical asset identifier, displayed symbol, address, decimals, issuer/source evidence, and eligibility state. +- Robinhood's intended USDe (`0x5d3a...`) and USDG (`0x5fc5...`) entries remain quarantined until the contracts and TokenList allowlist state are verified on chain. +- Zero address is never treated as a token and is never passed to `TokenList.isAllowed` as a positive validation target. +- Unknown symbols, duplicate addresses, symbol/address collisions, and decimals mismatches fail validation. + +### FR-8 — Solana redeployment data + +- `dev/devnet` resolves to program `8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y`. +- `prod/mainnet` and `prod/mainnet-beta` resolve to program `724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD`. +- Devnet uses `splitter.devnet.20260911-195503.json` as the requested IDL provenance. +- Mainnet uses `splitter.mainnet.20260911-200222.json` as the requested IDL provenance. +- The old SDK IDs `Dg9v95...` and the use of `8dty5b...` as mainnet are rejected by tests. +- Mainnet and devnet records remain quarantined from executable settlement until the IDL/build discrepancy and the activation checks in FR-10 pass. + +### FR-9 — EVM static validation gate + +A record cannot become `eligible` unless automated validation proves: + +- expected chain ID and normalized network name; +- nonzero, distinct splitter, TokenList, and Profiles addresses; +- valid address encoding and no disallowed cross-role/address collisions; +- deployed runtime code exists at every required address; +- runtime code hash matches the independently compiled/pinned expected artifact, not merely the value copied from the record being checked; +- required roles match the intended governance, signer, and pauser; +- paused state is acceptable; +- Safe address, owners, threshold, singleton/version, and governance ownership match policy; +- deployer has no unauthorized residual admin role; +- every advertised asset is nonzero, is the intended contract with expected decimals/identity, and is allowed by TokenList; +- every advertised route exists, is enabled, and has the approved economics; +- no unadvertised or policy-forbidden route/asset becomes selectable. + +### FR-10 — Solana activation gate + +A Solana record cannot become `eligible` for executable settlement unless automated evidence and human review prove: + +- the program account exists on the expected cluster and is executable; +- the deployment signature is finalized; +- program-data account and upgrade authority match the approved governance policy; +- expected binary/IDL/build provenance is reproducible and pinned; +- initialization/config PDA exists and decodes using the pinned IDL; +- program is not paused; +- route profiles and fee economics match the approved values; +- token mints, decimals, authorities, and allowlist state match policy; +- the `initialize.payer.address` discrepancy in the two supplied IDLs is resolved or documented with reproducible evidence; +- backend verification and receipt issuance support the exact program and assets; +- a funded test on a non-production environment completes without duplicate settlement. + +### FR-11 — Backend/execution compatibility gate + +- The SDK may expose a known deployment as metadata even when backend support is absent. +- It must not expose that deployment as executable or advertise it in settlement routes until backend verification, receipt validation, and replay/idempotency behavior support the exact chain/program and asset. +- A missing verifier produces a typed `unsupported settlement verifier` failure before funds move. + +### FR-12 — Backward compatibility and release behavior + +- Existing public types and direct legacy tables remain available where possible. +- Direct legacy metadata does not bypass policy/eligibility checks in payment APIs. +- Integrations that relied on omitted `version` silently falling back to v1.2 will now receive a typed error. This is an intentional safety behavior change and requires a changelog entry and an appropriate semver/RC decision by the release owner. +- Existing AIFP-1 v1.3 route selection and economics must pass regression tests. + +## Acceptance criteria + +### AC-1 — EVM fail-closed `auto` + +**Given** a production network with no eligible v1.4 deployment but with a legacy v1.2 record + +**When** the caller requests `auto` or omits the version + +**Then** resolution throws a typed unavailable/quarantined error and never returns v1.2. + +### AC-2 — Exact explicit selection + +**Given** an explicit version request + +**When** the exact environment/network/version record is eligible + +**Then** the resolver returns that exact record; otherwise it throws and never substitutes another version. + +### AC-3 — Environment isolation + +Automated tests prove that only Amoy resolves under EVM `dev`, only devnet resolves under Solana `dev`, and no development record resolves under `prod` or vice versa. + +### AC-4 — BOT Chain disabled + +Automated tests prove that BOT Chain is absent from advertised settlement enums and that all invoice, resolver, and execution entry points reject it before quote construction/signing for every requested version. + +### AC-5 — Robinhood representation + +The generated schema can represent USDe and USDG without pretending they are USDC/USDT. Robinhood remains quarantined until a validation report proves both desired asset contracts and TokenList state. Zero-address assets are absent from positive allowlist checks. + +### AC-6 — Solana IDs refreshed + +Tests assert the exact new devnet/mainnet program IDs and exact pinned artifact paths. Tests also assert that the former IDs cannot be returned for the wrong cluster. + +### AC-7 — Invalid deployment quarantine + +A fixture with identical splitter and TokenList addresses, including the current Base shape, fails validation and cannot resolve. Missing state, missing code, hash mismatch, zero required address, wrong chain ID, or malformed asset data also fail closed. + +### AC-8 — Polygon asset safety + +No SDK output identifies `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` as native Polygon USDT. Polygon v1.4 remains quarantined until an approved replacement classification/allowlist record passes validation. + +### AC-9 — Immutable provenance + +CI regenerates or verifies bundled metadata from a pinned commit and fails when generated output differs, the source SHA is unrecognized, or a runtime path attempts to load a mutable branch. + +### AC-10 — On-chain eligibility + +For every deployment marked `eligible`, CI/staging evidence covers all checks in FR-9 or FR-10. A copied deployment JSON without independent chain evidence cannot satisfy this criterion. + +### AC-11 — Backend safety + +For every deployment advertised as executable, an integration test proves the backend supports the exact chain/program and asset before funds move. Unsupported networks fail before wallet signing/broadcast. + +### AC-12 — Payment-path regression + +Tests cover quote binding, wrong environment, wrong chain, wrong asset, wrong merchant, expired quote, changed route profile, replay, receipt verification, and retry/idempotency. No retry causes a second payment for the same order/quote. + +### AC-13 — Existing AIFP-1 economics + +Regression tests prove AIFP-1 uses gross-inclusive 99% merchant / 1% AiFinPay / 0% creator economics on the approved production rail. AIFP-2 behavior remains isolated from AIFP-1. + +### AC-14 — Release gates + +Build, typecheck, lint, unit, integration, regression, edge-case, SAST, dependency, and secrets checks pass. Independent AI review plus human engineering/security review are recorded before merge. Production activation requires a separately logged human approval. + +## Edge cases and negative paths + +| Case | Required result | +|---|---| +| Empty/whitespace/mixed-case network | Normalize only recognized aliases; otherwise typed error | +| `staging`, unknown environment, or environment/network mismatch | Typed configuration error | +| Unknown version string | Typed version error | +| `auto` with only v1.2 known | Fail closed; no fallback | +| Explicit v1.4 is quarantined | Quarantine error, no fallback | +| Explicit v1.2 on Solana | Typed `v1.2 unavailable` error | +| Mutable source branch or missing commit SHA | Generation/CI failure | +| Duplicate chain ID under different network names | Validation failure unless one is an explicit documented alias | +| Splitter equals TokenList or Profiles | Invalid deployment | +| Missing bytecode or runtime hash mismatch | Quarantine/failure | +| Zero token address | Omit as absent; never validate as an allowed token | +| Token symbol matches but decimals/issuer/address do not | Quarantine/failure | +| Two symbols share one address without an explicit alias model | Validation failure | +| Robinhood record has only zero USDC/USDT fields | Quarantine; do not infer USDe/USDG were configured | +| BOT Chain appears in a legacy table/cache | Payment boundary still rejects it | +| SDK cache contains a formerly eligible record | Re-evaluate policy/eligibility; stale cache cannot override disabled/quarantined status | +| Backend verifier missing after SDK resolution | Fail before signing/broadcast | +| Route profile changes after quote | Fail contract/verification binding; never settle under unquoted economics | +| Solana IDL address differs from deployed build context | Quarantine pending reproducible verification | +| RPC unavailable during generation/validation | Do not promote state; retain quarantine | +| Retry after uncertain broadcast | Query/verify idempotently before any rebroadcast | + +## Out of scope + +- Sending mainnet transactions or Safe proposals. +- Redeploying Base, Robinhood, or any other contract. +- Changing Solana upgrade authority. +- Repairing the smart-contract quote/profile binding bug itself. +- Adding v1.3 to the AIFINP-223 version selector. +- Implementing unsupported backend verifiers. +- Enabling any production payment route solely because the SDK metadata was refreshed. +- Closing either Jira ticket before Testing, Security, Code Review, Deployment, and Definition-of-Done gates pass. + +## Human and on-chain blockers + +| Blocker | Required human action/evidence | +|---|---| +| Corrected no-fallback product behavior | Product/CTO approval because it intentionally changes current backward behavior | +| Missing SDK `AGENTS.md` and `ARCHITECTURE.md` | Bootstrap/approve the AI-readable repository baseline before Architecture Gate | +| Base invalid deployment record | CTO/security decision and human-authorized redeploy or replacement record | +| Polygon asset misclassification | Security review, canonical asset decision, and any required Safe allowlist transaction | +| Robinhood USDe/USDG activation | Generalized deployment/check schema, independent asset verification, Safe transaction, and funded test | +| EVM role/Safe/token/profile validation | Independent on-chain report and human Security approval per network | +| Solana IDL/deployer discrepancy | Reproducible build/IDL evidence and CTO/security approval | +| Solana upgrade authority and program configuration | Human governance approval and verified on-chain state | +| Backend support | Backend owner confirms verifier/receipt/idempotency support per rail | +| Production activation | Explicit release-owner authorization after all gates pass | + +## Priority and sequencing + +1. **P0 — Safety policy:** remove silent fallback, enforce BOT Chain disablement, introduce quarantine semantics. +2. **P0 — Data correction:** refresh Solana IDs; import EVM records as known but quarantined; reject Base; fix Polygon classification; generalize Robinhood assets. +3. **P0 — Validation:** add static/on-chain provenance and eligibility checks plus negative-path tests. +4. **P0 — Compatibility:** confirm backend verifier support and receipt/idempotency behavior before any executable route is advertised. +5. **P1 — Human operations:** perform approved redeploy/Safe/authority actions outside the SDK PR. +6. **P1 — Release:** independent security/code review, staging funded E2E, canary, monitoring, and production authorization. + +## Requirement Gate self-check + +- [x] Requirement is clearly defined. +- [x] Acceptance criteria are measurable and testable. +- [x] Edge cases and negative paths are identified. +- [x] Business requirements and safety constraints are understood. + +**Requirement Gate: PASS** + +Implementation must not start under the SDLC framework until the missing AI-readable codebase precondition is repaired and a human approves the corrected payment policy. + +**HANDOFF: sdlc-architect | artifact: `docs/business/aifinp-223-224-prd.md` | gate: pass; Architecture Gate: BLOCKED pending root `AGENTS.md`/`ARCHITECTURE.md` and human approval** diff --git a/docs/reviews/AIFINP-223-224_IMPLEMENTATION_2026-09-13_RU.md b/docs/reviews/AIFINP-223-224_IMPLEMENTATION_2026-09-13_RU.md new file mode 100644 index 0000000..f8c93d9 --- /dev/null +++ b/docs/reviews/AIFINP-223-224_IMPLEMENTATION_2026-09-13_RU.md @@ -0,0 +1,113 @@ +# AIFINP-223 / AIFINP-224 — отчёт по исправлениям + +Дата: 13 сентября 2026 +Статус: pull requests опубликованы; production settlement v1.4 остаётся выключенным + +## Что сделано + +### AIFINP-223 — EVM + +- Импортированы v1.4 deployments из `AiFinPay/evm-contract@78240ec` и последующие безопасные исправления из `a54a4c107de7bb42f54e411e621d3897938bfc31`. +- Добавлен единый статический SDK registry для EVM и Solana: + - `node/registry/payment-deployments.json`; + - `node/registry/payment-deployments.schema.json`; + - детерминированный генератор `node/scripts/generate-payment-deployments.mjs`; + - CI drift-check через `registry:check`. +- `auto` больше не делает скрытый downgrade `v1.4 → v1.2`. Legacy v1.2 доступен только при явном `version: "v1.2"`. +- Добавлена типизированная ошибка `DeploymentDisabledError` для quarantined deployment. +- В SDK добавлены v1.4 records: Polygon, Arbitrum, Avalanche, BNB, Base, Optimism, Unichain, XRPL EVM и Robinhood. Все production records выключены. +- Botchain исключён из v1.4 registry и из production deploy-конфига по ADR-0001. Legacy v1.3 таблицы не менялись. +- Robinhood chain ID `4663` добавлен как метаданные. Settlement выключен. +- Схема активов заменена на универсальный массив `assets[]`; поля `usdc/usdt` сохранены на один compatibility-релиз. + +### AIFINP-224 — Solana + +- Обновлены program ID из `AiFinPay/solana-contract@e5df8f5436cf646ab495381eee04e0d1a10b4e2f`: + - devnet: `8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y`; + - mainnet: `724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD`. +- Обновлены ссылки на IDL snapshots от 11 сентября 2026. +- Добавлена типизированная ошибка `SolanaDeploymentDisabledError`. +- Оба Solana deployment сохранены как метаданные, но settlement выключен: backend пока не верифицирует Solana receipts. + +### Исправления в evm-contract + +- Base latest record помечен `invalid`: splitter и TokenList указывали на один адрес; Profiles и ожидаемый splitter не имеют runtime code. +- Polygon `0x2791…` исправлен с ошибочного `USDT` на `USDC.e`; legacy `usdt` обнулён. +- Robinhood latest record отражает фактический пустой TokenList. Deploy-скрипт теперь использует все configured assets (`USDe`, `USDG`), а не только USDC/USDT. +- Новый deployment всегда записывается `disabled` до независимой проверки. +- При повторном использовании TokenList/Profiles deploy-скрипт сверяет admin, allowlist и route economics до deploy splitter. +- On-chain checker теперь проверяет: + - hash runtime-кода splitter; + - ненулевые и разные адреса splitter/TokenList/Profiles; + - запрет `address(0)` в TokenList; + - универсальный список активов; + - точный набор маршрутов; + - `agent-x402 = 0/0 bps`; + - `merchant-aifp1 = 100/0 bps`; + - нулевой routeTreasury override. +- Read-only checker больше не требует private key. Проверка deployer role включается только через публичный `AIFINPAY_DEPLOYER_ADDRESS`. +- Robinhood добавлен в Safe checker. +- Сломанный CI, который ссылался на удалённый legacy `registry/registry.json`, заменён offline v1.4 artifact gate. + +## Найденные ошибки и статус + +| Проблема | Риск | Статус | +|---|---:|---| +| Base record указывал TokenList как splitter | P0 | Quarantine; нужен redeploy | +| Polygon USDC.e был подписан как USDT | P0 | Исправлено в config/SDK; on-chain allowlist требует решения Safe | +| Robinhood TokenList пустой из-за USDC/USDT-only deploy-кода | P0 | Код исправлен; сеть выключена; нужна Safe-транзакция | +| `auto` silently fallback на v1.2 | P0 | Исправлено: fail-closed | +| Старые Solana program ID в SDK | P0 | Исправлено | +| Botchain оставался доступен для v1.4 deploy | P1 | Заблокирован кодом | +| Checker не сверял runtime hash и точную экономику routes | P1 | Исправлено для splitter/routes | +| CI требовал удалённый и несовместимый legacy registry | P1 | Исправлено offline v1.4 gate | +| Solana backend verification отсутствует | P0 | Открыто; settlement выключен | +| Quote v1.4 не фиксирует fee-profile values; Profiles может измениться до settlement | P0 | Открыто; нужен новый contract/version design | +| Hash TokenList и Profiles не закреплён в deployment record | P1 | Открыто | +| Safe checker ещё не проверяет modules/guard/fallback handler/proxy singleton | P1 | Открыто | +| Solana upgrade authority остаётся у одиночных ключей deployer | P1 | Открыто; перенос в Squads/multisig | + +## Результаты проверок + +### SDK + +- TypeScript build: PASS. +- Registry drift check: PASS. +- Целевые resolver/provenance тесты: **43/43 PASS**. +- Полный прогон: **356/357 PASS**. Один существующий network-path тест `funding.test.ts` превысил timeout 5 секунд. +- Изолированный повтор `funding.test.ts` с timeout 15 секунд: **8/8 PASS**. +- MCP TypeScript build после сборки SDK: PASS. + +### EVM contract repo + +- Offline deployment validator: **PASS, 10 records**. +- Новые production-config tests: **3/3 PASS**. +- Prettier check: PASS. +- GitHub Actions CI для опубликованного PR: **PASS**. +- `git diff --check`: PASS. +- Полная компиляция в текущем окружении заблокирована: Hardhat `HHE905` не смог скачать список версий компилятора. Запуск `--no-compile` подтвердил новые тесты, но полный contract suite без скомпилированных artifacts невалиден. + +## Что нельзя включать до пилота + +Production v1.4 нельзя активировать одним изменением флага. До включения каждой сети нужны: + +1. Backend verifier для конкретных chain, route и asset. +2. Две независимые RPC-проверки runtime code, roles, Safe, TokenList и Profiles. +3. Проверка Safe modules, guard, fallback handler и singleton. +4. Один funded E2E для AIFP-1 и один для AIFP-2; затем replay/duplicate test. +5. Kill-switch и rollback test без перехода на v1.2. +6. Отдельный контрактный фикс fee-profile TOCTOU или письменное принятие риска CTO/security owner. + +Отдельные on-chain действия: + +- Base: новый deployment splitter + Profiles + корректный record. +- Robinhood: 3-of-4 Safe allowlist для USDe/USDG после проверки адресов. +- Polygon: решить, поддерживаем ли USDC.e; если нет — удалить из TokenList через Safe. +- Solana: перенести upgrade authority в Squads/multisig и подключить backend receipt verification. + +## Коммиты и ветки + +- `evm-contract`: PR [AiFinPay/evm-contract#33](https://github.com/AiFinPay/evm-contract/pull/33), branch `codex/aifinp-223-deployment-safety`, commits `a54a4c107de7bb42f54e411e621d3897938bfc31` и `726fb1d7b151b31f7ad9a2f95731496ef72463c2`. +- `sdk`: PR [AiFinPay/sdk#70](https://github.com/AiFinPay/sdk/pull/70), branch `codex/aifinp-223-224-payment-registry`. + +Mainnet-транзакции, Safe-подписи и Jira-переходы не выполнялись. diff --git a/docs/stories/aifinp-223-224-tests.md b/docs/stories/aifinp-223-224-tests.md new file mode 100644 index 0000000..022352c --- /dev/null +++ b/docs/stories/aifinp-223-224-tests.md @@ -0,0 +1,363 @@ +# AIFINP-223 / AIFINP-224 — SDK deployment resolver QA plan + +Status: **IMPLEMENTED; TARGETED TESTS PASS; PRODUCTION ACTIVATION BLOCKED** +Date: 2026-09-13 + +## Execution update — 2026-09-13 + +- SDK resolver, Solana IDs, canonical v1.4 registry, generator and provenance tests implemented. +- Targeted SDK gate: **43/43 PASS**. +- SDK TypeScript build: **PASS**. +- Full SDK regression after build: **356/357 PASS**; the pre-existing live-path funding test exceeded its 5-second timeout. Isolated rerun with a 15-second timeout: **8/8 PASS**. +- EVM offline artifact validator: **PASS**, 10 records. +- EVM production-config tests: **3/3 PASS**. +- EVM formatting and diff checks: **PASS**. +- Full EVM compile is environment-blocked: Hardhat `HHE905` could not download the compiler version list. Running without compile cannot recreate missing artifacts, so only the new config suite was isolated and verified. +- All production v1.4 deployments remain disabled. Activation still requires backend verification, independent on-chain checks, governance approval and funded E2E tests. +Repositories: `AiFinPay/sdk`, `AiFinPay/evm-contract`, `AiFinPay/solana-contract` +Requirements: AIFINP-223 (EVM environment/version resolver), AIFINP-224 (Solana environment/version resolver) + +## Scope and risk + +This plan covers the SDK changes that consume: + +- EVM deployment artifacts through `AiFinPay/evm-contract@78240eccf96dd078c9b40be068d635b14876364c`; +- Solana deployment artifacts through `AiFinPay/solana-contract@e5df8f5436cf646ab495381eee04e0d1a10b4e2f`; +- the EVM and Solana deployment resolvers; +- the generated deployment data bundled into `@aifinpay/agent`; +- the MCP production settlement-chain allowlist and schema; +- the rule from ADR-0001 that BOT Chain is not a production settlement network; +- Robinhood Chain as a production configuration target. + +Risk class: **critical payment-routing change**. A wrong chain, program, splitter, token, role, or fallback can send funds to an unintended destination or produce an on-chain payment that the backend cannot verify. Suggested AI autonomy cap: **20%**. Human approval is required at architecture, security, code-review, and production-deployment gates. + +No mainnet transaction is part of this QA plan. Enabling any production route requires a separate authorized deployment decision and a funded end-to-end test. + +## Requirement clarification that blocks final sign-off + +The current AIFINP-223 implementation and tests define `auto` as `v1.4 -> v1.2`. That is unsafe for payment selection: a missing, disabled, stale, or invalid v1.4 record silently changes the settlement contract and protocol semantics. + +The test oracle for this change must be: + +1. `auto` selects an eligible v1.4 deployment. +2. `auto` fails closed with a typed error when an eligible v1.4 deployment is unavailable. +3. `auto` never selects v1.2. +4. Explicit v1.2 may remain only as an intentionally deprecated compatibility path if product/security explicitly approve it; it must never be reached by fallback. +5. The existence of a deployment record does not make a route settlement-enabled. Availability and settlement eligibility must be represented separately. + +If Jira continues to require automatic fallback to v1.2, the Requirement Gate remains failed. The ticket acceptance criteria and the security decision must agree before tests are treated as authoritative. + +## Evidence reviewed + +### Current SDK baseline (`97b00ef25aad4b90d22822b8dad4aeb074cdaa58`) + +- `node/src/v14Deployments.generated.ts` pins the old EVM source commit `67b3f518...` and contains only Amoy and Polygon. +- `node/src/solanaV14Deployments.generated.ts` pins the old Solana source commit `8a13d10...` and old program IDs. +- `node/src/deploymentResolver.ts` silently falls back to v1.2 in `auto` mode. +- `node/tests/deploymentResolver.test.ts` explicitly requires fallback for Base, Optimism, Unichain, BOT Chain, and XRPL EVM. +- `mcp/src/tools/production-control.ts` advertises BOT Chain and does not advertise Robinhood. +- No SDK test directly exercises `settlementInvoiceTool()` or `runSettlementInvoice()` against the chain allowlist. +- The existing registry provenance gate covers the vendored v1.3 splitter table. It does not verify `v14Deployments.generated.ts` or `solanaV14Deployments.generated.ts` against their pinned source commits. +- No enforced coverage threshold was found for the Node SDK or MCP test suites. + +### Expected deployment identity + +EVM v1.4 records expected from the reviewed contract branch: + +| Environment | Network | Chain ID | Expected SDK state | +|---|---|---:|---| +| dev | amoy | 80002 | present; dev only | +| prod | polygon | 137 | present; eligibility controlled separately | +| prod | base | 8453 | present only if validation passes; otherwise quarantined | +| prod | arbitrum | 42161 | present; eligibility controlled separately | +| prod | avalanche | 43114 | present; eligibility controlled separately | +| prod | bnb | 56 | present; eligibility controlled separately | +| prod | optimism | 10 | present; eligibility controlled separately | +| prod | robinhood | 4663 | present; eligibility controlled separately | +| prod | unichain | 130 | present; eligibility controlled separately | +| prod | xrplevm | 1440000 | present; eligibility controlled separately | +| prod | botchain | 677 | absent from selectable production deployments | + +Solana v1.4 records expected from commit `e5df8f5`: + +| Environment | Cluster | Program ID | IDL | +|---|---|---|---| +| dev | devnet | `8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y` | `splitter.devnet.20260911-195503.json` | +| prod | mainnet/mainnet-beta | `724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD` | `splitter.mainnet.20260911-200222.json` | + +## Test strategy + +Use the test pyramid for this change: + +- unit: pure resolver selection, validation, normalization, and schema tests; +- integration: generated data against immutable source artifacts and MCP handler/schema behavior; +- regression: previously observed unsafe fallback, stale identifiers, BOT Chain exposure, token confusion, and environment leakage; +- staging E2E: read-only on-chain identity checks followed by one separately authorized funded payment per enabled route. + +All PR tests must be deterministic and isolated. DNS, public RPC, GitHub availability, and wall-clock timing must not be dependencies of unit tests. + +## Unit tests + +### EVM resolver — `node/tests/deploymentResolver.test.ts` + +Replace tests that encode fallback with the following behavior tests: + +- dev/Amoy resolves only the dev v1.4 record. +- dev rejects Polygon and every production chain with `UnsupportedDevNetworkError`. +- prod never resolves Amoy. +- explicit v1.4 resolves each eligible production network and returns the exact chain ID and deployment object. +- explicit v1.4 on an absent, disabled, or quarantined network throws a typed unavailable/disabled error. +- `auto` returns v1.4 on an eligible network. +- `auto` on a network with only a legacy v1.2 record throws; it never returns v1.2. +- no version argument has the same fail-closed behavior as `version: "auto"`. +- explicit v1.2 behavior is tested separately and labelled deprecated if retained. +- BOT Chain fails under explicit v1.4 and `auto` in prod. +- Robinhood normalizes and resolves with chain ID 4663 only when eligible. +- `isV14Available` distinguishes record presence from settlement eligibility; if a second helper is introduced, test both meanings explicitly. +- unknown environment, unknown version, empty network, whitespace-only network, and unknown network return typed errors. +- normalization handles ASCII case and surrounding whitespace without accepting look-alike Unicode chain names. +- the discriminated result payload always matches its declared protocol version. +- dev/prod data cannot leak across environments. + +### EVM generated deployment data + +Add a dedicated generated-data test rather than asserting only Polygon: + +- source commit is a full 40-character SHA and equals the reviewed source revision used to generate the table; +- each map key equals `deployment.network`; +- chain IDs are unique and match the expected matrix; +- every critical contract/role address is a valid EVM address; +- splitter, TokenList, and Profiles are non-zero and pairwise distinct; +- runtime code hash is exactly 32 bytes; +- Safe threshold is positive and does not exceed the unique owner count; +- Safe owners contain no duplicates and are valid addresses; +- no production entry is silently considered eligible without a verified eligibility flag; +- BOT Chain is absent; +- Robinhood is present; +- a stablecoin collection accepts symbols beyond USDC/USDT; +- stablecoins contain no duplicate symbol or address within a chain; +- zero address means “not configured” and is not emitted as a real token entry; +- Polygon address `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` is never labelled USDT; +- Robinhood can represent USDe and USDG without mapping them to USDC/USDT fields; +- malformed fixture cases fail for duplicate chain ID, invalid address, zero splitter, identical component addresses, missing Safe, invalid hash, or unsupported environment. + +Base must remain quarantined unless cross-repository/on-chain validation proves that the three critical component addresses are distinct and contain the expected runtime code. A unit fixture should preserve this regression condition. + +### Solana resolver — `node/tests/solanaDeploymentResolver.test.ts` + +- replace both stale program ID constants with the `e5df8f5` IDs listed above; +- dev/devnet resolves the new devnet ID; +- prod/mainnet and prod/mainnet-beta resolve the new mainnet ID; +- dev cannot resolve mainnet and prod cannot resolve devnet; +- explicit v1.4 never downgrades; +- `auto` resolves v1.4 or throws `NoSolanaDeploymentError`; +- explicit v1.2 always throws `SolanaV12UnavailableError`; +- old devnet ID `Dg9v95m6...` and old mainnet ID `8dty5bD...` are absent from the bundled table; +- aliases, case, and surrounding whitespace are deterministic; +- empty, unknown, and Unicode look-alike cluster names fail with typed errors; +- `programId` equals the address inside the referenced IDL artifact; +- source commit is immutable full SHA and all referenced artifact paths exist at that commit; +- program IDs decode as valid 32-byte Solana public keys; +- IDL name/version and declared address match the intended artifact and deployment record. + +### MCP production-control + +Add `mcp/tests/production-control.test.ts`: + +- `settlementInvoiceTool().inputSchema.properties.chain.enum` includes Robinhood; +- the enum excludes BOT Chain; +- the enum has no duplicates and matches the SDK’s selectable production-chain source rather than a second hand-maintained list; +- `runSettlementInvoice` lowercases a valid Robinhood input and sends `chain: "robinhood"` to `/v1/settlement/invoice`; +- BOT Chain is rejected before any HTTP request; +- unknown, empty, whitespace-only, and Unicode look-alike chain values are rejected before HTTP; +- invalid route is rejected before HTTP; +- valid asset symbols beyond USDC/USDT are preserved according to the selected chain’s token policy; +- response failures return an MCP error and never retry another chain or protocol version; +- handler and advertised schema accept exactly the same chain set. + +## Integration tests + +### Immutable provenance + +Add a CI script for both new generated tables, parallel to `node/scripts/check-registry-provenance.mjs`: + +1. read the full commit SHA from the generated source metadata; +2. reject a branch, tag, short SHA, missing file, or moving reference; +3. fetch/read every listed deployment and Safe/IDL artifact at that exact commit; +4. regenerate in memory; +5. compare the generated output byte-for-byte; +6. fail closed on mismatch or unavailable evidence. + +The ordinary unit suite should use committed fixtures and remain offline. The cross-repository provenance check can run as its own CI job with a clear infrastructure-failure result. + +### EVM contract-repository gate + +Before an EVM entry becomes eligible: + +- deployment JSON passes schema validation; +- chain ID matches the target RPC; +- splitter, TokenList, and Profiles each have non-empty code; +- computed runtime bytecode hashes match the pinned hashes; +- critical addresses are pairwise distinct; +- admin, signer, pauser, treasury, profiles, and token list read back exactly; +- Safe owners and threshold match the deployment artifact; +- stablecoin allowlist readback matches the symbol/address configuration; +- two independent RPC providers agree for a production-enabled route; +- BOT Chain has no production-enabled registry record; +- Base fails the gate until its conflicting/invalid deployment evidence is replaced; +- Robinhood fails settlement eligibility until its intended USDe/USDG allowlist is confirmed on-chain. + +The current `evm-contract` checkout has scripts and CI steps that require `registry/registry.json`, but that directory/file is absent at revision `78240ec`. This is a cross-repository gate blocker: generation and live registry verification cannot pass until the canonical registry artifact is restored or the source-of-truth design is deliberately changed. + +### Solana artifact/on-chain gate + +Before a Solana entry becomes eligible: + +- the IDL address equals the expected program ID for the cluster; +- `getAccountInfo` reports the program executable and owned by the expected loader; +- deployment signature is finalized on the intended cluster; +- configuration PDA exists, belongs to the program, is initialized, and is not paused; +- route/profile and token-list PDAs match the intended policy; +- upgrade authority is recorded and approved; +- the deployed binary hash/build evidence is recorded and reproducible; +- backend settlement verification supports that exact cluster/program before the SDK advertises it as payable. + +### SDK-to-MCP contract + +- pack `@aifinpay/agent` from the current source; +- install that tarball into MCP; +- build and run MCP tests against it; +- assert production-control derives its selectable network set from the same SDK data/contract; +- verify the npm tarballs actually include the generated deployment metadata and resolver exports. + +## Regression and edge-case matrix + +| Regression | Expected result | +|---|---| +| Missing v1.4 record while a v1.2 record exists | typed failure; no automatic downgrade | +| BOT Chain requested in production | rejected before network/signing work | +| Robinhood requested | recognized only when registry and backend eligibility agree | +| Dev record requested under prod, or prod under dev | typed environment error | +| Stale Solana IDs from the previous table | absent and rejected by data assertions | +| Polygon bridged USDC labelled USDT | generated-data test fails | +| Splitter address equals TokenList/Profiles | validation fails; route quarantined | +| Runtime hash malformed or different | provenance/on-chain gate fails | +| Source metadata points to branch/short SHA | provenance gate fails | +| Duplicate token symbol/address | validation fails | +| Unsupported token on an otherwise valid chain | rejected before signing | +| Backend lacks verifier for an SDK network | network remains non-payable | +| RPC disagreement/unavailable verification | fail closed; do not enable route | +| MCP schema and handler chain lists differ | contract test fails | +| Request differs only by ASCII case/outer whitespace | normalizes deterministically | +| Unicode look-alike chain/program input | rejected | + +## Baseline execution results + +Executed against SDK commit `97b00ef25aad4b90d22822b8dad4aeb074cdaa58` on 2026-09-13: + +```text +cd node +npm test -- --run tests/deploymentResolver.test.ts tests/solanaDeploymentResolver.test.ts +Result: PASS — 2 files, 38 tests. + +cd node +npm test +Result: FAIL — 25 files passed, 1 failed; 351 tests passed, 1 timed out. +Failure: tests/funding.test.ts depends on a live/network path and exceeded 5 s. + +cd mcp +npm test -- --run tests/operator-allowlists.test.ts +Result: FAIL — 10 tests passed, 1 timed out. +Failure: trusted-host test makes a real request to a non-resolving host. + +cd mcp +npm test +Result: FAIL — 9 files passed, 2 failed; 129 tests passed, 5 failed. +Failures: four DNS-dependent safe-fetch tests (`EAI_AGAIN`) and one trusted-host timeout. +``` + +These are existing suite failures, but they still block the Testing Gate. Replace live DNS/RPC behavior in unit tests with injected deterministic fakes. Keep separate opt-in network smoke tests if live connectivity coverage is desired. + +## Existing and proposed commands + +### Fast PR checks + +```bash +cd node +npm ci --no-audit --no-fund +npm run registry:check +node scripts/check-registry-provenance.mjs +npm run build +npm test -- --run tests/deploymentResolver.test.ts tests/solanaDeploymentResolver.test.ts +npm pack --dry-run + +cd ../mcp +npm ci --no-audit --no-fund +npm run build +npm test -- --run tests/production-control.test.ts +npm pack --dry-run +``` + +### Full SDK gate + +```bash +cd node && npm test +cd ../mcp && npm test +``` + +### Contract artifact gate + +```bash +cd ../evm-contract +bun install --frozen-lockfile +bun run build +bun test +bun run lint +bun run prettify:check +node scripts/generate-sdk-table.mjs --check +node scripts/verify-registry.mjs +node scripts/verify-governance-docs.mjs +``` + +## Coverage and flake targets + +- 100% branch coverage for the small pure EVM and Solana resolver selection functions. +- At least 90% statements/branches for changed payment-routing and production-control modules. +- At least 80% statements and 70% branches repository-wide once coverage is configured. +- Zero network-dependent unit tests. +- Flake rate below 1%; a payment-selection test that flakes is a release blocker, not a retry-only exception. +- PR test runtime target under 5 minutes; complete merge validation under 15 minutes. + +## Quality gates + +### Testing Gate + +- [ ] Revised no-silent-downgrade acceptance criterion approved. +- [ ] EVM unit tests written and passing. +- [ ] Solana unit tests written and passing. +- [ ] Generated-data schema/provenance tests written and passing. +- [ ] MCP production-control tests written and passing. +- [ ] Regression and edge cases above covered. +- [ ] Full Node and MCP suites deterministic and green. +- [ ] Coverage thresholds enforced in CI. + +### Security/release gate + +- [ ] No BOT Chain production settlement exposure. +- [ ] Robinhood configuration and backend support agree. +- [ ] Polygon token confusion fixed. +- [ ] Base invalid/conflicting deployment is quarantined or replaced. +- [ ] All eligible EVM deployments verified on-chain with pinned runtime hashes. +- [ ] Both Solana program IDs and artifacts verified on the correct clusters. +- [ ] Backend can verify every route the SDK can execute. +- [ ] Human security/code review completed. +- [ ] One authorized funded E2E succeeds per newly enabled route. +- [ ] Duplicate/replay attempt does not move funds twice. +- [ ] Rollback disables the route without falling back to legacy v1.2. +- [ ] Production monitoring and route kill-switch are confirmed. + +## Testing Gate conclusion + +**TARGETED GATE PASS; PRODUCTION ACTIVATION BLOCKED.** The resolver and provenance tests now enforce fail-closed routing and the CTO-provided Solana IDs. The remaining blockers are operational/on-chain: backend verification, Safe actions, independent RPC verification, funded E2E and the existing flaky live-path tests. + +HANDOFF: CONDITIONAL PASS | restriction: do not enable production v1.4 settlement | return_to: release owner diff --git a/mcp/package-lock.json b/mcp/package-lock.json index a4de28e..6fd9a57 100644 --- a/mcp/package-lock.json +++ b/mcp/package-lock.json @@ -1,15 +1,15 @@ { "name": "@aifinpay/mcp", - "version": "2.0.0-rc.12", + "version": "2.0.0-rc.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aifinpay/mcp", - "version": "2.0.0-rc.12", + "version": "2.0.0-rc.13", "license": "MIT", "dependencies": { - "@aifinpay/agent": "^1.8.1", + "@aifinpay/agent": "2.0.0-rc.13", "@modelcontextprotocol/sdk": "^1.0.0", "bs58": "^6.0.0", "tweetnacl": "^1.0.3" @@ -33,9 +33,9 @@ "license": "MIT" }, "node_modules/@aifinpay/agent": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@aifinpay/agent/-/agent-1.8.4.tgz", - "integrity": "sha512-7QXSIIuii15pYOP/UH9Nxuu1tCRMDdxpor9jEdh5ZFiAKlAsc89RyZjANjb2XLOVpv7kxDd8YkFD2vWuzYjSUg==", + "version": "2.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@aifinpay/agent/-/agent-2.0.0-rc.13.tgz", + "integrity": "sha512-uVnXwEcZBzj8SazFJduKncDB1443Nx3gyZDvyMIOf8m8wm2CQF0Y7JrntAPd64PRlXJoq5iUYF3j5jCgtBLWSg==", "license": "MIT", "dependencies": { "@noble/curves": "^1.9.0", @@ -2926,21 +2926,6 @@ "node": ">= 0.8" } }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/node/package-lock.json b/node/package-lock.json index 2fc9bd8..6a419e2 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aifinpay/agent", - "version": "2.0.0-rc.12", + "version": "2.0.0-rc.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aifinpay/agent", - "version": "2.0.0-rc.12", + "version": "2.0.0-rc.15", "license": "MIT", "dependencies": { "@noble/curves": "^1.9.0", diff --git a/node/package.json b/node/package.json index 55b54dd..6292c57 100644 --- a/node/package.json +++ b/node/package.json @@ -1,6 +1,6 @@ { "name": "@aifinpay/agent", - "version": "2.0.0-rc.14", + "version": "2.0.0-rc.15", "description": "AiFinPay SDK for global Agent Passport identity and route-verified AIFP-1/AIFP-2 settlement for autonomous AI agents.", "type": "module", "main": "dist/index.js", @@ -23,8 +23,10 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "registry:sync": "node scripts/generate-splitter-routes.mjs", - "registry:check": "node scripts/generate-splitter-routes.mjs --check", + "registry:sync": "node scripts/generate-splitter-routes.mjs && node scripts/generate-payment-deployments.mjs", + "registry:check": "node scripts/generate-splitter-routes.mjs --check && node scripts/generate-payment-deployments.mjs --check", + "registry:payment:sync": "node scripts/generate-payment-deployments.mjs", + "registry:payment:check": "node scripts/generate-payment-deployments.mjs --check", "test": "vitest run", "prepublishOnly": "npm run build" }, diff --git a/node/registry/payment-deployments.json b/node/registry/payment-deployments.json new file mode 100644 index 0000000..9415ff7 --- /dev/null +++ b/node/registry/payment-deployments.json @@ -0,0 +1,442 @@ +{ + "schemaVersion": 1, + "generatedAt": "2026-09-13T00:00:00.000Z", + "sources": { + "evm": { + "repo": "AiFinPay/evm-contract", + "commit": "a54a4c107de7bb42f54e411e621d3897938bfc31", + "path": "deployments/*-v14-*-latest.json" + }, + "solana": { + "repo": "AiFinPay/solana-contract", + "commit": "e5df8f5436cf646ab495381eee04e0d1a10b4e2f", + "path": "deployments/splitter_v14/" + } + }, + "deployments": [ + { + "ecosystem": "evm", + "network": "amoy", + "environment": "dev", + "chainId": 80002, + "protocolVersion": "v1.4", + "status": "enabled", + "settlementEnabled": true, + "sourceArtifact": "deployments/amoy-v14-amoy-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0xBdC126193FADf38A86Cd509e56018a95d5B6eeFA", + "tokenList": "0xe67E48966a67AaaaDE5A97F6196E66cd5B16Ac3F", + "profiles": "0x632082e6b99E567005FA4e87774AC199Df9a81a6", + "admin": "0x00009352dc8a1041A724c01632dc549935e05B98", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x00008a55086A450Dc8D7789312D21ACEa142F45e", + "treasury": "0x0000DA1886e173C09A4e04723d8226D84d9c5122" + }, + "assets": [ + { + "symbol": "USDC", + "address": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582" + } + ], + "runtimeCodeHash": "0xd4990487312c00916aa218bdcd697bbf1a2729335b6a4fda903517b9d3153a27", + "safe": { + "address": "0xc9ab36c2af2888414c7ea9160d9e33b773c2b388", + "version": "1.4.1", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "polygon", + "environment": "prod", + "chainId": 137, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/polygon-v14-polygon-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", + "treasury": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3" + }, + "assets": [ + { + "symbol": "USDC", + "name": "USDC", + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" + }, + { + "symbol": "USDC.e", + "name": "Bridged USDC", + "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" + } + ], + "runtimeCodeHash": "0x974b871ac79082d92a7e3bba89ba52794f7906f99119dfad7959017e5b0bf038", + "safe": { + "address": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "arbitrum", + "environment": "prod", + "chainId": 42161, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/arbitrum-v14-arbitrum-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e" + }, + "assets": [ + { + "symbol": "USDC", + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" + } + ], + "runtimeCodeHash": "0xc15837e6f438d0fd2f23e0c5eb7a2f655b2af7dc717bd2861365819de45757f7", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "avalanche", + "environment": "prod", + "chainId": 43114, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/avalanche-v14-avalanche-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e" + }, + "assets": [ + { + "symbol": "USDC", + "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E" + }, + { + "symbol": "USDT", + "address": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7" + } + ], + "runtimeCodeHash": "0x65db1830b1d15400cba35b4ef54dd6c5852d5baa077a732bdc84f7c2218952ef", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "bnb", + "environment": "prod", + "chainId": 56, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/bnb-v14-bnb-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e" + }, + "assets": [ + { + "symbol": "USDC", + "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" + }, + { + "symbol": "USDT", + "address": "0x55d398326f99059fF775485246999027B3197955" + } + ], + "runtimeCodeHash": "0x8aa8c5999a1a0ee87198e380188faaa9d6df88819b47d68682096cc33db81638", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "base", + "environment": "prod", + "chainId": 8453, + "protocolVersion": "v1.4", + "status": "invalid", + "settlementEnabled": false, + "disabledReason": "INVALID: splitter address equals TokenList and Profiles has no runtime code; redeploy required", + "sourceArtifact": "deployments/base-v14-base-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0xbA98C0797707611787B04680E260036573D9D7a1", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "treasury": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e" + }, + "assets": [ + { + "symbol": "USDC", + "name": "USDC", + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + } + ], + "runtimeCodeHash": "0xc724e02657817177d1394e8d41189dd7f8299bb5cf2da04946572378c935883c", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "optimism", + "environment": "prod", + "chainId": 10, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/optimism-v14-optimism-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e" + }, + "assets": [ + { + "symbol": "USDC", + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + } + ], + "runtimeCodeHash": "0x5a2d0ffb996d5655fa483e41d3e5870c6a032fd00bea71c3107f95a4b405e152", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "unichain", + "environment": "prod", + "chainId": 130, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/unichain-v14-unichain-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e" + }, + "assets": [ + { + "symbol": "USDC", + "address": "0x078D782b760474a361dDA0AF3839290b0EF57AD6" + } + ], + "runtimeCodeHash": "0xcb4f1c15a87720c326daa574a7ab52c116c208805167b46dd055f47c9c8695ca", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "xrplevm", + "environment": "prod", + "chainId": 1440000, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "No stablecoin is configured and backend verification is incomplete", + "sourceArtifact": "deployments/xrplevm-v14-xrplevm-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e" + }, + "assets": [], + "runtimeCodeHash": "0xd26862dbc501481675f50924ca013acd5c20aca339f2a1aca818becedf7b5aa0", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "evm", + "network": "robinhood", + "environment": "prod", + "chainId": 4663, + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "TokenList is empty; USDe/USDG require Safe allowlisting and backend verification", + "sourceArtifact": "deployments/robinhood-v14-robinhood-latest.json", + "splitterVersion": "1.4", + "contracts": { + "splitter": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e" + }, + "assets": [], + "runtimeCodeHash": "0x96812eb70224f48c095b240bc0e1d739a7fcb878c3237b5f795629c239d7ef72", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + { + "ecosystem": "solana", + "network": "devnet", + "environment": "dev", + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend Solana receipt verification is not implemented", + "sourceArtifact": "deployments/splitter_v14/splitter.devnet.20260911-195503.json", + "programId": "8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y", + "idl": { + "name": "splitter", + "version": "1.4.1" + } + }, + { + "ecosystem": "solana", + "network": "mainnet", + "environment": "prod", + "protocolVersion": "v1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend Solana receipt verification is not implemented and upgrade authority is not multisig", + "sourceArtifact": "deployments/splitter_v14/splitter.mainnet.20260911-200222.json", + "programId": "724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD", + "idl": { + "name": "splitter", + "version": "1.4.1" + } + } + ] +} diff --git a/node/registry/payment-deployments.schema.json b/node/registry/payment-deployments.schema.json new file mode 100644 index 0000000..465035b --- /dev/null +++ b/node/registry/payment-deployments.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aifinpay.io/schemas/payment-deployments.schema.json", + "title": "AiFinPay payment deployment registry", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "generatedAt", "sources", "deployments"], + "properties": { + "schemaVersion": { "const": 1 }, + "generatedAt": { "type": "string", "format": "date-time" }, + "sources": { "type": "object" }, + "deployments": { + "type": "array", + "items": { "$ref": "#/$defs/deployment" } + } + }, + "$defs": { + "deployment": { + "type": "object", + "required": [ + "ecosystem", + "network", + "environment", + "protocolVersion", + "status", + "settlementEnabled", + "sourceArtifact" + ], + "properties": { + "ecosystem": { "enum": ["evm", "solana"] }, + "network": { "type": "string", "minLength": 1 }, + "environment": { "enum": ["dev", "prod"] }, + "protocolVersion": { "const": "v1.4" }, + "status": { "enum": ["enabled", "disabled", "invalid", "retired"] }, + "settlementEnabled": { "type": "boolean" }, + "disabledReason": { "type": "string", "minLength": 1 }, + "sourceArtifact": { "type": "string", "minLength": 1 }, + "chainId": { "type": "integer", "minimum": 1 }, + "contracts": { "type": "object" }, + "assets": { "type": "array" }, + "runtimeCodeHash": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{64}$" + }, + "safe": { "type": "object" }, + "programId": { "type": "string", "minLength": 32 }, + "idl": { "type": "object" } + }, + "allOf": [ + { + "if": { "properties": { "settlementEnabled": { "const": false } } }, + "then": { "required": ["disabledReason"] } + }, + { + "if": { "properties": { "ecosystem": { "const": "evm" } } }, + "then": { + "required": ["chainId", "contracts", "assets", "runtimeCodeHash"] + } + }, + { + "if": { "properties": { "ecosystem": { "const": "solana" } } }, + "then": { "required": ["programId", "idl"] } + } + ] + } + } +} diff --git a/node/scripts/generate-payment-deployments.mjs b/node/scripts/generate-payment-deployments.mjs new file mode 100644 index 0000000..ee84878 --- /dev/null +++ b/node/scripts/generate-payment-deployments.mjs @@ -0,0 +1,188 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const nodeRoot = path.resolve(here, ".."); +const registryPath = path.join(nodeRoot, "registry/payment-deployments.json"); +const evmOutput = path.join(nodeRoot, "src/v14Deployments.generated.ts"); +const solanaOutput = path.join( + nodeRoot, + "src/solanaV14Deployments.generated.ts", +); +const checkOnly = process.argv.includes("--check"); +const zeroAddress = "0x0000000000000000000000000000000000000000"; + +const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); + +function fail(message) { + throw new Error(`Invalid payment deployment registry: ${message}`); +} + +function validate() { + if (registry.schemaVersion !== 1) fail("schemaVersion must equal 1"); + if (!Array.isArray(registry.deployments)) + fail("deployments must be an array"); + + const keys = new Set(); + for (const deployment of registry.deployments) { + const key = `${deployment.ecosystem}:${deployment.environment}:${deployment.network}:${deployment.protocolVersion}`; + if (keys.has(key)) fail(`duplicate deployment ${key}`); + keys.add(key); + + if (deployment.protocolVersion !== "v1.4") + fail(`${key} has unsupported version`); + if (deployment.settlementEnabled && deployment.status !== "enabled") { + fail(`${key} enables settlement without status=enabled`); + } + if (!deployment.settlementEnabled && !deployment.disabledReason) { + fail(`${key} is disabled without disabledReason`); + } + if (deployment.network === "botchain") + fail("BOT Chain must not be registered for v1.4"); + + if (deployment.ecosystem === "evm") { + if (!Number.isInteger(deployment.chainId)) + fail(`${key} has invalid chainId`); + const addresses = [ + deployment.contracts?.splitter, + deployment.contracts?.tokenList, + deployment.contracts?.profiles, + ]; + if ( + addresses.some((address) => !/^0x[0-9a-fA-F]{40}$/.test(address ?? "")) + ) { + fail(`${key} has an invalid component address`); + } + if ( + deployment.status !== "invalid" && + new Set(addresses.map((a) => a.toLowerCase())).size !== 3 + ) { + fail(`${key} reuses a component address`); + } + for (const asset of deployment.assets ?? []) { + if (!asset.symbol || !/^0x[0-9a-fA-F]{40}$/.test(asset.address ?? "")) { + fail(`${key} has an invalid asset`); + } + if (asset.address.toLowerCase() === zeroAddress) + fail(`${key} contains a zero-address asset`); + } + } + } + + const polygon = registry.deployments.find( + (d) => d.ecosystem === "evm" && d.network === "polygon", + ); + const bridgedUsdc = polygon?.assets?.find( + (asset) => + asset.address.toLowerCase() === + "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + ); + if (bridgedUsdc?.symbol !== "USDC.e") + fail("Polygon 0x2791… must be identified as USDC.e"); + + const expectedSolana = { + devnet: "8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y", + mainnet: "724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD", + }; + for (const [network, programId] of Object.entries(expectedSolana)) { + const deployment = registry.deployments.find( + (d) => d.ecosystem === "solana" && d.network === network, + ); + if (deployment?.programId !== programId) + fail(`unexpected Solana ${network} program id`); + } +} + +function json(value) { + return JSON.stringify(value, null, 2); +} + +function renderEvm() { + const deployments = Object.fromEntries( + registry.deployments + .filter((deployment) => deployment.ecosystem === "evm") + .map((deployment) => { + const findAsset = (symbol) => + deployment.assets.find((asset) => asset.symbol === symbol)?.address ?? + zeroAddress; + return [ + deployment.network, + { + network: deployment.network, + chainId: deployment.chainId, + environment: deployment.environment, + splitterVersion: "1.4", + status: deployment.status, + settlementEnabled: deployment.settlementEnabled, + ...(deployment.disabledReason + ? { disabledReason: deployment.disabledReason } + : {}), + sourceArtifact: deployment.sourceArtifact, + splitter: { + address: deployment.contracts.splitter, + admin: deployment.contracts.admin, + signer: deployment.contracts.signer, + pauser: deployment.contracts.pauser, + treasury: deployment.contracts.treasury, + tokenList: deployment.contracts.tokenList, + profiles: deployment.contracts.profiles, + assets: deployment.assets, + usdc: findAsset("USDC"), + usdt: findAsset("USDT"), + }, + runtimeCodeHash: deployment.runtimeCodeHash, + safe: deployment.safe, + }, + ]; + }), + ); + const source = registry.sources.evm; + return `// DO NOT EDIT. Generated by scripts/generate-payment-deployments.mjs.\n\nimport type { SdkEnvironment } from "./deploymentResolver.js";\n\nexport interface V14Asset {\n symbol: string;\n address: \`0x\${string}\`;\n name?: string;\n source?: string | null;\n}\n\nexport interface V14Splitter {\n address: \`0x\${string}\`;\n admin: \`0x\${string}\`;\n signer: \`0x\${string}\`;\n pauser: \`0x\${string}\`;\n treasury: \`0x\${string}\`;\n tokenList: \`0x\${string}\`;\n profiles: \`0x\${string}\`;\n assets: readonly V14Asset[];\n /** @deprecated Use assets; retained for one compatibility release. */\n usdc: \`0x\${string}\`;\n /** @deprecated Use assets; retained for one compatibility release. */\n usdt: \`0x\${string}\`;\n}\n\nexport interface V14Safe {\n address: \`0x\${string}\`;\n version: string;\n threshold: number;\n owners: readonly \`0x\${string}\`[];\n}\n\nexport interface V14Deployment {\n network: string;\n chainId: number;\n environment: SdkEnvironment;\n splitterVersion: "1.4";\n status: "enabled" | "disabled" | "invalid" | "retired";\n settlementEnabled: boolean;\n disabledReason?: string;\n sourceArtifact: string;\n splitter: V14Splitter;\n runtimeCodeHash: \`0x\${string}\`;\n safe: V14Safe;\n}\n\nexport const V14_DEPLOYMENTS_SOURCE = ${json({ ...source, branch: "dev" })} as const;\n\nexport const V14_DEPLOYMENTS: Record = ${json(deployments)};\n\nexport const V14_DEV_NETWORKS = ["amoy"] as const;\n`; +} + +function renderSolana() { + const deployments = Object.fromEntries( + registry.deployments + .filter((deployment) => deployment.ecosystem === "solana") + .map((deployment) => [ + deployment.network, + { + network: deployment.network, + environment: deployment.environment, + splitterVersion: "1.4", + status: deployment.status, + settlementEnabled: deployment.settlementEnabled, + disabledReason: deployment.disabledReason, + programId: deployment.programId, + idl: { ...deployment.idl, artifact: deployment.sourceArtifact }, + }, + ]), + ); + const source = registry.sources.solana; + return `// DO NOT EDIT. Generated by scripts/generate-payment-deployments.mjs.\n\nimport type { SdkEnvironment } from "./deploymentResolver.js";\n\nexport type SolanaNetwork = "devnet" | "mainnet";\n\nexport interface SolanaV14Deployment {\n network: SolanaNetwork;\n environment: SdkEnvironment;\n splitterVersion: "1.4";\n status: "enabled" | "disabled" | "invalid" | "retired";\n settlementEnabled: boolean;\n disabledReason?: string;\n programId: string;\n idl: { name: string; version: string; artifact: string };\n}\n\nexport const SOLANA_V14_DEPLOYMENTS_SOURCE = ${json({ ...source, branch: "dev" })} as const;\n\nexport const SOLANA_V14_DEPLOYMENTS: Record = ${json(deployments)};\n\nexport const SOLANA_DEV_NETWORKS = ["devnet"] as const;\n`; +} + +function writeOrCheck(outputPath, content) { + if (checkOnly) { + if ( + !fs.existsSync(outputPath) || + fs.readFileSync(outputPath, "utf8") !== content + ) { + throw new Error( + `${path.relative(nodeRoot, outputPath)} is stale; run registry:payment:sync`, + ); + } + } else { + fs.writeFileSync(outputPath, content); + } +} + +validate(); +writeOrCheck(evmOutput, renderEvm()); +writeOrCheck(solanaOutput, renderSolana()); +console.log( + checkOnly + ? "Payment deployment registry is current." + : "Generated payment deployment tables.", +); diff --git a/node/src/chains.ts b/node/src/chains.ts index 870f563..6f2e529 100644 --- a/node/src/chains.ts +++ b/node/src/chains.ts @@ -1,4 +1,4 @@ -// BOT Chain (677) and XRPL EVM (1440000) are not shipped with viem/chains, so +// BOT Chain, Robinhood Chain, and XRPL EVM are not shipped with viem/chains, so // they are defined here rather than in each module that needs them. Two // definitions of the same chain is a drift risk: they would be edited // separately and eventually disagree about an RPC or a chain id. @@ -23,3 +23,18 @@ export const xrplevm: Chain = defineChain({ default: { name: "XRPL EVM Explorer", url: "https://explorer.xrplevm.org" }, }, }); + +/** Metadata only. v1.4 settlement remains disabled until the TokenList and + * backend verification gates pass. */ +export const robinhood: Chain = defineChain({ + id: 4663, + name: "Robinhood Chain", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } }, + blockExplorers: { + default: { + name: "Robinhood Chain Blockscout", + url: "https://robinhoodchain.blockscout.com", + }, + }, +}); diff --git a/node/src/deploymentResolver.ts b/node/src/deploymentResolver.ts index addcfe4..5c2798b 100644 --- a/node/src/deploymentResolver.ts +++ b/node/src/deploymentResolver.ts @@ -11,10 +11,10 @@ * environment — "dev" | "prod". Development supports Amoy only and uses the * v1.4 artifacts sourced from evm-contract's dev branch. Production uses the * configured production networks. - * version — "v1.2" | "v1.4" | "auto". "auto" prefers v1.4 where it is - * deployed for the environment+network and otherwise falls back to v1.2. - * An explicit "v1.4" never silently downgrades: if it is not deployed there - * the call throws, so a caller that asked for v1.4 knows it did not get it. + * version — "v1.2" | "v1.4" | "auto". "auto" means the current v1.4 + * deployment for that exact environment+network. It never downgrades to a + * legacy contract. Legacy v1.2 remains available only when requested + * explicitly. * * Addresses live in data files (v14Deployments.generated.ts and the legacy * SPLITTER_DEPLOYMENTS table), never inline here — this module is selection @@ -38,8 +38,8 @@ export type SdkEnvironment = "dev" | "prod"; * (`SplitterDeployment.version`, which is "1.1" or "1.2" depending on chain). */ export type ProtocolVersion = "v1.2" | "v1.4"; -/** What a caller may ask for. "auto" (the default) resolves v1.4 when present, - * else v1.2. */ +/** What a caller may ask for. "auto" (the default) resolves only an enabled + * v1.4 deployment and never silently downgrades. */ export type RequestedVersion = ProtocolVersion | "auto"; export interface ResolveDeploymentOptions { @@ -105,20 +105,34 @@ export class VersionUnavailableError extends DeploymentResolverError { ) { super( `${requested} is not deployed for ${environment}/${network}. It was ` + - `requested explicitly, so the SDK will not substitute another version. ` + - `Pass version "auto" to allow a documented fallback.`, + `requested explicitly, so the SDK will not substitute another version.`, ); this.name = "VersionUnavailableError"; this.requested = requested; } } -/** No deployment of any supported version exists for this environment+network. */ -export class NoDeploymentError extends DeploymentResolverError { - constructor(environment: SdkEnvironment, network: string) { +/** A deployment record exists, but is quarantined and must not receive money. */ +export class DeploymentDisabledError extends DeploymentResolverError { + readonly environment: SdkEnvironment; + readonly network: string; + readonly reason: string; + + constructor(environment: SdkEnvironment, network: string, reason: string) { super( - `No v1.4 or v1.2 deployment is known for ${environment}/${network}.`, + `v1.4 settlement is disabled for ${environment}/${network}: ${reason}`, ); + this.name = "DeploymentDisabledError"; + this.environment = environment; + this.network = network; + this.reason = reason; + } +} + +/** No current v1.4 deployment exists for this environment+network. */ +export class NoDeploymentError extends DeploymentResolverError { + constructor(environment: SdkEnvironment, network: string) { + super(`No v1.4 deployment is known for ${environment}/${network}.`); this.name = "NoDeploymentError"; } } @@ -158,7 +172,7 @@ export function isV14Available( environment: SdkEnvironment, network: string, ): boolean { - return findV14(environment, normalize(network)) !== undefined; + return findV14(environment, normalize(network))?.settlementEnabled === true; } // ── The resolver ───────────────────────────────────────────────────────────── @@ -169,7 +183,8 @@ export function isV14Available( * * @throws UnsupportedDevNetworkError dev asked for a non-Amoy network * @throws VersionUnavailableError an explicit version is not deployed here - * @throws NoDeploymentError neither version is deployed here + * @throws DeploymentDisabledError a v1.4 record exists but is quarantined + * @throws NoDeploymentError no v1.4 deployment is known here */ export function resolveDeployment( options: ResolveDeploymentOptions, @@ -208,18 +223,28 @@ export function resolveDeployment( chainId: d.chainId, deployment: d, }); + const enabledV14 = (): V14Deployment => { + if (!v14) throw new NoDeploymentError(environment, options.network); + if (!v14.settlementEnabled) { + throw new DeploymentDisabledError( + environment, + network, + v14.disabledReason ?? "deployment has not passed the settlement gate", + ); + } + return v14; + }; switch (requested) { case "v1.4": - if (v14) return asV14(v14); - throw new VersionUnavailableError("v1.4", environment, options.network); + if (!v14) + throw new VersionUnavailableError("v1.4", environment, options.network); + return asV14(enabledV14()); case "v1.2": if (v12) return asV12(v12); throw new VersionUnavailableError("v1.2", environment, options.network); case "auto": - if (v14) return asV14(v14); - if (v12) return asV12(v12); - throw new NoDeploymentError(environment, options.network); + return asV14(enabledV14()); default: throw new DeploymentResolverError( `Unknown version "${String(requested)}"; use "v1.2", "v1.4" or "auto".`, diff --git a/node/src/index.ts b/node/src/index.ts index 787b4db..54613d1 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -76,7 +76,7 @@ export type { SplitterRouteKey, SplitterRouteDeployment, } from "./splitterRoutes.js"; -export { botchain, xrplevm } from "./chains.js"; +export { botchain, robinhood, xrplevm } from "./chains.js"; // ── Environment & protocol-version resolver (AIFINP-223) ───────────────── export { @@ -85,6 +85,7 @@ export { DeploymentResolverError, UnsupportedDevNetworkError, VersionUnavailableError, + DeploymentDisabledError, NoDeploymentError, } from "./deploymentResolver.js"; export type { @@ -101,6 +102,7 @@ export { } from "./v14Deployments.generated.js"; export type { V14Deployment, + V14Asset, V14Splitter, V14Safe, } from "./v14Deployments.generated.js"; @@ -112,6 +114,7 @@ export { UnsupportedSolanaDevNetworkError, SolanaVersionUnavailableError, SolanaV12UnavailableError, + SolanaDeploymentDisabledError, NoSolanaDeploymentError, } from "./solanaDeploymentResolver.js"; export type { @@ -130,7 +133,11 @@ export type { SolanaNetwork, } from "./solanaV14Deployments.generated.js"; -export { AiFinPayAgent, SPLITTER_DEPLOYMENTS, paymentIdFor } from "./unifiedAgent.js"; +export { + AiFinPayAgent, + SPLITTER_DEPLOYMENTS, + paymentIdFor, +} from "./unifiedAgent.js"; export type { AiFinPayAgentOptions, CallOptions, diff --git a/node/src/solanaDeploymentResolver.ts b/node/src/solanaDeploymentResolver.ts index 8207d2e..c24b7a2 100644 --- a/node/src/solanaDeploymentResolver.ts +++ b/node/src/solanaDeploymentResolver.ts @@ -29,7 +29,10 @@ * Program ids live in solanaV14Deployments.generated.ts, never inline here — * this module is selection logic only. */ -import { DeploymentResolverError, type SdkEnvironment } from "./deploymentResolver.js"; +import { + DeploymentResolverError, + type SdkEnvironment, +} from "./deploymentResolver.js"; import { SOLANA_DEV_NETWORKS, SOLANA_V14_DEPLOYMENTS, @@ -111,6 +114,24 @@ export class SolanaV12UnavailableError extends DeploymentResolverError { } } +/** The program exists, but settlement is quarantined until verification and + * governance gates are complete. */ +export class SolanaDeploymentDisabledError extends DeploymentResolverError { + readonly environment: SdkEnvironment; + readonly network: string; + readonly reason: string; + + constructor(environment: SdkEnvironment, network: string, reason: string) { + super( + `Solana v1.4 settlement is disabled for ${environment}/${network}: ${reason}`, + ); + this.name = "SolanaDeploymentDisabledError"; + this.environment = environment; + this.network = network; + this.reason = reason; + } +} + /** No Solana v1.4 deployment exists for this environment+network, and Solana * has no fallback version. */ export class NoSolanaDeploymentError extends DeploymentResolverError { @@ -148,7 +169,10 @@ export function isSolanaV14Available( environment: SdkEnvironment, network: string, ): boolean { - return findSolanaV14(environment, normalizeNetwork(network)) !== undefined; + return ( + findSolanaV14(environment, normalizeNetwork(network))?.settlementEnabled === + true + ); } // ── The resolver ───────────────────────────────────────────────────────────── @@ -177,7 +201,10 @@ export function resolveSolanaDeployment( // Development is restricted to devnet, whatever version is asked for. Reject // any other development cluster rather than silently resolving mainnet. - if (environment === "dev" && !SOLANA_DEV_NETWORKS.includes(network as never)) { + if ( + environment === "dev" && + !SOLANA_DEV_NETWORKS.includes(network as never) + ) { throw new UnsupportedSolanaDevNetworkError(options.network); } @@ -189,17 +216,32 @@ export function resolveSolanaDeployment( programId: d.programId, deployment: d, }); + const enabledV14 = (): SolanaV14Deployment => { + if (!v14) throw new NoSolanaDeploymentError(environment, options.network); + if (!v14.settlementEnabled) { + throw new SolanaDeploymentDisabledError( + environment, + network, + v14.disabledReason ?? "deployment has not passed the settlement gate", + ); + } + return v14; + }; switch (requested) { case "v1.4": - if (v14) return asV14(v14); - throw new SolanaVersionUnavailableError("v1.4", environment, options.network); + if (!v14) + throw new SolanaVersionUnavailableError( + "v1.4", + environment, + options.network, + ); + return asV14(enabledV14()); case "v1.2": // No Solana v1.2 deployment exists — there is nothing to return. throw new SolanaV12UnavailableError(); case "auto": - if (v14) return asV14(v14); - throw new NoSolanaDeploymentError(environment, options.network); + return asV14(enabledV14()); default: throw new DeploymentResolverError( `Unknown version "${String(requested)}"; use "v1.2", "v1.4" or "auto".`, diff --git a/node/src/solanaV14Deployments.generated.ts b/node/src/solanaV14Deployments.generated.ts index 065f262..1692170 100644 --- a/node/src/solanaV14Deployments.generated.ts +++ b/node/src/solanaV14Deployments.generated.ts @@ -1,90 +1,56 @@ -// DO NOT EDIT BY HAND. Bundled copy of the Solana splitter v1.4 deployment -// artifacts from AiFinPay/solana-contract, so a shipped build resolves the -// Solana program id without a runtime GitHub fetch — the same discipline as -// v14Deployments.generated.ts (EVM) and splitterRoutes.generated.ts. -// -// Source: AiFinPay/solana-contract @ dev (commit -// 8a13d10d6210cd37345f7ae6684068bf849e24b1), files: -// deployments/splitter_v14/splitter.mainnet.20260910-170049.json (mainnet IDL) -// deployments/splitter_v14/splitter-mainnet-deploy-20260910-170049.log -// deployments/splitter_v14/splitter.devnet.20260910-143306.json (devnet IDL) -// deployments/splitter_v14/splitter-deploy-20260910-143306.log -// -// To refresh: read the same files off solana-contract's dev branch, e.g. -// git show origin/dev:deployments/splitter_v14/splitter.mainnet..json -// and update the program ids below. Development deployments are, by design, -// devnet; production is mainnet-beta. The program id in each entry is the -// canonical program id confirmed in the deploy log for that cluster. -// -// NOTE ON FALLBACK: unlike EVM (which falls back v1.4 -> v1.2), Solana has NO -// v1.2-equivalent splitter to fall back to. The previous Solana program -// (5g9zWHF1Vv6GiGpA2ZbJQbSCDZd5hAk9AyvabRJvKFx2) was closed and removed from -// mainnet, and the SDK never bundled it — it takes the program id at runtime -// from the bridge's pay_solana challenge. So the Solana resolver offers v1.4 -// only; "auto" resolves v1.4 and raises a typed no-deployment error where v1.4 -// is absent rather than inventing a downgrade. +// DO NOT EDIT. Generated by scripts/generate-payment-deployments.mjs. import type { SdkEnvironment } from "./deploymentResolver.js"; -/** Solana clusters the resolver understands. `dev` maps to devnet, `prod` to - * mainnet-beta. */ export type SolanaNetwork = "devnet" | "mainnet"; -/** A Solana splitter v1.4 deployment, as recorded by the deploy scripts. */ export interface SolanaV14Deployment { network: SolanaNetwork; - /** Which SDK environment this deployment belongs to: devnet is the dev - * target, mainnet is prod. */ environment: SdkEnvironment; splitterVersion: "1.4"; - /** Base58 on-chain program id, from the deploy log's "Canonical program ID - * confirmed" line. Never hardcoded in resolver logic — read from here. */ + status: "enabled" | "disabled" | "invalid" | "retired"; + settlementEnabled: boolean; + disabledReason?: string; programId: string; - /** IDL metadata (Anchor), for callers that load the IDL. */ - idl: { - name: string; - version: string; - /** Path of the source IDL artifact in solana-contract, for provenance. */ - artifact: string; - }; + idl: { name: string; version: string; artifact: string }; } -/** Where this table came from, so a build can be traced to a commit. */ export const SOLANA_V14_DEPLOYMENTS_SOURCE = { - repo: "AiFinPay/solana-contract", - branch: "dev", - commit: "8a13d10d6210cd37345f7ae6684068bf849e24b1", - path: "deployments/splitter_v14/", + "repo": "AiFinPay/solana-contract", + "commit": "e5df8f5436cf646ab495381eee04e0d1a10b4e2f", + "path": "deployments/splitter_v14/", + "branch": "dev" } as const; -/** Solana v1.4 deployments, keyed by cluster. Devnet is dev-only; mainnet is - * prod-only. Absence of a cluster here means no v1.4 deployment for it. */ export const SOLANA_V14_DEPLOYMENTS: Record = { - devnet: { - network: "devnet", - environment: "dev", - splitterVersion: "1.4", - programId: "Dg9v95m6ofTwaU9V69PNAyRaKeELwxrne4THUYuUTeon", - idl: { - name: "splitter", - version: "1.4.1", - artifact: - "deployments/splitter_v14/splitter.devnet.20260910-143306.json", - }, - }, - mainnet: { - network: "mainnet", - environment: "prod", - splitterVersion: "1.4", - programId: "8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y", - idl: { - name: "splitter", - version: "1.4.1", - artifact: - "deployments/splitter_v14/splitter.mainnet.20260910-170049.json", - }, + "devnet": { + "network": "devnet", + "environment": "dev", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend Solana receipt verification is not implemented", + "programId": "8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y", + "idl": { + "name": "splitter", + "version": "1.4.1", + "artifact": "deployments/splitter_v14/splitter.devnet.20260911-195503.json" + } }, + "mainnet": { + "network": "mainnet", + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend Solana receipt verification is not implemented and upgrade authority is not multisig", + "programId": "724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD", + "idl": { + "name": "splitter", + "version": "1.4.1", + "artifact": "deployments/splitter_v14/splitter.mainnet.20260911-200222.json" + } + } }; -/** The single development cluster Solana v1.4 supports today. */ export const SOLANA_DEV_NETWORKS = ["devnet"] as const; diff --git a/node/src/v14Deployments.generated.ts b/node/src/v14Deployments.generated.ts index b7f4d85..a2382cb 100644 --- a/node/src/v14Deployments.generated.ts +++ b/node/src/v14Deployments.generated.ts @@ -1,43 +1,29 @@ -// DO NOT EDIT BY HAND. Bundled copy of the B2BSplitter v1.4 deployment -// artifacts from AiFinPay/evm-contract, so a shipped build resolves v1.4 -// addresses without a runtime GitHub fetch — the same discipline as -// splitterRoutes.generated.ts. -// -// Source: AiFinPay/evm-contract @ dev (commit -// 67b3f518dbc615961f2207761fa6320e6c1a42ff), files: -// deployments/amoy-v14-amoy-latest.json + amoy-safe-multisig-latest.json -// deployments/polygon-v14-polygon-latest.json + polygon-safe-multisig-latest.json -// -// To refresh: read the same files off evm-contract's dev branch, e.g. -// git show origin/dev:deployments/polygon-v14-polygon-latest.json -// and update the entries below. Development deployments are, by design, read -// from the dev branch; production deployments from the same shared deployments -// directory. Every address here was written by the v1.4 deploy scripts and -// its runtimeCodeHash is the keccak-256 of the deployed runtime bytecode. +// DO NOT EDIT. Generated by scripts/generate-payment-deployments.mjs. import type { SdkEnvironment } from "./deploymentResolver.js"; -/** The v1.4 splitter's role holders and linked contracts, exactly as the - * deploy artifact records them. address(0) in a token slot means the token is - * not configured on that network (native settlement only for it). */ +export interface V14Asset { + symbol: string; + address: `0x${string}`; + name?: string; + source?: string | null; +} + export interface V14Splitter { address: `0x${string}`; - /** DEFAULT_ADMIN_ROLE holder (governance). */ admin: `0x${string}`; - /** SIGN_OPERATOR_ROLE holder — the only key that can sign a v1.4 quote. */ signer: `0x${string}`; - /** PAUSER_ROLE holder. */ pauser: `0x${string}`; treasury: `0x${string}`; - /** External TokenList contract (owner-mutable stablecoin allowlist). */ tokenList: `0x${string}`; - /** External Profiles contract (route fee profiles). */ profiles: `0x${string}`; + assets: readonly V14Asset[]; + /** @deprecated Use assets; retained for one compatibility release. */ usdc: `0x${string}`; + /** @deprecated Use assets; retained for one compatibility release. */ usdt: `0x${string}`; } -/** The Gnosis Safe that holds admin authority over the v1.4 splitter. */ export interface V14Safe { address: `0x${string}`; version: string; @@ -48,89 +34,419 @@ export interface V14Safe { export interface V14Deployment { network: string; chainId: number; - /** Which SDK environment this deployment belongs to. Amoy is the dev target; - * the production networks carry env "prod". */ environment: SdkEnvironment; splitterVersion: "1.4"; + status: "enabled" | "disabled" | "invalid" | "retired"; + settlementEnabled: boolean; + disabledReason?: string; + sourceArtifact: string; splitter: V14Splitter; - /** keccak-256 of the deployed runtime bytecode, from the deploy artifact. */ runtimeCodeHash: `0x${string}`; safe: V14Safe; } -/** Where this table came from, so a build can be traced to a commit. */ export const V14_DEPLOYMENTS_SOURCE = { - repo: "AiFinPay/evm-contract", - branch: "dev", - commit: "67b3f518dbc615961f2207761fa6320e6c1a42ff", - path: "deployments/", + "repo": "AiFinPay/evm-contract", + "commit": "a54a4c107de7bb42f54e411e621d3897938bfc31", + "path": "deployments/*-v14-*-latest.json", + "branch": "dev" } as const; -/** v1.4 EVM deployments, keyed by network name. Amoy is dev-only; Polygon is a - * production network. Chains absent here have no v1.4 deployment yet, which is - * what makes `version: "auto"` fall back to v1.2 for them. */ export const V14_DEPLOYMENTS: Record = { - amoy: { - network: "amoy", - chainId: 80002, - environment: "dev", - splitterVersion: "1.4", - splitter: { - address: "0xBdC126193FADf38A86Cd509e56018a95d5B6eeFA", - admin: "0x00009352dc8a1041A724c01632dc549935e05B98", - signer: "0x0000e81aEf36D89373FBF0012550B10B63dAa873", - pauser: "0x00008a55086A450Dc8D7789312D21ACEa142F45e", - treasury: "0x0000DA1886e173C09A4e04723d8226D84d9c5122", - tokenList: "0xe67E48966a67AaaaDE5A97F6196E66cd5B16Ac3F", - profiles: "0x632082e6b99E567005FA4e87774AC199Df9a81a6", - usdc: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582", - usdt: "0x0000000000000000000000000000000000000000", + "amoy": { + "network": "amoy", + "chainId": 80002, + "environment": "dev", + "splitterVersion": "1.4", + "status": "enabled", + "settlementEnabled": true, + "sourceArtifact": "deployments/amoy-v14-amoy-latest.json", + "splitter": { + "address": "0xBdC126193FADf38A86Cd509e56018a95d5B6eeFA", + "admin": "0x00009352dc8a1041A724c01632dc549935e05B98", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x00008a55086A450Dc8D7789312D21ACEa142F45e", + "treasury": "0x0000DA1886e173C09A4e04723d8226D84d9c5122", + "tokenList": "0xe67E48966a67AaaaDE5A97F6196E66cd5B16Ac3F", + "profiles": "0x632082e6b99E567005FA4e87774AC199Df9a81a6", + "assets": [ + { + "symbol": "USDC", + "address": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582" + } + ], + "usdc": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582", + "usdt": "0x0000000000000000000000000000000000000000" + }, + "runtimeCodeHash": "0xd4990487312c00916aa218bdcd697bbf1a2729335b6a4fda903517b9d3153a27", + "safe": { + "address": "0xc9ab36c2af2888414c7ea9160d9e33b773c2b388", + "version": "1.4.1", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + "polygon": { + "network": "polygon", + "chainId": 137, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/polygon-v14-polygon-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", + "treasury": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [ + { + "symbol": "USDC", + "name": "USDC", + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" + }, + { + "symbol": "USDC.e", + "name": "Bridged USDC", + "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" + } + ], + "usdc": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "usdt": "0x0000000000000000000000000000000000000000" + }, + "runtimeCodeHash": "0x974b871ac79082d92a7e3bba89ba52794f7906f99119dfad7959017e5b0bf038", + "safe": { + "address": "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + "arbitrum": { + "network": "arbitrum", + "chainId": 42161, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/arbitrum-v14-arbitrum-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [ + { + "symbol": "USDC", + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" + } + ], + "usdc": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "usdt": "0x0000000000000000000000000000000000000000" + }, + "runtimeCodeHash": "0xc15837e6f438d0fd2f23e0c5eb7a2f655b2af7dc717bd2861365819de45757f7", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + "avalanche": { + "network": "avalanche", + "chainId": 43114, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/avalanche-v14-avalanche-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [ + { + "symbol": "USDC", + "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E" + }, + { + "symbol": "USDT", + "address": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7" + } + ], + "usdc": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "usdt": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7" + }, + "runtimeCodeHash": "0x65db1830b1d15400cba35b4ef54dd6c5852d5baa077a732bdc84f7c2218952ef", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + "bnb": { + "network": "bnb", + "chainId": 56, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/bnb-v14-bnb-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [ + { + "symbol": "USDC", + "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" + }, + { + "symbol": "USDT", + "address": "0x55d398326f99059fF775485246999027B3197955" + } + ], + "usdc": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "usdt": "0x55d398326f99059fF775485246999027B3197955" }, - runtimeCodeHash: - "0xd4990487312c00916aa218bdcd697bbf1a2729335b6a4fda903517b9d3153a27", - safe: { - address: "0xc9ab36c2af2888414c7ea9160d9e33b773c2b388", - version: "1.4.1", - threshold: 3, - owners: [ + "runtimeCodeHash": "0x8aa8c5999a1a0ee87198e380188faaa9d6df88819b47d68682096cc33db81638", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", - "0x588A80e94a762C670711ff77CC60a2e65E64F53A", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + "base": { + "network": "base", + "chainId": 8453, + "environment": "prod", + "splitterVersion": "1.4", + "status": "invalid", + "settlementEnabled": false, + "disabledReason": "INVALID: splitter address equals TokenList and Profiles has no runtime code; redeploy required", + "sourceArtifact": "deployments/base-v14-base-latest.json", + "splitter": { + "address": "0xbA98C0797707611787B04680E260036573D9D7a1", + "admin": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "treasury": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [ + { + "symbol": "USDC", + "name": "USDC", + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + } ], + "usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "usdt": "0x0000000000000000000000000000000000000000" }, + "runtimeCodeHash": "0xc724e02657817177d1394e8d41189dd7f8299bb5cf2da04946572378c935883c", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } }, - polygon: { - network: "polygon", - chainId: 137, - environment: "prod", - splitterVersion: "1.4", - splitter: { - address: "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", - admin: "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", - signer: "0x0000e81aEf36D89373FBF0012550B10B63dAa873", - pauser: "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", - treasury: "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", - tokenList: "0xbA98C0797707611787B04680E260036573D9D7a1", - profiles: "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", - usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", - usdt: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "optimism": { + "network": "optimism", + "chainId": 10, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/optimism-v14-optimism-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [ + { + "symbol": "USDC", + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + } + ], + "usdc": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "usdt": "0x0000000000000000000000000000000000000000" }, - runtimeCodeHash: - "0x974b871ac79082d92a7e3bba89ba52794f7906f99119dfad7959017e5b0bf038", - safe: { - address: "0x01b80329ff81ce1d22a9e2e8807df5f92414c3c3", - version: "1.5.0", - threshold: 3, - owners: [ + "runtimeCodeHash": "0x5a2d0ffb996d5655fa483e41d3e5870c6a032fd00bea71c3107f95a4b405e152", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", - "0x588A80e94a762C670711ff77CC60a2e65E64F53A", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + "unichain": { + "network": "unichain", + "chainId": 130, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "Backend v1.4 receipt verification and end-to-end settlement gate are incomplete", + "sourceArtifact": "deployments/unichain-v14-unichain-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [ + { + "symbol": "USDC", + "address": "0x078D782b760474a361dDA0AF3839290b0EF57AD6" + } ], + "usdc": "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + "usdt": "0x0000000000000000000000000000000000000000" }, + "runtimeCodeHash": "0xcb4f1c15a87720c326daa574a7ab52c116c208805167b46dd055f47c9c8695ca", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } }, + "xrplevm": { + "network": "xrplevm", + "chainId": 1440000, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "No stablecoin is configured and backend verification is incomplete", + "sourceArtifact": "deployments/xrplevm-v14-xrplevm-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [], + "usdc": "0x0000000000000000000000000000000000000000", + "usdt": "0x0000000000000000000000000000000000000000" + }, + "runtimeCodeHash": "0xd26862dbc501481675f50924ca013acd5c20aca339f2a1aca818becedf7b5aa0", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + }, + "robinhood": { + "network": "robinhood", + "chainId": 4663, + "environment": "prod", + "splitterVersion": "1.4", + "status": "disabled", + "settlementEnabled": false, + "disabledReason": "TokenList is empty; USDe/USDG require Safe allowlisting and backend verification", + "sourceArtifact": "deployments/robinhood-v14-robinhood-latest.json", + "splitter": { + "address": "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", + "admin": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "signer": "0x0000e81aEf36D89373FBF0012550B10B63dAa873", + "pauser": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "treasury": "0x5AFe07483886DFa0B77C6d60212B6E52D78ac11e", + "tokenList": "0xbA98C0797707611787B04680E260036573D9D7a1", + "profiles": "0x4dcDd923d9c45bd306aA21c4438B3D325f8F783C", + "assets": [], + "usdc": "0x0000000000000000000000000000000000000000", + "usdt": "0x0000000000000000000000000000000000000000" + }, + "runtimeCodeHash": "0x96812eb70224f48c095b240bc0e1d739a7fcb878c3237b5f795629c239d7ef72", + "safe": { + "address": "0x5afe07483886dfa0b77c6d60212b6e52d78ac11e", + "version": "1.5.0", + "threshold": 3, + "owners": [ + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x588A80e94a762C670711ff77CC60a2e65E64F53A" + ] + } + } }; -/** The single development network v1.4 supports today. */ export const V14_DEV_NETWORKS = ["amoy"] as const; diff --git a/node/tests/deploymentResolver.test.ts b/node/tests/deploymentResolver.test.ts index ed410dd..7f3656c 100644 --- a/node/tests/deploymentResolver.test.ts +++ b/node/tests/deploymentResolver.test.ts @@ -8,11 +8,11 @@ import { isV14Available, UnsupportedDevNetworkError, VersionUnavailableError, + DeploymentDisabledError, NoDeploymentError, DeploymentResolverError, SPLITTER_DEPLOYMENTS, resolveSplitterRoute, - V14_DEPLOYMENTS, type ResolvedDeployment, } from "../src/index.js"; @@ -32,8 +32,12 @@ describe("resolveDeployment — environment switch", () => { ).toThrow(UnsupportedDevNetworkError); }); - it("prod resolves a production network", () => { - const r = resolveDeployment({ environment: "prod", network: "polygon" }); + it("prod resolves an explicitly requested legacy production network", () => { + const r = resolveDeployment({ + environment: "prod", + network: "polygon", + version: "v1.2", + }); expect(r.environment).toBe("prod"); expect(r.chainId).toBe(137); }); @@ -48,25 +52,24 @@ describe("resolveDeployment — environment switch", () => { it("does not leak the dev-only amoy deployment into prod", () => { // amoy is a dev deployment; asking for it under prod must not resolve it. expect(() => - resolveDeployment({ environment: "prod", network: "amoy", version: "v1.4" }), + resolveDeployment({ + environment: "prod", + network: "amoy", + version: "v1.4", + }), ).toThrow(VersionUnavailableError); }); }); describe("resolveDeployment — explicit version selection", () => { - it("explicit v1.4 returns v1.4 where deployed (polygon)", () => { - const r = resolveDeployment({ - environment: "prod", - network: "polygon", - version: "v1.4", - }); - expect(r.version).toBe("v1.4"); - if (r.version === "v1.4") { - expect(r.deployment.splitter.address).toBe( - V14_DEPLOYMENTS.polygon.splitter.address, - ); - expect(r.deployment.splitterVersion).toBe("1.4"); - } + it("explicit v1.4 refuses a quarantined Polygon deployment", () => { + expect(() => + resolveDeployment({ + environment: "prod", + network: "polygon", + version: "v1.4", + }), + ).toThrow(DeploymentDisabledError); }); it("explicit v1.2 returns the legacy deployment (polygon)", () => { @@ -90,33 +93,38 @@ describe("resolveDeployment — explicit version selection", () => { expect(r.version).toBe("v1.2"); }); - it("explicit v1.4 does NOT silently downgrade — it throws when unavailable", () => { - // base has a v1.2 deployment but no v1.4. + it("explicit v1.4 does NOT silently downgrade — it rejects invalid Base", () => { expect(() => resolveDeployment({ environment: "prod", network: "base", version: "v1.4", }), - ).toThrow(VersionUnavailableError); + ).toThrow(DeploymentDisabledError); }); it("explicit v1.2 on amoy throws (no legacy deployment on the dev network)", () => { expect(() => - resolveDeployment({ environment: "dev", network: "amoy", version: "v1.2" }), + resolveDeployment({ + environment: "dev", + network: "amoy", + version: "v1.2", + }), ).toThrow(VersionUnavailableError); }); }); describe("resolveDeployment — automatic version selection", () => { - it("auto uses v1.4 when available (polygon)", () => { - const r = resolveDeployment({ environment: "prod", network: "polygon" }); - expect(r.version).toBe("v1.4"); + it("auto refuses a quarantined production deployment", () => { + expect(() => + resolveDeployment({ environment: "prod", network: "polygon" }), + ).toThrow(DeploymentDisabledError); }); - it("auto falls back to v1.2 when v1.4 is unavailable (base)", () => { - const r = resolveDeployment({ environment: "prod", network: "base" }); - expect(r.version).toBe("v1.2"); + it("auto never falls back to v1.2", () => { + expect(() => + resolveDeployment({ environment: "prod", network: "botchain" }), + ).toThrow(NoDeploymentError); }); it("auto uses v1.4 on the dev network (amoy)", () => { @@ -124,14 +132,10 @@ describe("resolveDeployment — automatic version selection", () => { expect(r.version).toBe("v1.4"); }); - it("auto is the default when no version is given", () => { - const withAuto = resolveDeployment({ - environment: "prod", - network: "base", - version: "auto", - }); - const noVersion = resolveDeployment({ environment: "prod", network: "base" }); - expect(noVersion.version).toBe(withAuto.version); + it("auto is the fail-closed default when no version is given", () => { + expect(() => + resolveDeployment({ environment: "prod", network: "base" }), + ).toThrow(DeploymentDisabledError); }); it("auto throws when neither version exists for the network", () => { @@ -140,17 +144,25 @@ describe("resolveDeployment — automatic version selection", () => { ).toThrow(NoDeploymentError); }); - it("every legacy network without v1.4 falls back to v1.2 under auto", () => { - for (const network of ["base", "optimism", "unichain", "botchain", "xrplevm"]) { - const r = resolveDeployment({ environment: "prod", network }); - expect(r.version).toBe("v1.2"); + it("all imported production v1.4 deployments remain quarantined", () => { + for (const network of [ + "polygon", + "base", + "optimism", + "unichain", + "xrplevm", + "robinhood", + ]) { + expect(() => resolveDeployment({ environment: "prod", network })).toThrow( + DeploymentDisabledError, + ); } }); }); describe("isV14Available", () => { - it("is true for polygon prod and amoy dev, false where undeployed", () => { - expect(isV14Available("prod", "polygon")).toBe(true); + it("means settlement-enabled, not merely present in the registry", () => { + expect(isV14Available("prod", "polygon")).toBe(false); expect(isV14Available("dev", "amoy")).toBe(true); expect(isV14Available("prod", "base")).toBe(false); expect(isV14Available("prod", "amoy")).toBe(false); // amoy is dev-only @@ -160,18 +172,31 @@ describe("isV14Available", () => { describe("resolveDeployment — input handling", () => { it("is case-insensitive on the network name", () => { - const r = resolveDeployment({ environment: "prod", network: "PoLyGoN" }); + const r = resolveDeployment({ + environment: "prod", + network: "PoLyGoN", + version: "v1.2", + }); expect(r.chainId).toBe(137); expect(r.network).toBe("polygon"); }); it("returns a discriminated union whose payload matches its version", () => { const results: ResolvedDeployment[] = [ - resolveDeployment({ environment: "prod", network: "polygon", version: "v1.4" }), - resolveDeployment({ environment: "prod", network: "base", version: "v1.2" }), + resolveDeployment({ + environment: "dev", + network: "amoy", + version: "v1.4", + }), + resolveDeployment({ + environment: "prod", + network: "base", + version: "v1.2", + }), ]; for (const r of results) { - if (r.version === "v1.4") expect(r.deployment.splitterVersion).toBe("1.4"); + if (r.version === "v1.4") + expect(r.deployment.splitterVersion).toBe("1.4"); else expect(["1.1", "1.2"]).toContain(r.deployment.version); } }); diff --git a/node/tests/paymentDeploymentRegistry.test.ts b/node/tests/paymentDeploymentRegistry.test.ts new file mode 100644 index 0000000..bec6de6 --- /dev/null +++ b/node/tests/paymentDeploymentRegistry.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { + SOLANA_V14_DEPLOYMENTS, + SOLANA_V14_DEPLOYMENTS_SOURCE, + V14_DEPLOYMENTS, + V14_DEPLOYMENTS_SOURCE, +} from "../src/index.js"; + +describe("payment deployment registry provenance", () => { + it("pins the CTO-provided EVM and Solana commits", () => { + expect(V14_DEPLOYMENTS_SOURCE.commit).toBe( + "a54a4c107de7bb42f54e411e621d3897938bfc31", + ); + expect(SOLANA_V14_DEPLOYMENTS_SOURCE.commit).toBe( + "e5df8f5436cf646ab495381eee04e0d1a10b4e2f", + ); + }); + + it("excludes BOT Chain and imports Robinhood in quarantine", () => { + expect(V14_DEPLOYMENTS.botchain).toBeUndefined(); + expect(V14_DEPLOYMENTS.robinhood.chainId).toBe(4663); + expect(V14_DEPLOYMENTS.robinhood.settlementEnabled).toBe(false); + expect(V14_DEPLOYMENTS.robinhood.splitter.assets).toEqual([]); + }); + + it("marks the invalid Base deployment unusable", () => { + const base = V14_DEPLOYMENTS.base; + expect(base.status).toBe("invalid"); + expect(base.settlementEnabled).toBe(false); + expect(base.splitter.address).toBe(base.splitter.tokenList); + }); + + it("identifies Polygon 0x2791… as bridged USDC, never USDT", () => { + const asset = V14_DEPLOYMENTS.polygon.splitter.assets.find( + ({ address }) => + address.toLowerCase() === "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + ); + expect(asset?.symbol).toBe("USDC.e"); + expect(V14_DEPLOYMENTS.polygon.splitter.usdt).toBe( + "0x0000000000000000000000000000000000000000", + ); + }); + + it("contains the redeployed Solana program IDs but keeps them disabled", () => { + expect(SOLANA_V14_DEPLOYMENTS.devnet.programId).toBe( + "8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y", + ); + expect(SOLANA_V14_DEPLOYMENTS.mainnet.programId).toBe( + "724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD", + ); + expect(SOLANA_V14_DEPLOYMENTS.devnet.settlementEnabled).toBe(false); + expect(SOLANA_V14_DEPLOYMENTS.mainnet.settlementEnabled).toBe(false); + }); +}); diff --git a/node/tests/solanaDeploymentResolver.test.ts b/node/tests/solanaDeploymentResolver.test.ts index 480cf0f..6fd4671 100644 --- a/node/tests/solanaDeploymentResolver.test.ts +++ b/node/tests/solanaDeploymentResolver.test.ts @@ -10,35 +10,33 @@ import { UnsupportedSolanaDevNetworkError, SolanaVersionUnavailableError, SolanaV12UnavailableError, + SolanaDeploymentDisabledError, NoSolanaDeploymentError, DeploymentResolverError, SOLANA_V14_DEPLOYMENTS, SOLANA_DEV_NETWORKS, - type ResolvedSolanaDeployment, } from "../src/index.js"; -const DEVNET_PROGRAM = "Dg9v95m6ofTwaU9V69PNAyRaKeELwxrne4THUYuUTeon"; -const MAINNET_PROGRAM = "8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y"; +const DEVNET_PROGRAM = "8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y"; +const MAINNET_PROGRAM = "724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD"; describe("resolveSolanaDeployment — environment switch", () => { - it("dev supports devnet", () => { - const r = resolveSolanaDeployment({ environment: "dev", network: "devnet" }); - expect(r.environment).toBe("dev"); - expect(r.network).toBe("devnet"); - expect(r.programId).toBe(DEVNET_PROGRAM); + it("recognises devnet but keeps settlement quarantined", () => { + expect(() => + resolveSolanaDeployment({ environment: "dev", network: "devnet" }), + ).toThrow(SolanaDeploymentDisabledError); }); - it("prod resolves mainnet", () => { - const r = resolveSolanaDeployment({ environment: "prod", network: "mainnet" }); - expect(r.environment).toBe("prod"); - expect(r.network).toBe("mainnet"); - expect(r.programId).toBe(MAINNET_PROGRAM); + it("recognises mainnet but keeps settlement quarantined", () => { + expect(() => + resolveSolanaDeployment({ environment: "prod", network: "mainnet" }), + ).toThrow(SolanaDeploymentDisabledError); }); it("accepts mainnet-beta as an alias for mainnet", () => { - const r = resolveSolanaDeployment({ environment: "prod", network: "mainnet-beta" }); - expect(r.network).toBe("mainnet"); - expect(r.programId).toBe(MAINNET_PROGRAM); + expect(() => + resolveSolanaDeployment({ environment: "prod", network: "mainnet-beta" }), + ).toThrow(SolanaDeploymentDisabledError); }); it("dev rejects a non-devnet cluster with a typed error", () => { @@ -70,50 +68,70 @@ describe("resolveSolanaDeployment — environment switch", () => { }); describe("resolveSolanaDeployment — explicit version selection", () => { - it("explicit v1.4 returns v1.4 where deployed (mainnet)", () => { - const r = resolveSolanaDeployment({ - environment: "prod", - network: "mainnet", - version: "v1.4", - }); - expect(r.version).toBe("v1.4"); - expect(r.programId).toBe(MAINNET_PROGRAM); - expect(r.deployment.idl.version).toBe("1.4.1"); + it("explicit v1.4 refuses a quarantined mainnet deployment", () => { + expect(() => + resolveSolanaDeployment({ + environment: "prod", + network: "mainnet", + version: "v1.4", + }), + ).toThrow(SolanaDeploymentDisabledError); }); it("explicit v1.4 does NOT silently downgrade — it throws when unavailable", () => { // prod/devnet has no v1.4 (devnet is a dev deployment), so explicit v1.4 throws. expect(() => - resolveSolanaDeployment({ environment: "prod", network: "devnet", version: "v1.4" }), + resolveSolanaDeployment({ + environment: "prod", + network: "devnet", + version: "v1.4", + }), ).toThrow(SolanaVersionUnavailableError); }); it("explicit v1.2 always throws — Solana has no v1.2 deployment", () => { expect(() => - resolveSolanaDeployment({ environment: "prod", network: "mainnet", version: "v1.2" }), + resolveSolanaDeployment({ + environment: "prod", + network: "mainnet", + version: "v1.2", + }), ).toThrow(SolanaV12UnavailableError); expect(() => - resolveSolanaDeployment({ environment: "dev", network: "devnet", version: "v1.2" }), + resolveSolanaDeployment({ + environment: "dev", + network: "devnet", + version: "v1.2", + }), ).toThrow(SolanaV12UnavailableError); }); }); describe("resolveSolanaDeployment — automatic version selection", () => { - it("auto uses v1.4 on mainnet", () => { - const r = resolveSolanaDeployment({ environment: "prod", network: "mainnet", version: "auto" }); - expect(r.version).toBe("v1.4"); - expect(r.programId).toBe(MAINNET_PROGRAM); + it("auto refuses quarantined mainnet", () => { + expect(() => + resolveSolanaDeployment({ + environment: "prod", + network: "mainnet", + version: "auto", + }), + ).toThrow(SolanaDeploymentDisabledError); }); - it("auto uses v1.4 on the dev cluster (devnet)", () => { - const r = resolveSolanaDeployment({ environment: "dev", network: "devnet", version: "auto" }); - expect(r.version).toBe("v1.4"); - expect(r.programId).toBe(DEVNET_PROGRAM); + it("auto refuses quarantined devnet", () => { + expect(() => + resolveSolanaDeployment({ + environment: "dev", + network: "devnet", + version: "auto", + }), + ).toThrow(SolanaDeploymentDisabledError); }); - it("auto is the default when no version is given", () => { - const r = resolveSolanaDeployment({ environment: "prod", network: "mainnet" }); - expect(r.version).toBe("v1.4"); + it("auto is the fail-closed default when no version is given", () => { + expect(() => + resolveSolanaDeployment({ environment: "prod", network: "mainnet" }), + ).toThrow(SolanaDeploymentDisabledError); }); it("auto throws (no fallback) when v1.4 is not deployed for the network", () => { @@ -125,10 +143,10 @@ describe("resolveSolanaDeployment — automatic version selection", () => { }); describe("isSolanaV14Available", () => { - it("is true for mainnet prod and devnet dev, false otherwise", () => { - expect(isSolanaV14Available("prod", "mainnet")).toBe(true); - expect(isSolanaV14Available("prod", "mainnet-beta")).toBe(true); - expect(isSolanaV14Available("dev", "devnet")).toBe(true); + it("is false until settlement verification and governance gates pass", () => { + expect(isSolanaV14Available("prod", "mainnet")).toBe(false); + expect(isSolanaV14Available("prod", "mainnet-beta")).toBe(false); + expect(isSolanaV14Available("dev", "devnet")).toBe(false); expect(isSolanaV14Available("prod", "devnet")).toBe(false); expect(isSolanaV14Available("dev", "mainnet")).toBe(false); }); @@ -136,23 +154,23 @@ describe("isSolanaV14Available", () => { describe("resolveSolanaDeployment — input handling", () => { it("is case-insensitive on the cluster name", () => { - const r = resolveSolanaDeployment({ environment: "prod", network: "MAINNET" }); - expect(r.programId).toBe(MAINNET_PROGRAM); + expect(() => + resolveSolanaDeployment({ environment: "prod", network: "MAINNET" }), + ).toThrow(SolanaDeploymentDisabledError); }); - it("returns a payload whose program id matches the bundled data", () => { - const r: ResolvedSolanaDeployment = resolveSolanaDeployment({ - environment: "prod", - network: "mainnet", - }); - expect(r.deployment).toBe(SOLANA_V14_DEPLOYMENTS.mainnet); - expect(r.programId).toBe(SOLANA_V14_DEPLOYMENTS.mainnet!.programId); + it("exposes program IDs as metadata without enabling settlement", () => { + expect(SOLANA_V14_DEPLOYMENTS.devnet.programId).toBe(DEVNET_PROGRAM); + expect(SOLANA_V14_DEPLOYMENTS.mainnet.programId).toBe(MAINNET_PROGRAM); }); }); describe("bundled Solana v1.4 data", () => { it("has exactly devnet (dev) and mainnet (prod) entries with 1.4 programs", () => { - expect(Object.keys(SOLANA_V14_DEPLOYMENTS).sort()).toEqual(["devnet", "mainnet"]); + expect(Object.keys(SOLANA_V14_DEPLOYMENTS).sort()).toEqual([ + "devnet", + "mainnet", + ]); for (const [key, d] of Object.entries(SOLANA_V14_DEPLOYMENTS)) { expect(d.network).toBe(key); expect(d.splitterVersion).toBe("1.4"); From 40b8e982fe1be63eb7cc5f55ccce22e639dd22de Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 13 Sep 2026 15:59:51 +0300 Subject: [PATCH 2/2] fix: implement AIFINP-223 auto fallback --- ...il-closed-payment-deployment-resolution.md | 55 ++++++++-------- .../aifinp-223-224-payment-registry.md | 62 +++++++++---------- docs/business/aifinp-223-224-analysis.md | 22 +++---- docs/business/aifinp-223-224-prd.md | 48 +++++++------- ...NP-223-224_IMPLEMENTATION_2026-09-13_RU.md | 6 +- docs/stories/aifinp-223-224-tests.md | 40 ++++++------ node/src/deploymentResolver.ts | 34 ++++++---- node/tests/deploymentResolver.test.ts | 41 +++++++----- 8 files changed, 166 insertions(+), 142 deletions(-) diff --git a/docs/adr/0001-fail-closed-payment-deployment-resolution.md b/docs/adr/0001-fail-closed-payment-deployment-resolution.md index bc6ddac..eab8bfe 100644 --- a/docs/adr/0001-fail-closed-payment-deployment-resolution.md +++ b/docs/adr/0001-fail-closed-payment-deployment-resolution.md @@ -1,4 +1,4 @@ -# ADR-0001: Fail-closed payment deployment resolution +# ADR-0001: Controlled EVM automatic fallback **Date:** 2026-09-13 **Status:** proposed @@ -7,14 +7,14 @@ ## Context -The SDK currently resolves EVM and Solana deployments from separate generated -tables. EVM `auto` selection prefers v1.4 but silently falls back to v1.2 when -v1.4 is unavailable. Solana already fails when v1.4 is absent because it has no -valid v1.2 fallback. +The SDK resolves EVM and Solana deployments from separate generated tables. +AIFINP-223 requires EVM `auto` selection to prefer v1.4 and fall back to v1.2 +when v1.4 is unavailable. Explicit v1.4 must never downgrade. Solana has no +valid v1.2 fallback and therefore fails when v1.4 is unavailable. -Payment versions are not interchangeable. They may use different contracts, -quote formats, payout economics, governance, token allow-lists and backend -verification logic. Availability cannot safely choose those semantics. +Payment versions may use different contracts and verification logic. The +automatic fallback is therefore limited to the documented `auto` mode and the +resolved result always reports the concrete selected version. The reviewed deployment commits also contain networks that are deployed but not yet safe to advertise as payable. Examples include an internally @@ -32,19 +32,21 @@ Solana. - Contract-repository artifacts are imported offline from pinned commits and recorded with hashes. - Runtime code never fetches configuration from GitHub, Jira, or a mutable URL. -- Resolution uses an exact rail, environment, network and version tuple. -- Omitted version/`auto` means the single reviewed default for that exact - network. It does not mean a version search. -- Missing, disabled, mismatched or unverifiable entries return a typed error - before a wallet transaction is constructed. +- Resolution uses an exact rail, environment, network and requested version. +- EVM omitted version/`auto` selects enabled v1.4 first, then a known production + v1.2 deployment. If neither is usable, it returns a typed error. +- Explicit EVM v1.4 and v1.2 never substitute another version. +- Solana `auto` selects only v1.4 because no v1.2 deployment exists. +- Disabled or invalid v1.4 records are never returned as v1.4 payment targets. - Assets are an explicitly identified array. A token symbol alone is never a payment identity. - Deployment existence and settlement readiness are separate registry states. -- BOT Chain has no production payment default and remains disabled in payment - resolution under contract ADR-0001. +- BOT Chain has no v1.4 record under contract ADR-0001. Its existing legacy + v1.2 record remains available for backward compatibility until a separate + deprecation decision removes it. -No resolver may silently downgrade or switch protocol version, environment, -network, rail, route or asset. +No explicit version request may downgrade or switch environment, network, rail, +route or asset. The only version fallback is the AIFINP-223 EVM `auto` rule. ## Consequences @@ -56,12 +58,13 @@ network, rail, route or asset. - Robinhood can represent USDe/USDG without inventing USDC/USDT fields. - Networks can be distributed as disabled metadata and enabled independently after full verification. -- Rollback disables the affected target instead of redirecting money through a - legacy contract. +- A quarantined v1.4 deployment can fall back only where a reviewed legacy + deployment already exists. ### Negative -- Clients that relied on EVM `auto` fallback will receive a typed error. +- EVM callers that omit the version may receive v1.2 while v1.4 is quarantined; + callers that require v1.4 must request it explicitly. - Every deployment/configuration update requires registry regeneration and an SDK release. - Deprecated table exports must be maintained temporarily. @@ -70,11 +73,11 @@ network, rail, route or asset. ## Alternatives considered -### Keep automatic v1.4 → v1.2 fallback +### Remove automatic v1.4 → v1.2 fallback -Rejected. It changes payment semantics because a preferred deployment is -unavailable. A warning is not enough once an autonomous client can submit -funds. +Rejected because it contradicts AIFINP-223 and breaks existing integrations. +The safety boundary is explicit selection: `version: "v1.4"` always fails when +v1.4 is absent, disabled or invalid. ### Fetch the latest deployment files from GitHub at runtime @@ -98,8 +101,8 @@ starts disabled. 1. Add the canonical registry and schema. 2. Import EVM `78240ec...` and Solana `e5df8f5...` as disabled records. 3. Generate the unified TypeScript artifact and deprecated compatibility views. -4. Move both resolvers to exact registry lookup and remove version fallback. -5. Remove BOT Chain from payment allow-lists and add Robinhood as disabled. +4. Keep EVM `auto` fallback centralized in the resolver; keep explicit versions exact. +5. Exclude BOT Chain from v1.4 and add Robinhood as disabled v1.4 metadata. 6. Correct Polygon asset identity and reject the current Base record. 7. Enable deployments individually only after verification, funded E2E, security review and human approval. diff --git a/docs/architecture/aifinp-223-224-payment-registry.md b/docs/architecture/aifinp-223-224-payment-registry.md index 2816746..f67c056 100644 --- a/docs/architecture/aifinp-223-224-payment-registry.md +++ b/docs/architecture/aifinp-223-224-payment-registry.md @@ -4,8 +4,8 @@ **Date:** 2026-09-13 **Tickets:** AIFINP-223 (EVM), AIFINP-224 (Solana) **Risk:** high — payment routing; AI autonomy cap 10–30% -**Decision:** one static registry, generated SDK artifacts, exact fail-closed -resolution, no runtime GitHub fetch, and no silent protocol-version fallback. +**Decision:** one static registry, generated SDK artifacts, no runtime GitHub +fetch, exact explicit-version selection, and the AIFINP-223 EVM `auto` fallback. ## 1. Scope @@ -33,9 +33,9 @@ The SDK currently has three payment-routing sources: | `node/src/solanaV14Deployments.generated.ts` | Solana v1.4 | Contains the superseded devnet/mainnet program IDs from `8a13d10...`. | | `SPLITTER_DEPLOYMENTS` / `splitterRoutes.generated.ts` | legacy EVM | Separate model and selection path; includes BOT Chain. | -`node/src/deploymentResolver.ts` implements `auto` as “v1.4, otherwise v1.2”. -That is a change in payment semantics without payer consent. A missing or -disabled v1.4 record can therefore route money through a legacy contract. +`node/src/deploymentResolver.ts` implements the AIFINP-223 rule: `auto` prefers +v1.4 and otherwise uses v1.2. Explicit v1.4 must stay exact so callers that +require the new protocol cannot be downgraded. Other observed mismatches: @@ -184,13 +184,14 @@ Resolution is deterministic: 1. Normalize only documented aliases. 2. If the caller requests a version, find that exact record. -3. If the version is omitted or `auto`, read the single exact version from - `networkDefaults`. -4. Never search another protocol version, environment, network, or rail. -5. Reject records whose state is not `enabled`. -6. If an asset is requested, require its exact `assetId` and identifier to be +3. For EVM omitted version/`auto`, use enabled v1.4 first, then a known + production v1.2 deployment. +4. For Solana omitted version/`auto`, use v1.4 only; no v1.2 exists. +5. Never switch environment, network, rail, route or asset. +6. Never return a disabled or invalid record as v1.4. +7. If an asset is requested, require its exact `assetId` and identifier to be enabled for the selected deployment. -7. Return an immutable deployment object or a typed error. +8. Return an immutable deployment object or a typed error. Required errors: @@ -201,8 +202,8 @@ Required errors: - `AssetUnavailableError` - `RegistryInvariantError` for an impossible generated state -`auto` is retained only for API compatibility. It means “use the reviewed -default for this exact network”, not “try versions until one works”. +EVM `auto` is the only version-searching mode. Explicit `v1.4` and `v1.2` never +substitute another version. ## 7. Network-specific decisions @@ -213,7 +214,7 @@ default for this exact network”, not “try versions until one works”. | Base | disabled | Reject the current internally inconsistent deployment artifact. Re-enable only after a distinct verified splitter deployment and new artifact. | | Arbitrum, Avalanche, BNB, Optimism, Unichain, XRPL EVM | imported disabled | Addresses may be shipped as metadata, but payment resolution remains off until contract-state, asset and backend-verifier gates pass. | | Robinhood | imported disabled | Add chain ID 4663. Represent USDe and USDG through `assets[]`. Current zero USDC/USDT slots do not authorize stable settlement. | -| BOT Chain | excluded from payment defaults | Preserve a non-payment chain descriptor only if another SDK feature needs it. Resolver always throws `DeploymentDisabledError`; no production payment advertisement. | +| BOT Chain | excluded from v1.4 | No v1.4 deployment under ADR-0001. Preserve the existing v1.2 resolver record for backward compatibility; explicit v1.4 fails. | | Solana devnet | disabled until full evidence | Use program ID `8dty5bD738Z9TzEkDu8vLSnhpJNWtEGMUEcYaKCUTY6y`; require executable hash, initialized state and IDL hash before enablement. | | Solana mainnet | disabled until full evidence | Use program ID `724Ut31i4ecY4dJ25z8HuZetu3A43xtNkPdk4JdbsfdD`; require the same evidence plus backend settlement verification. | @@ -274,21 +275,19 @@ The backend is a cross-repository dependency. A deployment cannot become their results from the new table so existing callers keep their shapes. 4. Retain `V14_DEPLOYMENTS`, `SOLANA_V14_DEPLOYMENTS` and source metadata as deprecated generated views for one release cycle. -5. Change `auto` to exact default selection. Emit a development warning for - callers relying on `auto`; never emit a warning instead of blocking a - payment. -6. Legacy v1.2/v1.3 is reachable only by an explicit version and only while its - registry record is enabled. No new caller should default to it. -7. BOT Chain payment requests fail with a typed disabled error. Removing it - from all public TypeScript unions can wait for the next breaking release if - non-payment consumers still compile against the name. +5. Keep the EVM `auto` fallback centralized: enabled v1.4 first, then an + existing v1.2 deployment. +6. Explicit v1.2/v1.4 requests remain exact. Solana never invents a v1.2 + fallback. +7. BOT Chain remains absent from v1.4 data. Its legacy v1.2 record can be + removed only through a separate compatibility/deprecation decision. 8. Enable one deployment at a time after contract verification, backend support, funded end-to-end testing, security review and human approval. Because the current SDK line is a release candidate, the resolver semantic change should ship in the next RC. If published in a stable line, treat the change as security-significant and document the behavior change prominently; -do not preserve unsafe fallback for semantic-version convenience. +document the `auto` behavior prominently because it can select v1.2. ## 11. Required validation @@ -304,10 +303,11 @@ do not preserve unsafe fallback for semantic-version convenience. ### Resolver -- no v1.4 → v1.2 fallback; +- EVM `auto` falls back v1.4 → v1.2 only when a known v1.2 deployment exists; +- explicit v1.4 never falls back; - no dev → prod or prod → dev crossover; -- missing/disabled Base fails closed; -- BOT Chain always fails closed for payment; +- invalid Base v1.4 is never returned; `auto` uses the existing Base v1.2 record; +- BOT Chain never resolves as v1.4; - Robinhood cannot select zero USDC/USDT; - Polygon cannot resolve `0x2791...` as USDT; - Solana aliases resolve only to the exact cluster; @@ -326,10 +326,10 @@ Rollout is registry-first and network-by-network. Disabled metadata can ship without making a route payable. Enabling a route requires a reviewed registry diff and a new SDK/backend release. -Rollback never changes protocol version automatically. Publish a new registry -with the affected record `disabled`, pause the contract where appropriate, and -release the SDK/backend change. A caller receives `DeploymentDisabledError` and -does not submit funds. +When a v1.4 record is disabled, EVM callers using `auto` may resolve a known +v1.2 deployment as required by AIFINP-223. Callers that must stop instead use +explicit `version: "v1.4"`. Solana and networks without v1.2 fail with a typed +error. ## 13. Architecture gate @@ -337,7 +337,7 @@ does not submit funds. artifacts analyzed. - [x] Impacted SDK modules and cross-repository backend dependency identified. - [x] Data generation, runtime selection and trust dependencies documented. -- [x] ADR created for the non-trivial fail-closed decision. +- [x] ADR created for the controlled automatic-fallback decision. - [ ] Repository bootstrap requirement satisfied: root `AGENTS.md` and `ARCHITECTURE.md` are absent. - [ ] CTO/human reviewer approves the detailed schema and migration plan. diff --git a/docs/business/aifinp-223-224-analysis.md b/docs/business/aifinp-223-224-analysis.md index e133ae2..69149ec 100644 --- a/docs/business/aifinp-223-224-analysis.md +++ b/docs/business/aifinp-223-224-analysis.md @@ -18,8 +18,8 @@ The SDK contains environment/version resolvers for EVM and Solana, but the bundl - The EVM SDK table is pinned to an old `evm-contract` commit and contains only Amoy and Polygon v1.4. - The Solana SDK table is pinned to an old `solana-contract` commit and contains superseded program IDs. -- The EVM `auto` selector silently falls back from v1.4 to legacy v1.2. This can change the settlement contract and payment semantics without the caller explicitly accepting the change. -- BOT Chain remains selectable in SDK and MCP payment surfaces despite accepted ADR-0001 prohibiting production settlement there. +- The EVM `auto` selector is required by AIFINP-223 to fall back from unavailable v1.4 to an existing legacy v1.2 deployment. Explicit v1.4 must remain exact and fail when unavailable or quarantined. +- BOT Chain must not be added to the v1.4 registry or new MCP production surface; the existing legacy v1.2 resolver record is retained for backward compatibility. - Robinhood Chain was added upstream, but the current EVM deployment artifact can only represent `usdc` and `usdt`; Robinhood is configured upstream with USDe and USDG. The produced deployment record therefore contains two zero token addresses and cannot prove that either configured Robinhood stablecoin is allowed. - Some upstream records are unsafe to consume as valid deployments without quarantine and independent on-chain verification. @@ -38,7 +38,7 @@ Jira currently requests: - a centralized resolver using deployment records from `evm-contract`; - an SDK refresh from `evm-contract` commit `78240eccf96dd078c9b40be068d635b14876364c`. -The automatic downgrade requirement conflicts with fail-closed payment behavior and must be corrected before implementation approval. +The fallback is a stated acceptance criterion. It must remain centralized, apply only to `auto`, and return the concrete selected version to the caller. Jira: @@ -69,13 +69,13 @@ Deployment source: v1.2`. That is unsafe for payment selection: a missing, disabled, stale, or invalid v1.4 record silently changes the settlement contract and protocol semantics. +AIFINP-223 defines EVM `auto` as `v1.4 -> v1.2`. The fallback is allowed only in `auto`; an explicit version request remains exact. The test oracle for this change must be: 1. `auto` selects an eligible v1.4 deployment. -2. `auto` fails closed with a typed error when an eligible v1.4 deployment is unavailable. -3. `auto` never selects v1.2. -4. Explicit v1.2 may remain only as an intentionally deprecated compatibility path if product/security explicitly approve it; it must never be reached by fallback. +2. `auto` selects a known production v1.2 deployment when v1.4 is absent, disabled or invalid. +3. `auto` fails with a typed error when neither version is usable. +4. Explicit v1.4 never falls back; explicit v1.2 always selects v1.2 when present. 5. The existence of a deployment record does not make a route settlement-enabled. Availability and settlement eligibility must be represented separately. -If Jira continues to require automatic fallback to v1.2, the Requirement Gate remains failed. The ticket acceptance criteria and the security decision must agree before tests are treated as authoritative. +Solana remains v1.4-only because no Solana v1.2 deployment exists. ## Evidence reviewed @@ -53,7 +53,7 @@ If Jira continues to require automatic fallback to v1.2, the Requirement Gate re - `node/src/v14Deployments.generated.ts` pins the old EVM source commit `67b3f518...` and contains only Amoy and Polygon. - `node/src/solanaV14Deployments.generated.ts` pins the old Solana source commit `8a13d10...` and old program IDs. -- `node/src/deploymentResolver.ts` silently falls back to v1.2 in `auto` mode. +- `node/src/deploymentResolver.ts` implements the documented v1.4-to-v1.2 `auto` fallback. - `node/tests/deploymentResolver.test.ts` explicitly requires fallback for Base, Optimism, Unichain, BOT Chain, and XRPL EVM. - `mcp/src/tools/production-control.ts` advertises BOT Chain and does not advertise Robinhood. - No SDK test directly exercises `settlementInvoiceTool()` or `runSettlementInvoice()` against the chain allowlist. @@ -91,7 +91,7 @@ Use the test pyramid for this change: - unit: pure resolver selection, validation, normalization, and schema tests; - integration: generated data against immutable source artifacts and MCP handler/schema behavior; -- regression: previously observed unsafe fallback, stale identifiers, BOT Chain exposure, token confusion, and environment leakage; +- regression: incorrect explicit-version fallback, stale identifiers, BOT Chain v1.4 exposure, token confusion, and environment leakage; - staging E2E: read-only on-chain identity checks followed by one separately authorized funded payment per enabled route. All PR tests must be deterministic and isolated. DNS, public RPC, GitHub availability, and wall-clock timing must not be dependencies of unit tests. @@ -100,7 +100,7 @@ All PR tests must be deterministic and isolated. DNS, public RPC, GitHub availab ### EVM resolver — `node/tests/deploymentResolver.test.ts` -Replace tests that encode fallback with the following behavior tests: +Cover the Jira fallback matrix with the following behavior tests: - dev/Amoy resolves only the dev v1.4 record. - dev rejects Polygon and every production chain with `UnsupportedDevNetworkError`. @@ -108,10 +108,10 @@ Replace tests that encode fallback with the following behavior tests: - explicit v1.4 resolves each eligible production network and returns the exact chain ID and deployment object. - explicit v1.4 on an absent, disabled, or quarantined network throws a typed unavailable/disabled error. - `auto` returns v1.4 on an eligible network. -- `auto` on a network with only a legacy v1.2 record throws; it never returns v1.2. -- no version argument has the same fail-closed behavior as `version: "auto"`. -- explicit v1.2 behavior is tested separately and labelled deprecated if retained. -- BOT Chain fails under explicit v1.4 and `auto` in prod. +- `auto` falls back to a known v1.2 deployment when v1.4 is unavailable or quarantined. +- no version argument has the same behavior as `version: "auto"`. +- explicit v1.2 behavior is tested separately. +- BOT Chain has no v1.4 record; explicit v1.4 fails without downgrade. - Robinhood normalizes and resolves with chain ID 4663 only when eligible. - `isV14Available` distinguishes record presence from settlement eligibility; if a second helper is introduced, test both meanings explicitly. - unknown environment, unknown version, empty network, whitespace-only network, and unknown network return typed errors. @@ -234,8 +234,8 @@ Before a Solana entry becomes eligible: | Regression | Expected result | |---|---| -| Missing v1.4 record while a v1.2 record exists | typed failure; no automatic downgrade | -| BOT Chain requested in production | rejected before network/signing work | +| Missing/disabled v1.4 while a v1.2 record exists | `auto` returns v1.2; explicit v1.4 fails | +| BOT Chain requested as v1.4 | typed unavailable error; no v1.4 deployment | | Robinhood requested | recognized only when registry and backend eligibility agree | | Dev record requested under prod, or prod under dev | typed environment error | | Stale Solana IDs from the previous table | absent and rejected by data assertions | @@ -332,7 +332,7 @@ node scripts/verify-governance-docs.mjs ### Testing Gate -- [ ] Revised no-silent-downgrade acceptance criterion approved. +- [x] AIFINP-223 automatic-fallback acceptance criterion implemented. - [ ] EVM unit tests written and passing. - [ ] Solana unit tests written and passing. - [ ] Generated-data schema/provenance tests written and passing. @@ -343,7 +343,7 @@ node scripts/verify-governance-docs.mjs ### Security/release gate -- [ ] No BOT Chain production settlement exposure. +- [x] No BOT Chain v1.4 deployment exposure. - [ ] Robinhood configuration and backend support agree. - [ ] Polygon token confusion fixed. - [ ] Base invalid/conflicting deployment is quarantined or replaced. @@ -353,11 +353,11 @@ node scripts/verify-governance-docs.mjs - [ ] Human security/code review completed. - [ ] One authorized funded E2E succeeds per newly enabled route. - [ ] Duplicate/replay attempt does not move funds twice. -- [ ] Rollback disables the route without falling back to legacy v1.2. +- [ ] Rollback behavior is verified for both explicit v1.4 and documented `auto` fallback. - [ ] Production monitoring and route kill-switch are confirmed. ## Testing Gate conclusion -**TARGETED GATE PASS; PRODUCTION ACTIVATION BLOCKED.** The resolver and provenance tests now enforce fail-closed routing and the CTO-provided Solana IDs. The remaining blockers are operational/on-chain: backend verification, Safe actions, independent RPC verification, funded E2E and the existing flaky live-path tests. +**TARGETED GATE PASS; PRODUCTION v1.4 ACTIVATION BLOCKED.** The resolver tests now enforce the AIFINP-223 `auto` fallback, exact explicit versions, and the CTO-provided Solana IDs. The remaining v1.4 blockers are operational/on-chain: backend verification, Safe actions, independent RPC verification and funded E2E. HANDOFF: CONDITIONAL PASS | restriction: do not enable production v1.4 settlement | return_to: release owner diff --git a/node/src/deploymentResolver.ts b/node/src/deploymentResolver.ts index 5c2798b..7abdfba 100644 --- a/node/src/deploymentResolver.ts +++ b/node/src/deploymentResolver.ts @@ -11,10 +11,9 @@ * environment — "dev" | "prod". Development supports Amoy only and uses the * v1.4 artifacts sourced from evm-contract's dev branch. Production uses the * configured production networks. - * version — "v1.2" | "v1.4" | "auto". "auto" means the current v1.4 - * deployment for that exact environment+network. It never downgrades to a - * legacy contract. Legacy v1.2 remains available only when requested - * explicitly. + * version — "v1.2" | "v1.4" | "auto". "auto" uses an enabled v1.4 + * deployment for the exact environment+network and otherwise falls back + * to a valid legacy v1.2 deployment. An explicit version never changes. * * Addresses live in data files (v14Deployments.generated.ts and the legacy * SPLITTER_DEPLOYMENTS table), never inline here — this module is selection @@ -38,8 +37,8 @@ export type SdkEnvironment = "dev" | "prod"; * (`SplitterDeployment.version`, which is "1.1" or "1.2" depending on chain). */ export type ProtocolVersion = "v1.2" | "v1.4"; -/** What a caller may ask for. "auto" (the default) resolves only an enabled - * v1.4 deployment and never silently downgrades. */ +/** What a caller may ask for. "auto" (the default) prefers enabled v1.4 and + * falls back to a valid production v1.2 deployment. */ export type RequestedVersion = ProtocolVersion | "auto"; export interface ResolveDeploymentOptions { @@ -129,10 +128,13 @@ export class DeploymentDisabledError extends DeploymentResolverError { } } -/** No current v1.4 deployment exists for this environment+network. */ +/** No usable deployment of either supported version exists here. */ export class NoDeploymentError extends DeploymentResolverError { constructor(environment: SdkEnvironment, network: string) { - super(`No v1.4 deployment is known for ${environment}/${network}.`); + super( + `No enabled v1.4 or valid v1.2 deployment is known for ` + + `${environment}/${network}.`, + ); this.name = "NoDeploymentError"; } } @@ -183,8 +185,9 @@ export function isV14Available( * * @throws UnsupportedDevNetworkError dev asked for a non-Amoy network * @throws VersionUnavailableError an explicit version is not deployed here - * @throws DeploymentDisabledError a v1.4 record exists but is quarantined - * @throws NoDeploymentError no v1.4 deployment is known here + * @throws DeploymentDisabledError explicit v1.4 is quarantined, or auto + * has no valid v1.2 fallback + * @throws NoDeploymentError neither version is known here */ export function resolveDeployment( options: ResolveDeploymentOptions, @@ -244,7 +247,16 @@ export function resolveDeployment( if (v12) return asV12(v12); throw new VersionUnavailableError("v1.2", environment, options.network); case "auto": - return asV14(enabledV14()); + if (v14?.settlementEnabled) return asV14(v14); + if (v12) return asV12(v12); + if (v14) { + throw new DeploymentDisabledError( + environment, + network, + v14.disabledReason ?? "deployment has not passed the settlement gate", + ); + } + throw new NoDeploymentError(environment, options.network); default: throw new DeploymentResolverError( `Unknown version "${String(requested)}"; use "v1.2", "v1.4" or "auto".`, diff --git a/node/tests/deploymentResolver.test.ts b/node/tests/deploymentResolver.test.ts index 7f3656c..d1a9795 100644 --- a/node/tests/deploymentResolver.test.ts +++ b/node/tests/deploymentResolver.test.ts @@ -115,16 +115,12 @@ describe("resolveDeployment — explicit version selection", () => { }); describe("resolveDeployment — automatic version selection", () => { - it("auto refuses a quarantined production deployment", () => { - expect(() => - resolveDeployment({ environment: "prod", network: "polygon" }), - ).toThrow(DeploymentDisabledError); - }); - - it("auto never falls back to v1.2", () => { - expect(() => - resolveDeployment({ environment: "prod", network: "botchain" }), - ).toThrow(NoDeploymentError); + it("auto falls back to v1.2 when v1.4 is quarantined", () => { + const r = resolveDeployment({ environment: "prod", network: "polygon" }); + expect(r.version).toBe("v1.2"); + if (r.version === "v1.2") { + expect(r.deployment.splitter).toBe(SPLITTER_DEPLOYMENTS.polygon.splitter); + } }); it("auto uses v1.4 on the dev network (amoy)", () => { @@ -132,10 +128,15 @@ describe("resolveDeployment — automatic version selection", () => { expect(r.version).toBe("v1.4"); }); - it("auto is the fail-closed default when no version is given", () => { - expect(() => - resolveDeployment({ environment: "prod", network: "base" }), - ).toThrow(DeploymentDisabledError); + it("auto is the default when no version is given", () => { + const withAuto = resolveDeployment({ + environment: "prod", + network: "base", + version: "auto", + }); + const noVersion = resolveDeployment({ environment: "prod", network: "base" }); + expect(noVersion.version).toBe("v1.2"); + expect(noVersion.version).toBe(withAuto.version); }); it("auto throws when neither version exists for the network", () => { @@ -144,15 +145,23 @@ describe("resolveDeployment — automatic version selection", () => { ).toThrow(NoDeploymentError); }); - it("all imported production v1.4 deployments remain quarantined", () => { + it("falls back on every production network with a valid legacy deployment", () => { for (const network of [ "polygon", "base", "optimism", "unichain", + "botchain", "xrplevm", - "robinhood", ]) { + expect(resolveDeployment({ environment: "prod", network }).version).toBe( + "v1.2", + ); + } + }); + + it("fails when quarantined v1.4 has no valid v1.2 fallback", () => { + for (const network of ["arbitrum", "avalanche", "bnb", "robinhood"]) { expect(() => resolveDeployment({ environment: "prod", network })).toThrow( DeploymentDisabledError, );