@piprail/sdk 2.1.1 → 2.2.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 CHANGED
@@ -4,6 +4,30 @@ All notable changes to `@piprail/sdk` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  versions follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [2.2.0] — 2026-06-16 — broader x402 ecosystem interoperability
8
+
9
+ Both changes are additive and backward-compatible — defaults and the zero-config 402 stay
10
+ byte-identical; only previously-skipped cases become newly handled.
11
+
12
+ ### Changed
13
+
14
+ - **The `exact` buyer matches a rail's network whether it's a CAIP-2 id _or_ a chain slug.** The pay
15
+ path now normalizes the offered network (`eip155:8453`, `base`, `bsc`, `56`, …) before matching it to
16
+ the bound chain — the same normalization discovery already used — so a PipRail client interoperates
17
+ with any x402 server or facilitator regardless of how it labels the network. Strictly additive: a
18
+ CAIP-2 label behaves exactly as before; only a chain slug that resolves to the **bound** chain becomes
19
+ newly payable (a different-chain or unrecognized label is never selected, and the trusted EIP-712
20
+ domain still fixes the chain id at signing). Fixes `exact` rails that some facilitators label by slug
21
+ being silently unpayable.
22
+
23
+ ### Added
24
+
25
+ - **`facilitatorCoverage()` / `parseFacilitatorSupported()` surface two optional per-kind fields** —
26
+ `x402Version` and `assetTransferMethod` (`'eip3009' | 'permit2'`) — when a facilitator's `GET
27
+ /supported` advertises them, so coverage can tell a v1 rail from a v2 rail and an EIP-3009 kind from a
28
+ Permit2 one. Omitted entirely when absent (no `undefined` keys), so a facilitator reporting neither
29
+ parses exactly as before.
30
+
7
31
  ## [2.1.1] — 2026-06-15 — discoverability polish (post-2.1.0 audit)
8
32
 
9
33
  A consistency patch from a deep docs↔source audit of the 2.1.0 discoverability surface. No
@@ -1196,6 +1220,7 @@ straight into your wallet. The API is small and self-contained.
1196
1220
  to your wallet; PipRail never holds funds.
1197
1221
  - `viem ^2.21` is a peer dependency. Node 20+ or a modern browser.
1198
1222
 
1223
+ [2.2.0]: https://www.npmjs.com/package/@piprail/sdk
1199
1224
  [2.1.1]: https://www.npmjs.com/package/@piprail/sdk
1200
1225
  [2.1.0]: https://www.npmjs.com/package/@piprail/sdk
1201
1226
  [2.0.2]: https://www.npmjs.com/package/@piprail/sdk
package/dist/index.cjs CHANGED
@@ -3220,7 +3220,7 @@ var PipRailClient = (_class2 = class {
3220
3220
  const candidates = this.gatherCandidates(net, challenge, schemes);
3221
3221
  if (candidates.length === 0) {
3222
3222
  const exactOnNet = challenge.accepts.some(
3223
- (a) => a.scheme === "exact" && net.supports(a.network)
3223
+ (a) => a.scheme === "exact" && this.supportsNetwork(net, a.network)
3224
3224
  );
3225
3225
  if (schemes.includes("exact") && exactOnNet && typeof net.payExact !== "function") {
3226
3226
  throw new (0, _chunkJG6KRAW6cjs.UnsupportedSchemeError)(
@@ -3229,7 +3229,7 @@ var PipRailClient = (_class2 = class {
3229
3229
  }
3230
3230
  if (!schemes.includes("exact") && exactOnNet && typeof net.payExact === "function") {
3231
3231
  const payable = challenge.accepts.some(
3232
- (a) => a.scheme === "exact" && net.supports(a.network) && net.describeAsset(a.asset) != null
3232
+ (a) => a.scheme === "exact" && this.supportsNetwork(net, a.network) && net.describeAsset(a.asset) != null
3233
3233
  );
3234
3234
  if (payable) {
3235
3235
  throw new (0, _chunkJG6KRAW6cjs.NoCompatibleAcceptError)(
@@ -3249,6 +3249,17 @@ var PipRailClient = (_class2 = class {
3249
3249
  const chosen = _nullishCoalesce(priced.find((p) => p.quote.withinPolicy), () => ( priced[0]));
3250
3250
  return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
3251
3251
  }
3252
+ /** Match a foreign-supplied network string against the bound driver, tolerating a
3253
+ * SLUG ('bsc', 'base', '56') the SAME way discovery's `railOnNetwork` already does —
3254
+ * normalize to CAIP-2 first, since a foreign/AEON/community 402 may label the network
3255
+ * with a slug (AEON serves v1 duplicate kinds '56'/'bsc'). ADDITIVE: a value that's
3256
+ * already CAIP-2 passes through `normalizeNetwork` UNCHANGED, so every existing
3257
+ * exact-CAIP-2 match is byte-identical; only slugs resolving to the bound chain become
3258
+ * newly matchable (an unknown slug stays unresolved → still unmatched; a different
3259
+ * chain's slug resolves elsewhere → still unmatched). */
3260
+ supportsNetwork(net, network) {
3261
+ return net.supports(normalizeNetwork(network));
3262
+ }
3252
3263
  /** The candidate accepts this client could pay, on the bound network. Always the
3253
3264
  * backendless `onchain-proof` rails; PLUS standard `exact` rails when `schemes`
3254
3265
  * enables them AND the driver can settle them (EVM `payExact` + a recognised
@@ -3259,14 +3270,14 @@ var PipRailClient = (_class2 = class {
3259
3270
  if (schemes.includes("onchain-proof")) {
3260
3271
  out.push(
3261
3272
  ...challenge.accepts.filter(
3262
- (a) => a.scheme === "onchain-proof" && net.supports(a.network)
3273
+ (a) => a.scheme === "onchain-proof" && this.supportsNetwork(net, a.network)
3263
3274
  )
3264
3275
  );
3265
3276
  }
3266
3277
  if (schemes.includes("exact")) {
3267
3278
  out.push(
3268
3279
  ...challenge.accepts.filter(
3269
- (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
3280
+ (a) => a.scheme === "exact" && this.supportsNetwork(net, a.network) && typeof net.payExact === "function" && net.describeAsset(a.asset) != null && // a foreign rail's maxTimeoutSeconds must be a usable positive integer, or
3270
3281
  // signing it would build a NaN/garbage validBefore — drop it silently
3271
3282
  // (symmetric with an unrecognised token) rather than leak a raw SyntaxError.
3272
3283
  Number.isInteger(a.maxTimeoutSeconds) && a.maxTimeoutSeconds > 0
@@ -3508,7 +3519,7 @@ var PipRailClient = (_class2 = class {
3508
3519
  );
3509
3520
  }
3510
3521
  async payAndConfirm(net, wallet, accept) {
3511
- if (!net.supports(accept.network)) {
3522
+ if (!this.supportsNetwork(net, accept.network)) {
3512
3523
  throw new (0, _chunkJG6KRAW6cjs.WrongChainError)(
3513
3524
  `Challenge expects ${accept.network} but client is on ${net.network}.`
3514
3525
  );
@@ -4761,7 +4772,15 @@ function parseFacilitatorSupported(body) {
4761
4772
  const o = k;
4762
4773
  if (typeof o.scheme !== "string" || typeof o.network !== "string") continue;
4763
4774
  const fp = _optionalChain([o, 'access', _77 => _77.extra, 'optionalAccess', _78 => _78.feePayer]);
4764
- out.push({ scheme: o.scheme, network: o.network, ...typeof fp === "string" ? { feePayer: fp } : {} });
4775
+ const ver = o.x402Version;
4776
+ const method = _optionalChain([o, 'access', _79 => _79.extra, 'optionalAccess', _80 => _80.assetTransferMethod]);
4777
+ out.push({
4778
+ scheme: o.scheme,
4779
+ network: o.network,
4780
+ ...typeof fp === "string" ? { feePayer: fp } : {},
4781
+ ...typeof ver === "number" ? { x402Version: ver } : {},
4782
+ ...typeof method === "string" ? { assetTransferMethod: method } : {}
4783
+ });
4765
4784
  }
4766
4785
  return out;
4767
4786
  }
@@ -5067,7 +5086,7 @@ function createPaymentGate(options) {
5067
5086
  instruction: describeChallenge({ x402Version: 2, resource: { url: resourceUrl }, accepts }),
5068
5087
  ...endpointInfo ? { endpoint: endpointInfo } : {}
5069
5088
  });
5070
- const rejectionExt = _nullishCoalesce(_optionalChain([opts, 'optionalAccess', _79 => _79.extensions]), () => ( {}));
5089
+ const rejectionExt = _nullishCoalesce(_optionalChain([opts, 'optionalAccess', _81 => _81.extensions]), () => ( {}));
5071
5090
  const rejectionPiprail = _nullishCoalesce(rejectionExt.piprail, () => ( {}));
5072
5091
  const bodyPiprail = { ..._nullishCoalesce(selfDescribe, () => ( {})), ...rejectionPiprail };
5073
5092
  const bodyExtensions = {
@@ -5087,7 +5106,7 @@ function createPaymentGate(options) {
5087
5106
  ...options.mimeType ? { mimeType: options.mimeType } : {}
5088
5107
  },
5089
5108
  accepts,
5090
- ..._optionalChain([opts, 'optionalAccess', _80 => _80.error]) ? { error: opts.error } : {},
5109
+ ..._optionalChain([opts, 'optionalAccess', _82 => _82.error]) ? { error: opts.error } : {},
5091
5110
  ...Object.keys(bodyExtensions).length > 0 ? { extensions: bodyExtensions } : {}
5092
5111
  };
5093
5112
  const headerChallenge = {
@@ -5409,7 +5428,7 @@ function isRetryableStatus(status) {
5409
5428
  }
5410
5429
  var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
5411
5430
  async function signBody(secret, body) {
5412
- const subtle = _optionalChain([globalThis, 'access', _81 => _81.crypto, 'optionalAccess', _82 => _82.subtle]);
5431
+ const subtle = _optionalChain([globalThis, 'access', _83 => _83.crypto, 'optionalAccess', _84 => _84.subtle]);
5413
5432
  if (!subtle) return null;
5414
5433
  try {
5415
5434
  const enc = new TextEncoder();
@@ -5479,7 +5498,7 @@ async function deliverReceipt(receipt, options) {
5479
5498
  const retryable = status === void 0 ? true : isRetryableStatus(status);
5480
5499
  const willRetry = !ok && retryable && attempt < maxAttempts;
5481
5500
  try {
5482
- _optionalChain([onAttempt, 'optionalCall', _83 => _83({ attempt, ok, ...status !== void 0 ? { status } : {}, ...error ? { error } : {}, willRetry })]);
5501
+ _optionalChain([onAttempt, 'optionalCall', _85 => _85({ attempt, ok, ...status !== void 0 ? { status } : {}, ...error ? { error } : {}, willRetry })]);
5483
5502
  } catch (e40) {
5484
5503
  }
5485
5504
  if (ok) return { delivered: true, attempts: attempt, status };
package/dist/index.d.cts CHANGED
@@ -5562,6 +5562,15 @@ declare class PipRailClient {
5562
5562
  * `quote()` (read-only) and `fetch()` (which then authorises + pays).
5563
5563
  */
5564
5564
  private resolveChallenge;
5565
+ /** Match a foreign-supplied network string against the bound driver, tolerating a
5566
+ * SLUG ('bsc', 'base', '56') the SAME way discovery's `railOnNetwork` already does —
5567
+ * normalize to CAIP-2 first, since a foreign/AEON/community 402 may label the network
5568
+ * with a slug (AEON serves v1 duplicate kinds '56'/'bsc'). ADDITIVE: a value that's
5569
+ * already CAIP-2 passes through `normalizeNetwork` UNCHANGED, so every existing
5570
+ * exact-CAIP-2 match is byte-identical; only slugs resolving to the bound chain become
5571
+ * newly matchable (an unknown slug stays unresolved → still unmatched; a different
5572
+ * chain's slug resolves elsewhere → still unmatched). */
5573
+ private supportsNetwork;
5565
5574
  /** The candidate accepts this client could pay, on the bound network. Always the
5566
5575
  * backendless `onchain-proof` rails; PLUS standard `exact` rails when `schemes`
5567
5576
  * enables them AND the driver can settle them (EVM `payExact` + a recognised
@@ -6611,6 +6620,14 @@ interface FacilitatorSupportedKind {
6611
6620
  network: string;
6612
6621
  /** The fee-payer pubkey when the kind carries one (SVM rails). */
6613
6622
  feePayer?: string;
6623
+ /** The kind's x402 envelope version when the facilitator reports it per-kind — e.g.
6624
+ * AEON's `/supported` serves `{ x402Version, scheme, network }`, letting a reader tell
6625
+ * a v1 from a v2 BNB rail. Optional — absent when the facilitator doesn't advertise it. */
6626
+ x402Version?: number;
6627
+ /** The EVM exact transfer method (`eip3009` / `permit2`) when the facilitator advertises
6628
+ * it in the kind's `extra` — so coverage can tell whether a BNB exact kind is gasless
6629
+ * EIP-3009 or Permit2. Optional — most facilitators (AEON included) omit it. */
6630
+ assetTransferMethod?: string;
6614
6631
  }
6615
6632
  /**
6616
6633
  * Parse a facilitator `/supported` body into its advertised (scheme, network) kinds.
package/dist/index.d.ts CHANGED
@@ -5562,6 +5562,15 @@ declare class PipRailClient {
5562
5562
  * `quote()` (read-only) and `fetch()` (which then authorises + pays).
5563
5563
  */
5564
5564
  private resolveChallenge;
5565
+ /** Match a foreign-supplied network string against the bound driver, tolerating a
5566
+ * SLUG ('bsc', 'base', '56') the SAME way discovery's `railOnNetwork` already does —
5567
+ * normalize to CAIP-2 first, since a foreign/AEON/community 402 may label the network
5568
+ * with a slug (AEON serves v1 duplicate kinds '56'/'bsc'). ADDITIVE: a value that's
5569
+ * already CAIP-2 passes through `normalizeNetwork` UNCHANGED, so every existing
5570
+ * exact-CAIP-2 match is byte-identical; only slugs resolving to the bound chain become
5571
+ * newly matchable (an unknown slug stays unresolved → still unmatched; a different
5572
+ * chain's slug resolves elsewhere → still unmatched). */
5573
+ private supportsNetwork;
5565
5574
  /** The candidate accepts this client could pay, on the bound network. Always the
5566
5575
  * backendless `onchain-proof` rails; PLUS standard `exact` rails when `schemes`
5567
5576
  * enables them AND the driver can settle them (EVM `payExact` + a recognised
@@ -6611,6 +6620,14 @@ interface FacilitatorSupportedKind {
6611
6620
  network: string;
6612
6621
  /** The fee-payer pubkey when the kind carries one (SVM rails). */
6613
6622
  feePayer?: string;
6623
+ /** The kind's x402 envelope version when the facilitator reports it per-kind — e.g.
6624
+ * AEON's `/supported` serves `{ x402Version, scheme, network }`, letting a reader tell
6625
+ * a v1 from a v2 BNB rail. Optional — absent when the facilitator doesn't advertise it. */
6626
+ x402Version?: number;
6627
+ /** The EVM exact transfer method (`eip3009` / `permit2`) when the facilitator advertises
6628
+ * it in the kind's `extra` — so coverage can tell whether a BNB exact kind is gasless
6629
+ * EIP-3009 or Permit2. Optional — most facilitators (AEON included) omit it. */
6630
+ assetTransferMethod?: string;
6614
6631
  }
6615
6632
  /**
6616
6633
  * Parse a facilitator `/supported` body into its advertised (scheme, network) kinds.
package/dist/index.js CHANGED
@@ -3220,7 +3220,7 @@ var PipRailClient = class {
3220
3220
  const candidates = this.gatherCandidates(net, challenge, schemes);
3221
3221
  if (candidates.length === 0) {
3222
3222
  const exactOnNet = challenge.accepts.some(
3223
- (a) => a.scheme === "exact" && net.supports(a.network)
3223
+ (a) => a.scheme === "exact" && this.supportsNetwork(net, a.network)
3224
3224
  );
3225
3225
  if (schemes.includes("exact") && exactOnNet && typeof net.payExact !== "function") {
3226
3226
  throw new UnsupportedSchemeError(
@@ -3229,7 +3229,7 @@ var PipRailClient = class {
3229
3229
  }
3230
3230
  if (!schemes.includes("exact") && exactOnNet && typeof net.payExact === "function") {
3231
3231
  const payable = challenge.accepts.some(
3232
- (a) => a.scheme === "exact" && net.supports(a.network) && net.describeAsset(a.asset) != null
3232
+ (a) => a.scheme === "exact" && this.supportsNetwork(net, a.network) && net.describeAsset(a.asset) != null
3233
3233
  );
3234
3234
  if (payable) {
3235
3235
  throw new NoCompatibleAcceptError(
@@ -3249,6 +3249,17 @@ var PipRailClient = class {
3249
3249
  const chosen = priced.find((p) => p.quote.withinPolicy) ?? priced[0];
3250
3250
  return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
3251
3251
  }
3252
+ /** Match a foreign-supplied network string against the bound driver, tolerating a
3253
+ * SLUG ('bsc', 'base', '56') the SAME way discovery's `railOnNetwork` already does —
3254
+ * normalize to CAIP-2 first, since a foreign/AEON/community 402 may label the network
3255
+ * with a slug (AEON serves v1 duplicate kinds '56'/'bsc'). ADDITIVE: a value that's
3256
+ * already CAIP-2 passes through `normalizeNetwork` UNCHANGED, so every existing
3257
+ * exact-CAIP-2 match is byte-identical; only slugs resolving to the bound chain become
3258
+ * newly matchable (an unknown slug stays unresolved → still unmatched; a different
3259
+ * chain's slug resolves elsewhere → still unmatched). */
3260
+ supportsNetwork(net, network) {
3261
+ return net.supports(normalizeNetwork(network));
3262
+ }
3252
3263
  /** The candidate accepts this client could pay, on the bound network. Always the
3253
3264
  * backendless `onchain-proof` rails; PLUS standard `exact` rails when `schemes`
3254
3265
  * enables them AND the driver can settle them (EVM `payExact` + a recognised
@@ -3259,14 +3270,14 @@ var PipRailClient = class {
3259
3270
  if (schemes.includes("onchain-proof")) {
3260
3271
  out.push(
3261
3272
  ...challenge.accepts.filter(
3262
- (a) => a.scheme === "onchain-proof" && net.supports(a.network)
3273
+ (a) => a.scheme === "onchain-proof" && this.supportsNetwork(net, a.network)
3263
3274
  )
3264
3275
  );
3265
3276
  }
3266
3277
  if (schemes.includes("exact")) {
3267
3278
  out.push(
3268
3279
  ...challenge.accepts.filter(
3269
- (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
3280
+ (a) => a.scheme === "exact" && this.supportsNetwork(net, a.network) && typeof net.payExact === "function" && net.describeAsset(a.asset) != null && // a foreign rail's maxTimeoutSeconds must be a usable positive integer, or
3270
3281
  // signing it would build a NaN/garbage validBefore — drop it silently
3271
3282
  // (symmetric with an unrecognised token) rather than leak a raw SyntaxError.
3272
3283
  Number.isInteger(a.maxTimeoutSeconds) && a.maxTimeoutSeconds > 0
@@ -3508,7 +3519,7 @@ var PipRailClient = class {
3508
3519
  );
3509
3520
  }
3510
3521
  async payAndConfirm(net, wallet, accept) {
3511
- if (!net.supports(accept.network)) {
3522
+ if (!this.supportsNetwork(net, accept.network)) {
3512
3523
  throw new WrongChainError(
3513
3524
  `Challenge expects ${accept.network} but client is on ${net.network}.`
3514
3525
  );
@@ -4761,7 +4772,15 @@ function parseFacilitatorSupported(body) {
4761
4772
  const o = k;
4762
4773
  if (typeof o.scheme !== "string" || typeof o.network !== "string") continue;
4763
4774
  const fp = o.extra?.feePayer;
4764
- out.push({ scheme: o.scheme, network: o.network, ...typeof fp === "string" ? { feePayer: fp } : {} });
4775
+ const ver = o.x402Version;
4776
+ const method = o.extra?.assetTransferMethod;
4777
+ out.push({
4778
+ scheme: o.scheme,
4779
+ network: o.network,
4780
+ ...typeof fp === "string" ? { feePayer: fp } : {},
4781
+ ...typeof ver === "number" ? { x402Version: ver } : {},
4782
+ ...typeof method === "string" ? { assetTransferMethod: method } : {}
4783
+ });
4765
4784
  }
4766
4785
  return out;
4767
4786
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piprail/sdk",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Accept x402 crypto payments across 29 chains — every major EVM chain plus Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar & XRPL — in a couple of lines. No backend, no database, no fee; payments settle straight to your wallet.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",