@aifinpay/agent 2.3.2 → 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.
Files changed (68) hide show
  1. package/PAYMENT_RECEIPTS.md +194 -19
  2. package/README.md +37 -17
  3. package/dist/agentHistory.d.ts +9 -0
  4. package/dist/agentHistory.d.ts.map +1 -1
  5. package/dist/agentHistory.js +110 -16
  6. package/dist/agentHistory.js.map +1 -1
  7. package/dist/aifp1.d.ts +124 -17
  8. package/dist/aifp1.d.ts.map +1 -1
  9. package/dist/aifp1.js +580 -82
  10. package/dist/aifp1.js.map +1 -1
  11. package/dist/deploymentErrors.d.ts +5 -0
  12. package/dist/deploymentErrors.d.ts.map +1 -0
  13. package/dist/deploymentErrors.js +8 -0
  14. package/dist/deploymentErrors.js.map +1 -0
  15. package/dist/deploymentResolver.d.ts +2 -3
  16. package/dist/deploymentResolver.d.ts.map +1 -1
  17. package/dist/deploymentResolver.js +2 -6
  18. package/dist/deploymentResolver.js.map +1 -1
  19. package/dist/generated/solanaV14Deployments.generated.d.ts +1 -0
  20. package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -1
  21. package/dist/generated/solanaV14Deployments.generated.js +4 -2
  22. package/dist/generated/solanaV14Deployments.generated.js.map +1 -1
  23. package/dist/index.d.ts +8 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/paymentChains.d.ts +95 -0
  28. package/dist/paymentChains.d.ts.map +1 -0
  29. package/dist/paymentChains.js +116 -0
  30. package/dist/paymentChains.js.map +1 -0
  31. package/dist/settlement.d.ts +5 -5
  32. package/dist/settlementSolanaV14.d.ts +135 -0
  33. package/dist/settlementSolanaV14.d.ts.map +1 -0
  34. package/dist/settlementSolanaV14.js +733 -0
  35. package/dist/settlementSolanaV14.js.map +1 -0
  36. package/dist/settlementV14.d.ts +9 -2
  37. package/dist/settlementV14.d.ts.map +1 -1
  38. package/dist/settlementV14.js +81 -14
  39. package/dist/settlementV14.js.map +1 -1
  40. package/dist/solanaDeploymentResolver.d.ts +2 -1
  41. package/dist/solanaDeploymentResolver.d.ts.map +1 -1
  42. package/dist/solanaDeploymentResolver.js +1 -32
  43. package/dist/solanaDeploymentResolver.js.map +1 -1
  44. package/dist/spendLedger.d.ts +74 -33
  45. package/dist/spendLedger.d.ts.map +1 -1
  46. package/dist/spendLedger.js +523 -130
  47. package/dist/spendLedger.js.map +1 -1
  48. package/dist/unifiedAgent.d.ts +17 -8
  49. package/dist/unifiedAgent.d.ts.map +1 -1
  50. package/dist/unifiedAgent.js +222 -37
  51. package/dist/unifiedAgent.js.map +1 -1
  52. package/package.json +3 -3
  53. package/dist/cli.d.ts +0 -3
  54. package/dist/cli.d.ts.map +0 -1
  55. package/dist/cli.js +0 -127
  56. package/dist/cli.js.map +0 -1
  57. package/dist/solanaV14Deployments.generated.d.ts +0 -35
  58. package/dist/solanaV14Deployments.generated.d.ts.map +0 -1
  59. package/dist/solanaV14Deployments.generated.js +0 -61
  60. package/dist/solanaV14Deployments.generated.js.map +0 -1
  61. package/dist/splitterRoutes.generated.d.ts +0 -22
  62. package/dist/splitterRoutes.generated.d.ts.map +0 -1
  63. package/dist/splitterRoutes.generated.js +0 -459
  64. package/dist/splitterRoutes.generated.js.map +0 -1
  65. package/dist/v14Deployments.generated.d.ts +0 -53
  66. package/dist/v14Deployments.generated.d.ts.map +0 -1
  67. package/dist/v14Deployments.generated.js +0 -89
  68. package/dist/v14Deployments.generated.js.map +0 -1
@@ -0,0 +1,733 @@
1
+ /** Solana v1.4: fixed signed wire, owner-selected cluster, classic SPL only.
2
+ * Public entry points separately require registry availability. A timeout or
3
+ * expired blockhash is never permission to replace an already signed purchase. */
4
+ import { createHash } from "node:crypto";
5
+ import bs58 from "bs58";
6
+ import { secp256k1 } from "@noble/curves/secp256k1";
7
+ import { PublicKey, Transaction, TransactionInstruction, SystemProgram, ComputeBudgetProgram, } from "@solana/web3.js";
8
+ import { keccak256, stringToHex } from "viem";
9
+ import { settlementHttp } from "./settlementHttp.js";
10
+ import { SOLANA_V14_DEPLOYMENTS, } from "./generated/solanaV14Deployments.generated.js";
11
+ import { resolveSolanaDeployment } from "./solanaDeploymentResolver.js";
12
+ const ZERO = "11111111111111111111111111111111";
13
+ export const SOLANA_TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
14
+ const ATA_PROGRAM = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
15
+ const LOADER = "BPFLoaderUpgradeab1e11111111111111111111111";
16
+ export const SOLANA_GENESIS = Object.freeze({
17
+ mainnet: "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
18
+ devnet: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG",
19
+ });
20
+ // Official Circle contract addresses / Tether WDK Solana reference. These are
21
+ // identity pins, not a claim that the runtime TokenList accepts the mint.
22
+ export const SOLANA_STABLE_ASSETS = Object.freeze({
23
+ mainnet: Object.freeze({
24
+ USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
25
+ USDT: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
26
+ }),
27
+ devnet: Object.freeze({ USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" }),
28
+ });
29
+ export const SOLANA_QUOTE_FIELDS = [
30
+ "payer",
31
+ "merchant",
32
+ "token",
33
+ "grossAmount",
34
+ "ipCreator",
35
+ "validUntil",
36
+ "orderIdHash",
37
+ "nonce",
38
+ "routeId",
39
+ ];
40
+ const ACCOUNT_DISC = {
41
+ config: [155, 12, 170, 224, 30, 250, 204, 130],
42
+ payerNonce: [239, 33, 176, 46, 128, 49, 65, 40],
43
+ consumedNonce: [203, 122, 100, 36, 241, 214, 161, 246],
44
+ profiles: [226, 221, 186, 252, 104, 74, 245, 98],
45
+ tokenList: [145, 167, 153, 173, 5, 187, 157, 150],
46
+ };
47
+ const NATIVE_DISC = Buffer.from([117, 220, 69, 41, 231, 241, 119, 57]);
48
+ const STABLE_DISC = Buffer.from([122, 84, 19, 134, 147, 115, 154, 207]);
49
+ export class SolanaV14Error extends Error {
50
+ code;
51
+ prepared;
52
+ actualFeeLamports;
53
+ constructor(code, message, prepared, actualFeeLamports) {
54
+ super(message);
55
+ this.code = code;
56
+ this.prepared = prepared;
57
+ this.actualFeeLamports = actualFeeLamports;
58
+ this.name = "SolanaV14Error";
59
+ }
60
+ }
61
+ /** Exact admission-rate accounting; round fees upwards to whole USD micros. */
62
+ export function solanaLamportCostUsd(lamports, rate) {
63
+ if (typeof rate !== "string" || !/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,18})?$/.test(rate) || lamports < 0n)
64
+ fail("invalid exact SOL/USD admission rate");
65
+ const [whole, fraction = ""] = rate.split(".");
66
+ const numerator = BigInt(whole + fraction), denominator = 10n ** BigInt(fraction.length) * 1000000000n;
67
+ if (numerator <= 0n || Number(rate) >= 100000)
68
+ fail("invalid SOL/USD rate");
69
+ const micros = (lamports * numerator * 1000000n + denominator - 1n) / denominator;
70
+ if (micros > BigInt(Number.MAX_SAFE_INTEGER))
71
+ fail("unsafe USD fee accounting");
72
+ return Number(micros) / 1e6;
73
+ }
74
+ function fail(message) {
75
+ throw new SolanaV14Error("SOLANA_V14_INVALID", message);
76
+ }
77
+ function key(value) {
78
+ try {
79
+ const p = new PublicKey(String(value));
80
+ if (p.toBase58() !== value)
81
+ fail("noncanonical Solana key");
82
+ return p;
83
+ }
84
+ catch {
85
+ return fail("invalid Solana key");
86
+ }
87
+ }
88
+ function integer(value, bits = 64) {
89
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value))
90
+ fail("amount must be a canonical integer string");
91
+ const n = BigInt(value);
92
+ if (n >= 2n ** BigInt(bits))
93
+ fail("integer overflow");
94
+ return n;
95
+ }
96
+ function le(value) {
97
+ const b = Buffer.alloc(8);
98
+ b.writeBigUInt64LE(integer(value));
99
+ return b;
100
+ }
101
+ function hex32(value) {
102
+ if (!/^0x[0-9a-f]{64}$/.test(value))
103
+ fail("invalid canonical bytes32");
104
+ return Buffer.from(value.slice(2), "hex");
105
+ }
106
+ export function encodeSolanaV14Quote(q) {
107
+ const expiry = integer(q.validUntil, 63);
108
+ const exp = Buffer.alloc(8);
109
+ exp.writeBigInt64LE(expiry);
110
+ return Buffer.concat([
111
+ key(q.payer).toBuffer(),
112
+ key(q.merchant).toBuffer(),
113
+ key(q.token).toBuffer(),
114
+ le(q.grossAmount),
115
+ key(q.ipCreator).toBuffer(),
116
+ exp,
117
+ hex32(q.orderIdHash),
118
+ le(q.nonce),
119
+ hex32(q.routeId),
120
+ ]);
121
+ }
122
+ export function solanaV14Digest(programId, q) {
123
+ return createHash("sha256")
124
+ .update(Buffer.concat([Buffer.from("AiFinPay-Solana-v1.4"), key(programId).toBuffer(), encodeSolanaV14Quote(q)]))
125
+ .digest();
126
+ }
127
+ export function solanaV14PaymentId(q) {
128
+ return "0x" + createHash("sha256").update(encodeSolanaV14Quote(q)).digest("hex");
129
+ }
130
+ /** Zero-fee creator slot is an unused writable account, not SystemProgram.
131
+ * The signed creator remains zero. No lamports are sent to this placeholder. */
132
+ export function solanaCreatorPlaceholder(programId, payer) {
133
+ return new PublicKey(createHash("sha256")
134
+ .update(Buffer.concat([
135
+ Buffer.from("AiFinPay Solana creator placeholder v1"),
136
+ key(programId).toBuffer(),
137
+ key(payer).toBuffer(),
138
+ ]))
139
+ .digest()).toBase58();
140
+ }
141
+ export function solanaStableMint(network, asset) {
142
+ const table = SOLANA_STABLE_ASSETS[network];
143
+ const mint = Object.hasOwn(table, asset) ? table[asset] : undefined;
144
+ if (!mint)
145
+ fail("asset is not independently pinned for this Solana network");
146
+ return mint;
147
+ }
148
+ /** Immutable inventory is useful for encoding/recovery, never activates a rail. */
149
+ export function solanaV14Inventory(environment, network) {
150
+ const d = SOLANA_V14_DEPLOYMENTS[network];
151
+ if (!d || d.environment !== environment || !["mainnet", "devnet"].includes(network))
152
+ fail("Solana environment/network mismatch");
153
+ return d;
154
+ }
155
+ /** Production high-level authorizer; preserves the canonical disabled gate. */
156
+ export function authorizedSolanaV14Inventory(environment, network) {
157
+ return resolveSolanaDeployment({ environment, network, version: "v1.4" }).deployment;
158
+ }
159
+ export function solanaV14Pdas(programId, payer, nonce) {
160
+ const program = key(programId), p = key(payer).toBuffer();
161
+ const pda = (...seeds) => PublicKey.findProgramAddressSync(seeds, program)[0];
162
+ return {
163
+ config: pda(Buffer.from("config")),
164
+ profiles: pda(Buffer.from("profiles-index")),
165
+ tokenList: pda(Buffer.from("token-list")),
166
+ payerNonce: pda(Buffer.from("payer-nonce"), p),
167
+ consumedNonce: pda(Buffer.from("consumed-nonce"), p, le(nonce)),
168
+ };
169
+ }
170
+ export function validateSolanaV14Call(call, d, payer, orderId, historical = false) {
171
+ const q = call?.args?.quote;
172
+ if (!q ||
173
+ call.chain !== "solana" ||
174
+ call.network !== d.network ||
175
+ call.contract !== d.programId ||
176
+ call.idl_sha256 !== d.idl.sha256 ||
177
+ call.splitter_version !== "1.4" ||
178
+ call.route !== "merchant-aifp1" ||
179
+ call.arg_encoding !== "borsh-quote+signature" ||
180
+ call.bound_to_payer !== true ||
181
+ JSON.stringify(call.field_order) !== JSON.stringify(SOLANA_QUOTE_FIELDS) ||
182
+ q.payer !== payer ||
183
+ q.ipCreator !== ZERO ||
184
+ q.orderIdHash !== keccak256(stringToHex(orderId)) ||
185
+ q.routeId !== keccak256(stringToHex("merchant-aifp1")) ||
186
+ integer(q.grossAmount) < 100n ||
187
+ q.merchant === ZERO ||
188
+ q.merchant === payer ||
189
+ !/^0x[0-9a-fA-F]{130}$/.test(call.args.signature))
190
+ fail("Solana signed-call binding mismatch");
191
+ encodeSolanaV14Quote(q);
192
+ const native = call.asset === "SOL";
193
+ if (call.function !== (native ? "settle_native" : "settle_stable") ||
194
+ q.token !== (native ? ZERO : solanaStableMint(d.network, call.asset)) ||
195
+ (native ? call.value_lamports !== q.grossAmount : call.value_lamports !== undefined) ||
196
+ call.approval !== undefined)
197
+ fail("Solana asset/function/value mismatch");
198
+ const sig = Buffer.from(call.args.signature.slice(2), "hex");
199
+ if (![27, 28].includes(sig[64]) || secp256k1.Signature.fromCompact(sig.subarray(0, 64)).hasHighS())
200
+ fail("invalid Solana quote signature");
201
+ const now = BigInt(Math.floor(Date.now() / 1000));
202
+ if (!historical && (integer(q.validUntil, 63) <= now || integer(q.validUntil, 63) > now + 3600n))
203
+ fail("Solana quote expired or exceeds lifetime");
204
+ }
205
+ export function solanaV14Instruction(call, treasury) {
206
+ const q = call.args.quote, p = solanaV14Pdas(call.contract, q.payer, q.nonce), native = call.asset === "SOL";
207
+ const a = native
208
+ ? {
209
+ config: p.config.toBase58(),
210
+ payerNonce: p.payerNonce.toBase58(),
211
+ consumedNonce: p.consumedNonce.toBase58(),
212
+ payer: q.payer,
213
+ merchant: q.merchant,
214
+ treasury,
215
+ ipCreator: solanaCreatorPlaceholder(call.contract, q.payer),
216
+ profiles: p.profiles.toBase58(),
217
+ systemProgram: ZERO,
218
+ }
219
+ : {
220
+ config: p.config.toBase58(),
221
+ payerNonce: p.payerNonce.toBase58(),
222
+ consumedNonce: p.consumedNonce.toBase58(),
223
+ payer: q.payer,
224
+ tokenList: p.tokenList.toBase58(),
225
+ mint: q.token,
226
+ profiles: p.profiles.toBase58(),
227
+ tokenProgram: SOLANA_TOKEN_PROGRAM,
228
+ systemProgram: ZERO,
229
+ };
230
+ if (call.treasury_owner !== treasury ||
231
+ JSON.stringify(Object.keys(call.accounts ?? {}).sort()) !== JSON.stringify(Object.keys(a).sort()) ||
232
+ Object.entries(a).some(([name, address]) => call.accounts[name] !== address))
233
+ fail("Solana account metas disagree with independently derived accounts");
234
+ const writable = native
235
+ ? [true, true, true, true, true, true, true, true, false]
236
+ : [false, true, true, true, false, false, true, false, false];
237
+ const keys = Object.values(a).map((address, i) => ({
238
+ pubkey: key(address),
239
+ isSigner: i === 3,
240
+ isWritable: writable[i],
241
+ }));
242
+ const remaining = native
243
+ ? []
244
+ : [associatedToken(q.payer, q.token), associatedToken(q.merchant, q.token), associatedToken(treasury, q.token)].map((p) => p.toBase58());
245
+ if (JSON.stringify(call.remaining_accounts ?? []) !== JSON.stringify(remaining))
246
+ fail("Solana SPL remaining account order mismatch");
247
+ keys.push(...remaining.map((address) => ({ pubkey: key(address), isSigner: false, isWritable: true })));
248
+ return new TransactionInstruction({
249
+ programId: key(call.contract),
250
+ keys,
251
+ data: Buffer.concat([
252
+ native ? NATIVE_DISC : STABLE_DISC,
253
+ le(q.nonce),
254
+ encodeSolanaV14Quote(q),
255
+ Buffer.from(call.args.signature.slice(2), "hex"),
256
+ ]),
257
+ });
258
+ }
259
+ export function associatedToken(owner, mint) {
260
+ return PublicKey.findProgramAddressSync([key(owner).toBuffer(), key(SOLANA_TOKEN_PROGRAM).toBuffer(), key(mint).toBuffer()], ATA_PROGRAM)[0];
261
+ }
262
+ function createAta(payer, owner, mint) {
263
+ return new TransactionInstruction({
264
+ programId: ATA_PROGRAM,
265
+ data: Buffer.from([1]),
266
+ keys: [
267
+ { pubkey: key(payer), isSigner: true, isWritable: true },
268
+ { pubkey: associatedToken(owner, mint), isSigner: false, isWritable: true },
269
+ { pubkey: key(owner), isSigner: false, isWritable: false },
270
+ { pubkey: key(mint), isSigner: false, isWritable: false },
271
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
272
+ { pubkey: key(SOLANA_TOKEN_PROGRAM), isSigner: false, isWritable: false },
273
+ ],
274
+ });
275
+ }
276
+ export function solanaV14Rpc(url, fetchImpl = fetch) {
277
+ let id = 0;
278
+ return {
279
+ async request(method, params) {
280
+ const { response, text } = await settlementHttp(fetchImpl, url, {
281
+ method: "POST",
282
+ headers: { "content-type": "application/json" },
283
+ body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }),
284
+ });
285
+ const data = JSON.parse(text);
286
+ if (!response.ok || data.error || !Object.hasOwn(data, "result"))
287
+ fail("Solana RPC evidence unavailable");
288
+ return data.result;
289
+ },
290
+ };
291
+ }
292
+ function rpcInt(n) {
293
+ if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 0)
294
+ fail("unsafe Solana RPC integer");
295
+ return BigInt(n);
296
+ }
297
+ function accountBytes(a, owner, disc, length) {
298
+ if (!a ||
299
+ a.owner !== owner ||
300
+ a.executable !== false ||
301
+ !Array.isArray(a.data) ||
302
+ a.data.length !== 2 ||
303
+ typeof a.data[0] !== "string" ||
304
+ a.data[1] !== "base64")
305
+ fail("Solana account owner/encoding mismatch");
306
+ rpcInt(a.lamports);
307
+ const bytes = Buffer.from(a.data[0], "base64");
308
+ if (bytes.toString("base64") !== a.data[0] ||
309
+ (length !== undefined && bytes.length !== length) ||
310
+ (disc && !bytes.subarray(0, 8).equals(Buffer.from(disc))))
311
+ fail("Solana account layout mismatch");
312
+ return bytes;
313
+ }
314
+ function pub(bytes, offset) {
315
+ return new PublicKey(bytes.subarray(offset, offset + 32)).toBase58();
316
+ }
317
+ async function accounts(rpc, addresses) {
318
+ const result = (await rpc.request("getMultipleAccounts", [
319
+ addresses,
320
+ { encoding: "base64", commitment: "finalized" },
321
+ ]));
322
+ if (!result || !Array.isArray(result.value) || result.value.length !== addresses.length)
323
+ fail("missing Solana accounts");
324
+ return result.value;
325
+ }
326
+ function assertConfigQuoteSigner(call, config) {
327
+ const signature = Buffer.from(call.args.signature.slice(2), "hex");
328
+ const recovered = secp256k1.Signature.fromCompact(signature.subarray(0, 64))
329
+ .addRecoveryBit(signature[64] - 27)
330
+ .recoverPublicKey(solanaV14Digest(call.contract, call.args.quote))
331
+ .toRawBytes(false)
332
+ .subarray(1);
333
+ if (!Buffer.from(recovered).equals(config.subarray(40, 104)))
334
+ fail("quote signer does not match independently read Config");
335
+ }
336
+ /** Read-only verification for a locally unsent expired admission; not a payment proof. */
337
+ export async function verifyHistoricalSolanaV14Quote(call, ctx) {
338
+ const { rpc, deployment: d } = ctx;
339
+ validateSolanaV14Call(call, d, ctx.payer, ctx.orderId, true);
340
+ solanaV14Instruction(call, call.treasury_owner);
341
+ if ((await rpc.request("getGenesisHash", [])) !== SOLANA_GENESIS[d.network])
342
+ fail("Solana RPC network mismatch");
343
+ const p = solanaV14Pdas(d.programId, ctx.payer, call.args.quote.nonce);
344
+ const [program, config] = await accounts(rpc, [d.programId, p.config.toBase58()]);
345
+ if (program?.executable !== true || program.owner !== LOADER)
346
+ fail("Solana program runtime mismatch");
347
+ const c = accountBytes(config, d.programId, ACCOUNT_DISC.config, 234);
348
+ if (c[232] !== PublicKey.findProgramAddressSync([Buffer.from("config")], key(d.programId))[1] ||
349
+ pub(c, 168) !== p.tokenList.toBase58() ||
350
+ pub(c, 200) !== p.profiles.toBase58())
351
+ fail("inconsistent Solana Config");
352
+ assertConfigQuoteSigner(call, c);
353
+ }
354
+ /** Preflight performs no payer signature and no write. Reserve USD after this
355
+ * succeeds; sign/persist/broadcast the exact prepared message afterwards. */
356
+ export async function prepareSolanaV14Settlement(call, ctx) {
357
+ const { rpc, deployment: d, payer, maxFeeLamports } = ctx;
358
+ validateSolanaV14Call(call, d, payer, ctx.orderId);
359
+ if (typeof maxFeeLamports !== "bigint" || maxFeeLamports <= 0n)
360
+ fail("explicit Solana fee plus rent budget required");
361
+ if ((await rpc.request("getGenesisHash", [])) !== SOLANA_GENESIS[d.network])
362
+ fail("Solana RPC network mismatch");
363
+ const q = call.args.quote, p = solanaV14Pdas(d.programId, payer, q.nonce);
364
+ const placeholder = solanaCreatorPlaceholder(d.programId, payer);
365
+ const [program, config, profiles, pn, cn, payerAccount, placeholderAccount] = await accounts(rpc, [
366
+ d.programId,
367
+ p.config.toBase58(),
368
+ p.profiles.toBase58(),
369
+ p.payerNonce.toBase58(),
370
+ p.consumedNonce.toBase58(),
371
+ payer,
372
+ placeholder,
373
+ ]);
374
+ if (program?.executable !== true ||
375
+ program.owner !== LOADER ||
376
+ !payerAccount ||
377
+ payerAccount.owner !== ZERO ||
378
+ payerAccount.executable !== false ||
379
+ (placeholderAccount && placeholderAccount.executable !== false))
380
+ fail("Solana program/payer/creator runtime mismatch");
381
+ rpcInt(program.lamports);
382
+ // Existing creator placeholders must also be unused ordinary accounts. This
383
+ // excludes readonly sysvars/native programs as well as executable accounts.
384
+ if (call.asset === "SOL" && placeholderAccount)
385
+ accountBytes(placeholderAccount, ZERO, undefined, 0);
386
+ const bump = (...seeds) => PublicKey.findProgramAddressSync(seeds, key(d.programId))[1];
387
+ const c = accountBytes(config, d.programId, ACCOUNT_DISC.config, 234);
388
+ if (c[232] !== bump(Buffer.from("config")) ||
389
+ c[233] !== 0 ||
390
+ pub(c, 168) !== p.tokenList.toBase58() ||
391
+ pub(c, 200) !== p.profiles.toBase58())
392
+ fail("paused or inconsistent Solana Config");
393
+ assertConfigQuoteSigner(call, c);
394
+ const r = accountBytes(profiles, d.programId, ACCOUNT_DISC.profiles);
395
+ if (r.length < 14)
396
+ fail("truncated ProfilesIndex");
397
+ const count = r.readUInt32LE(8);
398
+ if (count > 32 ||
399
+ r.length < 12 + count * 77 + 2 ||
400
+ r[12 + count * 77] !== count ||
401
+ r[13 + count * 77] !== bump(Buffer.from("profiles-index")))
402
+ fail("invalid ProfilesIndex bounds/bump");
403
+ let treasury;
404
+ for (let i = 0; i < count; i++) {
405
+ const o = 12 + i * 77;
406
+ if (r.subarray(o, o + 32).equals(hex32(q.routeId))) {
407
+ if (treasury !== undefined || r.readUInt16LE(o + 32) !== 100 || r.readUInt16LE(o + 34) !== 0 || r[o + 36] !== 1)
408
+ fail("Solana merchant profile mismatch");
409
+ const routeTreasury = pub(r, o + 45);
410
+ treasury = routeTreasury === ZERO ? pub(c, 136) : routeTreasury;
411
+ }
412
+ }
413
+ if (!treasury ||
414
+ treasury === payer ||
415
+ treasury === q.merchant ||
416
+ treasury === ZERO ||
417
+ Object.values(p).some((address) => [treasury, q.merchant].includes(address.toBase58())))
418
+ fail("invalid Solana recipients/profile");
419
+ const effectiveTreasury = treasury;
420
+ if (pn) {
421
+ const b = accountBytes(pn, d.programId, ACCOUNT_DISC.payerNonce, 49);
422
+ if (pub(b, 8) !== payer ||
423
+ b.readBigUInt64LE(40) !== integer(q.nonce) ||
424
+ b[48] !== bump(Buffer.from("payer-nonce"), key(payer).toBuffer()))
425
+ fail("Solana payer nonce mismatch");
426
+ }
427
+ else if (q.nonce !== "0")
428
+ fail("Solana nonce account missing");
429
+ if (cn)
430
+ fail("Solana nonce marker already exists; recover original purchase");
431
+ const rent = async (size) => rpcInt(await rpc.request("getMinimumBalanceForRentExemption", [size, { commitment: "finalized" }]));
432
+ let rentTotal = (pn ? 0n : await rent(49)) + (await rent(50));
433
+ const tx = new Transaction();
434
+ if (call.asset !== "SOL") {
435
+ const [tl, mint, ...tokenAccounts] = await accounts(rpc, [p.tokenList.toBase58(), q.token].concat([payer, q.merchant, effectiveTreasury].map((owner) => associatedToken(owner, q.token).toBase58())));
436
+ const list = accountBytes(tl, d.programId, ACCOUNT_DISC.tokenList);
437
+ if (list.length < 45)
438
+ fail("truncated TokenList");
439
+ const n = list.readUInt32LE(40);
440
+ if (n > 16 ||
441
+ list.length < 44 + n * 32 + 1 ||
442
+ list[44 + n * 32] !== bump(Buffer.from("token-list")) ||
443
+ !Array.from({ length: n }, (_, i) => pub(list, 44 + i * 32)).includes(q.token))
444
+ fail("mint is not allowed by current TokenList");
445
+ const m = accountBytes(mint, SOLANA_TOKEN_PROGRAM, undefined, 82);
446
+ if (m[44] !== 6 || m[45] !== 1)
447
+ fail("mint decimals/state mismatch");
448
+ for (const [i, owner] of [payer, q.merchant, effectiveTreasury].entries()) {
449
+ const a = tokenAccounts[i];
450
+ if (!a) {
451
+ if (i === 0)
452
+ fail("payer SPL token account is missing");
453
+ tx.add(createAta(payer, owner, q.token));
454
+ rentTotal += await rent(165);
455
+ }
456
+ else {
457
+ const b = accountBytes(a, SOLANA_TOKEN_PROGRAM, undefined, 165);
458
+ if (pub(b, 0) !== q.token ||
459
+ pub(b, 32) !== owner ||
460
+ b[108] !== 1 ||
461
+ (i === 0 && b.readBigUInt64LE(64) < integer(q.grossAmount)))
462
+ fail("SPL account owner/mint/state/balance mismatch");
463
+ }
464
+ }
465
+ }
466
+ if ([payer, q.merchant, effectiveTreasury, d.programId, ZERO, ...Object.values(p).map((v) => v.toBase58())].includes(placeholder))
467
+ fail("Solana creator placeholder collision");
468
+ const settle = solanaV14Instruction(call, effectiveTreasury);
469
+ tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), settle);
470
+ const latest = (await rpc.request("getLatestBlockhash", [{ commitment: "finalized" }]));
471
+ key(latest?.value?.blockhash);
472
+ rpcInt(latest?.value?.lastValidBlockHeight);
473
+ tx.recentBlockhash = latest.value.blockhash;
474
+ tx.feePayer = key(payer);
475
+ const sim = (await rpc.request("simulateTransaction", [
476
+ tx.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64"),
477
+ { encoding: "base64", sigVerify: false, commitment: "finalized" },
478
+ ]));
479
+ if (!sim?.value || sim.value.err !== null)
480
+ fail("Solana simulation failed");
481
+ const units = rpcInt(sim.value.unitsConsumed);
482
+ if (units <= 0n || units > 1272727n)
483
+ fail("missing/unsafe Solana compute estimate");
484
+ tx.instructions[tx.instructions.indexOf(tx.instructions.find((i) => i.programId.equals(ComputeBudgetProgram.programId)))] = ComputeBudgetProgram.setComputeUnitLimit({ units: Number((units * 110n + 99n) / 100n) });
485
+ const exactSim = (await rpc.request("simulateTransaction", [
486
+ tx.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64"),
487
+ { encoding: "base64", sigVerify: false, commitment: "finalized" },
488
+ ]));
489
+ if (!exactSim?.value ||
490
+ exactSim.value.err !== null ||
491
+ rpcInt(exactSim.value.unitsConsumed) === 0n ||
492
+ rpcInt(exactSim.value.unitsConsumed) >
493
+ BigInt(tx.instructions.find((i) => i.programId.equals(ComputeBudgetProgram.programId)).data.readUInt32LE(1)))
494
+ fail("Exact final Solana message simulation failed");
495
+ const fee = (await rpc.request("getFeeForMessage", [
496
+ tx.serializeMessage().toString("base64"),
497
+ { commitment: "finalized" },
498
+ ]));
499
+ if (fee?.value === null || fee?.value === undefined)
500
+ fail("Solana fee evidence unavailable");
501
+ const cost = rpcInt(fee.value) + rentTotal;
502
+ if (cost > maxFeeLamports ||
503
+ rpcInt(payerAccount.lamports) < cost + (call.asset === "SOL" ? integer(q.grossAmount) : 0n))
504
+ fail("Solana fee/rent cap or native balance exceeded");
505
+ return {
506
+ call,
507
+ deployment: d,
508
+ payer,
509
+ orderId: ctx.orderId,
510
+ transaction: tx,
511
+ transactionFeeLamports: rpcInt(fee.value),
512
+ feeRentLamports: cost,
513
+ maxFeeLamports,
514
+ lastValidBlockHeight: latest.value.lastValidBlockHeight,
515
+ createdAtMs: Date.now(),
516
+ };
517
+ }
518
+ /** Signed bytes and sole signature are persisted before the single send. */
519
+ export async function executeSolanaV14Settlement(plan, ctx) {
520
+ if (ctx.keypair.publicKey.toBase58() !== plan.payer ||
521
+ Date.now() - plan.createdAtMs > 15_000 ||
522
+ plan.feeRentLamports > plan.maxFeeLamports)
523
+ fail("stale or unauthorized Solana preparation");
524
+ const tx = plan.transaction;
525
+ tx.sign(ctx.keypair);
526
+ const prepared = {
527
+ family: "solana",
528
+ hash: bs58.encode(tx.signature),
529
+ serializedTransactionBase64: tx.serialize().toString("base64"),
530
+ network: plan.deployment.network,
531
+ programId: plan.deployment.programId,
532
+ idlSha256: plan.deployment.idl.sha256,
533
+ recentBlockhash: tx.recentBlockhash,
534
+ lastValidBlockHeight: plan.lastValidBlockHeight,
535
+ settlementNonce: plan.call.args.quote.nonce,
536
+ };
537
+ assertPreparedSolanaV14Recovery(plan.call, prepared, plan.deployment, plan.payer, plan.orderId);
538
+ await ctx.onPrepared(prepared);
539
+ try {
540
+ const sig = await ctx.rpc.request("sendTransaction", [
541
+ prepared.serializedTransactionBase64,
542
+ { encoding: "base64", skipPreflight: false, preflightCommitment: "finalized", maxRetries: 0 },
543
+ ]);
544
+ if (sig !== prepared.hash)
545
+ fail("Solana RPC returned a different signature");
546
+ const status = (await ctx.rpc.request("getSignatureStatuses", [
547
+ [prepared.hash],
548
+ { searchTransactionHistory: true },
549
+ ]));
550
+ const s = status?.value?.[0];
551
+ if (s?.confirmationStatus === "finalized" && s.err !== null) {
552
+ const fee = await finalizedFailureFee(ctx.rpc, prepared, plan.transaction.instructions.length, plan.transactionFeeLamports, plan.maxFeeLamports, s);
553
+ throw new SolanaV14Error("SOLANA_V14_TRANSACTION_REVERTED", "Original Solana transaction finalized with failure; fee retained", prepared, fee);
554
+ }
555
+ if (!s || s.confirmationStatus !== "finalized")
556
+ throw new Error("confirmation pending");
557
+ return prepared;
558
+ }
559
+ catch (e) {
560
+ if (e instanceof SolanaV14Error && e.code === "SOLANA_V14_TRANSACTION_REVERTED")
561
+ throw e;
562
+ throw new SolanaV14Error("SOLANA_V14_PENDING", "Unknown Solana outcome; recover this signature without resending", prepared);
563
+ }
564
+ }
565
+ function validFailure(error, instructionCount) {
566
+ if (!error || typeof error !== "object" || Object.keys(error).join() !== "InstructionError")
567
+ return false;
568
+ const value = error.InstructionError;
569
+ if (!Array.isArray(value) ||
570
+ value.length !== 2 ||
571
+ !Number.isSafeInteger(value[0]) ||
572
+ value[0] < 0 ||
573
+ value[0] >= instructionCount)
574
+ return false;
575
+ if (typeof value[1] === "string")
576
+ return [
577
+ "InvalidArgument",
578
+ "InvalidInstructionData",
579
+ "InvalidAccountData",
580
+ "AccountDataTooSmall",
581
+ "InsufficientFunds",
582
+ "IncorrectProgramId",
583
+ "MissingRequiredSignature",
584
+ "AccountAlreadyInitialized",
585
+ "UninitializedAccount",
586
+ "NotEnoughAccountKeys",
587
+ "AccountBorrowFailed",
588
+ "MaxSeedLengthExceeded",
589
+ "InvalidSeeds",
590
+ "ComputationalBudgetExceeded",
591
+ "ProgramFailedToComplete",
592
+ "PrivilegeEscalation",
593
+ "InvalidAccountOwner",
594
+ ].includes(value[1]);
595
+ return (!!value[1] &&
596
+ typeof value[1] === "object" &&
597
+ Object.keys(value[1]).join() === "Custom" &&
598
+ Number.isSafeInteger(value[1].Custom) &&
599
+ value[1].Custom >= 0 &&
600
+ value[1].Custom <= 0xffffffff);
601
+ }
602
+ /** Fail closed on incomplete failure proof. Expired blockhash never proves it. */
603
+ async function finalizedFailureFee(rpc, p, instructionCount, transactionFeeLamports, maxFeeLamports, status) {
604
+ if ((await rpc.request("getGenesisHash", [])) !== SOLANA_GENESIS[p.network])
605
+ fail("failure proof changed Solana genesis");
606
+ const result = (await rpc.request("getTransaction", [
607
+ p.hash,
608
+ { encoding: "base64", commitment: "finalized", maxSupportedTransactionVersion: 0 },
609
+ ]));
610
+ if (!result ||
611
+ result.version !== "legacy" ||
612
+ rpcInt(result.slot) !== rpcInt(status.slot) ||
613
+ !Array.isArray(result.transaction) ||
614
+ result.transaction.length !== 2 ||
615
+ result.transaction[1] !== "base64" ||
616
+ result.transaction[0] !== p.serializedTransactionBase64 ||
617
+ !result.meta ||
618
+ !validFailure(result.meta.err, instructionCount) ||
619
+ JSON.stringify(result.meta.err) !== JSON.stringify(status.err))
620
+ fail("missing exact finalized Solana failure transaction");
621
+ const fee = rpcInt(result.meta.fee);
622
+ if (fee > transactionFeeLamports || fee > maxFeeLamports)
623
+ fail("failure fee exceeds original preflight cap");
624
+ const block = (await rpc.request("getBlock", [
625
+ result.slot,
626
+ {
627
+ encoding: "base64",
628
+ commitment: "finalized",
629
+ transactionDetails: "full",
630
+ rewards: false,
631
+ maxSupportedTransactionVersion: 0,
632
+ },
633
+ ]));
634
+ if (!block ||
635
+ rpcInt(block.parentSlot) >= rpcInt(result.slot) ||
636
+ rpcInt(block.blockHeight) === 0n ||
637
+ !Array.isArray(block.transactions))
638
+ fail("missing canonical finalized Solana failure block");
639
+ key(block.blockhash);
640
+ key(block.previousBlockhash);
641
+ const matches = block.transactions.filter((t) => Array.isArray(t.transaction) &&
642
+ t.transaction.length === 2 &&
643
+ t.transaction[1] === "base64" &&
644
+ t.transaction[0] === p.serializedTransactionBase64);
645
+ if (matches.length !== 1 ||
646
+ !matches[0].meta ||
647
+ rpcInt(matches[0].meta.fee) !== fee ||
648
+ JSON.stringify(matches[0].meta.err) !== JSON.stringify(result.meta.err))
649
+ fail("failed signature is not included in canonical block");
650
+ return fee;
651
+ }
652
+ /** Read-only reconciliation of the exact saved signature, never replacement. */
653
+ export async function readSolanaV14FinalizedFailure(call, p, ctx) {
654
+ assertPreparedSolanaV14Recovery(call, p, ctx.deployment, ctx.payer, ctx.orderId);
655
+ if (ctx.transactionFeeLamports < 0n || ctx.maxFeeLamports <= 0n || ctx.transactionFeeLamports > ctx.maxFeeLamports)
656
+ fail("invalid persisted Solana fee caps");
657
+ const status = (await ctx.rpc.request("getSignatureStatuses", [[p.hash], { searchTransactionHistory: true }]));
658
+ const s = status?.value?.[0];
659
+ if (!s || s.confirmationStatus !== "finalized" || s.err === null)
660
+ return null;
661
+ return finalizedFailureFee(ctx.rpc, p, Transaction.from(Buffer.from(p.serializedTransactionBase64, "base64")).instructions.length, ctx.transactionFeeLamports, ctx.maxFeeLamports, s);
662
+ }
663
+ /** Historical verification is local: no new signature, send, expiry or profile
664
+ * lookup. Issuance still requires the backend's finalized execution proof. */
665
+ export function assertPreparedSolanaV14Recovery(call, p, d, payer, orderId) {
666
+ validateSolanaV14Call(call, d, payer, orderId, true);
667
+ if (p.family !== "solana" ||
668
+ p.network !== d.network ||
669
+ p.programId !== d.programId ||
670
+ p.idlSha256 !== d.idl.sha256 ||
671
+ p.settlementNonce !== call.args.quote.nonce ||
672
+ !Number.isSafeInteger(p.lastValidBlockHeight) ||
673
+ p.lastValidBlockHeight < 0 ||
674
+ typeof p.serializedTransactionBase64 !== "string" ||
675
+ p.serializedTransactionBase64.length > 1644)
676
+ fail("invalid Solana recovery identity");
677
+ const bytes = Buffer.from(p.serializedTransactionBase64, "base64");
678
+ if (!bytes.length || bytes.toString("base64") !== p.serializedTransactionBase64)
679
+ fail("invalid Solana signed bytes");
680
+ const tx = Transaction.from(bytes);
681
+ if (!tx.verifySignatures() ||
682
+ tx.signatures.length !== 1 ||
683
+ tx.feePayer?.toBase58() !== payer ||
684
+ !tx.signature ||
685
+ bs58.encode(tx.signature) !== p.hash ||
686
+ tx.recentBlockhash !== p.recentBlockhash ||
687
+ !tx.serialize().equals(bytes))
688
+ fail("Solana signature/message recovery mismatch");
689
+ const settle = tx.instructions.at(-1);
690
+ const expected = solanaV14Instruction(call, call.treasury_owner);
691
+ if (!settle ||
692
+ !settle.programId.equals(expected.programId) ||
693
+ !settle.data.equals(expected.data) ||
694
+ settle.keys.length !== expected.keys.length ||
695
+ settle.keys.some((k, i) => !k.pubkey.equals(expected.keys[i].pubkey) ||
696
+ k.isSigner !== expected.keys[i].isSigner ||
697
+ (expected.keys[i].isWritable && !k.isWritable)))
698
+ fail("Solana settlement instruction recovery mismatch");
699
+ let compute = 0;
700
+ const canonical = new Transaction({ feePayer: key(payer), recentBlockhash: p.recentBlockhash });
701
+ for (const i of tx.instructions.slice(0, -1)) {
702
+ if (i.programId.equals(ComputeBudgetProgram.programId)) {
703
+ if (++compute !== 1 ||
704
+ i.data.length !== 5 ||
705
+ i.data[0] !== 2 ||
706
+ i.data.readUInt32LE(1) === 0 ||
707
+ i.data.readUInt32LE(1) > 1_400_000)
708
+ fail("unsupported Solana compute instruction");
709
+ canonical.add(ComputeBudgetProgram.setComputeUnitLimit({ units: i.data.readUInt32LE(1) }));
710
+ }
711
+ else {
712
+ const match = call.asset !== "SOL" &&
713
+ [call.args.quote.merchant, call.treasury_owner]
714
+ .filter(Boolean)
715
+ .map((owner) => createAta(payer, owner, call.args.quote.token))
716
+ .find((a) => i.programId.equals(a.programId) &&
717
+ i.data.equals(a.data) &&
718
+ i.keys.length === a.keys.length &&
719
+ i.keys.every((k, n) => k.pubkey.equals(a.keys[n].pubkey) &&
720
+ k.isSigner === a.keys[n].isSigner &&
721
+ (!a.keys[n].isWritable || k.isWritable)));
722
+ if (!match)
723
+ fail("unsupported extra Solana instruction");
724
+ canonical.add(match);
725
+ }
726
+ }
727
+ if (compute !== 1 || tx.instructions.length > 4)
728
+ fail("invalid Solana instruction count");
729
+ canonical.add(expected);
730
+ if (!canonical.serializeMessage().equals(tx.serializeMessage()))
731
+ fail("Solana message privileges/order mismatch");
732
+ }
733
+ //# sourceMappingURL=settlementSolanaV14.js.map