@piprail/sdk 2.3.0 → 2.4.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.
@@ -0,0 +1,678 @@
1
+ import {
2
+ ConfirmationTimeoutError,
3
+ InsufficientFundsError,
4
+ RecipientNotReadyError,
5
+ SettlementError,
6
+ UnknownTokenError,
7
+ UnsupportedSchemeError,
8
+ WrongFamilyError,
9
+ assertNoLegacyWalletKey,
10
+ nativeCost,
11
+ rejectForeignToken,
12
+ toInsufficientFundsError
13
+ } from "./chunk-7XK22JSQ.js";
14
+
15
+ // src/drivers/algorand/index.ts
16
+ import algosdk3 from "algosdk";
17
+
18
+ // src/drivers/algorand/chains.ts
19
+ var ALGO_DECIMALS = 6;
20
+ var ALGO_SYMBOL = "ALGO";
21
+ var ALGORAND_MAINNET = {
22
+ caip2: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
23
+ defaultAlgod: "https://mainnet-api.algonode.cloud",
24
+ defaultIndexer: "https://mainnet-idx.algonode.cloud",
25
+ tokens: {
26
+ // Circle USDC — ASA id + 6 decimals verified live on mainnet algod
27
+ // (/v2/assets/31566704 → unit-name "USDC", decimals 6, creator = Circle) before shipping.
28
+ // USDC-only: Tether deprecated USDT on Algorand, so it's intentionally omitted.
29
+ USDC: { assetId: 31566704, decimals: 6, symbol: "USDC" }
30
+ }
31
+ };
32
+ function algorandAssetId(assetId) {
33
+ return String(assetId);
34
+ }
35
+ function parseAlgorandAssetId(asset) {
36
+ if (asset === "native") return null;
37
+ if (!/^\d+$/.test(asset)) return null;
38
+ const n = Number(asset);
39
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
40
+ }
41
+
42
+ // src/drivers/algorand/pay.ts
43
+ async function payAlgorand(params) {
44
+ const { client, sk, sender, accept } = params;
45
+ const note = new TextEncoder().encode(accept.extra.nonce);
46
+ const amount = BigInt(accept.amount);
47
+ const assetId = parseAlgorandAssetId(accept.asset);
48
+ try {
49
+ const { txn, txId } = await client.build({
50
+ sender,
51
+ receiver: accept.payTo,
52
+ amount,
53
+ note,
54
+ ...assetId === null ? {} : { assetId }
55
+ });
56
+ await client.signSend({ txn, sk });
57
+ return txId;
58
+ } catch (err) {
59
+ const mapped = mapAlgorandError(err, accept.payTo);
60
+ if (mapped) throw mapped;
61
+ throw toInsufficientFundsError(err) ?? err;
62
+ }
63
+ }
64
+ function mapAlgorandError(err, payTo) {
65
+ const m = err instanceof Error ? err.message : String(err);
66
+ if (/must optin/i.test(m) || /missing from/i.test(m) && m.includes(payTo)) {
67
+ return new RecipientNotReadyError(
68
+ `Algorand recipient ${payTo} hasn't opted into this asset \u2014 it must opt in (a 0-amount asset transfer to itself) before it can receive. (Algorand: ${firstLine(m)})`,
69
+ { cause: err }
70
+ );
71
+ }
72
+ if (/overspend|below min|min(imum)? balance|tried to spend|balance \d+ below|asset \d+ missing from|insufficient|underflow/i.test(
73
+ m
74
+ )) {
75
+ return new InsufficientFundsError(
76
+ `Algorand payment failed: the sender can't cover it \u2014 token balance, ALGO for fees, the 0.1-ALGO minimum balance, or a missing asset opt-in on the sender. (Algorand: ${firstLine(m)})`,
77
+ { cause: err }
78
+ );
79
+ }
80
+ return null;
81
+ }
82
+ function firstLine(message) {
83
+ return message.split("\n")[0].slice(0, 160);
84
+ }
85
+
86
+ // src/drivers/algorand/exact.ts
87
+ import algosdk from "algosdk";
88
+ var ZERO_ADDRESS = algosdk.Address.zeroAddress().toString();
89
+ var GROUP_SIZE = 2;
90
+ var MAX_GROUP_FEE = 20000n;
91
+ var b64encode = (bytes) => Buffer.from(bytes).toString("base64");
92
+ var b64decode = (s) => new Uint8Array(Buffer.from(s, "base64"));
93
+ function addrEq(a, b) {
94
+ return a != null && a.toString() === b;
95
+ }
96
+ function isSet(addr) {
97
+ return addr != null && addr.toString() !== ZERO_ADDRESS;
98
+ }
99
+ function randomNote() {
100
+ const g = globalThis.crypto;
101
+ if (!g?.getRandomValues) {
102
+ throw new UnsupportedSchemeError("Algorand exact: no Web Crypto CSPRNG available to generate a unique note.");
103
+ }
104
+ return g.getRandomValues(new Uint8Array(32));
105
+ }
106
+ async function payExactAlgorand(input) {
107
+ const { suggestedParams, sk, sender, accept } = input;
108
+ if (accept.asset === "native") {
109
+ throw new UnsupportedSchemeError(
110
+ "Algorand exact is ASA-only (an asset transfer); native ALGO is not exact-payable. Pay via onchain-proof."
111
+ );
112
+ }
113
+ const feePayer = accept.extra.feePayer;
114
+ if (!feePayer) {
115
+ throw new UnsupportedSchemeError("Algorand exact rail must advertise extra.feePayer (the fee sponsor address).");
116
+ }
117
+ const assetId = parseAlgorandAssetId(accept.asset);
118
+ if (assetId === null) {
119
+ throw new UnsupportedSchemeError(`Algorand exact rail asset "${accept.asset}" must be a numeric ASA id.`);
120
+ }
121
+ if (!algosdk.isValidAddress(feePayer)) {
122
+ throw new UnsupportedSchemeError(`Algorand exact: extra.feePayer "${feePayer}" is not a valid Algorand address.`);
123
+ }
124
+ if (!algosdk.isValidAddress(accept.payTo)) {
125
+ throw new UnsupportedSchemeError(`Algorand exact: payTo "${accept.payTo}" is not a valid Algorand address.`);
126
+ }
127
+ if (feePayer === sender) {
128
+ throw new UnsupportedSchemeError(
129
+ "Algorand exact: the fee payer must differ from the payer \u2014 the buyer must pay ZERO ALGO (the sponsor pays the pooled fee)."
130
+ );
131
+ }
132
+ const minFee = suggestedParams.minFee != null ? BigInt(suggestedParams.minFee) : 1000n;
133
+ const groupFee = minFee * BigInt(GROUP_SIZE);
134
+ const axfer = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({
135
+ sender,
136
+ receiver: accept.payTo,
137
+ amount: BigInt(accept.amount),
138
+ assetIndex: assetId,
139
+ note: input.note ?? randomNote(),
140
+ suggestedParams: { ...suggestedParams, flatFee: true, fee: 0n }
141
+ });
142
+ const feeTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
143
+ sender: feePayer,
144
+ receiver: feePayer,
145
+ amount: 0n,
146
+ suggestedParams: { ...suggestedParams, flatFee: true, fee: groupFee }
147
+ });
148
+ algosdk.assignGroupID([axfer, feeTxn]);
149
+ const signedAxfer = axfer.signTxn(sk);
150
+ const unsignedFee = algosdk.encodeUnsignedTransaction(feeTxn);
151
+ return {
152
+ payload: { paymentIndex: 0, paymentGroup: [b64encode(signedAxfer), b64encode(unsignedFee)] },
153
+ payerFrom: sender,
154
+ nonce: axfer.txID()
155
+ };
156
+ }
157
+ function shorten(msg) {
158
+ const oneLine = msg.replace(/\s+/g, " ").trim();
159
+ return oneLine.length > 200 ? `${oneLine.slice(0, 200)}\u2026` : oneLine;
160
+ }
161
+ function fail(error, detail) {
162
+ return { ok: false, error, detail };
163
+ }
164
+ function decodeElement(bytes) {
165
+ try {
166
+ const st = algosdk.decodeSignedTransaction(bytes);
167
+ return { txn: st.txn, signed: st.sig != null || st.msig != null || st.lsig != null, raw: bytes };
168
+ } catch {
169
+ return { txn: algosdk.decodeUnsignedTransaction(bytes), signed: false, raw: bytes };
170
+ }
171
+ }
172
+ async function verifyAndSettleExactAlgorand(input) {
173
+ const { client, feePayerSk, feePayerAddr, payload, accept } = input;
174
+ const railFeePayer = accept.extra.feePayer;
175
+ if (!railFeePayer) throw new SettlementError("Algorand exact: rail is missing extra.feePayer.");
176
+ if (railFeePayer !== feePayerAddr) {
177
+ throw new SettlementError("Algorand exact: the gate relayer address does not match the rail extra.feePayer \u2014 misconfigured rail.");
178
+ }
179
+ const assetId = parseAlgorandAssetId(accept.asset);
180
+ if (assetId === null) throw new SettlementError(`Algorand exact: rail asset "${accept.asset}" is not a numeric ASA id.`);
181
+ if (!algosdk.isValidAddress(accept.payTo)) throw new SettlementError(`Algorand exact: rail payTo "${accept.payTo}" is invalid.`);
182
+ const { paymentIndex, paymentGroup } = payload;
183
+ if (paymentGroup.length !== GROUP_SIZE) {
184
+ return fail("signature_invalid", `Algorand exact expects a ${GROUP_SIZE}-transaction group (payment + fee), got ${paymentGroup.length}.`);
185
+ }
186
+ if (!Number.isInteger(paymentIndex) || paymentIndex < 0 || paymentIndex >= GROUP_SIZE) {
187
+ return fail("signature_invalid", `paymentIndex ${paymentIndex} is out of range for a ${GROUP_SIZE}-txn group.`);
188
+ }
189
+ let elements;
190
+ try {
191
+ elements = paymentGroup.map((b64) => decodeElement(b64decode(b64)));
192
+ } catch (err) {
193
+ return fail("signature_invalid", `Unparseable Algorand group: ${shorten(err instanceof Error ? err.message : String(err))}.`);
194
+ }
195
+ const payElement = elements[paymentIndex];
196
+ const feeElement = elements[paymentIndex === 0 ? 1 : 0];
197
+ if (!payElement.signed) {
198
+ return fail("signature_invalid", "The payment transaction is not signed by the buyer.");
199
+ }
200
+ const pay = payElement.txn;
201
+ if (pay.type !== "axfer" || !pay.assetTransfer) {
202
+ return fail("transfer_not_found", "The payment transaction is not an ASA asset-transfer (axfer).");
203
+ }
204
+ const at = pay.assetTransfer;
205
+ if (BigInt(at.assetIndex) !== BigInt(assetId)) {
206
+ return fail("transfer_not_found", `Transfer asset id ${at.assetIndex} \u2260 rail asset ${assetId}.`);
207
+ }
208
+ if (!addrEq(at.receiver, accept.payTo)) {
209
+ return fail("wrong_recipient", `Transfer pays ${at.receiver?.toString()}, not payTo ${accept.payTo}.`);
210
+ }
211
+ if (BigInt(at.amount) < BigInt(accept.amount)) {
212
+ return fail("amount_too_low", `Transfer pays ${at.amount} of the ASA, required ${accept.amount}.`);
213
+ }
214
+ if (isSet(at.closeRemainderTo)) return fail("signature_invalid", "The payment transaction has an asset-close \u2014 refused.");
215
+ if (isSet(pay.rekeyTo)) return fail("signature_invalid", "The payment transaction has a rekey \u2014 refused.");
216
+ if (feeElement.signed) {
217
+ return fail("signature_invalid", "The fee transaction must be left UNSIGNED for the sponsor to sign.");
218
+ }
219
+ const fee = feeElement.txn;
220
+ if (fee.type !== "pay" || !fee.payment) {
221
+ return fail("signature_invalid", "The fee transaction must be a payment (pay) transaction.");
222
+ }
223
+ if (!addrEq(fee.sender, railFeePayer)) {
224
+ return fail("signature_invalid", `The fee transaction sender ${fee.sender?.toString()} is not the rail fee payer ${railFeePayer}.`);
225
+ }
226
+ if (BigInt(fee.payment.amount) !== 0n) {
227
+ return fail("signature_invalid", "The fee transaction must move 0 ALGO (it only pools the group fee).");
228
+ }
229
+ const feeAmount = fee.fee != null ? BigInt(fee.fee) : 0n;
230
+ if (feeAmount > MAX_GROUP_FEE) {
231
+ return fail("signature_invalid", `The fee transaction fee ${feeAmount} \xB5ALGO exceeds the ${MAX_GROUP_FEE} cap (fee-payer drain guard).`);
232
+ }
233
+ if (isSet(fee.payment.closeRemainderTo)) return fail("signature_invalid", "The fee transaction has an account-close \u2014 refused.");
234
+ if (isSet(fee.rekeyTo)) return fail("signature_invalid", "The fee transaction has a rekey \u2014 refused.");
235
+ const payGid = pay.group;
236
+ const feeGid = fee.group;
237
+ if (!payGid || !feeGid || payGid.length === 0) {
238
+ return fail("signature_invalid", "The transactions are not part of an atomic group.");
239
+ }
240
+ if (Buffer.from(payGid).toString("hex") !== Buffer.from(feeGid).toString("hex")) {
241
+ return fail("signature_invalid", "The payment and fee transactions belong to different groups.");
242
+ }
243
+ let feeSigned;
244
+ try {
245
+ feeSigned = fee.signTxn(feePayerSk);
246
+ } catch (err) {
247
+ throw new SettlementError(`Algorand exact settle: the gate could not sign the fee transaction (${shorten(err instanceof Error ? err.message : String(err))}).`, { cause: err });
248
+ }
249
+ const signedGroup = elements.map((el, i) => i === paymentIndex ? payElement.raw : feeSigned);
250
+ try {
251
+ const sim = await client.simulate(signedGroup);
252
+ if (sim.failureMessage) {
253
+ const m = sim.failureMessage;
254
+ if (/signature|sig|auth/i.test(m)) return fail("signature_invalid", `Signature verification failed: ${shorten(m)}.`);
255
+ if (/\bround\b|expired|lifetime|validity|dead/i.test(m)) return fail("payment_expired", `Transaction validity window is no longer valid: ${shorten(m)}.`);
256
+ if (/overspend|below min|underflow|insufficient|asset|frozen|opt|missing/i.test(m)) return fail("tx_reverted", `Group would fail on-chain: ${shorten(m)}.`);
257
+ return fail("tx_reverted", `Group would fail on-chain: ${shorten(m)}.`);
258
+ }
259
+ } catch {
260
+ }
261
+ const buyerTxid = pay.txID();
262
+ try {
263
+ await client.submit(signedGroup);
264
+ } catch (err) {
265
+ const m = err instanceof Error ? err.message : String(err);
266
+ if (/signature|sig|auth|malformed/i.test(m)) return fail("signature_invalid", `Settle rejected \u2014 bad signature: ${shorten(m)}.`);
267
+ if (/already in ledger|already committed|duplicate/i.test(m)) return fail("tx_already_used", `This payment was already settled: ${shorten(m)}.`);
268
+ if (/\bround\b|expired|lifetime|stale|validity/i.test(m)) return fail("payment_expired", `Settle rejected \u2014 validity window expired: ${shorten(m)}.`);
269
+ throw new SettlementError(
270
+ `Algorand exact settle: the group failed to submit (${shorten(m)}). The buyer's signed transaction is still valid \u2014 fund/fix the fee payer (${railFeePayer}) and the buyer can re-present it.`,
271
+ { cause: err }
272
+ );
273
+ }
274
+ let round;
275
+ try {
276
+ round = await client.waitForConfirmation(buyerTxid);
277
+ } catch (err) {
278
+ throw new SettlementError(`Algorand exact settle: submitted ${buyerTxid} but confirmation read failed (${shorten(err instanceof Error ? err.message : String(err))}). It likely landed \u2014 re-verify before re-presenting; do NOT re-pay.`, { cause: err });
279
+ }
280
+ if (round === null) {
281
+ throw new SettlementError(`Algorand exact settle: submitted ${buyerTxid} but it did not confirm in time. It likely landed \u2014 re-verify before re-presenting; do NOT re-pay.`);
282
+ }
283
+ return {
284
+ ok: true,
285
+ receipt: {
286
+ scheme: "exact",
287
+ success: true,
288
+ network: accept.network,
289
+ transaction: buyerTxid,
290
+ asset: accept.asset,
291
+ amount: accept.amount,
292
+ payer: pay.sender.toString(),
293
+ payTo: accept.payTo,
294
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
295
+ }
296
+ };
297
+ }
298
+
299
+ // src/drivers/algorand/verify.ts
300
+ async function verifyAlgorand(params) {
301
+ const { reader, accept } = params;
302
+ const nonce = accept.extra.nonce;
303
+ const required = BigInt(accept.amount);
304
+ const wantAssetId = parseAlgorandAssetId(accept.asset);
305
+ let txs;
306
+ try {
307
+ txs = await reader.transactionsForAccount(accept.payTo, 50);
308
+ } catch {
309
+ return rpcFailed(nonce);
310
+ }
311
+ const tx = txs.find((t) => typeof t.note === "string" && t.note === nonce);
312
+ if (!tx) return notFound(nonce);
313
+ if (typeof tx.roundTime === "number") {
314
+ const ageSeconds = Math.floor(Date.now() / 1e3) - tx.roundTime;
315
+ if (Number.isFinite(ageSeconds) && ageSeconds > accept.maxTimeoutSeconds) {
316
+ return {
317
+ ok: false,
318
+ error: "payment_expired",
319
+ detail: `Payment is ${ageSeconds}s old; max allowed is ${accept.maxTimeoutSeconds}s.`
320
+ };
321
+ }
322
+ }
323
+ const isNative = wantAssetId === null;
324
+ const typeOk = isNative ? tx.txType === "pay" : tx.txType === "axfer";
325
+ const assetOk = isNative ? tx.assetId == null : tx.assetId === wantAssetId;
326
+ if (!typeOk || tx.receiver !== accept.payTo || !assetOk) {
327
+ return {
328
+ ok: false,
329
+ error: "transfer_not_found",
330
+ detail: `Algorand tx ${tx.id} carries our nonce but has no matching ${isNative ? "ALGO" : `ASA ${wantAssetId}`} transfer to ${accept.payTo}.`
331
+ };
332
+ }
333
+ let paid = 0n;
334
+ try {
335
+ paid = tx.amount ? BigInt(tx.amount) : 0n;
336
+ } catch {
337
+ paid = 0n;
338
+ }
339
+ if (paid < required) {
340
+ return { ok: false, error: "amount_too_low", detail: `Paid ${paid}, required ${required}.` };
341
+ }
342
+ return {
343
+ ok: true,
344
+ receipt: {
345
+ scheme: "onchain-proof",
346
+ success: true,
347
+ network: accept.network,
348
+ transaction: tx.id,
349
+ asset: accept.asset,
350
+ amount: accept.amount,
351
+ payer: tx.sender ?? "",
352
+ payTo: accept.payTo,
353
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
354
+ }
355
+ };
356
+ }
357
+ function notFound(nonce) {
358
+ return {
359
+ ok: false,
360
+ error: "transfer_not_found",
361
+ detail: `No matching Algorand payment found for nonce ${nonce} (not yet settled, or wrong recipient/amount/asset/note).`
362
+ };
363
+ }
364
+ function rpcFailed(nonce) {
365
+ return {
366
+ ok: false,
367
+ error: "tx_not_found",
368
+ detail: `Could not read the Algorand indexer for nonce ${nonce} (transient RPC failure) \u2014 retry.`
369
+ };
370
+ }
371
+
372
+ // src/drivers/algorand/wallet.ts
373
+ import algosdk2 from "algosdk";
374
+ function assertAlgorandWallet(wallet, network) {
375
+ if (typeof wallet !== "object" || wallet === null) {
376
+ throw new WrongFamilyError(
377
+ `chain ${network} is Algorand; wallet must be { key } (25-word mnemonic) or { account }.`
378
+ );
379
+ }
380
+ assertNoLegacyWalletKey(wallet, "Algorand");
381
+ if (!("key" in wallet) && !("account" in wallet)) {
382
+ throw new WrongFamilyError(
383
+ `chain ${network} is Algorand; wallet must be { key } (25-word mnemonic) or { account }.`
384
+ );
385
+ }
386
+ return wallet;
387
+ }
388
+ function resolveAlgorandWallet(config) {
389
+ if (config.account) {
390
+ return { addr: String(config.account.addr), sk: config.account.sk };
391
+ }
392
+ if (config.key != null) {
393
+ try {
394
+ const { addr, sk } = algosdk2.mnemonicToSecretKey(config.key);
395
+ return { addr: addr.toString(), sk };
396
+ } catch (cause) {
397
+ throw new WrongFamilyError(
398
+ "Algorand wallet { key } is not a valid 25-word Algorand mnemonic.",
399
+ { cause }
400
+ );
401
+ }
402
+ }
403
+ throw new WrongFamilyError("Algorand wallet needs { key } (25-word mnemonic) or { account }.");
404
+ }
405
+
406
+ // src/drivers/algorand/index.ts
407
+ var algorandDriver = {
408
+ family: "algorand",
409
+ resolve(opts) {
410
+ if (opts.chain !== "algorand") return null;
411
+ const algodUrl = opts.rpcUrl ?? ALGORAND_MAINNET.defaultAlgod;
412
+ return makeAlgorandNetwork(ALGORAND_MAINNET, algodUrl);
413
+ }
414
+ };
415
+ function makeAlgorandNetwork(preset, algodUrl) {
416
+ const algod = new algosdk3.Algodv2("", algodUrl, "");
417
+ const indexer = new algosdk3.Indexer("", preset.defaultIndexer, "");
418
+ const network = preset.caip2;
419
+ const reader = {
420
+ async transactionsForAccount(account, limit) {
421
+ const res = await indexer.lookupAccountTransactions(account).limit(limit).do();
422
+ return (res.transactions ?? []).map((t) => adaptTxn(t)).filter((r) => r !== null);
423
+ }
424
+ };
425
+ const payClient = {
426
+ async build(transfer) {
427
+ const suggestedParams = await algod.getTransactionParams().do();
428
+ const common = {
429
+ sender: transfer.sender,
430
+ receiver: transfer.receiver,
431
+ amount: transfer.amount,
432
+ note: transfer.note,
433
+ suggestedParams
434
+ };
435
+ const txn = transfer.assetId === void 0 ? algosdk3.makePaymentTxnWithSuggestedParamsFromObject(common) : algosdk3.makeAssetTransferTxnWithSuggestedParamsFromObject({
436
+ ...common,
437
+ assetIndex: transfer.assetId
438
+ });
439
+ return { txn, txId: txn.txID() };
440
+ },
441
+ async signSend({ txn, sk }) {
442
+ const signed = txn.signTxn(sk);
443
+ await algod.sendRawTransaction(signed).do();
444
+ }
445
+ };
446
+ const settleClient = {
447
+ async simulate(signedGroup) {
448
+ const resp = await algod.simulateRawTransactions(signedGroup).do();
449
+ const fm = resp?.txnGroups?.[0]?.failureMessage;
450
+ return { failureMessage: fm && fm.length > 0 ? fm : null };
451
+ },
452
+ async submit(signedGroup) {
453
+ const res = await algod.sendRawTransaction(signedGroup).do();
454
+ return res?.txid ?? "";
455
+ },
456
+ async waitForConfirmation(txid) {
457
+ const info = await algosdk3.waitForConfirmation(algod, txid, 10);
458
+ const r = info.confirmedRound;
459
+ return r != null && Number(r) > 0 ? Number(r) : null;
460
+ }
461
+ };
462
+ return {
463
+ family: "algorand",
464
+ network,
465
+ supports: (n) => n === network,
466
+ resolveToken(token) {
467
+ if (token === "native") {
468
+ return { asset: "native", decimals: ALGO_DECIMALS, symbol: ALGO_SYMBOL };
469
+ }
470
+ if (typeof token === "string") {
471
+ const info = preset.tokens[token.toUpperCase()];
472
+ if (!info) {
473
+ const known = Object.keys(preset.tokens).join(", ") || "(none built in)";
474
+ throw new UnknownTokenError(
475
+ `token "${token}" isn't built in for Algorand (known: ${known}). Pass { assetId, decimals } for a custom ASA, or use 'native'.`
476
+ );
477
+ }
478
+ return { asset: algorandAssetId(info.assetId), decimals: info.decimals, symbol: info.symbol };
479
+ }
480
+ rejectForeignToken(token, "algorand", network);
481
+ const t = token;
482
+ if (typeof t.assetId !== "number" || typeof t.decimals !== "number") {
483
+ throw new WrongFamilyError(
484
+ `chain ${network} is Algorand; a custom token must be { assetId, decimals }.`
485
+ );
486
+ }
487
+ return {
488
+ asset: algorandAssetId(t.assetId),
489
+ decimals: t.decimals,
490
+ ...t.symbol ? { symbol: t.symbol } : {}
491
+ };
492
+ },
493
+ describeAsset(asset) {
494
+ if (asset === "native") return { symbol: ALGO_SYMBOL, decimals: ALGO_DECIMALS };
495
+ for (const info of Object.values(preset.tokens)) {
496
+ if (algorandAssetId(info.assetId) === asset) {
497
+ return { symbol: info.symbol, decimals: info.decimals };
498
+ }
499
+ }
500
+ return null;
501
+ },
502
+ assertValidPayTo(payTo) {
503
+ if (payTo.startsWith("0x")) {
504
+ throw new WrongFamilyError(
505
+ `chain ${network} is Algorand, but payTo "${payTo}" looks like an EVM address.`
506
+ );
507
+ }
508
+ if (!algosdk3.isValidAddress(payTo)) {
509
+ throw new WrongFamilyError(
510
+ `chain ${network} is Algorand, but payTo "${payTo}" is not a valid Algorand address.`
511
+ );
512
+ }
513
+ },
514
+ bindWallet(wallet) {
515
+ return { _native: assertAlgorandWallet(wallet, network) };
516
+ },
517
+ async send(wallet, accept) {
518
+ const signer = resolveAlgorandWallet(wallet._native);
519
+ return payAlgorand({ client: payClient, sk: signer.sk, sender: signer.addr, accept });
520
+ },
521
+ async confirm(ref) {
522
+ try {
523
+ const info = await algosdk3.waitForConfirmation(algod, ref, 10);
524
+ return { height: String(info.confirmedRound ?? 0) };
525
+ } catch (err) {
526
+ throw new ConfirmationTimeoutError(`Algorand tx ${ref} did not confirm in time.`, {
527
+ cause: err
528
+ });
529
+ }
530
+ },
531
+ async estimateCost(accept) {
532
+ if (accept.scheme === "exact") {
533
+ return nativeCost({
534
+ symbol: ALGO_SYMBOL,
535
+ decimals: ALGO_DECIMALS,
536
+ fee: 0n,
537
+ basis: "estimated",
538
+ detail: "gasless \u2014 the fee payer (facilitator/relayer) pools the group fee; the buyer pays 0 ALGO"
539
+ });
540
+ }
541
+ return nativeCost({
542
+ symbol: ALGO_SYMBOL,
543
+ decimals: ALGO_DECIMALS,
544
+ fee: 1000n,
545
+ basis: "heuristic",
546
+ detail: "min fee 1000 \xB5Algos (1 transaction)"
547
+ });
548
+ },
549
+ async balanceOf(wallet, asset) {
550
+ let owner;
551
+ try {
552
+ owner = resolveAlgorandWallet(wallet._native).addr;
553
+ } catch {
554
+ return { token: null, native: null };
555
+ }
556
+ let info;
557
+ try {
558
+ info = await algod.accountInformation(owner).do();
559
+ } catch {
560
+ return { token: null, native: null };
561
+ }
562
+ const native = info.amount != null ? BigInt(info.amount) : null;
563
+ if (asset === "native") return { token: native, native };
564
+ const assetId = parseAlgorandAssetId(asset);
565
+ const holding = (info.assets ?? []).find((a) => Number(a.assetId) === assetId);
566
+ return { token: holding ? BigInt(holding.amount) : 0n, native };
567
+ },
568
+ async recipientReady(payTo, asset) {
569
+ if (asset === "native") return { ready: "n/a" };
570
+ const assetId = parseAlgorandAssetId(asset);
571
+ if (assetId == null) return { ready: "unknown" };
572
+ try {
573
+ const info = await algod.accountInformation(payTo).do();
574
+ const optedIn = (info.assets ?? []).some((a) => Number(a.assetId) === assetId);
575
+ return optedIn ? { ready: true } : { ready: false, reason: "NOT_OPTED_IN" };
576
+ } catch (e) {
577
+ if (/does not exist|no accounts found|404|account not found/i.test(String(e?.message ?? e))) {
578
+ return { ready: false, reason: "NOT_OPTED_IN" };
579
+ }
580
+ return { ready: "unknown" };
581
+ }
582
+ },
583
+ async verify(_ref, accept) {
584
+ return verifyAlgorand({ reader, accept });
585
+ },
586
+ // Standard x402 `exact` rail, BUYER side — build the canonical 2-txn group (buyer axfer at
587
+ // fee 0 + the fee payer's pooled-fee pay txn), sign ONLY the axfer (the buyer spends zero
588
+ // ALGO). Never submits. Throws UnsupportedSchemeError for native / a missing feePayer / a
589
+ // feePayer that equals the payer.
590
+ async payExact(wallet, accept) {
591
+ const signer = resolveAlgorandWallet(wallet._native);
592
+ const suggestedParams = await algod.getTransactionParams().do();
593
+ const { payload, payerFrom, nonce } = await payExactAlgorand({
594
+ suggestedParams,
595
+ sk: signer.sk,
596
+ sender: signer.addr,
597
+ accept
598
+ });
599
+ return { payload, accepted: accept, payerFrom, nonce };
600
+ },
601
+ // Standard x402 `exact` rail, SELLER side — verify the inbound group against the trusted
602
+ // accept, then co-sign the fee txn as the fee payer + submit the group (self-settle, no
603
+ // facilitator). The buyer stays gasless; the merchant's relayer pays the pooled group fee.
604
+ async settleExactSelf({ relayer, payload, accept }) {
605
+ if (!("paymentGroup" in payload)) {
606
+ return { ok: false, error: "signature_invalid", detail: "Algorand exact expects a { paymentGroup } payload." };
607
+ }
608
+ const signer = resolveAlgorandWallet(relayer._native);
609
+ return verifyAndSettleExactAlgorand({
610
+ client: settleClient,
611
+ feePayerSk: signer.sk,
612
+ feePayerAddr: signer.addr,
613
+ payload,
614
+ accept
615
+ });
616
+ },
617
+ // The gate's rail-advertisement SPI. The fee payer is EITHER a keyless facilitator's sponsor
618
+ // address (`feePayer` — facilitator mode, neither buyer nor merchant pays gas) OR the
619
+ // merchant's own bound `relayer` (self mode); native ALGO isn't exact-payable. `null` ⇒ no
620
+ // exact rail. The buyer fee-pools the group, so the merchant may even reuse `payTo` as the
621
+ // relayer (Algorand allows feePayer === payTo — the fee txn is separate, unlike Solana).
622
+ async resolveExactRail({ asset, relayer, feePayer }) {
623
+ if (asset === "native") return null;
624
+ let fp = feePayer;
625
+ if (!fp && relayer) {
626
+ try {
627
+ fp = resolveAlgorandWallet(relayer._native).addr;
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+ if (!fp || !algosdk3.isValidAddress(fp)) return null;
633
+ return { method: "algorand", extra: { feePayer: fp } };
634
+ }
635
+ };
636
+ }
637
+ function adaptTxn(raw) {
638
+ const t = raw;
639
+ if (!t || typeof t.id !== "string") return null;
640
+ const note = t.note && t.note.length ? decodeNote(t.note) : void 0;
641
+ const base = {
642
+ id: t.id,
643
+ txType: String(t.txType ?? ""),
644
+ ...note !== void 0 ? { note } : {},
645
+ ...t.sender != null ? { sender: String(t.sender) } : {},
646
+ ...typeof t.roundTime === "number" ? { roundTime: t.roundTime } : {}
647
+ };
648
+ if (t.paymentTransaction) {
649
+ const pt = t.paymentTransaction;
650
+ return {
651
+ ...base,
652
+ txType: "pay",
653
+ ...pt.receiver != null ? { receiver: String(pt.receiver) } : {},
654
+ ...pt.amount != null ? { amount: String(pt.amount) } : {}
655
+ };
656
+ }
657
+ if (t.assetTransferTransaction) {
658
+ const att = t.assetTransferTransaction;
659
+ return {
660
+ ...base,
661
+ txType: "axfer",
662
+ ...att.receiver != null ? { receiver: String(att.receiver) } : {},
663
+ ...att.amount != null ? { amount: String(att.amount) } : {},
664
+ ...att.assetId != null ? { assetId: Number(att.assetId) } : {}
665
+ };
666
+ }
667
+ return base;
668
+ }
669
+ function decodeNote(bytes) {
670
+ try {
671
+ return new TextDecoder().decode(bytes);
672
+ } catch {
673
+ return void 0;
674
+ }
675
+ }
676
+ export {
677
+ algorandDriver
678
+ };