@piprail/sdk 1.15.1 → 1.18.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,17 @@ 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,
520
+ // EIP-3009 USDC verified on-chain (authorizationState present) — gasless, no proxy:
521
+ sonic: 146,
522
+ linea: 59144,
523
+ celo: 42220,
524
+ unichain: 130,
525
+ worldchain: 480,
526
+ sei: 1329,
527
+ hyperevm: 999
509
528
  };
510
529
  function chainIdForExactNetwork(slug) {
511
530
  return EXACT_NETWORK_SLUGS[slug] ?? null;
@@ -690,10 +709,25 @@ var eip3009Abi = [
690
709
  outputs: [{ type: "bool" }]
691
710
  },
692
711
  { type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
693
- { type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] }
712
+ { type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
713
+ // EIP-3009 tokens that DON'T expose version() still expose DOMAIN_SEPARATOR — we match it to
714
+ // DERIVE the hardcoded domain version (e.g. FDUSD / USD1 on BNB Chain both use "1").
715
+ { type: "function", name: "DOMAIN_SEPARATOR", stateMutability: "view", inputs: [], outputs: [{ type: "bytes32" }] }
694
716
  ];
695
717
  var ZERO_ADDR = "0x0000000000000000000000000000000000000000";
696
718
  var ZERO_NONCE = `0x${"00".repeat(32)}`;
719
+ var EIP712_DOMAIN_TYPEHASH = keccak256(
720
+ toHex("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
721
+ );
722
+ var EXACT_DOMAIN_VERSION_CANDIDATES = ["1", "2"];
723
+ function eip712DomainSeparator(name, version, chainId, verifyingContract) {
724
+ return keccak256(
725
+ encodeAbiParameters(
726
+ [{ type: "bytes32" }, { type: "bytes32" }, { type: "bytes32" }, { type: "uint256" }, { type: "address" }],
727
+ [EIP712_DOMAIN_TYPEHASH, keccak256(toHex(name)), keccak256(toHex(version)), BigInt(chainId), verifyingContract]
728
+ )
729
+ );
730
+ }
697
731
  async function readExactDomain(publicClient, asset) {
698
732
  if (asset === "native") return null;
699
733
  let token;
@@ -702,12 +736,10 @@ async function readExactDomain(publicClient, asset) {
702
736
  } catch {
703
737
  return null;
704
738
  }
739
+ let name;
705
740
  try {
706
- const [name, version] = await Promise.all([
741
+ const [n] = await Promise.all([
707
742
  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
743
  publicClient.readContract({
712
744
  address: token,
713
745
  abi: eip3009Abi,
@@ -715,11 +747,34 @@ async function readExactDomain(publicClient, asset) {
715
747
  args: [ZERO_ADDR, ZERO_NONCE]
716
748
  })
717
749
  ]);
718
- if (typeof name !== "string" || typeof version !== "string" || !name || !version) return null;
719
- return { name, version };
750
+ if (typeof n !== "string" || !n) return null;
751
+ name = n;
720
752
  } catch {
721
753
  return null;
722
754
  }
755
+ try {
756
+ const version = await publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "version" });
757
+ if (typeof version === "string" && version) return { name, version };
758
+ } catch {
759
+ }
760
+ return deriveExactDomainVersion(publicClient, token, name);
761
+ }
762
+ async function deriveExactDomainVersion(publicClient, token, name) {
763
+ let onchain;
764
+ let chainId;
765
+ try {
766
+ onchain = await publicClient.readContract({ address: token, abi: eip3009Abi, functionName: "DOMAIN_SEPARATOR" });
767
+ chainId = publicClient.chain?.id ?? await publicClient.getChainId();
768
+ } catch {
769
+ return null;
770
+ }
771
+ const target = onchain.toLowerCase();
772
+ for (const version of EXACT_DOMAIN_VERSION_CANDIDATES) {
773
+ if (eip712DomainSeparator(name, version, chainId, token).toLowerCase() === target) {
774
+ return { name, version };
775
+ }
776
+ }
777
+ return null;
723
778
  }
724
779
  function shorten(msg) {
725
780
  const oneLine = msg.replace(/\s+/g, " ").trim();
@@ -874,6 +929,377 @@ async function verifyAndSettleExactEvm(input) {
874
929
  };
875
930
  }
876
931
 
932
+ // src/drivers/evm/permit2.ts
933
+ import {
934
+ erc20Abi as erc20Abi3,
935
+ getAddress as getAddress3,
936
+ maxUint256,
937
+ recoverTypedDataAddress as recoverTypedDataAddress2
938
+ } from "viem";
939
+ var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
940
+ var X402_EXACT_PERMIT2_PROXY = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";
941
+ var PERMIT2_PROXY_CHAIN_IDS = /* @__PURE__ */ new Set([
942
+ 1,
943
+ // Ethereum
944
+ 8453,
945
+ // Base
946
+ 84532,
947
+ // Base Sepolia
948
+ 42161,
949
+ // Arbitrum
950
+ 10,
951
+ // Optimism
952
+ 137,
953
+ // Polygon
954
+ 43114,
955
+ // Avalanche
956
+ 56,
957
+ // BNB
958
+ 42220,
959
+ // Celo
960
+ 480,
961
+ // World Chain
962
+ 1329,
963
+ // Sei
964
+ 999,
965
+ // HyperEVM
966
+ 143
967
+ // Monad
968
+ ]);
969
+ function isPermit2ProxyChain(chainId) {
970
+ return PERMIT2_PROXY_CHAIN_IDS.has(chainId);
971
+ }
972
+ var PERMIT2_WITNESS_TYPES = {
973
+ PermitWitnessTransferFrom: [
974
+ { name: "permitted", type: "TokenPermissions" },
975
+ { name: "spender", type: "address" },
976
+ { name: "nonce", type: "uint256" },
977
+ { name: "deadline", type: "uint256" },
978
+ { name: "witness", type: "Witness" }
979
+ ],
980
+ TokenPermissions: [
981
+ { name: "token", type: "address" },
982
+ { name: "amount", type: "uint256" }
983
+ ],
984
+ Witness: [
985
+ { name: "to", type: "address" },
986
+ { name: "validAfter", type: "uint256" }
987
+ ]
988
+ };
989
+ var x402Permit2ProxyAbi = [
990
+ {
991
+ type: "function",
992
+ name: "settle",
993
+ stateMutability: "nonpayable",
994
+ outputs: [],
995
+ inputs: [
996
+ {
997
+ name: "permit",
998
+ type: "tuple",
999
+ components: [
1000
+ {
1001
+ name: "permitted",
1002
+ type: "tuple",
1003
+ components: [
1004
+ { name: "token", type: "address" },
1005
+ { name: "amount", type: "uint256" }
1006
+ ]
1007
+ },
1008
+ { name: "nonce", type: "uint256" },
1009
+ { name: "deadline", type: "uint256" }
1010
+ ]
1011
+ },
1012
+ { name: "owner", type: "address" },
1013
+ {
1014
+ name: "witness",
1015
+ type: "tuple",
1016
+ components: [
1017
+ { name: "to", type: "address" },
1018
+ { name: "validAfter", type: "uint256" }
1019
+ ]
1020
+ },
1021
+ { name: "signature", type: "bytes" }
1022
+ ]
1023
+ }
1024
+ ];
1025
+ var permit2NonceBitmapAbi = [
1026
+ {
1027
+ type: "function",
1028
+ name: "nonceBitmap",
1029
+ stateMutability: "view",
1030
+ inputs: [
1031
+ { name: "owner", type: "address" },
1032
+ { name: "word", type: "uint256" }
1033
+ ],
1034
+ outputs: [{ type: "uint256" }]
1035
+ }
1036
+ ];
1037
+ function shorten2(msg) {
1038
+ const oneLine = msg.replace(/\s+/g, " ").trim();
1039
+ return oneLine.length > 200 ? `${oneLine.slice(0, 200)}\u2026` : oneLine;
1040
+ }
1041
+ function randomPermit2Nonce() {
1042
+ const g = globalThis.crypto;
1043
+ if (!g?.getRandomValues) {
1044
+ throw new UnsupportedSchemeError(
1045
+ "this runtime lacks Web Crypto (globalThis.crypto.getRandomValues); the permit2 rail needs a CSPRNG nonce."
1046
+ );
1047
+ }
1048
+ const raw = new Uint8Array(32);
1049
+ g.getRandomValues(raw);
1050
+ return BigInt(`0x${[...raw].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
1051
+ }
1052
+ async function ensurePermit2Allowance(input) {
1053
+ const { publicClient, walletClient, account, chain, token, amount } = input;
1054
+ let allowance;
1055
+ try {
1056
+ allowance = await publicClient.readContract({
1057
+ address: token,
1058
+ abi: erc20Abi3,
1059
+ functionName: "allowance",
1060
+ args: [account.address, PERMIT2_ADDRESS]
1061
+ });
1062
+ } catch (err) {
1063
+ throw new UnsupportedSchemeError(
1064
+ `permit2: couldn't read the Permit2 allowance for ${token} (${shorten2(err instanceof Error ? err.message : String(err))}).`
1065
+ );
1066
+ }
1067
+ if (allowance >= amount) return void 0;
1068
+ try {
1069
+ const hash = await walletClient.writeContract({
1070
+ account,
1071
+ chain,
1072
+ address: token,
1073
+ abi: erc20Abi3,
1074
+ functionName: "approve",
1075
+ args: [PERMIT2_ADDRESS, maxUint256]
1076
+ });
1077
+ await publicClient.waitForTransactionReceipt({ hash, confirmations: 1 });
1078
+ return hash;
1079
+ } catch (err) {
1080
+ throw toInsufficientFundsError(err) ?? new SettlementError(
1081
+ `permit2: the one-time Permit2 approval for ${token} failed to broadcast (${shorten2(err instanceof Error ? err.message : String(err))}).`,
1082
+ { cause: err }
1083
+ );
1084
+ }
1085
+ }
1086
+ async function payPermit2Evm(input) {
1087
+ const { publicClient, walletClient, account, chainId, chain, accept } = input;
1088
+ let code;
1089
+ try {
1090
+ code = await publicClient.getCode({ address: account.address });
1091
+ } catch {
1092
+ code = void 0;
1093
+ }
1094
+ if (code && code !== "0x") {
1095
+ throw new UnsupportedSchemeError(
1096
+ `permit2 buyer rail requires an EOA signer; ${account.address} is a contract / EIP-1271 / EIP-7702-delegated account. Pay via onchain-proof.`
1097
+ );
1098
+ }
1099
+ const token = getAddress3(accept.asset);
1100
+ const payTo = getAddress3(accept.payTo);
1101
+ const value = BigInt(accept.amount);
1102
+ const approvalTx = await ensurePermit2Allowance({
1103
+ publicClient,
1104
+ walletClient,
1105
+ account,
1106
+ chain,
1107
+ token,
1108
+ amount: value
1109
+ });
1110
+ const nonce = randomPermit2Nonce();
1111
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + accept.maxTimeoutSeconds);
1112
+ const validAfter = 0n;
1113
+ const spender = getAddress3(X402_EXACT_PERMIT2_PROXY);
1114
+ const from = account.address;
1115
+ const signature = await walletClient.signTypedData({
1116
+ account,
1117
+ domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS },
1118
+ types: PERMIT2_WITNESS_TYPES,
1119
+ primaryType: "PermitWitnessTransferFrom",
1120
+ message: {
1121
+ permitted: { token, amount: value },
1122
+ spender,
1123
+ nonce,
1124
+ deadline,
1125
+ witness: { to: payTo, validAfter }
1126
+ }
1127
+ });
1128
+ const permit2Authorization = {
1129
+ permitted: { token, amount: value.toString() },
1130
+ from,
1131
+ spender,
1132
+ nonce: nonce.toString(),
1133
+ deadline: deadline.toString(),
1134
+ witness: { to: payTo, validAfter: validAfter.toString() }
1135
+ };
1136
+ return {
1137
+ payload: { signature, permit2Authorization },
1138
+ payerFrom: from,
1139
+ nonce: nonce.toString(),
1140
+ ...approvalTx ? { approvalTx } : {}
1141
+ };
1142
+ }
1143
+ async function verifyAndSettlePermit2Evm(input) {
1144
+ const { publicClient, walletClient, account, chain, payload, accept } = input;
1145
+ const token = getAddress3(accept.asset);
1146
+ const payTo = getAddress3(accept.payTo);
1147
+ const requiredAmount = BigInt(accept.amount);
1148
+ const proxy = getAddress3(X402_EXACT_PERMIT2_PROXY);
1149
+ let from;
1150
+ let spender;
1151
+ let permittedToken;
1152
+ let witnessTo;
1153
+ let permittedAmount;
1154
+ let nonce;
1155
+ let deadline;
1156
+ let validAfter;
1157
+ const signature = payload.signature;
1158
+ try {
1159
+ const pa = payload.permit2Authorization;
1160
+ from = getAddress3(pa.from);
1161
+ spender = getAddress3(pa.spender);
1162
+ permittedToken = getAddress3(pa.permitted.token);
1163
+ witnessTo = getAddress3(pa.witness.to);
1164
+ permittedAmount = BigInt(pa.permitted.amount);
1165
+ nonce = BigInt(pa.nonce);
1166
+ deadline = BigInt(pa.deadline);
1167
+ validAfter = BigInt(pa.witness.validAfter);
1168
+ if (!/^0x[0-9a-fA-F]+$/.test(signature)) throw new Error("signature must be hex");
1169
+ } catch (err) {
1170
+ return {
1171
+ ok: false,
1172
+ error: "signature_invalid",
1173
+ detail: `Malformed permit2 authorization: ${err instanceof Error ? err.message : String(err)}.`
1174
+ };
1175
+ }
1176
+ if (witnessTo !== payTo) {
1177
+ return { ok: false, error: "wrong_recipient", detail: `Authorization pays witness.to ${witnessTo}, not ${payTo}.` };
1178
+ }
1179
+ if (permittedToken !== token) {
1180
+ return { ok: false, error: "signature_invalid", detail: `Authorization permits token ${permittedToken}, not the rail's ${token}.` };
1181
+ }
1182
+ if (spender !== proxy) {
1183
+ return { ok: false, error: "signature_invalid", detail: `Authorization spender ${spender} is not the x402ExactPermit2Proxy ${proxy}; it can't be settled here.` };
1184
+ }
1185
+ if (permittedAmount < requiredAmount) {
1186
+ return { ok: false, error: "amount_too_low", detail: `Permitted ${permittedAmount}, required ${requiredAmount}.` };
1187
+ }
1188
+ const now = BigInt(Math.floor(Date.now() / 1e3));
1189
+ if (deadline <= now) {
1190
+ return { ok: false, error: "payment_expired", detail: `Permit2 deadline ${deadline} <= now ${now}.` };
1191
+ }
1192
+ let fromCode;
1193
+ try {
1194
+ fromCode = await publicClient.getCode({ address: from });
1195
+ } catch {
1196
+ return { ok: false, error: "tx_not_found", detail: `Could not read code at ${from} (transient RPC) \u2014 retry.` };
1197
+ }
1198
+ if (!(fromCode && fromCode !== "0x")) {
1199
+ let recovered;
1200
+ try {
1201
+ recovered = await recoverTypedDataAddress2({
1202
+ domain: { name: "Permit2", chainId: chain.id, verifyingContract: PERMIT2_ADDRESS },
1203
+ types: PERMIT2_WITNESS_TYPES,
1204
+ primaryType: "PermitWitnessTransferFrom",
1205
+ message: {
1206
+ permitted: { token: permittedToken, amount: permittedAmount },
1207
+ spender,
1208
+ nonce,
1209
+ deadline,
1210
+ witness: { to: witnessTo, validAfter }
1211
+ },
1212
+ signature
1213
+ });
1214
+ } catch (err) {
1215
+ return { ok: false, error: "signature_invalid", detail: `Not a valid EIP-712 signature: ${shorten2(err instanceof Error ? err.message : String(err))}.` };
1216
+ }
1217
+ if (recovered !== from) {
1218
+ return { ok: false, error: "signature_invalid", detail: `Signature recovered to ${recovered}, not the authorizer ${from}.` };
1219
+ }
1220
+ }
1221
+ try {
1222
+ const word = nonce >> 8n;
1223
+ const bit = nonce & 0xffn;
1224
+ const bitmap = await publicClient.readContract({
1225
+ address: PERMIT2_ADDRESS,
1226
+ abi: permit2NonceBitmapAbi,
1227
+ functionName: "nonceBitmap",
1228
+ args: [from, word]
1229
+ });
1230
+ if ((bitmap >> bit & 1n) === 1n) {
1231
+ return { ok: false, error: "tx_already_used", detail: `Permit2 nonce ${nonce} already used or invalidated for ${from}.` };
1232
+ }
1233
+ } catch {
1234
+ return { ok: false, error: "tx_not_found", detail: "Could not read the Permit2 nonce bitmap (transient RPC) \u2014 retry." };
1235
+ }
1236
+ const settleArgs = [
1237
+ { permitted: { token: permittedToken, amount: permittedAmount }, nonce, deadline },
1238
+ from,
1239
+ { to: witnessTo, validAfter },
1240
+ signature
1241
+ ];
1242
+ try {
1243
+ await publicClient.simulateContract({
1244
+ account,
1245
+ address: proxy,
1246
+ abi: x402Permit2ProxyAbi,
1247
+ functionName: "settle",
1248
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1249
+ args: settleArgs
1250
+ });
1251
+ } catch (err) {
1252
+ const msg = err instanceof Error ? err.message : String(err);
1253
+ if (/nonce|invalidated|used/i.test(msg)) return { ok: false, error: "tx_already_used", detail: "Permit2 nonce is used or invalidated." };
1254
+ if (/expired|deadline|too early|not yet/i.test(msg)) return { ok: false, error: "payment_expired", detail: shorten2(msg) };
1255
+ if (/signature/i.test(msg)) return { ok: false, error: "signature_invalid", detail: shorten2(msg) };
1256
+ return { ok: false, error: "tx_reverted", detail: `permit2 settle would revert: ${shorten2(msg)}` };
1257
+ }
1258
+ let txHash;
1259
+ try {
1260
+ txHash = await walletClient.writeContract({
1261
+ account,
1262
+ chain,
1263
+ address: proxy,
1264
+ abi: x402Permit2ProxyAbi,
1265
+ functionName: "settle",
1266
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1267
+ args: settleArgs
1268
+ });
1269
+ } catch (err) {
1270
+ throw new SettlementError(
1271
+ `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.`,
1272
+ { cause: err }
1273
+ );
1274
+ }
1275
+ try {
1276
+ const confirmations = accept.extra.minConfirmations ?? 1;
1277
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations });
1278
+ if (receipt.status !== "success") {
1279
+ return { ok: false, error: "tx_reverted", detail: `Settlement tx ${txHash} reverted on-chain.` };
1280
+ }
1281
+ } catch (err) {
1282
+ throw new SettlementError(
1283
+ `permit2 settle: broadcast ${txHash} but couldn't confirm it (${shorten2(err instanceof Error ? err.message : String(err))}).`,
1284
+ { cause: err }
1285
+ );
1286
+ }
1287
+ return {
1288
+ ok: true,
1289
+ receipt: {
1290
+ scheme: "exact",
1291
+ success: true,
1292
+ network: accept.network,
1293
+ transaction: txHash,
1294
+ asset: accept.asset,
1295
+ amount: accept.amount,
1296
+ payer: from,
1297
+ payTo: accept.payTo,
1298
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
1299
+ }
1300
+ };
1301
+ }
1302
+
877
1303
  // src/x402.ts
878
1304
  var HEADER_REQUIRED = "payment-required";
879
1305
  var HEADER_SIGNATURE = "payment-signature";
@@ -988,23 +1414,38 @@ function parseExactPaymentHeader(value) {
988
1414
  const payload = v.payload;
989
1415
  if (!payload || typeof payload !== "object") return null;
990
1416
  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
- }
1417
+ if (typeof signature !== "string") return null;
996
1418
  const x402Version = typeof v.x402Version === "number" ? v.x402Version : 2;
997
1419
  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
- };
1420
+ const base2 = { x402Version, network, ...asset ? { asset } : {}, raw: v };
1421
+ const authorization = payload.authorization;
1422
+ if (authorization && typeof authorization === "object") {
1423
+ for (const k of ["from", "to", "value", "validAfter", "validBefore", "nonce"]) {
1424
+ if (typeof authorization[k] !== "string") return null;
1425
+ }
1426
+ return {
1427
+ ...base2,
1428
+ method: "eip3009",
1429
+ payload: { signature, authorization }
1430
+ };
1431
+ }
1432
+ const p2 = payload.permit2Authorization;
1433
+ if (p2 && typeof p2 === "object") {
1434
+ const permitted = p2.permitted;
1435
+ const witness = p2.witness;
1436
+ if (!permitted || typeof permitted !== "object" || !witness || typeof witness !== "object") return null;
1437
+ if (typeof permitted.token !== "string" || typeof permitted.amount !== "string") return null;
1438
+ if (typeof witness.to !== "string" || typeof witness.validAfter !== "string") return null;
1439
+ for (const k of ["from", "spender", "nonce", "deadline"]) {
1440
+ if (typeof p2[k] !== "string") return null;
1441
+ }
1442
+ return {
1443
+ ...base2,
1444
+ method: "permit2",
1445
+ payload: { signature, permit2Authorization: p2 }
1446
+ };
1447
+ }
1448
+ return null;
1008
1449
  }
1009
1450
  function isValidChallenge(value) {
1010
1451
  if (!value || typeof value !== "object") return false;
@@ -1095,12 +1536,12 @@ function makeEvmNetwork(resolved) {
1095
1536
  }
1096
1537
  let normalized;
1097
1538
  try {
1098
- normalized = getAddress3(asset);
1539
+ normalized = getAddress4(asset);
1099
1540
  } catch {
1100
1541
  return null;
1101
1542
  }
1102
1543
  for (const info of Object.values(resolved.tokens)) {
1103
- if (getAddress3(info.address) === normalized) {
1544
+ if (getAddress4(info.address) === normalized) {
1104
1545
  return { symbol: info.symbol, decimals: info.decimals };
1105
1546
  }
1106
1547
  }
@@ -1157,12 +1598,13 @@ function makeEvmNetwork(resolved) {
1157
1598
  async estimateCost(accept) {
1158
1599
  const { decimals, symbol } = resolved.chain.nativeCurrency;
1159
1600
  if (accept.scheme === "exact") {
1601
+ const permit2 = accept.extra.assetTransferMethod === "permit2";
1160
1602
  return nativeCost({
1161
1603
  symbol,
1162
1604
  decimals,
1163
1605
  fee: 0n,
1164
1606
  basis: "estimated",
1165
- detail: "gasless \u2014 the server/facilitator settles the signed authorization"
1607
+ 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
1608
  });
1167
1609
  }
1168
1610
  const gasLimit = accept.asset === "native" ? 21000n : 65000n;
@@ -1193,8 +1635,8 @@ function makeEvmNetwork(resolved) {
1193
1635
  let token = null;
1194
1636
  try {
1195
1637
  token = await publicClient.readContract({
1196
- address: getAddress3(asset),
1197
- abi: erc20Abi3,
1638
+ address: getAddress4(asset),
1639
+ abi: erc20Abi4,
1198
1640
  functionName: "balanceOf",
1199
1641
  args: [owner]
1200
1642
  });
@@ -1226,12 +1668,24 @@ function makeEvmNetwork(resolved) {
1226
1668
  minConfirmations: accept.extra.minConfirmations
1227
1669
  });
1228
1670
  },
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.
1671
+ // Standard x402 `exact` rail, BUYER side — EVM only. Routes on the rail's
1672
+ // `assetTransferMethod`: `permit2` (any ERC-20 e.g. Binance-Peg USDC on BNB, signs a
1673
+ // Permit2 witness transfer + lazily does the one-time approval) or `eip3009` (re-derives
1674
+ // the token's EIP-712 domain on-chain + signs transferWithAuthorization). Never broadcasts.
1675
+ // Throws UnsupportedSchemeError for a contract signer (or a non-EIP-3009 token on the eip3009 path).
1233
1676
  async payExact(wallet, accept) {
1234
1677
  const a = wallet._native;
1678
+ if (accept.extra.assetTransferMethod === "permit2") {
1679
+ const { payload: payload2, payerFrom: payerFrom2, nonce: nonce2 } = await payPermit2Evm({
1680
+ publicClient,
1681
+ walletClient: a.walletClient,
1682
+ account: a.account,
1683
+ chainId: resolved.chainId,
1684
+ chain: resolved.chain,
1685
+ accept
1686
+ });
1687
+ return { payload: payload2, accepted: accept, payerFrom: payerFrom2, nonce: nonce2 };
1688
+ }
1235
1689
  const { payload, payerFrom, nonce } = await payExactEvm({
1236
1690
  publicClient,
1237
1691
  walletClient: a.walletClient,
@@ -1245,8 +1699,23 @@ function makeEvmNetwork(resolved) {
1245
1699
  async exactDomain(asset) {
1246
1700
  return readExactDomain(publicClient, asset);
1247
1701
  },
1702
+ // Whether the Permit2 transfer method can settle here (proxy deployed). EIP-3009
1703
+ // needs no proxy; this only gates the Permit2 fallback for non-EIP-3009 tokens.
1704
+ exactPermit2Supported() {
1705
+ return isPermit2ProxyChain(resolved.chainId);
1706
+ },
1248
1707
  async settleExactSelf({ relayer, payload, accept }) {
1249
1708
  const a = relayer._native;
1709
+ if ("permit2Authorization" in payload) {
1710
+ return verifyAndSettlePermit2Evm({
1711
+ publicClient,
1712
+ walletClient: a.walletClient,
1713
+ account: a.account,
1714
+ chain: resolved.chain,
1715
+ payload,
1716
+ accept
1717
+ });
1718
+ }
1250
1719
  return verifyAndSettleExactEvm({
1251
1720
  publicClient,
1252
1721
  walletClient: a.walletClient,
@@ -3670,7 +4139,7 @@ function createPaymentGate(options) {
3670
4139
  );
3671
4140
  if (options.exact && !specs.some((s) => s.exact)) {
3672
4141
  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`."
4142
+ "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
4143
  );
3675
4144
  }
3676
4145
  return specs;
@@ -3683,26 +4152,47 @@ function createPaymentGate(options) {
3683
4152
  }
3684
4153
  async function resolveExactRail(net, asset) {
3685
4154
  const cfg = options.exact;
3686
- if (net.family !== "evm" || asset === "native" || !net.exactDomain || !net.settleExactSelf) {
4155
+ if (net.family !== "evm" || asset === "native" || !net.settleExactSelf) {
3687
4156
  return void 0;
3688
4157
  }
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
- );
4158
+ const want = cfg.method ?? "auto";
4159
+ let method;
4160
+ let domain;
4161
+ if (want === "permit2") {
4162
+ method = "permit2";
4163
+ } else {
4164
+ const d = net.exactDomain ? await net.exactDomain(asset) : null;
4165
+ if (d) {
4166
+ method = "eip3009";
4167
+ domain = d;
4168
+ } else if (want === "eip3009") {
4169
+ throw new Error(
4170
+ `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.)`
4171
+ );
4172
+ } else {
4173
+ method = "permit2";
4174
+ }
4175
+ }
4176
+ if (method === "permit2" && !(net.exactPermit2Supported?.() ?? false)) {
4177
+ if (cfg.method === "permit2") {
4178
+ throw new Error(
4179
+ `requirePayment: exact \`method: 'permit2'\` needs the x402 Permit2 proxy deployed on ${net.network}, but it isn't there. Offer an EIP-3009 token (gasless, no proxy), or drop \`exact\` on this chain. (See PERMIT2_PROXY_CHAIN_IDS.)`
4180
+ );
4181
+ }
4182
+ return void 0;
3694
4183
  }
3695
4184
  if (cfg.settle === "self") {
3696
4185
  if (cfg.relayer === void 0) {
3697
4186
  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 } }."
4187
+ "requirePayment: exact `settle: 'self'` needs a `relayer` wallet (the gas-paying key that broadcasts the settle), e.g. exact: { settle: 'self', relayer: { privateKey } }."
3699
4188
  );
3700
4189
  }
3701
4190
  const relayer = net.bindWallet(cfg.relayer);
3702
- return { domain, mode: { kind: "self", relayer } };
4191
+ return { method, ...domain ? { domain } : {}, mode: { kind: "self", relayer } };
3703
4192
  }
3704
4193
  return {
3705
- domain,
4194
+ method,
4195
+ ...domain ? { domain } : {},
3706
4196
  mode: {
3707
4197
  kind: "facilitator",
3708
4198
  url: cfg.settle.facilitator,
@@ -3746,7 +4236,7 @@ function createPaymentGate(options) {
3746
4236
  };
3747
4237
  }
3748
4238
  function buildExactAccept(s) {
3749
- const d = s.exact.domain;
4239
+ const rail = s.exact;
3750
4240
  return {
3751
4241
  scheme: "exact",
3752
4242
  network: s.net.network,
@@ -3755,9 +4245,8 @@ function createPaymentGate(options) {
3755
4245
  payTo: s.payTo,
3756
4246
  maxTimeoutSeconds,
3757
4247
  extra: {
3758
- assetTransferMethod: "eip3009",
3759
- name: d.name,
3760
- version: d.version,
4248
+ assetTransferMethod: rail.method,
4249
+ ...rail.domain ? { name: rail.domain.name, version: rail.domain.version } : {},
3761
4250
  minConfirmations,
3762
4251
  decimals: s.decimals,
3763
4252
  amountFormatted: s.amountFormatted,
@@ -3804,13 +4293,44 @@ function createPaymentGate(options) {
3804
4293
  });
3805
4294
  return { kind: "invalid", error: code, detail, challenge: c, requiredHeader, statusCode: 402 };
3806
4295
  }
4296
+ function enrichReceipt(spec, receipt) {
4297
+ let amountFormatted = receipt.amount;
4298
+ try {
4299
+ amountFormatted = formatUnits(BigInt(receipt.amount), spec.decimals);
4300
+ } catch {
4301
+ }
4302
+ return {
4303
+ ...receipt,
4304
+ decimals: spec.decimals,
4305
+ ...spec.symbol ? { symbol: spec.symbol } : {},
4306
+ amountFormatted,
4307
+ idempotencyKey: receipt.transaction
4308
+ };
4309
+ }
4310
+ function reportOnPaidError(error, receipt) {
4311
+ if (!options.onPaidError) return;
4312
+ try {
4313
+ options.onPaidError(error, receipt);
4314
+ } catch {
4315
+ }
4316
+ }
3807
4317
  function fireOnPaid(receipt) {
3808
- if (options.onPaid) {
3809
- try {
3810
- options.onPaid(receipt);
3811
- } catch {
3812
- }
4318
+ if (!options.onPaid) return;
4319
+ let outcome;
4320
+ try {
4321
+ outcome = options.onPaid(receipt);
4322
+ } catch (err) {
4323
+ reportOnPaidError(err, receipt);
4324
+ return;
3813
4325
  }
4326
+ if (outcome != null && typeof outcome.then === "function") {
4327
+ return Promise.resolve(outcome).catch((err) => reportOnPaidError(err, receipt));
4328
+ }
4329
+ }
4330
+ async function deliverOnPaid(spec, receipt) {
4331
+ const paid = enrichReceipt(spec, receipt);
4332
+ if (options.awaitOnPaid) await fireOnPaid(paid);
4333
+ else void fireOnPaid(paid);
3814
4334
  }
3815
4335
  async function describe(resourceUrl = "") {
3816
4336
  const specs = await ready();
@@ -3854,7 +4374,7 @@ function createPaymentGate(options) {
3854
4374
  return rejection(result.error, result.detail);
3855
4375
  }
3856
4376
  await settleTx(ref, true);
3857
- fireOnPaid(result.receipt);
4377
+ await deliverOnPaid(spec, result.receipt);
3858
4378
  return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
3859
4379
  }
3860
4380
  async function verifyExact(exact) {
@@ -3877,7 +4397,8 @@ function createPaymentGate(options) {
3877
4397
  `No \`exact\` rail offered for ${exact.network}${exact.asset ? `/${exact.asset}` : ""} (offered: ${exactSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
3878
4398
  );
3879
4399
  }
3880
- const nonce = exact.payload.authorization.nonce;
4400
+ const auth = "permit2Authorization" in exact.payload ? exact.payload.permit2Authorization : exact.payload.authorization;
4401
+ const nonce = auth.nonce;
3881
4402
  if (await claimTx(nonce)) {
3882
4403
  return rejection("tx_already_used", `Authorization nonce ${nonce} was already redeemed.`);
3883
4404
  }
@@ -3909,7 +4430,7 @@ function createPaymentGate(options) {
3909
4430
  extra: { name: accept.extra.name ?? "", version: accept.extra.version ?? "" }
3910
4431
  },
3911
4432
  receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
3912
- payerHint: exact.payload.authorization.from
4433
+ payerHint: auth.from
3913
4434
  });
3914
4435
  }
3915
4436
  } catch (err) {
@@ -3921,7 +4442,7 @@ function createPaymentGate(options) {
3921
4442
  return rejection(result.error, result.detail);
3922
4443
  }
3923
4444
  await settleTx(nonce, true);
3924
- fireOnPaid(result.receipt);
4445
+ await deliverOnPaid(spec, result.receipt);
3925
4446
  return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
3926
4447
  }
3927
4448
  async function verify(paymentSignature) {
@@ -3974,6 +4495,110 @@ function normaliseHeader(value) {
3974
4495
  if (Array.isArray(value)) return value[0];
3975
4496
  return value;
3976
4497
  }
4498
+
4499
+ // src/receipts.ts
4500
+ var DEFAULT_RETRIES = 5;
4501
+ var DEFAULT_TIMEOUT_MS = 1e4;
4502
+ function defaultBackoff(attempt) {
4503
+ const base2 = Math.min(3e4, 2 ** (attempt - 1) * 500);
4504
+ return Math.round(base2 * (0.5 + Math.random()));
4505
+ }
4506
+ function isRetryableStatus(status) {
4507
+ return status === 408 || status === 429 || status >= 500;
4508
+ }
4509
+ var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
4510
+ async function signBody(secret, body) {
4511
+ const subtle = globalThis.crypto?.subtle;
4512
+ if (!subtle) return null;
4513
+ try {
4514
+ const enc = new TextEncoder();
4515
+ const key = await subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
4516
+ "sign"
4517
+ ]);
4518
+ const sig = await subtle.sign("HMAC", key, enc.encode(body));
4519
+ const hex = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
4520
+ return `sha256=${hex}`;
4521
+ } catch {
4522
+ return null;
4523
+ }
4524
+ }
4525
+ async function deliverReceipt(receipt, options) {
4526
+ const {
4527
+ url,
4528
+ secret,
4529
+ retries = DEFAULT_RETRIES,
4530
+ timeoutMs = DEFAULT_TIMEOUT_MS,
4531
+ headers = {},
4532
+ signatureHeader = "piprail-signature",
4533
+ idempotencyHeader = "idempotency-key",
4534
+ backoff = defaultBackoff,
4535
+ fetchImpl = globalThis.fetch,
4536
+ onAttempt
4537
+ } = options;
4538
+ if (typeof fetchImpl !== "function") {
4539
+ return { delivered: false, attempts: 0, error: "no fetch implementation available" };
4540
+ }
4541
+ const body = JSON.stringify(receipt);
4542
+ const signature = secret ? await signBody(secret, body) : null;
4543
+ const baseHeaders = {
4544
+ ...headers,
4545
+ "content-type": "application/json",
4546
+ [idempotencyHeader]: receipt.idempotencyKey,
4547
+ ...signature ? { [signatureHeader]: signature } : {}
4548
+ };
4549
+ const maxAttempts = Math.max(1, retries + 1);
4550
+ let lastStatus;
4551
+ let lastError;
4552
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4553
+ const controller = new AbortController();
4554
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4555
+ let ok = false;
4556
+ let status;
4557
+ let error;
4558
+ try {
4559
+ const res = await fetchImpl(url, {
4560
+ method: "POST",
4561
+ headers: baseHeaders,
4562
+ body,
4563
+ signal: controller.signal
4564
+ });
4565
+ status = res.status;
4566
+ lastStatus = status;
4567
+ ok = res.ok;
4568
+ if (!ok) {
4569
+ error = `HTTP ${status}`;
4570
+ lastError = error;
4571
+ }
4572
+ } catch (err) {
4573
+ error = err instanceof Error ? err.message : String(err);
4574
+ lastError = error;
4575
+ } finally {
4576
+ clearTimeout(timer);
4577
+ }
4578
+ const retryable = status === void 0 ? true : isRetryableStatus(status);
4579
+ const willRetry = !ok && retryable && attempt < maxAttempts;
4580
+ try {
4581
+ onAttempt?.({ attempt, ok, ...status !== void 0 ? { status } : {}, ...error ? { error } : {}, willRetry });
4582
+ } catch {
4583
+ }
4584
+ if (ok) return { delivered: true, attempts: attempt, status };
4585
+ if (!willRetry) {
4586
+ return {
4587
+ delivered: false,
4588
+ attempts: attempt,
4589
+ ...lastStatus !== void 0 ? { status: lastStatus } : {},
4590
+ ...lastError ? { error: lastError } : {}
4591
+ };
4592
+ }
4593
+ await sleep(backoff(attempt));
4594
+ }
4595
+ return {
4596
+ delivered: false,
4597
+ attempts: maxAttempts,
4598
+ ...lastStatus !== void 0 ? { status: lastStatus } : {},
4599
+ ...lastError ? { error: lastError } : {}
4600
+ };
4601
+ }
3977
4602
  export {
3978
4603
  CHAINS,
3979
4604
  ConfirmationTimeoutError,
@@ -3992,6 +4617,9 @@ export {
3992
4617
  MissingDriverError,
3993
4618
  NoCompatibleAcceptError,
3994
4619
  NonReplayableBodyError,
4620
+ PERMIT2_ADDRESS,
4621
+ PERMIT2_PROXY_CHAIN_IDS,
4622
+ PERMIT2_WITNESS_TYPES,
3995
4623
  PIPRAIL_AGENT_GUIDE,
3996
4624
  PaymentDeclinedError,
3997
4625
  PaymentTimeoutError,
@@ -4004,6 +4632,7 @@ export {
4004
4632
  UnsupportedSchemeError,
4005
4633
  WrongChainError,
4006
4634
  WrongFamilyError,
4635
+ X402_EXACT_PERMIT2_PROXY,
4007
4636
  agentGuide,
4008
4637
  buildBazaarExtension,
4009
4638
  buildChallengeHeader,
@@ -4019,12 +4648,14 @@ export {
4019
4648
  classifyChallenge,
4020
4649
  createPaymentGate,
4021
4650
  decorateOutcome,
4651
+ deliverReceipt,
4022
4652
  eip3009Abi,
4023
4653
  encodeXPaymentHeader,
4024
4654
  evaluatePolicy,
4025
4655
  explainDecline,
4026
4656
  formatSpendReport,
4027
4657
  getDirectoryInfo,
4658
+ isPermit2ProxyChain,
4028
4659
  normalizeNetwork,
4029
4660
  parseChallenge,
4030
4661
  parseExactPaymentHeader,