feat(sdk): v1.3 settlement wired through the canonical registry — and the ABI that could never settle, fixed - #49
Open
syedhassan-aifinpay wants to merge 5 commits into
Open
feat(sdk): v1.3 settlement wired through the canonical registry — and the ABI that could never settle, fixed#49syedhassan-aifinpay wants to merge 5 commits into
syedhassan-aifinpay wants to merge 5 commits into
Conversation
added 3 commits
August 29, 2026 23:38
…he ABI that could never settle The v1.3 execution path, as specified on 2026-08-27: protocol route → chain → resolveSettlingSplitterRoute → v1.3 tuple ABI → signing → splitter No fallback to SPLITTER_DEPLOYMENTS. Nothing taken from a server. The contract address, its runtime hash and its owner come from the canonical registry via trustedPinFromRegistry(routeClass, chain); the chain re-confirms all three plus the bps immediately before the wallet signs; a backend invoice naming a different splitter is a hard reject. AiFinPayAgent.fetchPaid settles AIFP-1 through it instead of throwing. Found on the way, and fixed: settlement.ts could never have settled a real payment. It encoded payNative/payStable with six flat parameters — selector 0x8e4a8903 — while the deployed B2BSplitterV13 takes one struct and carries only 0x27a3bbaf. Proven by eth_call against Polygon merchant-aifp1: the flat shape gets an empty revert before any logic runs; the tuple shape reaches the contract and reverts with IncorrectNativeValue(1000, 0). No test caught it because no test reached a contract. V13_ABI is now the tuple form, SETTLEMENT_V13_SELECTORS pins the real selectors, and a test asserts them — verified to fail against the old ABI (3 of 10) and pass against the new. Every route stays closed. All 18 carry settlementEnabled: false, so the path throws SplitterRouteNotSettlingError today, and a test asserts exactly that. That is the gate working. It opens per chain-and-route in the registry after a supervised paid settlement — and the tool for that is here too: scripts/supervised-settle.mjs (AIFINP-213). `calldata` re-verifies hash, owner, bps and treasury against the chain, then prints the exact transaction for a wallet to sign; `verify` proves a hash from chain state — Payment event, payer/merchant/treasury balance deltas, splitter retaining nothing — and is the evidence that enables the route. It never holds a key and never reads the SDK gate. Dry-run against live Polygon: all checks match, selector 0x27a3bbaf, split 0.495/0.005/0 on 0.5 POL. 20 files, 197 tests. @aifinpay/agent 2.0.0-rc.4.
… treasury are one address The first supervised run pays the governance Safe as merchant, and the Safe is also the treasury, so both legs land on one balance. Checked as a single combined delta in that case; separate deltas otherwise.
…uire the event to come from the splitter The first supervised settlement on Polygon (0xa93a45a0…) was sent by a MetaMask smart account: an EIP-7702 transaction to MetaMask's delegation manager, which executed the call AS the account. tx.to was therefore not the splitter, and the verifier stopped there — while the Payment event, emitted by the registry splitter, named the sender as payer and every balance delta was exact. verify now accepts either path. Direct: tx.to is the splitter and the calldata selector is payNative. Delegated: the sender carries 0xef0100 delegation code or the transaction has an authorizationList, and the path is printed. In both cases the Payment event must be emitted BY the registry splitter address and name tx.from as payer, and the deltas must match the profile exactly. A transaction to some other contract from a plain EOA is still refused rather than guessed at.
syedhassan-aifinpay
force-pushed
the
feat/v13-execution
branch
from
August 29, 2026 18:39
72b8219 to
ed3aaf3
Compare
added 2 commits
August 30, 2026 12:14
…ement evm-contract #30 lets the canonical registry hold a testnet entry (testnet: true, only on a chain in a closed set) so a v1.3 rehearsal can happen on Amoy before mainnet. Such an entry is owned by a deployer key and verified from one provider — right for a rehearsal, wrong for anything this SDK settles real money through. The generator now excludes testnet entries from the production table, so the resolver cannot name them at all, and resolveSettlingSplitterRoute refuses one regardless of its flags in case an artifact ever reaches it another way. Both are tested; every shipped route asserts testnet: false. 199 tests.
…'s expected delta On Base, Optimism and Unichain the sender also pays an L1 data fee that is not in gasUsed × effectiveGasPrice. viem's op-stack formatters expose it as receipt.l1Fee; without it every OP-stack settlement read as a payer mismatch of a few hundred gwei while the merchant and treasury legs were exact. Found verifying the 30 Aug batch: 6 of the 13 routes are OP-stack.
enot3615
added a commit
that referenced
this pull request
Sep 2, 2026
The published-files gate is right: unifiedAgent.ts ships to users, so two different builds must not both call themselves rc.3. rc.6 rather than rc.4 on purpose. rc.4 is claimed by #46 and rc.5 by #49, both still open. Taking the next number above every open claim means this can merge in any order without making anyone renumber — and a royalty fix should not wait on a release-numbering negotiation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The v1.3 execution PR, as specified on 27 Aug. Rebased onto
mainafter #40 merged — three commits of its own. Merge #46 first (rc.4), then this (rc.5).The path
trustedPinFromRegistry(routeClass, chain)derives the pin — address, runtime hash, owner — from the canonical registry. No fallback toSPLITTER_DEPLOYMENTS; nothing from a server. It throws unless the route is enabled.verifySettlementRouteOnChainnow also readsowner()and requires the governance Safe, alongside the existing bytecode-hash and bps checks.trusted_pin_mismatch) — tested with the legacy v1.2 Polygon address, a real working contract with the wrong economics.settleInvoice()is the one-call form.AiFinPayAgent.fetchPaidsettles AIFP-1 through it instead of throwing.Found on the way: the existing v1.3 path could never have settled
settlement.tsonmainencodespayNative/payStablewith six flat parameters — selector0x8e4a8903. The deployedB2BSplitterV13takes one struct and carries only0x27a3bbaf. Proven byeth_callagainst Polygonmerchant-aifp1:0x8e4a8903…execution reverted, data0x— no such function0x27a3bbaf…IncorrectNativeValue(1000, 0)— reached the logicNo test caught it because no test reached a contract. Fixed:
V13_ABIis the tuple form,SETTLEMENT_V13_SELECTORSpins the real selectors, the canonical invoicefunctionlabel is the tuple signature (AIFINP-179's "canonical instruction shape"). The new tests were run against the old ABI: 3 of 10 fail, as they should.Every route stays closed
All 18 are
settlementEnabled: false, so the path throwsSplitterRouteNotSettlingErrortoday — asserted by a test at the agent level. That's the gate working. It opens per chain-and-route in the registry after a supervised paid settlement.The circle-breaker (AIFINP-213) —
scripts/supervised-settle.mjsDeliberately not the SDK path and never reads the SDK gate.
calldatare-verifies chain id, bytecode hash,owner(), bps andtreasury()against the chain, then prints the exactto / value / datafor a wallet to sign.verify <tx>proves it from chain state: Payment event, payer/merchant/treasury balance deltas at block−1 vs block, gas accounted, splitter retaining nothing — and fails closed on any disagreement. That output is the evidence for flipping the route inregistry.json. It never holds a key.Dry run against live Polygon just now:
Numbers
20 files, 197 tests.
@aifinpay/agent→2.0.0-rc.5(#40 landed as rc.3, #46 takes rc.4). Lockfile regenerated under npm 10.Proposed first activation, for Dimitry's decision: Polygon
merchant-aifp1, ~0.5 POL, from a wallet he names,verifyoutput posted on AIFINP-213 before the registry flips.