@aifinpay/agent 1.8.2 → 2.0.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PAYMENT_RECEIPTS.md +110 -0
- package/README.md +93 -35
- package/dist/agent.d.ts +6 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +50 -12
- package/dist/agent.js.map +1 -1
- package/dist/agentHistory.d.ts +15 -0
- package/dist/agentHistory.d.ts.map +1 -0
- package/dist/agentHistory.js +47 -0
- package/dist/agentHistory.js.map +1 -0
- package/dist/agentPassport.d.ts +95 -0
- package/dist/agentPassport.d.ts.map +1 -0
- package/dist/agentPassport.js +177 -0
- package/dist/agentPassport.js.map +1 -0
- package/dist/aifp1.d.ts +135 -8
- package/dist/aifp1.d.ts.map +1 -1
- package/dist/aifp1.js +317 -50
- package/dist/aifp1.js.map +1 -1
- package/dist/apiUrl.d.ts +3 -0
- package/dist/apiUrl.d.ts.map +1 -0
- package/dist/apiUrl.js +8 -0
- package/dist/apiUrl.js.map +1 -0
- package/dist/chains.d.ts +4 -0
- package/dist/chains.d.ts.map +1 -0
- package/dist/chains.js +24 -0
- package/dist/chains.js.map +1 -0
- package/dist/deploymentResolver.d.ts +87 -0
- package/dist/deploymentResolver.d.ts.map +1 -0
- package/dist/deploymentResolver.js +145 -0
- package/dist/deploymentResolver.js.map +1 -0
- package/dist/facilitators/aifinpay.d.ts +4 -5
- package/dist/facilitators/aifinpay.d.ts.map +1 -1
- package/dist/facilitators/aifinpay.js +49 -19
- package/dist/facilitators/aifinpay.js.map +1 -1
- package/dist/facilitators/base.d.ts +12 -1
- package/dist/facilitators/base.d.ts.map +1 -1
- package/dist/facilitators/detect.d.ts +3 -3
- package/dist/facilitators/detect.d.ts.map +1 -1
- package/dist/facilitators/detect.js +3 -22
- package/dist/facilitators/detect.js.map +1 -1
- package/dist/facilitators/standard-x402.d.ts +4 -8
- package/dist/facilitators/standard-x402.d.ts.map +1 -1
- package/dist/facilitators/standard-x402.js +218 -92
- package/dist/facilitators/standard-x402.js.map +1 -1
- package/dist/index.d.ts +26 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -31
- package/dist/index.js.map +1 -1
- package/dist/localIdentity.d.ts +17 -0
- package/dist/localIdentity.d.ts.map +1 -0
- package/dist/localIdentity.js +81 -0
- package/dist/localIdentity.js.map +1 -0
- package/dist/settlement.d.ts +198 -0
- package/dist/settlement.d.ts.map +1 -0
- package/dist/settlement.js +429 -0
- package/dist/settlement.js.map +1 -0
- package/dist/settlementHttp.d.ts +10 -0
- package/dist/settlementHttp.d.ts.map +1 -0
- package/dist/settlementHttp.js +70 -0
- package/dist/settlementHttp.js.map +1 -0
- package/dist/settlementV14.d.ts +136 -0
- package/dist/settlementV14.d.ts.map +1 -0
- package/dist/settlementV14.js +170 -0
- package/dist/settlementV14.js.map +1 -0
- package/dist/solanaDeploymentResolver.d.ts +90 -0
- package/dist/solanaDeploymentResolver.d.ts.map +1 -0
- package/dist/solanaDeploymentResolver.js +143 -0
- package/dist/solanaDeploymentResolver.js.map +1 -0
- package/dist/solanaV14Deployments.generated.d.ts +35 -0
- package/dist/solanaV14Deployments.generated.d.ts.map +1 -0
- package/dist/solanaV14Deployments.generated.js +61 -0
- package/dist/solanaV14Deployments.generated.js.map +1 -0
- package/dist/splitterRoutes.d.ts +120 -0
- package/dist/splitterRoutes.d.ts.map +1 -0
- package/dist/splitterRoutes.generated.d.ts +22 -0
- package/dist/splitterRoutes.generated.d.ts.map +1 -0
- package/dist/splitterRoutes.generated.js +459 -0
- package/dist/splitterRoutes.generated.js.map +1 -0
- package/dist/splitterRoutes.js +73 -0
- package/dist/splitterRoutes.js.map +1 -0
- package/dist/unifiedAgent.d.ts +96 -18
- package/dist/unifiedAgent.d.ts.map +1 -1
- package/dist/unifiedAgent.js +100 -325
- package/dist/unifiedAgent.js.map +1 -1
- package/dist/v14Deployments.generated.d.ts +53 -0
- package/dist/v14Deployments.generated.d.ts.map +1 -0
- package/dist/v14Deployments.generated.js +89 -0
- package/dist/v14Deployments.generated.js.map +1 -0
- package/dist/wallet.d.ts +28 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +97 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +42 -9
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -127
- package/dist/cli.js.map +0 -1
package/dist/unifiedAgent.js
CHANGED
|
@@ -15,14 +15,30 @@ import bs58 from "bs58";
|
|
|
15
15
|
import { createHash, randomBytes } from "node:crypto";
|
|
16
16
|
import { createPublicClient, createWalletClient, http, keccak256, toHex, toFunctionSelector, formatEther, } from "viem";
|
|
17
17
|
import { privateKeyToAccount, } from "viem/accounts";
|
|
18
|
-
import { defineChain } from "viem";
|
|
19
18
|
import { polygon, base, arbitrum, optimism, bsc, mainnet, unichain } from "viem/chains";
|
|
20
|
-
import {
|
|
19
|
+
import { botchain, xrplevm } from "./chains.js";
|
|
21
20
|
import { AiFinPayError, X402Error, } from "./errors.js";
|
|
22
21
|
import { Agent } from "./agent.js";
|
|
22
|
+
import { loadLocalWalletIdentity } from "./localIdentity.js";
|
|
23
|
+
import { SettlementClient, executeSettlementInvoice } from "./settlement.js";
|
|
23
24
|
import { MemorySpendLedger, FileSpendLedger } from "./spendLedger.js";
|
|
24
25
|
import { aifp1Fetch, Aifp1ReceiptCache, } from "./aifp1.js";
|
|
25
26
|
import { bridgeQuote as crossChainQuote, bridgeExecute as crossChainExecute, bridgeWaitForArrival, USDC_NATIVE, } from "./crossChain.js";
|
|
27
|
+
/**
|
|
28
|
+
* The native-token payment block of a bridge 402, whichever key it arrived
|
|
29
|
+
* under.
|
|
30
|
+
*
|
|
31
|
+
* Both names, newest first. The production bridges renamed the block
|
|
32
|
+
* `pay_matic` → `pay_native` on 2026-08-04 when the on-chain entrypoint became
|
|
33
|
+
* `payNative` — and no SDK branch followed, so `agent.call()` failed against
|
|
34
|
+
* every live bridge with an error blaming facilitator wiring (AIFINP-118).
|
|
35
|
+
* Exported so the captured-fixture test drives this exact function; the
|
|
36
|
+
* original bug survived because the tests built their 402s in the SDK's own
|
|
37
|
+
* vocabulary and never met a real one.
|
|
38
|
+
*/
|
|
39
|
+
export function nativePayBlock(challenge) {
|
|
40
|
+
return challenge.pay_native ?? challenge.pay_matic;
|
|
41
|
+
}
|
|
26
42
|
// ── B2BSplitter contract ABI ─────────────────────────────────────────────
|
|
27
43
|
// `payMatic` is the splitter's generic NATIVE-token payment entrypoint —
|
|
28
44
|
// the name is a Polygon-era legacy. On Base/Optimism/Unichain it settles
|
|
@@ -72,32 +88,6 @@ const SPLITTER_PAY_NATIVE_ABI = [
|
|
|
72
88
|
export function paymentIdFor(orderId) {
|
|
73
89
|
return keccak256(toHex(orderId));
|
|
74
90
|
}
|
|
75
|
-
// ── B2BSplitter multi-chain deployment registry ──────────────────────────
|
|
76
|
-
//
|
|
77
|
-
// Chains where the fee-on-top B2BSplitter contract is DEPLOYED AND VERIFIED
|
|
78
|
-
// on-chain (eth_getCode returned bytecode for every address below,
|
|
79
|
-
// 2026-07-15). Do NOT add chains here without re-running that check.
|
|
80
|
-
//
|
|
81
|
-
// BOT Chain (677) and XRPL EVM (1440000) are not shipped with viem/chains,
|
|
82
|
-
// so we defineChain() them locally.
|
|
83
|
-
const botchain = defineChain({
|
|
84
|
-
id: 677,
|
|
85
|
-
name: "BOT Chain",
|
|
86
|
-
nativeCurrency: { name: "BOT", symbol: "BOT", decimals: 18 },
|
|
87
|
-
rpcUrls: { default: { http: ["https://rpc.botchain.ai"] } },
|
|
88
|
-
blockExplorers: {
|
|
89
|
-
default: { name: "BOT Chain Explorer", url: "https://scan.botchain.ai" },
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
const xrplevm = defineChain({
|
|
93
|
-
id: 1440000,
|
|
94
|
-
name: "XRPL EVM",
|
|
95
|
-
nativeCurrency: { name: "XRP", symbol: "XRP", decimals: 18 },
|
|
96
|
-
rpcUrls: { default: { http: ["https://rpc.xrplevm.org"] } },
|
|
97
|
-
blockExplorers: {
|
|
98
|
-
default: { name: "XRPL EVM Explorer", url: "https://explorer.xrplevm.org" },
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
91
|
export const SPLITTER_DEPLOYMENTS = {
|
|
102
92
|
polygon: {
|
|
103
93
|
version: "1.2",
|
|
@@ -384,8 +374,30 @@ export class AiFinPayAgent {
|
|
|
384
374
|
}
|
|
385
375
|
// ── Constructors ─────────────────────────────────────────────────────────
|
|
386
376
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
377
|
+
* Load an existing local identity without creating or replacing a wallet.
|
|
378
|
+
* Priority: SEED_HASH → ./aifinpay/agents.json → AIFINPAY_AGENT_SECRET →
|
|
379
|
+
* the MCP keystore at AIFINPAY_HOME/agent.json (default ~/.aifinpay).
|
|
380
|
+
* AIFINPAY_AGENTS_FILE/AGENT_ID select a project file/record; encrypted
|
|
381
|
+
* keystores require AIFINPAY_WALLET_PASSPHRASE. Reads process.env, not .env.
|
|
382
|
+
* Missing, invalid or ambiguous configuration throws before any network call.
|
|
383
|
+
*/
|
|
384
|
+
static async fromEnvironment(opts = {}) {
|
|
385
|
+
const identity = loadLocalWalletIdentity();
|
|
386
|
+
try {
|
|
387
|
+
return identity.seedHash !== undefined
|
|
388
|
+
? await AiFinPayAgent.fromSeed(identity.seedHash, opts)
|
|
389
|
+
: await AiFinPayAgent.fromSolanaSecret(identity.secretB58, opts);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Decoder/library errors can include input. Never echo local key material.
|
|
393
|
+
throw new AiFinPayError(`Invalid wallet selected from ${identity.source}; refusing to generate a replacement wallet`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Generate a fresh, ephemeral agent. Every call selects a new identity;
|
|
398
|
+
* existing environment variables and keystores are NOT loaded or changed.
|
|
399
|
+
* This method does not persist its keys. Use fromEnvironment() for a funded
|
|
400
|
+
* existing wallet, or privately persist the new seed/secret before funding.
|
|
389
401
|
*/
|
|
390
402
|
static async new(opts = {}) {
|
|
391
403
|
// Seed-derived, NOT an independent random EVM key.
|
|
@@ -422,17 +434,21 @@ export class AiFinPayAgent {
|
|
|
422
434
|
* TODO(phase-1): replace with BIP-39/BIP-44 derivation
|
|
423
435
|
* m/44'/501'/0'/0' (Solana) + m/44'/60'/0'/0/0 (EVM) once the
|
|
424
436
|
* wallet-import audit recommendation lands.
|
|
437
|
+
* An explicit opts.evmPrivateKey imports an existing EVM wallet instead;
|
|
438
|
+
* keep that key as well as the seed to restore the same EVM address.
|
|
425
439
|
*/
|
|
426
440
|
static async fromSeed(seedHex, opts = {}) {
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
441
|
+
const normalizedSeed = seedHex.replace(/^0x/, "");
|
|
442
|
+
if (!/^[0-9a-fA-F]{64}$/.test(normalizedSeed)) {
|
|
429
443
|
throw new AiFinPayError(`fromSeed: seed must be 32 bytes (64 hex chars)`);
|
|
430
444
|
}
|
|
445
|
+
const seed = hexToBytes(normalizedSeed);
|
|
431
446
|
const kp = nacl.sign.keyPair.fromSeed(seed);
|
|
432
447
|
const inner = Agent
|
|
433
448
|
._ofKeyPair?.(kp, opts) ?? Agent.fromSecretB58(bs58.encode(kp.secretKey), opts);
|
|
434
449
|
// Derive EVM key from seed (independent, not BIP-44 — see TODO above)
|
|
435
|
-
const evmHex =
|
|
450
|
+
const evmHex = opts.evmPrivateKey
|
|
451
|
+
?? ("0x" + bytesToHex(crypto32(seed)));
|
|
436
452
|
const evmAccount = privateKeyToAccount(evmHex);
|
|
437
453
|
return new AiFinPayAgent(inner, evmAccount, opts);
|
|
438
454
|
}
|
|
@@ -959,14 +975,10 @@ export class AiFinPayAgent {
|
|
|
959
975
|
}
|
|
960
976
|
// ── Call ────────────────────────────────────────────────────────────────
|
|
961
977
|
/**
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
* (generic native-token entrypoint) on any SPLITTER_DEPLOYMENTS chain —
|
|
967
|
-
* polygon (default), base, optimism, unichain, botchain, xrplevm. Pass
|
|
968
|
-
* `chain` in CallOptions to override (the provider must accept it).
|
|
969
|
-
* Solana settlement: b2b_pay_with_split (live since 2026-05-18).
|
|
978
|
+
* Legacy bridge call. Free/read-only bridge responses are passed through;
|
|
979
|
+
* paid 402 challenges fail closed because this legacy settlement surface has
|
|
980
|
+
* no independently reviewed target. Use the reviewed fetchPaid v1.3 route
|
|
981
|
+
* with a fresh trusted quote/FX result for paid requests.
|
|
970
982
|
*
|
|
971
983
|
* Returns `null` (instead of a `Response`) iff a budget cap was hit
|
|
972
984
|
* AND budget.on_limit_exceeded is set to "skip" — the call is dropped
|
|
@@ -1031,119 +1043,22 @@ export class AiFinPayAgent {
|
|
|
1031
1043
|
catch (e) {
|
|
1032
1044
|
throw new X402Error(`bridge returned 402 with non-JSON body`);
|
|
1033
1045
|
}
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
// amount is in the same ballpark as the declared cost / caps before
|
|
1044
|
-
// signing anything. Stops a misquoting bridge from draining the agent.
|
|
1045
|
-
{
|
|
1046
|
-
const lamports = Number(ps.total_lamports ?? ps.merchant_amount_lamports);
|
|
1047
|
-
const solUsd = parseFloat(process.env.AIFINPAY_SOL_USD ?? "200");
|
|
1048
|
-
const estUsd = (Number.isFinite(lamports) ? lamports / 1e9 : 0) * solUsd;
|
|
1049
|
-
const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
|
|
1050
|
-
if (!guarded)
|
|
1051
|
-
return null;
|
|
1052
|
-
}
|
|
1053
|
-
const solTxSig = await this.submitSolanaB2BPayWithSplit(ps);
|
|
1054
|
-
const paidResp = await this.inner.fetchImpl(fullUrl, buildInit({
|
|
1055
|
-
"x-solana-tx": solTxSig,
|
|
1056
|
-
"x-order-id": ps.order_id,
|
|
1057
|
-
}));
|
|
1058
|
-
if (!paidResp.ok) {
|
|
1059
|
-
const detail = await paidResp.text().catch(() => "<unreadable>");
|
|
1060
|
-
throw new AiFinPayError(`Bridge retry failed ${paidResp.status} after Solana payment ${solTxSig}: ${detail.slice(0, 300)}`);
|
|
1061
|
-
}
|
|
1062
|
-
// Money moved here, and only here. The reservation becomes a
|
|
1063
|
-
// settled amount; the finally below then has nothing to release.
|
|
1064
|
-
this.spend24h.add(cost);
|
|
1065
|
-
if (typeof reservation === "string") {
|
|
1066
|
-
await this.ledger.commit(reservation, cost);
|
|
1067
|
-
}
|
|
1068
|
-
settled = true;
|
|
1069
|
-
if (this.telemetry)
|
|
1070
|
-
this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: solTxSig });
|
|
1071
|
-
// @internal — attach settlement metadata for consumers (MCP, telemetry
|
|
1072
|
-
// dashboards) that need to surface the tx hash without an extra RPC
|
|
1073
|
-
// call. Response headers are read-only after construction so we use
|
|
1074
|
-
// an instance property + cast on the read side.
|
|
1075
|
-
paidResp.aifinpayTx = solTxSig;
|
|
1076
|
-
paidResp.aifinpayChain = "solana";
|
|
1077
|
-
return paidResp;
|
|
1078
|
-
}
|
|
1079
|
-
// ── EVM branch (B2BSplitter.payMatic — generic native-token atomic split) ──
|
|
1080
|
-
// Settles on any chain in SPLITTER_DEPLOYMENTS (polygon default; base,
|
|
1081
|
-
// optimism, unichain, botchain, xrplevm). Native token only — the
|
|
1082
|
-
// deployed splitter payment path has no ERC-20/USDC entrypoint.
|
|
1083
|
-
if (!challenge.pay_matic) {
|
|
1084
|
-
throw new X402Error(`bridge ${provider.name} returned 402 but no pay_matic block — only legacy AiFinPay/Coinbase facilitators not yet wired into AiFinPayAgent.call()`);
|
|
1085
|
-
}
|
|
1086
|
-
const pm = challenge.pay_matic;
|
|
1087
|
-
const deployment = SPLITTER_DEPLOYMENTS[chain];
|
|
1088
|
-
if (!deployment) {
|
|
1089
|
-
throw new AiFinPayError(`No B2BSplitter deployment registered for chain "${chain}" — supported: ${Object.keys(SPLITTER_DEPLOYMENTS).join(", ")}`);
|
|
1046
|
+
// Legacy bridge settlement is fail-closed. The challenge historically
|
|
1047
|
+
// supplied arbitrary splitter/program addresses and native amounts, so
|
|
1048
|
+
// continuing from here could sign against an unreviewed target or let a
|
|
1049
|
+
// missing price bypass the budget. Keep free/read-only calls above, but
|
|
1050
|
+
// refuse every paid legacy challenge before any signing or broadcast.
|
|
1051
|
+
if (!(typeof provider.price_usd === "number" && Number.isFinite(provider.price_usd) && provider.price_usd > 0) ||
|
|
1052
|
+
!(typeof cost === "number" && Number.isFinite(cost) && cost > 0)) {
|
|
1053
|
+
throw new AiFinPayError(`legacy call() refused ${provider.name}: the provider has no trusted positive USD price; ` +
|
|
1054
|
+
`use the reviewed fetchPaid v1.3 route with fresh trusted FX instead`);
|
|
1090
1055
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (pm.chain && pm.chain !== chain) {
|
|
1095
|
-
throw new X402Error(`bridge ${provider.name} quoted pay_matic for chain "${pm.chain}" but the call was routed to "${chain}" — refusing cross-denomination payment`);
|
|
1056
|
+
if (!legacySettlementIsReviewed()) {
|
|
1057
|
+
throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
|
|
1058
|
+
`use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
|
|
1096
1059
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
// USD price per chain via SPLITTER_DEPLOYMENTS.nativeUsdEnv.
|
|
1100
|
-
{
|
|
1101
|
-
const wei = Number(pm.total_wei);
|
|
1102
|
-
const { usd: nativeUsd } = await this.nativeUsdFor(deployment);
|
|
1103
|
-
// NaN when no price is known — guardChallengeAmount then declines to
|
|
1104
|
-
// block rather than blocking on a guess. See nativeUsdFor().
|
|
1105
|
-
const estUsd = (Number.isFinite(wei) ? wei / 1e18 : 0) * nativeUsd;
|
|
1106
|
-
const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
|
|
1107
|
-
if (!guarded)
|
|
1108
|
-
return null;
|
|
1109
|
-
}
|
|
1110
|
-
// 2. Submit the B2BSplitter payment on the selected chain's mainnet and
|
|
1111
|
-
// wait for it to land. Prefer the challenge's splitter address (current
|
|
1112
|
-
// bridge behaviour); fall back to the registry address for bridges that
|
|
1113
|
-
// omit it.
|
|
1114
|
-
const splitterAddress = pm.splitter
|
|
1115
|
-
?? deployment.splitter;
|
|
1116
|
-
const txHash = await this.settleSplitterNative({
|
|
1117
|
-
chain,
|
|
1118
|
-
splitter: splitterAddress,
|
|
1119
|
-
splitterVersion: pm.splitter_version,
|
|
1120
|
-
merchantWallet: pm.merchant_wallet,
|
|
1121
|
-
totalWei: BigInt(pm.total_wei),
|
|
1122
|
-
orderId: pm.order_id,
|
|
1123
|
-
ipCreator: pm.ip_creator,
|
|
1124
|
-
});
|
|
1125
|
-
// 3. Retry the bridge with payment proof.
|
|
1126
|
-
const paidResp = await this.inner.fetchImpl(fullUrl, buildInit({
|
|
1127
|
-
"x-tx-hash": txHash,
|
|
1128
|
-
"x-order-id": pm.order_id,
|
|
1129
|
-
}));
|
|
1130
|
-
if (!paidResp.ok) {
|
|
1131
|
-
const detail = await paidResp.text().catch(() => "<unreadable>");
|
|
1132
|
-
throw new AiFinPayError(`Bridge retry failed ${paidResp.status} after on-chain payment ${txHash}: ${detail.slice(0, 300)}`);
|
|
1133
|
-
}
|
|
1134
|
-
// Money moved here, and only here. The reservation becomes a
|
|
1135
|
-
// settled amount; the finally below then has nothing to release.
|
|
1136
|
-
this.spend24h.add(cost);
|
|
1137
|
-
if (typeof reservation === "string") {
|
|
1138
|
-
await this.ledger.commit(reservation, cost);
|
|
1139
|
-
}
|
|
1140
|
-
settled = true;
|
|
1141
|
-
if (this.telemetry)
|
|
1142
|
-
this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: txHash });
|
|
1143
|
-
// @internal — see Solana branch above for rationale on property-attach.
|
|
1144
|
-
paidResp.aifinpayTx = txHash;
|
|
1145
|
-
paidResp.aifinpayChain = chain;
|
|
1146
|
-
return paidResp;
|
|
1060
|
+
throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
|
|
1061
|
+
`use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
|
|
1147
1062
|
}
|
|
1148
1063
|
finally {
|
|
1149
1064
|
// Every path that did not move money gives the budget back now rather
|
|
@@ -1167,74 +1082,14 @@ export class AiFinPayAgent {
|
|
|
1167
1082
|
// a reverted transaction would otherwise be handed to a merchant as proof of
|
|
1168
1083
|
// a payment that never moved.
|
|
1169
1084
|
async settleSplitterNative(p) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
// Passing address(0) would skip the transfer and permanently strand the
|
|
1179
|
-
// 1bp inside B2BSplitter — the contract has no sweep function.
|
|
1180
|
-
const ipCreator = p.ipCreator
|
|
1181
|
-
?? await this.splitterTreasury(p.splitter, p.chain)
|
|
1182
|
-
?? "0x0000000000000000000000000000000000000000";
|
|
1183
|
-
const { publicClient, walletClient } = this.splitterClients(p.chain);
|
|
1184
|
-
// The entrypoint follows the deployed contract, not the SDK release: v1.2
|
|
1185
|
-
// (Polygon, Optimism, BOT Chain, XRPL EVM as of 2026-07-31) takes a bytes32
|
|
1186
|
-
// paymentId and rejects one it has already settled, while Base and Unichain
|
|
1187
|
-
// still run v1.1. Sending v1.2 calldata to a v1.1 contract reverts with no
|
|
1188
|
-
// useful reason, so this must be decided per chain.
|
|
1189
|
-
// An explicit `splitterVersion` wins — a server that states it knows what
|
|
1190
|
-
// it deployed. What it must NOT fall back to is this registry's
|
|
1191
|
-
// chain -> version entry, because the ADDRESS came from the server and
|
|
1192
|
-
// the version would come from a table: two sources that can disagree.
|
|
1193
|
-
//
|
|
1194
|
-
// In production they did. The Exa bridge still hands out the pre-v1.2
|
|
1195
|
-
// splitter 0xE34Fc0E6… and sends no version; the table says "polygon is
|
|
1196
|
-
// 1.2"; the SDK called payNative on a contract that only has payMatic, and
|
|
1197
|
-
// the payment reverted with nothing in the reason to explain it.
|
|
1198
|
-
//
|
|
1199
|
-
// So when the server is silent, ask the contract at the address we were
|
|
1200
|
-
// actually handed. The registry is used only if the chain cannot be read.
|
|
1201
|
-
const splitterVersion = p.splitterVersion
|
|
1202
|
-
?? await this.detectSplitterVersion(p.splitter, p.chain, deployment.version);
|
|
1203
|
-
await this.assertCanAffordNative(publicClient, deployment, p.totalWei);
|
|
1204
|
-
const txHash = splitterVersion === "1.2"
|
|
1205
|
-
? await walletClient.writeContract({
|
|
1206
|
-
address: p.splitter,
|
|
1207
|
-
abi: SPLITTER_PAY_NATIVE_ABI,
|
|
1208
|
-
functionName: "payNative",
|
|
1209
|
-
args: [
|
|
1210
|
-
paymentIdFor(p.orderId),
|
|
1211
|
-
p.merchantWallet,
|
|
1212
|
-
ipCreator,
|
|
1213
|
-
p.orderId,
|
|
1214
|
-
],
|
|
1215
|
-
value: p.totalWei,
|
|
1216
|
-
chain: deployment.chain,
|
|
1217
|
-
account: this.evmAccount,
|
|
1218
|
-
})
|
|
1219
|
-
: await walletClient.writeContract({
|
|
1220
|
-
address: p.splitter,
|
|
1221
|
-
abi: SPLITTER_PAY_MATIC_ABI,
|
|
1222
|
-
functionName: "payMatic",
|
|
1223
|
-
args: [
|
|
1224
|
-
p.merchantWallet,
|
|
1225
|
-
ipCreator,
|
|
1226
|
-
p.orderId,
|
|
1227
|
-
],
|
|
1228
|
-
value: p.totalWei,
|
|
1229
|
-
chain: deployment.chain,
|
|
1230
|
-
account: this.evmAccount,
|
|
1231
|
-
});
|
|
1232
|
-
// Wait for receipt (inclusion is enough on these fast-block chains).
|
|
1233
|
-
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
1234
|
-
if (receipt.status !== "success") {
|
|
1235
|
-
throw new AiFinPayError(`${deployment.chain.name} tx reverted: ${txHash}`);
|
|
1236
|
-
}
|
|
1237
|
-
return txHash;
|
|
1085
|
+
void p;
|
|
1086
|
+
throw new AiFinPayError("legacy splitter settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
|
|
1087
|
+
}
|
|
1088
|
+
// Legacy Solana settlement remains a named private surface only so older
|
|
1089
|
+
// callers fail with a clear migration error instead of reaching a signer.
|
|
1090
|
+
async submitSolanaB2BPayWithSplit(ps) {
|
|
1091
|
+
void ps;
|
|
1092
|
+
throw new AiFinPayError("legacy Solana settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
|
|
1238
1093
|
}
|
|
1239
1094
|
// ── AIFP-1 merchant paywall (gateway.aifinpay.io) ───────────────────────
|
|
1240
1095
|
//
|
|
@@ -1251,6 +1106,24 @@ export class AiFinPayAgent {
|
|
|
1251
1106
|
* contain bearer JWTs — treat them like keys, and do not log them.
|
|
1252
1107
|
*/
|
|
1253
1108
|
get aifp1Receipts() { return this._aifp1Cache; }
|
|
1109
|
+
/**
|
|
1110
|
+
* Use the canonical invoice executor only with a caller-supplied deployment
|
|
1111
|
+
* pin. An unauthenticated quote cannot establish its own signing authority.
|
|
1112
|
+
*/
|
|
1113
|
+
async settleAifp1NativeV13(p, opts) {
|
|
1114
|
+
const pin = opts.settlementPin;
|
|
1115
|
+
if (!pin || pin.route_class !== "AIFP-1" || pin.chain !== "polygon" || pin.testnet) {
|
|
1116
|
+
throw new AiFinPayError("AIFP-1 fetchPaid requires an independent trusted Polygon v1.3 settlement pin; other receipt networks are not supported");
|
|
1117
|
+
}
|
|
1118
|
+
const client = new SettlementClient({ baseUrl: opts.apiBaseUrl ?? "https://api.aifinpay.io", fetchImpl: this.inner.fetchImpl, timeoutMs: this.inner.timeoutMs });
|
|
1119
|
+
const invoice = await client.invoice({
|
|
1120
|
+
route_class: "AIFP-1", chain: "polygon", asset: "POL", gross_amount: p.grossWei,
|
|
1121
|
+
merchant_wallet: p.merchantWallet, order_id: p.orderId, valid_until: Number(p.validUntil),
|
|
1122
|
+
});
|
|
1123
|
+
const { publicClient, walletClient } = this.polygonClients();
|
|
1124
|
+
const execution = await executeSettlementInvoice(invoice, walletClient, publicClient, pin);
|
|
1125
|
+
return execution.settlement_tx;
|
|
1126
|
+
}
|
|
1254
1127
|
/**
|
|
1255
1128
|
* Fetch a paywalled URL, paying the AIFP-1 gateway if it asks.
|
|
1256
1129
|
*
|
|
@@ -1266,7 +1139,6 @@ export class AiFinPayAgent {
|
|
|
1266
1139
|
* `budgetCaps.on_limit_exceeded` is "skip".
|
|
1267
1140
|
*/
|
|
1268
1141
|
async fetchPaid(url, init = {}, opts = {}) {
|
|
1269
|
-
const deployment = SPLITTER_DEPLOYMENTS.polygon;
|
|
1270
1142
|
const deps = {
|
|
1271
1143
|
fetchImpl: this.inner.fetchImpl,
|
|
1272
1144
|
cache: this._aifp1Cache,
|
|
@@ -1274,25 +1146,9 @@ export class AiFinPayAgent {
|
|
|
1274
1146
|
// (backend/aifp/agent-policy.js normalizeAddress) — a Solana pubkey here
|
|
1275
1147
|
// would silently opt the agent out of its owner's own limits.
|
|
1276
1148
|
agentId: opts.agentId ?? this.evmAddress,
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
// Polygon alone (backend/aifp/verify-settlement.js — Solana returns
|
|
1281
|
-
// notImplemented, anything else unsupported_chain). Settling elsewhere
|
|
1282
|
-
// would move real money for a receipt that can never be issued.
|
|
1283
|
-
chain: "polygon",
|
|
1284
|
-
// The address is not carried in the quote, so it comes from this SDK's
|
|
1285
|
-
// own verified registry. The backend checks the tx against its
|
|
1286
|
-
// configured SPLITTER_ADDRESS_POLYGON; if an operator has pointed that
|
|
1287
|
-
// somewhere else, /v1/pay answers no_splitter_payment_event, which is
|
|
1288
|
-
// loud. Override here if you are running against such a deployment.
|
|
1289
|
-
splitter: opts.splitter ?? deployment.splitter,
|
|
1290
|
-
// Left undetected on purpose — settleSplitterNative asks the contract,
|
|
1291
|
-
// which is the only source that cannot disagree with itself.
|
|
1292
|
-
merchantWallet: p.merchantWallet,
|
|
1293
|
-
totalWei: p.totalWei,
|
|
1294
|
-
orderId: p.orderId,
|
|
1295
|
-
}),
|
|
1149
|
+
payerAddress: this.evmAddress,
|
|
1150
|
+
signPaymentAuthorization: (message) => this.evmAccount.signMessage({ message }),
|
|
1151
|
+
settle: (p) => this.settleAifp1NativeV13(p, opts),
|
|
1296
1152
|
checkPerCall: (usd) => this.checkPerCall(usd),
|
|
1297
1153
|
reserveDaily: (usd) => this.reserveDaily(usd),
|
|
1298
1154
|
commit: (id, usd) => this.ledger.commit(id, usd),
|
|
@@ -1309,66 +1165,6 @@ export class AiFinPayAgent {
|
|
|
1309
1165
|
};
|
|
1310
1166
|
return aifp1Fetch(deps, url, init, opts);
|
|
1311
1167
|
}
|
|
1312
|
-
// ── Solana b2b_pay_with_split — build, sign, send via @solana/web3.js ───
|
|
1313
|
-
//
|
|
1314
|
-
// Manual instruction encoding (no Anchor dep):
|
|
1315
|
-
// discriminator = sha256("global:b2b_pay_with_split")[:8]
|
|
1316
|
-
// args (Borsh) = u64 merchant_amount_lamports + string order_id
|
|
1317
|
-
// accounts = [config_pda, vault_pda, agent (signer), treasury,
|
|
1318
|
-
// ip_creator, merchant_wallet, system_program]
|
|
1319
|
-
// PDAs derived from program_id with seeds ["config"] and ["vault"].
|
|
1320
|
-
async submitSolanaB2BPayWithSplit(ps) {
|
|
1321
|
-
const conn = new Connection(this.solanaRpc, "confirmed");
|
|
1322
|
-
const programId = new PublicKey(ps.program_id);
|
|
1323
|
-
const merchant = new PublicKey(ps.merchant_wallet);
|
|
1324
|
-
const treasury = new PublicKey(ps.treasury);
|
|
1325
|
-
// ip_creator slot: not surfaced by current bridge 402s; route through
|
|
1326
|
-
// treasury so the on-chain 1bp still settles atomically (treasury earns
|
|
1327
|
-
// both 100bp + 1bp). When bridges add per-merchant ip_creator support,
|
|
1328
|
-
// accept it from `ps` and pass through.
|
|
1329
|
-
const ipCreator = treasury;
|
|
1330
|
-
// Solana keypair from legacy Agent inner (tweetnacl 64-byte secret).
|
|
1331
|
-
const kp = Keypair.fromSecretKey(this.inner.secretKey);
|
|
1332
|
-
if (kp.publicKey.toString() !== this.inner.address) {
|
|
1333
|
-
throw new AiFinPayError(`Internal: Solana keypair pubkey ${kp.publicKey.toString()} does not match agent address ${this.inner.address}`);
|
|
1334
|
-
}
|
|
1335
|
-
// PDAs
|
|
1336
|
-
const [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], programId);
|
|
1337
|
-
const [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], programId);
|
|
1338
|
-
// Instruction discriminator: Anchor convention sha256("global:<fn_name>")[:8].
|
|
1339
|
-
const disc = createHash("sha256").update("global:b2b_pay_with_split").digest().subarray(0, 8);
|
|
1340
|
-
// Borsh args: merchant_amount_lamports (u64 LE) + order_id (string = u32 len + utf8)
|
|
1341
|
-
const merchantAmount = BigInt(ps.merchant_amount_lamports);
|
|
1342
|
-
const amountBuf = Buffer.alloc(8);
|
|
1343
|
-
amountBuf.writeBigUInt64LE(merchantAmount);
|
|
1344
|
-
const orderBytes = Buffer.from(ps.order_id, "utf8");
|
|
1345
|
-
if (orderBytes.length > 64) {
|
|
1346
|
-
throw new AiFinPayError(`order_id too long (${orderBytes.length} bytes > 64 limit)`);
|
|
1347
|
-
}
|
|
1348
|
-
const orderLenBuf = Buffer.alloc(4);
|
|
1349
|
-
orderLenBuf.writeUInt32LE(orderBytes.length);
|
|
1350
|
-
const data = Buffer.concat([disc, amountBuf, orderLenBuf, orderBytes]);
|
|
1351
|
-
const ix = new TransactionInstruction({
|
|
1352
|
-
programId,
|
|
1353
|
-
keys: [
|
|
1354
|
-
{ pubkey: configPda, isSigner: false, isWritable: false },
|
|
1355
|
-
{ pubkey: vaultPda, isSigner: false, isWritable: false },
|
|
1356
|
-
{ pubkey: kp.publicKey, isSigner: true, isWritable: true },
|
|
1357
|
-
{ pubkey: treasury, isSigner: false, isWritable: true },
|
|
1358
|
-
{ pubkey: ipCreator, isSigner: false, isWritable: true },
|
|
1359
|
-
{ pubkey: merchant, isSigner: false, isWritable: true },
|
|
1360
|
-
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
1361
|
-
],
|
|
1362
|
-
data,
|
|
1363
|
-
});
|
|
1364
|
-
const tx = new Transaction().add(ix);
|
|
1365
|
-
// sendAndConfirmTransaction sets recentBlockhash + feePayer + signs.
|
|
1366
|
-
const sig = await sendAndConfirmTransaction(conn, tx, [kp], {
|
|
1367
|
-
commitment: "confirmed",
|
|
1368
|
-
preflightCommitment: "confirmed",
|
|
1369
|
-
});
|
|
1370
|
-
return sig;
|
|
1371
|
-
}
|
|
1372
1168
|
// ── Sessions ────────────────────────────────────────────────────────────
|
|
1373
1169
|
/**
|
|
1374
1170
|
* TODO(phase-3): open a metered budget session.
|
|
@@ -1475,8 +1271,6 @@ export class AiFinPayAgent {
|
|
|
1475
1271
|
return 0; // never block balance() on Solana RPC issues
|
|
1476
1272
|
}
|
|
1477
1273
|
}
|
|
1478
|
-
// Cache: "chain:splitter address" → treasury address (constant per deployment).
|
|
1479
|
-
splitterTreasuryCache = new Map();
|
|
1480
1274
|
/** Read + cache B2BSplitter.treasury() on the given chain (default
|
|
1481
1275
|
* polygon for back-compat). Returns null on RPC failure. */
|
|
1482
1276
|
/**
|
|
@@ -1495,8 +1289,8 @@ export class AiFinPayAgent {
|
|
|
1495
1289
|
* six registry chains against mainnet.
|
|
1496
1290
|
*
|
|
1497
1291
|
* One eth_getCode, cached per (chain, address). Falls back to the registry
|
|
1498
|
-
* only when
|
|
1499
|
-
*
|
|
1292
|
+
* only for this read-only diagnostic when code cannot be read. The legacy
|
|
1293
|
+
* payment path is disabled regardless of the reported or detected version.
|
|
1500
1294
|
*/
|
|
1501
1295
|
splitterVersionCache = new Map();
|
|
1502
1296
|
async detectSplitterVersion(splitter, chainName, fallback) {
|
|
@@ -1518,27 +1312,6 @@ export class AiFinPayAgent {
|
|
|
1518
1312
|
return fallback;
|
|
1519
1313
|
}
|
|
1520
1314
|
}
|
|
1521
|
-
async splitterTreasury(splitter, chainName = "polygon") {
|
|
1522
|
-
const cacheKey = `${chainName}:${splitter.toLowerCase()}`;
|
|
1523
|
-
const cached = this.splitterTreasuryCache.get(cacheKey);
|
|
1524
|
-
if (cached)
|
|
1525
|
-
return cached;
|
|
1526
|
-
try {
|
|
1527
|
-
const { publicClient } = this.splitterClients(chainName);
|
|
1528
|
-
const treasury = await publicClient.readContract({
|
|
1529
|
-
address: splitter,
|
|
1530
|
-
abi: SPLITTER_TREASURY_ABI,
|
|
1531
|
-
functionName: "treasury",
|
|
1532
|
-
});
|
|
1533
|
-
if (!treasury || treasury === "0x0000000000000000000000000000000000000000")
|
|
1534
|
-
return null;
|
|
1535
|
-
this.splitterTreasuryCache.set(cacheKey, treasury);
|
|
1536
|
-
return treasury;
|
|
1537
|
-
}
|
|
1538
|
-
catch {
|
|
1539
|
-
return null;
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
1315
|
/**
|
|
1543
1316
|
* Read the native Polygon USDC (Circle-issued, 6 decimals) balance for the
|
|
1544
1317
|
* agent's EVM address via the existing viem publicClient. Returns 0
|
|
@@ -1583,6 +1356,8 @@ export class AiFinPayAgent {
|
|
|
1583
1356
|
}).catch(() => { });
|
|
1584
1357
|
}
|
|
1585
1358
|
}
|
|
1359
|
+
/** Legacy bridge settlement has no reviewed mainnet route; keep this closed. */
|
|
1360
|
+
function legacySettlementIsReviewed() { return false; }
|
|
1586
1361
|
// ── Internal hex helpers (small, no extra deps) ────────────────────────────
|
|
1587
1362
|
function hexToBytes(hex) {
|
|
1588
1363
|
const out = new Uint8Array(hex.length / 2);
|