@piprail/sdk 1.0.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,449 @@
1
+ import {
2
+ delay
3
+ } from "./chunk-FURB5RP7.js";
4
+ import {
5
+ ConfirmationTimeoutError,
6
+ InsufficientFundsError,
7
+ UnknownTokenError,
8
+ WrongFamilyError,
9
+ floorUnits,
10
+ nativeCost,
11
+ rejectForeignToken,
12
+ toInsufficientFundsError
13
+ } from "./chunk-3TQJJ4SQ.js";
14
+
15
+ // src/drivers/xrpl/index.ts
16
+ import { isValidClassicAddress } from "xrpl";
17
+
18
+ // src/drivers/xrpl/chains.ts
19
+ var XRP_DECIMALS = 6;
20
+ var XRP_SYMBOL = "XRP";
21
+ var XRPL_MAINNET = {
22
+ caip2: "xrpl:0",
23
+ // Public community cluster (serves HTTPS JSON-RPC). Not for sustained
24
+ // business use — pass your own `rpcUrl` in production.
25
+ defaultRpc: "https://xrplcluster.com/",
26
+ tokens: {
27
+ // Circle USDC — issuer Domain (`https://circle.com`) + currency code verified
28
+ // live on mainnet (gateway_balances) before shipping.
29
+ USDC: {
30
+ issuer: "rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE",
31
+ currencyHex: "5553444300000000000000000000000000000000",
32
+ decimals: 6,
33
+ symbol: "USDC"
34
+ },
35
+ // Ripple RLUSD — issuer Domain (`https://ripple.com/`) + currency code
36
+ // verified live on mainnet before shipping. Note: the RLUSD issuer sets
37
+ // requireDestinationTag, so payments always carry a DestinationTag (set below).
38
+ RLUSD: {
39
+ issuer: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
40
+ currencyHex: "524C555344000000000000000000000000000000",
41
+ decimals: 6,
42
+ symbol: "RLUSD"
43
+ }
44
+ }
45
+ };
46
+ function xrplAssetId(currencyHex, issuer) {
47
+ return `${currencyHex}:${issuer}`;
48
+ }
49
+ function parseXrplAssetId(asset) {
50
+ if (asset === "native") return null;
51
+ const i = asset.indexOf(":");
52
+ if (i <= 0 || i === asset.length - 1) return null;
53
+ return { currencyHex: asset.slice(0, i), issuer: asset.slice(i + 1) };
54
+ }
55
+
56
+ // src/drivers/xrpl/pay.ts
57
+ function nonceToMemoHex(nonce) {
58
+ return Buffer.from(nonce, "utf8").toString("hex").toUpperCase();
59
+ }
60
+ function nonceToDestinationTag(nonce) {
61
+ let h = 2166136261;
62
+ for (let i = 0; i < nonce.length; i += 1) {
63
+ h ^= nonce.charCodeAt(i);
64
+ h = Math.imul(h, 16777619);
65
+ }
66
+ const tag = h >>> 0;
67
+ return tag === 0 ? 1 : tag;
68
+ }
69
+ async function payXrpl(params) {
70
+ const { client, wallet, accept } = params;
71
+ try {
72
+ const [sequence, feeDrops, ledgerIndex] = await Promise.all([
73
+ client.accountSequence(wallet.classicAddress),
74
+ client.feeDrops(),
75
+ client.currentLedgerIndex()
76
+ ]);
77
+ const tx = {
78
+ TransactionType: "Payment",
79
+ Account: wallet.classicAddress,
80
+ Destination: accept.payTo,
81
+ DestinationTag: nonceToDestinationTag(accept.extra.nonce),
82
+ Amount: amountForAccept(accept),
83
+ Memos: [{ Memo: { MemoData: nonceToMemoHex(accept.extra.nonce) } }],
84
+ Sequence: sequence,
85
+ Fee: feeForSubmit(feeDrops),
86
+ LastLedgerSequence: ledgerIndex + 20,
87
+ Flags: 0
88
+ // NOT tfPartialPayment — require full delivery
89
+ };
90
+ const signed = wallet.sign(tx);
91
+ const res = await client.submit(signed.tx_blob);
92
+ const code = res.engine_result;
93
+ if (code.startsWith("tes")) return res.tx_json?.hash ?? signed.hash;
94
+ if (isAffordabilityCode(code)) {
95
+ throw new InsufficientFundsError(
96
+ `XRPL payment rejected (${code}): insufficient balance/reserve, or no trustline for this asset.`
97
+ );
98
+ }
99
+ throw new Error(
100
+ `XRPL payment rejected: ${code}${res.engine_result_message ? ` \u2014 ${res.engine_result_message}` : ""}`
101
+ );
102
+ } catch (err) {
103
+ if (err instanceof InsufficientFundsError) throw err;
104
+ throw toInsufficientFundsError(err) ?? err;
105
+ }
106
+ }
107
+ function amountForAccept(accept) {
108
+ if (accept.asset === "native") return accept.amount;
109
+ const parts = parseXrplAssetId(accept.asset);
110
+ if (!parts) throw new Error(`XRPL: malformed asset id "${accept.asset}".`);
111
+ return {
112
+ currency: parts.currencyHex,
113
+ issuer: parts.issuer,
114
+ value: accept.extra.amountFormatted
115
+ };
116
+ }
117
+ function feeForSubmit(openLedgerFee) {
118
+ const fee = Number(openLedgerFee);
119
+ return String(Number.isFinite(fee) && fee > 12 ? Math.ceil(fee) : 12);
120
+ }
121
+ function isAffordabilityCode(code) {
122
+ return /^(tecUNFUNDED|tecPATH_DRY|tecPATH_PARTIAL|tecINSUFF|tecNO_LINE_INSUF|terINSUF|tecNO_LINE)/.test(
123
+ code
124
+ );
125
+ }
126
+
127
+ // src/drivers/xrpl/verify.ts
128
+ var RIPPLE_EPOCH_OFFSET = 946684800;
129
+ async function verifyXrpl(params) {
130
+ const { reader, accept } = params;
131
+ const required = BigInt(accept.amount);
132
+ const nonce = accept.extra.nonce;
133
+ const wantMemo = nonceToMemoHex(nonce);
134
+ const wantAsset = parseXrplAssetId(accept.asset);
135
+ let txs;
136
+ try {
137
+ txs = await reader.transactionsForAccount(accept.payTo, 50);
138
+ } catch {
139
+ return rpcFailed(nonce);
140
+ }
141
+ const tx = txs.find(
142
+ (t) => t.TransactionType === "Payment" && t.Destination === accept.payTo && hasNonceMemo(t.Memos, wantMemo)
143
+ );
144
+ if (!tx) return notFound(nonce);
145
+ if (!tx.validated) {
146
+ return {
147
+ ok: false,
148
+ error: "insufficient_confirmations",
149
+ detail: `XRPL tx ${tx.hash} for nonce ${nonce} is not yet validated \u2014 retry.`
150
+ };
151
+ }
152
+ if (tx.result !== "tesSUCCESS") {
153
+ return {
154
+ ok: false,
155
+ error: "tx_reverted",
156
+ detail: `XRPL tx ${tx.hash} for nonce ${nonce} failed on-ledger (${tx.result}).`
157
+ };
158
+ }
159
+ if (typeof tx.date === "number") {
160
+ const ageSeconds = Math.floor(Date.now() / 1e3) - (tx.date + RIPPLE_EPOCH_OFFSET);
161
+ if (ageSeconds > accept.maxTimeoutSeconds) {
162
+ return {
163
+ ok: false,
164
+ error: "payment_expired",
165
+ detail: `Payment is ${ageSeconds}s old; max allowed is ${accept.maxTimeoutSeconds}s.`
166
+ };
167
+ }
168
+ }
169
+ const paid = deliveredBaseUnits(tx.delivered_amount, wantAsset, accept.extra.decimals);
170
+ if (paid === null) {
171
+ return {
172
+ ok: false,
173
+ error: "transfer_not_found",
174
+ detail: `XRPL tx ${tx.hash} carries our nonce but delivered no matching ${wantAsset ? "IOU" : "XRP"} to ${accept.payTo} (wrong asset, or delivered_amount unavailable).`
175
+ };
176
+ }
177
+ if (paid < required) {
178
+ return {
179
+ ok: false,
180
+ error: "amount_too_low",
181
+ detail: `Delivered ${paid}, required ${required}.`
182
+ };
183
+ }
184
+ return {
185
+ ok: true,
186
+ receipt: {
187
+ scheme: "onchain-proof",
188
+ success: true,
189
+ network: accept.network,
190
+ transaction: tx.hash,
191
+ asset: accept.asset,
192
+ amount: accept.amount,
193
+ payer: tx.Account,
194
+ payTo: accept.payTo,
195
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
196
+ }
197
+ };
198
+ }
199
+ function hasNonceMemo(memos, wantMemo) {
200
+ if (!memos) return false;
201
+ return memos.some((m) => (m.Memo?.MemoData ?? "").toUpperCase() === wantMemo);
202
+ }
203
+ function deliveredBaseUnits(delivered, want, decimals) {
204
+ if (delivered === void 0) return null;
205
+ if (want === null) {
206
+ if (typeof delivered !== "string") return null;
207
+ if (!/^\d+$/.test(delivered)) return null;
208
+ return BigInt(delivered);
209
+ }
210
+ if (typeof delivered === "string") return null;
211
+ if (delivered.currency.toUpperCase() !== want.currencyHex.toUpperCase() || delivered.issuer !== want.issuer) {
212
+ return null;
213
+ }
214
+ try {
215
+ return floorUnits(delivered.value, decimals);
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ function notFound(nonce) {
221
+ return {
222
+ ok: false,
223
+ error: "transfer_not_found",
224
+ detail: `No matching XRPL payment found for nonce ${nonce} (not yet validated, or wrong recipient/amount/asset/memo).`
225
+ };
226
+ }
227
+ function rpcFailed(nonce) {
228
+ return {
229
+ ok: false,
230
+ error: "tx_not_found",
231
+ detail: `Could not read XRPL for nonce ${nonce} (transient RPC failure) \u2014 retry.`
232
+ };
233
+ }
234
+
235
+ // src/drivers/xrpl/wallet.ts
236
+ import { Wallet, isValidSecret } from "xrpl";
237
+ function assertXrplWallet(wallet, network) {
238
+ if (typeof wallet !== "object" || wallet === null) {
239
+ throw new WrongFamilyError(
240
+ `chain ${network} is XRPL; wallet must be { seed } (s\u2026 seed) or { wallet }.`
241
+ );
242
+ }
243
+ if ("privateKey" in wallet || "walletClient" in wallet) {
244
+ throw new WrongFamilyError(
245
+ `chain ${network} is XRPL; an EVM wallet can't be used \u2014 pass { seed } (s\u2026 seed) or { wallet }.`
246
+ );
247
+ }
248
+ if ("secretKey" in wallet || "signer" in wallet || "mnemonic" in wallet || "keyPair" in wallet || "secret" in wallet || "keypair" in wallet) {
249
+ throw new WrongFamilyError(
250
+ `chain ${network} is XRPL; that looks like a Solana/TON/Stellar wallet \u2014 pass { seed } (s\u2026 seed) or { wallet }.`
251
+ );
252
+ }
253
+ if (!("seed" in wallet) && !("wallet" in wallet)) {
254
+ throw new WrongFamilyError(
255
+ `chain ${network} is XRPL; wallet must be { seed } (s\u2026 seed) or { wallet }.`
256
+ );
257
+ }
258
+ return wallet;
259
+ }
260
+ function resolveXrplWallet(config) {
261
+ if (config.wallet) return config.wallet;
262
+ if (config.seed) {
263
+ if (!isValidSecret(config.seed)) {
264
+ throw new WrongFamilyError("XRPL wallet { seed } is not a valid s\u2026 secret seed.");
265
+ }
266
+ return Wallet.fromSeed(config.seed);
267
+ }
268
+ throw new WrongFamilyError("XRPL wallet needs { seed } (s\u2026 seed) or { wallet }.");
269
+ }
270
+
271
+ // src/drivers/xrpl/index.ts
272
+ var xrplDriver = {
273
+ family: "xrpl",
274
+ resolve(opts) {
275
+ if (opts.chain !== "xrpl") return null;
276
+ const rpcUrl = opts.rpcUrl ?? XRPL_MAINNET.defaultRpc;
277
+ return makeXrplNetwork(XRPL_MAINNET, rpcUrl);
278
+ }
279
+ };
280
+ function makeXrplNetwork(preset, rpcUrl) {
281
+ const network = preset.caip2;
282
+ async function rpc(method, params) {
283
+ const res = await fetch(rpcUrl, {
284
+ method: "POST",
285
+ headers: { "content-type": "application/json" },
286
+ body: JSON.stringify({ method, params: [params] })
287
+ });
288
+ if (!res.ok) throw new Error(`XRPL RPC ${method} HTTP ${res.status}`);
289
+ const json = await res.json();
290
+ const result = json.result;
291
+ if (!result || result.status === "error" || result.error) {
292
+ throw new Error(`XRPL RPC ${method} error: ${result?.error ?? "unknown"}`);
293
+ }
294
+ return result;
295
+ }
296
+ const payClient = {
297
+ async accountSequence(account) {
298
+ const r = await rpc("account_info", {
299
+ account,
300
+ ledger_index: "current"
301
+ });
302
+ return r.account_data.Sequence;
303
+ },
304
+ async feeDrops() {
305
+ const r = await rpc("fee", {});
306
+ return r.drops.open_ledger_fee;
307
+ },
308
+ async currentLedgerIndex() {
309
+ const r = await rpc("ledger_current", {});
310
+ return r.ledger_current_index;
311
+ },
312
+ async submit(txBlob) {
313
+ return rpc("submit", { tx_blob: txBlob });
314
+ }
315
+ };
316
+ const reader = {
317
+ async transactionsForAccount(account, limit) {
318
+ const r = await rpc("account_tx", {
319
+ account,
320
+ ledger_index_min: -1,
321
+ ledger_index_max: -1,
322
+ forward: false,
323
+ limit
324
+ });
325
+ return r.transactions.map(mapAccountTxEntry);
326
+ }
327
+ };
328
+ return {
329
+ family: "xrpl",
330
+ network,
331
+ supports: (n) => n === network,
332
+ resolveToken(token) {
333
+ if (token === "native") {
334
+ return { asset: "native", decimals: XRP_DECIMALS, symbol: XRP_SYMBOL };
335
+ }
336
+ if (typeof token === "string") {
337
+ const info = preset.tokens[token.toUpperCase()];
338
+ if (!info) {
339
+ const known = Object.keys(preset.tokens).join(", ") || "(none built in)";
340
+ throw new UnknownTokenError(
341
+ `token "${token}" isn't built in for XRPL (known: ${known}). Pass { issuer, currencyHex, decimals } for a custom IOU, or use 'native'.`
342
+ );
343
+ }
344
+ return {
345
+ asset: xrplAssetId(info.currencyHex, info.issuer),
346
+ decimals: info.decimals,
347
+ symbol: info.symbol
348
+ };
349
+ }
350
+ rejectForeignToken(token, "xrpl", network);
351
+ const t = token;
352
+ if (!t.issuer || !t.currencyHex || typeof t.decimals !== "number") {
353
+ throw new WrongFamilyError(
354
+ `chain ${network} is XRPL; a custom token must be { issuer, currencyHex, decimals }.`
355
+ );
356
+ }
357
+ return {
358
+ asset: xrplAssetId(t.currencyHex, t.issuer),
359
+ decimals: t.decimals,
360
+ symbol: t.symbol ?? t.currencyHex
361
+ };
362
+ },
363
+ describeAsset(asset) {
364
+ if (asset === "native") return { symbol: XRP_SYMBOL, decimals: XRP_DECIMALS };
365
+ for (const info of Object.values(preset.tokens)) {
366
+ if (xrplAssetId(info.currencyHex, info.issuer) === asset) {
367
+ return { symbol: info.symbol, decimals: info.decimals };
368
+ }
369
+ }
370
+ return null;
371
+ },
372
+ assertValidPayTo(payTo) {
373
+ if (payTo.startsWith("0x")) {
374
+ throw new WrongFamilyError(
375
+ `chain ${network} is XRPL, but payTo "${payTo}" looks like an EVM address.`
376
+ );
377
+ }
378
+ if (!isValidClassicAddress(payTo)) {
379
+ throw new WrongFamilyError(
380
+ `chain ${network} is XRPL, but payTo "${payTo}" is not a valid XRPL account (r\u2026).`
381
+ );
382
+ }
383
+ },
384
+ bindWallet(wallet) {
385
+ return { _native: assertXrplWallet(wallet, network) };
386
+ },
387
+ async send(wallet, accept) {
388
+ const w = resolveXrplWallet(wallet._native);
389
+ return payXrpl({ client: payClient, wallet: w, accept });
390
+ },
391
+ async confirm(ref) {
392
+ for (let i = 0; i < 12; i += 1) {
393
+ try {
394
+ const tx = await rpc("tx", {
395
+ transaction: ref
396
+ });
397
+ if (tx.validated) return { height: String(tx.ledger_index ?? 0) };
398
+ } catch {
399
+ }
400
+ await delay(1500);
401
+ }
402
+ throw new ConfirmationTimeoutError(`XRPL tx ${ref} not validated on-ledger in time.`);
403
+ },
404
+ async estimateCost() {
405
+ try {
406
+ const drops = await payClient.feeDrops();
407
+ const n = Number(drops);
408
+ const fee = BigInt(Number.isFinite(n) && n > 12 ? Math.ceil(n) : 12);
409
+ return nativeCost({
410
+ symbol: XRP_SYMBOL,
411
+ decimals: XRP_DECIMALS,
412
+ fee,
413
+ basis: "estimated",
414
+ detail: `network fee ${fee} drops`
415
+ });
416
+ } catch {
417
+ return nativeCost({
418
+ symbol: XRP_SYMBOL,
419
+ decimals: XRP_DECIMALS,
420
+ fee: 12n,
421
+ basis: "heuristic",
422
+ detail: "~12 drops (open-ledger fee unavailable)"
423
+ });
424
+ }
425
+ },
426
+ async verify(_ref, accept) {
427
+ return verifyXrpl({ reader, accept });
428
+ }
429
+ };
430
+ }
431
+ function mapAccountTxEntry(entry) {
432
+ const e = entry;
433
+ const t = e.tx_json ?? e.tx ?? {};
434
+ const meta = e.meta ?? e.metaData ?? {};
435
+ return {
436
+ hash: e.hash ?? t.hash ?? "",
437
+ validated: Boolean(e.validated),
438
+ result: meta.TransactionResult ?? "",
439
+ TransactionType: t.TransactionType ?? "",
440
+ Account: t.Account ?? "",
441
+ Destination: t.Destination,
442
+ Memos: t.Memos,
443
+ delivered_amount: meta.delivered_amount ?? meta.DeliveredAmount,
444
+ date: typeof t.date === "number" ? t.date : void 0
445
+ };
446
+ }
447
+ export {
448
+ xrplDriver
449
+ };
package/package.json ADDED
@@ -0,0 +1,143 @@
1
+ {
2
+ "name": "@piprail/sdk",
3
+ "version": "1.0.0",
4
+ "description": "Accept x402 crypto payments across 24 chains — every major EVM chain plus Solana, TON, Tron, NEAR, Sui, Stellar & XRPL — in a couple of lines. No backend, no database, no fee; payments settle straight to your wallet.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "ERRORS.md",
25
+ "STANDARDS.md",
26
+ "CHANGELOG.md",
27
+ "LICENSE"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "dev": "tsup --watch",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "typecheck": "tsc --noEmit",
35
+ "typecheck:test": "tsc -p tsconfig.test.json",
36
+ "prepublishOnly": "npm run build && npm test && npm run typecheck && npm run typecheck:test"
37
+ },
38
+ "keywords": [
39
+ "piprail",
40
+ "x402",
41
+ "402",
42
+ "payment-required",
43
+ "multichain",
44
+ "evm",
45
+ "solana",
46
+ "ton",
47
+ "tron",
48
+ "near",
49
+ "sui",
50
+ "stellar",
51
+ "xrpl",
52
+ "base",
53
+ "bnb-chain",
54
+ "ethereum",
55
+ "usdc",
56
+ "usdt",
57
+ "stablecoin",
58
+ "crypto-payments",
59
+ "agent-payments",
60
+ "ai-agent",
61
+ "express-middleware",
62
+ "payment-middleware"
63
+ ],
64
+ "author": "PipRail",
65
+ "license": "MIT",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/piprail/piprail.git",
69
+ "directory": "sdk"
70
+ },
71
+ "homepage": "https://piprail.com",
72
+ "bugs": "https://github.com/piprail/piprail/issues",
73
+ "engines": {
74
+ "node": ">=20"
75
+ },
76
+ "peerDependencies": {
77
+ "@mysten/sui": ">=2 <3",
78
+ "@solana/spl-token": "^0.4.0",
79
+ "@solana/web3.js": "^1.95.0",
80
+ "@stellar/stellar-sdk": ">=13 <16",
81
+ "@ton/core": ">=0.59 <1",
82
+ "@ton/crypto": ">=3 <4",
83
+ "@ton/ton": ">=15 <17",
84
+ "bs58": "^5.0.0",
85
+ "near-api-js": ">=7 <8",
86
+ "tronweb": ">=6 <7",
87
+ "viem": "^2.21.0",
88
+ "xrpl": ">=4 <5"
89
+ },
90
+ "peerDependenciesMeta": {
91
+ "@solana/web3.js": {
92
+ "optional": true
93
+ },
94
+ "@solana/spl-token": {
95
+ "optional": true
96
+ },
97
+ "@stellar/stellar-sdk": {
98
+ "optional": true
99
+ },
100
+ "bs58": {
101
+ "optional": true
102
+ },
103
+ "@ton/ton": {
104
+ "optional": true
105
+ },
106
+ "@ton/core": {
107
+ "optional": true
108
+ },
109
+ "@ton/crypto": {
110
+ "optional": true
111
+ },
112
+ "xrpl": {
113
+ "optional": true
114
+ },
115
+ "tronweb": {
116
+ "optional": true
117
+ },
118
+ "@mysten/sui": {
119
+ "optional": true
120
+ },
121
+ "near-api-js": {
122
+ "optional": true
123
+ }
124
+ },
125
+ "devDependencies": {
126
+ "@mysten/sui": "^2.17.0",
127
+ "@solana/spl-token": "^0.4.14",
128
+ "@solana/web3.js": "^1.98.4",
129
+ "@stellar/stellar-sdk": "^15.1.0",
130
+ "@ton/core": "^0.63.1",
131
+ "@ton/crypto": "^3.3.0",
132
+ "@ton/ton": "^16.2.4",
133
+ "@types/node": "^22.10.0",
134
+ "bs58": "^5.0.0",
135
+ "near-api-js": "^7.2.0",
136
+ "tronweb": "^6.3.0",
137
+ "tsup": "^8.3.5",
138
+ "typescript": "^5.6.3",
139
+ "viem": "^2.51.2",
140
+ "vitest": "^2.1.8",
141
+ "xrpl": "^4.6.0"
142
+ }
143
+ }