@piprail/sdk 1.15.1 → 1.17.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 +84 -0
- package/dist/index.cjs +689 -112
- package/dist/index.d.cts +325 -39
- package/dist/index.d.ts +325 -39
- package/dist/index.js +636 -59
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ function resolveNetwork(opts) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// src/drivers/evm/index.ts
|
|
68
|
-
import { BaseError, createPublicClient, erc20Abi as
|
|
68
|
+
import { BaseError, createPublicClient, erc20Abi as erc20Abi4, getAddress as getAddress4, http as http2, isAddress } from "viem";
|
|
69
69
|
|
|
70
70
|
// src/drivers/evm/chains.ts
|
|
71
71
|
import { defineChain } from "viem";
|
|
@@ -136,9 +136,15 @@ var CHAINS = {
|
|
|
136
136
|
bnb: {
|
|
137
137
|
chain: bsc,
|
|
138
138
|
tokens: {
|
|
139
|
-
// Binance-Peg tokens on BNB Chain are 18 decimals (not the usual 6).
|
|
139
|
+
// Binance-Peg tokens on BNB Chain are 18 decimals (not the usual 6). USDC/USDT here are
|
|
140
|
+
// Binance-Peg (NOT EIP-3009) → the `exact` rail uses Permit2.
|
|
140
141
|
USDC: { address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", decimals: 18, symbol: "USDC" },
|
|
141
|
-
USDT: { address: "0x55d398326f99059fF775485246999027B3197955", decimals: 18, symbol: "USDT" }
|
|
142
|
+
USDT: { address: "0x55d398326f99059fF775485246999027B3197955", decimals: 18, symbol: "USDT" },
|
|
143
|
+
// FDUSD + USD1 ARE EIP-3009 (transferWithAuthorization) → the `exact` rail uses the gasless,
|
|
144
|
+
// no-Permit2-approve path. Both hardcode EIP-712 domain version "1" (no version() — the SDK
|
|
145
|
+
// derives it from DOMAIN_SEPARATOR). Verified on-chain (symbol/decimals/domain match).
|
|
146
|
+
FDUSD: { address: "0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", decimals: 18, symbol: "FDUSD" },
|
|
147
|
+
USD1: { address: "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d", decimals: 18, symbol: "USD1" }
|
|
142
148
|
}
|
|
143
149
|
},
|
|
144
150
|
avalanche: {
|
|
@@ -494,9 +500,12 @@ function sumTransfersTo(logs, asset, payTo) {
|
|
|
494
500
|
|
|
495
501
|
// src/drivers/evm/exact.ts
|
|
496
502
|
import {
|
|
503
|
+
encodeAbiParameters,
|
|
497
504
|
getAddress as getAddress2,
|
|
505
|
+
keccak256,
|
|
498
506
|
parseSignature,
|
|
499
|
-
recoverTypedDataAddress
|
|
507
|
+
recoverTypedDataAddress,
|
|
508
|
+
toHex
|
|
500
509
|
} from "viem";
|
|
501
510
|
var EXACT_NETWORK_SLUGS = {
|
|
502
511
|
ethereum: 1,
|
|
@@ -505,7 +514,9 @@ var EXACT_NETWORK_SLUGS = {
|
|
|
505
514
|
arbitrum: 42161,
|
|
506
515
|
optimism: 10,
|
|
507
516
|
polygon: 137,
|
|
508
|
-
avalanche: 43114
|
|
517
|
+
avalanche: 43114,
|
|
518
|
+
bnb: 56,
|
|
519
|
+
bsc: 56
|
|
509
520
|
};
|
|
510
521
|
function chainIdForExactNetwork(slug) {
|
|
511
522
|
return EXACT_NETWORK_SLUGS[slug] ?? null;
|
|
@@ -690,10 +701,25 @@ var eip3009Abi = [
|
|
|
690
701
|
outputs: [{ type: "bool" }]
|
|
691
702
|
},
|
|
692
703
|
{ type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
|
|
693
|
-
{ type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] }
|
|
704
|
+
{ type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
|
|
705
|
+
// EIP-3009 tokens that DON'T expose version() still expose DOMAIN_SEPARATOR — we match it to
|
|
706
|
+
// DERIVE the hardcoded domain version (e.g. FDUSD / USD1 on BNB Chain both use "1").
|
|
707
|
+
{ type: "function", name: "DOMAIN_SEPARATOR", stateMutability: "view", inputs: [], outputs: [{ type: "bytes32" }] }
|
|
694
708
|
];
|
|
695
709
|
var ZERO_ADDR = "0x0000000000000000000000000000000000000000";
|
|
696
710
|
var ZERO_NONCE = `0x${"00".repeat(32)}`;
|
|
711
|
+
var EIP712_DOMAIN_TYPEHASH = keccak256(
|
|
712
|
+
toHex("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
|
|
713
|
+
);
|
|
714
|
+
var EXACT_DOMAIN_VERSION_CANDIDATES = ["1", "2"];
|
|
715
|
+
function eip712DomainSeparator(name, version, chainId, verifyingContract) {
|
|
716
|
+
return keccak256(
|
|
717
|
+
encodeAbiParameters(
|
|
718
|
+
[{ type: "bytes32" }, { type: "bytes32" }, { type: "bytes32" }, { type: "uint256" }, { type: "address" }],
|
|
719
|
+
[EIP712_DOMAIN_TYPEHASH, keccak256(toHex(name)), keccak256(toHex(version)), BigInt(chainId), verifyingContract]
|
|
720
|
+
)
|
|
721
|
+
);
|
|
722
|
+
}
|
|
697
723
|
async function readExactDomain(publicClient, asset) {
|
|
698
724
|
if (asset === "native") return null;
|
|
699
725
|
let token;
|
|
@@ -702,12 +728,10 @@ async function readExactDomain(publicClient, asset) {
|
|
|
702
728
|
} catch {
|
|
703
729
|
return null;
|
|
704
730
|
}
|
|
731
|
+
let name;
|
|
705
732
|
try {
|
|
706
|
-
const [
|
|
733
|
+
const [n] = await Promise.all([
|
|
707
734
|
publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "name" }),
|
|
708
|
-
publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "version" }),
|
|
709
|
-
// The EIP-3009 probe: this view exists only on EIP-3009 tokens; it reverts on
|
|
710
|
-
// a plain ERC-20 / USDT, marking the token as not exact-payable.
|
|
711
735
|
publicClient.readContract({
|
|
712
736
|
address: token,
|
|
713
737
|
abi: eip3009Abi,
|
|
@@ -715,11 +739,34 @@ async function readExactDomain(publicClient, asset) {
|
|
|
715
739
|
args: [ZERO_ADDR, ZERO_NONCE]
|
|
716
740
|
})
|
|
717
741
|
]);
|
|
718
|
-
if (typeof
|
|
719
|
-
|
|
742
|
+
if (typeof n !== "string" || !n) return null;
|
|
743
|
+
name = n;
|
|
720
744
|
} catch {
|
|
721
745
|
return null;
|
|
722
746
|
}
|
|
747
|
+
try {
|
|
748
|
+
const version = await publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "version" });
|
|
749
|
+
if (typeof version === "string" && version) return { name, version };
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
return deriveExactDomainVersion(publicClient, token, name);
|
|
753
|
+
}
|
|
754
|
+
async function deriveExactDomainVersion(publicClient, token, name) {
|
|
755
|
+
let onchain;
|
|
756
|
+
let chainId;
|
|
757
|
+
try {
|
|
758
|
+
onchain = await publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "DOMAIN_SEPARATOR" });
|
|
759
|
+
chainId = publicClient.chain?.id ?? await publicClient.getChainId();
|
|
760
|
+
} catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
const target = onchain.toLowerCase();
|
|
764
|
+
for (const version of EXACT_DOMAIN_VERSION_CANDIDATES) {
|
|
765
|
+
if (eip712DomainSeparator(name, version, chainId, token).toLowerCase() === target) {
|
|
766
|
+
return { name, version };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return null;
|
|
723
770
|
}
|
|
724
771
|
function shorten(msg) {
|
|
725
772
|
const oneLine = msg.replace(/\s+/g, " ").trim();
|
|
@@ -874,6 +921,346 @@ async function verifyAndSettleExactEvm(input) {
|
|
|
874
921
|
};
|
|
875
922
|
}
|
|
876
923
|
|
|
924
|
+
// src/drivers/evm/permit2.ts
|
|
925
|
+
import {
|
|
926
|
+
erc20Abi as erc20Abi3,
|
|
927
|
+
getAddress as getAddress3,
|
|
928
|
+
maxUint256,
|
|
929
|
+
recoverTypedDataAddress as recoverTypedDataAddress2
|
|
930
|
+
} from "viem";
|
|
931
|
+
var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
932
|
+
var X402_EXACT_PERMIT2_PROXY = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";
|
|
933
|
+
var PERMIT2_WITNESS_TYPES = {
|
|
934
|
+
PermitWitnessTransferFrom: [
|
|
935
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
936
|
+
{ name: "spender", type: "address" },
|
|
937
|
+
{ name: "nonce", type: "uint256" },
|
|
938
|
+
{ name: "deadline", type: "uint256" },
|
|
939
|
+
{ name: "witness", type: "Witness" }
|
|
940
|
+
],
|
|
941
|
+
TokenPermissions: [
|
|
942
|
+
{ name: "token", type: "address" },
|
|
943
|
+
{ name: "amount", type: "uint256" }
|
|
944
|
+
],
|
|
945
|
+
Witness: [
|
|
946
|
+
{ name: "to", type: "address" },
|
|
947
|
+
{ name: "validAfter", type: "uint256" }
|
|
948
|
+
]
|
|
949
|
+
};
|
|
950
|
+
var x402Permit2ProxyAbi = [
|
|
951
|
+
{
|
|
952
|
+
type: "function",
|
|
953
|
+
name: "settle",
|
|
954
|
+
stateMutability: "nonpayable",
|
|
955
|
+
outputs: [],
|
|
956
|
+
inputs: [
|
|
957
|
+
{
|
|
958
|
+
name: "permit",
|
|
959
|
+
type: "tuple",
|
|
960
|
+
components: [
|
|
961
|
+
{
|
|
962
|
+
name: "permitted",
|
|
963
|
+
type: "tuple",
|
|
964
|
+
components: [
|
|
965
|
+
{ name: "token", type: "address" },
|
|
966
|
+
{ name: "amount", type: "uint256" }
|
|
967
|
+
]
|
|
968
|
+
},
|
|
969
|
+
{ name: "nonce", type: "uint256" },
|
|
970
|
+
{ name: "deadline", type: "uint256" }
|
|
971
|
+
]
|
|
972
|
+
},
|
|
973
|
+
{ name: "owner", type: "address" },
|
|
974
|
+
{
|
|
975
|
+
name: "witness",
|
|
976
|
+
type: "tuple",
|
|
977
|
+
components: [
|
|
978
|
+
{ name: "to", type: "address" },
|
|
979
|
+
{ name: "validAfter", type: "uint256" }
|
|
980
|
+
]
|
|
981
|
+
},
|
|
982
|
+
{ name: "signature", type: "bytes" }
|
|
983
|
+
]
|
|
984
|
+
}
|
|
985
|
+
];
|
|
986
|
+
var permit2NonceBitmapAbi = [
|
|
987
|
+
{
|
|
988
|
+
type: "function",
|
|
989
|
+
name: "nonceBitmap",
|
|
990
|
+
stateMutability: "view",
|
|
991
|
+
inputs: [
|
|
992
|
+
{ name: "owner", type: "address" },
|
|
993
|
+
{ name: "word", type: "uint256" }
|
|
994
|
+
],
|
|
995
|
+
outputs: [{ type: "uint256" }]
|
|
996
|
+
}
|
|
997
|
+
];
|
|
998
|
+
function shorten2(msg) {
|
|
999
|
+
const oneLine = msg.replace(/\s+/g, " ").trim();
|
|
1000
|
+
return oneLine.length > 200 ? `${oneLine.slice(0, 200)}\u2026` : oneLine;
|
|
1001
|
+
}
|
|
1002
|
+
function randomPermit2Nonce() {
|
|
1003
|
+
const g = globalThis.crypto;
|
|
1004
|
+
if (!g?.getRandomValues) {
|
|
1005
|
+
throw new UnsupportedSchemeError(
|
|
1006
|
+
"this runtime lacks Web Crypto (globalThis.crypto.getRandomValues); the permit2 rail needs a CSPRNG nonce."
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
const raw = new Uint8Array(32);
|
|
1010
|
+
g.getRandomValues(raw);
|
|
1011
|
+
return BigInt(`0x${[...raw].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
|
|
1012
|
+
}
|
|
1013
|
+
async function ensurePermit2Allowance(input) {
|
|
1014
|
+
const { publicClient, walletClient, account, chain, token, amount } = input;
|
|
1015
|
+
let allowance;
|
|
1016
|
+
try {
|
|
1017
|
+
allowance = await publicClient.readContract({
|
|
1018
|
+
address: token,
|
|
1019
|
+
abi: erc20Abi3,
|
|
1020
|
+
functionName: "allowance",
|
|
1021
|
+
args: [account.address, PERMIT2_ADDRESS]
|
|
1022
|
+
});
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
throw new UnsupportedSchemeError(
|
|
1025
|
+
`permit2: couldn't read the Permit2 allowance for ${token} (${shorten2(err instanceof Error ? err.message : String(err))}).`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
if (allowance >= amount) return void 0;
|
|
1029
|
+
try {
|
|
1030
|
+
const hash = await walletClient.writeContract({
|
|
1031
|
+
account,
|
|
1032
|
+
chain,
|
|
1033
|
+
address: token,
|
|
1034
|
+
abi: erc20Abi3,
|
|
1035
|
+
functionName: "approve",
|
|
1036
|
+
args: [PERMIT2_ADDRESS, maxUint256]
|
|
1037
|
+
});
|
|
1038
|
+
await publicClient.waitForTransactionReceipt({ hash, confirmations: 1 });
|
|
1039
|
+
return hash;
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
throw toInsufficientFundsError(err) ?? new SettlementError(
|
|
1042
|
+
`permit2: the one-time Permit2 approval for ${token} failed to broadcast (${shorten2(err instanceof Error ? err.message : String(err))}).`,
|
|
1043
|
+
{ cause: err }
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
async function payPermit2Evm(input) {
|
|
1048
|
+
const { publicClient, walletClient, account, chainId, chain, accept } = input;
|
|
1049
|
+
let code;
|
|
1050
|
+
try {
|
|
1051
|
+
code = await publicClient.getCode({ address: account.address });
|
|
1052
|
+
} catch {
|
|
1053
|
+
code = void 0;
|
|
1054
|
+
}
|
|
1055
|
+
if (code && code !== "0x") {
|
|
1056
|
+
throw new UnsupportedSchemeError(
|
|
1057
|
+
`permit2 buyer rail requires an EOA signer; ${account.address} is a contract / EIP-1271 / EIP-7702-delegated account. Pay via onchain-proof.`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
const token = getAddress3(accept.asset);
|
|
1061
|
+
const payTo = getAddress3(accept.payTo);
|
|
1062
|
+
const value = BigInt(accept.amount);
|
|
1063
|
+
const approvalTx = await ensurePermit2Allowance({
|
|
1064
|
+
publicClient,
|
|
1065
|
+
walletClient,
|
|
1066
|
+
account,
|
|
1067
|
+
chain,
|
|
1068
|
+
token,
|
|
1069
|
+
amount: value
|
|
1070
|
+
});
|
|
1071
|
+
const nonce = randomPermit2Nonce();
|
|
1072
|
+
const deadline = BigInt(Math.floor(Date.now() / 1e3) + accept.maxTimeoutSeconds);
|
|
1073
|
+
const validAfter = 0n;
|
|
1074
|
+
const spender = getAddress3(X402_EXACT_PERMIT2_PROXY);
|
|
1075
|
+
const from = account.address;
|
|
1076
|
+
const signature = await walletClient.signTypedData({
|
|
1077
|
+
account,
|
|
1078
|
+
domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS },
|
|
1079
|
+
types: PERMIT2_WITNESS_TYPES,
|
|
1080
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
1081
|
+
message: {
|
|
1082
|
+
permitted: { token, amount: value },
|
|
1083
|
+
spender,
|
|
1084
|
+
nonce,
|
|
1085
|
+
deadline,
|
|
1086
|
+
witness: { to: payTo, validAfter }
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
const permit2Authorization = {
|
|
1090
|
+
permitted: { token, amount: value.toString() },
|
|
1091
|
+
from,
|
|
1092
|
+
spender,
|
|
1093
|
+
nonce: nonce.toString(),
|
|
1094
|
+
deadline: deadline.toString(),
|
|
1095
|
+
witness: { to: payTo, validAfter: validAfter.toString() }
|
|
1096
|
+
};
|
|
1097
|
+
return {
|
|
1098
|
+
payload: { signature, permit2Authorization },
|
|
1099
|
+
payerFrom: from,
|
|
1100
|
+
nonce: nonce.toString(),
|
|
1101
|
+
...approvalTx ? { approvalTx } : {}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
async function verifyAndSettlePermit2Evm(input) {
|
|
1105
|
+
const { publicClient, walletClient, account, chain, payload, accept } = input;
|
|
1106
|
+
const token = getAddress3(accept.asset);
|
|
1107
|
+
const payTo = getAddress3(accept.payTo);
|
|
1108
|
+
const requiredAmount = BigInt(accept.amount);
|
|
1109
|
+
const proxy = getAddress3(X402_EXACT_PERMIT2_PROXY);
|
|
1110
|
+
let from;
|
|
1111
|
+
let spender;
|
|
1112
|
+
let permittedToken;
|
|
1113
|
+
let witnessTo;
|
|
1114
|
+
let permittedAmount;
|
|
1115
|
+
let nonce;
|
|
1116
|
+
let deadline;
|
|
1117
|
+
let validAfter;
|
|
1118
|
+
const signature = payload.signature;
|
|
1119
|
+
try {
|
|
1120
|
+
const pa = payload.permit2Authorization;
|
|
1121
|
+
from = getAddress3(pa.from);
|
|
1122
|
+
spender = getAddress3(pa.spender);
|
|
1123
|
+
permittedToken = getAddress3(pa.permitted.token);
|
|
1124
|
+
witnessTo = getAddress3(pa.witness.to);
|
|
1125
|
+
permittedAmount = BigInt(pa.permitted.amount);
|
|
1126
|
+
nonce = BigInt(pa.nonce);
|
|
1127
|
+
deadline = BigInt(pa.deadline);
|
|
1128
|
+
validAfter = BigInt(pa.witness.validAfter);
|
|
1129
|
+
if (!/^0x[0-9a-fA-F]+$/.test(signature)) throw new Error("signature must be hex");
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
return {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
error: "signature_invalid",
|
|
1134
|
+
detail: `Malformed permit2 authorization: ${err instanceof Error ? err.message : String(err)}.`
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
if (witnessTo !== payTo) {
|
|
1138
|
+
return { ok: false, error: "wrong_recipient", detail: `Authorization pays witness.to ${witnessTo}, not ${payTo}.` };
|
|
1139
|
+
}
|
|
1140
|
+
if (permittedToken !== token) {
|
|
1141
|
+
return { ok: false, error: "signature_invalid", detail: `Authorization permits token ${permittedToken}, not the rail's ${token}.` };
|
|
1142
|
+
}
|
|
1143
|
+
if (spender !== proxy) {
|
|
1144
|
+
return { ok: false, error: "signature_invalid", detail: `Authorization spender ${spender} is not the x402ExactPermit2Proxy ${proxy}; it can't be settled here.` };
|
|
1145
|
+
}
|
|
1146
|
+
if (permittedAmount < requiredAmount) {
|
|
1147
|
+
return { ok: false, error: "amount_too_low", detail: `Permitted ${permittedAmount}, required ${requiredAmount}.` };
|
|
1148
|
+
}
|
|
1149
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
1150
|
+
if (deadline <= now) {
|
|
1151
|
+
return { ok: false, error: "payment_expired", detail: `Permit2 deadline ${deadline} <= now ${now}.` };
|
|
1152
|
+
}
|
|
1153
|
+
let fromCode;
|
|
1154
|
+
try {
|
|
1155
|
+
fromCode = await publicClient.getCode({ address: from });
|
|
1156
|
+
} catch {
|
|
1157
|
+
return { ok: false, error: "tx_not_found", detail: `Could not read code at ${from} (transient RPC) \u2014 retry.` };
|
|
1158
|
+
}
|
|
1159
|
+
if (!(fromCode && fromCode !== "0x")) {
|
|
1160
|
+
let recovered;
|
|
1161
|
+
try {
|
|
1162
|
+
recovered = await recoverTypedDataAddress2({
|
|
1163
|
+
domain: { name: "Permit2", chainId: chain.id, verifyingContract: PERMIT2_ADDRESS },
|
|
1164
|
+
types: PERMIT2_WITNESS_TYPES,
|
|
1165
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
1166
|
+
message: {
|
|
1167
|
+
permitted: { token: permittedToken, amount: permittedAmount },
|
|
1168
|
+
spender,
|
|
1169
|
+
nonce,
|
|
1170
|
+
deadline,
|
|
1171
|
+
witness: { to: witnessTo, validAfter }
|
|
1172
|
+
},
|
|
1173
|
+
signature
|
|
1174
|
+
});
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
return { ok: false, error: "signature_invalid", detail: `Not a valid EIP-712 signature: ${shorten2(err instanceof Error ? err.message : String(err))}.` };
|
|
1177
|
+
}
|
|
1178
|
+
if (recovered !== from) {
|
|
1179
|
+
return { ok: false, error: "signature_invalid", detail: `Signature recovered to ${recovered}, not the authorizer ${from}.` };
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const word = nonce >> 8n;
|
|
1184
|
+
const bit = nonce & 0xffn;
|
|
1185
|
+
const bitmap = await publicClient.readContract({
|
|
1186
|
+
address: PERMIT2_ADDRESS,
|
|
1187
|
+
abi: permit2NonceBitmapAbi,
|
|
1188
|
+
functionName: "nonceBitmap",
|
|
1189
|
+
args: [from, word]
|
|
1190
|
+
});
|
|
1191
|
+
if ((bitmap >> bit & 1n) === 1n) {
|
|
1192
|
+
return { ok: false, error: "tx_already_used", detail: `Permit2 nonce ${nonce} already used or invalidated for ${from}.` };
|
|
1193
|
+
}
|
|
1194
|
+
} catch {
|
|
1195
|
+
return { ok: false, error: "tx_not_found", detail: "Could not read the Permit2 nonce bitmap (transient RPC) \u2014 retry." };
|
|
1196
|
+
}
|
|
1197
|
+
const settleArgs = [
|
|
1198
|
+
{ permitted: { token: permittedToken, amount: permittedAmount }, nonce, deadline },
|
|
1199
|
+
from,
|
|
1200
|
+
{ to: witnessTo, validAfter },
|
|
1201
|
+
signature
|
|
1202
|
+
];
|
|
1203
|
+
try {
|
|
1204
|
+
await publicClient.simulateContract({
|
|
1205
|
+
account,
|
|
1206
|
+
address: proxy,
|
|
1207
|
+
abi: x402Permit2ProxyAbi,
|
|
1208
|
+
functionName: "settle",
|
|
1209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1210
|
+
args: settleArgs
|
|
1211
|
+
});
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1214
|
+
if (/nonce|invalidated|used/i.test(msg)) return { ok: false, error: "tx_already_used", detail: "Permit2 nonce is used or invalidated." };
|
|
1215
|
+
if (/expired|deadline|too early|not yet/i.test(msg)) return { ok: false, error: "payment_expired", detail: shorten2(msg) };
|
|
1216
|
+
if (/signature/i.test(msg)) return { ok: false, error: "signature_invalid", detail: shorten2(msg) };
|
|
1217
|
+
return { ok: false, error: "tx_reverted", detail: `permit2 settle would revert: ${shorten2(msg)}` };
|
|
1218
|
+
}
|
|
1219
|
+
let txHash;
|
|
1220
|
+
try {
|
|
1221
|
+
txHash = await walletClient.writeContract({
|
|
1222
|
+
account,
|
|
1223
|
+
chain,
|
|
1224
|
+
address: proxy,
|
|
1225
|
+
abi: x402Permit2ProxyAbi,
|
|
1226
|
+
functionName: "settle",
|
|
1227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1228
|
+
args: settleArgs
|
|
1229
|
+
});
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
throw new SettlementError(
|
|
1232
|
+
`permit2 settle: the merchant relayer failed to broadcast the proxy settle (${shorten2(err instanceof Error ? err.message : String(err))}). The payer's signature is still valid and its nonce unused \u2014 fund/fix the relayer and the payer can retry.`,
|
|
1233
|
+
{ cause: err }
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
try {
|
|
1237
|
+
const confirmations = accept.extra.minConfirmations ?? 1;
|
|
1238
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations });
|
|
1239
|
+
if (receipt.status !== "success") {
|
|
1240
|
+
return { ok: false, error: "tx_reverted", detail: `Settlement tx ${txHash} reverted on-chain.` };
|
|
1241
|
+
}
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
throw new SettlementError(
|
|
1244
|
+
`permit2 settle: broadcast ${txHash} but couldn't confirm it (${shorten2(err instanceof Error ? err.message : String(err))}).`,
|
|
1245
|
+
{ cause: err }
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
return {
|
|
1249
|
+
ok: true,
|
|
1250
|
+
receipt: {
|
|
1251
|
+
scheme: "exact",
|
|
1252
|
+
success: true,
|
|
1253
|
+
network: accept.network,
|
|
1254
|
+
transaction: txHash,
|
|
1255
|
+
asset: accept.asset,
|
|
1256
|
+
amount: accept.amount,
|
|
1257
|
+
payer: from,
|
|
1258
|
+
payTo: accept.payTo,
|
|
1259
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
877
1264
|
// src/x402.ts
|
|
878
1265
|
var HEADER_REQUIRED = "payment-required";
|
|
879
1266
|
var HEADER_SIGNATURE = "payment-signature";
|
|
@@ -988,23 +1375,38 @@ function parseExactPaymentHeader(value) {
|
|
|
988
1375
|
const payload = v.payload;
|
|
989
1376
|
if (!payload || typeof payload !== "object") return null;
|
|
990
1377
|
const signature = payload.signature;
|
|
991
|
-
|
|
992
|
-
if (typeof signature !== "string" || !authorization || typeof authorization !== "object") return null;
|
|
993
|
-
for (const k of ["from", "to", "value", "validAfter", "validBefore", "nonce"]) {
|
|
994
|
-
if (typeof authorization[k] !== "string") return null;
|
|
995
|
-
}
|
|
1378
|
+
if (typeof signature !== "string") return null;
|
|
996
1379
|
const x402Version = typeof v.x402Version === "number" ? v.x402Version : 2;
|
|
997
1380
|
const asset = accepted && typeof accepted.asset === "string" ? accepted.asset : void 0;
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1381
|
+
const base2 = { x402Version, network, ...asset ? { asset } : {}, raw: v };
|
|
1382
|
+
const authorization = payload.authorization;
|
|
1383
|
+
if (authorization && typeof authorization === "object") {
|
|
1384
|
+
for (const k of ["from", "to", "value", "validAfter", "validBefore", "nonce"]) {
|
|
1385
|
+
if (typeof authorization[k] !== "string") return null;
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
...base2,
|
|
1389
|
+
method: "eip3009",
|
|
1390
|
+
payload: { signature, authorization }
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const p2 = payload.permit2Authorization;
|
|
1394
|
+
if (p2 && typeof p2 === "object") {
|
|
1395
|
+
const permitted = p2.permitted;
|
|
1396
|
+
const witness = p2.witness;
|
|
1397
|
+
if (!permitted || typeof permitted !== "object" || !witness || typeof witness !== "object") return null;
|
|
1398
|
+
if (typeof permitted.token !== "string" || typeof permitted.amount !== "string") return null;
|
|
1399
|
+
if (typeof witness.to !== "string" || typeof witness.validAfter !== "string") return null;
|
|
1400
|
+
for (const k of ["from", "spender", "nonce", "deadline"]) {
|
|
1401
|
+
if (typeof p2[k] !== "string") return null;
|
|
1402
|
+
}
|
|
1403
|
+
return {
|
|
1404
|
+
...base2,
|
|
1405
|
+
method: "permit2",
|
|
1406
|
+
payload: { signature, permit2Authorization: p2 }
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
return null;
|
|
1008
1410
|
}
|
|
1009
1411
|
function isValidChallenge(value) {
|
|
1010
1412
|
if (!value || typeof value !== "object") return false;
|
|
@@ -1095,12 +1497,12 @@ function makeEvmNetwork(resolved) {
|
|
|
1095
1497
|
}
|
|
1096
1498
|
let normalized;
|
|
1097
1499
|
try {
|
|
1098
|
-
normalized =
|
|
1500
|
+
normalized = getAddress4(asset);
|
|
1099
1501
|
} catch {
|
|
1100
1502
|
return null;
|
|
1101
1503
|
}
|
|
1102
1504
|
for (const info of Object.values(resolved.tokens)) {
|
|
1103
|
-
if (
|
|
1505
|
+
if (getAddress4(info.address) === normalized) {
|
|
1104
1506
|
return { symbol: info.symbol, decimals: info.decimals };
|
|
1105
1507
|
}
|
|
1106
1508
|
}
|
|
@@ -1157,12 +1559,13 @@ function makeEvmNetwork(resolved) {
|
|
|
1157
1559
|
async estimateCost(accept) {
|
|
1158
1560
|
const { decimals, symbol } = resolved.chain.nativeCurrency;
|
|
1159
1561
|
if (accept.scheme === "exact") {
|
|
1562
|
+
const permit2 = accept.extra.assetTransferMethod === "permit2";
|
|
1160
1563
|
return nativeCost({
|
|
1161
1564
|
symbol,
|
|
1162
1565
|
decimals,
|
|
1163
1566
|
fee: 0n,
|
|
1164
1567
|
basis: "estimated",
|
|
1165
|
-
detail: "gasless \u2014 the server/facilitator settles the signed authorization"
|
|
1568
|
+
detail: permit2 ? "gasless after a one-time Permit2 approval; the server/facilitator settles the signed authorization" : "gasless \u2014 the server/facilitator settles the signed authorization"
|
|
1166
1569
|
});
|
|
1167
1570
|
}
|
|
1168
1571
|
const gasLimit = accept.asset === "native" ? 21000n : 65000n;
|
|
@@ -1193,8 +1596,8 @@ function makeEvmNetwork(resolved) {
|
|
|
1193
1596
|
let token = null;
|
|
1194
1597
|
try {
|
|
1195
1598
|
token = await publicClient.readContract({
|
|
1196
|
-
address:
|
|
1197
|
-
abi:
|
|
1599
|
+
address: getAddress4(asset),
|
|
1600
|
+
abi: erc20Abi4,
|
|
1198
1601
|
functionName: "balanceOf",
|
|
1199
1602
|
args: [owner]
|
|
1200
1603
|
});
|
|
@@ -1226,12 +1629,24 @@ function makeEvmNetwork(resolved) {
|
|
|
1226
1629
|
minConfirmations: accept.extra.minConfirmations
|
|
1227
1630
|
});
|
|
1228
1631
|
},
|
|
1229
|
-
// Standard x402 `exact` rail
|
|
1230
|
-
//
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1632
|
+
// Standard x402 `exact` rail, BUYER side — EVM only. Routes on the rail's
|
|
1633
|
+
// `assetTransferMethod`: `permit2` (any ERC-20 — e.g. Binance-Peg USDC on BNB, signs a
|
|
1634
|
+
// Permit2 witness transfer + lazily does the one-time approval) or `eip3009` (re-derives
|
|
1635
|
+
// the token's EIP-712 domain on-chain + signs transferWithAuthorization). Never broadcasts.
|
|
1636
|
+
// Throws UnsupportedSchemeError for a contract signer (or a non-EIP-3009 token on the eip3009 path).
|
|
1233
1637
|
async payExact(wallet, accept) {
|
|
1234
1638
|
const a = wallet._native;
|
|
1639
|
+
if (accept.extra.assetTransferMethod === "permit2") {
|
|
1640
|
+
const { payload: payload2, payerFrom: payerFrom2, nonce: nonce2 } = await payPermit2Evm({
|
|
1641
|
+
publicClient,
|
|
1642
|
+
walletClient: a.walletClient,
|
|
1643
|
+
account: a.account,
|
|
1644
|
+
chainId: resolved.chainId,
|
|
1645
|
+
chain: resolved.chain,
|
|
1646
|
+
accept
|
|
1647
|
+
});
|
|
1648
|
+
return { payload: payload2, accepted: accept, payerFrom: payerFrom2, nonce: nonce2 };
|
|
1649
|
+
}
|
|
1235
1650
|
const { payload, payerFrom, nonce } = await payExactEvm({
|
|
1236
1651
|
publicClient,
|
|
1237
1652
|
walletClient: a.walletClient,
|
|
@@ -1247,6 +1662,16 @@ function makeEvmNetwork(resolved) {
|
|
|
1247
1662
|
},
|
|
1248
1663
|
async settleExactSelf({ relayer, payload, accept }) {
|
|
1249
1664
|
const a = relayer._native;
|
|
1665
|
+
if ("permit2Authorization" in payload) {
|
|
1666
|
+
return verifyAndSettlePermit2Evm({
|
|
1667
|
+
publicClient,
|
|
1668
|
+
walletClient: a.walletClient,
|
|
1669
|
+
account: a.account,
|
|
1670
|
+
chain: resolved.chain,
|
|
1671
|
+
payload,
|
|
1672
|
+
accept
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1250
1675
|
return verifyAndSettleExactEvm({
|
|
1251
1676
|
publicClient,
|
|
1252
1677
|
walletClient: a.walletClient,
|
|
@@ -3670,7 +4095,7 @@ function createPaymentGate(options) {
|
|
|
3670
4095
|
);
|
|
3671
4096
|
if (options.exact && !specs.some((s) => s.exact)) {
|
|
3672
4097
|
throw new Error(
|
|
3673
|
-
"requirePayment: `exact` was requested but none of the offered rails support it. The standard `exact` rail is EVM
|
|
4098
|
+
"requirePayment: `exact` was requested but none of the offered rails support it. The standard `exact` rail is EVM ERC-20 only \u2014 EIP-3009 (USDC / EURC) or Permit2 (any ERC-20, e.g. Binance-Peg USDC on BNB) \u2014 NOT native coins, NOT non-EVM chains. Offer an EVM ERC-20 token, or drop `exact`."
|
|
3674
4099
|
);
|
|
3675
4100
|
}
|
|
3676
4101
|
return specs;
|
|
@@ -3683,26 +4108,39 @@ function createPaymentGate(options) {
|
|
|
3683
4108
|
}
|
|
3684
4109
|
async function resolveExactRail(net, asset) {
|
|
3685
4110
|
const cfg = options.exact;
|
|
3686
|
-
if (net.family !== "evm" || asset === "native" || !net.
|
|
4111
|
+
if (net.family !== "evm" || asset === "native" || !net.settleExactSelf) {
|
|
3687
4112
|
return void 0;
|
|
3688
4113
|
}
|
|
3689
|
-
const
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
4114
|
+
const want = cfg.method ?? "auto";
|
|
4115
|
+
let method;
|
|
4116
|
+
let domain;
|
|
4117
|
+
if (want === "permit2") {
|
|
4118
|
+
method = "permit2";
|
|
4119
|
+
} else {
|
|
4120
|
+
const d = net.exactDomain ? await net.exactDomain(asset) : null;
|
|
4121
|
+
if (d) {
|
|
4122
|
+
method = "eip3009";
|
|
4123
|
+
domain = d;
|
|
4124
|
+
} else if (want === "eip3009") {
|
|
4125
|
+
throw new Error(
|
|
4126
|
+
`requirePayment: exact \`method: 'eip3009'\` requested for ${asset} on ${net.network}, but it isn't an EIP-3009 token (no name()/version()/authorizationState). Use \`method: 'permit2'\` (any ERC-20, e.g. Binance-Peg USDC on BNB) or \`'auto'\`. (Or check your rpcUrl is reachable.)`
|
|
4127
|
+
);
|
|
4128
|
+
} else {
|
|
4129
|
+
method = "permit2";
|
|
4130
|
+
}
|
|
3694
4131
|
}
|
|
3695
4132
|
if (cfg.settle === "self") {
|
|
3696
4133
|
if (cfg.relayer === void 0) {
|
|
3697
4134
|
throw new Error(
|
|
3698
|
-
"requirePayment: exact `settle: 'self'` needs a `relayer` wallet (the gas-paying key that broadcasts
|
|
4135
|
+
"requirePayment: exact `settle: 'self'` needs a `relayer` wallet (the gas-paying key that broadcasts the settle), e.g. exact: { settle: 'self', relayer: { privateKey } }."
|
|
3699
4136
|
);
|
|
3700
4137
|
}
|
|
3701
4138
|
const relayer = net.bindWallet(cfg.relayer);
|
|
3702
|
-
return { domain, mode: { kind: "self", relayer } };
|
|
4139
|
+
return { method, ...domain ? { domain } : {}, mode: { kind: "self", relayer } };
|
|
3703
4140
|
}
|
|
3704
4141
|
return {
|
|
3705
|
-
|
|
4142
|
+
method,
|
|
4143
|
+
...domain ? { domain } : {},
|
|
3706
4144
|
mode: {
|
|
3707
4145
|
kind: "facilitator",
|
|
3708
4146
|
url: cfg.settle.facilitator,
|
|
@@ -3746,7 +4184,7 @@ function createPaymentGate(options) {
|
|
|
3746
4184
|
};
|
|
3747
4185
|
}
|
|
3748
4186
|
function buildExactAccept(s) {
|
|
3749
|
-
const
|
|
4187
|
+
const rail = s.exact;
|
|
3750
4188
|
return {
|
|
3751
4189
|
scheme: "exact",
|
|
3752
4190
|
network: s.net.network,
|
|
@@ -3755,9 +4193,8 @@ function createPaymentGate(options) {
|
|
|
3755
4193
|
payTo: s.payTo,
|
|
3756
4194
|
maxTimeoutSeconds,
|
|
3757
4195
|
extra: {
|
|
3758
|
-
assetTransferMethod:
|
|
3759
|
-
name:
|
|
3760
|
-
version: d.version,
|
|
4196
|
+
assetTransferMethod: rail.method,
|
|
4197
|
+
...rail.domain ? { name: rail.domain.name, version: rail.domain.version } : {},
|
|
3761
4198
|
minConfirmations,
|
|
3762
4199
|
decimals: s.decimals,
|
|
3763
4200
|
amountFormatted: s.amountFormatted,
|
|
@@ -3804,13 +4241,44 @@ function createPaymentGate(options) {
|
|
|
3804
4241
|
});
|
|
3805
4242
|
return { kind: "invalid", error: code, detail, challenge: c, requiredHeader, statusCode: 402 };
|
|
3806
4243
|
}
|
|
4244
|
+
function enrichReceipt(spec, receipt) {
|
|
4245
|
+
let amountFormatted = receipt.amount;
|
|
4246
|
+
try {
|
|
4247
|
+
amountFormatted = formatUnits(BigInt(receipt.amount), spec.decimals);
|
|
4248
|
+
} catch {
|
|
4249
|
+
}
|
|
4250
|
+
return {
|
|
4251
|
+
...receipt,
|
|
4252
|
+
decimals: spec.decimals,
|
|
4253
|
+
...spec.symbol ? { symbol: spec.symbol } : {},
|
|
4254
|
+
amountFormatted,
|
|
4255
|
+
idempotencyKey: receipt.transaction
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
function reportOnPaidError(error, receipt) {
|
|
4259
|
+
if (!options.onPaidError) return;
|
|
4260
|
+
try {
|
|
4261
|
+
options.onPaidError(error, receipt);
|
|
4262
|
+
} catch {
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
3807
4265
|
function fireOnPaid(receipt) {
|
|
3808
|
-
if (options.onPaid)
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4266
|
+
if (!options.onPaid) return;
|
|
4267
|
+
let outcome;
|
|
4268
|
+
try {
|
|
4269
|
+
outcome = options.onPaid(receipt);
|
|
4270
|
+
} catch (err) {
|
|
4271
|
+
reportOnPaidError(err, receipt);
|
|
4272
|
+
return;
|
|
3813
4273
|
}
|
|
4274
|
+
if (outcome != null && typeof outcome.then === "function") {
|
|
4275
|
+
return Promise.resolve(outcome).catch((err) => reportOnPaidError(err, receipt));
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
async function deliverOnPaid(spec, receipt) {
|
|
4279
|
+
const paid = enrichReceipt(spec, receipt);
|
|
4280
|
+
if (options.awaitOnPaid) await fireOnPaid(paid);
|
|
4281
|
+
else void fireOnPaid(paid);
|
|
3814
4282
|
}
|
|
3815
4283
|
async function describe(resourceUrl = "") {
|
|
3816
4284
|
const specs = await ready();
|
|
@@ -3854,7 +4322,7 @@ function createPaymentGate(options) {
|
|
|
3854
4322
|
return rejection(result.error, result.detail);
|
|
3855
4323
|
}
|
|
3856
4324
|
await settleTx(ref, true);
|
|
3857
|
-
|
|
4325
|
+
await deliverOnPaid(spec, result.receipt);
|
|
3858
4326
|
return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
|
|
3859
4327
|
}
|
|
3860
4328
|
async function verifyExact(exact) {
|
|
@@ -3877,7 +4345,8 @@ function createPaymentGate(options) {
|
|
|
3877
4345
|
`No \`exact\` rail offered for ${exact.network}${exact.asset ? `/${exact.asset}` : ""} (offered: ${exactSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
|
|
3878
4346
|
);
|
|
3879
4347
|
}
|
|
3880
|
-
const
|
|
4348
|
+
const auth = "permit2Authorization" in exact.payload ? exact.payload.permit2Authorization : exact.payload.authorization;
|
|
4349
|
+
const nonce = auth.nonce;
|
|
3881
4350
|
if (await claimTx(nonce)) {
|
|
3882
4351
|
return rejection("tx_already_used", `Authorization nonce ${nonce} was already redeemed.`);
|
|
3883
4352
|
}
|
|
@@ -3909,7 +4378,7 @@ function createPaymentGate(options) {
|
|
|
3909
4378
|
extra: { name: accept.extra.name ?? "", version: accept.extra.version ?? "" }
|
|
3910
4379
|
},
|
|
3911
4380
|
receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
|
|
3912
|
-
payerHint:
|
|
4381
|
+
payerHint: auth.from
|
|
3913
4382
|
});
|
|
3914
4383
|
}
|
|
3915
4384
|
} catch (err) {
|
|
@@ -3921,7 +4390,7 @@ function createPaymentGate(options) {
|
|
|
3921
4390
|
return rejection(result.error, result.detail);
|
|
3922
4391
|
}
|
|
3923
4392
|
await settleTx(nonce, true);
|
|
3924
|
-
|
|
4393
|
+
await deliverOnPaid(spec, result.receipt);
|
|
3925
4394
|
return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
|
|
3926
4395
|
}
|
|
3927
4396
|
async function verify(paymentSignature) {
|
|
@@ -3974,6 +4443,110 @@ function normaliseHeader(value) {
|
|
|
3974
4443
|
if (Array.isArray(value)) return value[0];
|
|
3975
4444
|
return value;
|
|
3976
4445
|
}
|
|
4446
|
+
|
|
4447
|
+
// src/receipts.ts
|
|
4448
|
+
var DEFAULT_RETRIES = 5;
|
|
4449
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
4450
|
+
function defaultBackoff(attempt) {
|
|
4451
|
+
const base2 = Math.min(3e4, 2 ** (attempt - 1) * 500);
|
|
4452
|
+
return Math.round(base2 * (0.5 + Math.random()));
|
|
4453
|
+
}
|
|
4454
|
+
function isRetryableStatus(status) {
|
|
4455
|
+
return status === 408 || status === 429 || status >= 500;
|
|
4456
|
+
}
|
|
4457
|
+
var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
4458
|
+
async function signBody(secret, body) {
|
|
4459
|
+
const subtle = globalThis.crypto?.subtle;
|
|
4460
|
+
if (!subtle) return null;
|
|
4461
|
+
try {
|
|
4462
|
+
const enc = new TextEncoder();
|
|
4463
|
+
const key = await subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
4464
|
+
"sign"
|
|
4465
|
+
]);
|
|
4466
|
+
const sig = await subtle.sign("HMAC", key, enc.encode(body));
|
|
4467
|
+
const hex = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4468
|
+
return `sha256=${hex}`;
|
|
4469
|
+
} catch {
|
|
4470
|
+
return null;
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
async function deliverReceipt(receipt, options) {
|
|
4474
|
+
const {
|
|
4475
|
+
url,
|
|
4476
|
+
secret,
|
|
4477
|
+
retries = DEFAULT_RETRIES,
|
|
4478
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
4479
|
+
headers = {},
|
|
4480
|
+
signatureHeader = "piprail-signature",
|
|
4481
|
+
idempotencyHeader = "idempotency-key",
|
|
4482
|
+
backoff = defaultBackoff,
|
|
4483
|
+
fetchImpl = globalThis.fetch,
|
|
4484
|
+
onAttempt
|
|
4485
|
+
} = options;
|
|
4486
|
+
if (typeof fetchImpl !== "function") {
|
|
4487
|
+
return { delivered: false, attempts: 0, error: "no fetch implementation available" };
|
|
4488
|
+
}
|
|
4489
|
+
const body = JSON.stringify(receipt);
|
|
4490
|
+
const signature = secret ? await signBody(secret, body) : null;
|
|
4491
|
+
const baseHeaders = {
|
|
4492
|
+
...headers,
|
|
4493
|
+
"content-type": "application/json",
|
|
4494
|
+
[idempotencyHeader]: receipt.idempotencyKey,
|
|
4495
|
+
...signature ? { [signatureHeader]: signature } : {}
|
|
4496
|
+
};
|
|
4497
|
+
const maxAttempts = Math.max(1, retries + 1);
|
|
4498
|
+
let lastStatus;
|
|
4499
|
+
let lastError;
|
|
4500
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4501
|
+
const controller = new AbortController();
|
|
4502
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4503
|
+
let ok = false;
|
|
4504
|
+
let status;
|
|
4505
|
+
let error;
|
|
4506
|
+
try {
|
|
4507
|
+
const res = await fetchImpl(url, {
|
|
4508
|
+
method: "POST",
|
|
4509
|
+
headers: baseHeaders,
|
|
4510
|
+
body,
|
|
4511
|
+
signal: controller.signal
|
|
4512
|
+
});
|
|
4513
|
+
status = res.status;
|
|
4514
|
+
lastStatus = status;
|
|
4515
|
+
ok = res.ok;
|
|
4516
|
+
if (!ok) {
|
|
4517
|
+
error = `HTTP ${status}`;
|
|
4518
|
+
lastError = error;
|
|
4519
|
+
}
|
|
4520
|
+
} catch (err) {
|
|
4521
|
+
error = err instanceof Error ? err.message : String(err);
|
|
4522
|
+
lastError = error;
|
|
4523
|
+
} finally {
|
|
4524
|
+
clearTimeout(timer);
|
|
4525
|
+
}
|
|
4526
|
+
const retryable = status === void 0 ? true : isRetryableStatus(status);
|
|
4527
|
+
const willRetry = !ok && retryable && attempt < maxAttempts;
|
|
4528
|
+
try {
|
|
4529
|
+
onAttempt?.({ attempt, ok, ...status !== void 0 ? { status } : {}, ...error ? { error } : {}, willRetry });
|
|
4530
|
+
} catch {
|
|
4531
|
+
}
|
|
4532
|
+
if (ok) return { delivered: true, attempts: attempt, status };
|
|
4533
|
+
if (!willRetry) {
|
|
4534
|
+
return {
|
|
4535
|
+
delivered: false,
|
|
4536
|
+
attempts: attempt,
|
|
4537
|
+
...lastStatus !== void 0 ? { status: lastStatus } : {},
|
|
4538
|
+
...lastError ? { error: lastError } : {}
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
await sleep(backoff(attempt));
|
|
4542
|
+
}
|
|
4543
|
+
return {
|
|
4544
|
+
delivered: false,
|
|
4545
|
+
attempts: maxAttempts,
|
|
4546
|
+
...lastStatus !== void 0 ? { status: lastStatus } : {},
|
|
4547
|
+
...lastError ? { error: lastError } : {}
|
|
4548
|
+
};
|
|
4549
|
+
}
|
|
3977
4550
|
export {
|
|
3978
4551
|
CHAINS,
|
|
3979
4552
|
ConfirmationTimeoutError,
|
|
@@ -3992,6 +4565,8 @@ export {
|
|
|
3992
4565
|
MissingDriverError,
|
|
3993
4566
|
NoCompatibleAcceptError,
|
|
3994
4567
|
NonReplayableBodyError,
|
|
4568
|
+
PERMIT2_ADDRESS,
|
|
4569
|
+
PERMIT2_WITNESS_TYPES,
|
|
3995
4570
|
PIPRAIL_AGENT_GUIDE,
|
|
3996
4571
|
PaymentDeclinedError,
|
|
3997
4572
|
PaymentTimeoutError,
|
|
@@ -4004,6 +4579,7 @@ export {
|
|
|
4004
4579
|
UnsupportedSchemeError,
|
|
4005
4580
|
WrongChainError,
|
|
4006
4581
|
WrongFamilyError,
|
|
4582
|
+
X402_EXACT_PERMIT2_PROXY,
|
|
4007
4583
|
agentGuide,
|
|
4008
4584
|
buildBazaarExtension,
|
|
4009
4585
|
buildChallengeHeader,
|
|
@@ -4019,6 +4595,7 @@ export {
|
|
|
4019
4595
|
classifyChallenge,
|
|
4020
4596
|
createPaymentGate,
|
|
4021
4597
|
decorateOutcome,
|
|
4598
|
+
deliverReceipt,
|
|
4022
4599
|
eip3009Abi,
|
|
4023
4600
|
encodeXPaymentHeader,
|
|
4024
4601
|
evaluatePolicy,
|