@aifinpay/agent 2.3.0 → 2.5.0
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 +194 -19
- package/README.md +58 -27
- package/dist/agentHistory.d.ts +9 -0
- package/dist/agentHistory.d.ts.map +1 -1
- package/dist/agentHistory.js +110 -16
- package/dist/agentHistory.js.map +1 -1
- package/dist/aifp1.d.ts +124 -17
- package/dist/aifp1.d.ts.map +1 -1
- package/dist/aifp1.js +580 -82
- package/dist/aifp1.js.map +1 -1
- package/dist/deploymentErrors.d.ts +5 -0
- package/dist/deploymentErrors.d.ts.map +1 -0
- package/dist/deploymentErrors.js +8 -0
- package/dist/deploymentErrors.js.map +1 -0
- package/dist/deploymentResolver.d.ts +2 -3
- package/dist/deploymentResolver.d.ts.map +1 -1
- package/dist/deploymentResolver.js +2 -6
- package/dist/deploymentResolver.js.map +1 -1
- package/dist/facilitators/index.d.ts +1 -1
- package/dist/facilitators/index.d.ts.map +1 -1
- package/dist/generated/solanaV14Deployments.generated.d.ts +1 -0
- package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -1
- package/dist/generated/solanaV14Deployments.generated.js +4 -2
- package/dist/generated/solanaV14Deployments.generated.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/paymentChains.d.ts +95 -0
- package/dist/paymentChains.d.ts.map +1 -0
- package/dist/paymentChains.js +116 -0
- package/dist/paymentChains.js.map +1 -0
- package/dist/settlement.d.ts +5 -5
- package/dist/settlementSolanaV14.d.ts +135 -0
- package/dist/settlementSolanaV14.d.ts.map +1 -0
- package/dist/settlementSolanaV14.js +733 -0
- package/dist/settlementSolanaV14.js.map +1 -0
- package/dist/settlementV14.d.ts +9 -2
- package/dist/settlementV14.d.ts.map +1 -1
- package/dist/settlementV14.js +81 -14
- package/dist/settlementV14.js.map +1 -1
- package/dist/solanaDeploymentResolver.d.ts +2 -1
- package/dist/solanaDeploymentResolver.d.ts.map +1 -1
- package/dist/solanaDeploymentResolver.js +1 -32
- package/dist/solanaDeploymentResolver.js.map +1 -1
- package/dist/spendLedger.d.ts +74 -33
- package/dist/spendLedger.d.ts.map +1 -1
- package/dist/spendLedger.js +523 -130
- package/dist/spendLedger.js.map +1 -1
- package/dist/unifiedAgent.d.ts +17 -8
- package/dist/unifiedAgent.d.ts.map +1 -1
- package/dist/unifiedAgent.js +227 -42
- package/dist/unifiedAgent.js.map +1 -1
- package/package.json +3 -3
package/dist/unifiedAgent.js
CHANGED
|
@@ -15,18 +15,21 @@ 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 { polygon, base, arbitrum, optimism, bsc, mainnet, unichain } from "viem/chains";
|
|
18
|
+
import { polygon, base, arbitrum, optimism, bsc, mainnet, unichain, avalanche } from "viem/chains";
|
|
19
|
+
import { paymentChain } from "./paymentChains.js";
|
|
19
20
|
import { botchain, xrplevm, robinhood } from "./chains.js";
|
|
20
21
|
import { V14_DEPLOYMENTS } from "./generated/v14Deployments.generated.js";
|
|
21
22
|
import { SPLITTER_ROUTES } from "./generated/splitterRoutes.generated.js";
|
|
23
|
+
import { Keypair, } from "@solana/web3.js";
|
|
22
24
|
import { AiFinPayError, X402Error } from "./errors.js";
|
|
23
25
|
import { Agent } from "./agent.js";
|
|
24
26
|
import { loadLocalWalletIdentity } from "./localIdentity.js";
|
|
25
27
|
import { SettlementClient, executeSettlementInvoice, } from "./settlement.js";
|
|
26
|
-
import { executeV14Settlement } from "./settlementV14.js";
|
|
27
|
-
import {
|
|
28
|
+
import { executeV14Settlement, assertPreparedV14Recovery } from "./settlementV14.js";
|
|
29
|
+
import { solanaV14Rpc, authorizedSolanaV14Inventory, prepareSolanaV14Settlement, executeSolanaV14Settlement, verifyHistoricalSolanaV14Quote, solanaStableMint, SOLANA_GENESIS, } from "./settlementSolanaV14.js";
|
|
30
|
+
import { getAgentHistory, getQuota } from "./agentHistory.js";
|
|
28
31
|
import { MemorySpendLedger, FileSpendLedger } from "./spendLedger.js";
|
|
29
|
-
import { aifp1Fetch, recoverAifp1Payment, Aifp1ReceiptCache, } from "./aifp1.js";
|
|
32
|
+
import { aifp1Fetch, recoverAifp1Payment, Aifp1ReceiptCache, Aifp1QuoteError, } from "./aifp1.js";
|
|
30
33
|
import { bridgeQuote as crossChainQuote, bridgeExecute as crossChainExecute, bridgeWaitForArrival, USDC_NATIVE, } from "./crossChain.js";
|
|
31
34
|
/**
|
|
32
35
|
* The native-token payment block of a bridge 402, whichever key it arrived
|
|
@@ -109,10 +112,26 @@ const CHAIN_OBJECTS = {
|
|
|
109
112
|
// Legacy v1.2 splitter addresses, verified on-chain 2026-08-01. These are the
|
|
110
113
|
// pre-v1.3 contracts; v1.3+ addresses live in the generated registry files.
|
|
111
114
|
const LEGACY_SPLITTER = {
|
|
112
|
-
polygon: {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
polygon: {
|
|
116
|
+
splitter: "0xbD1fa5453f212F096c0213788a645eC597FB4DDe",
|
|
117
|
+
version: "1.2",
|
|
118
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
119
|
+
},
|
|
120
|
+
base: {
|
|
121
|
+
splitter: "0x8Ad9830D16b1f10333866a3f38C949CbB19f4BAD",
|
|
122
|
+
version: "1.1",
|
|
123
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
124
|
+
},
|
|
125
|
+
optimism: {
|
|
126
|
+
splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74",
|
|
127
|
+
version: "1.2",
|
|
128
|
+
usdc: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
|
129
|
+
},
|
|
130
|
+
unichain: {
|
|
131
|
+
splitter: "0xeE92807decAa3A02F1e165dd7Efcd92ab9aA83CB",
|
|
132
|
+
version: "1.1",
|
|
133
|
+
usdc: "0x078D782b760474a361dDA0AF3839290b0EF57AD6",
|
|
134
|
+
},
|
|
116
135
|
robinhood: { splitter: "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", version: "1.2" },
|
|
117
136
|
xrplevm: { splitter: "0x147d8fF8c027E24303b5B99CbC8843e1D3dF94cC", version: "1.2" },
|
|
118
137
|
botchain: { splitter: "0x147d8fF8c027E24303b5B99CbC8843e1D3dF94cC", version: "1.2" },
|
|
@@ -141,7 +160,10 @@ const NATIVE_USD_DEFAULT = {
|
|
|
141
160
|
const TRANSPORT_OVERRIDE = {
|
|
142
161
|
polygon: { defaultRpc: "https://polygon.drpc.org" },
|
|
143
162
|
botchain: { defaultRpc: "https://rpc.botchain.ai", explorer: "https://scan.botchain.ai" },
|
|
144
|
-
robinhood: {
|
|
163
|
+
robinhood: {
|
|
164
|
+
defaultRpc: "https://rpc.mainnet.chain.robinhood.com",
|
|
165
|
+
explorer: "https://robinhoodchain.blockscout.com",
|
|
166
|
+
},
|
|
145
167
|
};
|
|
146
168
|
function buildSplitterDeployments() {
|
|
147
169
|
const result = {};
|
|
@@ -156,7 +178,7 @@ function buildSplitterDeployments() {
|
|
|
156
178
|
version: legacy.version,
|
|
157
179
|
chainId: route?.chainId ?? chainObj.id,
|
|
158
180
|
chain: chainObj,
|
|
159
|
-
defaultRpc: override?.defaultRpc ?? route?.defaultRpc ??
|
|
181
|
+
defaultRpc: override?.defaultRpc ?? route?.defaultRpc ?? chainObj.rpcUrls?.default?.http?.[0] ?? "",
|
|
160
182
|
splitter: legacy.splitter,
|
|
161
183
|
usdc: legacy.usdc,
|
|
162
184
|
explorer: override?.explorer ?? route?.explorer ?? "",
|
|
@@ -178,7 +200,11 @@ const EVM_CHAIN_OBJECTS = {
|
|
|
178
200
|
robinhood,
|
|
179
201
|
};
|
|
180
202
|
function evmChainObject(name) {
|
|
181
|
-
return (
|
|
203
|
+
return ((name === "bnb"
|
|
204
|
+
? bsc
|
|
205
|
+
: name === "avalanche"
|
|
206
|
+
? avalanche
|
|
207
|
+
: EVM_CHAIN_OBJECTS[name]) ??
|
|
182
208
|
SPLITTER_DEPLOYMENTS[name]?.chain);
|
|
183
209
|
}
|
|
184
210
|
// Mainnet USDC SPL mint on Solana (Circle native, not Wormhole-wrapped USDCet)
|
|
@@ -323,12 +349,12 @@ export class AiFinPayAgent {
|
|
|
323
349
|
if (opts.spendLedger)
|
|
324
350
|
this.ledgerOverride = opts.spendLedger;
|
|
325
351
|
this.telemetry = opts.telemetry !== false;
|
|
326
|
-
this.polygonRpc = opts.polygonRpc ?? "https://polygon.drpc.org";
|
|
352
|
+
this.polygonRpc = opts.polygonRpc ?? opts.evmRpcUrls?.polygon ?? "https://polygon.drpc.org";
|
|
327
353
|
this.solanaRpc = opts.solanaRpc ?? process.env.AIFINPAY_SOLANA_RPC ?? "https://api.mainnet-beta.solana.com";
|
|
328
354
|
// Optional RPC overrides for non-Polygon EVM chains used in bridge flows.
|
|
329
355
|
// Falls back to viem's chain.rpcUrls.default if not provided.
|
|
330
356
|
if (opts.evmRpcUrls)
|
|
331
|
-
this.evmRpcUrls = opts.evmRpcUrls;
|
|
357
|
+
this.evmRpcUrls = { ...opts.evmRpcUrls };
|
|
332
358
|
if (this.polygonRpc)
|
|
333
359
|
this.evmRpcUrls.polygon = this.polygonRpc;
|
|
334
360
|
}
|
|
@@ -365,7 +391,9 @@ export class AiFinPayAgent {
|
|
|
365
391
|
if (!chain) {
|
|
366
392
|
throw new AiFinPayError(`evmClients: unsupported EVM chain "${name}"`);
|
|
367
393
|
}
|
|
368
|
-
const rpcUrl = this.evmRpcUrls[name] ??
|
|
394
|
+
const rpcUrl = this.evmRpcUrls[name] ??
|
|
395
|
+
paymentChain(name)?.defaultRpc ??
|
|
396
|
+
SPLITTER_DEPLOYMENTS[name]?.defaultRpc;
|
|
369
397
|
const transport = rpcUrl ? http(rpcUrl) : http();
|
|
370
398
|
const publicClient = createPublicClient({ chain, transport });
|
|
371
399
|
const walletClient = createWalletClient({ chain, transport, account: this.evmAccount });
|
|
@@ -518,7 +546,7 @@ export class AiFinPayAgent {
|
|
|
518
546
|
for (const url of this.registryCandidates) {
|
|
519
547
|
let r;
|
|
520
548
|
try {
|
|
521
|
-
r = await
|
|
549
|
+
r = await this.inner.fetchImpl(url);
|
|
522
550
|
}
|
|
523
551
|
catch (err) {
|
|
524
552
|
attempts.push(`${url} → ${err.message}`);
|
|
@@ -591,7 +619,7 @@ export class AiFinPayAgent {
|
|
|
591
619
|
const nonce = await this.networkNonce();
|
|
592
620
|
const message = `AiFinPay-network-publish:polygon:${addr}:${nonce}`;
|
|
593
621
|
const signature = await this.evmAccount.signMessage({ message });
|
|
594
|
-
const r = await
|
|
622
|
+
const r = await this.inner.fetchImpl(`${base}/api/network/agents/${addr}/publish`, {
|
|
595
623
|
method: "POST",
|
|
596
624
|
headers: { "content-type": "application/json" },
|
|
597
625
|
body: JSON.stringify({
|
|
@@ -617,7 +645,7 @@ export class AiFinPayAgent {
|
|
|
617
645
|
const nonce = await this.networkNonce();
|
|
618
646
|
const message = `AiFinPay-network-unpublish:polygon:${addr}:${nonce}`;
|
|
619
647
|
const signature = await this.evmAccount.signMessage({ message });
|
|
620
|
-
const r = await
|
|
648
|
+
const r = await this.inner.fetchImpl(`${base}/api/network/agents/${addr}/unpublish`, {
|
|
621
649
|
method: "POST",
|
|
622
650
|
headers: { "content-type": "application/json" },
|
|
623
651
|
body: JSON.stringify({ nonce, signature }),
|
|
@@ -647,14 +675,14 @@ export class AiFinPayAgent {
|
|
|
647
675
|
if (query.limit)
|
|
648
676
|
params.set("limit", String(query.limit));
|
|
649
677
|
}
|
|
650
|
-
const r = await
|
|
678
|
+
const r = await this.inner.fetchImpl(`${base}/api/network/agents?${params.toString()}`);
|
|
651
679
|
if (!r.ok)
|
|
652
680
|
throw new AiFinPayError(`network search ${base} → ${r.status}`);
|
|
653
681
|
const j = (await r.json());
|
|
654
682
|
return j.agents ?? [];
|
|
655
683
|
}
|
|
656
684
|
async networkNonce() {
|
|
657
|
-
const r = await
|
|
685
|
+
const r = await this.inner.fetchImpl(`${this.inner.baseUrl}/api/network/nonce`);
|
|
658
686
|
if (!r.ok)
|
|
659
687
|
throw new AiFinPayError(`network nonce → ${r.status}`);
|
|
660
688
|
const { nonce } = (await r.json());
|
|
@@ -698,11 +726,11 @@ export class AiFinPayAgent {
|
|
|
698
726
|
* total and then reserved would have rebuilt the race this replaces, where
|
|
699
727
|
* two concurrent calls both read the same total, both passed, and both paid.
|
|
700
728
|
*/
|
|
701
|
-
async reserveDaily(costUsd) {
|
|
729
|
+
async reserveDaily(costUsd, binding) {
|
|
702
730
|
const cap = this.budgetCaps.daily_usd;
|
|
703
731
|
if (cap === undefined)
|
|
704
732
|
return null; // no daily cap to enforce
|
|
705
|
-
const id = await this.ledger.reserve(costUsd, cap, 24 * 3600 * 1000);
|
|
733
|
+
const id = await this.ledger.reserve(costUsd, cap, 24 * 3600 * 1000, binding);
|
|
706
734
|
if (id)
|
|
707
735
|
return id;
|
|
708
736
|
const err = new BudgetCapExceededError("daily", `this call would take daily spend past the $${cap} cap`);
|
|
@@ -1150,7 +1178,7 @@ export class AiFinPayAgent {
|
|
|
1150
1178
|
if (p.settlementCall?.splitter_version !== "1.4")
|
|
1151
1179
|
throw new AiFinPayError("expected signed v1.4 settlement call");
|
|
1152
1180
|
const chain = opts.v14.chain ?? "polygon";
|
|
1153
|
-
const { publicClient, walletClient } = this.
|
|
1181
|
+
const { publicClient, walletClient } = chain === "polygon" ? this.polygonClients() : this.evmClients(chain);
|
|
1154
1182
|
const result = await executeV14Settlement(p.settlementCall, {
|
|
1155
1183
|
publicClient,
|
|
1156
1184
|
walletClient,
|
|
@@ -1202,27 +1230,97 @@ export class AiFinPayAgent {
|
|
|
1202
1230
|
* `budgetCaps.on_limit_exceeded` is "skip".
|
|
1203
1231
|
*/
|
|
1204
1232
|
async fetchPaid(url, init = {}, opts = {}) {
|
|
1233
|
+
// Quote admission is durable even when the owner did not configure a daily cap.
|
|
1234
|
+
const ledger = opts.solanaV14 && !this.ledgerOverride ? (this._ledger = FileSpendLedger.forAgent(this.evmAddress)) : this.ledger;
|
|
1235
|
+
if (opts.solanaV14 && (!ledger.beginQuoteAdmission || !ledger.adoptQuoteAdmission || !ledger.closeQuoteAdmission))
|
|
1236
|
+
throw new Aifp1QuoteError("Solana custom ledger requires durable quote-admission hooks before POST");
|
|
1237
|
+
if ((opts.v14 || opts.solanaV14) &&
|
|
1238
|
+
this.budgetCaps.daily_usd !== undefined &&
|
|
1239
|
+
(!this.ledger.prepare || !this.ledger.assertRecovery || !this.ledger.complete)) {
|
|
1240
|
+
throw new Aifp1QuoteError("Custom capped v1.4 ledger requires prepare/assertRecovery/complete hooks before payment");
|
|
1241
|
+
}
|
|
1205
1242
|
const deps = {
|
|
1243
|
+
walletIdentity: this.evmAddress.toLowerCase(),
|
|
1244
|
+
quoteOwnerContext: JSON.stringify([
|
|
1245
|
+
this.solanaRpc,
|
|
1246
|
+
this.budgetCaps.daily_usd ?? null,
|
|
1247
|
+
this.budgetCaps.per_call_usd ?? null,
|
|
1248
|
+
this.budgetCaps.on_limit_exceeded ?? "throw",
|
|
1249
|
+
]),
|
|
1250
|
+
beginQuoteAdmission: (binding, context, create) => ledger.beginQuoteAdmission(binding, context, create),
|
|
1251
|
+
adoptQuoteAdmission: (id, binding, context, quoteJson) => ledger.adoptQuoteAdmission(id, binding, context, quoteJson),
|
|
1252
|
+
closeQuoteAdmission: (id, binding, context, terminal, quoteJson) => ledger.closeQuoteAdmission(id, binding, context, terminal, quoteJson),
|
|
1253
|
+
verifyHistoricalSolanaQuote: (call, auth, orderId) => verifyHistoricalSolanaV14Quote(call, {
|
|
1254
|
+
rpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
|
|
1255
|
+
deployment: authorizedSolanaV14Inventory(auth.environment, auth.network),
|
|
1256
|
+
payer: this.solanaAddress,
|
|
1257
|
+
orderId,
|
|
1258
|
+
}),
|
|
1206
1259
|
fetchImpl: this.inner.fetchImpl,
|
|
1207
1260
|
cache: this._aifp1Cache,
|
|
1208
1261
|
// A 0x address, because AIFP agent policies are address-keyed
|
|
1209
1262
|
// (backend/aifp/agent-policy.js normalizeAddress) — a Solana pubkey here
|
|
1210
1263
|
// would silently opt the agent out of its owner's own limits.
|
|
1211
|
-
agentId: opts.agentId ?? this.evmAddress,
|
|
1212
|
-
payerAddress: this.evmAddress,
|
|
1213
|
-
signPaymentAuthorization: (message) =>
|
|
1264
|
+
agentId: opts.agentId ?? (opts.solanaV14 ? this.solanaAddress : this.evmAddress),
|
|
1265
|
+
payerAddress: opts.solanaV14 ? this.solanaAddress : this.evmAddress,
|
|
1266
|
+
signPaymentAuthorization: (message) => opts.solanaV14
|
|
1267
|
+
? Promise.resolve(bs58.encode(nacl.sign.detached(Buffer.from(message), this.inner.secretKey)))
|
|
1268
|
+
: this.evmAccount.signMessage({ message }),
|
|
1269
|
+
prepareSolana: (call, auth, orderId) => prepareSolanaV14Settlement(call, {
|
|
1270
|
+
rpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
|
|
1271
|
+
deployment: authorizedSolanaV14Inventory(auth.environment, auth.network),
|
|
1272
|
+
payer: this.solanaAddress,
|
|
1273
|
+
orderId,
|
|
1274
|
+
maxFeeLamports: auth.maxFeeLamports,
|
|
1275
|
+
}),
|
|
1276
|
+
settleSolana: (plan, onPrepared) => executeSolanaV14Settlement(plan, {
|
|
1277
|
+
rpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
|
|
1278
|
+
keypair: Keypair.fromSecretKey(this.inner.secretKey),
|
|
1279
|
+
onPrepared,
|
|
1280
|
+
}),
|
|
1214
1281
|
settle: (p) => this.settleAifp1NativeV13(p, opts),
|
|
1215
1282
|
checkPerCall: (usd) => this.checkPerCall(usd),
|
|
1216
|
-
reserveDaily: (usd) =>
|
|
1283
|
+
reserveDaily: async (usd, binding, admissionId) => {
|
|
1284
|
+
if (!admissionId)
|
|
1285
|
+
return this.reserveDaily(usd, binding);
|
|
1286
|
+
const cap = this.budgetCaps.daily_usd ?? Number.MAX_VALUE;
|
|
1287
|
+
const id = await ledger.reserve(usd, cap, 24 * 3600 * 1000, binding, admissionId);
|
|
1288
|
+
if (id)
|
|
1289
|
+
return id;
|
|
1290
|
+
if (this.budgetCaps.on_limit_exceeded === "skip")
|
|
1291
|
+
return "skip";
|
|
1292
|
+
throw new BudgetCapExceededError("daily", `this call would take daily spend past the $${cap} cap`);
|
|
1293
|
+
},
|
|
1217
1294
|
commit: (id, usd) => this.ledger.commit(id, usd),
|
|
1218
1295
|
release: (id) => this.ledger.release(id),
|
|
1296
|
+
prepareReservation: async (id, tx, binding) => {
|
|
1297
|
+
if (!this.ledger.prepare || !this.ledger.assertRecovery || !this.ledger.complete) {
|
|
1298
|
+
throw new Aifp1QuoteError("Custom capped v1.4 ledger requires prepare/assertRecovery/complete hooks");
|
|
1299
|
+
}
|
|
1300
|
+
await this.ledger.prepare(id, tx, binding);
|
|
1301
|
+
},
|
|
1302
|
+
assertReservation: async (id, tx, binding) => {
|
|
1303
|
+
if (!this.ledger.assertRecovery)
|
|
1304
|
+
throw new Aifp1QuoteError("Custom ledger cannot verify the original budget reservation");
|
|
1305
|
+
await this.ledger.assertRecovery(id, tx, binding);
|
|
1306
|
+
},
|
|
1307
|
+
completeReservation: async (id, tx, binding) => {
|
|
1308
|
+
if (!this.ledger.complete)
|
|
1309
|
+
throw new Aifp1QuoteError("Custom ledger cannot reconcile the prepared payment");
|
|
1310
|
+
await this.ledger.complete(id, tx, binding);
|
|
1311
|
+
},
|
|
1312
|
+
finalizeFailedReservation: async (id, tx, binding, feeUsd) => {
|
|
1313
|
+
if (!this.ledger.finalizeFailure)
|
|
1314
|
+
throw new Aifp1QuoteError("Custom ledger cannot reconcile proven finalized failure; retain the reservation");
|
|
1315
|
+
await this.ledger.finalizeFailure(id, tx, binding, feeUsd);
|
|
1316
|
+
},
|
|
1219
1317
|
onPaid: ({ merchantId, amountUsd, txRef }) => {
|
|
1220
1318
|
this.spend24h.add(amountUsd);
|
|
1221
1319
|
if (this.telemetry) {
|
|
1222
1320
|
this.reportTelemetry({
|
|
1223
1321
|
kind: "aifp1",
|
|
1224
1322
|
merchant: merchantId,
|
|
1225
|
-
chain: opts.v14?.chain ?? "polygon",
|
|
1323
|
+
chain: opts.solanaV14 ? "solana" : (opts.v14?.chain ?? "polygon"),
|
|
1226
1324
|
cost: amountUsd,
|
|
1227
1325
|
tx: txRef,
|
|
1228
1326
|
});
|
|
@@ -1233,10 +1331,37 @@ export class AiFinPayAgent {
|
|
|
1233
1331
|
}
|
|
1234
1332
|
/** Recover receipt issuance for an existing payment; never sends a transaction. */
|
|
1235
1333
|
async recoverPaidPayment(recovery, opts = {}) {
|
|
1334
|
+
if (recovery.family !== "solana" && recovery.budgetReservationId !== undefined) {
|
|
1335
|
+
const chain = recovery.chain ?? "polygon";
|
|
1336
|
+
await assertPreparedV14Recovery(recovery.quote.settlement_call, { hash: recovery.txRef, serializedTransaction: recovery.serializedTransaction }, chain, this.evmAddress, recovery.quote.quote_id);
|
|
1337
|
+
}
|
|
1236
1338
|
return recoverAifp1Payment(recovery, {
|
|
1237
|
-
|
|
1238
|
-
|
|
1339
|
+
walletIdentity: this.evmAddress.toLowerCase(),
|
|
1340
|
+
payerAddress: recovery.family === "solana" ? this.solanaAddress : this.evmAddress,
|
|
1341
|
+
...(recovery.family === "solana"
|
|
1342
|
+
? {
|
|
1343
|
+
solanaRpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
|
|
1344
|
+
finalizeFailedReservation: async (id, tx, binding, feeUsd) => {
|
|
1345
|
+
if (!this.ledger.finalizeFailure)
|
|
1346
|
+
throw new Aifp1QuoteError("Custom ledger cannot reconcile canonical failure; retain reservation");
|
|
1347
|
+
await this.ledger.finalizeFailure(id, tx, binding, feeUsd);
|
|
1348
|
+
},
|
|
1349
|
+
}
|
|
1350
|
+
: {}),
|
|
1351
|
+
signPaymentAuthorization: (message) => recovery.family === "solana"
|
|
1352
|
+
? Promise.resolve(bs58.encode(nacl.sign.detached(Buffer.from(message), this.inner.secretKey)))
|
|
1353
|
+
: this.evmAccount.signMessage({ message }),
|
|
1239
1354
|
fetchImpl: this.inner.fetchImpl,
|
|
1355
|
+
assertReservation: async (id, tx, binding) => {
|
|
1356
|
+
if (!this.ledger.assertRecovery)
|
|
1357
|
+
throw new Aifp1QuoteError("Custom ledger cannot verify the original budget reservation");
|
|
1358
|
+
await this.ledger.assertRecovery(id, tx, binding);
|
|
1359
|
+
},
|
|
1360
|
+
completeReservation: async (id, tx, binding) => {
|
|
1361
|
+
if (!this.ledger.complete)
|
|
1362
|
+
throw new Aifp1QuoteError("Custom ledger cannot reconcile the prepared payment");
|
|
1363
|
+
await this.ledger.complete(id, tx, binding);
|
|
1364
|
+
},
|
|
1240
1365
|
}, opts);
|
|
1241
1366
|
}
|
|
1242
1367
|
/**
|
|
@@ -1266,9 +1391,18 @@ export class AiFinPayAgent {
|
|
|
1266
1391
|
* Prepaid quota batches for this agent's address, most room first with a
|
|
1267
1392
|
* per-merchant rollup. Non-signing read of retained receipts metadata.
|
|
1268
1393
|
*/
|
|
1394
|
+
async getPaymentHistory(opts = {}) {
|
|
1395
|
+
return getAgentHistory({
|
|
1396
|
+
...opts,
|
|
1397
|
+
address: opts.network === "solana" ? this.solanaAddress : this.evmAddress,
|
|
1398
|
+
fetchImpl: this.inner.fetchImpl,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1269
1401
|
async getQuota(opts = {}) {
|
|
1270
1402
|
return getQuota({
|
|
1271
|
-
address: this.evmAddress,
|
|
1403
|
+
address: opts.network === "solana" ? this.solanaAddress : this.evmAddress,
|
|
1404
|
+
network: opts.network,
|
|
1405
|
+
solanaNetwork: opts.solanaNetwork,
|
|
1272
1406
|
merchantId: opts.merchantId,
|
|
1273
1407
|
includeExhausted: opts.includeExhausted,
|
|
1274
1408
|
baseUrl: opts.baseUrl,
|
|
@@ -1292,29 +1426,53 @@ export class AiFinPayAgent {
|
|
|
1292
1426
|
* with no known price is excluded from the total and listed in
|
|
1293
1427
|
* `unknown_legs` instead of being fabricated.
|
|
1294
1428
|
*/
|
|
1295
|
-
async balance() {
|
|
1429
|
+
async balance(options = {}) {
|
|
1430
|
+
const cluster = options.solanaNetwork;
|
|
1431
|
+
if (cluster !== undefined) {
|
|
1432
|
+
if (cluster !== "mainnet" && cluster !== "devnet")
|
|
1433
|
+
throw new AiFinPayError("Explicit Solana balance cluster is invalid");
|
|
1434
|
+
if ((await solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl).request("getGenesisHash", [])) !==
|
|
1435
|
+
SOLANA_GENESIS[cluster])
|
|
1436
|
+
throw new AiFinPayError("Solana balance RPC does not match the owner-selected cluster");
|
|
1437
|
+
}
|
|
1438
|
+
const mint = cluster ? solanaStableMint(cluster, "USDC") : USDC_SOLANA_MINT;
|
|
1296
1439
|
const [pol, sol] = await Promise.all([
|
|
1297
1440
|
this.tokenUsd("POL", SPLITTER_DEPLOYMENTS.polygon.nativeUsdEnv),
|
|
1298
|
-
|
|
1441
|
+
cluster === "devnet"
|
|
1442
|
+
? Promise.resolve({ usd: null, source: "test-assets-not-valued" })
|
|
1443
|
+
: this.tokenUsd("SOL", "AIFINPAY_SOL_USD"),
|
|
1299
1444
|
]);
|
|
1300
1445
|
const polygon = await this.fetchPolygonNative().catch(() => 0);
|
|
1301
|
-
const solana = await this.fetchSolanaNative().catch(() => 0);
|
|
1302
|
-
const solana_usdc =
|
|
1446
|
+
const solana = cluster ? await this.fetchSolanaNative(true) : await this.fetchSolanaNative().catch(() => 0);
|
|
1447
|
+
const solana_usdc = cluster
|
|
1448
|
+
? await this.fetchSolanaUsdc(mint, true)
|
|
1449
|
+
: await this.fetchSolanaUsdc(mint).catch(() => 0);
|
|
1303
1450
|
const polygon_usdc = await this.fetchPolygonUsdc().catch(() => 0);
|
|
1304
1451
|
const unknown_legs = [];
|
|
1305
|
-
let agent_balance_usd = solana_usdc + polygon_usdc; //
|
|
1452
|
+
let agent_balance_usd = (cluster === "devnet" ? 0 : solana_usdc) + polygon_usdc; // Devnet tokens have no fiat value.
|
|
1306
1453
|
if (Number.isFinite(pol.usd))
|
|
1307
1454
|
agent_balance_usd += polygon * pol.usd;
|
|
1308
1455
|
else
|
|
1309
1456
|
unknown_legs.push("pol");
|
|
1310
|
-
if (Number.isFinite(sol.usd))
|
|
1457
|
+
if (typeof sol.usd === "number" && Number.isFinite(sol.usd))
|
|
1311
1458
|
agent_balance_usd += solana * sol.usd;
|
|
1312
1459
|
else
|
|
1313
1460
|
unknown_legs.push("sol");
|
|
1314
1461
|
return {
|
|
1315
1462
|
agent_balance_usd,
|
|
1316
1463
|
chains: {
|
|
1317
|
-
solana: {
|
|
1464
|
+
solana: {
|
|
1465
|
+
sol: solana,
|
|
1466
|
+
usdc: solana_usdc,
|
|
1467
|
+
msecco_balance: 0,
|
|
1468
|
+
network: cluster ?? null,
|
|
1469
|
+
usdc_mint: mint,
|
|
1470
|
+
valuation: cluster === "devnet"
|
|
1471
|
+
? "test-assets-not-valued"
|
|
1472
|
+
: cluster === "mainnet"
|
|
1473
|
+
? "spot-estimate"
|
|
1474
|
+
: "unverified-owner-rpc/mainnet-mint",
|
|
1475
|
+
},
|
|
1318
1476
|
polygon: { matic: polygon, usdc: polygon_usdc, msecco_balance: 0 },
|
|
1319
1477
|
},
|
|
1320
1478
|
spend_24h_usd: this.spend24h.total24h(),
|
|
@@ -1331,7 +1489,15 @@ export class AiFinPayAgent {
|
|
|
1331
1489
|
});
|
|
1332
1490
|
return Number(wei) / 1e18;
|
|
1333
1491
|
}
|
|
1334
|
-
async fetchSolanaNative() {
|
|
1492
|
+
async fetchSolanaNative(strict = false) {
|
|
1493
|
+
if (strict) {
|
|
1494
|
+
const result = (await solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl).request("getBalance", [
|
|
1495
|
+
this.solanaAddress,
|
|
1496
|
+
]));
|
|
1497
|
+
if (!result || !Number.isSafeInteger(result.value) || result.value < 0)
|
|
1498
|
+
throw new AiFinPayError("Solana balance RPC returned an invalid native balance");
|
|
1499
|
+
return result.value / 1e9;
|
|
1500
|
+
}
|
|
1335
1501
|
// Raw JSON-RPC; honours the constructor's solanaRpc option (previously
|
|
1336
1502
|
// re-read env here with a different default endpoint).
|
|
1337
1503
|
const rpc = this.solanaRpc;
|
|
@@ -1355,10 +1521,29 @@ export class AiFinPayAgent {
|
|
|
1355
1521
|
* Sum SPL USDC balances across all token accounts owned by the agent's
|
|
1356
1522
|
* Solana pubkey. Uses raw JSON-RPC `getTokenAccountsByOwner` so we don't
|
|
1357
1523
|
* pull `@solana/spl-token` as a dep. Returns 0 (never throws) when the
|
|
1358
|
-
* RPC is unreachable or returns nothing
|
|
1359
|
-
*
|
|
1524
|
+
* RPC is unreachable or returns nothing in the legacy, unverified context.
|
|
1525
|
+
* An explicit cluster requires a successful, well-formed balance read.
|
|
1360
1526
|
*/
|
|
1361
|
-
async fetchSolanaUsdc() {
|
|
1527
|
+
async fetchSolanaUsdc(mint = USDC_SOLANA_MINT, strict = false) {
|
|
1528
|
+
if (strict) {
|
|
1529
|
+
const result = (await solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl).request("getTokenAccountsByOwner", [
|
|
1530
|
+
this.solanaAddress,
|
|
1531
|
+
{ mint },
|
|
1532
|
+
{ encoding: "jsonParsed" },
|
|
1533
|
+
]));
|
|
1534
|
+
if (!result || !Array.isArray(result.value))
|
|
1535
|
+
throw new AiFinPayError("Solana balance RPC returned invalid token accounts");
|
|
1536
|
+
let total = 0;
|
|
1537
|
+
for (const account of result.value) {
|
|
1538
|
+
const ui = account?.account?.data?.parsed?.info?.tokenAmount?.uiAmount;
|
|
1539
|
+
if (typeof ui !== "number" || !Number.isFinite(ui) || ui < 0)
|
|
1540
|
+
throw new AiFinPayError("Solana balance RPC returned an invalid token balance");
|
|
1541
|
+
total += ui;
|
|
1542
|
+
if (!Number.isFinite(total))
|
|
1543
|
+
throw new AiFinPayError("Solana token balance overflow");
|
|
1544
|
+
}
|
|
1545
|
+
return total;
|
|
1546
|
+
}
|
|
1362
1547
|
const rpc = this.solanaRpc;
|
|
1363
1548
|
try {
|
|
1364
1549
|
const r = await this.inner.fetchImpl(rpc, {
|
|
@@ -1368,7 +1553,7 @@ export class AiFinPayAgent {
|
|
|
1368
1553
|
jsonrpc: "2.0",
|
|
1369
1554
|
id: 1,
|
|
1370
1555
|
method: "getTokenAccountsByOwner",
|
|
1371
|
-
params: [this.solanaAddress, { mint
|
|
1556
|
+
params: [this.solanaAddress, { mint }, { encoding: "jsonParsed" }],
|
|
1372
1557
|
}),
|
|
1373
1558
|
});
|
|
1374
1559
|
if (!r.ok)
|