@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/CHANGELOG.md +115 -0
- package/dist/index.cjs +743 -112
- package/dist/index.d.cts +347 -39
- package/dist/index.d.ts +347 -39
- package/dist/index.js +690 -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,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 [
|
|
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
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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 =
|
|
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 (
|
|
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:
|
|
1197
|
-
abi:
|
|
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
|
|
1230
|
-
//
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
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
|
|
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.
|
|
4155
|
+
if (net.family !== "evm" || asset === "native" || !net.settleExactSelf) {
|
|
3687
4156
|
return void 0;
|
|
3688
4157
|
}
|
|
3689
|
-
const
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
3759
|
-
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
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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,
|