From 571f530a035e668c912d7242475c78a96922ee20 Mon Sep 17 00:00:00 2001 From: Syed Hassan <304852340+syedhassan-aifinpay@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:23:15 +0500 Subject: [PATCH 1/5] feat(sdk): select v1.3 splitters by chain AND route, with no fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From v1.3 a chain carries one splitter per protocol route, because the fee split is immutable at construction and the two protocols need different economics: merchant-aifp1 is 100/0 and agent-x402 is 0/0. The existing SPLITTER_DEPLOYMENTS map is keyed by chain alone and its version union is "1.1" | "1.2", so it can express neither. Adds SPLITTER_ROUTES, keyed ":", covering all eighteen v1.3 deployments across the nine chains, with resolveSplitterRoute() that throws on an unknown pair instead of falling back. The no-fallback rule is not a style preference. The splitters were deployed with CREATE, so an address derives from deployer and nonce and the same address recurs on other chains for the other route: 0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55 is OP's merchant-aifp1 and also Base's agent-x402. 0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74 is Unichain's merchant-aifp1, Avalanche's agent-x402, and the legacy v1.2 splitter on Optimism. Every route therefore resolves to a real, deployed, working contract. A fallback would not fail — it would settle at the wrong fee split, and the amounts would look plausible in every log. A test asserts that this reuse exists and that a shared address still resolves to different economics per chain. resolveSettlingSplitterRoute() is deliberately separate: reading the registry and being cleared to move money are different questions. Settlement is disabled on all eighteen and each route is enabled individually after a paid mainnet end-to-end with verified balance deltas, and the policy window is enforced so an unreviewed route stops settling rather than drifting on. Values are generated from the canonical registry in evm-contract (registry/generated/splitter-table.json, schemaVersion 2), where treasury, both bps values and the runtime code hash were read from chain rather than transcribed. A test pins the consequence: exactly two distinct runtime code hashes across eighteen contracts, one per route, which is what proves the right immutable profile reached every chain. Also moves the botchain and xrplevm chain definitions into src/chains.ts so unifiedAgent and splitterRoutes cannot drift apart on a chain id. SPLITTER_DEPLOYMENTS is left untouched; the v1.1/v1.2 entries it serves are marked superseded with settlement disabled in the registry. Build clean; 170 tests passing across 18 files, 15 of them new. --- node/src/chains.ts | 25 ++ node/src/index.ts | 15 + node/src/splitterRoutes.ts | 438 ++++++++++++++++++++++++++++++ node/src/unifiedAgent.ts | 27 +- node/tests/splitterRoutes.test.ts | 142 ++++++++++ 5 files changed, 624 insertions(+), 23 deletions(-) create mode 100644 node/src/chains.ts create mode 100644 node/src/splitterRoutes.ts create mode 100644 node/tests/splitterRoutes.test.ts diff --git a/node/src/chains.ts b/node/src/chains.ts new file mode 100644 index 0000000..870f563 --- /dev/null +++ b/node/src/chains.ts @@ -0,0 +1,25 @@ +// BOT Chain (677) and XRPL EVM (1440000) 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. +import { defineChain, type Chain } from "viem"; + +export const botchain: Chain = defineChain({ + id: 677, + name: "BOT Chain", + nativeCurrency: { name: "BOT", symbol: "BOT", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.botchain.ai"] } }, + blockExplorers: { + default: { name: "BOT Chain Explorer", url: "https://scan.botchain.ai" }, + }, +}); + +export const xrplevm: Chain = defineChain({ + id: 1440000, + name: "XRPL EVM", + nativeCurrency: { name: "XRP", symbol: "XRP", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.xrplevm.org"] } }, + blockExplorers: { + default: { name: "XRPL EVM Explorer", url: "https://explorer.xrplevm.org" }, + }, +}); diff --git a/node/src/index.ts b/node/src/index.ts index 2b5a0a7..98d2fcb 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -46,6 +46,21 @@ export type { } from "./settlement.js"; // ── Unified surface (Phase 1+ / legacy callers remain source-compatible) ── +export { + SPLITTER_ROUTES, + resolveSplitterRoute, + resolveSettlingSplitterRoute, + UnknownSplitterRouteError, + SplitterRouteNotSettlingError, +} from "./splitterRoutes.js"; +export type { + SplitterRoute, + SplitterRouteChain, + SplitterRouteKey, + SplitterRouteDeployment, +} from "./splitterRoutes.js"; +export { botchain, xrplevm } from "./chains.js"; + export { AiFinPayAgent, SPLITTER_DEPLOYMENTS, paymentIdFor } from "./unifiedAgent.js"; export type { AiFinPayAgentOptions, diff --git a/node/src/splitterRoutes.ts b/node/src/splitterRoutes.ts new file mode 100644 index 0000000..b281ffe --- /dev/null +++ b/node/src/splitterRoutes.ts @@ -0,0 +1,438 @@ +/** + * v1.3 splitter selection, keyed by chain AND protocol route. + * + * From v1.3 a chain carries one splitter per protocol route, because the fee + * split is immutable at construction and the two protocols need different + * economics: + * + * merchant-aifp1 (100/0) — the agent pays the quoted gross amount, the + * AiFinPay treasury receives 1%, the merchant receives 99%. + * agent-x402 (0/0) — the provider receives 100% of the provider-defined + * price and the AiFinPay fee is 0% for now. This route is NOT fee-on-top; + * fee-on-top semantics arrive in a future contract version. + * + * Selection must use both chain and route, and must never fall back from one + * route to the other. That is not a style preference. The splitters were + * deployed with CREATE, so an address derives from deployer and nonce and the + * same address recurs on other chains for the other route: + * + * 0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55 + * is OP's merchant-aifp1 AND Base's agent-x402 + * 0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74 + * is Unichain's merchant-aifp1, Avalanche's agent-x402, AND the legacy + * v1.2 splitter on Optimism + * + * An address on its own therefore says nothing about which economics apply. + * Resolving by chain alone would settle at the wrong fee split, silently, + * and the amounts would still look plausible in every log. + * + * Generated from the canonical registry in AiFinPay/evm-contract + * (registry/generated/splitter-table.json, schemaVersion 2). Every field below + * was read from the chain by verify-registry.mjs — treasury, both bps values + * and the runtime code hash — not transcribed by hand. + */ +import { polygon, base, optimism, unichain, bsc, arbitrum, avalanche, type Chain } from "viem/chains"; +import { botchain, xrplevm } from "./chains.js"; + +/** Protocol routes. A route is a fee profile fixed at construction. */ +export type SplitterRoute = "merchant-aifp1" | "agent-x402"; + +/** Chains carrying v1.3 route splitters. */ +export type SplitterRouteChain = + | "polygon" + | "optimism" + | "bnb" + | "unichain" + | "botchain" + | "base" + | "arbitrum" + | "avalanche" + | "xrplevm"; + +/** Key into SPLITTER_ROUTES. Both halves are required. */ +export type SplitterRouteKey = `${SplitterRouteChain}:${SplitterRoute}`; + +export interface SplitterRouteDeployment { + chain: SplitterRouteChain; + route: SplitterRoute; + chainId: number; + viemChain: Chain; + splitter: `0x${string}`; + /** Owner and treasury are the same governance Safe on every chain. */ + treasury: `0x${string}`; + /** Immutable, baked into runtime code. 100 = 1%. */ + treasuryBps: number; + ipCreatorBps: number; + /** keccak-256 of the deployed runtime bytecode, read from chain. */ + runtimeCodeHash: `0x${string}`; + /** + * Deployed and verified is not the same as payable. A route is enabled + * individually, and only after a successful mainnet paid end-to-end + * settlement on that exact route with verified balance deltas. + */ + settlementEnabled: boolean; + /** Policy review window. Outside it, a route must not settle. */ + validFrom: string; + validUntil: string; + defaultRpc: string; + explorer: string; +} + +export const SPLITTER_ROUTES: Record = { + "arbitrum:agent-x402": { + chain: "arbitrum", + route: "agent-x402", + chainId: 42161, + viemChain: arbitrum, + splitter: "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://arb1.arbitrum.io/rpc", + explorer: "https://arbiscan.io", + }, + "arbitrum:merchant-aifp1": { + chain: "arbitrum", + route: "merchant-aifp1", + chainId: 42161, + viemChain: arbitrum, + splitter: "0x80e2B445DFc44B3B2254aa376B31AEdDd3Ff934a", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://arb1.arbitrum.io/rpc", + explorer: "https://arbiscan.io", + }, + "avalanche:agent-x402": { + chain: "avalanche", + route: "agent-x402", + chainId: 43114, + viemChain: avalanche, + splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://api.avax.network/ext/bc/C/rpc", + explorer: "https://snowtrace.io", + }, + "avalanche:merchant-aifp1": { + chain: "avalanche", + route: "merchant-aifp1", + chainId: 43114, + viemChain: avalanche, + splitter: "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://api.avax.network/ext/bc/C/rpc", + explorer: "https://snowtrace.io", + }, + "base:agent-x402": { + chain: "base", + route: "agent-x402", + chainId: 8453, + viemChain: base, + splitter: "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.base.org", + explorer: "https://basescan.org", + }, + "base:merchant-aifp1": { + chain: "base", + route: "merchant-aifp1", + chainId: 8453, + viemChain: base, + splitter: "0xB385Cc32fe39CF5B5778DF0Df0e8E9978b5F662a", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.base.org", + explorer: "https://basescan.org", + }, + "bnb:agent-x402": { + chain: "bnb", + route: "agent-x402", + chainId: 56, + viemChain: bsc, + splitter: "0x7656fb8B6627311A7d87273913D31b837Bb2b5A4", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://bsc-dataseed.bnbchain.org", + explorer: "https://bscscan.com", + }, + "bnb:merchant-aifp1": { + chain: "bnb", + route: "merchant-aifp1", + chainId: 56, + viemChain: bsc, + splitter: "0x79D481B835Cb050FAb7a045E619A6Fb9Cd73f510", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://bsc-dataseed.bnbchain.org", + explorer: "https://bscscan.com", + }, + "botchain:agent-x402": { + chain: "botchain", + route: "agent-x402", + chainId: 677, + viemChain: botchain, + splitter: "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.botchain.ai", + explorer: "https://scan.botchain.ai", + }, + "botchain:merchant-aifp1": { + chain: "botchain", + route: "merchant-aifp1", + chainId: 677, + viemChain: botchain, + splitter: "0xe855e491D0950140704DB9Cec6B7b3F725360a56", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.botchain.ai", + explorer: "https://scan.botchain.ai", + }, + "optimism:agent-x402": { + chain: "optimism", + route: "agent-x402", + chainId: 10, + viemChain: optimism, + splitter: "0x38Ef6173ce0AC540f129680C2Aa4Ef739787bdBf", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.optimism.io", + explorer: "https://optimistic.etherscan.io", + }, + "optimism:merchant-aifp1": { + chain: "optimism", + route: "merchant-aifp1", + chainId: 10, + viemChain: optimism, + splitter: "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.optimism.io", + explorer: "https://optimistic.etherscan.io", + }, + "polygon:agent-x402": { + chain: "polygon", + route: "agent-x402", + chainId: 137, + viemChain: polygon, + splitter: "0x660Cd915Fc54A7EaE5CEA6854505638bd2A08531", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://polygon-bor-rpc.publicnode.com", + explorer: "https://polygonscan.com", + }, + "polygon:merchant-aifp1": { + chain: "polygon", + route: "merchant-aifp1", + chainId: 137, + viemChain: polygon, + splitter: "0x27C1C07563c92C1AEa52cC9b4452dF49dC5a7942", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://polygon-bor-rpc.publicnode.com", + explorer: "https://polygonscan.com", + }, + "unichain:agent-x402": { + chain: "unichain", + route: "agent-x402", + chainId: 130, + viemChain: unichain, + splitter: "0xC701F45b3Bae9CA3a58cB33fCBA6291594D17843", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.unichain.org", + explorer: "https://uniscan.xyz", + }, + "unichain:merchant-aifp1": { + chain: "unichain", + route: "merchant-aifp1", + chainId: 130, + viemChain: unichain, + splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.unichain.org", + explorer: "https://uniscan.xyz", + }, + "xrplevm:agent-x402": { + chain: "xrplevm", + route: "agent-x402", + chainId: 1440000, + viemChain: xrplevm, + splitter: "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.xrplevm.org", + explorer: "https://explorer.xrplevm.org", + }, + "xrplevm:merchant-aifp1": { + chain: "xrplevm", + route: "merchant-aifp1", + chainId: 1440000, + viemChain: xrplevm, + splitter: "0xe855e491D0950140704DB9Cec6B7b3F725360a56", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.xrplevm.org", + explorer: "https://explorer.xrplevm.org", + },}; + +export class UnknownSplitterRouteError extends Error { + constructor(chain: string, route: string) { + super( + `No v1.3 splitter registered for ${chain}:${route}. Supported: ` + + `${Object.keys(SPLITTER_ROUTES).join(", ")}. There is deliberately no ` + + `fallback between routes — merchant-aifp1 and agent-x402 have different ` + + `immutable fee splits, so substituting one for the other would settle ` + + `at the wrong amount.`, + ); + this.name = "UnknownSplitterRouteError"; + } +} + +export class SplitterRouteNotSettlingError extends Error { + constructor(key: string, reason: string) { + super(`Splitter route ${key} must not settle: ${reason}`); + this.name = "SplitterRouteNotSettlingError"; + } +} + +/** + * Resolve a splitter by chain AND route. Throws on an unknown pair rather + * than falling back, because the fallback is the bug: every route resolves to + * a real, deployed, working contract with the wrong economics. + */ +export function resolveSplitterRoute( + chain: SplitterRouteChain | string, + route: SplitterRoute | string, +): SplitterRouteDeployment { + const entry = (SPLITTER_ROUTES as Partial>)[ + `${chain}:${route}` + ]; + if (!entry) throw new UnknownSplitterRouteError(String(chain), String(route)); + return entry; +} + +/** + * Resolve a route that is cleared to move money. Separate from + * resolveSplitterRoute on purpose: reading the registry and being allowed to + * settle are different questions, and conflating them is how a disabled route + * ends up paying. + */ +export function resolveSettlingSplitterRoute( + chain: SplitterRouteChain | string, + route: SplitterRoute | string, + now: Date = new Date(), +): SplitterRouteDeployment { + const entry = resolveSplitterRoute(chain, route); + const key = `${entry.chain}:${entry.route}`; + if (!entry.settlementEnabled) { + throw new SplitterRouteNotSettlingError( + key, + "settlement is not enabled for this route yet — it is enabled only after a " + + "successful mainnet paid end-to-end settlement with verified balance deltas", + ); + } + const t = now.getTime(); + if (t < Date.parse(entry.validFrom)) { + throw new SplitterRouteNotSettlingError(key, `its policy window opens ${entry.validFrom}`); + } + if (t >= Date.parse(entry.validUntil)) { + throw new SplitterRouteNotSettlingError( + key, + `its policy window expired ${entry.validUntil} and has not been re-reviewed`, + ); + } + return entry; +} diff --git a/node/src/unifiedAgent.ts b/node/src/unifiedAgent.ts index ee1eb5d..ef7a705 100644 --- a/node/src/unifiedAgent.ts +++ b/node/src/unifiedAgent.ts @@ -28,8 +28,8 @@ import { privateKeyToAccount, type PrivateKeyAccount, } from "viem/accounts"; -import { defineChain } from "viem"; import { polygon, base, arbitrum, optimism, bsc, mainnet, unichain, type Chain } from "viem/chains"; +import { botchain, xrplevm } from "./chains.js"; import { Connection, Keypair, @@ -319,28 +319,9 @@ export function paymentIdFor(orderId: string): `0x${string}` { // on-chain (eth_getCode returned bytecode for every address below, // 2026-07-15). Do NOT add chains here without re-running that check. // -// BOT Chain (677) and XRPL EVM (1440000) are not shipped with viem/chains, -// so we defineChain() them locally. - -const botchain: Chain = defineChain({ - id: 677, - name: "BOT Chain", - nativeCurrency: { name: "BOT", symbol: "BOT", decimals: 18 }, - rpcUrls: { default: { http: ["https://rpc.botchain.ai"] } }, - blockExplorers: { - default: { name: "BOT Chain Explorer", url: "https://scan.botchain.ai" }, - }, -}); - -const xrplevm: Chain = defineChain({ - id: 1440000, - name: "XRPL EVM", - nativeCurrency: { name: "XRP", symbol: "XRP", decimals: 18 }, - rpcUrls: { default: { http: ["https://rpc.xrplevm.org"] } }, - blockExplorers: { - default: { name: "XRPL EVM Explorer", url: "https://explorer.xrplevm.org" }, - }, -}); +// BOT Chain (677) and XRPL EVM (1440000) are not shipped with viem/chains. +// They live in ./chains.js so this module and splitterRoutes.ts cannot drift +// apart on a chain id or RPC. /** EVM chains with a live, on-chain-verified B2BSplitter deployment. */ export type SplitterChainName = diff --git a/node/tests/splitterRoutes.test.ts b/node/tests/splitterRoutes.test.ts new file mode 100644 index 0000000..331cff8 --- /dev/null +++ b/node/tests/splitterRoutes.test.ts @@ -0,0 +1,142 @@ +// v1.3 route selection. Static-shape tests only, no network calls — every +// value here was read from chain by verify-registry.mjs in evm-contract when +// the registry was authored (2026-08-27). +import { describe, expect, it } from "vitest"; +import { getAddress } from "viem"; +import { + SPLITTER_ROUTES, + resolveSplitterRoute, + resolveSettlingSplitterRoute, + UnknownSplitterRouteError, + SplitterRouteNotSettlingError, + type SplitterRouteDeployment, +} from "../src/index.js"; + +const CHAIN_IDS: Record = { + polygon: 137, optimism: 10, bnb: 56, unichain: 130, botchain: 677, + base: 8453, arbitrum: 42161, avalanche: 43114, xrplevm: 1440000, +}; +const BPS: Record = { "merchant-aifp1": 100, "agent-x402": 0 }; +const entries = Object.entries(SPLITTER_ROUTES); + +describe("SPLITTER_ROUTES", () => { + it("has both routes on all nine chains", () => { + expect(entries).toHaveLength(18); + for (const chain of Object.keys(CHAIN_IDS)) { + for (const route of Object.keys(BPS)) { + expect(SPLITTER_ROUTES[`${chain}:${route}` as keyof typeof SPLITTER_ROUTES], `${chain}:${route}`).toBeDefined(); + } + } + }); + + it("every key agrees with the chain and route inside it", () => { + for (const [key, d] of entries) expect(`${d.chain}:${d.route}`, key).toBe(key); + }); + + it("chainId and viem chain agree", () => { + for (const [key, d] of entries) { + expect(d.chainId, key).toBe(CHAIN_IDS[d.chain]); + expect(d.viemChain.id, key).toBe(d.chainId); + } + }); + + it("fee split matches the route, and no route carries a creator leg", () => { + for (const [key, d] of entries) { + expect(d.treasuryBps, key).toBe(BPS[d.route]); + expect(d.ipCreatorBps, key).toBe(0); + } + }); + + it("addresses are checksummed and the treasury is the same Safe everywhere", () => { + const treasuries = new Set(entries.map(([, d]) => d.treasury)); + expect(treasuries.size).toBe(1); + for (const [key, d] of entries) { + expect(getAddress(d.splitter), key).toBe(d.splitter); + expect(getAddress(d.treasury), key).toBe(d.treasury); + } + }); + + it("there are exactly two runtime code hashes, one per route", () => { + // The bps are immutable and baked into runtime code, so a route with the + // wrong profile would hash differently. Two hashes across eighteen + // contracts is the evidence that the right profile reached every chain. + const byRoute = new Map>(); + for (const [, d] of entries) { + if (!byRoute.has(d.route)) byRoute.set(d.route, new Set()); + byRoute.get(d.route)!.add(d.runtimeCodeHash); + } + expect([...byRoute.keys()].sort()).toEqual(["agent-x402", "merchant-aifp1"]); + for (const [route, hashes] of byRoute) expect(hashes.size, route).toBe(1); + expect(new Set(entries.map(([, d]) => d.runtimeCodeHash)).size).toBe(2); + }); + + it("ships with settlement disabled on every route", () => { + // Deployed and verified is not payable. Flipping these on is a deliberate + // per-route act after a paid mainnet E2E, never a side effect of a release. + for (const [key, d] of entries) expect(d.settlementEnabled, key).toBe(false); + }); +}); + +describe("address reuse across chains", () => { + // The splitters were deployed with CREATE, so the same address recurs on + // other chains for the OTHER route. This is the reason selection is keyed on + // chain AND route, and the reason an address must never be used as a key. + it("the same address really does appear under more than one route", () => { + const byAddress = new Map(); + for (const [key, d] of entries) { + const k = d.splitter.toLowerCase(); + byAddress.set(k, [...(byAddress.get(k) ?? []), key]); + } + const shared = [...byAddress.values()].filter((keys) => keys.length > 1); + expect(shared.length, "expected CREATE address reuse across chains").toBeGreaterThan(0); + // and every reuse spans different chains, never the same chain twice + for (const keys of shared) { + const chains = keys.map((k) => k.split(":")[0]); + expect(new Set(chains).size, keys.join(" / ")).toBe(chains.length); + } + }); + + it("a shared address still resolves to the right economics per chain", () => { + const op = resolveSplitterRoute("optimism", "merchant-aifp1"); + const base = resolveSplitterRoute("base", "agent-x402"); + expect(op.splitter).toBe(base.splitter); // same address, different chains + expect(op.treasuryBps).toBe(100); + expect(base.treasuryBps).toBe(0); + }); +}); + +describe("resolveSplitterRoute", () => { + it("returns the entry for a known pair", () => { + const d: SplitterRouteDeployment = resolveSplitterRoute("polygon", "merchant-aifp1"); + expect(d.chainId).toBe(137); + expect(d.treasuryBps).toBe(100); + }); + + it("throws rather than falling back to the other route", () => { + expect(() => resolveSplitterRoute("polygon", "not-a-route")).toThrow(UnknownSplitterRouteError); + expect(() => resolveSplitterRoute("ethereum", "agent-x402")).toThrow(UnknownSplitterRouteError); + }); + + it("refuses a chain-only lookup", () => { + expect(() => resolveSplitterRoute("polygon", "")).toThrow(UnknownSplitterRouteError); + }); +}); + +describe("resolveSettlingSplitterRoute", () => { + it("refuses a route that is not enabled for settlement", () => { + expect(() => resolveSettlingSplitterRoute("polygon", "merchant-aifp1")) + .toThrow(SplitterRouteNotSettlingError); + }); + + it("still refuses an unknown pair", () => { + expect(() => resolveSettlingSplitterRoute("polygon", "nope")).toThrow(UnknownSplitterRouteError); + }); + + it("refuses once the policy window has expired", () => { + const d = SPLITTER_ROUTES["polygon:merchant-aifp1"]; + const after = new Date(Date.parse(d.validUntil) + 86_400_000); + // enabled or not, an expired window must never settle + expect(() => resolveSettlingSplitterRoute("polygon", "merchant-aifp1", after)) + .toThrow(SplitterRouteNotSettlingError); + }); +}); From ecc2dc33d520b8244288424329c44b3347a44231 Mon Sep 17 00:00:00 2001 From: Syed Hassan <304852340+syedhassan-aifinpay@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:35:39 +0500 Subject: [PATCH 2/5] =?UTF-8?q?chore(agent):=202.0.0-rc.3=20=E2=80=94=20ne?= =?UTF-8?q?w=20route-selection=20exports=20are=20published=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version gate is right to fail: splitterRoutes.ts, chains.ts and the new index exports all ship in the package, so consumers can pin them. Additive only — SPLITTER_DEPLOYMENTS and every existing export are unchanged, so this is a prerelease bump rather than a breaking one. --- node/package.json | 69 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/node/package.json b/node/package.json index 43e66a3..f3e225e 100644 --- a/node/package.json +++ b/node/package.json @@ -1,6 +1,6 @@ { "name": "@aifinpay/agent", - "version": "2.0.0-rc.2", + "version": "2.0.0-rc.3", "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", @@ -15,23 +15,60 @@ "default": "./dist/wallet.js" } }, - "files": ["dist", "README.md", "LICENSE"], + "files": [ + "dist", + "README.md", + "LICENSE" + ], "scripts": { "build": "tsc -p tsconfig.json", "test": "vitest run", "prepublishOnly": "npm run build" }, "keywords": [ - "aifinpay", "ai-agent", "ai-agents", "ai-payments", "ai-agent-payments", - "agentic-ai", "autonomous-agents", "autonomous-payments", "agent-payments", - "agent-payment-protocol", "agent-commerce", "agentic-commerce", "agent-economy", - "ai-commerce", "ai-payment-infrastructure", "ai-financial-infrastructure", - "payment-rails", "financial-rails", "x402", "x402-payments", "http-402", - "mcp", "mcp-payments", "mcp-monetization", "model-context-protocol", - "payments", "api-payments", "api-monetization", "paid-apis", "machine-payments", - "m2m-payments", "programmable-payments", "crypto-payments", "stablecoin-payments", - "stablecoin", "usdc", "agent-wallet", "agent-passport", "non-custodial", - "fintech", "polygon", "solana", "ed25519" + "aifinpay", + "ai-agent", + "ai-agents", + "ai-payments", + "ai-agent-payments", + "agentic-ai", + "autonomous-agents", + "autonomous-payments", + "agent-payments", + "agent-payment-protocol", + "agent-commerce", + "agentic-commerce", + "agent-economy", + "ai-commerce", + "ai-payment-infrastructure", + "ai-financial-infrastructure", + "payment-rails", + "financial-rails", + "x402", + "x402-payments", + "http-402", + "mcp", + "mcp-payments", + "mcp-monetization", + "model-context-protocol", + "payments", + "api-payments", + "api-monetization", + "paid-apis", + "machine-payments", + "m2m-payments", + "programmable-payments", + "crypto-payments", + "stablecoin-payments", + "stablecoin", + "usdc", + "agent-wallet", + "agent-passport", + "non-custodial", + "fintech", + "polygon", + "solana", + "ed25519" ], "author": "CoinSecurities (SECCO) ", "license": "MIT", @@ -55,6 +92,10 @@ "typescript": "^5.4.0", "vitest": "^4.1.5" }, - "engines": { "node": ">=18" }, - "publishConfig": { "access": "public" } + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + } } From 512aedde14e677763e8ad50ab52554132f618eb8 Mon Sep 17 00:00:00 2001 From: Syed Hassan Date: Fri, 28 Aug 2026 00:41:28 +0500 Subject: [PATCH 3/5] feat(sdk): generate SPLITTER_ROUTES from the canonical registry, fail closed on time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two repositories were hand-maintaining the same payment-critical table: addresses, code hashes, fee splits, policy dates and settlement flags lived in evm-contract's registry AND were typed out again in splitterRoutes.ts. Those disagree eventually, and the failure is silent — the amounts still look plausible in every log. They are now one table. registry/splitter-table.json is a byte-for-byte copy of the canonical artifact, registry/source.json records the evm-contract commit and its sha256, and src/splitterRoutes.generated.ts is produced from it: npm run registry:sync -- --from ../evm-contract npm run registry:check (CI gate) `registry:check` regenerates and compares byte-for-byte, and re-hashes the vendored artifact against its recorded provenance, so hand-editing either one turns CI red. Offline and deterministic: this gate must not need a network read of another repository to know whether it is in sync. Rejected, as it should: a hand-edited payout address, a hand-edited fee split, a hand-edited settlement flag in the artifact. Only the 18 current v1.3 routes are generated. The superseded v1.1/v1.2 entries stay in the canonical registry as deployment evidence but are deliberately not representable here — a resolver that cannot name a legacy splitter cannot silently fall back to one. owner is carried through and asserted against the governance Safe, in the generator and again in the tests. Not generated, deliberately: viemChain, defaultRpc and explorer. A wrong RPC fails loudly and pays nobody; a wrong splitter address pays the wrong party successfully. A chain with no transport entry is an error, not a default. resolveSettlingSplitterRoute failed OPEN on the input it could least trust. Date.parse("nonsense") is NaN, NaN fails every comparison, so with `t < from` / `t >= until` both gates were false and a route with a malformed policy window settled with no time check at all. Every comparison is now written as "prove it is inside the window", and an unparseable window, an inverted window or an invalid `now` are each rejected explicitly. The old expiry test could not have caught this: every shipped route has settlementEnabled false, so it rejected on the flag before reaching validUntil — it proved the settlement flag worked, twice. The window is now tested against a synthetic ENABLED route, asserting the reason and not just the throw: before validFrom, exactly at validFrom, mid-window, one ms before validUntil, exactly at validUntil, after it, and malformed/inverted/invalid-now failing closed. Verified against the pre-fix implementation: the five fail-closed tests fail, the boundary tests still pass. 185 tests, 18 files. --- .github/workflows/ci.yml | 12 + node/package.json | 2 + node/registry/source.json | 14 + node/registry/splitter-table.json | 430 ++++++++++++++++++++++ node/scripts/generate-splitter-routes.mjs | 261 +++++++++++++ node/src/index.ts | 2 + node/src/splitterRoutes.generated.ts | 366 ++++++++++++++++++ node/src/splitterRoutes.ts | 356 +++--------------- node/tests/splitterRoutes.test.ts | 134 ++++++- 9 files changed, 1271 insertions(+), 306 deletions(-) create mode 100644 node/registry/source.json create mode 100644 node/registry/splitter-table.json create mode 100644 node/scripts/generate-splitter-routes.mjs create mode 100644 node/src/splitterRoutes.generated.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a035aa7..4c2a7b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,18 @@ jobs: - name: Install dependencies run: npm ci --no-audit --no-fund + # The route table is generated from a byte-for-byte copy of the canonical + # registry artifact in AiFinPay/evm-contract, where every payment-critical + # field was read from the chain. This regenerates it and compares, so a + # hand-edited payout address, fee split or settlement flag turns CI red + # instead of shipping. It also re-hashes the vendored artifact against its + # recorded provenance, so editing the copy fails the same way. + # + # Offline and deterministic on purpose: this job must not depend on a + # network read of another repository to know whether it is in sync. + - name: Registry — SPLITTER_ROUTES matches the canonical registry + run: npm run registry:check + - name: Build (tsc) run: npm run build diff --git a/node/package.json b/node/package.json index f3e225e..bad0e3a 100644 --- a/node/package.json +++ b/node/package.json @@ -22,6 +22,8 @@ ], "scripts": { "build": "tsc -p tsconfig.json", + "registry:sync": "node scripts/generate-splitter-routes.mjs", + "registry:check": "node scripts/generate-splitter-routes.mjs --check", "test": "vitest run", "prepublishOnly": "npm run build" }, diff --git a/node/registry/source.json b/node/registry/source.json new file mode 100644 index 0000000..57ae4b5 --- /dev/null +++ b/node/registry/source.json @@ -0,0 +1,14 @@ +{ + "$comment": [ + "Provenance for registry/splitter-table.json, which is a byte-for-byte copy of", + "the canonical artifact in AiFinPay/evm-contract. Nothing here is", + "hand-maintained: 'npm run registry:sync -- --from '", + "rewrites both files together, and 'npm run registry:check' fails if the copy", + "no longer hashes to what is recorded here, or if src/splitterRoutes.generated.ts", + "no longer regenerates from it byte-for-byte." + ], + "repo": "AiFinPay/evm-contract", + "path": "registry/generated/splitter-table.json", + "commit": "9af67e0fb3e502a50c1ef4263a7704de3e8bb45b", + "sha256": "b745426f84a39aa0d6b2a9b93486fff3901db48c7e84c5e7b1ee48c993bd5de3" +} diff --git a/node/registry/splitter-table.json b/node/registry/splitter-table.json new file mode 100644 index 0000000..9eba458 --- /dev/null +++ b/node/registry/splitter-table.json @@ -0,0 +1,430 @@ +{ + "$generated": [ + "DO NOT EDIT. Generated from registry/registry.json by", + "scripts/generate-sdk-table.mjs. CI regenerates this and fails on any", + "difference, so hand-edits are rejected rather than shipped." + ], + "schemaVersion": 3, + "sourceUpdatedAt": "2026-08-28", + "governance": { + "safe": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "threshold": 3, + "owners": [ + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "0x849930eB20ED0a697c71BcE565f18702D202C0F8" + ] + }, + "routes": { + "polygon:legacy": { + "chain": "polygon", + "route": "legacy", + "chainId": 137, + "version": "1.2", + "superseded": true, + "splitter": "0xbD1fa5453f212F096c0213788a645eC597FB4DDe", + "runtimeCodeHash": "0x9001fbb7ec70097909415325dc70c5b2102c4312dcd8e01e7495cfcaca2edaff", + "owner": "0xD31d82c4b35DABaA2ad7023C89A78A052D1f3c8e", + "treasury": "0xD31d82c4b35DABaA2ad7023C89A78A052D1f3c8e", + "treasuryBps": 100, + "ipCreatorBps": 1, + "validFrom": "2026-08-04T00:00:00.000Z", + "validUntil": "2026-09-03T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-07" + }, + "base:legacy": { + "chain": "base", + "route": "legacy", + "chainId": 8453, + "version": "1.1", + "superseded": true, + "splitter": "0x8Ad9830D16b1f10333866a3f38C949CbB19f4BAD", + "runtimeCodeHash": "0x545b3a4ba195edc6b728df8cc64f28da528c9e7805c15f1aa61ef58c3c562197", + "owner": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasuryBps": 100, + "ipCreatorBps": 1, + "validFrom": "2026-08-04T00:00:00.000Z", + "validUntil": "2026-09-03T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-07" + }, + "optimism:legacy": { + "chain": "optimism", + "route": "legacy", + "chainId": 10, + "version": "1.2", + "superseded": true, + "splitter": "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", + "runtimeCodeHash": "0xcdf939fd4f9a189e3dba991c5d538bd77b3d493d2ce4e356b61e5742dbde1899", + "owner": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasuryBps": 100, + "ipCreatorBps": 1, + "validFrom": "2026-08-04T00:00:00.000Z", + "validUntil": "2026-09-03T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-07" + }, + "unichain:legacy": { + "chain": "unichain", + "route": "legacy", + "chainId": 130, + "version": "1.1", + "superseded": true, + "splitter": "0xeE92807decAa3A02F1e165dd7Efcd92ab9aA83CB", + "runtimeCodeHash": "0x545b3a4ba195edc6b728df8cc64f28da528c9e7805c15f1aa61ef58c3c562197", + "owner": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasuryBps": 100, + "ipCreatorBps": 1, + "validFrom": "2026-08-04T00:00:00.000Z", + "validUntil": "2026-09-03T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-07" + }, + "botchain:legacy": { + "chain": "botchain", + "route": "legacy", + "chainId": 677, + "version": "1.2", + "superseded": true, + "splitter": "0x147d8fF8c027E24303b5B99CbC8843e1D3dF94cC", + "runtimeCodeHash": "0xabd084ff64e98bb8ac7db7783d80c6a6bcc69716dfb8f64a4686bed5cf428d96", + "owner": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasuryBps": 100, + "ipCreatorBps": 1, + "validFrom": "2026-08-04T00:00:00.000Z", + "validUntil": "2026-09-03T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-07" + }, + "xrplevm:legacy": { + "chain": "xrplevm", + "route": "legacy", + "chainId": 1440000, + "version": "1.2", + "superseded": true, + "splitter": "0x147d8fF8c027E24303b5B99CbC8843e1D3dF94cC", + "runtimeCodeHash": "0xeb68cf314d335f888726a527dec10d989c26e2c5d6a8df68d117cc7d4dcec239", + "owner": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "treasuryBps": 100, + "ipCreatorBps": 1, + "validFrom": "2026-08-04T00:00:00.000Z", + "validUntil": "2026-09-03T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-07" + }, + "polygon:merchant-aifp1": { + "chain": "polygon", + "route": "merchant-aifp1", + "chainId": 137, + "version": "1.3", + "superseded": false, + "splitter": "0x27C1C07563c92C1AEa52cC9b4452dF49dC5a7942", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "polygon:agent-x402": { + "chain": "polygon", + "route": "agent-x402", + "chainId": 137, + "version": "1.3", + "superseded": false, + "splitter": "0x660Cd915Fc54A7EaE5CEA6854505638bd2A08531", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "optimism:merchant-aifp1": { + "chain": "optimism", + "route": "merchant-aifp1", + "chainId": 10, + "version": "1.3", + "superseded": false, + "splitter": "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "optimism:agent-x402": { + "chain": "optimism", + "route": "agent-x402", + "chainId": 10, + "version": "1.3", + "superseded": false, + "splitter": "0x38Ef6173ce0AC540f129680C2Aa4Ef739787bdBf", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "bnb:merchant-aifp1": { + "chain": "bnb", + "route": "merchant-aifp1", + "chainId": 56, + "version": "1.3", + "superseded": false, + "splitter": "0x79D481B835Cb050FAb7a045E619A6Fb9Cd73f510", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "bnb:agent-x402": { + "chain": "bnb", + "route": "agent-x402", + "chainId": 56, + "version": "1.3", + "superseded": false, + "splitter": "0x7656fb8B6627311A7d87273913D31b837Bb2b5A4", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "unichain:merchant-aifp1": { + "chain": "unichain", + "route": "merchant-aifp1", + "chainId": 130, + "version": "1.3", + "superseded": false, + "splitter": "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "unichain:agent-x402": { + "chain": "unichain", + "route": "agent-x402", + "chainId": 130, + "version": "1.3", + "superseded": false, + "splitter": "0xC701F45b3Bae9CA3a58cB33fCBA6291594D17843", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "botchain:merchant-aifp1": { + "chain": "botchain", + "route": "merchant-aifp1", + "chainId": 677, + "version": "1.3", + "superseded": false, + "splitter": "0xe855e491D0950140704DB9Cec6B7b3F725360a56", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "botchain:agent-x402": { + "chain": "botchain", + "route": "agent-x402", + "chainId": 677, + "version": "1.3", + "superseded": false, + "splitter": "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "base:merchant-aifp1": { + "chain": "base", + "route": "merchant-aifp1", + "chainId": 8453, + "version": "1.3", + "superseded": false, + "splitter": "0xB385Cc32fe39CF5B5778DF0Df0e8E9978b5F662a", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "base:agent-x402": { + "chain": "base", + "route": "agent-x402", + "chainId": 8453, + "version": "1.3", + "superseded": false, + "splitter": "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "arbitrum:merchant-aifp1": { + "chain": "arbitrum", + "route": "merchant-aifp1", + "chainId": 42161, + "version": "1.3", + "superseded": false, + "splitter": "0x80e2B445DFc44B3B2254aa376B31AEdDd3Ff934a", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "arbitrum:agent-x402": { + "chain": "arbitrum", + "route": "agent-x402", + "chainId": 42161, + "version": "1.3", + "superseded": false, + "splitter": "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "avalanche:merchant-aifp1": { + "chain": "avalanche", + "route": "merchant-aifp1", + "chainId": 43114, + "version": "1.3", + "superseded": false, + "splitter": "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "avalanche:agent-x402": { + "chain": "avalanche", + "route": "agent-x402", + "chainId": 43114, + "version": "1.3", + "superseded": false, + "splitter": "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "xrplevm:merchant-aifp1": { + "chain": "xrplevm", + "route": "merchant-aifp1", + "chainId": 1440000, + "version": "1.3", + "superseded": false, + "splitter": "0xe855e491D0950140704DB9Cec6B7b3F725360a56", + "runtimeCodeHash": "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 100, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + }, + "xrplevm:agent-x402": { + "chain": "xrplevm", + "route": "agent-x402", + "chainId": 1440000, + "version": "1.3", + "superseded": false, + "splitter": "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", + "runtimeCodeHash": "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + "owner": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + "treasuryBps": 0, + "ipCreatorBps": 0, + "validFrom": "2026-08-27T00:00:00.000Z", + "validUntil": "2026-11-25T00:00:00.000Z", + "settlementEnabled": false, + "verifiedAt": "2026-08-27" + } + } +} diff --git a/node/scripts/generate-splitter-routes.mjs b/node/scripts/generate-splitter-routes.mjs new file mode 100644 index 0000000..8458bc9 --- /dev/null +++ b/node/scripts/generate-splitter-routes.mjs @@ -0,0 +1,261 @@ +#!/usr/bin/env node +/** + * Generate src/splitterRoutes.generated.ts from the canonical registry artifact. + * + * The addresses, code hashes, fee splits, owner, policy dates and settlement + * flags below decide where money goes and who can redirect it. They are + * maintained in exactly one place — registry/registry.json in + * AiFinPay/evm-contract, where every one of them is read from the chain by + * verify-registry.mjs — and copied here as a byte-for-byte artifact. Nothing in + * that set is typed by a human twice. + * + * That is the whole point of this script. Two repositories holding the same + * payment-critical table, each edited by hand, disagree eventually, and the + * failure is silent: the amounts still look plausible in every log. So: + * + * npm run registry:sync -- --from ../../evm-contract refresh + regenerate + * npm run registry:check CI gate, fails on drift + * + * `--check` regenerates in memory and compares byte-for-byte, so hand-editing + * the generated file turns CI red rather than quietly changing a payout address. + * It also re-hashes the vendored artifact against registry/source.json, so + * editing the artifact instead of syncing it fails the same way. + * + * NOT generated, and deliberately so: viemChain, defaultRpc and explorer. Those + * are transport and presentation — a wrong RPC URL fails loudly and pays nobody, + * whereas a wrong splitter address pays the wrong party successfully. They live + * in CHAIN_TRANSPORT below, and a chain appearing in the artifact without an + * entry there is an error rather than a default. + */ + +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const ARTIFACT = join(ROOT, "registry/splitter-table.json"); +const SOURCE = join(ROOT, "registry/source.json"); +const OUTPUT = join(ROOT, "src/splitterRoutes.generated.ts"); + +const args = process.argv.slice(2); +const CHECK = args.includes("--check"); +const fromIndex = args.indexOf("--from"); +const FROM = fromIndex === -1 ? null : args[fromIndex + 1]; + +/** viem's chain export name, a default RPC and an explorer, per chain. */ +const CHAIN_TRANSPORT = { + polygon: { viem: "polygon", rpc: "https://polygon-bor-rpc.publicnode.com", explorer: "https://polygonscan.com" }, + optimism: { viem: "optimism", rpc: "https://mainnet.optimism.io", explorer: "https://optimistic.etherscan.io" }, + bnb: { viem: "bsc", rpc: "https://bsc-dataseed.bnbchain.org", explorer: "https://bscscan.com" }, + unichain: { viem: "unichain", rpc: "https://mainnet.unichain.org", explorer: "https://uniscan.xyz" }, + botchain: { viem: "botchain", rpc: "https://rpc.botchain.ai", explorer: "https://scan.botchain.ai" }, + base: { viem: "base", rpc: "https://mainnet.base.org", explorer: "https://basescan.org" }, + arbitrum: { viem: "arbitrum", rpc: "https://arb1.arbitrum.io/rpc", explorer: "https://arbiscan.io" }, + avalanche: { viem: "avalanche", rpc: "https://api.avax.network/ext/bc/C/rpc", explorer: "https://snowtrace.io" }, + xrplevm: { viem: "xrplevm", rpc: "https://rpc.xrplevm.org", explorer: "https://explorer.xrplevm.org" }, +}; + +/** Chains whose viem export comes from ./chains.js rather than viem/chains. */ +const LOCAL_CHAINS = new Set(["botchain", "xrplevm"]); + +/** The two v1.3 protocol routes. An unexpected route is an error, not a pass. */ +const ROUTES = new Set(["merchant-aifp1", "agent-x402"]); + +const EXPECTED_ROUTE_COUNT = 18; + +function sha256(buffer) { + return createHash("sha256").update(buffer).digest("hex"); +} + +function loadArtifact() { + if (!existsSync(ARTIFACT)) { + throw new Error(`${ARTIFACT} is missing. Run: npm run registry:sync -- --from `); + } + const raw = readFileSync(ARTIFACT); + const source = JSON.parse(readFileSync(SOURCE, "utf8")); + const actual = sha256(raw); + if (actual !== source.sha256) { + throw new Error( + "registry/splitter-table.json does not hash to what registry/source.json records.\n" + + ` recorded ${source.sha256}\n actual ${actual}\n` + + "The artifact is a copy of the canonical registry, not a file to edit here. " + + "Re-run: npm run registry:sync -- --from ", + ); + } + return { artifact: JSON.parse(raw.toString("utf8")), source }; +} + +/** + * Only current v1.3 routes reach the SDK table. The superseded v1.1/v1.2 + * entries stay in the canonical registry as deployment evidence, but they are + * not representable here on purpose: a resolver that cannot name a legacy + * splitter cannot silently fall back to one. + */ +function selectRoutes(artifact) { + const selected = Object.entries(artifact.routes) + .filter(([, route]) => route.version === "1.3" && !route.superseded) + .sort(([a], [b]) => (a < b ? -1 : 1)); + + if (selected.length !== EXPECTED_ROUTE_COUNT) { + throw new Error( + `expected ${EXPECTED_ROUTE_COUNT} current v1.3 routes, found ${selected.length}. ` + + "Adding or removing a settlement route is not a regeneration; say so in the PR.", + ); + } + + for (const [key, route] of selected) { + if (!CHAIN_TRANSPORT[route.chain]) { + throw new Error( + `${key}: no transport entry for chain "${route.chain}". Add it to CHAIN_TRANSPORT — ` + + "guessing an RPC for an unknown chain is how a route ends up pointing at nothing.", + ); + } + if (!ROUTES.has(route.route)) { + throw new Error(`${key}: unknown protocol route "${route.route}".`); + } + if (route.owner.toLowerCase() !== artifact.governance.safe.toLowerCase()) { + throw new Error( + `${key}: owner ${route.owner} is not the governance Safe ${artifact.governance.safe}.`, + ); + } + if (route.settlementEnabled !== false && route.settlementEnabled !== true) { + throw new Error(`${key}: settlementEnabled must be a boolean.`); + } + } + + return selected; +} + +function render({ artifact, source }, selected) { + const chains = [...new Set(selected.map(([, r]) => r.chain))]; + const viemImports = chains + .filter((c) => !LOCAL_CHAINS.has(c)) + .map((c) => CHAIN_TRANSPORT[c].viem) + .sort(); + const localImports = chains.filter((c) => LOCAL_CHAINS.has(c)).map((c) => CHAIN_TRANSPORT[c].viem).sort(); + + const entries = selected + .map(([key, r]) => { + const t = CHAIN_TRANSPORT[r.chain]; + return ` "${key}": { + chain: "${r.chain}", + route: "${r.route}", + chainId: ${r.chainId}, + viemChain: ${t.viem}, + splitter: "${r.splitter}", + owner: "${r.owner}", + treasury: "${r.treasury}", + treasuryBps: ${r.treasuryBps}, + ipCreatorBps: ${r.ipCreatorBps}, + runtimeCodeHash: "${r.runtimeCodeHash}", + settlementEnabled: ${r.settlementEnabled}, + validFrom: "${r.validFrom}", + validUntil: "${r.validUntil}", + defaultRpc: "${t.rpc}", + explorer: "${t.explorer}", + verifiedAt: "${r.verifiedAt}", + },`; + }) + .join("\n"); + + return `// DO NOT EDIT. Generated by scripts/generate-splitter-routes.mjs from +// registry/splitter-table.json, a byte-for-byte copy of the canonical registry +// artifact in AiFinPay/evm-contract. CI regenerates this file and fails on any +// difference, so a hand-edited payout address turns the build red instead of +// shipping. +// +// To change anything here: change registry/registry.json in evm-contract, let +// verify-registry.mjs read it back off the chain, then run +// npm run registry:sync -- --from +import { ${viemImports.join(", ")} } from "viem/chains"; +import { ${localImports.join(", ")} } from "./chains.js"; +import type { SplitterRouteDeployment, SplitterRouteKey } from "./splitterRoutes.js"; + +/** Where this table came from, so a deployed build can be traced to a commit. */ +export const SPLITTER_REGISTRY_SOURCE = { + repo: "${source.repo}", + path: "${source.path}", + commit: "${source.commit}", + artifactSha256: "${source.sha256}", + schemaVersion: ${artifact.schemaVersion}, + registryUpdatedAt: "${artifact.sourceUpdatedAt}", +} as const; + +/** + * The governance Safe that owns every splitter below, and the exact signer + * shape it was verified under. Read from the chain by verify-registry.mjs, which + * compares the signer set and threshold exactly rather than as a floor. + */ +export const SPLITTER_GOVERNANCE = { + safe: "${artifact.governance.safe}", + threshold: ${artifact.governance.threshold}, + owners: [ +${artifact.governance.owners.map((o) => ` "${o}",`).join("\n")} + ], +} as const; + +export const SPLITTER_ROUTES: Record = { +${entries} +}; +`; +} + +function syncFrom(evmContractPath) { + const root = resolve(evmContractPath); + const artifactPath = join(root, "registry/generated/splitter-table.json"); + if (!existsSync(artifactPath)) { + throw new Error(`${artifactPath} does not exist — is ${root} an evm-contract checkout?`); + } + const raw = readFileSync(artifactPath); + const commit = execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + const dirty = execFileSync("git", ["-C", root, "status", "--porcelain", "registry/"], { + encoding: "utf8", + }).trim(); + if (dirty) { + throw new Error( + `${root} has uncommitted changes under registry/. Commit them first — provenance ` + + "recorded against a dirty tree points at a commit that does not contain this artifact.", + ); + } + writeFileSync(ARTIFACT, raw); + const source = JSON.parse(readFileSync(SOURCE, "utf8")); + source.commit = commit; + source.sha256 = sha256(raw); + writeFileSync(SOURCE, `${JSON.stringify(source, null, 2)}\n`); + console.log(`Synced registry/splitter-table.json from ${source.repo}@${commit.slice(0, 8)}`); +} + +try { + if (FROM) syncFrom(FROM); + + const loaded = loadArtifact(); + const selected = selectRoutes(loaded.artifact); + const generated = render(loaded, selected); + + if (!CHECK) { + writeFileSync(OUTPUT, generated); + const enabled = selected.filter(([, r]) => r.settlementEnabled).length; + console.log(`Wrote src/splitterRoutes.generated.ts`); + console.log(` ${selected.length} v1.3 routes, ${enabled} with settlement enabled`); + console.log(` source ${loaded.source.repo}@${loaded.source.commit.slice(0, 8)}`); + process.exit(0); + } + + if (!existsSync(OUTPUT)) { + throw new Error(`${OUTPUT} is missing. Run: npm run registry:sync`); + } + if (readFileSync(OUTPUT, "utf8") !== generated) { + throw new Error( + "src/splitterRoutes.generated.ts has drifted from the canonical registry artifact.\n" + + " Either it was hand-edited, or the artifact changed and it was not regenerated.\n" + + " Run: npm run registry:sync", + ); + } + console.log("✓ SPLITTER_ROUTES matches the canonical registry artifact."); + console.log(` ${selected.length} v1.3 routes · ${loaded.source.repo}@${loaded.source.commit.slice(0, 8)}`); +} catch (error) { + console.error(`✗ ${error.message}`); + process.exit(1); +} diff --git a/node/src/index.ts b/node/src/index.ts index 98d2fcb..cf3433f 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -48,6 +48,8 @@ export type { // ── Unified surface (Phase 1+ / legacy callers remain source-compatible) ── export { SPLITTER_ROUTES, + SPLITTER_GOVERNANCE, + SPLITTER_REGISTRY_SOURCE, resolveSplitterRoute, resolveSettlingSplitterRoute, UnknownSplitterRouteError, diff --git a/node/src/splitterRoutes.generated.ts b/node/src/splitterRoutes.generated.ts new file mode 100644 index 0000000..19cdcee --- /dev/null +++ b/node/src/splitterRoutes.generated.ts @@ -0,0 +1,366 @@ +// DO NOT EDIT. Generated by scripts/generate-splitter-routes.mjs from +// registry/splitter-table.json, a byte-for-byte copy of the canonical registry +// artifact in AiFinPay/evm-contract. CI regenerates this file and fails on any +// difference, so a hand-edited payout address turns the build red instead of +// shipping. +// +// To change anything here: change registry/registry.json in evm-contract, let +// verify-registry.mjs read it back off the chain, then run +// npm run registry:sync -- --from +import { arbitrum, avalanche, base, bsc, optimism, polygon, unichain } from "viem/chains"; +import { botchain, xrplevm } from "./chains.js"; +import type { SplitterRouteDeployment, SplitterRouteKey } from "./splitterRoutes.js"; + +/** Where this table came from, so a deployed build can be traced to a commit. */ +export const SPLITTER_REGISTRY_SOURCE = { + repo: "AiFinPay/evm-contract", + path: "registry/generated/splitter-table.json", + commit: "9af67e0fb3e502a50c1ef4263a7704de3e8bb45b", + artifactSha256: "b745426f84a39aa0d6b2a9b93486fff3901db48c7e84c5e7b1ee48c993bd5de3", + schemaVersion: 3, + registryUpdatedAt: "2026-08-28", +} as const; + +/** + * The governance Safe that owns every splitter below, and the exact signer + * shape it was verified under. Read from the chain by verify-registry.mjs, which + * compares the signer set and threshold exactly rather than as a floor. + */ +export const SPLITTER_GOVERNANCE = { + safe: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + threshold: 3, + owners: [ + "0x2118c57dEBD53f614DDfE464Ff2941BE6646cA82", + "0x3C31dd9daCeC5473cC9B660CD69247A20701cF19", + "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", + "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", + "0x849930eB20ED0a697c71BcE565f18702D202C0F8", + ], +} as const; + +export const SPLITTER_ROUTES: Record = { + "arbitrum:agent-x402": { + chain: "arbitrum", + route: "agent-x402", + chainId: 42161, + viemChain: arbitrum, + splitter: "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://arb1.arbitrum.io/rpc", + explorer: "https://arbiscan.io", + verifiedAt: "2026-08-27", + }, + "arbitrum:merchant-aifp1": { + chain: "arbitrum", + route: "merchant-aifp1", + chainId: 42161, + viemChain: arbitrum, + splitter: "0x80e2B445DFc44B3B2254aa376B31AEdDd3Ff934a", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://arb1.arbitrum.io/rpc", + explorer: "https://arbiscan.io", + verifiedAt: "2026-08-27", + }, + "avalanche:agent-x402": { + chain: "avalanche", + route: "agent-x402", + chainId: 43114, + viemChain: avalanche, + splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://api.avax.network/ext/bc/C/rpc", + explorer: "https://snowtrace.io", + verifiedAt: "2026-08-27", + }, + "avalanche:merchant-aifp1": { + chain: "avalanche", + route: "merchant-aifp1", + chainId: 43114, + viemChain: avalanche, + splitter: "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://api.avax.network/ext/bc/C/rpc", + explorer: "https://snowtrace.io", + verifiedAt: "2026-08-27", + }, + "base:agent-x402": { + chain: "base", + route: "agent-x402", + chainId: 8453, + viemChain: base, + splitter: "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.base.org", + explorer: "https://basescan.org", + verifiedAt: "2026-08-27", + }, + "base:merchant-aifp1": { + chain: "base", + route: "merchant-aifp1", + chainId: 8453, + viemChain: base, + splitter: "0xB385Cc32fe39CF5B5778DF0Df0e8E9978b5F662a", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.base.org", + explorer: "https://basescan.org", + verifiedAt: "2026-08-27", + }, + "bnb:agent-x402": { + chain: "bnb", + route: "agent-x402", + chainId: 56, + viemChain: bsc, + splitter: "0x7656fb8B6627311A7d87273913D31b837Bb2b5A4", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://bsc-dataseed.bnbchain.org", + explorer: "https://bscscan.com", + verifiedAt: "2026-08-27", + }, + "bnb:merchant-aifp1": { + chain: "bnb", + route: "merchant-aifp1", + chainId: 56, + viemChain: bsc, + splitter: "0x79D481B835Cb050FAb7a045E619A6Fb9Cd73f510", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://bsc-dataseed.bnbchain.org", + explorer: "https://bscscan.com", + verifiedAt: "2026-08-27", + }, + "botchain:agent-x402": { + chain: "botchain", + route: "agent-x402", + chainId: 677, + viemChain: botchain, + splitter: "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.botchain.ai", + explorer: "https://scan.botchain.ai", + verifiedAt: "2026-08-27", + }, + "botchain:merchant-aifp1": { + chain: "botchain", + route: "merchant-aifp1", + chainId: 677, + viemChain: botchain, + splitter: "0xe855e491D0950140704DB9Cec6B7b3F725360a56", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.botchain.ai", + explorer: "https://scan.botchain.ai", + verifiedAt: "2026-08-27", + }, + "optimism:agent-x402": { + chain: "optimism", + route: "agent-x402", + chainId: 10, + viemChain: optimism, + splitter: "0x38Ef6173ce0AC540f129680C2Aa4Ef739787bdBf", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.optimism.io", + explorer: "https://optimistic.etherscan.io", + verifiedAt: "2026-08-27", + }, + "optimism:merchant-aifp1": { + chain: "optimism", + route: "merchant-aifp1", + chainId: 10, + viemChain: optimism, + splitter: "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.optimism.io", + explorer: "https://optimistic.etherscan.io", + verifiedAt: "2026-08-27", + }, + "polygon:agent-x402": { + chain: "polygon", + route: "agent-x402", + chainId: 137, + viemChain: polygon, + splitter: "0x660Cd915Fc54A7EaE5CEA6854505638bd2A08531", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://polygon-bor-rpc.publicnode.com", + explorer: "https://polygonscan.com", + verifiedAt: "2026-08-27", + }, + "polygon:merchant-aifp1": { + chain: "polygon", + route: "merchant-aifp1", + chainId: 137, + viemChain: polygon, + splitter: "0x27C1C07563c92C1AEa52cC9b4452dF49dC5a7942", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://polygon-bor-rpc.publicnode.com", + explorer: "https://polygonscan.com", + verifiedAt: "2026-08-27", + }, + "unichain:agent-x402": { + chain: "unichain", + route: "agent-x402", + chainId: 130, + viemChain: unichain, + splitter: "0xC701F45b3Bae9CA3a58cB33fCBA6291594D17843", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.unichain.org", + explorer: "https://uniscan.xyz", + verifiedAt: "2026-08-27", + }, + "unichain:merchant-aifp1": { + chain: "unichain", + route: "merchant-aifp1", + chainId: 130, + viemChain: unichain, + splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://mainnet.unichain.org", + explorer: "https://uniscan.xyz", + verifiedAt: "2026-08-27", + }, + "xrplevm:agent-x402": { + chain: "xrplevm", + route: "agent-x402", + chainId: 1440000, + viemChain: xrplevm, + splitter: "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 0, + ipCreatorBps: 0, + runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.xrplevm.org", + explorer: "https://explorer.xrplevm.org", + verifiedAt: "2026-08-27", + }, + "xrplevm:merchant-aifp1": { + chain: "xrplevm", + route: "merchant-aifp1", + chainId: 1440000, + viemChain: xrplevm, + splitter: "0xe855e491D0950140704DB9Cec6B7b3F725360a56", + owner: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", + treasuryBps: 100, + ipCreatorBps: 0, + runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", + settlementEnabled: false, + validFrom: "2026-08-27T00:00:00.000Z", + validUntil: "2026-11-25T00:00:00.000Z", + defaultRpc: "https://rpc.xrplevm.org", + explorer: "https://explorer.xrplevm.org", + verifiedAt: "2026-08-27", + }, +}; diff --git a/node/src/splitterRoutes.ts b/node/src/splitterRoutes.ts index b281ffe..9311af2 100644 --- a/node/src/splitterRoutes.ts +++ b/node/src/splitterRoutes.ts @@ -26,13 +26,26 @@ * Resolving by chain alone would settle at the wrong fee split, silently, * and the amounts would still look plausible in every log. * - * Generated from the canonical registry in AiFinPay/evm-contract - * (registry/generated/splitter-table.json, schemaVersion 2). Every field below - * was read from the chain by verify-registry.mjs — treasury, both bps values - * and the runtime code hash — not transcribed by hand. + * The table itself is NOT in this file. It is generated into + * splitterRoutes.generated.ts from registry/splitter-table.json, a byte-for-byte + * copy of the canonical artifact in AiFinPay/evm-contract, where every + * payment-critical field — splitter, owner, treasury, both bps values, the + * runtime code hash — was read from the chain by verify-registry.mjs. Two + * repositories hand-maintaining the same payout addresses is the failure this + * split prevents; `npm run registry:check` fails CI if they disagree. + * + * What stays here is the part worth reading: the types, the errors, and the two + * resolvers. Logic belongs in a reviewed file, not in a generated one. */ -import { polygon, base, optimism, unichain, bsc, arbitrum, avalanche, type Chain } from "viem/chains"; -import { botchain, xrplevm } from "./chains.js"; +import type { Chain } from "viem/chains"; + +import { SPLITTER_ROUTES } from "./splitterRoutes.generated.js"; + +export { + SPLITTER_ROUTES, + SPLITTER_GOVERNANCE, + SPLITTER_REGISTRY_SOURCE, +} from "./splitterRoutes.generated.js"; /** Protocol routes. A route is a fee profile fixed at construction. */ export type SplitterRoute = "merchant-aifp1" | "agent-x402"; @@ -58,6 +71,13 @@ export interface SplitterRouteDeployment { chainId: number; viemChain: Chain; splitter: `0x${string}`; + /** + * The governance Safe, read from the contract's own owner(). It controls + * pause/unpause, the treasury address and the stablecoin whitelist, so it is + * carried here rather than assumed: every other field is only as trustworthy + * as whoever can change it. + */ + owner: `0x${string}`; /** Owner and treasury are the same governance Safe on every chain. */ treasury: `0x${string}`; /** Immutable, baked into runtime code. 100 = 1%. */ @@ -76,298 +96,10 @@ export interface SplitterRouteDeployment { validUntil: string; defaultRpc: string; explorer: string; + /** The date the fields above were last read from the chain. */ + verifiedAt: string; } -export const SPLITTER_ROUTES: Record = { - "arbitrum:agent-x402": { - chain: "arbitrum", - route: "agent-x402", - chainId: 42161, - viemChain: arbitrum, - splitter: "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://arb1.arbitrum.io/rpc", - explorer: "https://arbiscan.io", - }, - "arbitrum:merchant-aifp1": { - chain: "arbitrum", - route: "merchant-aifp1", - chainId: 42161, - viemChain: arbitrum, - splitter: "0x80e2B445DFc44B3B2254aa376B31AEdDd3Ff934a", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://arb1.arbitrum.io/rpc", - explorer: "https://arbiscan.io", - }, - "avalanche:agent-x402": { - chain: "avalanche", - route: "agent-x402", - chainId: 43114, - viemChain: avalanche, - splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://api.avax.network/ext/bc/C/rpc", - explorer: "https://snowtrace.io", - }, - "avalanche:merchant-aifp1": { - chain: "avalanche", - route: "merchant-aifp1", - chainId: 43114, - viemChain: avalanche, - splitter: "0xE34Fc0E6694821c600Fa0955C0F74720ea6d8440", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://api.avax.network/ext/bc/C/rpc", - explorer: "https://snowtrace.io", - }, - "base:agent-x402": { - chain: "base", - route: "agent-x402", - chainId: 8453, - viemChain: base, - splitter: "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://mainnet.base.org", - explorer: "https://basescan.org", - }, - "base:merchant-aifp1": { - chain: "base", - route: "merchant-aifp1", - chainId: 8453, - viemChain: base, - splitter: "0xB385Cc32fe39CF5B5778DF0Df0e8E9978b5F662a", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://mainnet.base.org", - explorer: "https://basescan.org", - }, - "bnb:agent-x402": { - chain: "bnb", - route: "agent-x402", - chainId: 56, - viemChain: bsc, - splitter: "0x7656fb8B6627311A7d87273913D31b837Bb2b5A4", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://bsc-dataseed.bnbchain.org", - explorer: "https://bscscan.com", - }, - "bnb:merchant-aifp1": { - chain: "bnb", - route: "merchant-aifp1", - chainId: 56, - viemChain: bsc, - splitter: "0x79D481B835Cb050FAb7a045E619A6Fb9Cd73f510", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://bsc-dataseed.bnbchain.org", - explorer: "https://bscscan.com", - }, - "botchain:agent-x402": { - chain: "botchain", - route: "agent-x402", - chainId: 677, - viemChain: botchain, - splitter: "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://rpc.botchain.ai", - explorer: "https://scan.botchain.ai", - }, - "botchain:merchant-aifp1": { - chain: "botchain", - route: "merchant-aifp1", - chainId: 677, - viemChain: botchain, - splitter: "0xe855e491D0950140704DB9Cec6B7b3F725360a56", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://rpc.botchain.ai", - explorer: "https://scan.botchain.ai", - }, - "optimism:agent-x402": { - chain: "optimism", - route: "agent-x402", - chainId: 10, - viemChain: optimism, - splitter: "0x38Ef6173ce0AC540f129680C2Aa4Ef739787bdBf", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://mainnet.optimism.io", - explorer: "https://optimistic.etherscan.io", - }, - "optimism:merchant-aifp1": { - chain: "optimism", - route: "merchant-aifp1", - chainId: 10, - viemChain: optimism, - splitter: "0x1Fe2021336596655Fac72bC7bC40F7FFFA501d55", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://mainnet.optimism.io", - explorer: "https://optimistic.etherscan.io", - }, - "polygon:agent-x402": { - chain: "polygon", - route: "agent-x402", - chainId: 137, - viemChain: polygon, - splitter: "0x660Cd915Fc54A7EaE5CEA6854505638bd2A08531", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://polygon-bor-rpc.publicnode.com", - explorer: "https://polygonscan.com", - }, - "polygon:merchant-aifp1": { - chain: "polygon", - route: "merchant-aifp1", - chainId: 137, - viemChain: polygon, - splitter: "0x27C1C07563c92C1AEa52cC9b4452dF49dC5a7942", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://polygon-bor-rpc.publicnode.com", - explorer: "https://polygonscan.com", - }, - "unichain:agent-x402": { - chain: "unichain", - route: "agent-x402", - chainId: 130, - viemChain: unichain, - splitter: "0xC701F45b3Bae9CA3a58cB33fCBA6291594D17843", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://mainnet.unichain.org", - explorer: "https://uniscan.xyz", - }, - "unichain:merchant-aifp1": { - chain: "unichain", - route: "merchant-aifp1", - chainId: 130, - viemChain: unichain, - splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://mainnet.unichain.org", - explorer: "https://uniscan.xyz", - }, - "xrplevm:agent-x402": { - chain: "xrplevm", - route: "agent-x402", - chainId: 1440000, - viemChain: xrplevm, - splitter: "0x7E92FbE28aAc3a3942FDf019d29172bd02c96Cf0", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 0, - ipCreatorBps: 0, - runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://rpc.xrplevm.org", - explorer: "https://explorer.xrplevm.org", - }, - "xrplevm:merchant-aifp1": { - chain: "xrplevm", - route: "merchant-aifp1", - chainId: 1440000, - viemChain: xrplevm, - splitter: "0xe855e491D0950140704DB9Cec6B7b3F725360a56", - treasury: "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", - treasuryBps: 100, - ipCreatorBps: 0, - runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", - settlementEnabled: false, - validFrom: "2026-08-27T00:00:00.000Z", - validUntil: "2026-11-25T00:00:00.000Z", - defaultRpc: "https://rpc.xrplevm.org", - explorer: "https://explorer.xrplevm.org", - },}; - export class UnknownSplitterRouteError extends Error { constructor(chain: string, route: string) { super( @@ -424,11 +156,39 @@ export function resolveSettlingSplitterRoute( "successful mainnet paid end-to-end settlement with verified balance deltas", ); } + // Every comparison below is written as "prove it is inside the window", never + // "prove it is outside". With `t < from` / `t >= until`, one malformed date + // parses to NaN, both comparisons are false, and the route settles with no + // time gate at all — the gate fails OPEN on exactly the input you cannot + // trust. Requiring the positive fact instead means NaN fails every check. + const from = Date.parse(entry.validFrom); + const until = Date.parse(entry.validUntil); const t = now.getTime(); - if (t < Date.parse(entry.validFrom)) { + + if (!Number.isFinite(from) || !Number.isFinite(until)) { + throw new SplitterRouteNotSettlingError( + key, + `its policy window is unreadable (validFrom ${entry.validFrom}, validUntil ` + + `${entry.validUntil}) — a window that cannot be parsed is not a window that has opened`, + ); + } + if (!Number.isFinite(t)) { + throw new SplitterRouteNotSettlingError( + key, + "the current time was passed as an invalid Date, so the policy window cannot be evaluated", + ); + } + if (from >= until) { + throw new SplitterRouteNotSettlingError( + key, + `its policy window is inverted (validFrom ${entry.validFrom} is not before ` + + `validUntil ${entry.validUntil})`, + ); + } + if (!(t >= from)) { throw new SplitterRouteNotSettlingError(key, `its policy window opens ${entry.validFrom}`); } - if (t >= Date.parse(entry.validUntil)) { + if (!(t < until)) { throw new SplitterRouteNotSettlingError( key, `its policy window expired ${entry.validUntil} and has not been re-reviewed`, diff --git a/node/tests/splitterRoutes.test.ts b/node/tests/splitterRoutes.test.ts index 331cff8..4f0070b 100644 --- a/node/tests/splitterRoutes.test.ts +++ b/node/tests/splitterRoutes.test.ts @@ -1,10 +1,12 @@ // v1.3 route selection. Static-shape tests only, no network calls — every // value here was read from chain by verify-registry.mjs in evm-contract when // the registry was authored (2026-08-27). -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { getAddress } from "viem"; import { SPLITTER_ROUTES, + SPLITTER_GOVERNANCE, + SPLITTER_REGISTRY_SOURCE, resolveSplitterRoute, resolveSettlingSplitterRoute, UnknownSplitterRouteError, @@ -125,18 +127,134 @@ describe("resolveSplitterRoute", () => { describe("resolveSettlingSplitterRoute", () => { it("refuses a route that is not enabled for settlement", () => { expect(() => resolveSettlingSplitterRoute("polygon", "merchant-aifp1")) - .toThrow(SplitterRouteNotSettlingError); + .toThrow(/settlement is not enabled/); }); it("still refuses an unknown pair", () => { expect(() => resolveSettlingSplitterRoute("polygon", "nope")).toThrow(UnknownSplitterRouteError); }); +}); + +/** + * The policy window, tested against a route that is actually enabled. + * + * Every shipped route has settlementEnabled false, so resolveSettlingSplitterRoute + * rejects on that before it ever looks at validFrom/validUntil. A window test + * written against a shipped route therefore passes without exercising the window + * at all — it proves the settlement flag works, twice. These install a synthetic + * enabled route instead, and assert on the REASON rather than just the throw, so + * a test cannot pass for the wrong reason again. + */ +describe("policy window (enabled route)", () => { + const KEY = "testchain:merchant-aifp1"; + const FROM = "2026-08-27T00:00:00.000Z"; + const UNTIL = "2026-11-25T00:00:00.000Z"; + + const install = (overrides: Partial = {}) => { + const table = SPLITTER_ROUTES as unknown as Record; + table[KEY] = { + ...SPLITTER_ROUTES["polygon:merchant-aifp1"], + settlementEnabled: true, + validFrom: FROM, + validUntil: UNTIL, + ...overrides, + }; + }; + + afterEach(() => { + delete (SPLITTER_ROUTES as unknown as Record)[KEY]; + }); + + const resolve = (now: Date) => resolveSettlingSplitterRoute("testchain", "merchant-aifp1", now); + + it("the synthetic route is genuinely enabled, so these tests exercise the window", () => { + install(); + expect(SPLITTER_ROUTES[KEY as keyof typeof SPLITTER_ROUTES].settlementEnabled).toBe(true); + expect(() => resolve(new Date(FROM))).not.toThrow(); + }); + + it("rejects before validFrom", () => { + install(); + const before = new Date(Date.parse(FROM) - 1); + expect(() => resolve(before)).toThrow(SplitterRouteNotSettlingError); + expect(() => resolve(before)).toThrow(/policy window opens/); + }); + + it("allows exactly at validFrom — the window is inclusive at its start", () => { + install(); + expect(resolve(new Date(FROM)).chain).toBe("polygon"); + }); + + it("allows inside the window", () => { + install(); + const middle = new Date((Date.parse(FROM) + Date.parse(UNTIL)) / 2); + expect(resolve(middle).settlementEnabled).toBe(true); + }); + + it("allows one millisecond before validUntil", () => { + install(); + expect(() => resolve(new Date(Date.parse(UNTIL) - 1))).not.toThrow(); + }); + + it("rejects exactly at validUntil — the window is exclusive at its end", () => { + install(); + expect(() => resolve(new Date(UNTIL))).toThrow(/policy window expired/); + }); + + it("rejects after validUntil", () => { + install(); + const after = new Date(Date.parse(UNTIL) + 86_400_000); + expect(() => resolve(after)).toThrow(/policy window expired/); + }); + + // The reason the comparisons are written as "prove it is inside the window". + // Date.parse("nonsense") is NaN, and NaN fails every comparison — so with + // `t < from` / `t >= until` both gates are false and the route settles with no + // time check at all. Fail-open, on precisely the input you cannot trust. + it.each([ + ["validFrom", { validFrom: "not a date" }], + ["validUntil", { validUntil: "2026-13-45T99:99:99Z" }], + ["both", { validFrom: "", validUntil: "" }], + ])("fails closed when %s is malformed", (_label, overrides) => { + install(overrides as Partial); + const middle = new Date((Date.parse(FROM) + Date.parse(UNTIL)) / 2); + expect(() => resolve(middle)).toThrow(SplitterRouteNotSettlingError); + expect(() => resolve(middle)).toThrow(/policy window is unreadable/); + }); + + it("fails closed when the window is inverted", () => { + install({ validFrom: UNTIL, validUntil: FROM }); + expect(() => resolve(new Date(FROM))).toThrow(/policy window is inverted/); + }); + + it("fails closed when the caller passes an invalid Date as now", () => { + install(); + expect(() => resolve(new Date("nonsense"))).toThrow(/invalid Date/); + }); + + it("the settlement flag still wins over a valid window", () => { + install({ settlementEnabled: false }); + const middle = new Date((Date.parse(FROM) + Date.parse(UNTIL)) / 2); + expect(() => resolve(middle)).toThrow(/settlement is not enabled/); + }); +}); + +describe("registry provenance", () => { + it("records the evm-contract commit the route table was generated from", () => { + expect(SPLITTER_REGISTRY_SOURCE.repo).toBe("AiFinPay/evm-contract"); + expect(SPLITTER_REGISTRY_SOURCE.commit).toMatch(/^[0-9a-f]{40}$/); + expect(SPLITTER_REGISTRY_SOURCE.artifactSha256).toMatch(/^[0-9a-f]{64}$/); + }); + + it("every route is owned by the governance Safe the registry verified", () => { + for (const [key, d] of entries) { + expect(getAddress(d.owner), key).toBe(getAddress(SPLITTER_GOVERNANCE.safe)); + } + }); - it("refuses once the policy window has expired", () => { - const d = SPLITTER_ROUTES["polygon:merchant-aifp1"]; - const after = new Date(Date.parse(d.validUntil) + 86_400_000); - // enabled or not, an expired window must never settle - expect(() => resolveSettlingSplitterRoute("polygon", "merchant-aifp1", after)) - .toThrow(SplitterRouteNotSettlingError); + it("governance is recorded as an exact shape, 3 of 5", () => { + expect(SPLITTER_GOVERNANCE.threshold).toBe(3); + expect(SPLITTER_GOVERNANCE.owners).toHaveLength(5); + expect(new Set(SPLITTER_GOVERNANCE.owners).size).toBe(5); }); }); From 405eeaf44faf0e71058941e96fea40768da1f5f3 Mon Sep 17 00:00:00 2001 From: Syed Hassan Date: Fri, 28 Aug 2026 13:30:20 +0500 Subject: [PATCH 4/5] ci(registry): verify the vendored artifact against evm-contract at the recorded commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit registry:check is offline and self-consistent — a writer who can change this repository can change the artifact, its provenance and the generated table together, and the check blesses the set. That was the audit's HIGH: a green CI can certify a self-consistent forged snapshot. This step fetches the artifact from AiFinPay/evm-contract itself, at the exact 40-hex commit source.json records, over HTTPS from GitHub, and requires the bytes to be identical to the vendored copy. The commit is immutable and both repositories are public, so nothing in this check can be satisfied by editing files here. An unreachable GitHub fails; it does not skip. --- .github/workflows/ci.yml | 10 ++++ node/scripts/check-registry-provenance.mjs | 55 ++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 node/scripts/check-registry-provenance.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c2a7b6..56e4cf0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,16 @@ jobs: - name: Registry — SPLITTER_ROUTES matches the canonical registry run: npm run registry:check + # The check above is offline and self-consistent: a writer who can change + # this repository can change the artifact, its recorded provenance and the + # generated table together, and the offline check would bless the set. + # This step breaks that loop by fetching the artifact from the OTHER + # repository at the exact commit provenance names and requiring the bytes + # to match. Both repositories are public, so no token is involved and + # there is nothing here for a compromised writer to forge. + - name: Registry — vendored artifact is byte-identical to evm-contract at the recorded commit + run: node scripts/check-registry-provenance.mjs + - name: Build (tsc) run: npm run build diff --git a/node/scripts/check-registry-provenance.mjs b/node/scripts/check-registry-provenance.mjs new file mode 100644 index 0000000..2aadc9e --- /dev/null +++ b/node/scripts/check-registry-provenance.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * Independent cross-repository check of the vendored registry artifact. + * + * registry/source.json names the AiFinPay/evm-contract commit the artifact was + * copied from and its sha256. registry:check proves the copy still hashes to + * that — but everything it compares lives in this repository, so a writer here + * could change all three together and the check would pass. This fetches the + * artifact from evm-contract itself, at that exact commit, over HTTPS from + * GitHub, and requires the bytes to be identical. The commit is immutable and + * the repository is public; nothing in this check can be satisfied by editing + * files here. + * + * Fails closed: an unreachable GitHub is a failure, not a pass. + */ +import { readFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const source = JSON.parse(readFileSync(join(ROOT, "registry/source.json"), "utf8")); +const local = readFileSync(join(ROOT, "registry/splitter-table.json")); +const sha256 = (b) => createHash("sha256").update(b).digest("hex"); + +if (!/^[0-9a-f]{40}$/.test(source.commit)) { + console.error(`✗ source.json commit "${source.commit}" is not a full 40-hex SHA — a branch or tag can move`); + process.exit(1); +} + +const url = `https://raw.githubusercontent.com/${source.repo}/${source.commit}/${source.path}`; +let remote; +try { + const res = await fetch(url, { signal: AbortSignal.timeout(20_000) }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + remote = Buffer.from(await res.arrayBuffer()); +} catch (error) { + console.error(`✗ could not fetch ${url}: ${error.message}`); + console.error(" An unverifiable artifact is a failure, not a skip."); + process.exit(1); +} + +const localHash = sha256(local); +const remoteHash = sha256(remote); +if (localHash !== source.sha256) { + console.error(`✗ local artifact sha256 ${localHash} ≠ recorded ${source.sha256}`); + process.exit(1); +} +if (remoteHash !== localHash) { + console.error(`✗ evm-contract@${source.commit.slice(0, 8)} serves sha256 ${remoteHash}, local is ${localHash}`); + console.error(" The vendored artifact is not what the canonical repository holds at that commit."); + process.exit(1); +} +console.log(`✓ registry/splitter-table.json is byte-identical to ${source.repo}@${source.commit.slice(0, 8)}:${source.path}`); +console.log(` sha256 ${localHash}`); From 60ce3d4d8b01e438385235eac3c3355499849515 Mon Sep 17 00:00:00 2001 From: Syed Hassan Date: Fri, 28 Aug 2026 16:22:02 +0500 Subject: [PATCH 5/5] =?UTF-8?q?chore(registry):=20sync=20to=20evm-contract?= =?UTF-8?q?=20main=20after=20#20=20=E2=80=94=20schema=204,=20quorum=20and?= =?UTF-8?q?=20allowlist=20carried=20through?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The canonical artifact now carries the fields #20 added: rpcQuorum, the per-chain stablecoin allowlist, the full Safe shape and build provenance. Re-vendored byte-for-byte from evm-contract main and regenerated. rpcQuorum and stablecoins are carried into SplitterRouteDeployment. The generator refuses an artifact that marks a single-provider route enabled, mirroring the registry's own gate so a hand-edited copy cannot get past the SDK either, and refuses a route with no recorded allowlist. All 18 routes remain settlementEnabled: false. --- node/registry/source.json | 4 +- node/registry/splitter-table.json | 116 +++++++++++++++++++++- node/scripts/generate-splitter-routes.mjs | 10 ++ node/src/splitterRoutes.generated.ts | 42 +++++++- node/src/splitterRoutes.ts | 12 +++ 5 files changed, 177 insertions(+), 7 deletions(-) diff --git a/node/registry/source.json b/node/registry/source.json index 57ae4b5..1b09b0c 100644 --- a/node/registry/source.json +++ b/node/registry/source.json @@ -9,6 +9,6 @@ ], "repo": "AiFinPay/evm-contract", "path": "registry/generated/splitter-table.json", - "commit": "9af67e0fb3e502a50c1ef4263a7704de3e8bb45b", - "sha256": "b745426f84a39aa0d6b2a9b93486fff3901db48c7e84c5e7b1ee48c993bd5de3" + "commit": "8577d568932cae1e551fc08c7e7f22202cdffeb1", + "sha256": "ba556b411304c33861c778538d13921ecfbcec31937e0866a74f5e26e21e7a9f" } diff --git a/node/registry/splitter-table.json b/node/registry/splitter-table.json index 9eba458..ba0c002 100644 --- a/node/registry/splitter-table.json +++ b/node/registry/splitter-table.json @@ -4,7 +4,7 @@ "scripts/generate-sdk-table.mjs. CI regenerates this and fails on any", "difference, so hand-edits are rejected rather than shipped." ], - "schemaVersion": 3, + "schemaVersion": 4, "sourceUpdatedAt": "2026-08-28", "governance": { "safe": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", @@ -15,7 +15,17 @@ "0x25A834b6fEC79e9ee6ED04Ef5b97440149C6Cc24", "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", "0x849930eB20ED0a697c71BcE565f18702D202C0F8" - ] + ], + "singleton": "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", + "fallbackHandler": "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + "guard": "0x0000000000000000000000000000000000000000", + "modules": [] + }, + "build": { + "contract": "B2BSplitterV13", + "solcVersion": "0.8.35", + "evmVersion": "cancun", + "contractsTreeHash": "1711d24b711c9d7c80136c5e5716691f35e45eb4" }, "routes": { "polygon:legacy": { @@ -30,6 +40,8 @@ "treasury": "0xD31d82c4b35DABaA2ad7023C89A78A052D1f3c8e", "treasuryBps": 100, "ipCreatorBps": 1, + "stablecoins": null, + "rpcQuorum": 2, "validFrom": "2026-08-04T00:00:00.000Z", "validUntil": "2026-09-03T00:00:00.000Z", "settlementEnabled": false, @@ -47,6 +59,8 @@ "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", "treasuryBps": 100, "ipCreatorBps": 1, + "stablecoins": null, + "rpcQuorum": 2, "validFrom": "2026-08-04T00:00:00.000Z", "validUntil": "2026-09-03T00:00:00.000Z", "settlementEnabled": false, @@ -64,6 +78,8 @@ "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", "treasuryBps": 100, "ipCreatorBps": 1, + "stablecoins": null, + "rpcQuorum": 2, "validFrom": "2026-08-04T00:00:00.000Z", "validUntil": "2026-09-03T00:00:00.000Z", "settlementEnabled": false, @@ -81,6 +97,8 @@ "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", "treasuryBps": 100, "ipCreatorBps": 1, + "stablecoins": null, + "rpcQuorum": 2, "validFrom": "2026-08-04T00:00:00.000Z", "validUntil": "2026-09-03T00:00:00.000Z", "settlementEnabled": false, @@ -98,6 +116,8 @@ "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", "treasuryBps": 100, "ipCreatorBps": 1, + "stablecoins": null, + "rpcQuorum": 1, "validFrom": "2026-08-04T00:00:00.000Z", "validUntil": "2026-09-03T00:00:00.000Z", "settlementEnabled": false, @@ -115,6 +135,8 @@ "treasury": "0x1D5eF769A024B3157c76884fbd10302d8d83fAB9", "treasuryBps": 100, "ipCreatorBps": 1, + "stablecoins": null, + "rpcQuorum": 1, "validFrom": "2026-08-04T00:00:00.000Z", "validUntil": "2026-09-03T00:00:00.000Z", "settlementEnabled": false, @@ -132,6 +154,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -149,6 +176,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -166,6 +198,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -183,6 +220,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -200,6 +242,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": null, + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -217,6 +264,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": null, + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -234,6 +286,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -251,6 +308,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -268,6 +330,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": null, + "USDT": null + }, + "rpcQuorum": 1, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -285,6 +352,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": null, + "USDT": null + }, + "rpcQuorum": 1, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -302,6 +374,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -319,6 +396,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -336,6 +418,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -353,6 +440,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "USDT": null + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -370,6 +462,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "USDT": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7" + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -387,6 +484,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "USDT": "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7" + }, + "rpcQuorum": 2, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -404,6 +506,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 100, "ipCreatorBps": 0, + "stablecoins": { + "USDC": null, + "USDT": null + }, + "rpcQuorum": 1, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, @@ -421,6 +528,11 @@ "treasury": "0xFd936f75D9221949f2FEaB54Cd342F7527154eD5", "treasuryBps": 0, "ipCreatorBps": 0, + "stablecoins": { + "USDC": null, + "USDT": null + }, + "rpcQuorum": 1, "validFrom": "2026-08-27T00:00:00.000Z", "validUntil": "2026-11-25T00:00:00.000Z", "settlementEnabled": false, diff --git a/node/scripts/generate-splitter-routes.mjs b/node/scripts/generate-splitter-routes.mjs index 8458bc9..9df455d 100644 --- a/node/scripts/generate-splitter-routes.mjs +++ b/node/scripts/generate-splitter-routes.mjs @@ -123,6 +123,14 @@ function selectRoutes(artifact) { if (route.settlementEnabled !== false && route.settlementEnabled !== true) { throw new Error(`${key}: settlementEnabled must be a boolean.`); } + // The registry already refuses to enable a single-provider route; mirrored + // here so a hand-edited artifact cannot smuggle one past the SDK either. + if (route.settlementEnabled && (route.rpcQuorum ?? 0) < 2) { + throw new Error(`${key}: enabled for settlement but verified from ${route.rpcQuorum} provider(s).`); + } + if (!route.stablecoins || typeof route.stablecoins !== "object") { + throw new Error(`${key}: no stablecoins block — the allowlist is owner-mutable and must be recorded.`); + } } return selected; @@ -151,6 +159,8 @@ function render({ artifact, source }, selected) { ipCreatorBps: ${r.ipCreatorBps}, runtimeCodeHash: "${r.runtimeCodeHash}", settlementEnabled: ${r.settlementEnabled}, + rpcQuorum: ${r.rpcQuorum}, + stablecoins: ${JSON.stringify(r.stablecoins)}, validFrom: "${r.validFrom}", validUntil: "${r.validUntil}", defaultRpc: "${t.rpc}", diff --git a/node/src/splitterRoutes.generated.ts b/node/src/splitterRoutes.generated.ts index 19cdcee..08afc9e 100644 --- a/node/src/splitterRoutes.generated.ts +++ b/node/src/splitterRoutes.generated.ts @@ -15,9 +15,9 @@ import type { SplitterRouteDeployment, SplitterRouteKey } from "./splitterRoutes export const SPLITTER_REGISTRY_SOURCE = { repo: "AiFinPay/evm-contract", path: "registry/generated/splitter-table.json", - commit: "9af67e0fb3e502a50c1ef4263a7704de3e8bb45b", - artifactSha256: "b745426f84a39aa0d6b2a9b93486fff3901db48c7e84c5e7b1ee48c993bd5de3", - schemaVersion: 3, + commit: "8577d568932cae1e551fc08c7e7f22202cdffeb1", + artifactSha256: "ba556b411304c33861c778538d13921ecfbcec31937e0866a74f5e26e21e7a9f", + schemaVersion: 4, registryUpdatedAt: "2026-08-28", } as const; @@ -51,6 +51,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0xaf88d065e77c8cC2239327C5EDb3A432268e5831","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://arb1.arbitrum.io/rpc", @@ -69,6 +71,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0xaf88d065e77c8cC2239327C5EDb3A432268e5831","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://arb1.arbitrum.io/rpc", @@ -87,6 +91,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E","USDT":"0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7"}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://api.avax.network/ext/bc/C/rpc", @@ -105,6 +111,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E","USDT":"0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7"}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://api.avax.network/ext/bc/C/rpc", @@ -123,6 +131,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://mainnet.base.org", @@ -141,6 +151,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://mainnet.base.org", @@ -159,6 +171,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":null,"USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://bsc-dataseed.bnbchain.org", @@ -177,6 +191,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":null,"USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://bsc-dataseed.bnbchain.org", @@ -195,6 +211,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 1, + stablecoins: {"USDC":null,"USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://rpc.botchain.ai", @@ -213,6 +231,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 1, + stablecoins: {"USDC":null,"USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://rpc.botchain.ai", @@ -231,6 +251,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://mainnet.optimism.io", @@ -249,6 +271,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://mainnet.optimism.io", @@ -267,6 +291,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://polygon-bor-rpc.publicnode.com", @@ -285,6 +311,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://polygon-bor-rpc.publicnode.com", @@ -303,6 +331,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x078D782b760474a361dDA0AF3839290b0EF57AD6","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://mainnet.unichain.org", @@ -321,6 +351,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 2, + stablecoins: {"USDC":"0x078D782b760474a361dDA0AF3839290b0EF57AD6","USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://mainnet.unichain.org", @@ -339,6 +371,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x0eb0f8ca7792b13ab70f2aa3e779609cd352d279e925ddcd9e901fd9fd68b1b0", settlementEnabled: false, + rpcQuorum: 1, + stablecoins: {"USDC":null,"USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://rpc.xrplevm.org", @@ -357,6 +391,8 @@ export const SPLITTER_ROUTES: Record ipCreatorBps: 0, runtimeCodeHash: "0x4ba01815b55bf6ed2d608bed91f480c179fd644d706680c3e4a91d8181ba5c6b", settlementEnabled: false, + rpcQuorum: 1, + stablecoins: {"USDC":null,"USDT":null}, validFrom: "2026-08-27T00:00:00.000Z", validUntil: "2026-11-25T00:00:00.000Z", defaultRpc: "https://rpc.xrplevm.org", diff --git a/node/src/splitterRoutes.ts b/node/src/splitterRoutes.ts index 9311af2..8de8d92 100644 --- a/node/src/splitterRoutes.ts +++ b/node/src/splitterRoutes.ts @@ -91,6 +91,18 @@ export interface SplitterRouteDeployment { * settlement on that exact route with verified balance deltas. */ settlementEnabled: boolean; + /** + * How many independent RPC providers agreed on every field above when the + * registry was verified. A route verified from one provider can never be + * enabled — BOT Chain and XRPL EVM have exactly one public provider each. + */ + rpcQuorum: number; + /** + * Stablecoins the splitter accepts, read live via whitelistedTokens() and + * owner-mutable, so pinned separately from the runtime hash. null = not + * accepted on this chain; a chain with both null settles native only. + */ + stablecoins: { USDC: `0x${string}` | null; USDT: `0x${string}` | null }; /** Policy review window. Outside it, a route must not settle. */ validFrom: string; validUntil: string;