@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/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 erc20Abi3, getAddress as getAddress3, http as http2, isAddress } from "viem";
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 [name, version] = await Promise.all([
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 name !== "string" || typeof version !== "string" || !name || !version) return null;
719
- return { name, version };
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
- const authorization = payload.authorization;
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
- return {
999
- x402Version,
1000
- network,
1001
- ...asset ? { asset } : {},
1002
- payload: {
1003
- signature,
1004
- authorization
1005
- },
1006
- raw: v
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 = getAddress3(asset);
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 (getAddress3(info.address) === normalized) {
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: getAddress3(asset),
1197
- abi: erc20Abi3,
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 (EIP-3009), BUYER side — EVM only. Re-derives the
1230
- // token's EIP-712 domain on-chain, signs an authorization with the agent's own
1231
- // key, and returns it for the client to frame into PAYMENT-SIGNATURE. Never
1232
- // broadcasts. Throws UnsupportedSchemeError for a non-EIP-3009 token / contract signer.
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 + EIP-3009 only (USDC / EURC) \u2014 not native coins, not USDT, not non-EVM chains. Offer an EVM EIP-3009 token, or drop `exact`."
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.exactDomain || !net.settleExactSelf) {
4111
+ if (net.family !== "evm" || asset === "native" || !net.settleExactSelf) {
3687
4112
  return void 0;
3688
4113
  }
3689
- const domain = await net.exactDomain(asset);
3690
- if (!domain) {
3691
- throw new Error(
3692
- `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.)`
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 transferWithAuthorization), e.g. exact: { settle: 'self', relayer: { privateKey } }."
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
- domain,
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 d = s.exact.domain;
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: "eip3009",
3759
- name: d.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
- try {
3810
- options.onPaid(receipt);
3811
- } catch {
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
- fireOnPaid(result.receipt);
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 nonce = exact.payload.authorization.nonce;
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: exact.payload.authorization.from
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
- fireOnPaid(result.receipt);
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,