@piprail/sdk 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -401,6 +401,11 @@ function resolveChain(input, rpcUrlOverride) {
401
401
  }
402
402
  return { chain: input, chainId: input.id, rpcUrl: rpcUrl2, tokens: knownTokensForId(input.id) };
403
403
  }
404
+ if (!Number.isSafeInteger(input.id) || input.id <= 0) {
405
+ throw new Error(
406
+ `resolveChain: chain id must be a positive safe integer (EIP-155), got ${String(input.id)}.`
407
+ );
408
+ }
404
409
  const rpcUrl = rpcUrlOverride ?? input.rpcUrl;
405
410
  if (!rpcUrl) {
406
411
  throw new Error(`resolveChain: chain ${input.id} needs an rpcUrl.`);
@@ -574,8 +579,25 @@ async function quoteEvmSwap(p) {
574
579
  } catch {
575
580
  return null;
576
581
  }
577
- const real = await route(needIn);
578
- const summary = real?.data?.routeSummary;
582
+ const MAX_REFINEMENTS = 3;
583
+ let real = await route(needIn);
584
+ let summary = real?.data?.routeSummary;
585
+ for (let attempt = 0; attempt < MAX_REFINEMENTS; attempt++) {
586
+ if (!summary?.amountOut) break;
587
+ let out;
588
+ try {
589
+ out = BigInt(summary.amountOut);
590
+ } catch {
591
+ return null;
592
+ }
593
+ if (out >= p.wantAmount) break;
594
+ if (out <= 0n) return null;
595
+ const next = applySlippage((needIn * p.wantAmount + out - 1n) / out, p.slippageBps);
596
+ if (next <= needIn) break;
597
+ needIn = next;
598
+ real = await route(needIn);
599
+ summary = real?.data?.routeSummary;
600
+ }
579
601
  if (!summary?.amountOut || !real?.data?.routerAddress) return null;
580
602
  try {
581
603
  if (BigInt(summary.amountOut) < p.wantAmount) return null;
@@ -2440,7 +2462,7 @@ var loaders = {
2440
2462
  solana: async () => {
2441
2463
  let mod;
2442
2464
  try {
2443
- mod = await import("./solana-O6Q6QILH.js");
2465
+ mod = await import("./solana-WRXL54MR.js");
2444
2466
  } catch (cause) {
2445
2467
  throw new MissingDriverError(
2446
2468
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -2464,7 +2486,7 @@ var loaders = {
2464
2486
  stellar: async () => {
2465
2487
  let mod;
2466
2488
  try {
2467
- mod = await import("./stellar-YF5LOJEM.js");
2489
+ mod = await import("./stellar-YQLOIFDD.js");
2468
2490
  } catch (cause) {
2469
2491
  throw new MissingDriverError(
2470
2492
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
@@ -2476,7 +2498,7 @@ var loaders = {
2476
2498
  xrpl: async () => {
2477
2499
  let mod;
2478
2500
  try {
2479
- mod = await import("./xrpl-GOVHMYYK.js");
2501
+ mod = await import("./xrpl-5K444RGX.js");
2480
2502
  } catch (cause) {
2481
2503
  throw new MissingDriverError(
2482
2504
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -2536,7 +2558,7 @@ var loaders = {
2536
2558
  algorand: async () => {
2537
2559
  let mod;
2538
2560
  try {
2539
- mod = await import("./algorand-HL57PQHE.js");
2561
+ mod = await import("./algorand-KJ5XHSMT.js");
2540
2562
  } catch (cause) {
2541
2563
  throw new MissingDriverError(
2542
2564
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
@@ -2846,8 +2868,39 @@ var SpendLedger = class {
2846
2868
  }
2847
2869
  }
2848
2870
  /** Running total (base units) already spent on this (network, asset). */
2871
+ /*
2872
+ * ── IN-FLIGHT RESERVATIONS ─────────────────────────────────────────────────────────
2873
+ *
2874
+ * A cap is read when a quote is priced and written when the payment settles, and a whole
2875
+ * network round trip sits between the two. Without a reservation, N concurrent payments all
2876
+ * price against the same "spent so far", all pass, and all settle: an agent with a 2.50 cap
2877
+ * spends 4.00 and every individual check was correct. It is the same read-await-write shape
2878
+ * that let one proof be redeemed N times, on the other side of the wire.
2879
+ *
2880
+ * A reservation is taken SYNCHRONOUSLY by the client before it pays, counts toward every
2881
+ * total below while it is outstanding, and is released the moment the payment either settles
2882
+ * (the real record replaces it) or fails (so a refused payment never consumes the leash).
2883
+ */
2884
+ pending = /* @__PURE__ */ new Map();
2885
+ pendingSeq = 0;
2886
+ /** Reserve budget for a payment about to be attempted. Returns the token to settle it with. */
2887
+ reserve(network, asset, amountBase, decimals, denom) {
2888
+ const token = `r${++this.pendingSeq}`;
2889
+ const scaled = denom ? scaleToDenom(amountBase, decimals) ?? 0n : 0n;
2890
+ this.pending.set(token, { network, asset, amountBase, denom: denom?.toUpperCase(), scaled, at: Date.now() });
2891
+ return token;
2892
+ }
2893
+ /** Drop a reservation: the payment settled (its real record now counts) or it failed. */
2894
+ release(token) {
2895
+ if (token) this.pending.delete(token);
2896
+ }
2897
+ pendingFor(network, asset) {
2898
+ let sum = 0n;
2899
+ for (const p of this.pending.values()) if (p.network === network && p.asset === asset) sum += p.amountBase;
2900
+ return sum;
2901
+ }
2849
2902
  totalFor(network, asset) {
2850
- return this.buckets.get(keyFor(network, asset))?.total ?? 0n;
2903
+ return (this.buckets.get(keyFor(network, asset))?.total ?? 0n) + this.pendingFor(network, asset);
2851
2904
  }
2852
2905
  /**
2853
2906
  * Running grand total for a DENOMINATION, scaled to {@link DENOM_PRECISION} (so
@@ -2856,11 +2909,14 @@ var SpendLedger = class {
2856
2909
  * `0n` for a denomination never spent on. Case-insensitive.
2857
2910
  */
2858
2911
  totalForDenom(denom) {
2859
- return this.denomTotals.get(denom.toUpperCase()) ?? 0n;
2912
+ const key = denom.toUpperCase();
2913
+ let pending = 0n;
2914
+ for (const p of this.pending.values()) if (p.denom === key) pending += p.scaled;
2915
+ return (this.denomTotals.get(key) ?? 0n) + pending;
2860
2916
  }
2861
2917
  /** Total number of settled payments (across every chain + token). Powers `maxPayments`. */
2862
2918
  count() {
2863
- return this.records.length;
2919
+ return this.records.length + this.pending.size;
2864
2920
  }
2865
2921
  /** Mark a `warnAtFraction` threshold key as fired; returns `true` the FIRST time (so the
2866
2922
  * caller emits the `budget-threshold` event once) and `false` thereafter. Shared across
@@ -2878,6 +2934,7 @@ var SpendLedger = class {
2878
2934
  */
2879
2935
  countSince(sinceMs) {
2880
2936
  let n = 0;
2937
+ for (const p of this.pending.values()) if (p.at >= sinceMs) n += 1;
2881
2938
  for (const r of this.records) {
2882
2939
  const t = Date.parse(r.at);
2883
2940
  if (Number.isNaN(t) || t >= sinceMs) n += 1;
@@ -2893,6 +2950,9 @@ var SpendLedger = class {
2893
2950
  */
2894
2951
  totalSince(network, asset, sinceMs) {
2895
2952
  let sum = 0n;
2953
+ for (const p of this.pending.values()) {
2954
+ if (p.network === network && p.asset === asset && p.at >= sinceMs) sum += p.amountBase;
2955
+ }
2896
2956
  for (const r of this.records) {
2897
2957
  if (r.network !== network || r.asset !== asset) continue;
2898
2958
  const t = Date.parse(r.at);
@@ -3303,6 +3363,32 @@ var PipRailClient = class {
3303
3363
  this.assertPolicyTimeOptions(opts.policy);
3304
3364
  this.assertPolicySpendControls(opts.policy);
3305
3365
  this.assertModeIsHonest(opts);
3366
+ this.sealAuthority();
3367
+ }
3368
+ /**
3369
+ * Pin the three authority accessors to THIS instance, non-writable and non-configurable.
3370
+ *
3371
+ * `paymentTools()` decides which tools a model is handed by calling `canAgentSell()` /
3372
+ * `canAgentSwap()`, which read `mode()`. Those were plain prototype methods, so any code
3373
+ * holding the client could reassign one — `client.mode = () => 'sovereign'` turned a
3374
+ * budgeted client's eight tools into sovereign's fourteen.
3375
+ *
3376
+ * A MODEL could never do that (it sends JSON tool arguments; it does not hold the object),
3377
+ * so this is not a path a model can walk. It is defence in depth for the case where a
3378
+ * client passes through code that is not the operator's own: an agent framework, a plugin,
3379
+ * some middleware that wraps or proxies objects. Authority is set once, by whoever
3380
+ * provisioned the key, and nothing downstream gets to revise it.
3381
+ */
3382
+ sealAuthority() {
3383
+ const mode = this.opts.mode ?? DEFAULT_AGENT_MODE;
3384
+ const sovereign = mode === "sovereign";
3385
+ for (const [name, fn] of [
3386
+ ["mode", () => mode],
3387
+ ["canAgentSell", () => sovereign],
3388
+ ["canAgentSwap", () => sovereign]
3389
+ ]) {
3390
+ Object.defineProperty(this, name, { value: fn, writable: false, configurable: false, enumerable: false });
3391
+ }
3306
3392
  }
3307
3393
  /**
3308
3394
  * Fail LOUDLY at construction on a malformed amount cap — a security boundary
@@ -4245,22 +4331,35 @@ var PipRailClient = class {
4245
4331
  quote = plan.best.quote;
4246
4332
  }
4247
4333
  this.safeEmit({ kind: "payment-required", challenge, accept });
4248
- await this.authorize(quote);
4334
+ const reservation = await this.authorize(quote);
4249
4335
  if (accept.scheme === "upto") {
4250
- return this.payUptoRail(net, wallet, accept, url, init, quote);
4336
+ try {
4337
+ return await this.payUptoRail(net, wallet, accept, url, init, quote, reservation);
4338
+ } finally {
4339
+ this.ledger.release(reservation);
4340
+ }
4251
4341
  }
4252
4342
  if (accept.scheme === "exact") {
4253
- return this.payExactRail(net, wallet, accept, url, init, quote, challenge.x402Version);
4343
+ try {
4344
+ return await this.payExactRail(net, wallet, accept, url, init, quote, challenge.x402Version, reservation);
4345
+ } finally {
4346
+ this.ledger.release(reservation);
4347
+ }
4254
4348
  }
4255
4349
  if (accept.scheme !== "onchain-proof") {
4350
+ this.ledger.release(reservation);
4256
4351
  throw new UnsupportedSchemeError(
4257
4352
  `internal: unrouted accept scheme '${accept.scheme}' reached the onchain-proof pay path.`
4258
4353
  );
4259
4354
  }
4260
- const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
4261
- const response = await this.retryWithProof(url, init, accept, ref, confirmed);
4262
- this.recordSpend(quote, ref);
4263
- return response;
4355
+ try {
4356
+ const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
4357
+ const response = await this.retryWithProof(url, init, accept, ref, confirmed);
4358
+ this.recordSpend(quote, ref, void 0, reservation);
4359
+ return response;
4360
+ } finally {
4361
+ this.ledger.release(reservation);
4362
+ }
4264
4363
  }
4265
4364
  /* ------------------------- internals ------------------------- */
4266
4365
  /**
@@ -4449,9 +4548,11 @@ var PipRailClient = class {
4449
4548
  shortfall.token = formatUnits(amount - bal.token, quote.decimals);
4450
4549
  }
4451
4550
  } else if (isNative) {
4452
- if (nativeKnown && bal.native < amount + fee) {
4551
+ const spendable = bal.token ?? bal.native;
4552
+ const spendableKnown = spendable != null;
4553
+ if (spendableKnown && spendable < amount + fee) {
4453
4554
  blockers.push("INSUFFICIENT_TOKEN");
4454
- shortfall.token = formatUnits(amount + fee - bal.native, quote.decimals);
4555
+ shortfall.token = formatUnits(amount + fee - spendable, quote.decimals);
4455
4556
  }
4456
4557
  } else {
4457
4558
  if (tokenKnown && bal.token < amount) {
@@ -4603,18 +4704,28 @@ var PipRailClient = class {
4603
4704
  quote
4604
4705
  });
4605
4706
  }
4707
+ const reservation = this.ledger.reserve(
4708
+ quote.network,
4709
+ quote.asset,
4710
+ BigInt(quote.amount),
4711
+ quote.decimals,
4712
+ denomOf(quote.symbol, quote.asset, this.opts.policy)
4713
+ );
4606
4714
  const hook = this.opts.onBeforePay;
4607
- if (!hook) return;
4715
+ if (!hook) return reservation;
4608
4716
  let approved;
4609
4717
  try {
4610
4718
  approved = await hook(quote);
4611
4719
  } catch (err) {
4720
+ this.ledger.release(reservation);
4612
4721
  this.refuse("onBeforePay threw \u2014 refusing to pay.", { reasonCode: "APPROVAL", quote, cause: err });
4613
4722
  }
4614
4723
  if (!approved) {
4724
+ this.ledger.release(reservation);
4615
4725
  const reason = `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`;
4616
4726
  this.refuse(reason, { reasonCode: "APPROVAL", quote });
4617
4727
  }
4728
+ return reservation;
4618
4729
  }
4619
4730
  /**
4620
4731
  * Refuse a payment BEFORE any send: emit BOTH the legacy `payment-failed` (so existing
@@ -4653,7 +4764,7 @@ var PipRailClient = class {
4653
4764
  * spend (POL-1). So the cap-bearing `amountBase` is the MAX; the clamped actual is surfaced
4654
4765
  * separately on `settledBase`/`settledFormatted` for transparency (it equals the receipt's
4655
4766
  * amount). When absent (onchain-proof/exact) this is byte-identical to before. */
4656
- recordSpend(quote, ref, settledAmountBase) {
4767
+ recordSpend(quote, ref, settledAmountBase, reservation) {
4657
4768
  const denom = denomOf(quote.symbol, quote.asset, this.opts.policy);
4658
4769
  const amountBase = quote.amount;
4659
4770
  const amountFormatted = quote.amountFormatted;
@@ -4684,6 +4795,7 @@ var PipRailClient = class {
4684
4795
  at: (/* @__PURE__ */ new Date()).toISOString()
4685
4796
  };
4686
4797
  this.ledger.record(record, quote.decimals, denom);
4798
+ this.ledger.release(reservation);
4687
4799
  const budget = this.budget();
4688
4800
  if (this.opts.onSpend) {
4689
4801
  try {
@@ -4855,7 +4967,7 @@ var PipRailClient = class {
4855
4967
  * • a 200 whose SettleResponse says `success:false` → a rejection, NEVER a spend;
4856
4968
  * • the spend is recorded EXACTLY ONCE, on an affirmative settlement only.
4857
4969
  */
4858
- async payExactRail(net, wallet, accept, url, init, quote, x402Version = 2) {
4970
+ async payExactRail(net, wallet, accept, url, init, quote, x402Version = 2, reservation) {
4859
4971
  if (!net.payExact) {
4860
4972
  throw new UnsupportedSchemeError(
4861
4973
  `the ${net.family} family can't pay a standard 'exact' rail (supported on EVM, Solana, Algorand, Aptos + NEAR today).`
@@ -4919,7 +5031,7 @@ var PipRailClient = class {
4919
5031
  this.captureReceipt(response, url);
4920
5032
  this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
4921
5033
  const ref = settle?.transaction || receipt?.transaction || `${net.family === "evm" ? "eip3009" : net.family}-nonce:${nonce}`;
4922
- this.recordSpend(quote, ref);
5034
+ this.recordSpend(quote, ref, void 0, reservation);
4923
5035
  return response;
4924
5036
  }
4925
5037
  if (response.status >= 500) {
@@ -4950,7 +5062,7 @@ var PipRailClient = class {
4950
5062
  * `settle.amount` FAILS SAFE to the MAX (over-counts, never under-counts). The buyer SIGNS, the
4951
5063
  * merchant self-settles — the buyer never broadcasts.
4952
5064
  */
4953
- async payUptoRail(net, wallet, accept, url, init, quote) {
5065
+ async payUptoRail(net, wallet, accept, url, init, quote, reservation) {
4954
5066
  if (!net.payUpto) {
4955
5067
  throw new UnsupportedSchemeError(
4956
5068
  `the ${net.family} family can't pay a standard 'upto' rail (EVM-Permit2 only today).`
@@ -5004,7 +5116,7 @@ var PipRailClient = class {
5004
5116
  this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
5005
5117
  const ref = settle?.transaction || receipt?.transaction || `upto-nonce:${nonce}`;
5006
5118
  const settledAmount = settle?.amount ?? receipt?.amount;
5007
- this.recordSpend(quote, ref, settledAmount);
5119
+ this.recordSpend(quote, ref, settledAmount, reservation);
5008
5120
  return response;
5009
5121
  }
5010
5122
  if (response.status >= 500) {
@@ -6018,7 +6130,7 @@ function renderLandingPage(sd) {
6018
6130
  }
6019
6131
 
6020
6132
  // src/facilitator.ts
6021
- async function fetchFacilitatorFeePayer(url, network, timeoutMs = 8e3) {
6133
+ async function fetchFacilitatorFeePayer(url, network, timeoutMs = 15e3) {
6022
6134
  const base2 = url.replace(/\/+$/, "");
6023
6135
  const ctrl = new AbortController();
6024
6136
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -6058,7 +6170,7 @@ function parseFacilitatorSupported(body) {
6058
6170
  }
6059
6171
  return out;
6060
6172
  }
6061
- async function facilitatorCoverage(url, timeoutMs = 8e3) {
6173
+ async function facilitatorCoverage(url, timeoutMs = 15e3) {
6062
6174
  const base2 = url.replace(/\/+$/, "");
6063
6175
  const ctrl = new AbortController();
6064
6176
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -6580,6 +6692,11 @@ function createPaymentGate(options) {
6580
6692
  net.assertValidPayTo(payTo);
6581
6693
  const { asset, decimals, symbol } = net.resolveToken(a.token);
6582
6694
  const amountBase = parseUnits(a.amount, decimals);
6695
+ if (amountBase <= 0n) {
6696
+ throw new InvalidConfigError(
6697
+ `requirePayment: amount must be greater than zero, got "${a.amount}" on ${net.network}. A gate that charges nothing gates nothing; omit the gate instead.`
6698
+ );
6699
+ }
6583
6700
  const spec = { net, asset, decimals, symbol, amountBase, amountFormatted: a.amount, payTo };
6584
6701
  if (exactOption) {
6585
6702
  const outcome = await resolveExactRail(net, asset);
@@ -6698,6 +6815,11 @@ function createPaymentGate(options) {
6698
6815
  "requirePayment/createPaymentGate: `isUsed` and `markUsed` must be provided TOGETHER \u2014 a custom replay store needs both a read and a write. Supplying only " + (hasIsUsed ? "`isUsed`" : "`markUsed`") + " silently disables replay protection (double-spend). Provide both, or neither (the built-in in-memory store)."
6699
6816
  );
6700
6817
  }
6818
+ if (typeof options.releaseUsed === "function" && !(hasIsUsed && hasMarkUsed)) {
6819
+ throw new Error(
6820
+ "requirePayment/createPaymentGate: `releaseUsed` needs `isUsed` + `markUsed` too \u2014 it releases a reservation a CUSTOM store made, so without one it would never fire. Provide all three, or none (the built-in store already releases on failure)."
6821
+ );
6822
+ }
6701
6823
  const hasCustomStore = hasIsUsed && hasMarkUsed;
6702
6824
  const localUsed = /* @__PURE__ */ new Map();
6703
6825
  const replayWindowMs = maxTimeoutSeconds * 1e3;
@@ -6707,23 +6829,35 @@ function createPaymentGate(options) {
6707
6829
  localUsed.delete(key);
6708
6830
  }
6709
6831
  }
6832
+ const localKey = (ref) => ref.startsWith("pid:") ? ref : ref.toLowerCase();
6710
6833
  async function claimTx(ref) {
6711
- if (hasCustomStore) {
6712
- return options.isUsed ? Boolean(await options.isUsed(ref)) : false;
6713
- }
6714
- const key = ref.startsWith("pid:") ? ref : ref.toLowerCase();
6834
+ const key = localKey(ref);
6715
6835
  const now = Date.now();
6716
6836
  pruneUsed(now);
6717
6837
  if (localUsed.has(key)) return true;
6718
6838
  localUsed.set(key, now + replayWindowMs);
6839
+ if (hasCustomStore) {
6840
+ try {
6841
+ if (options.isUsed && await options.isUsed(ref)) return true;
6842
+ } catch (err) {
6843
+ localUsed.delete(key);
6844
+ throw err;
6845
+ }
6846
+ }
6719
6847
  return false;
6720
6848
  }
6721
6849
  async function settleTx(ref, ok) {
6722
- if (hasCustomStore) {
6723
- if (ok && options.markUsed) await options.markUsed(ref);
6850
+ if (!ok) {
6851
+ localUsed.delete(localKey(ref));
6852
+ if (hasCustomStore && options.releaseUsed) {
6853
+ try {
6854
+ await options.releaseUsed(ref);
6855
+ } catch {
6856
+ }
6857
+ }
6724
6858
  return;
6725
6859
  }
6726
- if (!ok) localUsed.delete(ref.startsWith("pid:") ? ref : ref.toLowerCase());
6860
+ if (hasCustomStore && options.markUsed) await options.markUsed(ref);
6727
6861
  }
6728
6862
  function buildAccept(s, nonce) {
6729
6863
  return {
@@ -7413,6 +7547,12 @@ function nonceIn(payload) {
7413
7547
  const fromAccept = typeof accepted?.extra?.nonce === "string" ? accepted.extra.nonce : void 0;
7414
7548
  return fromPayload ?? fromAccept;
7415
7549
  }
7550
+ function refIn(payload) {
7551
+ if (typeof payload !== "object" || payload === null) return void 0;
7552
+ const inner = payload.payload;
7553
+ const ref = inner && typeof inner.txHash === "string" ? inner.txHash.trim() : void 0;
7554
+ return ref ? ref.toLowerCase() : void 0;
7555
+ }
7416
7556
  function toToolError(err) {
7417
7557
  if (!(err instanceof PipRailError)) throw err;
7418
7558
  const out = {
@@ -7953,16 +8093,12 @@ function paymentTools(client) {
7953
8093
  const railSchemes = (t) => [
7954
8094
  ...new Set((t.rails ?? []).flatMap((r) => [...r.schemes ?? []]))
7955
8095
  ];
7956
- const shared = {
7957
- isUsed: (ref) => spentProofs.has(ref),
7958
- markUsed: (ref) => void spentProofs.add(ref)
7959
- };
7960
- let gate = createPaymentGate({ ...base3, ...shared, exact: true });
8096
+ let gate = createPaymentGate({ ...base3, exact: true });
7961
8097
  let check = await gate.selfTest();
7962
8098
  const warnings = [];
7963
8099
  if (!check.ok || !railSchemes(check).includes("exact")) {
7964
8100
  const why = check.error ?? "it did not resolve on this RPC";
7965
- gate = createPaymentGate({ ...base3, ...shared });
8101
+ gate = createPaymentGate({ ...base3 });
7966
8102
  check = await gate.selfTest();
7967
8103
  warnings.push(
7968
8104
  `This offer carries onchain-proof ONLY, so a standard x402 agent-buyer cannot pay it. A PipRail buyer (piprail_pay_request) and a human still can. The gasless exact rail was dropped because ${why} Two things cause this: the token or family has no exact scheme (a native coin never does, so price in USDC), or the token domain read failed on a busy public RPC, which is transient. Retrying on a dedicated rpcUrl is worth one attempt before you settle for this.`
@@ -8059,7 +8195,30 @@ function paymentTools(client) {
8059
8195
  next: "Do NOT deliver. Ask the buyer to pay THIS offer's challenge."
8060
8196
  };
8061
8197
  }
8062
- const result = asObject !== void 0 ? await offer.gate.verifyObject(asObject) : await offer.gate.verify(raw);
8198
+ const proofRef = refIn(asObject ?? decodeBase64Json(raw));
8199
+ let reservedHere;
8200
+ if (proofRef) {
8201
+ if (spentProofs.has(proofRef)) {
8202
+ return {
8203
+ ok: true,
8204
+ paid: false,
8205
+ offerId: offer.id,
8206
+ reason: "this settlement was already collected \u2014 one payment settles exactly one offer.",
8207
+ code: "tx_already_used",
8208
+ next: "Do NOT deliver. Ask the buyer to pay THIS offer's challenge."
8209
+ };
8210
+ }
8211
+ spentProofs.add(proofRef);
8212
+ reservedHere = proofRef;
8213
+ }
8214
+ let result;
8215
+ try {
8216
+ result = asObject !== void 0 ? await offer.gate.verifyObject(asObject) : await offer.gate.verify(raw);
8217
+ } catch (err) {
8218
+ if (reservedHere) spentProofs.delete(reservedHere);
8219
+ throw err;
8220
+ }
8221
+ if (result.kind !== "paid" && reservedHere) spentProofs.delete(reservedHere);
8063
8222
  if (result.kind === "paid") {
8064
8223
  const r = result.receipt;
8065
8224
  const entry = {
@@ -1056,6 +1056,13 @@ declare class SpendLedger {
1056
1056
  * {@link SpendStore} when one is configured (a failed append never throws). */
1057
1057
  record(r: SpendRecord, decimals: number, denom?: string): void;
1058
1058
  /** Running total (base units) already spent on this (network, asset). */
1059
+ private readonly pending;
1060
+ private pendingSeq;
1061
+ /** Reserve budget for a payment about to be attempted. Returns the token to settle it with. */
1062
+ reserve(network: string, asset: string, amountBase: bigint, decimals: number, denom?: string): string;
1063
+ /** Drop a reservation: the payment settled (its real record now counts) or it failed. */
1064
+ release(token: string | undefined): void;
1065
+ private pendingFor;
1059
1066
  totalFor(network: string, asset: string): bigint;
1060
1067
  /**
1061
1068
  * Running grand total for a DENOMINATION, scaled to {@link DENOM_PRECISION} (so
@@ -1056,6 +1056,13 @@ declare class SpendLedger {
1056
1056
  * {@link SpendStore} when one is configured (a failed append never throws). */
1057
1057
  record(r: SpendRecord, decimals: number, denom?: string): void;
1058
1058
  /** Running total (base units) already spent on this (network, asset). */
1059
+ private readonly pending;
1060
+ private pendingSeq;
1061
+ /** Reserve budget for a payment about to be attempted. Returns the token to settle it with. */
1062
+ reserve(network: string, asset: string, amountBase: bigint, decimals: number, denom?: string): string;
1063
+ /** Drop a reservation: the payment settled (its real record now counts) or it failed. */
1064
+ release(token: string | undefined): void;
1065
+ private pendingFor;
1059
1066
  totalFor(network: string, asset: string): bigint;
1060
1067
  /**
1061
1068
  * Running grand total for a DENOMINATION, scaled to {@link DENOM_PRECISION} (so
package/dist/node.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { v as SpendStore } from './ledger-Crc1bZox.cjs';
2
- export { _ as memorySpendStore } from './ledger-Crc1bZox.cjs';
1
+ import { v as SpendStore } from './ledger-DkHUORUe.cjs';
2
+ export { _ as memorySpendStore } from './ledger-DkHUORUe.cjs';
3
3
 
4
4
  /**
5
5
  * A durable {@link SpendStore} backed by a local JSONL file (one settled payment per
package/dist/node.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { v as SpendStore } from './ledger-Crc1bZox.js';
2
- export { _ as memorySpendStore } from './ledger-Crc1bZox.js';
1
+ import { v as SpendStore } from './ledger-DkHUORUe.js';
2
+ export { _ as memorySpendStore } from './ledger-DkHUORUe.js';
3
3
 
4
4
  /**
5
5
  * A durable {@link SpendStore} backed by a local JSONL file (one settled payment per
@@ -763,6 +763,7 @@ function toKeypair(wallet, network) {
763
763
  }
764
764
 
765
765
  // src/drivers/solana/index.ts
766
+ var RENT_EXEMPT_MIN_LAMPORTS = 890880n;
766
767
  var solanaDriver = {
767
768
  family: "solana",
768
769
  resolve(opts) {
@@ -890,7 +891,12 @@ function makeSolanaNetwork(preset, rpcUrl) {
890
891
  async balanceOf(wallet, asset) {
891
892
  const owner = wallet._native.publicKey;
892
893
  const native = await connection.getBalance(owner).then((n) => BigInt(n)).catch(() => null);
893
- if (asset === "native") return { token: native, native };
894
+ if (asset === "native") {
895
+ if (native === null) return { token: null, native };
896
+ const reserve = await connection.getMinimumBalanceForRentExemption(0).then((n) => BigInt(n)).catch(() => RENT_EXEMPT_MIN_LAMPORTS);
897
+ const spendable = native > reserve ? native - reserve : 0n;
898
+ return { token: spendable, native };
899
+ }
894
900
  let token;
895
901
  try {
896
902
  const mint = new (0, _web3js.PublicKey)(asset);
@@ -763,6 +763,7 @@ function toKeypair(wallet, network) {
763
763
  }
764
764
 
765
765
  // src/drivers/solana/index.ts
766
+ var RENT_EXEMPT_MIN_LAMPORTS = 890880n;
766
767
  var solanaDriver = {
767
768
  family: "solana",
768
769
  resolve(opts) {
@@ -890,7 +891,12 @@ function makeSolanaNetwork(preset, rpcUrl) {
890
891
  async balanceOf(wallet, asset) {
891
892
  const owner = wallet._native.publicKey;
892
893
  const native = await connection.getBalance(owner).then((n) => BigInt(n)).catch(() => null);
893
- if (asset === "native") return { token: native, native };
894
+ if (asset === "native") {
895
+ if (native === null) return { token: null, native };
896
+ const reserve = await connection.getMinimumBalanceForRentExemption(0).then((n) => BigInt(n)).catch(() => RENT_EXEMPT_MIN_LAMPORTS);
897
+ const spendable = native > reserve ? native - reserve : 0n;
898
+ return { token: spendable, native };
899
+ }
894
900
  let token;
895
901
  try {
896
902
  const mint = new PublicKey3(asset);
@@ -419,6 +419,7 @@ function isStellarNotFound(e) {
419
419
  const x = e;
420
420
  return _optionalChain([x, 'optionalAccess', _14 => _14.response, 'optionalAccess', _15 => _15.status]) === 404 || _optionalChain([x, 'optionalAccess', _16 => _16.name]) === "NotFoundError";
421
421
  }
422
+ var STELLAR_BASE_RESERVE = 5000000n;
422
423
  var stellarDriver = {
423
424
  family: "stellar",
424
425
  resolve(opts) {
@@ -550,9 +551,11 @@ function makeStellarNetwork(preset, rpcUrl) {
550
551
  return { token: null, native: null };
551
552
  }
552
553
  let lines;
554
+ let subentryCount;
553
555
  try {
554
556
  const account = await server.loadAccount(owner);
555
557
  lines = account.balances;
558
+ subentryCount = account.subentry_count;
556
559
  } catch (e) {
557
560
  return isStellarNotFound(e) ? { token: 0n, native: 0n } : { token: null, native: null };
558
561
  }
@@ -564,7 +567,12 @@ function makeStellarNetwork(preset, rpcUrl) {
564
567
  }
565
568
  };
566
569
  const native = toBase(_optionalChain([lines, 'access', _17 => _17.find, 'call', _18 => _18((b) => b.asset_type === "native"), 'optionalAccess', _19 => _19.balance]));
567
- if (asset === "native") return { token: native, native };
570
+ if (asset === "native") {
571
+ if (native === null) return { token: null, native };
572
+ const subentries = BigInt(_nullishCoalesce(subentryCount, () => ( 0)));
573
+ const min = (2n + subentries) * STELLAR_BASE_RESERVE;
574
+ return { token: native > min ? native - min : 0n, native };
575
+ }
568
576
  const parts = parseStellarAssetId(asset);
569
577
  const line = parts ? lines.find(
570
578
  (b) => (b.asset_type === "credit_alphanum4" || b.asset_type === "credit_alphanum12") && b.asset_code === parts.code && b.asset_issuer === parts.issuer
@@ -419,6 +419,7 @@ function isStellarNotFound(e) {
419
419
  const x = e;
420
420
  return x?.response?.status === 404 || x?.name === "NotFoundError";
421
421
  }
422
+ var STELLAR_BASE_RESERVE = 5000000n;
422
423
  var stellarDriver = {
423
424
  family: "stellar",
424
425
  resolve(opts) {
@@ -550,9 +551,11 @@ function makeStellarNetwork(preset, rpcUrl) {
550
551
  return { token: null, native: null };
551
552
  }
552
553
  let lines;
554
+ let subentryCount;
553
555
  try {
554
556
  const account = await server.loadAccount(owner);
555
557
  lines = account.balances;
558
+ subentryCount = account.subentry_count;
556
559
  } catch (e) {
557
560
  return isStellarNotFound(e) ? { token: 0n, native: 0n } : { token: null, native: null };
558
561
  }
@@ -564,7 +567,12 @@ function makeStellarNetwork(preset, rpcUrl) {
564
567
  }
565
568
  };
566
569
  const native = toBase(lines.find((b) => b.asset_type === "native")?.balance);
567
- if (asset === "native") return { token: native, native };
570
+ if (asset === "native") {
571
+ if (native === null) return { token: null, native };
572
+ const subentries = BigInt(subentryCount ?? 0);
573
+ const min = (2n + subentries) * STELLAR_BASE_RESERVE;
574
+ return { token: native > min ? native - min : 0n, native };
575
+ }
568
576
  const parts = parseStellarAssetId(asset);
569
577
  const line = parts ? lines.find(
570
578
  (b) => (b.asset_type === "credit_alphanum4" || b.asset_type === "credit_alphanum12") && b.asset_code === parts.code && b.asset_issuer === parts.issuer