@piprail/sdk 3.1.0 → 3.1.2

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,46 @@ 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
+ ## [3.1.2] — 2026-09-09 — a balance says what it means
8
+
9
+ ### Fixed
10
+
11
+ - **Three descriptions of a balance still said HOLDS after 3.1.0 made it SPENDABLE.** 3.1.0
12
+ changed the drivers to deduct a chain's retained minimum (Solana rent, XRPL/Stellar/Algorand
13
+ reserves) from the native figure, and documented that precisely on `WalletBalance.token` — but
14
+ every layer above it kept the old wording. `PipRailClient.balanceOf`'s TSDoc, the interface it
15
+ implements, and the `piprail_wallet` tool description a MODEL reads all still promised "what you
16
+ hold". They now say what they mean, and name the gap as locked rather than lost.
17
+
18
+ The wording mattered most in the tool description, because an agent comparing its own balance
19
+ against a block explorer sees a smaller number and has to decide whether it is being lied to. On
20
+ a live Stellar account holding 1.5398 XLM with three subentries, `balanceOf` reports 0.0398: the
21
+ 1.5 XLM difference is the base reserve. Text only; no behaviour changes.
22
+
23
+ ## [3.1.1] — 2026-09-09 — XRPL payments work again
24
+
25
+ ### Fixed
26
+
27
+ - **A facilitator's payload rejection answered 5xx instead of 402.** Every non-200 from a
28
+ facilitator's `/verify` was treated as a transport failure, so a **400** — a forged or
29
+ malformed authorization — made the gate return a server error. That tells the buyer "our
30
+ fault, try again" about a payment that can never succeed, and shows in the merchant's metrics
31
+ as an outage they do not have. Now only `400`/`422` (the payload is bad, and only the buyer
32
+ can fix it) reject with the facilitator's own reason; `401`/`403` (our credentials),
33
+ `404` (our URL), `429` and `5xx` stay `SettlementError`, because a buyer can do nothing about
34
+ any of them. Found by tampering with a real authorization on Base.
35
+
36
+ - 🔴 **Every XRPL payment failed.** `ledger_current` is documented to return
37
+ `ledger_current_index`, and rippled does, but the public clusters in front of it answer with
38
+ `ledger_index`. Reading only the documented name returned `undefined`, `undefined + 20` became
39
+ `NaN`, and the transaction died in xrpl.js's serializer. Both names are now accepted, and a
40
+ response carrying neither is refused with a message that says nothing was signed.
41
+
42
+ Latent since XRPL shipped and intermittent by nature: it depends on which backend the cluster
43
+ routes to, which is why the same wallet settled all morning and then could not settle at all.
44
+ Caught by a post-release live sweep against the published package — the pre-flight guard added
45
+ in 3.1.0 is what turned an unreadable serializer error into a sentence naming the field.
46
+
7
47
  ## [3.1.0] — 2026-09-09 — spendable is not held, and a swap refused for no reason
8
48
 
9
49
  ### Fixed
@@ -2642,6 +2682,8 @@ straight into your wallet. The API is small and self-contained.
2642
2682
  [1.5.0]: https://www.npmjs.com/package/@piprail/sdk
2643
2683
  [1.4.0]: https://www.npmjs.com/package/@piprail/sdk
2644
2684
  [1.3.1]: https://www.npmjs.com/package/@piprail/sdk
2685
+ [3.1.2]: https://www.npmjs.com/package/@piprail/sdk
2686
+ [3.1.1]: https://www.npmjs.com/package/@piprail/sdk
2645
2687
  [3.1.0]: https://www.npmjs.com/package/@piprail/sdk
2646
2688
  [3.0.0]: https://www.npmjs.com/package/@piprail/sdk
2647
2689
  [1.3.0]: https://www.npmjs.com/package/@piprail/sdk
package/dist/index.cjs CHANGED
@@ -2498,7 +2498,7 @@ var loaders = {
2498
2498
  xrpl: async () => {
2499
2499
  let mod;
2500
2500
  try {
2501
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./xrpl-G2FKFXRI.cjs")));
2501
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./xrpl-YUGDNI2R.cjs")));
2502
2502
  } catch (cause) {
2503
2503
  throw new (0, _chunk6XTNI2OQcjs.MissingDriverError)(
2504
2504
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -3800,7 +3800,16 @@ var PipRailClient = (_class2 = class {
3800
3800
  return this.opts.chain;
3801
3801
  }
3802
3802
  /**
3803
- * What this wallet actually HOLDS, per asset — the balance sheet, not the leash.
3803
+ * What this wallet can actually SPEND, per asset — the balance sheet, not the leash.
3804
+ *
3805
+ * 🔴 For a NATIVE asset this is not always what the chain says the account holds. Solana
3806
+ * (rent exemption), XRPL (base + owner reserve), Stellar (base reserve per subentry) and
3807
+ * Algorand (minimum balance) all require an account to retain a minimum it can never send,
3808
+ * and since 3.1.0 the drivers report that reserve DEDUCTED, because affordability measured
3809
+ * against the raw balance calls a payment affordable right up until the chain refuses it.
3810
+ * Live example: a Stellar account holding 1.5398 XLM with three subentries reports 0.0398,
3811
+ * the 1.5 XLM difference being the reserve. That is the number to spend against; it is not
3812
+ * missing money. `estimateCost()` judges gas against the TRUE balance separately.
3804
3813
  *
3805
3814
  * `budget()` answers "how much of my allowance is left", a different question and the only
3806
3815
  * one an agent could previously ask. An agent that OWNS a wallet has to answer "what do I
@@ -6238,6 +6247,14 @@ async function settleViaFacilitator(input) {
6238
6247
  );
6239
6248
  }
6240
6249
  if (verify.status !== 200) {
6250
+ if (verify.status === 400 || verify.status === 422) {
6251
+ const vr4 = _nullishCoalesce(verify.json, () => ( {}));
6252
+ return {
6253
+ ok: false,
6254
+ error: mapReason(vr4.invalidReason),
6255
+ detail: `Facilitator rejected the payment payload (HTTP ${verify.status})${vr4.invalidReason ? `: ${vr4.invalidReason}` : ""}${vr4.invalidMessage ? ` \u2014 ${vr4.invalidMessage}` : ""}.`
6256
+ };
6257
+ }
6241
6258
  throw new (0, _chunk6XTNI2OQcjs.SettlementError)(
6242
6259
  `exact settle (facilitator ${base2}): /verify returned HTTP ${verify.status} (transport/auth error).`
6243
6260
  );
@@ -8303,7 +8320,7 @@ function paymentTools(client) {
8303
8320
  },
8304
8321
  {
8305
8322
  name: "piprail_wallet",
8306
- description: "What YOU hold, and where you get paid \u2014 your balance sheet, which is a different question from piprail_budget (that is how much of your allowance is left). Returns your own address per chain plus the amount of each asset you actually hold. Use it before deciding to sell, swap or ask to be topped up, and give the address to anyone who needs to send you funds. A null amount means the read was UNAVAILABLE, not zero: do NOT treat it as being broke. Read-only; moves nothing and needs no approval.",
8323
+ description: "What YOU can SPEND, and where you get paid \u2014 your balance sheet, which is a different question from piprail_budget (that is how much of your allowance is left). Returns your own address per chain plus the spendable amount of each asset. Use it before deciding to sell, swap or ask to be topped up, and give the address to anyone who needs to send you funds. On some chains a native amount is LOWER than the figure a block explorer shows, because the chain makes an account retain a minimum it can never send (Solana, XRPL, Stellar, Algorand). That gap is locked, not lost, and this number is the one you can actually pay with. A null amount means the read was UNAVAILABLE, not zero: do NOT treat it as being broke. Read-only; moves nothing and needs no approval.",
8307
8324
  annotations: {
8308
8325
  title: "Your wallet",
8309
8326
  readOnlyHint: true,
package/dist/index.d.cts CHANGED
@@ -5974,7 +5974,9 @@ interface PayingClient {
5974
5974
  canAgentSell?(): boolean;
5975
5975
  /** The chain this client is configured for — the default an offer is priced on. */
5976
5976
  chain?(): ChainSelector;
5977
- /** What this wallet HOLDS, per asset — the balance sheet, distinct from the budget leash. */
5977
+ /** What this wallet can SPEND, per asset — the balance sheet, distinct from the budget
5978
+ * leash. On a chain with a retained minimum (Solana rent, XRPL/Stellar/Algorand
5979
+ * reserves) a native balance reports the reserve DEDUCTED. See {@link WalletBalance}. */
5978
5980
  balanceOf?(assets?: readonly string[]): Promise<WalletAssetBalance[]>;
5979
5981
  }
5980
5982
  /**
@@ -6214,7 +6216,16 @@ declare class PipRailClient {
6214
6216
  * defaults to it, so an agent prices on the chain it actually holds funds on. */
6215
6217
  chain(): ChainSelector;
6216
6218
  /**
6217
- * What this wallet actually HOLDS, per asset — the balance sheet, not the leash.
6219
+ * What this wallet can actually SPEND, per asset — the balance sheet, not the leash.
6220
+ *
6221
+ * 🔴 For a NATIVE asset this is not always what the chain says the account holds. Solana
6222
+ * (rent exemption), XRPL (base + owner reserve), Stellar (base reserve per subentry) and
6223
+ * Algorand (minimum balance) all require an account to retain a minimum it can never send,
6224
+ * and since 3.1.0 the drivers report that reserve DEDUCTED, because affordability measured
6225
+ * against the raw balance calls a payment affordable right up until the chain refuses it.
6226
+ * Live example: a Stellar account holding 1.5398 XLM with three subentries reports 0.0398,
6227
+ * the 1.5 XLM difference being the reserve. That is the number to spend against; it is not
6228
+ * missing money. `estimateCost()` judges gas against the TRUE balance separately.
6218
6229
  *
6219
6230
  * `budget()` answers "how much of my allowance is left", a different question and the only
6220
6231
  * one an agent could previously ask. An agent that OWNS a wallet has to answer "what do I
package/dist/index.d.ts CHANGED
@@ -5974,7 +5974,9 @@ interface PayingClient {
5974
5974
  canAgentSell?(): boolean;
5975
5975
  /** The chain this client is configured for — the default an offer is priced on. */
5976
5976
  chain?(): ChainSelector;
5977
- /** What this wallet HOLDS, per asset — the balance sheet, distinct from the budget leash. */
5977
+ /** What this wallet can SPEND, per asset — the balance sheet, distinct from the budget
5978
+ * leash. On a chain with a retained minimum (Solana rent, XRPL/Stellar/Algorand
5979
+ * reserves) a native balance reports the reserve DEDUCTED. See {@link WalletBalance}. */
5978
5980
  balanceOf?(assets?: readonly string[]): Promise<WalletAssetBalance[]>;
5979
5981
  }
5980
5982
  /**
@@ -6214,7 +6216,16 @@ declare class PipRailClient {
6214
6216
  * defaults to it, so an agent prices on the chain it actually holds funds on. */
6215
6217
  chain(): ChainSelector;
6216
6218
  /**
6217
- * What this wallet actually HOLDS, per asset — the balance sheet, not the leash.
6219
+ * What this wallet can actually SPEND, per asset — the balance sheet, not the leash.
6220
+ *
6221
+ * 🔴 For a NATIVE asset this is not always what the chain says the account holds. Solana
6222
+ * (rent exemption), XRPL (base + owner reserve), Stellar (base reserve per subentry) and
6223
+ * Algorand (minimum balance) all require an account to retain a minimum it can never send,
6224
+ * and since 3.1.0 the drivers report that reserve DEDUCTED, because affordability measured
6225
+ * against the raw balance calls a payment affordable right up until the chain refuses it.
6226
+ * Live example: a Stellar account holding 1.5398 XLM with three subentries reports 0.0398,
6227
+ * the 1.5 XLM difference being the reserve. That is the number to spend against; it is not
6228
+ * missing money. `estimateCost()` judges gas against the TRUE balance separately.
6218
6229
  *
6219
6230
  * `budget()` answers "how much of my allowance is left", a different question and the only
6220
6231
  * one an agent could previously ask. An agent that OWNS a wallet has to answer "what do I
package/dist/index.js CHANGED
@@ -2498,7 +2498,7 @@ var loaders = {
2498
2498
  xrpl: async () => {
2499
2499
  let mod;
2500
2500
  try {
2501
- mod = await import("./xrpl-5K444RGX.js");
2501
+ mod = await import("./xrpl-WIDRV2XY.js");
2502
2502
  } catch (cause) {
2503
2503
  throw new MissingDriverError(
2504
2504
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -3800,7 +3800,16 @@ var PipRailClient = class {
3800
3800
  return this.opts.chain;
3801
3801
  }
3802
3802
  /**
3803
- * What this wallet actually HOLDS, per asset — the balance sheet, not the leash.
3803
+ * What this wallet can actually SPEND, per asset — the balance sheet, not the leash.
3804
+ *
3805
+ * 🔴 For a NATIVE asset this is not always what the chain says the account holds. Solana
3806
+ * (rent exemption), XRPL (base + owner reserve), Stellar (base reserve per subentry) and
3807
+ * Algorand (minimum balance) all require an account to retain a minimum it can never send,
3808
+ * and since 3.1.0 the drivers report that reserve DEDUCTED, because affordability measured
3809
+ * against the raw balance calls a payment affordable right up until the chain refuses it.
3810
+ * Live example: a Stellar account holding 1.5398 XLM with three subentries reports 0.0398,
3811
+ * the 1.5 XLM difference being the reserve. That is the number to spend against; it is not
3812
+ * missing money. `estimateCost()` judges gas against the TRUE balance separately.
3804
3813
  *
3805
3814
  * `budget()` answers "how much of my allowance is left", a different question and the only
3806
3815
  * one an agent could previously ask. An agent that OWNS a wallet has to answer "what do I
@@ -6238,6 +6247,14 @@ async function settleViaFacilitator(input) {
6238
6247
  );
6239
6248
  }
6240
6249
  if (verify.status !== 200) {
6250
+ if (verify.status === 400 || verify.status === 422) {
6251
+ const vr4 = verify.json ?? {};
6252
+ return {
6253
+ ok: false,
6254
+ error: mapReason(vr4.invalidReason),
6255
+ detail: `Facilitator rejected the payment payload (HTTP ${verify.status})${vr4.invalidReason ? `: ${vr4.invalidReason}` : ""}${vr4.invalidMessage ? ` \u2014 ${vr4.invalidMessage}` : ""}.`
6256
+ };
6257
+ }
6241
6258
  throw new SettlementError(
6242
6259
  `exact settle (facilitator ${base2}): /verify returned HTTP ${verify.status} (transport/auth error).`
6243
6260
  );
@@ -8303,7 +8320,7 @@ function paymentTools(client) {
8303
8320
  },
8304
8321
  {
8305
8322
  name: "piprail_wallet",
8306
- description: "What YOU hold, and where you get paid \u2014 your balance sheet, which is a different question from piprail_budget (that is how much of your allowance is left). Returns your own address per chain plus the amount of each asset you actually hold. Use it before deciding to sell, swap or ask to be topped up, and give the address to anyone who needs to send you funds. A null amount means the read was UNAVAILABLE, not zero: do NOT treat it as being broke. Read-only; moves nothing and needs no approval.",
8323
+ description: "What YOU can SPEND, and where you get paid \u2014 your balance sheet, which is a different question from piprail_budget (that is how much of your allowance is left). Returns your own address per chain plus the spendable amount of each asset. Use it before deciding to sell, swap or ask to be topped up, and give the address to anyone who needs to send you funds. On some chains a native amount is LOWER than the figure a block explorer shows, because the chain makes an account retain a minimum it can never send (Solana, XRPL, Stellar, Algorand). That gap is locked, not lost, and this number is the one you can actually pay with. A null amount means the read was UNAVAILABLE, not zero: do NOT treat it as being broke. Read-only; moves nothing and needs no approval.",
8307
8324
  annotations: {
8308
8325
  title: "Your wallet",
8309
8326
  readOnlyHint: true,
@@ -745,7 +745,13 @@ function makeXrplNetwork(preset, rpcUrl) {
745
745
  },
746
746
  async currentLedgerIndex() {
747
747
  const r = await rpc("ledger_current", {});
748
- return r.ledger_current_index;
748
+ const index = r.ledger_current_index ?? r.ledger_index;
749
+ if (typeof index !== "number") {
750
+ throw new Error(
751
+ `XRPL: ledger_current returned neither ledger_current_index nor ledger_index (got ${JSON.stringify(Object.keys(r ?? {}))}). Nothing was signed or submitted.`
752
+ );
753
+ }
754
+ return index;
749
755
  },
750
756
  async submit(txBlob) {
751
757
  return rpc("submit", { tx_blob: txBlob });
@@ -745,7 +745,13 @@ function makeXrplNetwork(preset, rpcUrl) {
745
745
  },
746
746
  async currentLedgerIndex() {
747
747
  const r = await rpc("ledger_current", {});
748
- return r.ledger_current_index;
748
+ const index = _nullishCoalesce(r.ledger_current_index, () => ( r.ledger_index));
749
+ if (typeof index !== "number") {
750
+ throw new Error(
751
+ `XRPL: ledger_current returned neither ledger_current_index nor ledger_index (got ${JSON.stringify(Object.keys(_nullishCoalesce(r, () => ( {}))))}). Nothing was signed or submitted.`
752
+ );
753
+ }
754
+ return index;
749
755
  },
750
756
  async submit(txBlob) {
751
757
  return rpc("submit", { tx_blob: txBlob });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piprail/sdk",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "Accept x402 crypto payments across 30 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",