@piprail/sdk 1.9.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/CHANGELOG.md +42 -0
- package/ERRORS.md +21 -8
- package/README.md +20 -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 +811 -253
- package/dist/index.d.cts +400 -31
- package/dist/index.d.ts +400 -31
- package/dist/index.js +734 -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 +1 -1
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";
|
|
@@ -483,20 +484,367 @@ function sumTransfersTo(logs, asset, payTo) {
|
|
|
483
484
|
return { total, from };
|
|
484
485
|
}
|
|
485
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
|
+
|
|
486
822
|
// src/x402.ts
|
|
487
823
|
var HEADER_REQUIRED = "payment-required";
|
|
488
824
|
var HEADER_SIGNATURE = "payment-signature";
|
|
489
825
|
var HEADER_RESPONSE = "payment-response";
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64").toString("utf8");
|
|
493
|
-
throw new Error("No base64 decoder available in this runtime.");
|
|
494
|
-
}
|
|
826
|
+
var HEADER_SIGNATURE_V1 = "x-payment";
|
|
827
|
+
var HEADER_RESPONSE_V1 = "x-payment-response";
|
|
495
828
|
function encodeBase64(str) {
|
|
496
|
-
if (typeof btoa === "function") return btoa(str);
|
|
497
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
|
+
}
|
|
498
836
|
throw new Error("No base64 encoder available in this runtime.");
|
|
499
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
|
+
}
|
|
500
848
|
function fromBase64Json(b64) {
|
|
501
849
|
try {
|
|
502
850
|
return JSON.parse(decodeBase64(b64));
|
|
@@ -557,6 +905,36 @@ function parseSignatureHeader(value) {
|
|
|
557
905
|
}
|
|
558
906
|
return parsed;
|
|
559
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
|
+
}
|
|
560
938
|
function isValidChallenge(value) {
|
|
561
939
|
if (!value || typeof value !== "object") return false;
|
|
562
940
|
const v = value;
|
|
@@ -568,7 +946,7 @@ function isValidChallenge(value) {
|
|
|
568
946
|
function isValidReceipt(value) {
|
|
569
947
|
if (!value || typeof value !== "object") return false;
|
|
570
948
|
const v = value;
|
|
571
|
-
if (v.scheme !== "onchain-proof") return false;
|
|
949
|
+
if (v.scheme !== "onchain-proof" && v.scheme !== "exact") return false;
|
|
572
950
|
if (typeof v.transaction !== "string" && typeof v.txHash !== "string") return false;
|
|
573
951
|
if (typeof v.payer !== "string") return false;
|
|
574
952
|
return true;
|
|
@@ -646,12 +1024,12 @@ function makeEvmNetwork(resolved) {
|
|
|
646
1024
|
}
|
|
647
1025
|
let normalized;
|
|
648
1026
|
try {
|
|
649
|
-
normalized =
|
|
1027
|
+
normalized = getAddress3(asset);
|
|
650
1028
|
} catch {
|
|
651
1029
|
return null;
|
|
652
1030
|
}
|
|
653
1031
|
for (const info of Object.values(resolved.tokens)) {
|
|
654
|
-
if (
|
|
1032
|
+
if (getAddress3(info.address) === normalized) {
|
|
655
1033
|
return { symbol: info.symbol, decimals: info.decimals };
|
|
656
1034
|
}
|
|
657
1035
|
}
|
|
@@ -735,7 +1113,7 @@ function makeEvmNetwork(resolved) {
|
|
|
735
1113
|
let token = null;
|
|
736
1114
|
try {
|
|
737
1115
|
token = await publicClient.readContract({
|
|
738
|
-
address:
|
|
1116
|
+
address: getAddress3(asset),
|
|
739
1117
|
abi: erc20Abi3,
|
|
740
1118
|
functionName: "balanceOf",
|
|
741
1119
|
args: [owner]
|
|
@@ -767,6 +1145,21 @@ function makeEvmNetwork(resolved) {
|
|
|
767
1145
|
accept,
|
|
768
1146
|
minConfirmations: accept.extra.minConfirmations
|
|
769
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
|
+
});
|
|
770
1163
|
}
|
|
771
1164
|
};
|
|
772
1165
|
}
|
|
@@ -783,7 +1176,7 @@ var loaders = {
|
|
|
783
1176
|
solana: async () => {
|
|
784
1177
|
let mod;
|
|
785
1178
|
try {
|
|
786
|
-
mod = await import("./solana-
|
|
1179
|
+
mod = await import("./solana-S3UFI3FE.js");
|
|
787
1180
|
} catch (cause) {
|
|
788
1181
|
throw new MissingDriverError(
|
|
789
1182
|
`Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
|
|
@@ -795,7 +1188,7 @@ var loaders = {
|
|
|
795
1188
|
ton: async () => {
|
|
796
1189
|
let mod;
|
|
797
1190
|
try {
|
|
798
|
-
mod = await import("./ton-
|
|
1191
|
+
mod = await import("./ton-WPTXGLVK.js");
|
|
799
1192
|
} catch (cause) {
|
|
800
1193
|
throw new MissingDriverError(
|
|
801
1194
|
`TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
|
|
@@ -807,7 +1200,7 @@ var loaders = {
|
|
|
807
1200
|
stellar: async () => {
|
|
808
1201
|
let mod;
|
|
809
1202
|
try {
|
|
810
|
-
mod = await import("./stellar-
|
|
1203
|
+
mod = await import("./stellar-Q5PO23SC.js");
|
|
811
1204
|
} catch (cause) {
|
|
812
1205
|
throw new MissingDriverError(
|
|
813
1206
|
`Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
|
|
@@ -819,7 +1212,7 @@ var loaders = {
|
|
|
819
1212
|
xrpl: async () => {
|
|
820
1213
|
let mod;
|
|
821
1214
|
try {
|
|
822
|
-
mod = await import("./xrpl-
|
|
1215
|
+
mod = await import("./xrpl-HEAPEXAM.js");
|
|
823
1216
|
} catch (cause) {
|
|
824
1217
|
throw new MissingDriverError(
|
|
825
1218
|
`XRPL selected, but its package isn't installed. Run: npm install xrpl`,
|
|
@@ -831,7 +1224,7 @@ var loaders = {
|
|
|
831
1224
|
tron: async () => {
|
|
832
1225
|
let mod;
|
|
833
1226
|
try {
|
|
834
|
-
mod = await import("./tron-
|
|
1227
|
+
mod = await import("./tron-6GXBXTR4.js");
|
|
835
1228
|
} catch (cause) {
|
|
836
1229
|
throw new MissingDriverError(
|
|
837
1230
|
`Tron selected, but its package isn't installed. Run: npm install tronweb`,
|
|
@@ -843,7 +1236,7 @@ var loaders = {
|
|
|
843
1236
|
sui: async () => {
|
|
844
1237
|
let mod;
|
|
845
1238
|
try {
|
|
846
|
-
mod = await import("./sui-
|
|
1239
|
+
mod = await import("./sui-WOXRKJXS.js");
|
|
847
1240
|
} catch (cause) {
|
|
848
1241
|
throw new MissingDriverError(
|
|
849
1242
|
`Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
|
|
@@ -855,7 +1248,7 @@ var loaders = {
|
|
|
855
1248
|
near: async () => {
|
|
856
1249
|
let mod;
|
|
857
1250
|
try {
|
|
858
|
-
mod = await import("./near-
|
|
1251
|
+
mod = await import("./near-K6BDBABG.js");
|
|
859
1252
|
} catch (cause) {
|
|
860
1253
|
throw new MissingDriverError(
|
|
861
1254
|
`NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
|
|
@@ -867,7 +1260,7 @@ var loaders = {
|
|
|
867
1260
|
aptos: async () => {
|
|
868
1261
|
let mod;
|
|
869
1262
|
try {
|
|
870
|
-
mod = await import("./aptos-
|
|
1263
|
+
mod = await import("./aptos-LPBLSEIQ.js");
|
|
871
1264
|
} catch (cause) {
|
|
872
1265
|
throw new MissingDriverError(
|
|
873
1266
|
`Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
|
|
@@ -879,7 +1272,7 @@ var loaders = {
|
|
|
879
1272
|
algorand: async () => {
|
|
880
1273
|
let mod;
|
|
881
1274
|
try {
|
|
882
|
-
mod = await import("./algorand-
|
|
1275
|
+
mod = await import("./algorand-WGVF4KTU.js");
|
|
883
1276
|
} catch (cause) {
|
|
884
1277
|
throw new MissingDriverError(
|
|
885
1278
|
`Algorand selected, but its package isn't installed. Run: npm install algosdk`,
|
|
@@ -1663,7 +2056,10 @@ var PipRailClient = class {
|
|
|
1663
2056
|
const chosen = priced.find((p) => p.quote.withinPolicy) ?? priced[0];
|
|
1664
2057
|
return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
|
|
1665
2058
|
}
|
|
1666
|
-
/** 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. */
|
|
1667
2063
|
gatherCandidates(net, challenge) {
|
|
1668
2064
|
return challenge.accepts.filter(
|
|
1669
2065
|
(a) => a.scheme === "onchain-proof" && net.supports(a.network)
|
|
@@ -2026,6 +2422,14 @@ function isReplayableBodyInit(value) {
|
|
|
2026
2422
|
async function readInvalidReason(response) {
|
|
2027
2423
|
try {
|
|
2028
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
|
+
}
|
|
2029
2433
|
if (body && (body.status === "invalid" || typeof body.error === "string")) {
|
|
2030
2434
|
return {
|
|
2031
2435
|
error: typeof body.error === "string" ? body.error : "no error code",
|
|
@@ -2257,6 +2661,100 @@ function paymentTools(client) {
|
|
|
2257
2661
|
];
|
|
2258
2662
|
}
|
|
2259
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
|
+
|
|
2260
2758
|
// src/server.ts
|
|
2261
2759
|
function toInvalidBody(result) {
|
|
2262
2760
|
return { x402Version: 2, status: "invalid", error: result.error, detail: result.detail };
|
|
@@ -2283,9 +2781,10 @@ function createPaymentGate(options) {
|
|
|
2283
2781
|
const genNonce = options.generateNonce ?? (() => globalThis.crypto.randomUUID());
|
|
2284
2782
|
let resolved;
|
|
2285
2783
|
function ready() {
|
|
2286
|
-
|
|
2784
|
+
if (resolved) return resolved;
|
|
2785
|
+
const p = (async () => {
|
|
2287
2786
|
const accepts = normaliseAccepts(options);
|
|
2288
|
-
|
|
2787
|
+
const specs = await Promise.all(
|
|
2289
2788
|
accepts.map(async (a) => {
|
|
2290
2789
|
const net = await resolveNetwork2({ chain: a.chain, rpcUrl: a.rpcUrl ?? options.rpcUrl });
|
|
2291
2790
|
const payTo = a.payTo ?? options.payTo;
|
|
@@ -2297,10 +2796,52 @@ function createPaymentGate(options) {
|
|
|
2297
2796
|
net.assertValidPayTo(payTo);
|
|
2298
2797
|
const { asset, decimals, symbol } = net.resolveToken(a.token);
|
|
2299
2798
|
const amountBase = parseUnits(a.amount, decimals);
|
|
2300
|
-
|
|
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;
|
|
2301
2802
|
})
|
|
2302
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;
|
|
2303
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
|
+
};
|
|
2304
2845
|
}
|
|
2305
2846
|
const hasCustomStore = Boolean(options.isUsed || options.markUsed);
|
|
2306
2847
|
const localUsed = /* @__PURE__ */ new Set();
|
|
@@ -2337,93 +2878,191 @@ function createPaymentGate(options) {
|
|
|
2337
2878
|
}
|
|
2338
2879
|
};
|
|
2339
2880
|
}
|
|
2340
|
-
|
|
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) {
|
|
2341
2910
|
const specs = await ready();
|
|
2342
2911
|
const nonce = genNonce();
|
|
2343
2912
|
const challenge2 = {
|
|
2344
2913
|
x402Version: 2,
|
|
2345
|
-
error: null,
|
|
2346
2914
|
resource: {
|
|
2347
2915
|
url: resourceUrl,
|
|
2348
2916
|
...options.description ? { description: options.description } : {}
|
|
2349
2917
|
},
|
|
2350
|
-
accepts: specs
|
|
2918
|
+
accepts: buildAccepts(specs, nonce),
|
|
2919
|
+
...opts?.error ? { error: opts.error } : {},
|
|
2920
|
+
...opts?.extensions ? { extensions: opts.extensions } : {}
|
|
2351
2921
|
};
|
|
2352
2922
|
return { challenge: challenge2, requiredHeader: buildChallengeHeader(challenge2) };
|
|
2353
2923
|
}
|
|
2924
|
+
async function challenge(resourceUrl = "") {
|
|
2925
|
+
return makeChallenge(resourceUrl);
|
|
2926
|
+
}
|
|
2354
2927
|
async function asChallenge() {
|
|
2355
|
-
const { challenge: c, requiredHeader } = await
|
|
2928
|
+
const { challenge: c, requiredHeader } = await makeChallenge("");
|
|
2356
2929
|
return { kind: "challenge", challenge: c, requiredHeader, statusCode: 402 };
|
|
2357
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
|
+
}
|
|
2358
2946
|
async function describe(resourceUrl = "") {
|
|
2359
2947
|
const specs = await ready();
|
|
2360
|
-
const accepts =
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
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
|
+
}
|
|
2371
2963
|
return {
|
|
2372
2964
|
url: resourceUrl,
|
|
2373
2965
|
...options.description ? { description: options.description } : {},
|
|
2374
2966
|
accepts
|
|
2375
2967
|
};
|
|
2376
2968
|
}
|
|
2377
|
-
async function
|
|
2378
|
-
const raw = normaliseHeader(paymentSignature);
|
|
2379
|
-
if (!raw) return asChallenge();
|
|
2380
|
-
const sig = parseSignatureHeader(raw);
|
|
2381
|
-
if (!sig || !sig.accepted || typeof sig.accepted.network !== "string" || typeof sig.accepted.asset !== "string") {
|
|
2382
|
-
return asChallenge();
|
|
2383
|
-
}
|
|
2969
|
+
async function verifyOnchainProof(sig) {
|
|
2384
2970
|
const specs = await ready();
|
|
2385
2971
|
const spec = specs.find(
|
|
2386
2972
|
(s) => s.net.network === sig.accepted.network && s.asset === sig.accepted.asset
|
|
2387
2973
|
);
|
|
2388
2974
|
if (!spec) {
|
|
2389
|
-
return
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
statusCode: 402
|
|
2394
|
-
};
|
|
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
|
+
);
|
|
2395
2979
|
}
|
|
2396
2980
|
const ref = sig.payload.txHash;
|
|
2397
|
-
if (await claimTx(ref)) {
|
|
2398
|
-
return {
|
|
2399
|
-
kind: "invalid",
|
|
2400
|
-
error: "tx_already_used",
|
|
2401
|
-
detail: `Proof ${ref} was already redeemed.`,
|
|
2402
|
-
statusCode: 402
|
|
2403
|
-
};
|
|
2404
|
-
}
|
|
2981
|
+
if (await claimTx(ref)) return rejection("tx_already_used", `Proof ${ref} was already redeemed.`);
|
|
2405
2982
|
const result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
|
|
2406
2983
|
if (!result.ok) {
|
|
2407
2984
|
await settleTx(ref, false);
|
|
2408
|
-
return
|
|
2409
|
-
kind: "invalid",
|
|
2410
|
-
error: result.error,
|
|
2411
|
-
detail: result.detail,
|
|
2412
|
-
statusCode: 402
|
|
2413
|
-
};
|
|
2985
|
+
return rejection(result.error, result.detail);
|
|
2414
2986
|
}
|
|
2415
2987
|
await settleTx(ref, true);
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
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
|
+
});
|
|
2420
3043
|
}
|
|
3044
|
+
} catch (err) {
|
|
3045
|
+
await settleTx(nonce, false);
|
|
3046
|
+
throw err;
|
|
2421
3047
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
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();
|
|
2427
3066
|
}
|
|
2428
3067
|
return { challenge, verify, describe };
|
|
2429
3068
|
}
|
|
@@ -2432,14 +3071,20 @@ function requirePayment(options) {
|
|
|
2432
3071
|
return async (req, res, next) => {
|
|
2433
3072
|
let result;
|
|
2434
3073
|
try {
|
|
2435
|
-
result = await gate.verify(req.headers[HEADER_SIGNATURE]);
|
|
3074
|
+
result = await gate.verify(req.headers[HEADER_SIGNATURE] ?? req.headers[HEADER_SIGNATURE_V1]);
|
|
2436
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
|
+
}
|
|
2437
3081
|
next(err);
|
|
2438
3082
|
return;
|
|
2439
3083
|
}
|
|
2440
3084
|
switch (result.kind) {
|
|
2441
3085
|
case "paid":
|
|
2442
3086
|
res.setHeader(HEADER_RESPONSE, result.receiptHeader);
|
|
3087
|
+
res.setHeader(HEADER_RESPONSE_V1, result.receiptHeader);
|
|
2443
3088
|
return next();
|
|
2444
3089
|
case "challenge":
|
|
2445
3090
|
res.setHeader(HEADER_REQUIRED, result.requiredHeader);
|
|
@@ -2447,8 +3092,9 @@ function requirePayment(options) {
|
|
|
2447
3092
|
res.json(result.challenge);
|
|
2448
3093
|
return;
|
|
2449
3094
|
case "invalid":
|
|
3095
|
+
res.setHeader(HEADER_REQUIRED, result.requiredHeader);
|
|
2450
3096
|
res.status(result.statusCode);
|
|
2451
|
-
res.json(
|
|
3097
|
+
res.json(result.challenge);
|
|
2452
3098
|
return;
|
|
2453
3099
|
}
|
|
2454
3100
|
};
|
|
@@ -2458,104 +3104,6 @@ function normaliseHeader(value) {
|
|
|
2458
3104
|
return value;
|
|
2459
3105
|
}
|
|
2460
3106
|
|
|
2461
|
-
// src/drivers/evm/exact.ts
|
|
2462
|
-
var EXACT_NETWORK_SLUGS = {
|
|
2463
|
-
ethereum: 1,
|
|
2464
|
-
base: 8453,
|
|
2465
|
-
"base-sepolia": 84532,
|
|
2466
|
-
arbitrum: 42161,
|
|
2467
|
-
optimism: 10,
|
|
2468
|
-
polygon: 137,
|
|
2469
|
-
avalanche: 43114
|
|
2470
|
-
};
|
|
2471
|
-
function chainIdForExactNetwork(slug) {
|
|
2472
|
-
return EXACT_NETWORK_SLUGS[slug] ?? null;
|
|
2473
|
-
}
|
|
2474
|
-
var EIP3009_TYPES = {
|
|
2475
|
-
TransferWithAuthorization: [
|
|
2476
|
-
{ name: "from", type: "address" },
|
|
2477
|
-
{ name: "to", type: "address" },
|
|
2478
|
-
{ name: "value", type: "uint256" },
|
|
2479
|
-
{ name: "validAfter", type: "uint256" },
|
|
2480
|
-
{ name: "validBefore", type: "uint256" },
|
|
2481
|
-
{ name: "nonce", type: "bytes32" }
|
|
2482
|
-
]
|
|
2483
|
-
};
|
|
2484
|
-
function parseExactRequirements(body) {
|
|
2485
|
-
if (!body || typeof body !== "object") return null;
|
|
2486
|
-
const accepts = body.accepts;
|
|
2487
|
-
if (!Array.isArray(accepts)) return null;
|
|
2488
|
-
const out = [];
|
|
2489
|
-
for (const raw of accepts) {
|
|
2490
|
-
if (!raw || typeof raw !== "object") continue;
|
|
2491
|
-
const a = raw;
|
|
2492
|
-
if (a.scheme !== "exact") continue;
|
|
2493
|
-
const amount = a.maxAmountRequired ?? a.amount;
|
|
2494
|
-
if (typeof a.network !== "string" || typeof amount !== "string" || typeof a.asset !== "string" || typeof a.payTo !== "string") {
|
|
2495
|
-
continue;
|
|
2496
|
-
}
|
|
2497
|
-
out.push({
|
|
2498
|
-
scheme: "exact",
|
|
2499
|
-
network: a.network,
|
|
2500
|
-
maxAmountRequired: amount,
|
|
2501
|
-
asset: a.asset,
|
|
2502
|
-
payTo: a.payTo,
|
|
2503
|
-
maxTimeoutSeconds: typeof a.maxTimeoutSeconds === "number" ? a.maxTimeoutSeconds : 600,
|
|
2504
|
-
...a.extra && typeof a.extra === "object" ? { extra: a.extra } : {},
|
|
2505
|
-
...typeof a.description === "string" ? { description: a.description } : {},
|
|
2506
|
-
...typeof a.resource === "string" ? { resource: a.resource } : {}
|
|
2507
|
-
});
|
|
2508
|
-
}
|
|
2509
|
-
return out;
|
|
2510
|
-
}
|
|
2511
|
-
async function buildExactAuthorization(params) {
|
|
2512
|
-
const { account, accept, chainId, now, nonce } = params;
|
|
2513
|
-
if (!account.signTypedData) {
|
|
2514
|
-
throw new Error("buildExactAuthorization: the account cannot sign EIP-712 typed data.");
|
|
2515
|
-
}
|
|
2516
|
-
const authorization = {
|
|
2517
|
-
from: account.address,
|
|
2518
|
-
to: accept.payTo,
|
|
2519
|
-
value: accept.maxAmountRequired,
|
|
2520
|
-
validAfter: "0",
|
|
2521
|
-
validBefore: String(now + accept.maxTimeoutSeconds),
|
|
2522
|
-
nonce
|
|
2523
|
-
};
|
|
2524
|
-
const signature = await account.signTypedData({
|
|
2525
|
-
domain: {
|
|
2526
|
-
name: accept.extra?.name ?? "USD Coin",
|
|
2527
|
-
version: accept.extra?.version ?? "2",
|
|
2528
|
-
chainId,
|
|
2529
|
-
verifyingContract: accept.asset
|
|
2530
|
-
},
|
|
2531
|
-
types: EIP3009_TYPES,
|
|
2532
|
-
primaryType: "TransferWithAuthorization",
|
|
2533
|
-
message: {
|
|
2534
|
-
from: authorization.from,
|
|
2535
|
-
to: authorization.to,
|
|
2536
|
-
value: BigInt(authorization.value),
|
|
2537
|
-
validAfter: BigInt(authorization.validAfter),
|
|
2538
|
-
validBefore: BigInt(authorization.validBefore),
|
|
2539
|
-
nonce: authorization.nonce
|
|
2540
|
-
}
|
|
2541
|
-
});
|
|
2542
|
-
return { authorization, signature };
|
|
2543
|
-
}
|
|
2544
|
-
function base64(str) {
|
|
2545
|
-
if (typeof btoa === "function") return btoa(str);
|
|
2546
|
-
if (typeof Buffer !== "undefined") return Buffer.from(str, "utf8").toString("base64");
|
|
2547
|
-
throw new Error("No base64 encoder available in this runtime.");
|
|
2548
|
-
}
|
|
2549
|
-
function encodeXPaymentHeader(input) {
|
|
2550
|
-
const payload = {
|
|
2551
|
-
x402Version: input.x402Version ?? 1,
|
|
2552
|
-
scheme: "exact",
|
|
2553
|
-
network: input.network,
|
|
2554
|
-
payload: { signature: input.signature, authorization: input.authorization }
|
|
2555
|
-
};
|
|
2556
|
-
return base64(JSON.stringify(payload));
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
3107
|
// src/discovery.ts
|
|
2560
3108
|
var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
|
|
2561
3109
|
function pathOf(url) {
|
|
@@ -2616,6 +3164,11 @@ export {
|
|
|
2616
3164
|
EIP3009_TYPES,
|
|
2617
3165
|
EXACT_NETWORK_SLUGS,
|
|
2618
3166
|
GENERATOR,
|
|
3167
|
+
HEADER_REQUIRED,
|
|
3168
|
+
HEADER_RESPONSE,
|
|
3169
|
+
HEADER_RESPONSE_V1,
|
|
3170
|
+
HEADER_SIGNATURE,
|
|
3171
|
+
HEADER_SIGNATURE_V1,
|
|
2619
3172
|
InsufficientFundsError,
|
|
2620
3173
|
InvalidEnvelopeError,
|
|
2621
3174
|
MaxRetriesExceededError,
|
|
@@ -2627,6 +3180,7 @@ export {
|
|
|
2627
3180
|
PipRailClient,
|
|
2628
3181
|
PipRailError,
|
|
2629
3182
|
RecipientNotReadyError,
|
|
3183
|
+
SettlementError,
|
|
2630
3184
|
UnknownTokenError,
|
|
2631
3185
|
UnsupportedNetworkError,
|
|
2632
3186
|
WrongChainError,
|
|
@@ -2640,22 +3194,26 @@ export {
|
|
|
2640
3194
|
buildX402DnsTxt,
|
|
2641
3195
|
chainIdForExactNetwork,
|
|
2642
3196
|
createPaymentGate,
|
|
3197
|
+
eip3009Abi,
|
|
2643
3198
|
encodeXPaymentHeader,
|
|
2644
3199
|
evaluatePolicy,
|
|
2645
3200
|
normalizeNetwork,
|
|
2646
3201
|
parseChallenge,
|
|
3202
|
+
parseExactPaymentHeader,
|
|
2647
3203
|
parseExactRequirements,
|
|
2648
3204
|
parseReceipt,
|
|
2649
3205
|
parseSignatureHeader,
|
|
2650
3206
|
paymentTools,
|
|
2651
3207
|
pickAccept,
|
|
2652
3208
|
planAcross,
|
|
3209
|
+
readExactDomain,
|
|
2653
3210
|
register402Index,
|
|
2654
3211
|
registerDriver,
|
|
2655
3212
|
registerX402Scan,
|
|
2656
3213
|
requirePayment,
|
|
2657
3214
|
resolveChain,
|
|
2658
3215
|
searchOpenIndexes,
|
|
3216
|
+
settleViaFacilitator,
|
|
2659
3217
|
toInsufficientFundsError,
|
|
2660
3218
|
toInvalidBody
|
|
2661
3219
|
};
|