@piprail/sdk 1.20.1 → 1.21.1

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.
@@ -0,0 +1,724 @@
1
+ import {
2
+ ConfirmationTimeoutError,
3
+ SettlementError,
4
+ UnknownTokenError,
5
+ UnsupportedSchemeError,
6
+ WrongFamilyError,
7
+ nativeCost,
8
+ rejectForeignToken,
9
+ toInsufficientFundsError
10
+ } from "./chunk-ILPABTI2.js";
11
+
12
+ // src/drivers/solana/index.ts
13
+ import { Connection, PublicKey as PublicKey3 } from "@solana/web3.js";
14
+ import {
15
+ getAccount,
16
+ getAssociatedTokenAddressSync as getAssociatedTokenAddressSync3,
17
+ TOKEN_2022_PROGRAM_ID as TOKEN_2022_PROGRAM_ID2,
18
+ TokenAccountNotFoundError
19
+ } from "@solana/spl-token";
20
+
21
+ // src/drivers/solana/chains.ts
22
+ var SOL_DECIMALS = 9;
23
+ var SOLANA_MAINNET = {
24
+ caip2: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
25
+ defaultRpc: "https://api.mainnet-beta.solana.com",
26
+ tokens: {
27
+ USDC: {
28
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
29
+ decimals: 6,
30
+ symbol: "USDC"
31
+ },
32
+ USDT: {
33
+ mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
34
+ decimals: 6,
35
+ symbol: "USDT"
36
+ }
37
+ }
38
+ };
39
+
40
+ // src/drivers/solana/pay.ts
41
+ import {
42
+ PublicKey,
43
+ SystemProgram,
44
+ Transaction
45
+ } from "@solana/web3.js";
46
+ import {
47
+ createAssociatedTokenAccountIdempotentInstruction,
48
+ createTransferCheckedInstruction,
49
+ getAssociatedTokenAddressSync
50
+ } from "@solana/spl-token";
51
+ async function paySolana(params) {
52
+ const { connection, keypair, accept } = params;
53
+ const payTo = new PublicKey(accept.payTo);
54
+ const amount = BigInt(accept.amount);
55
+ const tx = new Transaction();
56
+ if (accept.asset === "native") {
57
+ tx.add(
58
+ SystemProgram.transfer({
59
+ fromPubkey: keypair.publicKey,
60
+ toPubkey: payTo,
61
+ lamports: amount
62
+ })
63
+ );
64
+ } else {
65
+ const mint = new PublicKey(accept.asset);
66
+ const source = getAssociatedTokenAddressSync(mint, keypair.publicKey);
67
+ const dest = getAssociatedTokenAddressSync(mint, payTo);
68
+ tx.add(
69
+ createAssociatedTokenAccountIdempotentInstruction(
70
+ keypair.publicKey,
71
+ // payer for any rent
72
+ dest,
73
+ payTo,
74
+ mint
75
+ )
76
+ );
77
+ tx.add(
78
+ createTransferCheckedInstruction(
79
+ source,
80
+ mint,
81
+ dest,
82
+ keypair.publicKey,
83
+ amount,
84
+ accept.extra.decimals
85
+ )
86
+ );
87
+ }
88
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
89
+ tx.recentBlockhash = blockhash;
90
+ tx.lastValidBlockHeight = lastValidBlockHeight;
91
+ tx.feePayer = keypair.publicKey;
92
+ tx.sign(keypair);
93
+ const signature = await connection.sendRawTransaction(tx.serialize(), {
94
+ maxRetries: 5
95
+ });
96
+ await connection.confirmTransaction(
97
+ { signature, blockhash, lastValidBlockHeight },
98
+ "confirmed"
99
+ );
100
+ return signature;
101
+ }
102
+
103
+ // src/drivers/solana/verify.ts
104
+ async function verifySolana(params) {
105
+ const { connection, signature, accept } = params;
106
+ const required = BigInt(accept.amount);
107
+ let tx;
108
+ try {
109
+ tx = await connection.getParsedTransaction(signature, {
110
+ commitment: "confirmed",
111
+ maxSupportedTransactionVersion: 0
112
+ });
113
+ } catch {
114
+ return notFound(signature);
115
+ }
116
+ if (!tx) return notFound(signature);
117
+ const meta = tx.meta;
118
+ if (!meta) {
119
+ return { ok: false, error: "no_meta", detail: `No metadata for ${signature}.` };
120
+ }
121
+ if (meta.err !== null) {
122
+ return {
123
+ ok: false,
124
+ error: "tx_reverted",
125
+ detail: `Transaction ${signature} failed on-chain.`
126
+ };
127
+ }
128
+ if (typeof tx.blockTime !== "number") {
129
+ return {
130
+ ok: false,
131
+ error: "payment_expired",
132
+ detail: `Cannot determine the age of ${signature} (no blockTime).`
133
+ };
134
+ }
135
+ const ageSeconds = Math.floor(Date.now() / 1e3) - tx.blockTime;
136
+ if (ageSeconds > accept.maxTimeoutSeconds) {
137
+ return {
138
+ ok: false,
139
+ error: "payment_expired",
140
+ detail: `Payment is ${ageSeconds}s old; max allowed is ${accept.maxTimeoutSeconds}s.`
141
+ };
142
+ }
143
+ const accountKeys = tx.transaction.message.accountKeys.map(
144
+ (k) => k.pubkey.toBase58()
145
+ );
146
+ const payer = accountKeys[0] ?? "";
147
+ if (accept.asset === "native") {
148
+ const idx = accountKeys.indexOf(accept.payTo);
149
+ if (idx < 0) {
150
+ return {
151
+ ok: false,
152
+ error: "wrong_recipient",
153
+ detail: `Native payment in ${signature} did not credit ${accept.payTo}.`
154
+ };
155
+ }
156
+ const delta = BigInt(meta.postBalances[idx] ?? 0) - BigInt(meta.preBalances[idx] ?? 0);
157
+ if (delta < required) {
158
+ return {
159
+ ok: false,
160
+ error: "amount_too_low",
161
+ detail: `Credited ${delta} lamports, required ${required}.`
162
+ };
163
+ }
164
+ } else {
165
+ const mint = accept.asset;
166
+ const pre = meta.preTokenBalances ?? [];
167
+ const post = meta.postTokenBalances ?? [];
168
+ let delta = 0n;
169
+ for (const p of post) {
170
+ if (p.mint !== mint || p.owner !== accept.payTo) continue;
171
+ const before = pre.find((x) => x.accountIndex === p.accountIndex);
172
+ delta += BigInt(p.uiTokenAmount.amount) - BigInt(before?.uiTokenAmount.amount ?? "0");
173
+ }
174
+ if (delta < required) {
175
+ return {
176
+ ok: false,
177
+ error: "transfer_not_found",
178
+ detail: `No SPL transfer of >= ${required} (mint ${mint}) to ${accept.payTo} in ${signature}.`
179
+ };
180
+ }
181
+ }
182
+ return {
183
+ ok: true,
184
+ receipt: {
185
+ scheme: "onchain-proof",
186
+ success: true,
187
+ network: accept.network,
188
+ transaction: signature,
189
+ asset: accept.asset,
190
+ amount: accept.amount,
191
+ payer,
192
+ payTo: accept.payTo,
193
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
194
+ }
195
+ };
196
+ }
197
+ function notFound(signature) {
198
+ return {
199
+ ok: false,
200
+ error: "tx_not_found",
201
+ detail: `Signature ${signature} not found or not yet confirmed.`
202
+ };
203
+ }
204
+
205
+ // src/drivers/solana/exact.ts
206
+ import {
207
+ ComputeBudgetProgram,
208
+ PublicKey as PublicKey2,
209
+ TransactionInstruction,
210
+ TransactionMessage,
211
+ VersionedTransaction
212
+ } from "@solana/web3.js";
213
+ import {
214
+ createTransferCheckedInstruction as createTransferCheckedInstruction2,
215
+ decodeTransferCheckedInstruction,
216
+ getAssociatedTokenAddressSync as getAssociatedTokenAddressSync2,
217
+ TOKEN_2022_PROGRAM_ID,
218
+ TOKEN_PROGRAM_ID
219
+ } from "@solana/spl-token";
220
+ import bs58 from "bs58";
221
+ var COMPUTE_UNIT_LIMIT = 2e4;
222
+ var COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
223
+ function tokenProgramFor(accept) {
224
+ return accept.extra.tokenProgram === "token-2022" ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
225
+ }
226
+ function isEmptySig(sig) {
227
+ return !sig || sig.every((b) => b === 0);
228
+ }
229
+ async function payExactSolana(input) {
230
+ const { connection, keypair, accept } = input;
231
+ if (accept.asset === "native") {
232
+ throw new UnsupportedSchemeError(
233
+ "SVM exact is SPL-token only (TransferChecked); native SOL is not exact-payable. Pay via onchain-proof."
234
+ );
235
+ }
236
+ if (!accept.extra.feePayer) {
237
+ throw new UnsupportedSchemeError("SVM exact rail must advertise extra.feePayer (the merchant sponsor key).");
238
+ }
239
+ if (accept.extra.decimals === void 0) {
240
+ throw new UnsupportedSchemeError("SVM exact rail must advertise extra.decimals for the TransferChecked.");
241
+ }
242
+ let feePayer;
243
+ let mint;
244
+ let payTo;
245
+ try {
246
+ feePayer = new PublicKey2(accept.extra.feePayer);
247
+ mint = new PublicKey2(accept.asset);
248
+ payTo = new PublicKey2(accept.payTo);
249
+ } catch (err) {
250
+ throw new UnsupportedSchemeError(
251
+ `SVM exact: bad feePayer/asset/payTo (${err instanceof Error ? err.message : String(err)}).`
252
+ );
253
+ }
254
+ if (feePayer.equals(payTo)) {
255
+ throw new UnsupportedSchemeError(
256
+ "SVM exact: the fee payer must differ from payTo \u2014 payTo appears in the transfer instruction, which the fee payer must not (a scheme MUST-rule). Use a separate relayer key for the gate."
257
+ );
258
+ }
259
+ const program = tokenProgramFor(accept);
260
+ const source = getAssociatedTokenAddressSync2(mint, keypair.publicKey, true, program);
261
+ const dest = getAssociatedTokenAddressSync2(mint, payTo, true, program);
262
+ let destInfo = "unknown";
263
+ try {
264
+ destInfo = await input.connection.getAccountInfo(dest);
265
+ } catch {
266
+ destInfo = "unknown";
267
+ }
268
+ if (destInfo === null) {
269
+ throw new UnsupportedSchemeError(
270
+ `SVM exact: the recipient's token account for ${mint.toBase58()} doesn't exist yet \u2014 the exact rail can't create it. Pay via onchain-proof (which creates it), or have ${payTo.toBase58()} create its associated token account first.`
271
+ );
272
+ }
273
+ const instructions = [
274
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNIT_LIMIT }),
275
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE_MICROLAMPORTS }),
276
+ createTransferCheckedInstruction2(
277
+ source,
278
+ mint,
279
+ dest,
280
+ keypair.publicKey,
281
+ BigInt(accept.amount),
282
+ accept.extra.decimals,
283
+ [],
284
+ program
285
+ )
286
+ ];
287
+ const { blockhash } = await connection.getLatestBlockhash();
288
+ const message = new TransactionMessage({
289
+ payerKey: feePayer,
290
+ recentBlockhash: blockhash,
291
+ instructions
292
+ }).compileToV0Message();
293
+ const tx = new VersionedTransaction(message);
294
+ tx.sign([keypair]);
295
+ const buyerIndex = message.staticAccountKeys.findIndex((k) => k.equals(keypair.publicKey));
296
+ if (buyerIndex < 1 || buyerIndex >= message.header.numRequiredSignatures) {
297
+ throw new UnsupportedSchemeError("SVM exact: could not locate the buyer signer slot.");
298
+ }
299
+ const buyerSig = tx.signatures[buyerIndex];
300
+ if (!buyerSig || isEmptySig(buyerSig)) {
301
+ throw new UnsupportedSchemeError("SVM exact: the wallet did not produce a buyer signature.");
302
+ }
303
+ const transaction = Buffer.from(tx.serialize()).toString("base64");
304
+ return {
305
+ payload: { transaction },
306
+ payerFrom: keypair.publicKey.toBase58(),
307
+ nonce: bs58.encode(buyerSig)
308
+ };
309
+ }
310
+ function shorten(msg) {
311
+ const oneLine = msg.replace(/\s+/g, " ").trim();
312
+ return oneLine.length > 200 ? `${oneLine.slice(0, 200)}\u2026` : oneLine;
313
+ }
314
+ function fail(error, detail) {
315
+ return { ok: false, error, detail };
316
+ }
317
+ async function verifyAndSettleExactSolana(input) {
318
+ const { connection, feePayerKeypair, payload, accept } = input;
319
+ let payTo;
320
+ let mint;
321
+ let railFeePayer;
322
+ try {
323
+ payTo = new PublicKey2(accept.payTo);
324
+ mint = new PublicKey2(accept.asset);
325
+ if (!accept.extra.feePayer) throw new Error("rail is missing extra.feePayer");
326
+ railFeePayer = new PublicKey2(accept.extra.feePayer);
327
+ } catch (err) {
328
+ throw new SettlementError(`SVM exact: rail has a bad payTo/asset/feePayer (${err instanceof Error ? err.message : String(err)}).`);
329
+ }
330
+ if (!railFeePayer.equals(feePayerKeypair.publicKey)) {
331
+ throw new SettlementError(
332
+ "SVM exact: the gate relayer key does not match the rail extra.feePayer \u2014 misconfigured rail."
333
+ );
334
+ }
335
+ if (feePayerKeypair.publicKey.equals(payTo)) {
336
+ throw new SettlementError("SVM exact: the fee payer must differ from payTo \u2014 misconfigured rail.");
337
+ }
338
+ const program = tokenProgramFor(accept);
339
+ let tx;
340
+ try {
341
+ tx = VersionedTransaction.deserialize(Buffer.from(payload.transaction, "base64"));
342
+ } catch (err) {
343
+ return fail("signature_invalid", `Unparseable SVM transaction: ${shorten(err instanceof Error ? err.message : String(err))}.`);
344
+ }
345
+ const message = tx.message;
346
+ if (message.version !== 0) {
347
+ return fail("signature_invalid", "SVM exact requires a versioned (v0) transaction.");
348
+ }
349
+ const altAccounts = [];
350
+ for (const lookup of message.addressTableLookups) {
351
+ let value;
352
+ try {
353
+ ;
354
+ ({ value } = await connection.getAddressLookupTable(lookup.accountKey));
355
+ } catch {
356
+ return fail("tx_not_found", `Could not read lookup table ${lookup.accountKey.toBase58()} (transient RPC) \u2014 retry.`);
357
+ }
358
+ if (!value) {
359
+ return fail("signature_invalid", `Lookup table ${lookup.accountKey.toBase58()} is not resolvable \u2014 account visibility cannot be guaranteed.`);
360
+ }
361
+ altAccounts.push(value);
362
+ }
363
+ const keys = message.getAccountKeys({ addressLookupTableAccounts: altAccounts });
364
+ const feePayerKey = keys.get(0);
365
+ if (!feePayerKey || !feePayerKey.equals(feePayerKeypair.publicKey)) {
366
+ return fail("signature_invalid", "Transaction fee payer is not the merchant sponsor key.");
367
+ }
368
+ if (!isEmptySig(tx.signatures[0])) {
369
+ return fail("signature_invalid", "The fee-payer signature slot must be empty \u2014 the buyer must not sign it.");
370
+ }
371
+ for (const ix of message.compiledInstructions) {
372
+ const ixProgram = keys.get(ix.programIdIndex);
373
+ if (ixProgram && ixProgram.equals(feePayerKey)) {
374
+ return fail("signature_invalid", "The fee payer is invoked as a program.");
375
+ }
376
+ for (const i of ix.accountKeyIndexes) {
377
+ if (keys.get(i)?.equals(feePayerKey)) {
378
+ return fail("signature_invalid", "The fee payer appears in an instruction account list (would risk a fund drain).");
379
+ }
380
+ }
381
+ }
382
+ const isSignedSigner = (pubkey) => {
383
+ const idx = message.staticAccountKeys.findIndex((k) => k.equals(pubkey));
384
+ return idx >= 1 && idx < message.header.numRequiredSignatures && !isEmptySig(tx.signatures[idx]);
385
+ };
386
+ const expectedDest = getAssociatedTokenAddressSync2(mint, payTo, true, program);
387
+ const requiredAmount = BigInt(accept.amount);
388
+ let paidToPayTo = 0n;
389
+ let buyerOwner = null;
390
+ let sawTransferToPayTo = false;
391
+ for (const ix of message.compiledInstructions) {
392
+ const programId = keys.get(ix.programIdIndex);
393
+ if (!programId || !programId.equals(program)) continue;
394
+ let decoded;
395
+ try {
396
+ decoded = decodeTransferCheckedInstruction(
397
+ new TransactionInstruction({
398
+ programId,
399
+ keys: ix.accountKeyIndexes.map((i) => ({
400
+ pubkey: keys.get(i),
401
+ isSigner: message.isAccountSigner(i),
402
+ isWritable: message.isAccountWritable(i)
403
+ })),
404
+ data: Buffer.from(ix.data)
405
+ }),
406
+ program
407
+ );
408
+ } catch {
409
+ continue;
410
+ }
411
+ if (!decoded.keys.mint.pubkey.equals(mint)) continue;
412
+ if (!decoded.keys.destination.pubkey.equals(expectedDest)) {
413
+ return fail("wrong_recipient", `A transfer pays ${decoded.keys.destination.pubkey.toBase58()}, not payTo's ATA ${expectedDest.toBase58()}.`);
414
+ }
415
+ if (decoded.data.decimals !== accept.extra.decimals) {
416
+ return fail("transfer_not_found", `Transfer decimals ${decoded.data.decimals} \u2260 rail decimals ${accept.extra.decimals}.`);
417
+ }
418
+ sawTransferToPayTo = true;
419
+ if (!isSignedSigner(decoded.keys.owner.pubkey)) continue;
420
+ paidToPayTo += BigInt(decoded.data.amount);
421
+ buyerOwner = decoded.keys.owner.pubkey;
422
+ }
423
+ if (!buyerOwner) {
424
+ return fail(
425
+ sawTransferToPayTo ? "signature_invalid" : "transfer_not_found",
426
+ sawTransferToPayTo ? "A TransferChecked to payTo's ATA is not authorized by a transaction signer." : `No TransferChecked of mint ${mint.toBase58()} (program ${program.toBase58()}) found.`
427
+ );
428
+ }
429
+ if (paidToPayTo < requiredAmount) {
430
+ return fail("amount_too_low", `Signed transfers pay ${paidToPayTo} to payTo, required ${requiredAmount}.`);
431
+ }
432
+ const buyerIndex = message.staticAccountKeys.findIndex((k) => k.equals(buyerOwner));
433
+ if (buyerIndex < 1 || buyerIndex >= message.header.numRequiredSignatures) {
434
+ return fail("signature_invalid", "The transfer authority is not a transaction signer.");
435
+ }
436
+ if (isEmptySig(tx.signatures[buyerIndex])) {
437
+ return fail("signature_invalid", "The buyer signature slot is empty.");
438
+ }
439
+ tx.sign([feePayerKeypair]);
440
+ try {
441
+ const sim = await connection.simulateTransaction(tx, { sigVerify: true, replaceRecentBlockhash: false, commitment: "confirmed" });
442
+ if (sim.value.err) {
443
+ const errStr = typeof sim.value.err === "string" ? sim.value.err : JSON.stringify(sim.value.err);
444
+ if (/blockhash|block height|expired/i.test(errStr)) return fail("payment_expired", `Transaction blockhash is no longer valid: ${shorten(errStr)}.`);
445
+ if (/signature/i.test(errStr)) return fail("signature_invalid", `Signature verification failed: ${shorten(errStr)}.`);
446
+ return fail("tx_reverted", `Transaction would fail on-chain: ${shorten(errStr)}.`);
447
+ }
448
+ } catch (err) {
449
+ const msg = err instanceof Error ? err.message : String(err);
450
+ if (/signature verification|invalid signature/i.test(msg)) return fail("signature_invalid", `Signature verification failed: ${shorten(msg)}.`);
451
+ if (/blockhash|block height|expired/i.test(msg)) return fail("payment_expired", `Transaction blockhash is no longer valid: ${shorten(msg)}.`);
452
+ return fail("tx_not_found", `Could not simulate the transaction (transient RPC) \u2014 retry: ${shorten(msg)}.`);
453
+ }
454
+ let txid;
455
+ try {
456
+ txid = await connection.sendRawTransaction(tx.serialize(), { maxRetries: 5 });
457
+ } catch (err) {
458
+ const msg = err instanceof Error ? err.message : String(err);
459
+ if (/blockhash|block height|expired/i.test(msg)) return fail("payment_expired", `Settle broadcast rejected \u2014 blockhash expired: ${shorten(msg)}.`);
460
+ throw new SettlementError(
461
+ `SVM exact settle: the merchant fee payer failed to broadcast (${shorten(msg)}). The buyer's signed transaction is still valid \u2014 fund/fix the fee payer and the buyer can re-present it.`,
462
+ { cause: err }
463
+ );
464
+ }
465
+ const confirmed = await pollConfirmed(connection, txid);
466
+ if (confirmed === "reverted") {
467
+ return fail("tx_reverted", `Settle tx ${txid} reverted on-chain (a post-simulate race).`);
468
+ }
469
+ if (confirmed === "timeout") {
470
+ throw new SettlementError(
471
+ `SVM exact settle: broadcast ${txid} but it did not confirm in time. It likely landed \u2014 re-verify by signature before re-presenting; do NOT re-pay.`
472
+ );
473
+ }
474
+ return {
475
+ ok: true,
476
+ receipt: {
477
+ scheme: "exact",
478
+ success: true,
479
+ network: accept.network,
480
+ transaction: txid,
481
+ asset: accept.asset,
482
+ amount: accept.amount,
483
+ payer: buyerOwner.toBase58(),
484
+ payTo: accept.payTo,
485
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
486
+ }
487
+ };
488
+ }
489
+ async function pollConfirmed(connection, signature) {
490
+ const deadline = Date.now() + 3e4;
491
+ for (; ; ) {
492
+ let info;
493
+ try {
494
+ const { value } = await connection.getSignatureStatuses([signature], { searchTransactionHistory: true });
495
+ info = value[0];
496
+ } catch {
497
+ info = null;
498
+ }
499
+ if (info) {
500
+ if (info.err) return "reverted";
501
+ if (info.confirmationStatus === "confirmed" || info.confirmationStatus === "finalized") return "ok";
502
+ }
503
+ if (Date.now() >= deadline) return "timeout";
504
+ await new Promise((r) => setTimeout(r, 1e3));
505
+ }
506
+ }
507
+
508
+ // src/drivers/solana/wallet.ts
509
+ import { Keypair as Keypair3 } from "@solana/web3.js";
510
+ import bs582 from "bs58";
511
+ function toKeypair(wallet, network) {
512
+ if (typeof wallet !== "object" || wallet === null) {
513
+ throw new WrongFamilyError(
514
+ `chain ${network} is Solana; wallet must be { secretKey } or { signer }.`
515
+ );
516
+ }
517
+ if ("privateKey" in wallet || "walletClient" in wallet) {
518
+ throw new WrongFamilyError(
519
+ `chain ${network} is Solana; an EVM wallet can't be used \u2014 pass { secretKey } or { signer }.`
520
+ );
521
+ }
522
+ if ("signer" in wallet) {
523
+ return wallet.signer;
524
+ }
525
+ if ("secretKey" in wallet) {
526
+ const sk = wallet.secretKey;
527
+ const bytes = typeof sk === "string" ? bs582.decode(sk) : sk;
528
+ return Keypair3.fromSecretKey(bytes);
529
+ }
530
+ throw new WrongFamilyError(
531
+ `chain ${network} is Solana; wallet must be { secretKey } or { signer }.`
532
+ );
533
+ }
534
+
535
+ // src/drivers/solana/index.ts
536
+ var solanaDriver = {
537
+ family: "solana",
538
+ resolve(opts) {
539
+ if (opts.chain !== "solana") return null;
540
+ const rpcUrl = opts.rpcUrl ?? SOLANA_MAINNET.defaultRpc;
541
+ return makeSolanaNetwork(SOLANA_MAINNET, rpcUrl);
542
+ }
543
+ };
544
+ function makeSolanaNetwork(preset, rpcUrl) {
545
+ const connection = new Connection(rpcUrl, "confirmed");
546
+ const network = preset.caip2;
547
+ return {
548
+ family: "solana",
549
+ network,
550
+ supports: (n) => n === network,
551
+ resolveToken(token) {
552
+ if (token === "native") {
553
+ return { asset: "native", decimals: SOL_DECIMALS, symbol: "SOL" };
554
+ }
555
+ if (typeof token === "string") {
556
+ const info = preset.tokens[token.toUpperCase()];
557
+ if (!info) {
558
+ const known = Object.keys(preset.tokens).join(", ") || "(none built in)";
559
+ throw new UnknownTokenError(
560
+ `token "${token}" isn't built in for Solana (known: ${known}). Pass { mint, decimals } instead, or use 'native'.`
561
+ );
562
+ }
563
+ return { asset: info.mint, decimals: info.decimals, symbol: info.symbol };
564
+ }
565
+ rejectForeignToken(token, "solana", network);
566
+ if (!("mint" in token)) {
567
+ throw new WrongFamilyError(
568
+ `chain ${network} is Solana; a custom token must be { mint, decimals }.`
569
+ );
570
+ }
571
+ return {
572
+ asset: token.mint,
573
+ decimals: token.decimals,
574
+ ...token.symbol ? { symbol: token.symbol } : {}
575
+ };
576
+ },
577
+ describeAsset(asset) {
578
+ if (asset === "native") return { symbol: "SOL", decimals: SOL_DECIMALS };
579
+ for (const info of Object.values(preset.tokens)) {
580
+ if (info.mint === asset) return { symbol: info.symbol, decimals: info.decimals };
581
+ }
582
+ return null;
583
+ },
584
+ assertValidPayTo(payTo) {
585
+ if (payTo.startsWith("0x")) {
586
+ throw new WrongFamilyError(
587
+ `chain ${network} is Solana, but payTo "${payTo}" looks like an EVM address.`
588
+ );
589
+ }
590
+ try {
591
+ new PublicKey3(payTo);
592
+ } catch {
593
+ throw new WrongFamilyError(
594
+ `chain ${network} is Solana, but payTo "${payTo}" is not a base58 address.`
595
+ );
596
+ }
597
+ },
598
+ bindWallet(wallet) {
599
+ return { _native: toKeypair(wallet, network) };
600
+ },
601
+ async send(wallet, accept) {
602
+ try {
603
+ return await paySolana({ connection, keypair: wallet._native, accept });
604
+ } catch (err) {
605
+ throw toInsufficientFundsError(err) ?? err;
606
+ }
607
+ },
608
+ async confirm(ref) {
609
+ let info;
610
+ try {
611
+ const { value } = await connection.getSignatureStatuses([ref], {
612
+ searchTransactionHistory: true
613
+ });
614
+ info = value[0];
615
+ } catch (err) {
616
+ throw new ConfirmationTimeoutError(
617
+ `Solana payment ${ref} could not be confirmed (RPC read failed).`,
618
+ { cause: err }
619
+ );
620
+ }
621
+ if (!info || info.err || info.confirmationStatus !== "confirmed" && info.confirmationStatus !== "finalized") {
622
+ throw new ConfirmationTimeoutError(`Solana payment ${ref} did not confirm in time.`);
623
+ }
624
+ return { height: String(info.slot) };
625
+ },
626
+ async estimateCost(accept) {
627
+ if (accept.scheme === "exact") {
628
+ return nativeCost({
629
+ symbol: "SOL",
630
+ decimals: SOL_DECIMALS,
631
+ fee: 0n,
632
+ basis: "estimated",
633
+ detail: "gasless \u2014 the fee payer (facilitator/relayer) broadcasts and pays the SOL fee"
634
+ });
635
+ }
636
+ const base = 5000n;
637
+ if (accept.asset === "native") {
638
+ return nativeCost({
639
+ symbol: "SOL",
640
+ decimals: SOL_DECIMALS,
641
+ fee: base,
642
+ basis: "heuristic",
643
+ detail: "1 signature (5000 lamports)"
644
+ });
645
+ }
646
+ const ataRent = 2039280n;
647
+ return nativeCost({
648
+ symbol: "SOL",
649
+ decimals: SOL_DECIMALS,
650
+ fee: base + ataRent,
651
+ basis: "heuristic",
652
+ detail: "1 signature + recipient token-account rent (~0.00204 SOL, if not already created)"
653
+ });
654
+ },
655
+ async balanceOf(wallet, asset) {
656
+ const owner = wallet._native.publicKey;
657
+ const native = await connection.getBalance(owner).then((n) => BigInt(n)).catch(() => null);
658
+ if (asset === "native") return { token: native, native };
659
+ let token;
660
+ try {
661
+ const ata = getAssociatedTokenAddressSync3(new PublicKey3(asset), owner);
662
+ token = (await getAccount(connection, ata, "confirmed")).amount;
663
+ } catch (e) {
664
+ token = e instanceof TokenAccountNotFoundError ? 0n : null;
665
+ }
666
+ return { token, native };
667
+ },
668
+ // No receive prerequisite — the payer's tx idempotently creates the recipient's ATA (pay.ts).
669
+ async recipientReady() {
670
+ return { ready: "n/a" };
671
+ },
672
+ async verify(ref, accept) {
673
+ return verifySolana({ connection, signature: ref, accept });
674
+ },
675
+ // Standard x402 `exact` rail, BUYER side — partial-sign an SPL TransferChecked with the
676
+ // merchant as fee payer (the buyer spends zero SOL). Never broadcasts. Throws
677
+ // UnsupportedSchemeError for native / a missing feePayer / feePayer === payTo.
678
+ async payExact(wallet, accept) {
679
+ const { payload, payerFrom, nonce } = await payExactSolana({
680
+ connection,
681
+ keypair: wallet._native,
682
+ accept
683
+ });
684
+ return { payload, accepted: accept, payerFrom, nonce };
685
+ },
686
+ // Standard x402 `exact` rail, SELLER side — verify the partial-signed tx against the
687
+ // trusted accept, then co-sign as the fee payer + broadcast (self-settle, no facilitator).
688
+ async settleExactSelf({ relayer, payload, accept }) {
689
+ if (!("transaction" in payload)) {
690
+ return { ok: false, error: "signature_invalid", detail: "SVM exact expects a { transaction } payload." };
691
+ }
692
+ return verifyAndSettleExactSolana({
693
+ connection,
694
+ feePayerKeypair: relayer._native,
695
+ payload,
696
+ accept
697
+ });
698
+ },
699
+ // The gate's rail-advertisement SPI. The SVM fee payer is EITHER a facilitator's sponsor
700
+ // pubkey (`feePayer` — facilitator mode, neither buyer nor merchant pays gas) OR the
701
+ // merchant's own bound `relayer` (self mode); native isn't exact-payable. Reads the mint's
702
+ // owner once to flag token-2022 (so both ends derive the same ATA). `null` ⇒ no exact rail.
703
+ async resolveExactRail({ asset, relayer, feePayer }) {
704
+ if (asset === "native") return null;
705
+ const fp = feePayer ?? (relayer ? relayer._native.publicKey.toBase58() : void 0);
706
+ if (!fp) return null;
707
+ try {
708
+ new PublicKey3(fp);
709
+ } catch {
710
+ return null;
711
+ }
712
+ let tokenProgram = "spl-token";
713
+ try {
714
+ const info = await connection.getAccountInfo(new PublicKey3(asset));
715
+ if (info?.owner.equals(TOKEN_2022_PROGRAM_ID2)) tokenProgram = "token-2022";
716
+ } catch {
717
+ }
718
+ return { method: "svm", extra: { feePayer: fp, tokenProgram } };
719
+ }
720
+ };
721
+ }
722
+ export {
723
+ solanaDriver
724
+ };