@piprail/sdk 2.15.1 → 2.16.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.
- package/CHANGELOG.md +209 -1
- package/README.md +1 -1
- package/dist/{algorand-UF7BLBFN.js → algorand-25FMBCT3.js} +12 -3
- package/dist/{algorand-XGSWDMCZ.cjs → algorand-Q3TUQLPK.cjs} +17 -8
- package/dist/{aptos-KDLX42CR.js → aptos-SBV6SGDP.js} +2 -2
- package/dist/{aptos-J2QSWHTO.cjs → aptos-VBJONBFY.cjs} +5 -5
- package/dist/chunk-2CX7XRZK.js +649 -0
- package/dist/chunk-C52H5TYB.js +474 -0
- package/dist/chunk-OXEFPLZA.cjs +474 -0
- package/dist/chunk-V2IJ5HUW.cjs +649 -0
- package/dist/index.cjs +362 -1181
- package/dist/index.d.cts +39 -6
- package/dist/index.d.ts +39 -6
- package/dist/index.js +178 -997
- package/dist/{ledger-uFtXlIHY.d.cts → ledger-Crc1bZox.d.cts} +164 -10
- package/dist/{ledger-uFtXlIHY.d.ts → ledger-Crc1bZox.d.ts} +164 -10
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/{solana-S2T3JNR7.cjs → solana-AI2G7V33.cjs} +42 -21
- package/dist/{solana-WO2G7WWZ.js → solana-HTKDRTD3.js} +31 -10
- package/dist/{xrpl-6B2XOXGC.js → xrpl-RUOB37QH.js} +282 -4
- package/dist/{xrpl-VOUXIUYD.cjs → xrpl-SVVS445B.cjs} +292 -14
- package/package.json +1 -1
|
@@ -229,14 +229,24 @@ var MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 100000n;
|
|
|
229
229
|
var CB_SET_UNIT_LIMIT = 2;
|
|
230
230
|
var CB_SET_UNIT_PRICE = 3;
|
|
231
231
|
function tokenProgramFor(accept) {
|
|
232
|
-
return accept.extra.tokenProgram === "token-2022" ? _spltoken.TOKEN_2022_PROGRAM_ID : _spltoken.TOKEN_PROGRAM_ID;
|
|
232
|
+
return _optionalChain([accept, 'access', _3 => _3.extra, 'optionalAccess', _4 => _4.tokenProgram]) === "token-2022" ? _spltoken.TOKEN_2022_PROGRAM_ID : _spltoken.TOKEN_PROGRAM_ID;
|
|
233
|
+
}
|
|
234
|
+
async function readMintDecimals(connection, mint) {
|
|
235
|
+
try {
|
|
236
|
+
const info = await connection.getAccountInfo(mint);
|
|
237
|
+
const data = _optionalChain([info, 'optionalAccess', _5 => _5.data]);
|
|
238
|
+
if (!data || data.length < 45) return void 0;
|
|
239
|
+
return data[44];
|
|
240
|
+
} catch (e3) {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
233
243
|
}
|
|
234
244
|
function isEmptySig(sig) {
|
|
235
245
|
return !sig || sig.every((b) => b === 0);
|
|
236
246
|
}
|
|
237
247
|
function memoRandomNonce() {
|
|
238
248
|
const g = globalThis.crypto;
|
|
239
|
-
if (!_optionalChain([g, 'optionalAccess',
|
|
249
|
+
if (!_optionalChain([g, 'optionalAccess', _6 => _6.getRandomValues])) {
|
|
240
250
|
throw new (0, _chunkQONQSZHJcjs.UnsupportedSchemeError)(
|
|
241
251
|
"this runtime lacks Web Crypto (globalThis.crypto.getRandomValues); SVM exact needs a CSPRNG nonce."
|
|
242
252
|
);
|
|
@@ -246,7 +256,7 @@ function memoRandomNonce() {
|
|
|
246
256
|
return Buffer.from([...raw].map((b) => b.toString(16).padStart(2, "0")).join(""), "utf8");
|
|
247
257
|
}
|
|
248
258
|
function memoDataFor(accept) {
|
|
249
|
-
if (accept.extra.memo !== void 0) {
|
|
259
|
+
if (_optionalChain([accept, 'access', _7 => _7.extra, 'optionalAccess', _8 => _8.memo]) !== void 0) {
|
|
250
260
|
const bytes = Buffer.from(accept.extra.memo, "utf8");
|
|
251
261
|
if (bytes.length > MAX_MEMO_BYTES) {
|
|
252
262
|
throw new (0, _chunkQONQSZHJcjs.UnsupportedSchemeError)(
|
|
@@ -267,17 +277,15 @@ async function payExactSolana(input) {
|
|
|
267
277
|
"SVM exact is SPL-token only (TransferChecked); native SOL is not exact-payable. Pay via onchain-proof."
|
|
268
278
|
);
|
|
269
279
|
}
|
|
270
|
-
|
|
280
|
+
const extra = _nullishCoalesce(accept.extra, () => ( {}));
|
|
281
|
+
if (!extra.feePayer) {
|
|
271
282
|
throw new (0, _chunkQONQSZHJcjs.UnsupportedSchemeError)("SVM exact rail must advertise extra.feePayer (the merchant sponsor key).");
|
|
272
283
|
}
|
|
273
|
-
if (accept.extra.decimals === void 0) {
|
|
274
|
-
throw new (0, _chunkQONQSZHJcjs.UnsupportedSchemeError)("SVM exact rail must advertise extra.decimals for the TransferChecked.");
|
|
275
|
-
}
|
|
276
284
|
let feePayer;
|
|
277
285
|
let mint;
|
|
278
286
|
let payTo;
|
|
279
287
|
try {
|
|
280
|
-
feePayer = new (0, _web3js.PublicKey)(
|
|
288
|
+
feePayer = new (0, _web3js.PublicKey)(extra.feePayer);
|
|
281
289
|
mint = new (0, _web3js.PublicKey)(accept.asset);
|
|
282
290
|
payTo = new (0, _web3js.PublicKey)(accept.payTo);
|
|
283
291
|
} catch (err) {
|
|
@@ -285,6 +293,18 @@ async function payExactSolana(input) {
|
|
|
285
293
|
`SVM exact: bad feePayer/asset/payTo (${err instanceof Error ? err.message : String(err)}).`
|
|
286
294
|
);
|
|
287
295
|
}
|
|
296
|
+
const onChainDecimals = await readMintDecimals(connection, mint);
|
|
297
|
+
const decimals = _nullishCoalesce(onChainDecimals, () => ( extra.decimals));
|
|
298
|
+
if (decimals === void 0) {
|
|
299
|
+
throw new (0, _chunkQONQSZHJcjs.UnsupportedSchemeError)(
|
|
300
|
+
`SVM exact: couldn't read the decimals of mint ${accept.asset} on-chain, and the rail states none. Retry with a reliable rpcUrl \u2014 TransferChecked cannot be built without them.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (onChainDecimals !== void 0 && extra.decimals !== void 0 && extra.decimals !== onChainDecimals) {
|
|
304
|
+
throw new (0, _chunkQONQSZHJcjs.UnsupportedSchemeError)(
|
|
305
|
+
`SVM exact: the rail states ${extra.decimals} decimals for ${accept.asset} but the mint says ${onChainDecimals} \u2014 refusing to sign (a mismatch this size misprices the transfer).`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
288
308
|
if (feePayer.equals(payTo)) {
|
|
289
309
|
throw new (0, _chunkQONQSZHJcjs.UnsupportedSchemeError)(
|
|
290
310
|
"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."
|
|
@@ -296,7 +316,7 @@ async function payExactSolana(input) {
|
|
|
296
316
|
let destInfo = "unknown";
|
|
297
317
|
try {
|
|
298
318
|
destInfo = await input.connection.getAccountInfo(dest);
|
|
299
|
-
} catch (
|
|
319
|
+
} catch (e4) {
|
|
300
320
|
destInfo = "unknown";
|
|
301
321
|
}
|
|
302
322
|
if (destInfo === null) {
|
|
@@ -314,7 +334,8 @@ async function payExactSolana(input) {
|
|
|
314
334
|
dest,
|
|
315
335
|
keypair.publicKey,
|
|
316
336
|
BigInt(accept.amount),
|
|
317
|
-
|
|
337
|
+
decimals,
|
|
338
|
+
// read from the MINT (see above), not from the server's `extra`
|
|
318
339
|
[],
|
|
319
340
|
program
|
|
320
341
|
),
|
|
@@ -359,7 +380,7 @@ async function verifyAndSettleExactSolana(input) {
|
|
|
359
380
|
try {
|
|
360
381
|
payTo = new (0, _web3js.PublicKey)(accept.payTo);
|
|
361
382
|
mint = new (0, _web3js.PublicKey)(accept.asset);
|
|
362
|
-
if (!accept.extra.feePayer) throw new Error("rail is missing extra.feePayer");
|
|
383
|
+
if (!_optionalChain([accept, 'access', _9 => _9.extra, 'optionalAccess', _10 => _10.feePayer])) throw new Error("rail is missing extra.feePayer");
|
|
363
384
|
railFeePayer = new (0, _web3js.PublicKey)(accept.extra.feePayer);
|
|
364
385
|
} catch (err) {
|
|
365
386
|
throw new (0, _chunkQONQSZHJcjs.SettlementError)(`SVM exact: rail has a bad payTo/asset/feePayer (${err instanceof Error ? err.message : String(err)}).`);
|
|
@@ -389,7 +410,7 @@ async function verifyAndSettleExactSolana(input) {
|
|
|
389
410
|
try {
|
|
390
411
|
;
|
|
391
412
|
({ value } = await connection.getAddressLookupTable(lookup.accountKey));
|
|
392
|
-
} catch (
|
|
413
|
+
} catch (e5) {
|
|
393
414
|
return fail("tx_not_found", `Could not read lookup table ${lookup.accountKey.toBase58()} (transient RPC) \u2014 retry.`);
|
|
394
415
|
}
|
|
395
416
|
if (!value) {
|
|
@@ -411,7 +432,7 @@ async function verifyAndSettleExactSolana(input) {
|
|
|
411
432
|
return fail("signature_invalid", "The fee payer is invoked as a program.");
|
|
412
433
|
}
|
|
413
434
|
for (const i of ix.accountKeyIndexes) {
|
|
414
|
-
if (_optionalChain([keys, 'access',
|
|
435
|
+
if (_optionalChain([keys, 'access', _11 => _11.get, 'call', _12 => _12(i), 'optionalAccess', _13 => _13.equals, 'call', _14 => _14(feePayerKey)])) {
|
|
415
436
|
return fail("signature_invalid", "The fee payer appears in an instruction account list (would risk a fund drain).");
|
|
416
437
|
}
|
|
417
438
|
}
|
|
@@ -460,14 +481,14 @@ async function verifyAndSettleExactSolana(input) {
|
|
|
460
481
|
}),
|
|
461
482
|
program
|
|
462
483
|
);
|
|
463
|
-
} catch (
|
|
484
|
+
} catch (e6) {
|
|
464
485
|
continue;
|
|
465
486
|
}
|
|
466
487
|
if (!decoded.keys.mint.pubkey.equals(mint)) continue;
|
|
467
488
|
if (!decoded.keys.destination.pubkey.equals(expectedDest)) {
|
|
468
489
|
return fail("wrong_recipient", `A transfer pays ${decoded.keys.destination.pubkey.toBase58()}, not payTo's ATA ${expectedDest.toBase58()}.`);
|
|
469
490
|
}
|
|
470
|
-
if (decoded.data.decimals !== accept.extra.decimals) {
|
|
491
|
+
if (_optionalChain([accept, 'access', _15 => _15.extra, 'optionalAccess', _16 => _16.decimals]) !== void 0 && decoded.data.decimals !== accept.extra.decimals) {
|
|
471
492
|
return fail("transfer_not_found", `Transfer decimals ${decoded.data.decimals} \u2260 rail decimals ${accept.extra.decimals}.`);
|
|
472
493
|
}
|
|
473
494
|
sawTransferToPayTo = true;
|
|
@@ -548,7 +569,7 @@ async function pollConfirmed(connection, signature) {
|
|
|
548
569
|
try {
|
|
549
570
|
const { value } = await connection.getSignatureStatuses([signature], { searchTransactionHistory: true });
|
|
550
571
|
info = value[0];
|
|
551
|
-
} catch (
|
|
572
|
+
} catch (e7) {
|
|
552
573
|
info = null;
|
|
553
574
|
}
|
|
554
575
|
if (info) {
|
|
@@ -651,7 +672,7 @@ function makeSolanaNetwork(preset, rpcUrl) {
|
|
|
651
672
|
}
|
|
652
673
|
try {
|
|
653
674
|
new (0, _web3js.PublicKey)(payTo);
|
|
654
|
-
} catch (
|
|
675
|
+
} catch (e8) {
|
|
655
676
|
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
656
677
|
`chain ${network} is Solana, but payTo "${payTo}" is not a base58 address.`
|
|
657
678
|
);
|
|
@@ -722,7 +743,7 @@ function makeSolanaNetwork(preset, rpcUrl) {
|
|
|
722
743
|
try {
|
|
723
744
|
const mint = new (0, _web3js.PublicKey)(asset);
|
|
724
745
|
const info = await connection.getAccountInfo(mint);
|
|
725
|
-
const programId = _optionalChain([info, 'optionalAccess',
|
|
746
|
+
const programId = _optionalChain([info, 'optionalAccess', _17 => _17.owner, 'access', _18 => _18.equals, 'call', _19 => _19(_spltoken.TOKEN_2022_PROGRAM_ID)]) ? _spltoken.TOKEN_2022_PROGRAM_ID : _spltoken.TOKEN_PROGRAM_ID;
|
|
726
747
|
const ata = _spltoken.getAssociatedTokenAddressSync.call(void 0, mint, owner, false, programId);
|
|
727
748
|
token = (await _spltoken.getAccount.call(void 0, connection, ata, "confirmed", programId)).amount;
|
|
728
749
|
} catch (e) {
|
|
@@ -780,14 +801,14 @@ function makeSolanaNetwork(preset, rpcUrl) {
|
|
|
780
801
|
if (!fp) return null;
|
|
781
802
|
try {
|
|
782
803
|
new (0, _web3js.PublicKey)(fp);
|
|
783
|
-
} catch (
|
|
804
|
+
} catch (e9) {
|
|
784
805
|
return null;
|
|
785
806
|
}
|
|
786
807
|
let tokenProgram = "spl-token";
|
|
787
808
|
try {
|
|
788
809
|
const info = await connection.getAccountInfo(new (0, _web3js.PublicKey)(asset));
|
|
789
|
-
if (_optionalChain([info, 'optionalAccess',
|
|
790
|
-
} catch (
|
|
810
|
+
if (_optionalChain([info, 'optionalAccess', _20 => _20.owner, 'access', _21 => _21.equals, 'call', _22 => _22(_spltoken.TOKEN_2022_PROGRAM_ID)])) tokenProgram = "token-2022";
|
|
811
|
+
} catch (e10) {
|
|
791
812
|
}
|
|
792
813
|
return { method: "svm", extra: { feePayer: fp, tokenProgram } };
|
|
793
814
|
}
|
|
@@ -229,7 +229,17 @@ var MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 100000n;
|
|
|
229
229
|
var CB_SET_UNIT_LIMIT = 2;
|
|
230
230
|
var CB_SET_UNIT_PRICE = 3;
|
|
231
231
|
function tokenProgramFor(accept) {
|
|
232
|
-
return accept.extra
|
|
232
|
+
return accept.extra?.tokenProgram === "token-2022" ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
233
|
+
}
|
|
234
|
+
async function readMintDecimals(connection, mint) {
|
|
235
|
+
try {
|
|
236
|
+
const info = await connection.getAccountInfo(mint);
|
|
237
|
+
const data = info?.data;
|
|
238
|
+
if (!data || data.length < 45) return void 0;
|
|
239
|
+
return data[44];
|
|
240
|
+
} catch {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
233
243
|
}
|
|
234
244
|
function isEmptySig(sig) {
|
|
235
245
|
return !sig || sig.every((b) => b === 0);
|
|
@@ -246,7 +256,7 @@ function memoRandomNonce() {
|
|
|
246
256
|
return Buffer.from([...raw].map((b) => b.toString(16).padStart(2, "0")).join(""), "utf8");
|
|
247
257
|
}
|
|
248
258
|
function memoDataFor(accept) {
|
|
249
|
-
if (accept.extra
|
|
259
|
+
if (accept.extra?.memo !== void 0) {
|
|
250
260
|
const bytes = Buffer.from(accept.extra.memo, "utf8");
|
|
251
261
|
if (bytes.length > MAX_MEMO_BYTES) {
|
|
252
262
|
throw new UnsupportedSchemeError(
|
|
@@ -267,17 +277,15 @@ async function payExactSolana(input) {
|
|
|
267
277
|
"SVM exact is SPL-token only (TransferChecked); native SOL is not exact-payable. Pay via onchain-proof."
|
|
268
278
|
);
|
|
269
279
|
}
|
|
270
|
-
|
|
280
|
+
const extra = accept.extra ?? {};
|
|
281
|
+
if (!extra.feePayer) {
|
|
271
282
|
throw new UnsupportedSchemeError("SVM exact rail must advertise extra.feePayer (the merchant sponsor key).");
|
|
272
283
|
}
|
|
273
|
-
if (accept.extra.decimals === void 0) {
|
|
274
|
-
throw new UnsupportedSchemeError("SVM exact rail must advertise extra.decimals for the TransferChecked.");
|
|
275
|
-
}
|
|
276
284
|
let feePayer;
|
|
277
285
|
let mint;
|
|
278
286
|
let payTo;
|
|
279
287
|
try {
|
|
280
|
-
feePayer = new PublicKey2(
|
|
288
|
+
feePayer = new PublicKey2(extra.feePayer);
|
|
281
289
|
mint = new PublicKey2(accept.asset);
|
|
282
290
|
payTo = new PublicKey2(accept.payTo);
|
|
283
291
|
} catch (err) {
|
|
@@ -285,6 +293,18 @@ async function payExactSolana(input) {
|
|
|
285
293
|
`SVM exact: bad feePayer/asset/payTo (${err instanceof Error ? err.message : String(err)}).`
|
|
286
294
|
);
|
|
287
295
|
}
|
|
296
|
+
const onChainDecimals = await readMintDecimals(connection, mint);
|
|
297
|
+
const decimals = onChainDecimals ?? extra.decimals;
|
|
298
|
+
if (decimals === void 0) {
|
|
299
|
+
throw new UnsupportedSchemeError(
|
|
300
|
+
`SVM exact: couldn't read the decimals of mint ${accept.asset} on-chain, and the rail states none. Retry with a reliable rpcUrl \u2014 TransferChecked cannot be built without them.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (onChainDecimals !== void 0 && extra.decimals !== void 0 && extra.decimals !== onChainDecimals) {
|
|
304
|
+
throw new UnsupportedSchemeError(
|
|
305
|
+
`SVM exact: the rail states ${extra.decimals} decimals for ${accept.asset} but the mint says ${onChainDecimals} \u2014 refusing to sign (a mismatch this size misprices the transfer).`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
288
308
|
if (feePayer.equals(payTo)) {
|
|
289
309
|
throw new UnsupportedSchemeError(
|
|
290
310
|
"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."
|
|
@@ -314,7 +334,8 @@ async function payExactSolana(input) {
|
|
|
314
334
|
dest,
|
|
315
335
|
keypair.publicKey,
|
|
316
336
|
BigInt(accept.amount),
|
|
317
|
-
|
|
337
|
+
decimals,
|
|
338
|
+
// read from the MINT (see above), not from the server's `extra`
|
|
318
339
|
[],
|
|
319
340
|
program
|
|
320
341
|
),
|
|
@@ -359,7 +380,7 @@ async function verifyAndSettleExactSolana(input) {
|
|
|
359
380
|
try {
|
|
360
381
|
payTo = new PublicKey2(accept.payTo);
|
|
361
382
|
mint = new PublicKey2(accept.asset);
|
|
362
|
-
if (!accept.extra
|
|
383
|
+
if (!accept.extra?.feePayer) throw new Error("rail is missing extra.feePayer");
|
|
363
384
|
railFeePayer = new PublicKey2(accept.extra.feePayer);
|
|
364
385
|
} catch (err) {
|
|
365
386
|
throw new SettlementError(`SVM exact: rail has a bad payTo/asset/feePayer (${err instanceof Error ? err.message : String(err)}).`);
|
|
@@ -467,7 +488,7 @@ async function verifyAndSettleExactSolana(input) {
|
|
|
467
488
|
if (!decoded.keys.destination.pubkey.equals(expectedDest)) {
|
|
468
489
|
return fail("wrong_recipient", `A transfer pays ${decoded.keys.destination.pubkey.toBase58()}, not payTo's ATA ${expectedDest.toBase58()}.`);
|
|
469
490
|
}
|
|
470
|
-
if (decoded.data.decimals !== accept.extra.decimals) {
|
|
491
|
+
if (accept.extra?.decimals !== void 0 && decoded.data.decimals !== accept.extra.decimals) {
|
|
471
492
|
return fail("transfer_not_found", `Transfer decimals ${decoded.data.decimals} \u2260 rail decimals ${accept.extra.decimals}.`);
|
|
472
493
|
}
|
|
473
494
|
sawTransferToPayTo = true;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exactTransferMethod
|
|
3
|
+
} from "./chunk-C52H5TYB.js";
|
|
4
|
+
import "./chunk-2CX7XRZK.js";
|
|
1
5
|
import {
|
|
2
6
|
delay
|
|
3
7
|
} from "./chunk-FURB5RP7.js";
|
|
@@ -5,7 +9,9 @@ import {
|
|
|
5
9
|
ConfirmationTimeoutError,
|
|
6
10
|
InsufficientFundsError,
|
|
7
11
|
RecipientNotReadyError,
|
|
12
|
+
SettlementError,
|
|
8
13
|
UnknownTokenError,
|
|
14
|
+
UnsupportedSchemeError,
|
|
9
15
|
WrongFamilyError,
|
|
10
16
|
assertNoLegacyWalletKey,
|
|
11
17
|
floorUnits,
|
|
@@ -48,8 +54,11 @@ var XRPL_MAINNET = {
|
|
|
48
54
|
function xrplAssetId(currencyHex, issuer) {
|
|
49
55
|
return `${currencyHex}:${issuer}`;
|
|
50
56
|
}
|
|
57
|
+
function isXrpNative(asset) {
|
|
58
|
+
return asset === "native" || asset === "XRP";
|
|
59
|
+
}
|
|
51
60
|
function parseXrplAssetId(asset) {
|
|
52
|
-
if (asset
|
|
61
|
+
if (isXrpNative(asset)) return null;
|
|
53
62
|
const i = asset.indexOf(":");
|
|
54
63
|
if (i <= 0 || i === asset.length - 1) return null;
|
|
55
64
|
return { currencyHex: asset.slice(0, i), issuer: asset.slice(i + 1) };
|
|
@@ -141,6 +150,216 @@ function isAffordabilityCode(code) {
|
|
|
141
150
|
return /^(tecUNFUNDED|tecINSUFF|terINSUF)/.test(code);
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
// src/drivers/xrpl/exact.ts
|
|
154
|
+
import { createHash } from "crypto";
|
|
155
|
+
var SECONDS_PER_LEDGER = 4;
|
|
156
|
+
var MIN_LEDGER_WINDOW = 5;
|
|
157
|
+
var MAX_LEDGER_WINDOW = 60;
|
|
158
|
+
function ledgerWindowFor(maxTimeoutSeconds) {
|
|
159
|
+
const secs = Number.isFinite(maxTimeoutSeconds) && maxTimeoutSeconds > 0 ? maxTimeoutSeconds : 60;
|
|
160
|
+
const ledgers = Math.floor(secs / SECONDS_PER_LEDGER);
|
|
161
|
+
return Math.min(MAX_LEDGER_WINDOW, Math.max(MIN_LEDGER_WINDOW, ledgers));
|
|
162
|
+
}
|
|
163
|
+
var MIN_FEE_DROPS = 12;
|
|
164
|
+
var MAX_FEE_DROPS = 1e6;
|
|
165
|
+
var TF_FULLY_CANONICAL_SIG = 2147483648;
|
|
166
|
+
var TF_PARTIAL_PAYMENT = 131072;
|
|
167
|
+
function feesAreSponsored(accept) {
|
|
168
|
+
return accept.extra?.areFeesSponsored === true;
|
|
169
|
+
}
|
|
170
|
+
function invoiceIdHash(invoiceId) {
|
|
171
|
+
return createHash("sha256").update(invoiceId, "utf8").digest("hex").toUpperCase();
|
|
172
|
+
}
|
|
173
|
+
function feeForSubmit2(openLedgerFee) {
|
|
174
|
+
const fee = Number(openLedgerFee);
|
|
175
|
+
const chosen = Number.isFinite(fee) && fee > MIN_FEE_DROPS ? Math.ceil(fee) : MIN_FEE_DROPS;
|
|
176
|
+
return String(Math.min(chosen, MAX_FEE_DROPS));
|
|
177
|
+
}
|
|
178
|
+
async function payExactXrpl(input) {
|
|
179
|
+
const { client, wallet, accept } = input;
|
|
180
|
+
if (!isXrpNative(accept.asset)) {
|
|
181
|
+
throw new UnsupportedSchemeError(
|
|
182
|
+
`XRPL exact currently signs native XRP only \u2014 ${accept.asset} is an issued currency, whose wire amount is a decimal rather than base units and would misprice the spend cap. Pay it via onchain-proof.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (feesAreSponsored(accept)) {
|
|
186
|
+
throw new UnsupportedSchemeError(
|
|
187
|
+
"XRPL exact rail advertises areFeesSponsored:true, but the XRP Ledger charges the fee to the transaction Account \u2014 the scheme requires false and PipRail cannot honour a sponsor here."
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const method = exactTransferMethod(accept, "xrpl");
|
|
191
|
+
if (method !== "sequence") {
|
|
192
|
+
throw new UnsupportedSchemeError(
|
|
193
|
+
`XRPL exact: unsupported assetTransferMethod "${method}". Only "sequence" is signable \u2014 "ticketSequence" needs a Ticket pre-minted on the payer account, which PipRail does not manage.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (!/^[0-9]+$/.test(accept.amount)) {
|
|
197
|
+
throw new UnsupportedSchemeError(
|
|
198
|
+
`XRPL exact: native amount "${accept.amount}" is not an integer drops string.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
let sequence;
|
|
202
|
+
let openLedgerFee;
|
|
203
|
+
let ledgerIndex;
|
|
204
|
+
try {
|
|
205
|
+
;
|
|
206
|
+
[sequence, openLedgerFee, ledgerIndex] = await Promise.all([
|
|
207
|
+
client.accountSequence(wallet.classicAddress),
|
|
208
|
+
client.feeDrops(),
|
|
209
|
+
client.currentLedgerIndex()
|
|
210
|
+
]);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (/actNotFound/i.test(String(err?.message ?? err))) {
|
|
213
|
+
throw new InsufficientFundsError(
|
|
214
|
+
`XRPL exact: the payer account ${wallet.classicAddress} does not exist on the ledger. An XRPL account must hold the base reserve (currently 1 XRP) before it can send anything \u2014 fund it first.`,
|
|
215
|
+
{ cause: err instanceof Error ? err : new Error(String(err)) }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
const tx = {
|
|
221
|
+
TransactionType: "Payment",
|
|
222
|
+
Account: wallet.classicAddress,
|
|
223
|
+
Destination: accept.payTo,
|
|
224
|
+
Amount: accept.amount,
|
|
225
|
+
// integer drops, verbatim from the trusted accept
|
|
226
|
+
Sequence: sequence,
|
|
227
|
+
Fee: feeForSubmit2(openLedgerFee),
|
|
228
|
+
LastLedgerSequence: ledgerIndex + ledgerWindowFor(accept.maxTimeoutSeconds),
|
|
229
|
+
// tfFullyCanonicalSig ON, tfPartialPayment OFF — the shape every other XRPL client sends.
|
|
230
|
+
Flags: TF_FULLY_CANONICAL_SIG
|
|
231
|
+
};
|
|
232
|
+
if (typeof accept.extra?.destinationTag === "number") {
|
|
233
|
+
tx.DestinationTag = accept.extra.destinationTag;
|
|
234
|
+
}
|
|
235
|
+
if (typeof accept.extra?.sourceTag === "number") {
|
|
236
|
+
tx.SourceTag = accept.extra.sourceTag;
|
|
237
|
+
}
|
|
238
|
+
if (typeof accept.extra?.invoiceId === "string" && accept.extra.invoiceId.length > 0) {
|
|
239
|
+
tx.InvoiceID = invoiceIdHash(accept.extra.invoiceId);
|
|
240
|
+
}
|
|
241
|
+
let signed;
|
|
242
|
+
try {
|
|
243
|
+
signed = wallet.sign(tx);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
throw new UnsupportedSchemeError(
|
|
246
|
+
`XRPL exact: could not sign this rail \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
247
|
+
{ cause: err instanceof Error ? err : new Error(String(err)) }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
payload: { signedTxBlob: signed.tx_blob },
|
|
252
|
+
accepted: accept,
|
|
253
|
+
payerFrom: wallet.classicAddress,
|
|
254
|
+
// The tx hash is the natural single-use key: the ledger itself enforces one settlement per
|
|
255
|
+
// (account, sequence), so a replay can never move funds twice even if a gate forgot to dedupe.
|
|
256
|
+
nonce: signed.hash
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
var fail = (error, detail) => ({ ok: false, error, detail });
|
|
260
|
+
async function verifyAndSettleExactXrpl(input) {
|
|
261
|
+
const { client, decode, payload, accept } = input;
|
|
262
|
+
const attempts = input.pollAttempts ?? 8;
|
|
263
|
+
const sleep = input.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
264
|
+
if (feesAreSponsored(accept)) {
|
|
265
|
+
throw new SettlementError("XRPL exact: rail advertises areFeesSponsored:true, which this ledger cannot honour.");
|
|
266
|
+
}
|
|
267
|
+
let tx;
|
|
268
|
+
try {
|
|
269
|
+
tx = decode(payload.signedTxBlob);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return fail("signature_invalid", `XRPL exact: undecodable tx blob (${err instanceof Error ? err.message : String(err)}).`);
|
|
272
|
+
}
|
|
273
|
+
if (tx.TransactionType !== "Payment") {
|
|
274
|
+
return fail("signature_invalid", `XRPL exact: expected a Payment, got ${String(tx.TransactionType)}.`);
|
|
275
|
+
}
|
|
276
|
+
if (tx.Memos !== void 0) return fail("signature_invalid", "XRPL exact: Memos are rejected by the scheme.");
|
|
277
|
+
if (tx.Delegate !== void 0) return fail("signature_invalid", "XRPL exact: Delegate is rejected by the scheme.");
|
|
278
|
+
if (tx.Paths !== void 0) return fail("signature_invalid", "XRPL exact: Paths must be omitted.");
|
|
279
|
+
if (tx.DeliverMin !== void 0) return fail("signature_invalid", "XRPL exact: DeliverMin must be omitted.");
|
|
280
|
+
if (tx.Amount !== void 0 && tx.DeliverMax !== void 0) {
|
|
281
|
+
return fail("signature_invalid", "XRPL exact: Amount and DeliverMax must not both be present.");
|
|
282
|
+
}
|
|
283
|
+
if (typeof tx.Flags === "number" && (tx.Flags & TF_PARTIAL_PAYMENT) !== 0) {
|
|
284
|
+
return fail("signature_invalid", "XRPL exact: tfPartialPayment is rejected.");
|
|
285
|
+
}
|
|
286
|
+
if (typeof tx.LastLedgerSequence !== "number") {
|
|
287
|
+
return fail("signature_invalid", "XRPL exact: LastLedgerSequence must be present.");
|
|
288
|
+
}
|
|
289
|
+
if (!tx.TxnSignature && !tx.Signers) {
|
|
290
|
+
return fail("signature_invalid", "XRPL exact: the transaction is not signed.");
|
|
291
|
+
}
|
|
292
|
+
if (tx.Destination !== accept.payTo) {
|
|
293
|
+
return fail("wrong_recipient", `XRPL exact: pays ${String(tx.Destination)}, not payTo ${accept.payTo}.`);
|
|
294
|
+
}
|
|
295
|
+
if (tx.Amount !== accept.amount) {
|
|
296
|
+
return fail("amount_too_low", `XRPL exact: Amount ${String(tx.Amount)} \u2260 the rail's ${accept.amount} drops.`);
|
|
297
|
+
}
|
|
298
|
+
if (typeof accept.extra?.destinationTag === "number" && tx.DestinationTag !== accept.extra.destinationTag) {
|
|
299
|
+
return fail("signature_invalid", `XRPL exact: DestinationTag ${String(tx.DestinationTag)} \u2260 the rail's ${accept.extra.destinationTag}.`);
|
|
300
|
+
}
|
|
301
|
+
if (typeof accept.extra?.invoiceId === "string" && accept.extra.invoiceId.length > 0) {
|
|
302
|
+
const want = invoiceIdHash(accept.extra.invoiceId);
|
|
303
|
+
if (String(tx.InvoiceID ?? "").toUpperCase() !== want) {
|
|
304
|
+
return fail("signature_invalid", "XRPL exact: InvoiceID does not bind this challenge.");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const feeDrops = Number(tx.Fee);
|
|
308
|
+
if (!Number.isFinite(feeDrops) || feeDrops < 0 || feeDrops > MAX_FEE_DROPS) {
|
|
309
|
+
return fail("signature_invalid", `XRPL exact: implausible Fee ${String(tx.Fee)} drops.`);
|
|
310
|
+
}
|
|
311
|
+
let hash;
|
|
312
|
+
try {
|
|
313
|
+
const res = await client.submit(payload.signedTxBlob);
|
|
314
|
+
if (!res.engine_result.startsWith("tes") && !res.engine_result.startsWith("ter")) {
|
|
315
|
+
return fail(
|
|
316
|
+
"transfer_not_found",
|
|
317
|
+
`XRPL exact: submit rejected \u2014 ${res.engine_result}${res.engine_result_message ? ` (${res.engine_result_message})` : ""}.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
hash = res.tx_json?.hash ?? "";
|
|
321
|
+
if (!hash) throw new Error("submit returned no transaction hash");
|
|
322
|
+
} catch (err) {
|
|
323
|
+
throw new SettlementError(`XRPL exact: submit failed (${err instanceof Error ? err.message : String(err)}).`);
|
|
324
|
+
}
|
|
325
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
326
|
+
let record;
|
|
327
|
+
try {
|
|
328
|
+
record = await client.txByHash(hash);
|
|
329
|
+
} catch {
|
|
330
|
+
record = null;
|
|
331
|
+
}
|
|
332
|
+
if (record?.validated) {
|
|
333
|
+
const code = record.meta?.TransactionResult;
|
|
334
|
+
if (code !== "tesSUCCESS") {
|
|
335
|
+
return fail("transfer_not_found", `XRPL exact: validated with ${String(code)}, not tesSUCCESS.`);
|
|
336
|
+
}
|
|
337
|
+
const delivered = record.meta?.delivered_amount;
|
|
338
|
+
if (delivered !== void 0 && typeof delivered === "string" && delivered !== accept.amount) {
|
|
339
|
+
return fail("amount_too_low", `XRPL exact: delivered ${delivered} drops, expected ${accept.amount}.`);
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
ok: true,
|
|
343
|
+
receipt: {
|
|
344
|
+
scheme: "exact",
|
|
345
|
+
success: true,
|
|
346
|
+
network: accept.network,
|
|
347
|
+
transaction: hash,
|
|
348
|
+
asset: accept.asset,
|
|
349
|
+
amount: accept.amount,
|
|
350
|
+
payer: String(record.Account ?? tx.Account ?? ""),
|
|
351
|
+
payTo: accept.payTo,
|
|
352
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
await sleep(1200);
|
|
357
|
+
}
|
|
358
|
+
throw new SettlementError(
|
|
359
|
+
`XRPL exact: submitted ${hash} but it was not validated within ${attempts} polls. It may still settle; retry the request.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
144
363
|
// src/drivers/xrpl/verify.ts
|
|
145
364
|
var RIPPLE_EPOCH_OFFSET = 946684800;
|
|
146
365
|
async function verifyXrpl(params) {
|
|
@@ -325,6 +544,16 @@ function makeXrplNetwork(preset, rpcUrl) {
|
|
|
325
544
|
return rpc("submit", { tx_blob: txBlob });
|
|
326
545
|
}
|
|
327
546
|
};
|
|
547
|
+
const exactSettleClient = {
|
|
548
|
+
submit: (txBlob) => payClient.submit(txBlob),
|
|
549
|
+
async txByHash(hash) {
|
|
550
|
+
try {
|
|
551
|
+
return await rpc("tx", { transaction: hash, binary: false });
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
328
557
|
const reader = {
|
|
329
558
|
async transactionsForAccount(account, limit) {
|
|
330
559
|
const r = await rpc("account_tx", {
|
|
@@ -373,7 +602,7 @@ function makeXrplNetwork(preset, rpcUrl) {
|
|
|
373
602
|
};
|
|
374
603
|
},
|
|
375
604
|
describeAsset(asset) {
|
|
376
|
-
if (asset
|
|
605
|
+
if (isXrpNative(asset)) return { symbol: XRP_SYMBOL, decimals: XRP_DECIMALS };
|
|
377
606
|
for (const info of Object.values(preset.tokens)) {
|
|
378
607
|
if (xrplAssetId(info.currencyHex, info.issuer) === asset) {
|
|
379
608
|
return { symbol: info.symbol, decimals: info.decimals };
|
|
@@ -400,6 +629,55 @@ function makeXrplNetwork(preset, rpcUrl) {
|
|
|
400
629
|
const w = resolveXrplWallet(wallet._native);
|
|
401
630
|
return payXrpl({ client: payClient, wallet: w, accept });
|
|
402
631
|
},
|
|
632
|
+
/*
|
|
633
|
+
* ── Standard x402 `exact` rail (scheme_exact_xrpl.md) ───────────────────────────
|
|
634
|
+
* XRPL is the family where the PAYER pays the fee, so there is no sponsor to wire: the buyer
|
|
635
|
+
* signs a complete `Payment` and the merchant simply submits it. See drivers/xrpl/exact.ts for
|
|
636
|
+
* why the binding is `InvoiceID` (never a memo) and why only native XRP is exact-payable today.
|
|
637
|
+
*/
|
|
638
|
+
// Native XRP only. An issued currency states its amount as a DECIMAL on the wire while the SDK
|
|
639
|
+
// prices and spend-caps in base units — signing one would let a 12-RLUSD rail pass a cap set in
|
|
640
|
+
// base units. Dropping it here means the gather skips it, rather than planning `payable` and
|
|
641
|
+
// throwing at signing time.
|
|
642
|
+
exactPayableAsset: (asset) => isXrpNative(asset),
|
|
643
|
+
async payExact(wallet, accept) {
|
|
644
|
+
const w = resolveXrplWallet(wallet._native);
|
|
645
|
+
return payExactXrpl({ client: payClient, wallet: w, accept });
|
|
646
|
+
},
|
|
647
|
+
// The gate advertises the rail. No `feePayer` and no sponsor key: uniquely on XRPL the buyer's
|
|
648
|
+
// own signed transaction carries its fee, so settlement is a bare submit.
|
|
649
|
+
//
|
|
650
|
+
// NB `server.ts` still requires a `relayer` for `settle: 'self'` — an unconditional config
|
|
651
|
+
// guard, and correctly chain-agnostic, so it cannot special-case XRPL. The relayer is accepted
|
|
652
|
+
// and then ignored here. Making it optional needs a driver-declared capability flag (the same
|
|
653
|
+
// shape as `exactPermit2Supported`); worth doing, since an XRPL merchant currently has to hand
|
|
654
|
+
// the gate a key it never uses, but it is a separate change from the buyer this rail is for.
|
|
655
|
+
async resolveExactRail({ asset }) {
|
|
656
|
+
if (asset !== "native") return null;
|
|
657
|
+
return {
|
|
658
|
+
method: "sequence",
|
|
659
|
+
// `areFeesSponsored` is REQUIRED by the scheme and must be false; we emit it explicitly
|
|
660
|
+
// even though no deployed rail does, because emitting the conformant shape costs nothing
|
|
661
|
+
// and a strict third-party buyer may look for it. (Our own buyer reads it as false when
|
|
662
|
+
// absent — requiring it would have rejected the entire deployed XRPL web.)
|
|
663
|
+
extra: { areFeesSponsored: false }
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
// SELLER side — decode + check the buyer's blob against the trusted accept, submit it, and wait
|
|
667
|
+
// for a validated tesSUCCESS. `relayer` is accepted for signature-compatibility with the other
|
|
668
|
+
// families and deliberately unused: the payer already paid the fee inside the signed blob.
|
|
669
|
+
async settleExactSelf({ payload, accept }) {
|
|
670
|
+
if (!("signedTxBlob" in payload)) {
|
|
671
|
+
return { ok: false, error: "signature_invalid", detail: "XRPL exact expects a { signedTxBlob } payload." };
|
|
672
|
+
}
|
|
673
|
+
const { decode } = await import("xrpl");
|
|
674
|
+
return verifyAndSettleExactXrpl({
|
|
675
|
+
client: exactSettleClient,
|
|
676
|
+
decode: (blob) => decode(blob),
|
|
677
|
+
payload,
|
|
678
|
+
accept
|
|
679
|
+
});
|
|
680
|
+
},
|
|
403
681
|
async confirm(ref) {
|
|
404
682
|
for (let i = 0; i < 12; i += 1) {
|
|
405
683
|
try {
|
|
@@ -452,7 +730,7 @@ function makeXrplNetwork(preset, rpcUrl) {
|
|
|
452
730
|
} catch (e) {
|
|
453
731
|
native = isXrplActNotFound(e) ? 0n : null;
|
|
454
732
|
}
|
|
455
|
-
if (asset
|
|
733
|
+
if (isXrpNative(asset)) return { token: native, native };
|
|
456
734
|
let token = null;
|
|
457
735
|
try {
|
|
458
736
|
const [currencyHex, issuer] = asset.split(":");
|
|
@@ -476,7 +754,7 @@ function makeXrplNetwork(preset, rpcUrl) {
|
|
|
476
754
|
if (isXrplActNotFound(e)) return { ready: false, reason: "INACTIVE" };
|
|
477
755
|
return { ready: "unknown" };
|
|
478
756
|
}
|
|
479
|
-
if (asset
|
|
757
|
+
if (isXrpNative(asset)) return { ready: true };
|
|
480
758
|
try {
|
|
481
759
|
const [currencyHex, issuer] = asset.split(":");
|
|
482
760
|
const r = await rpc("account_lines", {
|