@piprail/sdk 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +260 -0
- package/README.md +2 -2
- package/dist/{algorand-CSEUTWNW.js → algorand-25FMBCT3.js} +13 -4
- package/dist/{algorand-I2O24RSM.cjs → algorand-Q3TUQLPK.cjs} +49 -40
- package/dist/{aptos-J7QGCWNW.js → aptos-SBV6SGDP.js} +3 -3
- package/dist/{aptos-GA5AEYYL.cjs → aptos-VBJONBFY.cjs} +36 -36
- package/dist/chunk-2CX7XRZK.js +649 -0
- package/dist/chunk-C52H5TYB.js +474 -0
- package/dist/chunk-OXEFPLZA.cjs +474 -0
- package/dist/{chunk-3FR22M3O.cjs → chunk-QONQSZHJ.cjs} +2 -2
- package/dist/{chunk-3ZWM3DKM.js → chunk-QU25LSVS.js} +2 -2
- package/dist/chunk-V2IJ5HUW.cjs +649 -0
- package/dist/index.cjs +507 -1347
- package/dist/index.d.cts +39 -6
- package/dist/index.d.ts +39 -6
- package/dist/index.js +192 -1032
- 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/{near-NDQLGI4U.cjs → near-5LTTDU6G.cjs} +27 -27
- package/dist/{near-YA4Y4IGC.js → near-OLKCMTBI.js} +1 -1
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/{solana-OU6IUMQM.cjs → solana-AI2G7V33.cjs} +73 -52
- package/dist/{solana-MB6G3OA5.js → solana-HTKDRTD3.js} +32 -11
- package/dist/{stellar-RSNVOIKL.cjs → stellar-E2KWEV2E.cjs} +20 -20
- package/dist/{stellar-WDWSYOED.js → stellar-EUFZLX6J.js} +1 -1
- package/dist/{sui-GVSFNNJX.js → sui-KCIITCYH.js} +6 -2
- package/dist/{sui-MVC46BFJ.cjs → sui-Q3NJOJZS.cjs} +21 -17
- package/dist/{ton-MYOI3U3H.cjs → ton-J7TQWRN4.cjs} +16 -16
- package/dist/{ton-EJMGM3YI.js → ton-WH2JVQOO.js} +1 -1
- package/dist/{tron-VAPVMGJK.js → tron-FXBXDNEY.js} +1 -1
- package/dist/{tron-42X5N4GQ.cjs → tron-HIPMOX7S.cjs} +25 -25
- package/dist/{xrpl-Q2UWPCTJ.js → xrpl-RUOB37QH.js} +283 -5
- package/dist/{xrpl-MGUBUNSZ.cjs → xrpl-SVVS445B.cjs} +312 -34
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ var _chunkCQREG5LEcjs = require('./chunk-CQREG5LE.cjs');
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
var
|
|
13
|
+
var _chunkQONQSZHJcjs = require('./chunk-QONQSZHJ.cjs');
|
|
14
14
|
|
|
15
15
|
// src/drivers/tron/index.ts
|
|
16
16
|
var _tronweb = require('tronweb');
|
|
@@ -62,7 +62,7 @@ async function payTron(params) {
|
|
|
62
62
|
return _nullishCoalesce(_nullishCoalesce(broadcast.txid, () => ( _optionalChain([broadcast, 'access', _7 => _7.transaction, 'optionalAccess', _8 => _8.txID]))), () => ( signed.txID));
|
|
63
63
|
}
|
|
64
64
|
if (isTronAffordability(broadcast.code, broadcast.message)) {
|
|
65
|
-
throw new (0,
|
|
65
|
+
throw new (0, _chunkQONQSZHJcjs.InsufficientFundsError)(
|
|
66
66
|
`Tron payment rejected (${_nullishCoalesce(broadcast.code, () => ( "broadcast"))}): not enough TRX for energy/bandwidth, or insufficient token balance.`
|
|
67
67
|
);
|
|
68
68
|
}
|
|
@@ -70,15 +70,15 @@ async function payTron(params) {
|
|
|
70
70
|
`Tron broadcast failed: ${_nullishCoalesce(broadcast.code, () => ( ""))} ${decodeMaybeHex(broadcast.message)}`.trim()
|
|
71
71
|
);
|
|
72
72
|
} catch (err) {
|
|
73
|
-
if (err instanceof
|
|
74
|
-
throw _nullishCoalesce(
|
|
73
|
+
if (err instanceof _chunkQONQSZHJcjs.InsufficientFundsError) throw err;
|
|
74
|
+
throw _nullishCoalesce(_chunkQONQSZHJcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
async function payTronNative(params) {
|
|
78
78
|
const { client, from, privateKey, accept } = params;
|
|
79
79
|
const sun = BigInt(accept.amount);
|
|
80
80
|
if (sun > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
81
|
-
throw new (0,
|
|
81
|
+
throw new (0, _chunkQONQSZHJcjs.InsufficientFundsError)(
|
|
82
82
|
`Tron native amount ${accept.amount} sun exceeds the safe-integer range tronweb.sendTrx accepts \u2014 price this resource in a smaller native amount or a TRC-20 token.`
|
|
83
83
|
);
|
|
84
84
|
}
|
|
@@ -90,7 +90,7 @@ async function payTronNative(params) {
|
|
|
90
90
|
return _nullishCoalesce(_nullishCoalesce(broadcast.txid, () => ( _optionalChain([broadcast, 'access', _11 => _11.transaction, 'optionalAccess', _12 => _12.txID]))), () => ( signed.txID));
|
|
91
91
|
}
|
|
92
92
|
if (isTronAffordability(broadcast.code, broadcast.message)) {
|
|
93
|
-
throw new (0,
|
|
93
|
+
throw new (0, _chunkQONQSZHJcjs.InsufficientFundsError)(
|
|
94
94
|
`Tron TRX payment rejected (${_nullishCoalesce(broadcast.code, () => ( "broadcast"))}): not enough TRX for the amount plus bandwidth (and the ~1 TRX account-creation fee if the recipient is new).`
|
|
95
95
|
);
|
|
96
96
|
}
|
|
@@ -98,8 +98,8 @@ async function payTronNative(params) {
|
|
|
98
98
|
`Tron TRX broadcast failed: ${_nullishCoalesce(broadcast.code, () => ( ""))} ${decodeMaybeHex(broadcast.message)}`.trim()
|
|
99
99
|
);
|
|
100
100
|
} catch (err) {
|
|
101
|
-
if (err instanceof
|
|
102
|
-
throw _nullishCoalesce(
|
|
101
|
+
if (err instanceof _chunkQONQSZHJcjs.InsufficientFundsError) throw err;
|
|
102
|
+
throw _nullishCoalesce(_chunkQONQSZHJcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
function decodeMaybeHex(message) {
|
|
@@ -273,13 +273,13 @@ function txNotFound(txid) {
|
|
|
273
273
|
// src/drivers/tron/wallet.ts
|
|
274
274
|
function assertTronWallet(wallet, network) {
|
|
275
275
|
if (typeof wallet !== "object" || wallet === null) {
|
|
276
|
-
throw new (0,
|
|
276
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
277
277
|
`chain ${network} is Tron; wallet must be { key } (32-byte hex).`
|
|
278
278
|
);
|
|
279
279
|
}
|
|
280
|
-
|
|
280
|
+
_chunkQONQSZHJcjs.assertNoLegacyWalletKey.call(void 0, wallet, "Tron");
|
|
281
281
|
if (!("key" in wallet)) {
|
|
282
|
-
throw new (0,
|
|
282
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
283
283
|
`chain ${network} is Tron; wallet must be { key } (32-byte hex).`
|
|
284
284
|
);
|
|
285
285
|
}
|
|
@@ -287,14 +287,14 @@ function assertTronWallet(wallet, network) {
|
|
|
287
287
|
}
|
|
288
288
|
function resolveTronPrivateKey(config) {
|
|
289
289
|
if (!config.key) {
|
|
290
|
-
throw new (0,
|
|
290
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)("Tron wallet needs { key } (32-byte hex).");
|
|
291
291
|
}
|
|
292
292
|
if (typeof config.key !== "string") {
|
|
293
|
-
throw new (0,
|
|
293
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)("Tron wallet { key } must be a 32-byte hex string.");
|
|
294
294
|
}
|
|
295
295
|
const hex = config.key.replace(/^0x/i, "").toLowerCase();
|
|
296
296
|
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
|
297
|
-
throw new (0,
|
|
297
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
298
298
|
"Tron wallet { key } must be a 32-byte hex string (64 hex chars)."
|
|
299
299
|
);
|
|
300
300
|
}
|
|
@@ -344,21 +344,21 @@ function makeTronNetwork(preset, rpcUrl) {
|
|
|
344
344
|
const info = preset.tokens[token.toUpperCase()];
|
|
345
345
|
if (!info) {
|
|
346
346
|
const known = Object.keys(preset.tokens).join(", ") || "(none built in)";
|
|
347
|
-
throw new (0,
|
|
347
|
+
throw new (0, _chunkQONQSZHJcjs.UnknownTokenError)(
|
|
348
348
|
`token "${token}" isn't built in for Tron (known: ${known}). Note: native USDC doesn't exist on Tron. Pass { address, decimals } for a custom TRC-20.`
|
|
349
349
|
);
|
|
350
350
|
}
|
|
351
351
|
return { asset: info.address, decimals: info.decimals, symbol: info.symbol };
|
|
352
352
|
}
|
|
353
|
-
|
|
353
|
+
_chunkQONQSZHJcjs.rejectForeignToken.call(void 0, token, "tron", network);
|
|
354
354
|
if (!("address" in token)) {
|
|
355
|
-
throw new (0,
|
|
355
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
356
356
|
`chain ${network} is Tron; a custom token must be { address, decimals } (Base58 T\u2026 contract).`
|
|
357
357
|
);
|
|
358
358
|
}
|
|
359
359
|
const t = token;
|
|
360
360
|
if (t.address.startsWith("0x") || !tronWeb.isAddress(t.address)) {
|
|
361
|
-
throw new (0,
|
|
361
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
362
362
|
`chain ${network} is Tron, but token address "${t.address}" is not a valid Tron contract (T\u2026).`
|
|
363
363
|
);
|
|
364
364
|
}
|
|
@@ -377,12 +377,12 @@ function makeTronNetwork(preset, rpcUrl) {
|
|
|
377
377
|
},
|
|
378
378
|
assertValidPayTo(payTo) {
|
|
379
379
|
if (payTo.startsWith("0x")) {
|
|
380
|
-
throw new (0,
|
|
380
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
381
381
|
`chain ${network} is Tron, but payTo "${payTo}" looks like an EVM address.`
|
|
382
382
|
);
|
|
383
383
|
}
|
|
384
384
|
if (!tronWeb.isAddress(payTo)) {
|
|
385
|
-
throw new (0,
|
|
385
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)(
|
|
386
386
|
`chain ${network} is Tron, but payTo "${payTo}" is not a valid Tron address (T\u2026).`
|
|
387
387
|
);
|
|
388
388
|
}
|
|
@@ -394,7 +394,7 @@ function makeTronNetwork(preset, rpcUrl) {
|
|
|
394
394
|
const privateKey = resolveTronPrivateKey(wallet._native);
|
|
395
395
|
const from = tronWeb.address.fromPrivateKey(privateKey);
|
|
396
396
|
if (!from) {
|
|
397
|
-
throw new (0,
|
|
397
|
+
throw new (0, _chunkQONQSZHJcjs.WrongFamilyError)("Tron wallet { key } could not derive an address.");
|
|
398
398
|
}
|
|
399
399
|
if (accept.asset === "native") {
|
|
400
400
|
return payTronNative({ client: tronWeb, from, privateKey, accept });
|
|
@@ -417,11 +417,11 @@ function makeTronNetwork(preset, rpcUrl) {
|
|
|
417
417
|
}
|
|
418
418
|
await _chunkCQREG5LEcjs.delay.call(void 0, 2500);
|
|
419
419
|
}
|
|
420
|
-
throw new (0,
|
|
420
|
+
throw new (0, _chunkQONQSZHJcjs.ConfirmationTimeoutError)(`Tron tx ${txid} did not solidify in time.`);
|
|
421
421
|
},
|
|
422
422
|
async estimateCost(accept, opts) {
|
|
423
423
|
if (accept.asset === "native") {
|
|
424
|
-
return
|
|
424
|
+
return _chunkQONQSZHJcjs.nativeCost.call(void 0, {
|
|
425
425
|
symbol: "TRX",
|
|
426
426
|
decimals: TRX_DECIMALS,
|
|
427
427
|
fee: 1300000n,
|
|
@@ -445,7 +445,7 @@ function makeTronNetwork(preset, rpcUrl) {
|
|
|
445
445
|
);
|
|
446
446
|
const energy = BigInt(_nullishCoalesce(r.energy_used, () => ( 0)));
|
|
447
447
|
if (energy > 0n) {
|
|
448
|
-
return
|
|
448
|
+
return _chunkQONQSZHJcjs.nativeCost.call(void 0, {
|
|
449
449
|
symbol: "TRX",
|
|
450
450
|
decimals: TRX_DECIMALS,
|
|
451
451
|
fee: energy * ENERGY_PRICE + BANDWIDTH_SUN,
|
|
@@ -456,7 +456,7 @@ function makeTronNetwork(preset, rpcUrl) {
|
|
|
456
456
|
} catch (e8) {
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
|
-
return
|
|
459
|
+
return _chunkQONQSZHJcjs.nativeCost.call(void 0, {
|
|
460
460
|
symbol: "TRX",
|
|
461
461
|
decimals: TRX_DECIMALS,
|
|
462
462
|
fee: 30000n * ENERGY_PRICE + BANDWIDTH_SUN,
|
|
@@ -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,14 +9,16 @@ 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,
|
|
12
18
|
nativeCost,
|
|
13
19
|
rejectForeignToken,
|
|
14
20
|
toInsufficientFundsError
|
|
15
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-QU25LSVS.js";
|
|
16
22
|
|
|
17
23
|
// src/drivers/xrpl/index.ts
|
|
18
24
|
import { isValidClassicAddress } from "xrpl";
|
|
@@ -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", {
|