@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/CHANGELOG.md +105 -0
- package/dist/{algorand-W776EEM2.cjs → algorand-4FTTEV7X.cjs} +7 -1
- package/dist/{algorand-HL57PQHE.js → algorand-KJ5XHSMT.js} +7 -1
- package/dist/index.cjs +389 -230
- package/dist/index.d.cts +54 -8
- package/dist/index.d.ts +54 -8
- package/dist/index.js +199 -40
- package/dist/{ledger-Crc1bZox.d.cts → ledger-DkHUORUe.d.cts} +7 -0
- package/dist/{ledger-Crc1bZox.d.ts → ledger-DkHUORUe.d.ts} +7 -0
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/{solana-EBV6PUCU.cjs → solana-TIEJV742.cjs} +7 -1
- package/dist/{solana-O6Q6QILH.js → solana-WRXL54MR.js} +7 -1
- package/dist/{stellar-5C7FQLPS.cjs → stellar-5GMZJBTA.cjs} +9 -1
- package/dist/{stellar-YF5LOJEM.js → stellar-YQLOIFDD.js} +9 -1
- package/dist/{xrpl-GOVHMYYK.js → xrpl-5K444RGX.js} +16 -1
- package/dist/{xrpl-YS3IXPLV.cjs → xrpl-G2FKFXRI.cjs} +16 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,110 @@ 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.0] — 2026-09-09 — spendable is not held, and a swap refused for no reason
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- 🔴 **A spend cap could be breached by CONCURRENT payments.** A cap is read when a quote is
|
|
12
|
+
priced and written when the payment settles, and a whole network round trip sits between the
|
|
13
|
+
two. Six simultaneous `fetch()` calls against a `maxTotal` of `'2.50'` each priced against the
|
|
14
|
+
same "spent so far", each passed, and four settled: 4.00 spent against a 2.50 leash while every
|
|
15
|
+
individual check was correct. `maxPayments` and `maxTotalPerDenom` leaked the same way. It is
|
|
16
|
+
the same read-await-write shape as the replay race, on the buyer's side of the wire, and the
|
|
17
|
+
leash is the entire safety story for the default `budgeted` mode.
|
|
18
|
+
|
|
19
|
+
`authorize()` now RESERVES the budget synchronously before anything is signed or sent, so an
|
|
20
|
+
in-flight payment is visible to the next one's check. `recordSpend()` commits the reservation
|
|
21
|
+
as the real record lands, and every failure path releases it, so a refused or failed payment
|
|
22
|
+
never permanently consumes the leash. Sequential behaviour is unchanged: a 3.00 cap still
|
|
23
|
+
spends exactly 3.00, no more and no fewer.
|
|
24
|
+
|
|
25
|
+
- 🔴 **A custom `isUsed`/`markUsed` replay store could redeem ONE proof N times, concurrently.**
|
|
26
|
+
The gate's built-in set has always reserved a ref synchronously, which is what stops two
|
|
27
|
+
simultaneous requests carrying the same proof from both settling. The CUSTOM-store branch did
|
|
28
|
+
not: it `await`ed `isUsed(ref)` before verification and wrote only after it, so every concurrent
|
|
29
|
+
request read "unused" and every one settled. Five simultaneous requests redeemed one payment
|
|
30
|
+
five times. The gate now takes the same synchronous in-process reservation BEFORE consulting a
|
|
31
|
+
custom store, so a single instance is safe whichever store is configured. Across several
|
|
32
|
+
processes that set cannot help, so an atomic check-and-reserve (Redis `SET NX`) is still what
|
|
33
|
+
protects a multi-instance deployment, and the documented example now uses one: it previously
|
|
34
|
+
showed `exists()` then `set()`, which is the exact non-atomic pattern the surrounding text
|
|
35
|
+
warned against. Found by a concurrency sweep; the suite had no concurrent replay test at all.
|
|
36
|
+
|
|
37
|
+
- 🔴 **A sovereign agent could deliver N goods for ONE payment.** `piprail_sell` gives every offer
|
|
38
|
+
its own gate, and a gate's replay set is scoped to itself, so cross-offer replay was guarded by
|
|
39
|
+
a store-level `isUsed`/`markUsed` pair sharing the race above. Sequentially it refused correctly;
|
|
40
|
+
five concurrent `piprail_collect` calls against five offers all collected the same settlement,
|
|
41
|
+
which is the normal shape of a shop with more than one buyer. `collect` now reserves the proof
|
|
42
|
+
ref synchronously before any await, and releases it if the payment does not settle.
|
|
43
|
+
|
|
44
|
+
- **A chain id that is not a chain id is refused at config time.** `{ id: NaN }` (and negative,
|
|
45
|
+
fractional or unsafe-integer ids) resolved happily, and the gate went on to publish
|
|
46
|
+
`network: "eip155:NaN"` in a live 402 — an unparseable CAIP-2 that a standard x402 client cannot
|
|
47
|
+
read. It failed closed later, but surfaced as `tx_not_found` at payment time rather than as the
|
|
48
|
+
configuration error it is.
|
|
49
|
+
|
|
50
|
+
- **A gate must charge more than zero.** `amount: '0'` built a rail whose amount check any transfer
|
|
51
|
+
satisfies, so a paywall could read as configured and gate nothing. A metered `upto` SETTLE of
|
|
52
|
+
zero stays legitimate, because that is the settled amount and not the advertised one.
|
|
53
|
+
`piprail_sell` inherits the same floor, so a zero price can no longer mint an offer.
|
|
54
|
+
|
|
55
|
+
- **A facilitator's cold start no longer costs the buyer gas.** The lazy `/supported` probe that
|
|
56
|
+
discovers a facilitator's fee payer timed out at 8s. These are serverless hosts: measured
|
|
57
|
+
2026-09-09, `x402.dexter.cash` answered in **8497ms cold** and ~310ms warm, so a cold facilitator
|
|
58
|
+
read as absent, the gasless `exact` rail was dropped, and the buyer paid gas instead. The probe
|
|
59
|
+
now allows 15s. It only runs for a family that cannot resolve `exact` without a fee payer
|
|
60
|
+
(Solana; EVM never reaches it), at most once per gate, and it still fails safe with the same
|
|
61
|
+
`skipReason` when the host really is down. Live re-probe after the change: **9/9 registry hosts
|
|
62
|
+
answering, 0 contradicted claims**.
|
|
63
|
+
|
|
64
|
+
- **Authority is sealed on the client instance.** `paymentTools()` picks a model's tool set from
|
|
65
|
+
`canAgentSell()` / `canAgentSwap()`, which read `mode()`. Those were plain prototype methods, so
|
|
66
|
+
any code holding the client could reassign one and turn a budgeted client's eight tools into
|
|
67
|
+
sovereign's fourteen. A MODEL could never do that — it sends JSON tool arguments and does not
|
|
68
|
+
hold the object — so this is defence in depth for a client passing through an agent framework,
|
|
69
|
+
a plugin, or middleware that wraps objects. All three are now non-writable and non-configurable:
|
|
70
|
+
authority is set once, by whoever provisioned the key.
|
|
71
|
+
|
|
72
|
+
- 🔴 **`planPayment` called a native payment affordable that the chain then refused, on FOUR
|
|
73
|
+
families.** Some chains require an account to retain a minimum it can never send, and
|
|
74
|
+
affordability was measured against the raw balance on every one of them:
|
|
75
|
+
|
|
76
|
+
| | retained minimum |
|
|
77
|
+
|---|---|
|
|
78
|
+
| Solana | the account's rent-exempt minimum |
|
|
79
|
+
| XRPL | a base reserve, plus an increment for each owned object (a trustline is one) |
|
|
80
|
+
| Stellar | `(2 + subentries) x` the base reserve, and a trustline is a subentry |
|
|
81
|
+
| Algorand | 0.1 ALGO, plus 0.1 for every ASA opted into |
|
|
82
|
+
|
|
83
|
+
A live wallet holding 0.0011 SOL was told it could send 0.0005 SOL; the transfer failed
|
|
84
|
+
simulation with a bare `SendTransactionError` after the agent had already signed, which is the
|
|
85
|
+
exact outcome the pre-flight check exists to prevent.
|
|
86
|
+
|
|
87
|
+
`WalletBalance.token` is now documented as the SPENDABLE figure (the reserve deducted) with
|
|
88
|
+
`native` staying the true balance for gas, all four drivers report it that way (reading the
|
|
89
|
+
chain's own figure where it offers one), and the client measures a native payment against it.
|
|
90
|
+
A family with no reserve reports the two as equal and is unchanged. The refusal names the
|
|
91
|
+
exact shortfall, and a payment comfortably inside the spendable balance still goes through.
|
|
92
|
+
|
|
93
|
+
- **XRPL: a throttled ledger read no longer surfaces as a serializer error.** `Sequence` and
|
|
94
|
+
`LastLedgerSequence` are UInt32 fields, so a failed or rate-limited pre-flight read left one
|
|
95
|
+
`undefined` (and `undefined + 20` is `NaN`), and xrpl.js rejected the transaction with
|
|
96
|
+
"Cannot construct UInt32 from given value": no field, no cause and no remedy, from a library
|
|
97
|
+
the caller never imported. Found under a batch of live mainnet payments, where the public
|
|
98
|
+
cluster throttles. Both reads are now checked before the transaction is built, and the refusal
|
|
99
|
+
says the thing that matters most on an ambiguous payment error: nothing was signed or
|
|
100
|
+
submitted, so retrying cannot double-pay.
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- **`releaseUsed` — the third replay hook.** Optional, and only meaningful when `isUsed` RESERVES
|
|
105
|
+
(the `SET NX` shape). Without it, making a custom store atomic traded a double-spend for a worse
|
|
106
|
+
failure: a transient RPC error left the reservation standing, so a buyer whose funds had already
|
|
107
|
+
moved could never redeem the proof. The built-in store has always released on failure; this is
|
|
108
|
+
how a custom store does the same. Supplying it without `isUsed`/`markUsed` throws, because it
|
|
109
|
+
would never fire.
|
|
110
|
+
|
|
7
111
|
## [3.0.0] — 2026-09-09 — agent modes: a wallet an agent can EARN with, not only spend from
|
|
8
112
|
|
|
9
113
|
### BREAKING
|
|
@@ -2538,6 +2642,7 @@ straight into your wallet. The API is small and self-contained.
|
|
|
2538
2642
|
[1.5.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
2539
2643
|
[1.4.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
2540
2644
|
[1.3.1]: https://www.npmjs.com/package/@piprail/sdk
|
|
2645
|
+
[3.1.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
2541
2646
|
[3.0.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
2542
2647
|
[1.3.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
2543
2648
|
[1.2.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
@@ -550,6 +550,8 @@ function resolveAlgorandWallet(config) {
|
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
// src/drivers/algorand/index.ts
|
|
553
|
+
var ALGO_MIN_BALANCE = 100000n;
|
|
554
|
+
var ALGO_MIN_BALANCE_PER_ASSET = 100000n;
|
|
553
555
|
var algorandDriver = {
|
|
554
556
|
family: "algorand",
|
|
555
557
|
resolve(opts) {
|
|
@@ -717,7 +719,11 @@ function makeAlgorandNetwork(preset, algodUrl) {
|
|
|
717
719
|
return { token: null, native: null };
|
|
718
720
|
}
|
|
719
721
|
const native = info.amount != null ? BigInt(info.amount) : null;
|
|
720
|
-
if (asset === "native")
|
|
722
|
+
if (asset === "native") {
|
|
723
|
+
if (native === null) return { token: null, native };
|
|
724
|
+
const min = info.minBalance != null ? BigInt(info.minBalance) : ALGO_MIN_BALANCE + ALGO_MIN_BALANCE_PER_ASSET * BigInt((_nullishCoalesce(info.assets, () => ( []))).length);
|
|
725
|
+
return { token: native > min ? native - min : 0n, native };
|
|
726
|
+
}
|
|
721
727
|
const assetId = parseAlgorandAssetId(asset);
|
|
722
728
|
const holding = (_nullishCoalesce(info.assets, () => ( []))).find((a) => Number(a.assetId) === assetId);
|
|
723
729
|
return { token: holding ? BigInt(holding.amount) : 0n, native };
|
|
@@ -550,6 +550,8 @@ function resolveAlgorandWallet(config) {
|
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
// src/drivers/algorand/index.ts
|
|
553
|
+
var ALGO_MIN_BALANCE = 100000n;
|
|
554
|
+
var ALGO_MIN_BALANCE_PER_ASSET = 100000n;
|
|
553
555
|
var algorandDriver = {
|
|
554
556
|
family: "algorand",
|
|
555
557
|
resolve(opts) {
|
|
@@ -717,7 +719,11 @@ function makeAlgorandNetwork(preset, algodUrl) {
|
|
|
717
719
|
return { token: null, native: null };
|
|
718
720
|
}
|
|
719
721
|
const native = info.amount != null ? BigInt(info.amount) : null;
|
|
720
|
-
if (asset === "native")
|
|
722
|
+
if (asset === "native") {
|
|
723
|
+
if (native === null) return { token: null, native };
|
|
724
|
+
const min = info.minBalance != null ? BigInt(info.minBalance) : ALGO_MIN_BALANCE + ALGO_MIN_BALANCE_PER_ASSET * BigInt((info.assets ?? []).length);
|
|
725
|
+
return { token: native > min ? native - min : 0n, native };
|
|
726
|
+
}
|
|
721
727
|
const assetId = parseAlgorandAssetId(asset);
|
|
722
728
|
const holding = (info.assets ?? []).find((a) => Number(a.assetId) === assetId);
|
|
723
729
|
return { token: holding ? BigInt(holding.amount) : 0n, native };
|