@piprail/sdk 1.8.0 → 1.10.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/CHAINS.md +5 -3
- package/CHANGELOG.md +59 -0
- package/ERRORS.md +21 -8
- package/README.md +23 -0
- package/STANDARDS.md +5 -2
- package/dist/{algorand-IJJKE35X.cjs → algorand-MXUSKX46.cjs} +17 -17
- package/dist/{algorand-B67G4335.js → algorand-WGVF4KTU.js} +1 -1
- package/dist/{aptos-YQWTGFRZ.js → aptos-LPBLSEIQ.js} +1 -1
- package/dist/{aptos-X3G2UBYW.cjs → aptos-YT7SXWPF.cjs} +16 -16
- package/dist/{chunk-IQGT65WS.cjs → chunk-MDLZJGLY.cjs} +20 -16
- package/dist/{chunk-QDS6FBZP.js → chunk-SVMGHASK.js} +4 -0
- package/dist/index.cjs +823 -253
- package/dist/index.d.cts +458 -35
- package/dist/index.d.ts +458 -35
- package/dist/index.js +746 -176
- package/dist/{near-GGUHLXAF.cjs → near-7ZDNISUX.cjs} +19 -19
- package/dist/{near-7MBBCDUE.js → near-K6BDBABG.js} +1 -1
- package/dist/{solana-W24TCJV4.cjs → solana-PU7N2M64.cjs} +14 -14
- package/dist/{solana-7WJVZGDW.js → solana-S3UFI3FE.js} +1 -1
- package/dist/{stellar-HV6VGZX3.js → stellar-Q5PO23SC.js} +1 -1
- package/dist/{stellar-YMY3K2YB.cjs → stellar-VDQOFQEO.cjs} +21 -21
- package/dist/{sui-32KVESR5.cjs → sui-FKSMLKRF.cjs} +17 -17
- package/dist/{sui-2WFWVFJX.js → sui-WOXRKJXS.js} +1 -1
- package/dist/{ton-FIQGV2LC.cjs → ton-VK6KRJHP.cjs} +14 -14
- package/dist/{ton-DGZB7W4U.js → ton-WPTXGLVK.js} +1 -1
- package/dist/{tron-RLIL2FDI.js → tron-6GXBXTR4.js} +1 -1
- package/dist/{tron-ZSXAPZ2C.cjs → tron-WLOF5OUV.cjs} +24 -24
- package/dist/{xrpl-2PKP7HOI.cjs → xrpl-CMNI25BV.cjs} +21 -21
- package/dist/{xrpl-UEC2GYVV.js → xrpl-HEAPEXAM.js} +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
PaymentTimeoutError,
|
|
11
11
|
PipRailError,
|
|
12
12
|
RecipientNotReadyError,
|
|
13
|
+
SettlementError,
|
|
13
14
|
UnknownTokenError,
|
|
14
15
|
UnsupportedNetworkError,
|
|
15
16
|
WrongChainError,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
parseUnits,
|
|
21
22
|
rejectForeignToken,
|
|
22
23
|
toInsufficientFundsError
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-SVMGHASK.js";
|
|
24
25
|
|
|
25
26
|
// src/drivers/registry.ts
|
|
26
27
|
var byFamily = /* @__PURE__ */ new Map();
|
|
@@ -63,7 +64,7 @@ function resolveNetwork(opts) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// src/drivers/evm/index.ts
|
|
66
|
-
import { BaseError, createPublicClient, erc20Abi as erc20Abi3, getAddress as
|
|
67
|
+
import { BaseError, createPublicClient, erc20Abi as erc20Abi3, getAddress as getAddress3, http as http2, isAddress } from "viem";
|
|
67
68
|
|
|
68
69
|
// src/drivers/evm/chains.ts
|
|
69
70
|
import { defineChain } from "viem";
|
|
@@ -75,6 +76,7 @@ import {
|
|
|
75
76
|
celo,
|
|
76
77
|
hyperEvm,
|
|
77
78
|
injective,
|
|
79
|
+
kaia,
|
|
78
80
|
linea,
|
|
79
81
|
mainnet,
|
|
80
82
|
mantle,
|
|
@@ -236,6 +238,17 @@ var CHAINS = {
|
|
|
236
238
|
tokens: {
|
|
237
239
|
USDC: { address: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", decimals: 6, symbol: "USDC" }
|
|
238
240
|
}
|
|
241
|
+
},
|
|
242
|
+
// Kaia (ex-Klaytn, chainId 8217) — Tether-native USD₮ verified on-chain 2026-06-08
|
|
243
|
+
// (0xd077…4fDb: symbol "USD₮", name "Tether USD", 6 dp, no bridge markers). Circle issues
|
|
244
|
+
// NO native USDC on Kaia (absent from Circle's list), so USDC is intentionally omitted —
|
|
245
|
+
// pay native KAIA or USD₮ (or pass a custom { address, decimals }). Asia's stablecoin-
|
|
246
|
+
// settlement chain, born from Kakao + LINE.
|
|
247
|
+
kaia: {
|
|
248
|
+
chain: kaia,
|
|
249
|
+
tokens: {
|
|
250
|
+
USDT: { address: "0xd077A400968890Eacc75cdc901F0356c943e4fDb", decimals: 6, symbol: "USDT" }
|
|
251
|
+
}
|
|
239
252
|
}
|
|
240
253
|
};
|
|
241
254
|
function isViemChain(input) {
|
|
@@ -471,20 +484,367 @@ function sumTransfersTo(logs, asset, payTo) {
|
|
|
471
484
|
return { total, from };
|
|
472
485
|
}
|
|
473
486
|
|
|
487
|
+
// src/drivers/evm/exact.ts
|
|
488
|
+
import {
|
|
489
|
+
getAddress as getAddress2,
|
|
490
|
+
parseSignature,
|
|
491
|
+
recoverTypedDataAddress
|
|
492
|
+
} from "viem";
|
|
493
|
+
var EXACT_NETWORK_SLUGS = {
|
|
494
|
+
ethereum: 1,
|
|
495
|
+
base: 8453,
|
|
496
|
+
"base-sepolia": 84532,
|
|
497
|
+
arbitrum: 42161,
|
|
498
|
+
optimism: 10,
|
|
499
|
+
polygon: 137,
|
|
500
|
+
avalanche: 43114
|
|
501
|
+
};
|
|
502
|
+
function chainIdForExactNetwork(slug) {
|
|
503
|
+
return EXACT_NETWORK_SLUGS[slug] ?? null;
|
|
504
|
+
}
|
|
505
|
+
var EIP3009_TYPES = {
|
|
506
|
+
TransferWithAuthorization: [
|
|
507
|
+
{ name: "from", type: "address" },
|
|
508
|
+
{ name: "to", type: "address" },
|
|
509
|
+
{ name: "value", type: "uint256" },
|
|
510
|
+
{ name: "validAfter", type: "uint256" },
|
|
511
|
+
{ name: "validBefore", type: "uint256" },
|
|
512
|
+
{ name: "nonce", type: "bytes32" }
|
|
513
|
+
]
|
|
514
|
+
};
|
|
515
|
+
function parseExactRequirements(body) {
|
|
516
|
+
if (!body || typeof body !== "object") return null;
|
|
517
|
+
const accepts = body.accepts;
|
|
518
|
+
if (!Array.isArray(accepts)) return null;
|
|
519
|
+
const out = [];
|
|
520
|
+
for (const raw of accepts) {
|
|
521
|
+
if (!raw || typeof raw !== "object") continue;
|
|
522
|
+
const a = raw;
|
|
523
|
+
if (a.scheme !== "exact") continue;
|
|
524
|
+
const amount = a.maxAmountRequired ?? a.amount;
|
|
525
|
+
if (typeof a.network !== "string" || typeof amount !== "string" || typeof a.asset !== "string" || typeof a.payTo !== "string") {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
out.push({
|
|
529
|
+
scheme: "exact",
|
|
530
|
+
network: a.network,
|
|
531
|
+
maxAmountRequired: amount,
|
|
532
|
+
asset: a.asset,
|
|
533
|
+
payTo: a.payTo,
|
|
534
|
+
maxTimeoutSeconds: typeof a.maxTimeoutSeconds === "number" ? a.maxTimeoutSeconds : 600,
|
|
535
|
+
...a.extra && typeof a.extra === "object" ? { extra: a.extra } : {},
|
|
536
|
+
...typeof a.description === "string" ? { description: a.description } : {},
|
|
537
|
+
...typeof a.resource === "string" ? { resource: a.resource } : {}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
async function buildExactAuthorization(params) {
|
|
543
|
+
const { account, accept, chainId, now, nonce } = params;
|
|
544
|
+
if (!account.signTypedData) {
|
|
545
|
+
throw new Error("buildExactAuthorization: the account cannot sign EIP-712 typed data.");
|
|
546
|
+
}
|
|
547
|
+
const authorization = {
|
|
548
|
+
from: account.address,
|
|
549
|
+
to: accept.payTo,
|
|
550
|
+
value: accept.maxAmountRequired,
|
|
551
|
+
validAfter: "0",
|
|
552
|
+
validBefore: String(now + accept.maxTimeoutSeconds),
|
|
553
|
+
nonce
|
|
554
|
+
};
|
|
555
|
+
const signature = await account.signTypedData({
|
|
556
|
+
domain: {
|
|
557
|
+
name: accept.extra?.name ?? "USD Coin",
|
|
558
|
+
version: accept.extra?.version ?? "2",
|
|
559
|
+
chainId,
|
|
560
|
+
verifyingContract: accept.asset
|
|
561
|
+
},
|
|
562
|
+
types: EIP3009_TYPES,
|
|
563
|
+
primaryType: "TransferWithAuthorization",
|
|
564
|
+
message: {
|
|
565
|
+
from: authorization.from,
|
|
566
|
+
to: authorization.to,
|
|
567
|
+
value: BigInt(authorization.value),
|
|
568
|
+
validAfter: BigInt(authorization.validAfter),
|
|
569
|
+
validBefore: BigInt(authorization.validBefore),
|
|
570
|
+
nonce: authorization.nonce
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
return { authorization, signature };
|
|
574
|
+
}
|
|
575
|
+
function base64(str) {
|
|
576
|
+
if (typeof Buffer !== "undefined") return Buffer.from(str, "utf8").toString("base64");
|
|
577
|
+
if (typeof btoa === "function" && typeof TextEncoder !== "undefined") {
|
|
578
|
+
const bytes = new TextEncoder().encode(str);
|
|
579
|
+
let binary = "";
|
|
580
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
581
|
+
return btoa(binary);
|
|
582
|
+
}
|
|
583
|
+
throw new Error("No base64 encoder available in this runtime.");
|
|
584
|
+
}
|
|
585
|
+
function encodeXPaymentHeader(input) {
|
|
586
|
+
const payload = {
|
|
587
|
+
x402Version: input.x402Version ?? 1,
|
|
588
|
+
scheme: "exact",
|
|
589
|
+
network: input.network,
|
|
590
|
+
payload: { signature: input.signature, authorization: input.authorization }
|
|
591
|
+
};
|
|
592
|
+
return base64(JSON.stringify(payload));
|
|
593
|
+
}
|
|
594
|
+
var eip3009Abi = [
|
|
595
|
+
{
|
|
596
|
+
type: "function",
|
|
597
|
+
name: "transferWithAuthorization",
|
|
598
|
+
stateMutability: "nonpayable",
|
|
599
|
+
outputs: [],
|
|
600
|
+
inputs: [
|
|
601
|
+
{ name: "from", type: "address" },
|
|
602
|
+
{ name: "to", type: "address" },
|
|
603
|
+
{ name: "value", type: "uint256" },
|
|
604
|
+
{ name: "validAfter", type: "uint256" },
|
|
605
|
+
{ name: "validBefore", type: "uint256" },
|
|
606
|
+
{ name: "nonce", type: "bytes32" },
|
|
607
|
+
{ name: "v", type: "uint8" },
|
|
608
|
+
{ name: "r", type: "bytes32" },
|
|
609
|
+
{ name: "s", type: "bytes32" }
|
|
610
|
+
]
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
type: "function",
|
|
614
|
+
name: "transferWithAuthorization",
|
|
615
|
+
stateMutability: "nonpayable",
|
|
616
|
+
outputs: [],
|
|
617
|
+
inputs: [
|
|
618
|
+
{ name: "from", type: "address" },
|
|
619
|
+
{ name: "to", type: "address" },
|
|
620
|
+
{ name: "value", type: "uint256" },
|
|
621
|
+
{ name: "validAfter", type: "uint256" },
|
|
622
|
+
{ name: "validBefore", type: "uint256" },
|
|
623
|
+
{ name: "nonce", type: "bytes32" },
|
|
624
|
+
{ name: "signature", type: "bytes" }
|
|
625
|
+
]
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
type: "function",
|
|
629
|
+
name: "authorizationState",
|
|
630
|
+
stateMutability: "view",
|
|
631
|
+
inputs: [
|
|
632
|
+
{ name: "authorizer", type: "address" },
|
|
633
|
+
{ name: "nonce", type: "bytes32" }
|
|
634
|
+
],
|
|
635
|
+
outputs: [{ type: "bool" }]
|
|
636
|
+
},
|
|
637
|
+
{ type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
|
|
638
|
+
{ type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] }
|
|
639
|
+
];
|
|
640
|
+
var ZERO_ADDR = "0x0000000000000000000000000000000000000000";
|
|
641
|
+
var ZERO_NONCE = `0x${"00".repeat(32)}`;
|
|
642
|
+
async function readExactDomain(publicClient, asset) {
|
|
643
|
+
if (asset === "native") return null;
|
|
644
|
+
let token;
|
|
645
|
+
try {
|
|
646
|
+
token = getAddress2(asset);
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const [name, version] = await Promise.all([
|
|
652
|
+
publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "name" }),
|
|
653
|
+
publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "version" }),
|
|
654
|
+
// The EIP-3009 probe: this view exists only on EIP-3009 tokens; it reverts on
|
|
655
|
+
// a plain ERC-20 / USDT, marking the token as not exact-payable.
|
|
656
|
+
publicClient.readContract({
|
|
657
|
+
address: token,
|
|
658
|
+
abi: eip3009Abi,
|
|
659
|
+
functionName: "authorizationState",
|
|
660
|
+
args: [ZERO_ADDR, ZERO_NONCE]
|
|
661
|
+
})
|
|
662
|
+
]);
|
|
663
|
+
if (typeof name !== "string" || typeof version !== "string" || !name || !version) return null;
|
|
664
|
+
return { name, version };
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function shorten(msg) {
|
|
670
|
+
const oneLine = msg.replace(/\s+/g, " ").trim();
|
|
671
|
+
return oneLine.length > 200 ? `${oneLine.slice(0, 200)}\u2026` : oneLine;
|
|
672
|
+
}
|
|
673
|
+
async function verifyAndSettleExactEvm(input) {
|
|
674
|
+
const { publicClient, walletClient, account, chain, payload, accept } = input;
|
|
675
|
+
const token = getAddress2(accept.asset);
|
|
676
|
+
const payTo = getAddress2(accept.payTo);
|
|
677
|
+
const requiredAmount = BigInt(accept.amount);
|
|
678
|
+
let from;
|
|
679
|
+
let to;
|
|
680
|
+
let value;
|
|
681
|
+
let validAfter;
|
|
682
|
+
let validBefore;
|
|
683
|
+
let nonce;
|
|
684
|
+
try {
|
|
685
|
+
from = getAddress2(payload.authorization.from);
|
|
686
|
+
to = getAddress2(payload.authorization.to);
|
|
687
|
+
value = BigInt(payload.authorization.value);
|
|
688
|
+
validAfter = BigInt(payload.authorization.validAfter);
|
|
689
|
+
validBefore = BigInt(payload.authorization.validBefore);
|
|
690
|
+
nonce = payload.authorization.nonce;
|
|
691
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(nonce)) throw new Error("nonce must be 32-byte hex");
|
|
692
|
+
if (!/^0x[0-9a-fA-F]+$/.test(payload.signature)) throw new Error("signature must be hex");
|
|
693
|
+
} catch (err) {
|
|
694
|
+
return {
|
|
695
|
+
ok: false,
|
|
696
|
+
error: "signature_invalid",
|
|
697
|
+
detail: `Malformed exact authorization: ${err instanceof Error ? err.message : String(err)}.`
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
if (to !== payTo) {
|
|
701
|
+
return { ok: false, error: "wrong_recipient", detail: `Authorization pays ${to}, not ${payTo}.` };
|
|
702
|
+
}
|
|
703
|
+
if (value < requiredAmount) {
|
|
704
|
+
return { ok: false, error: "amount_too_low", detail: `Authorized ${value}, required ${requiredAmount}.` };
|
|
705
|
+
}
|
|
706
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
707
|
+
if (validBefore <= now) {
|
|
708
|
+
return { ok: false, error: "payment_expired", detail: `Authorization expired: validBefore ${validBefore} <= now ${now}.` };
|
|
709
|
+
}
|
|
710
|
+
let fromCode;
|
|
711
|
+
try {
|
|
712
|
+
fromCode = await publicClient.getCode({ address: from });
|
|
713
|
+
} catch {
|
|
714
|
+
return { ok: false, error: "tx_not_found", detail: `Could not read code at ${from} (transient RPC) \u2014 retry.` };
|
|
715
|
+
}
|
|
716
|
+
const isContractWallet = Boolean(fromCode && fromCode !== "0x");
|
|
717
|
+
if (!isContractWallet) {
|
|
718
|
+
let recovered;
|
|
719
|
+
try {
|
|
720
|
+
recovered = await recoverTypedDataAddress({
|
|
721
|
+
domain: {
|
|
722
|
+
name: accept.extra.name,
|
|
723
|
+
version: accept.extra.version,
|
|
724
|
+
chainId: chain.id,
|
|
725
|
+
verifyingContract: token
|
|
726
|
+
},
|
|
727
|
+
types: EIP3009_TYPES,
|
|
728
|
+
primaryType: "TransferWithAuthorization",
|
|
729
|
+
message: { from, to, value, validAfter, validBefore, nonce },
|
|
730
|
+
signature: payload.signature
|
|
731
|
+
});
|
|
732
|
+
} catch (err) {
|
|
733
|
+
return { ok: false, error: "signature_invalid", detail: `Not a valid EIP-712 signature: ${shorten(err instanceof Error ? err.message : String(err))}.` };
|
|
734
|
+
}
|
|
735
|
+
if (recovered !== from) {
|
|
736
|
+
return { ok: false, error: "signature_invalid", detail: `Signature recovered to ${recovered}, not the authorizer ${from}.` };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
const used = await publicClient.readContract({
|
|
741
|
+
address: token,
|
|
742
|
+
abi: eip3009Abi,
|
|
743
|
+
functionName: "authorizationState",
|
|
744
|
+
args: [from, nonce]
|
|
745
|
+
});
|
|
746
|
+
if (used) {
|
|
747
|
+
return { ok: false, error: "tx_already_used", detail: `Authorization nonce ${nonce} already used or canceled on-chain.` };
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
return { ok: false, error: "tx_not_found", detail: "Could not read authorizationState (transient RPC) \u2014 retry." };
|
|
751
|
+
}
|
|
752
|
+
const baseArgs = [from, to, value, validAfter, validBefore, nonce];
|
|
753
|
+
const isEcdsa = !isContractWallet && payload.signature.length - 2 === 130;
|
|
754
|
+
let writeArgs;
|
|
755
|
+
if (isEcdsa) {
|
|
756
|
+
const { r, s, yParity } = parseSignature(payload.signature);
|
|
757
|
+
writeArgs = [...baseArgs, BigInt(yParity + 27), r, s];
|
|
758
|
+
} else {
|
|
759
|
+
writeArgs = [...baseArgs, payload.signature];
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
await publicClient.simulateContract({
|
|
763
|
+
account,
|
|
764
|
+
address: token,
|
|
765
|
+
abi: eip3009Abi,
|
|
766
|
+
functionName: "transferWithAuthorization",
|
|
767
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
768
|
+
args: writeArgs
|
|
769
|
+
});
|
|
770
|
+
} catch (err) {
|
|
771
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
772
|
+
if (/used or canceled/i.test(msg)) return { ok: false, error: "tx_already_used", detail: "Authorization is used or canceled." };
|
|
773
|
+
if (/expired|not yet valid/i.test(msg)) return { ok: false, error: "payment_expired", detail: shorten(msg) };
|
|
774
|
+
if (/invalid signature/i.test(msg)) return { ok: false, error: "signature_invalid", detail: shorten(msg) };
|
|
775
|
+
return { ok: false, error: "tx_reverted", detail: `transferWithAuthorization would revert: ${shorten(msg)}` };
|
|
776
|
+
}
|
|
777
|
+
let txHash;
|
|
778
|
+
try {
|
|
779
|
+
txHash = await walletClient.writeContract({
|
|
780
|
+
account,
|
|
781
|
+
chain,
|
|
782
|
+
address: token,
|
|
783
|
+
abi: eip3009Abi,
|
|
784
|
+
functionName: "transferWithAuthorization",
|
|
785
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
786
|
+
args: writeArgs
|
|
787
|
+
});
|
|
788
|
+
} catch (err) {
|
|
789
|
+
throw new SettlementError(
|
|
790
|
+
`exact settle: the merchant relayer failed to broadcast transferWithAuthorization (${shorten(err instanceof Error ? err.message : String(err))}). The payer's authorization is still valid and unused \u2014 fund/fix the relayer and the payer can retry.`,
|
|
791
|
+
{ cause: err }
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
const confirmations = accept.extra.minConfirmations ?? 1;
|
|
796
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations });
|
|
797
|
+
if (receipt.status !== "success") {
|
|
798
|
+
return { ok: false, error: "tx_reverted", detail: `Settlement tx ${txHash} reverted on-chain.` };
|
|
799
|
+
}
|
|
800
|
+
} catch (err) {
|
|
801
|
+
throw new SettlementError(
|
|
802
|
+
`exact settle: broadcast ${txHash} but couldn't confirm it (${shorten(err instanceof Error ? err.message : String(err))}).`,
|
|
803
|
+
{ cause: err }
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
ok: true,
|
|
808
|
+
receipt: {
|
|
809
|
+
scheme: "exact",
|
|
810
|
+
success: true,
|
|
811
|
+
network: accept.network,
|
|
812
|
+
transaction: txHash,
|
|
813
|
+
asset: accept.asset,
|
|
814
|
+
amount: accept.amount,
|
|
815
|
+
payer: from,
|
|
816
|
+
payTo: accept.payTo,
|
|
817
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
474
822
|
// src/x402.ts
|
|
475
823
|
var HEADER_REQUIRED = "payment-required";
|
|
476
824
|
var HEADER_SIGNATURE = "payment-signature";
|
|
477
825
|
var HEADER_RESPONSE = "payment-response";
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64").toString("utf8");
|
|
481
|
-
throw new Error("No base64 decoder available in this runtime.");
|
|
482
|
-
}
|
|
826
|
+
var HEADER_SIGNATURE_V1 = "x-payment";
|
|
827
|
+
var HEADER_RESPONSE_V1 = "x-payment-response";
|
|
483
828
|
function encodeBase64(str) {
|
|
484
|
-
if (typeof btoa === "function") return btoa(str);
|
|
485
829
|
if (typeof Buffer !== "undefined") return Buffer.from(str, "utf8").toString("base64");
|
|
830
|
+
if (typeof btoa === "function" && typeof TextEncoder !== "undefined") {
|
|
831
|
+
const bytes = new TextEncoder().encode(str);
|
|
832
|
+
let binary = "";
|
|
833
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
834
|
+
return btoa(binary);
|
|
835
|
+
}
|
|
486
836
|
throw new Error("No base64 encoder available in this runtime.");
|
|
487
837
|
}
|
|
838
|
+
function decodeBase64(b64) {
|
|
839
|
+
if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64").toString("utf8");
|
|
840
|
+
if (typeof atob === "function" && typeof TextDecoder !== "undefined") {
|
|
841
|
+
const binary = atob(b64);
|
|
842
|
+
const bytes = new Uint8Array(binary.length);
|
|
843
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
844
|
+
return new TextDecoder().decode(bytes);
|
|
845
|
+
}
|
|
846
|
+
throw new Error("No base64 decoder available in this runtime.");
|
|
847
|
+
}
|
|
488
848
|
function fromBase64Json(b64) {
|
|
489
849
|
try {
|
|
490
850
|
return JSON.parse(decodeBase64(b64));
|
|
@@ -545,6 +905,36 @@ function parseSignatureHeader(value) {
|
|
|
545
905
|
}
|
|
546
906
|
return parsed;
|
|
547
907
|
}
|
|
908
|
+
function parseExactPaymentHeader(value) {
|
|
909
|
+
const parsed = fromBase64Json(value);
|
|
910
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
911
|
+
const v = parsed;
|
|
912
|
+
const accepted = v.accepted ?? null;
|
|
913
|
+
const scheme = accepted?.scheme ?? v.scheme;
|
|
914
|
+
if (scheme !== "exact") return null;
|
|
915
|
+
const network = accepted?.network ?? v.network;
|
|
916
|
+
if (typeof network !== "string") return null;
|
|
917
|
+
const payload = v.payload;
|
|
918
|
+
if (!payload || typeof payload !== "object") return null;
|
|
919
|
+
const signature = payload.signature;
|
|
920
|
+
const authorization = payload.authorization;
|
|
921
|
+
if (typeof signature !== "string" || !authorization || typeof authorization !== "object") return null;
|
|
922
|
+
for (const k of ["from", "to", "value", "validAfter", "validBefore", "nonce"]) {
|
|
923
|
+
if (typeof authorization[k] !== "string") return null;
|
|
924
|
+
}
|
|
925
|
+
const x402Version = typeof v.x402Version === "number" ? v.x402Version : 2;
|
|
926
|
+
const asset = accepted && typeof accepted.asset === "string" ? accepted.asset : void 0;
|
|
927
|
+
return {
|
|
928
|
+
x402Version,
|
|
929
|
+
network,
|
|
930
|
+
...asset ? { asset } : {},
|
|
931
|
+
payload: {
|
|
932
|
+
signature,
|
|
933
|
+
authorization
|
|
934
|
+
},
|
|
935
|
+
raw: v
|
|
936
|
+
};
|
|
937
|
+
}
|
|
548
938
|
function isValidChallenge(value) {
|
|
549
939
|
if (!value || typeof value !== "object") return false;
|
|
550
940
|
const v = value;
|
|
@@ -556,7 +946,7 @@ function isValidChallenge(value) {
|
|
|
556
946
|
function isValidReceipt(value) {
|
|
557
947
|
if (!value || typeof value !== "object") return false;
|
|
558
948
|
const v = value;
|
|
559
|
-
if (v.scheme !== "onchain-proof") return false;
|
|
949
|
+
if (v.scheme !== "onchain-proof" && v.scheme !== "exact") return false;
|
|
560
950
|
if (typeof v.transaction !== "string" && typeof v.txHash !== "string") return false;
|
|
561
951
|
if (typeof v.payer !== "string") return false;
|
|
562
952
|
return true;
|
|
@@ -634,12 +1024,12 @@ function makeEvmNetwork(resolved) {
|
|
|
634
1024
|
}
|
|
635
1025
|
let normalized;
|
|
636
1026
|
try {
|
|
637
|
-
normalized =
|
|
1027
|
+
normalized = getAddress3(asset);
|
|
638
1028
|
} catch {
|
|
639
1029
|
return null;
|
|
640
1030
|
}
|
|
641
1031
|
for (const info of Object.values(resolved.tokens)) {
|
|
642
|
-
if (
|
|
1032
|
+
if (getAddress3(info.address) === normalized) {
|
|
643
1033
|
return { symbol: info.symbol, decimals: info.decimals };
|
|
644
1034
|
}
|
|
645
1035
|
}
|
|
@@ -723,7 +1113,7 @@ function makeEvmNetwork(resolved) {
|
|
|
723
1113
|
let token = null;
|
|
724
1114
|
try {
|
|
725
1115
|
token = await publicClient.readContract({
|
|
726
|
-
address:
|
|
1116
|
+
address: getAddress3(asset),
|
|
727
1117
|
abi: erc20Abi3,
|
|
728
1118
|
functionName: "balanceOf",
|
|
729
1119
|
args: [owner]
|
|
@@ -755,6 +1145,21 @@ function makeEvmNetwork(resolved) {
|
|
|
755
1145
|
accept,
|
|
756
1146
|
minConfirmations: accept.extra.minConfirmations
|
|
757
1147
|
});
|
|
1148
|
+
},
|
|
1149
|
+
// Standard x402 `exact` rail (EIP-3009), seller side — EVM only.
|
|
1150
|
+
async exactDomain(asset) {
|
|
1151
|
+
return readExactDomain(publicClient, asset);
|
|
1152
|
+
},
|
|
1153
|
+
async settleExactSelf({ relayer, payload, accept }) {
|
|
1154
|
+
const a = relayer._native;
|
|
1155
|
+
return verifyAndSettleExactEvm({
|
|
1156
|
+
publicClient,
|
|
1157
|
+
walletClient: a.walletClient,
|
|
1158
|
+
account: a.account,
|
|
1159
|
+
chain: resolved.chain,
|
|
1160
|
+
payload,
|
|
1161
|
+
accept
|
|
1162
|
+
});
|
|
758
1163
|
}
|
|
759
1164
|
};
|
|
760
1165
|
}
|
|
@@ -771,7 +1176,7 @@ var loaders = {
|
|
|
771
1176
|
solana: async () => {
|
|
772
1177
|
let mod;
|
|
773
1178
|
try {
|
|
774
|
-
mod = await import("./solana-
|
|
1179
|
+
mod = await import("./solana-S3UFI3FE.js");
|
|
775
1180
|
} catch (cause) {
|
|
776
1181
|
throw new MissingDriverError(
|
|
777
1182
|
`Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
|
|
@@ -783,7 +1188,7 @@ var loaders = {
|
|
|
783
1188
|
ton: async () => {
|
|
784
1189
|
let mod;
|
|
785
1190
|
try {
|
|
786
|
-
mod = await import("./ton-
|
|
1191
|
+
mod = await import("./ton-WPTXGLVK.js");
|
|
787
1192
|
} catch (cause) {
|
|
788
1193
|
throw new MissingDriverError(
|
|
789
1194
|
`TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
|
|
@@ -795,7 +1200,7 @@ var loaders = {
|
|
|
795
1200
|
stellar: async () => {
|
|
796
1201
|
let mod;
|
|
797
1202
|
try {
|
|
798
|
-
mod = await import("./stellar-
|
|
1203
|
+
mod = await import("./stellar-Q5PO23SC.js");
|
|
799
1204
|
} catch (cause) {
|
|
800
1205
|
throw new MissingDriverError(
|
|
801
1206
|
`Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
|
|
@@ -807,7 +1212,7 @@ var loaders = {
|
|
|
807
1212
|
xrpl: async () => {
|
|
808
1213
|
let mod;
|
|
809
1214
|
try {
|
|
810
|
-
mod = await import("./xrpl-
|
|
1215
|
+
mod = await import("./xrpl-HEAPEXAM.js");
|
|
811
1216
|
} catch (cause) {
|
|
812
1217
|
throw new MissingDriverError(
|
|
813
1218
|
`XRPL selected, but its package isn't installed. Run: npm install xrpl`,
|
|
@@ -819,7 +1224,7 @@ var loaders = {
|
|
|
819
1224
|
tron: async () => {
|
|
820
1225
|
let mod;
|
|
821
1226
|
try {
|
|
822
|
-
mod = await import("./tron-
|
|
1227
|
+
mod = await import("./tron-6GXBXTR4.js");
|
|
823
1228
|
} catch (cause) {
|
|
824
1229
|
throw new MissingDriverError(
|
|
825
1230
|
`Tron selected, but its package isn't installed. Run: npm install tronweb`,
|
|
@@ -831,7 +1236,7 @@ var loaders = {
|
|
|
831
1236
|
sui: async () => {
|
|
832
1237
|
let mod;
|
|
833
1238
|
try {
|
|
834
|
-
mod = await import("./sui-
|
|
1239
|
+
mod = await import("./sui-WOXRKJXS.js");
|
|
835
1240
|
} catch (cause) {
|
|
836
1241
|
throw new MissingDriverError(
|
|
837
1242
|
`Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
|
|
@@ -843,7 +1248,7 @@ var loaders = {
|
|
|
843
1248
|
near: async () => {
|
|
844
1249
|
let mod;
|
|
845
1250
|
try {
|
|
846
|
-
mod = await import("./near-
|
|
1251
|
+
mod = await import("./near-K6BDBABG.js");
|
|
847
1252
|
} catch (cause) {
|
|
848
1253
|
throw new MissingDriverError(
|
|
849
1254
|
`NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
|
|
@@ -855,7 +1260,7 @@ var loaders = {
|
|
|
855
1260
|
aptos: async () => {
|
|
856
1261
|
let mod;
|
|
857
1262
|
try {
|
|
858
|
-
mod = await import("./aptos-
|
|
1263
|
+
mod = await import("./aptos-LPBLSEIQ.js");
|
|
859
1264
|
} catch (cause) {
|
|
860
1265
|
throw new MissingDriverError(
|
|
861
1266
|
`Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
|
|
@@ -867,7 +1272,7 @@ var loaders = {
|
|
|
867
1272
|
algorand: async () => {
|
|
868
1273
|
let mod;
|
|
869
1274
|
try {
|
|
870
|
-
mod = await import("./algorand-
|
|
1275
|
+
mod = await import("./algorand-WGVF4KTU.js");
|
|
871
1276
|
} catch (cause) {
|
|
872
1277
|
throw new MissingDriverError(
|
|
873
1278
|
`Algorand selected, but its package isn't installed. Run: npm install algosdk`,
|
|
@@ -1651,7 +2056,10 @@ var PipRailClient = class {
|
|
|
1651
2056
|
const chosen = priced.find((p) => p.quote.withinPolicy) ?? priced[0];
|
|
1652
2057
|
return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
|
|
1653
2058
|
}
|
|
1654
|
-
/** The candidate accepts this client could pay: our scheme, on the bound network.
|
|
2059
|
+
/** The candidate accepts this client could pay: our scheme, on the bound network.
|
|
2060
|
+
* A dual-advertised challenge may also carry standard `exact` rails — the PipRail
|
|
2061
|
+
* client ignores those (it pays the backendless `onchain-proof` rail); the type
|
|
2062
|
+
* predicate narrows the `X402AnyAccept` union to the rails we settle. */
|
|
1655
2063
|
gatherCandidates(net, challenge) {
|
|
1656
2064
|
return challenge.accepts.filter(
|
|
1657
2065
|
(a) => a.scheme === "onchain-proof" && net.supports(a.network)
|
|
@@ -2014,6 +2422,14 @@ function isReplayableBodyInit(value) {
|
|
|
2014
2422
|
async function readInvalidReason(response) {
|
|
2015
2423
|
try {
|
|
2016
2424
|
const body = await response.clone().json();
|
|
2425
|
+
const ext = body?.extensions;
|
|
2426
|
+
const piprail = ext?.piprail;
|
|
2427
|
+
if (piprail && typeof piprail.code === "string") {
|
|
2428
|
+
return {
|
|
2429
|
+
error: piprail.code,
|
|
2430
|
+
detail: typeof piprail.detail === "string" ? piprail.detail : ""
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2017
2433
|
if (body && (body.status === "invalid" || typeof body.error === "string")) {
|
|
2018
2434
|
return {
|
|
2019
2435
|
error: typeof body.error === "string" ? body.error : "no error code",
|
|
@@ -2245,6 +2661,100 @@ function paymentTools(client) {
|
|
|
2245
2661
|
];
|
|
2246
2662
|
}
|
|
2247
2663
|
|
|
2664
|
+
// src/facilitator.ts
|
|
2665
|
+
function safeStringify(value) {
|
|
2666
|
+
return JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v);
|
|
2667
|
+
}
|
|
2668
|
+
function mapReason(reason) {
|
|
2669
|
+
const r = (reason ?? "").toLowerCase();
|
|
2670
|
+
if (r.includes("signature")) return "signature_invalid";
|
|
2671
|
+
if (r.includes("recipient")) return "wrong_recipient";
|
|
2672
|
+
if (r.includes("value") || r.includes("amount")) return "amount_too_low";
|
|
2673
|
+
if (r.includes("valid_before") || r.includes("valid_after") || r.includes("expired")) return "payment_expired";
|
|
2674
|
+
if (r.includes("used") || r.includes("replay") || r.includes("nonce") || r.includes("transaction_state")) {
|
|
2675
|
+
return "tx_already_used";
|
|
2676
|
+
}
|
|
2677
|
+
return "tx_reverted";
|
|
2678
|
+
}
|
|
2679
|
+
async function post(url, body, headers) {
|
|
2680
|
+
const res = await fetch(url, {
|
|
2681
|
+
method: "POST",
|
|
2682
|
+
headers: { "content-type": "application/json", ...headers },
|
|
2683
|
+
body: safeStringify(body)
|
|
2684
|
+
});
|
|
2685
|
+
let json = null;
|
|
2686
|
+
try {
|
|
2687
|
+
json = await res.json();
|
|
2688
|
+
} catch {
|
|
2689
|
+
}
|
|
2690
|
+
return { status: res.status, json };
|
|
2691
|
+
}
|
|
2692
|
+
async function settleViaFacilitator(input) {
|
|
2693
|
+
const base2 = input.url.replace(/\/+$/, "");
|
|
2694
|
+
const body = {
|
|
2695
|
+
x402Version: input.x402Version,
|
|
2696
|
+
paymentPayload: input.paymentPayload,
|
|
2697
|
+
paymentRequirements: input.paymentRequirements
|
|
2698
|
+
};
|
|
2699
|
+
const auth = input.authHeaders ? await input.authHeaders() : {};
|
|
2700
|
+
let verify;
|
|
2701
|
+
try {
|
|
2702
|
+
verify = await post(`${base2}/verify`, body, auth);
|
|
2703
|
+
} catch (err) {
|
|
2704
|
+
throw new SettlementError(
|
|
2705
|
+
`exact settle (facilitator ${base2}): /verify request failed (${err instanceof Error ? err.message : String(err)}).`,
|
|
2706
|
+
{ cause: err }
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
if (verify.status !== 200) {
|
|
2710
|
+
throw new SettlementError(
|
|
2711
|
+
`exact settle (facilitator ${base2}): /verify returned HTTP ${verify.status} (transport/auth error).`
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
const vr = verify.json ?? {};
|
|
2715
|
+
if (vr.isValid === false) {
|
|
2716
|
+
return {
|
|
2717
|
+
ok: false,
|
|
2718
|
+
error: mapReason(vr.invalidReason),
|
|
2719
|
+
detail: `Facilitator rejected the payment: ${vr.invalidReason ?? "invalid"}${vr.invalidMessage ? ` \u2014 ${vr.invalidMessage}` : ""}.`
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
let settle;
|
|
2723
|
+
try {
|
|
2724
|
+
settle = await post(`${base2}/settle`, body, auth);
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
throw new SettlementError(
|
|
2727
|
+
`exact settle (facilitator ${base2}): /settle request failed (${err instanceof Error ? err.message : String(err)}).`,
|
|
2728
|
+
{ cause: err }
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
if (settle.status !== 200) {
|
|
2732
|
+
throw new SettlementError(
|
|
2733
|
+
`exact settle (facilitator ${base2}): /settle returned HTTP ${settle.status} (transport/auth error).`
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
const sr = settle.json ?? {};
|
|
2737
|
+
if (!sr.success) {
|
|
2738
|
+
return {
|
|
2739
|
+
ok: false,
|
|
2740
|
+
error: mapReason(sr.errorReason),
|
|
2741
|
+
detail: `Facilitator settlement failed: ${sr.errorReason ?? "unknown"}${sr.errorMessage ? ` \u2014 ${sr.errorMessage}` : ""}.`
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
const receipt = {
|
|
2745
|
+
scheme: "exact",
|
|
2746
|
+
success: true,
|
|
2747
|
+
network: input.receipt.network,
|
|
2748
|
+
transaction: sr.transaction,
|
|
2749
|
+
asset: input.receipt.asset,
|
|
2750
|
+
amount: input.receipt.amount,
|
|
2751
|
+
payer: sr.payer ?? input.payerHint ?? "",
|
|
2752
|
+
payTo: input.receipt.payTo,
|
|
2753
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2754
|
+
};
|
|
2755
|
+
return { ok: true, receipt };
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2248
2758
|
// src/server.ts
|
|
2249
2759
|
function toInvalidBody(result) {
|
|
2250
2760
|
return { x402Version: 2, status: "invalid", error: result.error, detail: result.detail };
|
|
@@ -2271,9 +2781,10 @@ function createPaymentGate(options) {
|
|
|
2271
2781
|
const genNonce = options.generateNonce ?? (() => globalThis.crypto.randomUUID());
|
|
2272
2782
|
let resolved;
|
|
2273
2783
|
function ready() {
|
|
2274
|
-
|
|
2784
|
+
if (resolved) return resolved;
|
|
2785
|
+
const p = (async () => {
|
|
2275
2786
|
const accepts = normaliseAccepts(options);
|
|
2276
|
-
|
|
2787
|
+
const specs = await Promise.all(
|
|
2277
2788
|
accepts.map(async (a) => {
|
|
2278
2789
|
const net = await resolveNetwork2({ chain: a.chain, rpcUrl: a.rpcUrl ?? options.rpcUrl });
|
|
2279
2790
|
const payTo = a.payTo ?? options.payTo;
|
|
@@ -2285,10 +2796,52 @@ function createPaymentGate(options) {
|
|
|
2285
2796
|
net.assertValidPayTo(payTo);
|
|
2286
2797
|
const { asset, decimals, symbol } = net.resolveToken(a.token);
|
|
2287
2798
|
const amountBase = parseUnits(a.amount, decimals);
|
|
2288
|
-
|
|
2799
|
+
const spec = { net, asset, decimals, symbol, amountBase, amountFormatted: a.amount, payTo };
|
|
2800
|
+
if (options.exact) spec.exact = await resolveExactRail(net, asset);
|
|
2801
|
+
return spec;
|
|
2289
2802
|
})
|
|
2290
2803
|
);
|
|
2804
|
+
if (options.exact && !specs.some((s) => s.exact)) {
|
|
2805
|
+
throw new Error(
|
|
2806
|
+
"requirePayment: `exact` was requested but none of the offered rails support it. The standard `exact` rail is EVM + EIP-3009 only (USDC / EURC) \u2014 not native coins, not USDT, not non-EVM chains. Offer an EVM EIP-3009 token, or drop `exact`."
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
return specs;
|
|
2291
2810
|
})();
|
|
2811
|
+
p.catch(() => {
|
|
2812
|
+
if (resolved === p) resolved = void 0;
|
|
2813
|
+
});
|
|
2814
|
+
resolved = p;
|
|
2815
|
+
return p;
|
|
2816
|
+
}
|
|
2817
|
+
async function resolveExactRail(net, asset) {
|
|
2818
|
+
const cfg = options.exact;
|
|
2819
|
+
if (net.family !== "evm" || asset === "native" || !net.exactDomain || !net.settleExactSelf) {
|
|
2820
|
+
return void 0;
|
|
2821
|
+
}
|
|
2822
|
+
const domain = await net.exactDomain(asset);
|
|
2823
|
+
if (!domain) {
|
|
2824
|
+
throw new Error(
|
|
2825
|
+
`requirePayment: \`exact\` requested for asset ${asset} on ${net.network}, but it isn't an EIP-3009 token (couldn't read name()/version()/authorizationState). The exact rail supports USDC / EURC and other EIP-3009 tokens \u2014 USDT and native coins need onchain-proof. (Or check your rpcUrl is reachable.)`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
if (cfg.settle === "self") {
|
|
2829
|
+
if (cfg.relayer === void 0) {
|
|
2830
|
+
throw new Error(
|
|
2831
|
+
"requirePayment: exact `settle: 'self'` needs a `relayer` wallet (the gas-paying key that broadcasts transferWithAuthorization), e.g. exact: { settle: 'self', relayer: { privateKey } }."
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
const relayer = net.bindWallet(cfg.relayer);
|
|
2835
|
+
return { domain, mode: { kind: "self", relayer } };
|
|
2836
|
+
}
|
|
2837
|
+
return {
|
|
2838
|
+
domain,
|
|
2839
|
+
mode: {
|
|
2840
|
+
kind: "facilitator",
|
|
2841
|
+
url: cfg.settle.facilitator,
|
|
2842
|
+
...cfg.settle.authHeaders ? { authHeaders: cfg.settle.authHeaders } : {}
|
|
2843
|
+
}
|
|
2844
|
+
};
|
|
2292
2845
|
}
|
|
2293
2846
|
const hasCustomStore = Boolean(options.isUsed || options.markUsed);
|
|
2294
2847
|
const localUsed = /* @__PURE__ */ new Set();
|
|
@@ -2325,93 +2878,191 @@ function createPaymentGate(options) {
|
|
|
2325
2878
|
}
|
|
2326
2879
|
};
|
|
2327
2880
|
}
|
|
2328
|
-
|
|
2881
|
+
function buildExactAccept(s) {
|
|
2882
|
+
const d = s.exact.domain;
|
|
2883
|
+
return {
|
|
2884
|
+
scheme: "exact",
|
|
2885
|
+
network: s.net.network,
|
|
2886
|
+
amount: s.amountBase.toString(),
|
|
2887
|
+
asset: s.asset,
|
|
2888
|
+
payTo: s.payTo,
|
|
2889
|
+
maxTimeoutSeconds,
|
|
2890
|
+
extra: {
|
|
2891
|
+
assetTransferMethod: "eip3009",
|
|
2892
|
+
name: d.name,
|
|
2893
|
+
version: d.version,
|
|
2894
|
+
minConfirmations,
|
|
2895
|
+
decimals: s.decimals,
|
|
2896
|
+
amountFormatted: s.amountFormatted,
|
|
2897
|
+
...s.symbol ? { symbol: s.symbol } : {}
|
|
2898
|
+
}
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function buildAccepts(specs, nonce) {
|
|
2902
|
+
const out = [];
|
|
2903
|
+
for (const s of specs) {
|
|
2904
|
+
if (s.exact) out.push(buildExactAccept(s));
|
|
2905
|
+
out.push(buildAccept(s, nonce));
|
|
2906
|
+
}
|
|
2907
|
+
return out;
|
|
2908
|
+
}
|
|
2909
|
+
async function makeChallenge(resourceUrl, opts) {
|
|
2329
2910
|
const specs = await ready();
|
|
2330
2911
|
const nonce = genNonce();
|
|
2331
2912
|
const challenge2 = {
|
|
2332
2913
|
x402Version: 2,
|
|
2333
|
-
error: null,
|
|
2334
2914
|
resource: {
|
|
2335
2915
|
url: resourceUrl,
|
|
2336
2916
|
...options.description ? { description: options.description } : {}
|
|
2337
2917
|
},
|
|
2338
|
-
accepts: specs
|
|
2918
|
+
accepts: buildAccepts(specs, nonce),
|
|
2919
|
+
...opts?.error ? { error: opts.error } : {},
|
|
2920
|
+
...opts?.extensions ? { extensions: opts.extensions } : {}
|
|
2339
2921
|
};
|
|
2340
2922
|
return { challenge: challenge2, requiredHeader: buildChallengeHeader(challenge2) };
|
|
2341
2923
|
}
|
|
2924
|
+
async function challenge(resourceUrl = "") {
|
|
2925
|
+
return makeChallenge(resourceUrl);
|
|
2926
|
+
}
|
|
2342
2927
|
async function asChallenge() {
|
|
2343
|
-
const { challenge: c, requiredHeader } = await
|
|
2928
|
+
const { challenge: c, requiredHeader } = await makeChallenge("");
|
|
2344
2929
|
return { kind: "challenge", challenge: c, requiredHeader, statusCode: 402 };
|
|
2345
2930
|
}
|
|
2931
|
+
async function rejection(code, detail) {
|
|
2932
|
+
const { challenge: c, requiredHeader } = await makeChallenge("", {
|
|
2933
|
+
error: `${code}: ${detail}`,
|
|
2934
|
+
extensions: { piprail: { code, detail } }
|
|
2935
|
+
});
|
|
2936
|
+
return { kind: "invalid", error: code, detail, challenge: c, requiredHeader, statusCode: 402 };
|
|
2937
|
+
}
|
|
2938
|
+
function fireOnPaid(receipt) {
|
|
2939
|
+
if (options.onPaid) {
|
|
2940
|
+
try {
|
|
2941
|
+
options.onPaid(receipt);
|
|
2942
|
+
} catch {
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2346
2946
|
async function describe(resourceUrl = "") {
|
|
2347
2947
|
const specs = await ready();
|
|
2348
|
-
const accepts =
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2948
|
+
const accepts = [];
|
|
2949
|
+
for (const s of specs) {
|
|
2950
|
+
const base2 = {
|
|
2951
|
+
network: s.net.network,
|
|
2952
|
+
asset: s.asset,
|
|
2953
|
+
payTo: s.payTo,
|
|
2954
|
+
amount: s.amountBase.toString(),
|
|
2955
|
+
amountFormatted: s.amountFormatted,
|
|
2956
|
+
decimals: s.decimals,
|
|
2957
|
+
maxTimeoutSeconds,
|
|
2958
|
+
...s.symbol ? { symbol: s.symbol } : {}
|
|
2959
|
+
};
|
|
2960
|
+
if (s.exact) accepts.push({ scheme: "exact", ...base2 });
|
|
2961
|
+
accepts.push({ scheme: "onchain-proof", ...base2 });
|
|
2962
|
+
}
|
|
2359
2963
|
return {
|
|
2360
2964
|
url: resourceUrl,
|
|
2361
2965
|
...options.description ? { description: options.description } : {},
|
|
2362
2966
|
accepts
|
|
2363
2967
|
};
|
|
2364
2968
|
}
|
|
2365
|
-
async function
|
|
2366
|
-
const raw = normaliseHeader(paymentSignature);
|
|
2367
|
-
if (!raw) return asChallenge();
|
|
2368
|
-
const sig = parseSignatureHeader(raw);
|
|
2369
|
-
if (!sig || !sig.accepted || typeof sig.accepted.network !== "string" || typeof sig.accepted.asset !== "string") {
|
|
2370
|
-
return asChallenge();
|
|
2371
|
-
}
|
|
2969
|
+
async function verifyOnchainProof(sig) {
|
|
2372
2970
|
const specs = await ready();
|
|
2373
2971
|
const spec = specs.find(
|
|
2374
2972
|
(s) => s.net.network === sig.accepted.network && s.asset === sig.accepted.asset
|
|
2375
2973
|
);
|
|
2376
2974
|
if (!spec) {
|
|
2377
|
-
return
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
statusCode: 402
|
|
2382
|
-
};
|
|
2975
|
+
return rejection(
|
|
2976
|
+
"transfer_not_found",
|
|
2977
|
+
`Proof claims ${sig.accepted.asset} on ${sig.accepted.network}, which this resource doesn't accept (offered: ${specs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
|
|
2978
|
+
);
|
|
2383
2979
|
}
|
|
2384
2980
|
const ref = sig.payload.txHash;
|
|
2385
|
-
if (await claimTx(ref)) {
|
|
2386
|
-
return {
|
|
2387
|
-
kind: "invalid",
|
|
2388
|
-
error: "tx_already_used",
|
|
2389
|
-
detail: `Proof ${ref} was already redeemed.`,
|
|
2390
|
-
statusCode: 402
|
|
2391
|
-
};
|
|
2392
|
-
}
|
|
2981
|
+
if (await claimTx(ref)) return rejection("tx_already_used", `Proof ${ref} was already redeemed.`);
|
|
2393
2982
|
const result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
|
|
2394
2983
|
if (!result.ok) {
|
|
2395
2984
|
await settleTx(ref, false);
|
|
2396
|
-
return
|
|
2397
|
-
kind: "invalid",
|
|
2398
|
-
error: result.error,
|
|
2399
|
-
detail: result.detail,
|
|
2400
|
-
statusCode: 402
|
|
2401
|
-
};
|
|
2985
|
+
return rejection(result.error, result.detail);
|
|
2402
2986
|
}
|
|
2403
2987
|
await settleTx(ref, true);
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2988
|
+
fireOnPaid(result.receipt);
|
|
2989
|
+
return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
|
|
2990
|
+
}
|
|
2991
|
+
async function verifyExact(exact) {
|
|
2992
|
+
const specs = await ready();
|
|
2993
|
+
const exactSpecs = specs.filter((s) => s.exact);
|
|
2994
|
+
if (exactSpecs.length === 0) {
|
|
2995
|
+
return rejection("transfer_not_found", "This resource offers no standard `exact` rail.");
|
|
2996
|
+
}
|
|
2997
|
+
const isCaip = exact.network.startsWith("eip155:");
|
|
2998
|
+
let candidates = isCaip ? exactSpecs.filter((s) => s.net.network === exact.network) : exactSpecs;
|
|
2999
|
+
if (exact.asset) {
|
|
3000
|
+
candidates = candidates.filter((s) => s.asset.toLowerCase() === exact.asset.toLowerCase());
|
|
3001
|
+
}
|
|
3002
|
+
let spec = candidates[0];
|
|
3003
|
+
if (!isCaip && !exact.asset && exactSpecs.length > 1) spec = void 0;
|
|
3004
|
+
if (!spec && !isCaip && !exact.asset && exactSpecs.length === 1) spec = exactSpecs[0];
|
|
3005
|
+
if (!spec || !spec.exact) {
|
|
3006
|
+
return rejection(
|
|
3007
|
+
"transfer_not_found",
|
|
3008
|
+
`No \`exact\` rail offered for ${exact.network}${exact.asset ? `/${exact.asset}` : ""} (offered: ${exactSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
const nonce = exact.payload.authorization.nonce;
|
|
3012
|
+
if (await claimTx(nonce)) {
|
|
3013
|
+
return rejection("tx_already_used", `Authorization nonce ${nonce} was already redeemed.`);
|
|
3014
|
+
}
|
|
3015
|
+
const accept = buildExactAccept(spec);
|
|
3016
|
+
const mode = spec.exact.mode;
|
|
3017
|
+
let result;
|
|
3018
|
+
try {
|
|
3019
|
+
if (mode.kind === "self") {
|
|
3020
|
+
result = await spec.net.settleExactSelf({ relayer: mode.relayer, payload: exact.payload, accept });
|
|
3021
|
+
} else {
|
|
3022
|
+
result = await settleViaFacilitator({
|
|
3023
|
+
url: mode.url,
|
|
3024
|
+
...mode.authHeaders ? { authHeaders: mode.authHeaders } : {},
|
|
3025
|
+
// PipRail always builds a v2-shaped paymentRequirements (CAIP-2 network + `amount`),
|
|
3026
|
+
// so force x402Version:2 — echoing a v1 client's version here would hand the facilitator
|
|
3027
|
+
// a self-inconsistent request (v1 envelope, v2 requirements). The inner payload is
|
|
3028
|
+
// byte-identical across versions, so forwarding it verbatim is fine.
|
|
3029
|
+
x402Version: 2,
|
|
3030
|
+
paymentPayload: exact.raw,
|
|
3031
|
+
paymentRequirements: {
|
|
3032
|
+
scheme: "exact",
|
|
3033
|
+
network: accept.network,
|
|
3034
|
+
asset: accept.asset,
|
|
3035
|
+
amount: accept.amount,
|
|
3036
|
+
payTo: accept.payTo,
|
|
3037
|
+
maxTimeoutSeconds: accept.maxTimeoutSeconds,
|
|
3038
|
+
extra: { name: accept.extra.name, version: accept.extra.version }
|
|
3039
|
+
},
|
|
3040
|
+
receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
|
|
3041
|
+
payerHint: exact.payload.authorization.from
|
|
3042
|
+
});
|
|
2408
3043
|
}
|
|
3044
|
+
} catch (err) {
|
|
3045
|
+
await settleTx(nonce, false);
|
|
3046
|
+
throw err;
|
|
2409
3047
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
3048
|
+
if (!result.ok) {
|
|
3049
|
+
await settleTx(nonce, false);
|
|
3050
|
+
return rejection(result.error, result.detail);
|
|
3051
|
+
}
|
|
3052
|
+
await settleTx(nonce, true);
|
|
3053
|
+
fireOnPaid(result.receipt);
|
|
3054
|
+
return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
|
|
3055
|
+
}
|
|
3056
|
+
async function verify(paymentSignature) {
|
|
3057
|
+
const raw = normaliseHeader(paymentSignature);
|
|
3058
|
+
if (!raw) return asChallenge();
|
|
3059
|
+
const sig = parseSignatureHeader(raw);
|
|
3060
|
+
if (sig && sig.accepted && typeof sig.accepted.network === "string" && typeof sig.accepted.asset === "string") {
|
|
3061
|
+
return verifyOnchainProof(sig);
|
|
3062
|
+
}
|
|
3063
|
+
const exact = parseExactPaymentHeader(raw);
|
|
3064
|
+
if (exact) return verifyExact(exact);
|
|
3065
|
+
return asChallenge();
|
|
2415
3066
|
}
|
|
2416
3067
|
return { challenge, verify, describe };
|
|
2417
3068
|
}
|
|
@@ -2420,14 +3071,20 @@ function requirePayment(options) {
|
|
|
2420
3071
|
return async (req, res, next) => {
|
|
2421
3072
|
let result;
|
|
2422
3073
|
try {
|
|
2423
|
-
result = await gate.verify(req.headers[HEADER_SIGNATURE]);
|
|
3074
|
+
result = await gate.verify(req.headers[HEADER_SIGNATURE] ?? req.headers[HEADER_SIGNATURE_V1]);
|
|
2424
3075
|
} catch (err) {
|
|
3076
|
+
if (err instanceof SettlementError) {
|
|
3077
|
+
res.status(502);
|
|
3078
|
+
res.json({ x402Version: 2, error: "settlement_failed", detail: err.message });
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
2425
3081
|
next(err);
|
|
2426
3082
|
return;
|
|
2427
3083
|
}
|
|
2428
3084
|
switch (result.kind) {
|
|
2429
3085
|
case "paid":
|
|
2430
3086
|
res.setHeader(HEADER_RESPONSE, result.receiptHeader);
|
|
3087
|
+
res.setHeader(HEADER_RESPONSE_V1, result.receiptHeader);
|
|
2431
3088
|
return next();
|
|
2432
3089
|
case "challenge":
|
|
2433
3090
|
res.setHeader(HEADER_REQUIRED, result.requiredHeader);
|
|
@@ -2435,8 +3092,9 @@ function requirePayment(options) {
|
|
|
2435
3092
|
res.json(result.challenge);
|
|
2436
3093
|
return;
|
|
2437
3094
|
case "invalid":
|
|
3095
|
+
res.setHeader(HEADER_REQUIRED, result.requiredHeader);
|
|
2438
3096
|
res.status(result.statusCode);
|
|
2439
|
-
res.json(
|
|
3097
|
+
res.json(result.challenge);
|
|
2440
3098
|
return;
|
|
2441
3099
|
}
|
|
2442
3100
|
};
|
|
@@ -2446,104 +3104,6 @@ function normaliseHeader(value) {
|
|
|
2446
3104
|
return value;
|
|
2447
3105
|
}
|
|
2448
3106
|
|
|
2449
|
-
// src/drivers/evm/exact.ts
|
|
2450
|
-
var EXACT_NETWORK_SLUGS = {
|
|
2451
|
-
ethereum: 1,
|
|
2452
|
-
base: 8453,
|
|
2453
|
-
"base-sepolia": 84532,
|
|
2454
|
-
arbitrum: 42161,
|
|
2455
|
-
optimism: 10,
|
|
2456
|
-
polygon: 137,
|
|
2457
|
-
avalanche: 43114
|
|
2458
|
-
};
|
|
2459
|
-
function chainIdForExactNetwork(slug) {
|
|
2460
|
-
return EXACT_NETWORK_SLUGS[slug] ?? null;
|
|
2461
|
-
}
|
|
2462
|
-
var EIP3009_TYPES = {
|
|
2463
|
-
TransferWithAuthorization: [
|
|
2464
|
-
{ name: "from", type: "address" },
|
|
2465
|
-
{ name: "to", type: "address" },
|
|
2466
|
-
{ name: "value", type: "uint256" },
|
|
2467
|
-
{ name: "validAfter", type: "uint256" },
|
|
2468
|
-
{ name: "validBefore", type: "uint256" },
|
|
2469
|
-
{ name: "nonce", type: "bytes32" }
|
|
2470
|
-
]
|
|
2471
|
-
};
|
|
2472
|
-
function parseExactRequirements(body) {
|
|
2473
|
-
if (!body || typeof body !== "object") return null;
|
|
2474
|
-
const accepts = body.accepts;
|
|
2475
|
-
if (!Array.isArray(accepts)) return null;
|
|
2476
|
-
const out = [];
|
|
2477
|
-
for (const raw of accepts) {
|
|
2478
|
-
if (!raw || typeof raw !== "object") continue;
|
|
2479
|
-
const a = raw;
|
|
2480
|
-
if (a.scheme !== "exact") continue;
|
|
2481
|
-
const amount = a.maxAmountRequired ?? a.amount;
|
|
2482
|
-
if (typeof a.network !== "string" || typeof amount !== "string" || typeof a.asset !== "string" || typeof a.payTo !== "string") {
|
|
2483
|
-
continue;
|
|
2484
|
-
}
|
|
2485
|
-
out.push({
|
|
2486
|
-
scheme: "exact",
|
|
2487
|
-
network: a.network,
|
|
2488
|
-
maxAmountRequired: amount,
|
|
2489
|
-
asset: a.asset,
|
|
2490
|
-
payTo: a.payTo,
|
|
2491
|
-
maxTimeoutSeconds: typeof a.maxTimeoutSeconds === "number" ? a.maxTimeoutSeconds : 600,
|
|
2492
|
-
...a.extra && typeof a.extra === "object" ? { extra: a.extra } : {},
|
|
2493
|
-
...typeof a.description === "string" ? { description: a.description } : {},
|
|
2494
|
-
...typeof a.resource === "string" ? { resource: a.resource } : {}
|
|
2495
|
-
});
|
|
2496
|
-
}
|
|
2497
|
-
return out;
|
|
2498
|
-
}
|
|
2499
|
-
async function buildExactAuthorization(params) {
|
|
2500
|
-
const { account, accept, chainId, now, nonce } = params;
|
|
2501
|
-
if (!account.signTypedData) {
|
|
2502
|
-
throw new Error("buildExactAuthorization: the account cannot sign EIP-712 typed data.");
|
|
2503
|
-
}
|
|
2504
|
-
const authorization = {
|
|
2505
|
-
from: account.address,
|
|
2506
|
-
to: accept.payTo,
|
|
2507
|
-
value: accept.maxAmountRequired,
|
|
2508
|
-
validAfter: "0",
|
|
2509
|
-
validBefore: String(now + accept.maxTimeoutSeconds),
|
|
2510
|
-
nonce
|
|
2511
|
-
};
|
|
2512
|
-
const signature = await account.signTypedData({
|
|
2513
|
-
domain: {
|
|
2514
|
-
name: accept.extra?.name ?? "USD Coin",
|
|
2515
|
-
version: accept.extra?.version ?? "2",
|
|
2516
|
-
chainId,
|
|
2517
|
-
verifyingContract: accept.asset
|
|
2518
|
-
},
|
|
2519
|
-
types: EIP3009_TYPES,
|
|
2520
|
-
primaryType: "TransferWithAuthorization",
|
|
2521
|
-
message: {
|
|
2522
|
-
from: authorization.from,
|
|
2523
|
-
to: authorization.to,
|
|
2524
|
-
value: BigInt(authorization.value),
|
|
2525
|
-
validAfter: BigInt(authorization.validAfter),
|
|
2526
|
-
validBefore: BigInt(authorization.validBefore),
|
|
2527
|
-
nonce: authorization.nonce
|
|
2528
|
-
}
|
|
2529
|
-
});
|
|
2530
|
-
return { authorization, signature };
|
|
2531
|
-
}
|
|
2532
|
-
function base64(str) {
|
|
2533
|
-
if (typeof btoa === "function") return btoa(str);
|
|
2534
|
-
if (typeof Buffer !== "undefined") return Buffer.from(str, "utf8").toString("base64");
|
|
2535
|
-
throw new Error("No base64 encoder available in this runtime.");
|
|
2536
|
-
}
|
|
2537
|
-
function encodeXPaymentHeader(input) {
|
|
2538
|
-
const payload = {
|
|
2539
|
-
x402Version: input.x402Version ?? 1,
|
|
2540
|
-
scheme: "exact",
|
|
2541
|
-
network: input.network,
|
|
2542
|
-
payload: { signature: input.signature, authorization: input.authorization }
|
|
2543
|
-
};
|
|
2544
|
-
return base64(JSON.stringify(payload));
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
3107
|
// src/discovery.ts
|
|
2548
3108
|
var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
|
|
2549
3109
|
function pathOf(url) {
|
|
@@ -2604,6 +3164,11 @@ export {
|
|
|
2604
3164
|
EIP3009_TYPES,
|
|
2605
3165
|
EXACT_NETWORK_SLUGS,
|
|
2606
3166
|
GENERATOR,
|
|
3167
|
+
HEADER_REQUIRED,
|
|
3168
|
+
HEADER_RESPONSE,
|
|
3169
|
+
HEADER_RESPONSE_V1,
|
|
3170
|
+
HEADER_SIGNATURE,
|
|
3171
|
+
HEADER_SIGNATURE_V1,
|
|
2607
3172
|
InsufficientFundsError,
|
|
2608
3173
|
InvalidEnvelopeError,
|
|
2609
3174
|
MaxRetriesExceededError,
|
|
@@ -2615,6 +3180,7 @@ export {
|
|
|
2615
3180
|
PipRailClient,
|
|
2616
3181
|
PipRailError,
|
|
2617
3182
|
RecipientNotReadyError,
|
|
3183
|
+
SettlementError,
|
|
2618
3184
|
UnknownTokenError,
|
|
2619
3185
|
UnsupportedNetworkError,
|
|
2620
3186
|
WrongChainError,
|
|
@@ -2628,22 +3194,26 @@ export {
|
|
|
2628
3194
|
buildX402DnsTxt,
|
|
2629
3195
|
chainIdForExactNetwork,
|
|
2630
3196
|
createPaymentGate,
|
|
3197
|
+
eip3009Abi,
|
|
2631
3198
|
encodeXPaymentHeader,
|
|
2632
3199
|
evaluatePolicy,
|
|
2633
3200
|
normalizeNetwork,
|
|
2634
3201
|
parseChallenge,
|
|
3202
|
+
parseExactPaymentHeader,
|
|
2635
3203
|
parseExactRequirements,
|
|
2636
3204
|
parseReceipt,
|
|
2637
3205
|
parseSignatureHeader,
|
|
2638
3206
|
paymentTools,
|
|
2639
3207
|
pickAccept,
|
|
2640
3208
|
planAcross,
|
|
3209
|
+
readExactDomain,
|
|
2641
3210
|
register402Index,
|
|
2642
3211
|
registerDriver,
|
|
2643
3212
|
registerX402Scan,
|
|
2644
3213
|
requirePayment,
|
|
2645
3214
|
resolveChain,
|
|
2646
3215
|
searchOpenIndexes,
|
|
3216
|
+
settleViaFacilitator,
|
|
2647
3217
|
toInsufficientFundsError,
|
|
2648
3218
|
toInvalidBody
|
|
2649
3219
|
};
|