@piprail/sdk 1.13.1 → 1.14.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.
Files changed (30) hide show
  1. package/CHAINS.md +3 -2
  2. package/CHANGELOG.md +39 -0
  3. package/ERRORS.md +2 -1
  4. package/README.md +19 -2
  5. package/STANDARDS.md +2 -0
  6. package/dist/{algorand-WGVF4KTU.js → algorand-7EUZYL2Z.js} +1 -1
  7. package/dist/{algorand-MXUSKX46.cjs → algorand-OIHGJN5S.cjs} +17 -17
  8. package/dist/{aptos-LPBLSEIQ.js → aptos-CDEYDDM5.js} +1 -1
  9. package/dist/{aptos-YT7SXWPF.cjs → aptos-WDWZOU25.cjs} +16 -16
  10. package/dist/{chunk-MDLZJGLY.cjs → chunk-FTKVCP6K.cjs} +24 -13
  11. package/dist/{chunk-SVMGHASK.js → chunk-H3A4KWLJ.js} +12 -1
  12. package/dist/index.cjs +405 -146
  13. package/dist/index.d.cts +209 -31
  14. package/dist/index.d.ts +209 -31
  15. package/dist/index.js +302 -43
  16. package/dist/{near-K6BDBABG.js → near-DT6LRIKB.js} +1 -1
  17. package/dist/{near-7ZDNISUX.cjs → near-FUH3VAXT.cjs} +19 -19
  18. package/dist/{solana-S3UFI3FE.js → solana-3TRYD4QB.js} +1 -1
  19. package/dist/{solana-PU7N2M64.cjs → solana-QUVXPKBZ.cjs} +14 -14
  20. package/dist/{stellar-VDQOFQEO.cjs → stellar-APZEBFAD.cjs} +21 -21
  21. package/dist/{stellar-Q5PO23SC.js → stellar-IK3UML6O.js} +1 -1
  22. package/dist/{sui-FKSMLKRF.cjs → sui-L7BQNJWO.cjs} +17 -17
  23. package/dist/{sui-WOXRKJXS.js → sui-VSE63WQM.js} +1 -1
  24. package/dist/{ton-VK6KRJHP.cjs → ton-5DLKKOFE.cjs} +14 -14
  25. package/dist/{ton-WPTXGLVK.js → ton-QHGQLJX2.js} +1 -1
  26. package/dist/{tron-6GXBXTR4.js → tron-2N2GA62O.js} +1 -1
  27. package/dist/{tron-WLOF5OUV.cjs → tron-HHIT6WKY.cjs} +24 -24
  28. package/dist/{xrpl-HEAPEXAM.js → xrpl-2GZMDYW5.js} +1 -1
  29. package/dist/{xrpl-CMNI25BV.cjs → xrpl-USEG4AHX.cjs} +21 -21
  30. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  SettlementError,
14
14
  UnknownTokenError,
15
15
  UnsupportedNetworkError,
16
+ UnsupportedSchemeError,
16
17
  WrongChainError,
17
18
  WrongFamilyError,
18
19
  floorUnits,
@@ -21,7 +22,7 @@ import {
21
22
  parseUnits,
22
23
  rejectForeignToken,
23
24
  toInsufficientFundsError
24
- } from "./chunk-SVMGHASK.js";
25
+ } from "./chunk-H3A4KWLJ.js";
25
26
 
26
27
  // src/drivers/registry.ts
27
28
  var byFamily = /* @__PURE__ */ new Map();
@@ -97,13 +98,18 @@ var CHAINS = {
97
98
  defaultRpc: "https://ethereum-rpc.publicnode.com",
98
99
  tokens: {
99
100
  USDC: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6, symbol: "USDC" },
100
- USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6, symbol: "USDT" }
101
+ USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6, symbol: "USDT" },
102
+ // Circle EURC — EIP-3009 (exact-payable). On-chain EIP-712 domain name is "Euro Coin" here
103
+ // (NOT "EURC"); the buyer re-derives it on-chain, so the symbol below is display-only.
104
+ EURC: { address: "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", decimals: 6, symbol: "EURC" }
101
105
  }
102
106
  },
103
107
  base: {
104
108
  chain: base,
105
109
  tokens: {
106
- USDC: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6, symbol: "USDC" }
110
+ USDC: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6, symbol: "USDC" },
111
+ // Circle EURC — EIP-3009 (exact-payable). On-chain EIP-712 domain name is "EURC" here.
112
+ EURC: { address: "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42", decimals: 6, symbol: "EURC" }
107
113
  }
108
114
  },
109
115
  arbitrum: {
@@ -139,7 +145,9 @@ var CHAINS = {
139
145
  chain: avalanche,
140
146
  tokens: {
141
147
  USDC: { address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", decimals: 6, symbol: "USDC" },
142
- USDT: { address: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", decimals: 6, symbol: "USDT" }
148
+ USDT: { address: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", decimals: 6, symbol: "USDT" },
149
+ // Circle EURC — EIP-3009 (exact-payable). On-chain EIP-712 domain name is "Euro Coin" here.
150
+ EURC: { address: "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD", decimals: 6, symbol: "EURC" }
143
151
  }
144
152
  },
145
153
  // ── More popular EVM mainnets. Every address below was verified on-chain
@@ -591,6 +599,53 @@ function encodeXPaymentHeader(input) {
591
599
  };
592
600
  return base64(JSON.stringify(payload));
593
601
  }
602
+ async function payExactEvm(input) {
603
+ const { publicClient, walletClient, account, chainId, accept } = input;
604
+ let code;
605
+ try {
606
+ code = await publicClient.getCode({ address: account.address });
607
+ } catch {
608
+ code = void 0;
609
+ }
610
+ if (code && code !== "0x") {
611
+ throw new UnsupportedSchemeError(
612
+ `exact buyer rail requires an EOA signer; ${account.address} is a contract / EIP-1271 / EIP-7702-delegated account (no recoverable ECDSA signature). Pay via onchain-proof.`
613
+ );
614
+ }
615
+ const domain = await readExactDomain(publicClient, accept.asset);
616
+ if (!domain) {
617
+ throw new UnsupportedSchemeError(
618
+ `exact: ${accept.asset} on ${accept.network} isn't an EIP-3009 token (USDT needs Permit2; native coin and plain ERC-20s aren't exact-payable). Pay via onchain-proof.`
619
+ );
620
+ }
621
+ const g = globalThis.crypto;
622
+ if (!g?.getRandomValues) {
623
+ throw new UnsupportedSchemeError(
624
+ "this runtime lacks Web Crypto (globalThis.crypto.getRandomValues); the exact rail needs a CSPRNG nonce."
625
+ );
626
+ }
627
+ const raw = new Uint8Array(32);
628
+ g.getRandomValues(raw);
629
+ const nonce = `0x${[...raw].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
630
+ const now = Math.floor(Date.now() / 1e3);
631
+ const from = account.address;
632
+ const to = getAddress2(accept.payTo);
633
+ const value = accept.amount;
634
+ const validAfter = "0";
635
+ const validBefore = String(now + accept.maxTimeoutSeconds);
636
+ const signature = await walletClient.signTypedData({
637
+ account,
638
+ domain: { name: domain.name, version: domain.version, chainId, verifyingContract: getAddress2(accept.asset) },
639
+ types: EIP3009_TYPES,
640
+ primaryType: "TransferWithAuthorization",
641
+ message: { from, to, value: BigInt(value), validAfter: 0n, validBefore: BigInt(validBefore), nonce }
642
+ });
643
+ return {
644
+ payload: { signature, authorization: { from, to, value, validAfter, validBefore, nonce } },
645
+ payerFrom: from,
646
+ nonce
647
+ };
648
+ }
594
649
  var eip3009Abi = [
595
650
  {
596
651
  type: "function",
@@ -873,6 +928,9 @@ function buildReceiptHeader(receipt) {
873
928
  function buildSignatureHeader(signature) {
874
929
  return toBase64Json(signature);
875
930
  }
931
+ function buildExactSignatureHeader(input) {
932
+ return toBase64Json({ x402Version: 2, accepted: input.accepted, payload: input.payload });
933
+ }
876
934
  async function parseChallenge(response) {
877
935
  const headerValue = response.headers.get(HEADER_REQUIRED);
878
936
  if (headerValue) {
@@ -887,11 +945,24 @@ async function parseChallenge(response) {
887
945
  return null;
888
946
  }
889
947
  function parseReceipt(response) {
890
- const headerValue = response.headers.get(HEADER_RESPONSE);
948
+ const headerValue = response.headers.get(HEADER_RESPONSE) ?? response.headers.get(HEADER_RESPONSE_V1);
891
949
  if (!headerValue) return null;
892
950
  const parsed = fromBase64Json(headerValue);
893
951
  return isValidReceipt(parsed) ? parsed : null;
894
952
  }
953
+ function parseSettleResponse(response) {
954
+ const headerValue = response.headers.get(HEADER_RESPONSE) ?? response.headers.get(HEADER_RESPONSE_V1);
955
+ if (!headerValue) return null;
956
+ const parsed = fromBase64Json(headerValue);
957
+ if (!parsed || typeof parsed !== "object" || typeof parsed.success !== "boolean") return null;
958
+ return {
959
+ success: parsed.success,
960
+ ...typeof parsed.transaction === "string" ? { transaction: parsed.transaction } : {},
961
+ ...typeof parsed.network === "string" ? { network: parsed.network } : {},
962
+ ...typeof parsed.payer === "string" ? { payer: parsed.payer } : {},
963
+ ...typeof parsed.errorReason === "string" ? { errorReason: parsed.errorReason } : {}
964
+ };
965
+ }
895
966
  function parseSignatureHeader(value) {
896
967
  const parsed = fromBase64Json(value);
897
968
  if (!parsed || typeof parsed !== "object") return null;
@@ -1085,6 +1156,15 @@ function makeEvmNetwork(resolved) {
1085
1156
  },
1086
1157
  async estimateCost(accept) {
1087
1158
  const { decimals, symbol } = resolved.chain.nativeCurrency;
1159
+ if (accept.scheme === "exact") {
1160
+ return nativeCost({
1161
+ symbol,
1162
+ decimals,
1163
+ fee: 0n,
1164
+ basis: "estimated",
1165
+ detail: "gasless \u2014 the server/facilitator settles the signed authorization"
1166
+ });
1167
+ }
1088
1168
  const gasLimit = accept.asset === "native" ? 21000n : 65000n;
1089
1169
  try {
1090
1170
  const gasPrice = await publicClient.getGasPrice();
@@ -1146,6 +1226,21 @@ function makeEvmNetwork(resolved) {
1146
1226
  minConfirmations: accept.extra.minConfirmations
1147
1227
  });
1148
1228
  },
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.
1233
+ async payExact(wallet, accept) {
1234
+ const a = wallet._native;
1235
+ const { payload, payerFrom, nonce } = await payExactEvm({
1236
+ publicClient,
1237
+ walletClient: a.walletClient,
1238
+ account: a.account,
1239
+ chainId: resolved.chainId,
1240
+ accept
1241
+ });
1242
+ return { payload, accepted: accept, payerFrom, nonce };
1243
+ },
1149
1244
  // Standard x402 `exact` rail (EIP-3009), seller side — EVM only.
1150
1245
  async exactDomain(asset) {
1151
1246
  return readExactDomain(publicClient, asset);
@@ -1176,7 +1271,7 @@ var loaders = {
1176
1271
  solana: async () => {
1177
1272
  let mod;
1178
1273
  try {
1179
- mod = await import("./solana-S3UFI3FE.js");
1274
+ mod = await import("./solana-3TRYD4QB.js");
1180
1275
  } catch (cause) {
1181
1276
  throw new MissingDriverError(
1182
1277
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -1188,7 +1283,7 @@ var loaders = {
1188
1283
  ton: async () => {
1189
1284
  let mod;
1190
1285
  try {
1191
- mod = await import("./ton-WPTXGLVK.js");
1286
+ mod = await import("./ton-QHGQLJX2.js");
1192
1287
  } catch (cause) {
1193
1288
  throw new MissingDriverError(
1194
1289
  `TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
@@ -1200,7 +1295,7 @@ var loaders = {
1200
1295
  stellar: async () => {
1201
1296
  let mod;
1202
1297
  try {
1203
- mod = await import("./stellar-Q5PO23SC.js");
1298
+ mod = await import("./stellar-IK3UML6O.js");
1204
1299
  } catch (cause) {
1205
1300
  throw new MissingDriverError(
1206
1301
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
@@ -1212,7 +1307,7 @@ var loaders = {
1212
1307
  xrpl: async () => {
1213
1308
  let mod;
1214
1309
  try {
1215
- mod = await import("./xrpl-HEAPEXAM.js");
1310
+ mod = await import("./xrpl-2GZMDYW5.js");
1216
1311
  } catch (cause) {
1217
1312
  throw new MissingDriverError(
1218
1313
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -1224,7 +1319,7 @@ var loaders = {
1224
1319
  tron: async () => {
1225
1320
  let mod;
1226
1321
  try {
1227
- mod = await import("./tron-6GXBXTR4.js");
1322
+ mod = await import("./tron-2N2GA62O.js");
1228
1323
  } catch (cause) {
1229
1324
  throw new MissingDriverError(
1230
1325
  `Tron selected, but its package isn't installed. Run: npm install tronweb`,
@@ -1236,7 +1331,7 @@ var loaders = {
1236
1331
  sui: async () => {
1237
1332
  let mod;
1238
1333
  try {
1239
- mod = await import("./sui-WOXRKJXS.js");
1334
+ mod = await import("./sui-VSE63WQM.js");
1240
1335
  } catch (cause) {
1241
1336
  throw new MissingDriverError(
1242
1337
  `Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
@@ -1248,7 +1343,7 @@ var loaders = {
1248
1343
  near: async () => {
1249
1344
  let mod;
1250
1345
  try {
1251
- mod = await import("./near-K6BDBABG.js");
1346
+ mod = await import("./near-DT6LRIKB.js");
1252
1347
  } catch (cause) {
1253
1348
  throw new MissingDriverError(
1254
1349
  `NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
@@ -1260,7 +1355,7 @@ var loaders = {
1260
1355
  aptos: async () => {
1261
1356
  let mod;
1262
1357
  try {
1263
- mod = await import("./aptos-LPBLSEIQ.js");
1358
+ mod = await import("./aptos-CDEYDDM5.js");
1264
1359
  } catch (cause) {
1265
1360
  throw new MissingDriverError(
1266
1361
  `Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
@@ -1272,7 +1367,7 @@ var loaders = {
1272
1367
  algorand: async () => {
1273
1368
  let mod;
1274
1369
  try {
1275
- mod = await import("./algorand-WGVF4KTU.js");
1370
+ mod = await import("./algorand-7EUZYL2Z.js");
1276
1371
  } catch (cause) {
1277
1372
  throw new MissingDriverError(
1278
1373
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
@@ -1859,6 +1954,7 @@ var SpendLedger = class {
1859
1954
  };
1860
1955
 
1861
1956
  // src/client.ts
1957
+ var DEFAULT_SCHEMES = ["onchain-proof"];
1862
1958
  var RECIPIENT_FIX = {
1863
1959
  NO_TRUSTLINE: "the recipient needs a one-time trustline for this asset before it can receive",
1864
1960
  NOT_REGISTERED: "the recipient must be storage_deposit-registered on this token (NEP-145, one-time)",
@@ -1901,6 +1997,11 @@ var PipRailClient = class {
1901
1997
  return { net, wallet };
1902
1998
  })();
1903
1999
  }
2000
+ /** Resolve the effective scheme set: a per-call override, else the constructor's
2001
+ * `schemes`, else the `onchain-proof`-only default. */
2002
+ resolveSchemes(perCall) {
2003
+ return perCall ?? this.opts.schemes ?? DEFAULT_SCHEMES;
2004
+ }
1904
2005
  /** GET that auto-handles 402. Pass a full URL to any x402-gated endpoint. */
1905
2006
  get(url, init) {
1906
2007
  return this.fetch(url, { ...init ?? {}, method: "GET" });
@@ -1945,7 +2046,7 @@ var PipRailClient = class {
1945
2046
  async quote(url, init) {
1946
2047
  const res = await fetch(url, { ...init ?? {}, method: init?.method ?? "GET" });
1947
2048
  if (res.status !== 402) return null;
1948
- const { quote } = await this.resolveChallenge(url, res);
2049
+ const { quote } = await this.resolveChallenge(url, res, this.resolveSchemes());
1949
2050
  return quote;
1950
2051
  }
1951
2052
  /**
@@ -1964,7 +2065,7 @@ var PipRailClient = class {
1964
2065
  async estimateCost(url, init) {
1965
2066
  const res = await fetch(url, { ...init ?? {}, method: init?.method ?? "GET" });
1966
2067
  if (res.status !== 402) return null;
1967
- const { net, accept, quote } = await this.resolveChallenge(url, res);
2068
+ const { net, accept, quote } = await this.resolveChallenge(url, res, this.resolveSchemes());
1968
2069
  const cost = await net.estimateCost(accept);
1969
2070
  return { quote, cost };
1970
2071
  }
@@ -2000,7 +2101,7 @@ var PipRailClient = class {
2000
2101
  throw new InvalidEnvelopeError("402 response did not include a parseable x402 challenge.");
2001
2102
  }
2002
2103
  const { net, wallet } = await this.ensure();
2003
- return this.planFromChallenge(net, wallet, challenge, url);
2104
+ return this.planFromChallenge(net, wallet, challenge, url, this.resolveSchemes());
2004
2105
  }
2005
2106
  /**
2006
2107
  * Convenience over {@link planPayment}: can the wallet settle this URL right now?
@@ -2028,8 +2129,8 @@ var PipRailClient = class {
2028
2129
  * - A resource just listed via {@link register} may not appear yet — 402 Index reviews
2029
2130
  * before publishing, so retry with a brief backoff if a fresh listing is missing.
2030
2131
  * - Results are cross-scheme (mostly the mainstream `exact` scheme); `fetch()` pays
2031
- * only `onchain-proof` rails directly (pay `exact` resources with the experimental
2032
- * `drivers/evm/exact.ts`).
2132
+ * `onchain-proof` rails by default, and standard `exact` rails too once you opt in
2133
+ * with `schemes: ['onchain-proof', 'exact']` (EVM + EIP-3009 — USDC/EURC).
2033
2134
  */
2034
2135
  async discover(opts = {}) {
2035
2136
  const found = await searchOpenIndexes({
@@ -2168,13 +2269,14 @@ var PipRailClient = class {
2168
2269
  }
2169
2270
  const firstResponse = await fetch(url, init);
2170
2271
  if (firstResponse.status !== 402) return firstResponse;
2171
- const resolved = await this.resolveChallenge(url, firstResponse);
2272
+ const schemes = this.resolveSchemes(init?.schemes);
2273
+ const resolved = await this.resolveChallenge(url, firstResponse, schemes);
2172
2274
  const { net, wallet, challenge } = resolved;
2173
2275
  let accept = resolved.accept;
2174
2276
  let quote = resolved.quote;
2175
2277
  const autoRoute = init?.autoRoute ?? this.opts.autoRoute ?? false;
2176
2278
  if (autoRoute) {
2177
- const plan = await this.planFromChallenge(net, wallet, challenge, url);
2279
+ const plan = await this.planFromChallenge(net, wallet, challenge, url, schemes);
2178
2280
  if (!plan.best) {
2179
2281
  throw new PaymentDeclinedError(plan.fundingHint ?? "No rail is settleable for this payment.");
2180
2282
  }
@@ -2183,6 +2285,9 @@ var PipRailClient = class {
2183
2285
  }
2184
2286
  this.safeEmit({ kind: "payment-required", challenge, accept });
2185
2287
  await this.authorize(quote);
2288
+ if (accept.scheme === "exact") {
2289
+ return this.payExactRail(net, wallet, accept, url, init, quote);
2290
+ }
2186
2291
  const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
2187
2292
  const response = await this.retryWithProof(url, init, accept, ref, confirmed);
2188
2293
  this.recordSpend(quote, ref);
@@ -2194,7 +2299,7 @@ var PipRailClient = class {
2194
2299
  * network, pick the accept the client can pay, and build its quote. Shared by
2195
2300
  * `quote()` (read-only) and `fetch()` (which then authorises + pays).
2196
2301
  */
2197
- async resolveChallenge(url, response) {
2302
+ async resolveChallenge(url, response, schemes) {
2198
2303
  const challenge = await parseChallenge(response);
2199
2304
  if (!challenge) {
2200
2305
  throw new InvalidEnvelopeError(
@@ -2202,11 +2307,29 @@ var PipRailClient = class {
2202
2307
  );
2203
2308
  }
2204
2309
  const { net, wallet } = await this.ensure();
2205
- const candidates = this.gatherCandidates(net, challenge);
2310
+ const candidates = this.gatherCandidates(net, challenge, schemes);
2206
2311
  if (candidates.length === 0) {
2207
- const networks = challenge.accepts.map((a) => a.network).join(", ");
2312
+ const exactOnNet = challenge.accepts.some(
2313
+ (a) => a.scheme === "exact" && net.supports(a.network)
2314
+ );
2315
+ if (schemes.includes("exact") && exactOnNet && typeof net.payExact !== "function") {
2316
+ throw new UnsupportedSchemeError(
2317
+ `This 402 offers a standard 'exact' rail on ${net.network}, but the ${net.family} family can't pay 'exact' (EVM + EIP-3009 only), and no 'onchain-proof' rail was offered.`
2318
+ );
2319
+ }
2320
+ if (!schemes.includes("exact") && exactOnNet && typeof net.payExact === "function") {
2321
+ const payable = challenge.accepts.some(
2322
+ (a) => a.scheme === "exact" && net.supports(a.network) && net.describeAsset(a.asset) != null
2323
+ );
2324
+ if (payable) {
2325
+ throw new NoCompatibleAcceptError(
2326
+ `This 402 is payable only via the standard 'exact' rail on ${net.network}, which is OFF by default. Enable it: new PipRailClient({ \u2026, schemes: ['onchain-proof', 'exact'] }) or per call fetch(url, { schemes: ['exact'] }) (MCP: PIPRAIL_SCHEMES=onchain-proof,exact).`
2327
+ );
2328
+ }
2329
+ }
2330
+ const networks = [...new Set(challenge.accepts.map((a) => a.network))].join(", ");
2208
2331
  throw new NoCompatibleAcceptError(
2209
- `No accepts[] entry for ${net.network} (challenge offered: ${networks || "none"}).`
2332
+ `No accepts[] entry payable by this client on ${net.network} (schemes: ${schemes.join(", ")}; challenge offered: ${networks || "none"}).`
2210
2333
  );
2211
2334
  }
2212
2335
  const priced = candidates.map((accept) => ({
@@ -2216,20 +2339,37 @@ var PipRailClient = class {
2216
2339
  const chosen = priced.find((p) => p.quote.withinPolicy) ?? priced[0];
2217
2340
  return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
2218
2341
  }
2219
- /** The candidate accepts this client could pay: our scheme, on the bound network.
2220
- * A dual-advertised challenge may also carry standard `exact` rails the PipRail
2221
- * client ignores those (it pays the backendless `onchain-proof` rail); the type
2222
- * predicate narrows the `X402AnyAccept` union to the rails we settle. */
2223
- gatherCandidates(net, challenge) {
2224
- return challenge.accepts.filter(
2225
- (a) => a.scheme === "onchain-proof" && net.supports(a.network)
2226
- );
2342
+ /** The candidate accepts this client could pay, on the bound network. Always the
2343
+ * backendless `onchain-proof` rails; PLUS standard `exact` rails when `schemes`
2344
+ * enables them AND the driver can settle them (EVM `payExact` + a recognised
2345
+ * EIP-3009 token). `onchain-proof` is gathered FIRST so default selection is
2346
+ * unchanged when `exact` is off. */
2347
+ gatherCandidates(net, challenge, schemes) {
2348
+ const out = [];
2349
+ if (schemes.includes("onchain-proof")) {
2350
+ out.push(
2351
+ ...challenge.accepts.filter(
2352
+ (a) => a.scheme === "onchain-proof" && net.supports(a.network)
2353
+ )
2354
+ );
2355
+ }
2356
+ if (schemes.includes("exact")) {
2357
+ out.push(
2358
+ ...challenge.accepts.filter(
2359
+ (a) => a.scheme === "exact" && net.supports(a.network) && typeof net.payExact === "function" && net.describeAsset(a.asset) != null && // a foreign rail's maxTimeoutSeconds must be a usable positive integer, or
2360
+ // signing it would build a NaN/garbage validBefore — drop it silently
2361
+ // (symmetric with an unrecognised token) rather than leak a raw SyntaxError.
2362
+ Number.isInteger(a.maxTimeoutSeconds) && a.maxTimeoutSeconds > 0
2363
+ )
2364
+ );
2365
+ }
2366
+ return out;
2227
2367
  }
2228
2368
  /** Build the full {@link PaymentPlan} from an already-parsed challenge + bound
2229
2369
  * net/wallet. Shared by `planPayment` (read-only) and `fetch`'s autoRoute. */
2230
- async planFromChallenge(net, wallet, challenge, url) {
2370
+ async planFromChallenge(net, wallet, challenge, url, schemes) {
2231
2371
  const chainLabel = typeof this.opts.chain === "string" ? this.opts.chain : net.network;
2232
- const candidates = this.gatherCandidates(net, challenge);
2372
+ const candidates = this.gatherCandidates(net, challenge, schemes);
2233
2373
  if (candidates.length === 0) {
2234
2374
  const offered = [...new Set(challenge.accepts.map((a) => a.network))].join(", ") || "none";
2235
2375
  return {
@@ -2269,17 +2409,23 @@ var PipRailClient = class {
2269
2409
  const rr = await net.recipientReady(accept.payTo, accept.asset).catch(() => ({ ready: "unknown" }));
2270
2410
  const amount = BigInt(accept.amount);
2271
2411
  const fee = safeBig(cost.fee);
2412
+ const isExact = accept.scheme === "exact";
2272
2413
  const isNative = accept.asset === "native";
2273
2414
  const blockers = [];
2274
2415
  const warnings = [];
2275
2416
  const shortfall = {};
2276
2417
  if (!quote.withinPolicy) blockers.push("OUTSIDE_POLICY");
2277
2418
  if (quote.symbolMismatch) warnings.push("SYMBOL_MISMATCH");
2278
- if (cost.basis === "heuristic") warnings.push("GAS_HEURISTIC");
2419
+ if (!isExact && cost.basis === "heuristic") warnings.push("GAS_HEURISTIC");
2279
2420
  const tokenKnown = bal.token != null;
2280
2421
  const nativeKnown = bal.native != null;
2281
- if (!tokenKnown || !nativeKnown) warnings.push("BALANCE_UNREADABLE");
2282
- if (isNative) {
2422
+ if (isExact ? !tokenKnown : !tokenKnown || !nativeKnown) warnings.push("BALANCE_UNREADABLE");
2423
+ if (isExact) {
2424
+ if (tokenKnown && bal.token < amount) {
2425
+ blockers.push("INSUFFICIENT_TOKEN");
2426
+ shortfall.token = formatUnits(amount - bal.token, quote.decimals);
2427
+ }
2428
+ } else if (isNative) {
2283
2429
  if (nativeKnown && bal.native < amount + fee) {
2284
2430
  blockers.push("INSUFFICIENT_TOKEN");
2285
2431
  shortfall.token = formatUnits(amount + fee - bal.native, quote.decimals);
@@ -2306,7 +2452,7 @@ var PipRailClient = class {
2306
2452
  } else {
2307
2453
  recipient = { ready: rr.ready };
2308
2454
  }
2309
- const unreadable = isNative ? !nativeKnown : !tokenKnown || !nativeKnown;
2455
+ const unreadable = isExact ? !tokenKnown : isNative ? !nativeKnown : !tokenKnown || !nativeKnown;
2310
2456
  const state = blockers.length ? "blocked" : unreadable || rr.ready === "unknown" ? "unknown" : "payable";
2311
2457
  return {
2312
2458
  accept,
@@ -2335,6 +2481,11 @@ var PipRailClient = class {
2335
2481
  const amountBase = BigInt(accept.amount);
2336
2482
  const described = net.describeAsset(accept.asset);
2337
2483
  const decimals = described?.decimals ?? accept.extra.decimals;
2484
+ if (decimals === void 0) {
2485
+ throw new InvalidEnvelopeError(
2486
+ `challenge for ${accept.asset} on ${accept.network} states no decimals and the SDK doesn't recognise the token \u2014 refusing to price it.`
2487
+ );
2488
+ }
2338
2489
  const symbol = described?.symbol ?? accept.extra.symbol;
2339
2490
  const amountFormatted = formatUnits(amountBase, decimals);
2340
2491
  const intent = {
@@ -2495,7 +2646,102 @@ var PipRailClient = class {
2495
2646
  { ref }
2496
2647
  );
2497
2648
  }
2649
+ /**
2650
+ * Pay a standard x402 `exact` rail — a SEPARATE, fundamentally more conservative
2651
+ * path than {@link retryWithProof}. The buyer SIGNS an EIP-3009 authorization ONCE
2652
+ * (the driver's `payExact`) and the server / merchant-chosen facilitator BROADCASTS
2653
+ * it synchronously, so a blind re-POST of a still-in-flight authorization could
2654
+ * double-BROADCAST it. Hence, unlike the onchain-proof loop:
2655
+ *
2656
+ * • sign exactly once — reuse the SAME header on every retry, never re-sign;
2657
+ * • retry ONLY an explicit 402 (a definitive pre-broadcast rejection), bounded
2658
+ * well under `maxTimeoutSeconds` so the loop can't outlive the authorization;
2659
+ * • a post-POST transport error/timeout → {@link PaymentTimeoutError} carrying the
2660
+ * nonce (the facilitator MAY have settled — verify on-chain, NEVER re-pay);
2661
+ * • a 5xx → return as-is (server settle failure; the authorization stays valid +
2662
+ * its nonce unused) — no settled event, no spend;
2663
+ * • a 200 whose SettleResponse says `success:false` → a rejection, NEVER a spend;
2664
+ * • the spend is recorded EXACTLY ONCE, on an affirmative settlement only.
2665
+ */
2666
+ async payExactRail(net, wallet, accept, url, init, quote) {
2667
+ if (!net.payExact) {
2668
+ throw new UnsupportedSchemeError(
2669
+ `the ${net.family} family can't pay a standard 'exact' rail (EVM + EIP-3009 only).`
2670
+ );
2671
+ }
2672
+ throwIfAborted(init?.signal);
2673
+ const { payload, accepted, payerFrom, nonce } = await net.payExact(wallet, accept);
2674
+ const headers = new Headers(init?.headers);
2675
+ headers.set(HEADER_SIGNATURE, buildExactSignatureHeader({ accepted, payload }));
2676
+ const rejectDefinitive = (why2) => {
2677
+ this.safeEmit({ kind: "payment-failed", reason: `exact: facilitator rejected nonce=${nonce} (${why2})` });
2678
+ throw new MaxRetriesExceededError(
2679
+ `exact: the facilitator rejected the payment (${why2}). Fix the cause, then re-present the SAME signed authorization (nonce=${nonce}) \u2014 do NOT re-sign a fresh nonce. ref=${nonce}.`,
2680
+ { ref: nonce }
2681
+ );
2682
+ };
2683
+ const deadline = Date.now() + Math.max(1, Math.floor(accept.maxTimeoutSeconds / 2)) * 1e3;
2684
+ const maxAttempts = Math.min(this.maxRetries, 3);
2685
+ let lastReason = null;
2686
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
2687
+ if (attempt > 0) {
2688
+ if (Date.now() >= deadline) break;
2689
+ await new Promise((r) => setTimeout(r, Math.min(2e3, 400 * 2 ** (attempt - 1))));
2690
+ }
2691
+ throwIfAborted(init?.signal);
2692
+ const budget = Math.min(this.retryTimeoutMs, deadline - Date.now());
2693
+ if (budget <= 0) break;
2694
+ const timeoutController = new AbortController();
2695
+ const timeoutId = setTimeout(() => timeoutController.abort(), budget);
2696
+ const signal = init?.signal && typeof AbortSignal.any === "function" ? AbortSignal.any([timeoutController.signal, init.signal]) : timeoutController.signal;
2697
+ let response;
2698
+ try {
2699
+ response = await fetch(url, { ...init ?? {}, headers, signal });
2700
+ } catch (err) {
2701
+ throw new PaymentTimeoutError(
2702
+ `exact: no response after submitting the authorization (nonce=${nonce}) to ${hostOf2(url)}. The facilitator may have already settled it \u2014 verify on-chain with authorizationState(${payerFrom}, ${nonce}) before re-presenting; do NOT re-pay.`,
2703
+ { cause: err, ref: nonce }
2704
+ );
2705
+ } finally {
2706
+ clearTimeout(timeoutId);
2707
+ }
2708
+ const settle = parseSettleResponse(response);
2709
+ if (response.status === 402) {
2710
+ if (settle && settle.success === false) rejectDefinitive(settle.errorReason ?? "the facilitator reported success:false");
2711
+ lastReason = await readInvalidReason(response) ?? lastReason;
2712
+ continue;
2713
+ }
2714
+ if (response.ok && !(settle && settle.success === false)) {
2715
+ const receipt = parseReceipt(response);
2716
+ this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
2717
+ const ref = settle?.transaction || receipt?.transaction || `eip3009-nonce:${nonce}`;
2718
+ this.recordSpend(quote, ref);
2719
+ return response;
2720
+ }
2721
+ if (response.status >= 500) {
2722
+ this.safeEmit({ kind: "payment-failed", reason: `exact: server ${response.status} \u2014 authorization nonce=${nonce} not settled` });
2723
+ return response;
2724
+ }
2725
+ if (settle && settle.success === false) rejectDefinitive(settle.errorReason ?? "the facilitator reported success:false");
2726
+ this.safeEmit({ kind: "payment-failed", reason: `exact: server ${response.status} \u2014 authorization nonce=${nonce} not settled` });
2727
+ return response;
2728
+ }
2729
+ const why = lastReason ? `${lastReason.error}${lastReason.detail ? ` \u2014 ${lastReason.detail}` : ""}` : "server gave no reason";
2730
+ this.safeEmit({
2731
+ kind: "payment-failed",
2732
+ reason: `exact: 402 after submitting authorization nonce=${nonce} (${why})`
2733
+ });
2734
+ throw new MaxRetriesExceededError(
2735
+ `exact: server still returned 402 after submitting the signed authorization (nonce=${nonce}). Last rejection: ${why}. Re-present the SAME authorization \u2014 do NOT re-sign a fresh nonce; verify authorizationState(${payerFrom}, ${nonce}) first. ref=${nonce}.`,
2736
+ { ref: nonce }
2737
+ );
2738
+ }
2498
2739
  };
2740
+ function throwIfAborted(signal) {
2741
+ if (signal?.aborted) {
2742
+ throw signal.reason ?? new DOMException("This operation was aborted.", "AbortError");
2743
+ }
2744
+ }
2499
2745
  function safeBig(s) {
2500
2746
  try {
2501
2747
  return BigInt(s);
@@ -2596,8 +2842,16 @@ async function readInvalidReason(response) {
2596
2842
  detail: typeof body.detail === "string" ? body.detail : ""
2597
2843
  };
2598
2844
  }
2845
+ if (body && body.isValid === false && typeof body.invalidReason === "string") {
2846
+ return {
2847
+ error: body.invalidReason,
2848
+ detail: typeof body.invalidMessage === "string" ? body.invalidMessage : ""
2849
+ };
2850
+ }
2599
2851
  } catch {
2600
2852
  }
2853
+ const settle = parseSettleResponse(response);
2854
+ if (settle?.errorReason) return { error: settle.errorReason, detail: "" };
2601
2855
  return null;
2602
2856
  }
2603
2857
 
@@ -2726,17 +2980,17 @@ function paymentTools(client) {
2726
2980
  },
2727
2981
  {
2728
2982
  name: "piprail_pay_request",
2729
- description: "Fetch an x402 payment-gated URL, automatically paying the required on-chain payment if needed (subject to the spend policy + approval hook). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
2983
+ description: "Fetch an x402 payment-gated URL, automatically making the required payment if needed (subject to the spend policy + approval hook). Pays whichever rail the client is configured for \u2014 PipRail's backendless on-chain rail, or, when enabled, the standard `exact` rail (where the buyer signs and the server settles, so no buyer gas). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
2730
2984
  annotations: {
2731
2985
  title: "Pay an x402 request",
2732
2986
  readOnlyHint: false,
2733
2987
  // this is the one tool that MOVES FUNDS
2734
2988
  destructiveHint: true,
2735
- // an on-chain payment is value-moving and not reversible
2989
+ // a payment is value-moving and not reversible
2736
2990
  idempotentHint: false,
2737
2991
  // paying twice = two payments
2738
2992
  openWorldHint: true
2739
- // fetches a URL and settles on-chain
2993
+ // fetches a URL and settles a payment
2740
2994
  },
2741
2995
  parameters: {
2742
2996
  type: "object",
@@ -3278,7 +3532,9 @@ function createPaymentGate(options) {
3278
3532
  amount: accept.amount,
3279
3533
  payTo: accept.payTo,
3280
3534
  maxTimeoutSeconds: accept.maxTimeoutSeconds,
3281
- extra: { name: accept.extra.name, version: accept.extra.version }
3535
+ // name/version are OPTIONAL on the wire type (a foreign rail may omit them), but the
3536
+ // gate's OWN exact rail always read them on-chain at resolution — so they're present here.
3537
+ extra: { name: accept.extra.name ?? "", version: accept.extra.version ?? "" }
3282
3538
  },
3283
3539
  receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
3284
3540
  payerHint: exact.payload.authorization.from
@@ -3372,11 +3628,13 @@ export {
3372
3628
  SettlementError,
3373
3629
  UnknownTokenError,
3374
3630
  UnsupportedNetworkError,
3631
+ UnsupportedSchemeError,
3375
3632
  WrongChainError,
3376
3633
  WrongFamilyError,
3377
3634
  buildBazaarExtension,
3378
3635
  buildChallengeHeader,
3379
3636
  buildExactAuthorization,
3637
+ buildExactSignatureHeader,
3380
3638
  buildOpenApi,
3381
3639
  buildReceiptHeader,
3382
3640
  buildSignatureHeader,
@@ -3395,6 +3653,7 @@ export {
3395
3653
  parseExactPaymentHeader,
3396
3654
  parseExactRequirements,
3397
3655
  parseReceipt,
3656
+ parseSettleResponse,
3398
3657
  parseSignatureHeader,
3399
3658
  paymentTools,
3400
3659
  pickAccept,
@@ -7,7 +7,7 @@ import {
7
7
  nativeCost,
8
8
  rejectForeignToken,
9
9
  toInsufficientFundsError
10
- } from "./chunk-SVMGHASK.js";
10
+ } from "./chunk-H3A4KWLJ.js";
11
11
 
12
12
  // src/drivers/near/index.ts
13
13
  import { JsonRpcProvider, Account, actions } from "near-api-js";