@piprail/sdk 1.4.0 → 1.5.1
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/CHAINS.md +2 -2
- package/CHANGELOG.md +65 -0
- package/ERRORS.md +15 -3
- package/README.md +38 -4
- package/dist/{algorand-XJ5OVWQB.js → algorand-B67G4335.js} +34 -0
- package/dist/{algorand-IDFUG5CI.cjs → algorand-IJJKE35X.cjs} +38 -4
- package/dist/{aptos-TPSOQ2VL.cjs → aptos-X3G2UBYW.cjs} +28 -0
- package/dist/{aptos-AWWSCPDH.js → aptos-YQWTGFRZ.js} +28 -0
- package/dist/index.cjs +307 -29
- package/dist/index.d.cts +174 -15
- package/dist/index.d.ts +174 -15
- package/dist/index.js +295 -17
- package/dist/{near-NOJTO4GX.js → near-7MBBCDUE.js} +50 -0
- package/dist/{near-KDA5DPTX.cjs → near-GGUHLXAF.cjs} +57 -7
- package/dist/{solana-HNRTS4KM.js → solana-7WJVZGDW.js} +22 -0
- package/dist/{solana-DVA6I55L.cjs → solana-W24TCJV4.cjs} +25 -3
- package/dist/{stellar-4TDVVJYO.js → stellar-HV6VGZX3.js} +50 -0
- package/dist/{stellar-4D5EWT3V.cjs → stellar-YMY3K2YB.cjs} +50 -0
- package/dist/{sui-ALUTM5GX.js → sui-2WFWVFJX.js} +23 -0
- package/dist/{sui-5HMIHOZK.cjs → sui-32KVESR5.cjs} +23 -0
- package/dist/{ton-3XMIM2FU.js → ton-DGZB7W4U.js} +23 -0
- package/dist/{ton-TVK4TEDX.cjs → ton-FIQGV2LC.cjs} +23 -0
- package/dist/{tron-6D65YJEU.js → tron-RLIL2FDI.js} +28 -0
- package/dist/{tron-Y5RZJZRT.cjs → tron-ZSXAPZ2C.cjs} +28 -0
- package/dist/{xrpl-ICO6G7UK.cjs → xrpl-2PKP7HOI.cjs} +61 -1
- package/dist/{xrpl-ISFG3SSN.js → xrpl-UEC2GYVV.js} +60 -0
- package/package.json +2 -2
package/CHAINS.md
CHANGED
|
@@ -13,7 +13,7 @@ read those sections before you ship them.
|
|
|
13
13
|
|
|
14
14
|
| Chain(s) | Pay in native coin? | Built-in stablecoins | Receiver needs setup? | Wallet input |
|
|
15
15
|
|---|:--:|---|---|---|
|
|
16
|
-
| **EVM** (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, Avalanche, Mantle, Sonic, Linea, Scroll, Celo, zkSync, Unichain, World Chain, Sei, Injective, + any EVM chain) | ✅ ETH/BNB/POL/… | USDC (all) · USDT (all **except Base, World Chain, Sei**) | No | `{ privateKey }` |
|
|
16
|
+
| **EVM** (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, Avalanche, Mantle, Sonic, Linea, Scroll, Celo, zkSync, Unichain, World Chain, Sei, Injective, HyperEVM, Monad, + any EVM chain) | ✅ ETH/BNB/POL/… | USDC (all) · USDT (all **except Base, World Chain, Sei, HyperEVM, Monad**) | No | `{ privateKey }` |
|
|
17
17
|
| **Solana** | ✅ SOL | USDC · USDT | No (payer creates the recipient's token account) | `{ secretKey }` |
|
|
18
18
|
| **Sui** | ✅ SUI | USDC (no USDT) | No | `{ privateKey }` (`suiprivkey1…`) |
|
|
19
19
|
| **Aptos** | ✅ APT | USDC · USDT | No (primary FA store auto-creates) | `{ privateKey }` (`ed25519-priv-0x…`) |
|
|
@@ -45,7 +45,7 @@ read those sections before you ship them.
|
|
|
45
45
|
|
|
46
46
|
### EVM — Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, Avalanche, …
|
|
47
47
|
- **Pay in:** native coin (`'native'`), `'USDC'`, `'USDT'`, or a custom `{ address, decimals }`.
|
|
48
|
-
- **USDT gap:** built in on every preset **except Base, World Chain, and
|
|
48
|
+
- **USDT gap:** built in on every preset **except Base, World Chain, Sei, HyperEVM, and Monad** (USDC only there).
|
|
49
49
|
- **Decimals:** on **BNB Chain**, Binance-Peg USDC/USDT are **18 decimals**, not 6 (the SDK handles it; don't hardcode 6).
|
|
50
50
|
- **USDT branding:** on **Arbitrum, Polygon, and Unichain** the canonical Tether is the omnichain **USD₮0 / USDT0** (LayerZero), and on **Celo** it's native **USD₮** — all genuine, Tether-issued, 6-decimal USDT at the addresses shipped (verified live on-chain by symbol/decimals/supply). You still ask for it as `token: 'USDT'`; only the on-chain `symbol()` string differs from the plain `USDT` your wallet may show elsewhere.
|
|
51
51
|
- **Receiver setup:** none — any `0x…` address receives ERC-20 or native immediately.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,64 @@ 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
|
+
## [1.5.1] — 2026-06-04
|
|
8
|
+
|
|
9
|
+
**Cosmetic polish — docs & comments only, zero behavior change.** A repo-wide tidy pass so the
|
|
10
|
+
in-code docs match the SDK as it actually ships (10 families / 28 chains). No runtime, API, type,
|
|
11
|
+
or wire change — every existing program behaves identically.
|
|
12
|
+
|
|
13
|
+
- **JSDoc parity across the public surface.** The `chain` / `token` / `payTo` / wallet docs on
|
|
14
|
+
`RequirePaymentOptions`, `AcceptOption`, and `PipRailClientOptions` now enumerate all 10 families
|
|
15
|
+
(Aptos + Algorand were missing); the typed error JSDoc (`WrongFamilyError`, `UnknownTokenError`,
|
|
16
|
+
`MissingDriverError`, `RecipientNotReadyError`) lists every family + install command + custom-token form.
|
|
17
|
+
- **Stale comments corrected.** Native TRX and native NEAR are documented as the payment assets they've
|
|
18
|
+
been since 1.1.0 (the old "not a payment asset" / "`'native'` is rejected" notes were removed); the
|
|
19
|
+
`'native'` coin list, the barrel header, the tsup code-split note, and the lazy-mount docs now name all
|
|
20
|
+
9 non-EVM families; the `paymentTools` doc says "three tools" (quote · plan · pay).
|
|
21
|
+
- **Driver-family symmetry.** `evm/wallet.ts` gained the `── EVM SECTION: wallet ──` banner the other 9
|
|
22
|
+
families carry, and `evm/index.ts`'s `recipientReady()` comment now uses the shared "No receive
|
|
23
|
+
prerequisite —" lead-in.
|
|
24
|
+
- **Docs:** README contract-method list adds `balanceOf` / `recipientReady`; README custom-token examples
|
|
25
|
+
add Aptos + Algorand; CHAINS.md lists HyperEVM + Monad (and their USDT gap); ERRORS.md + AGENTS.md list
|
|
26
|
+
all 10 families; CHANGELOG version footer links restored.
|
|
27
|
+
- **Packaging:** `algosdk` moved to its alphabetical slot in `peerDependencies` (no dependency change).
|
|
28
|
+
|
|
29
|
+
## [1.5.0] — 2026-06-04
|
|
30
|
+
|
|
31
|
+
**The killer agent feature — `client.planPayment(url)`.** A read-only call that surveys a 402
|
|
32
|
+
across every rail it offers *on your chain* against your wallet's OWN holdings — **token balance +
|
|
33
|
+
native gas + recipient-readiness** (trustline / ATA / storage_deposit / ASA opt-in / activation) —
|
|
34
|
+
and tells you, crystal-clear, whether it's settleable, on which rail, and if not, exactly what to
|
|
35
|
+
top up. It completes the trio the SDK already ships: **`quote()` (what it costs) → `estimateCost()`
|
|
36
|
+
(the gas) → `planPayment()` (can I actually settle, and where).** Fully backward-compatible and
|
|
37
|
+
opt-in; defaults are unchanged. The official x402 client picks `accepts[0]` blind; PipRail is the
|
|
38
|
+
only backendless SDK that can answer "can I actually pay this?" across 28 chains with pure RPC
|
|
39
|
+
reads, no oracle/facilitator/bridge. Live-proven on Algorand mainnet (ready / recipient-not-ready /
|
|
40
|
+
insufficient / multi-rail-rank, 4/4).
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- **`client.planPayment(url, init?)` → `PaymentPlan | null`.** Never throws for a read problem (a
|
|
44
|
+
transient/RPC failure surfaces as a rail in `state: 'unknown'` + a warning, never a false
|
|
45
|
+
"unaffordable"); returns `null` when the URL isn't 402-gated; and when the 402 offers no rail on
|
|
46
|
+
your chain it EXPLAINS that (status `blocked` + a hint) instead of throwing. The plan carries:
|
|
47
|
+
`payable` + `best` (the cheapest settleable rail), `options[]` (every rail with typed `blockers`
|
|
48
|
+
— `INSUFFICIENT_TOKEN`/`INSUFFICIENT_GAS`/`RECIPIENT_NOT_READY`/`OUTSIDE_POLICY` — plus soft
|
|
49
|
+
`warnings`, a `shortfall`, live `balance`, and `recipient.fix`), and a one-sentence `fundingHint`.
|
|
50
|
+
- **`client.canAfford(url)` → `boolean`** — convenience over the above.
|
|
51
|
+
- **`fetch(url, { autoRoute: true })` / `new PipRailClient({ autoRoute: true })`** — opt-in:
|
|
52
|
+
`fetch` pays the cheapest rail the wallet can ACTUALLY settle (not the first policy-passing one),
|
|
53
|
+
or throws `PaymentDeclinedError` carrying the funding hint before any send. **Default off** —
|
|
54
|
+
the zero-config path is byte-identical.
|
|
55
|
+
- **`planAcross(clients, url)`** — the cross-chain brain: give it one client per chain you fund and
|
|
56
|
+
it merges their plans, payable-first (no oracle, so the cross-coin tiebreak is your client order).
|
|
57
|
+
- **`piprail_plan_payment`** agent tool (budget-bound; `paymentTools(client)` now returns 3 tools).
|
|
58
|
+
- **Driver contract:** `balanceOf(wallet, asset)` + `recipientReady(payTo, asset)` on every family
|
|
59
|
+
(10/10), RPC-read-only and NEVER-throw (transient ⇒ `null`/`'unknown'`, per ERRORS.md §5). Real
|
|
60
|
+
receive-prerequisite probes on NEAR (`storage_balance_of`), Stellar/XRPL (trustline presence),
|
|
61
|
+
Algorand (ASA opt-in); truthful `'n/a'` on EVM/Solana/TON/Tron/Sui/Aptos (no prerequisite).
|
|
62
|
+
- New exported types: `PaymentPlan`, `PayOption`, `PayBlocker`, `PayWarning`, `RecipientReason`,
|
|
63
|
+
`WalletBalance` (and the previously-missing `AptosToken`/`AlgorandToken`).
|
|
64
|
+
|
|
7
65
|
## [1.4.0] — 2026-06-04
|
|
8
66
|
|
|
9
67
|
A new chain **family** — **Algorand** — the **10th driver family**, bringing the built-in count to
|
|
@@ -348,6 +406,13 @@ straight into your wallet. The API is small and self-contained.
|
|
|
348
406
|
to your wallet; PipRail never holds funds.
|
|
349
407
|
- `viem ^2.21` is a peer dependency. Node 20+ or a modern browser.
|
|
350
408
|
|
|
409
|
+
[1.5.1]: https://www.npmjs.com/package/@piprail/sdk
|
|
410
|
+
[1.5.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
411
|
+
[1.4.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
412
|
+
[1.3.1]: https://www.npmjs.com/package/@piprail/sdk
|
|
413
|
+
[1.3.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
414
|
+
[1.2.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
415
|
+
[1.1.1]: https://www.npmjs.com/package/@piprail/sdk
|
|
351
416
|
[1.1.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
352
417
|
[1.0.0]: https://www.npmjs.com/package/@piprail/sdk
|
|
353
418
|
[0.1.0]: https://www.npmjs.com/package/@piprail/sdk
|
package/ERRORS.md
CHANGED
|
@@ -149,6 +149,17 @@ Every `PaymentDriver` / `ResolvedNetwork` method has a fixed error behaviour:
|
|
|
149
149
|
| `send(wallet, accept)` | wrap the broadcast; map **sender** affordability → `InsufficientFundsError` (§6) and **recipient** setup → `RecipientNotReadyError` (§6.1); **rethrow everything else unchanged** (never swallow). Every mapped throw carries `{ cause }` = the raw chain error. |
|
|
150
150
|
| `verify(ref, accept)` | **return** a `VerifyResult` with a canonical `VerifyErrorCode`. **Guard every RPC read** so a transient failure returns `tx_not_found` — `verify()` must not throw for an RPC hiccup. Re-derive the watched account from the trusted `accept`, never the client ref. |
|
|
151
151
|
| `confirm(ref, n)` | broadcast-but-not-confirmed / timeout → `ConfirmationTimeoutError`. |
|
|
152
|
+
| `estimateCost(accept, opts?)` | **never throw** — guard the RPC read and fall back to a `'heuristic'` constant; always return a valid `CostEstimate`. |
|
|
153
|
+
| `balanceOf(wallet, asset)` | **never throw** — RPC-read-only. A field whose read was unavailable (transient/rate-limit) returns `null`, NOT `0` (a false 0 reads as "broke"). For `asset==='native'`, `token === native`. |
|
|
154
|
+
| `recipientReady(payTo, asset)` | **never throw** — report the receive prerequisite: `{ ready:'n/a' }` (no prerequisite on this family/native), `{ ready:true }`, `{ ready:false, reason }` (a `RecipientReason`), or `{ ready:'unknown' }` on a transient read. `'n/a'` must be TRUTHFUL — never a stand-in for "didn't check". |
|
|
155
|
+
|
|
156
|
+
> **`planPayment` is a RETURN-channel feature.** The client's `planPayment`/`canAfford` compose
|
|
157
|
+
> `balanceOf` + `recipientReady` + `estimateCost` + the policy verdict into a `PaymentPlan` — and,
|
|
158
|
+
> like `verify()`, they **return** the outcome rather than throwing: a transient read becomes a rail
|
|
159
|
+
> in `state:'unknown'` (+ a warning), an unsettleable rail carries typed `blockers`, and a 402 with
|
|
160
|
+
> no rail on the client's chain is *explained* in the plan. The only throw is `InvalidEnvelopeError`
|
|
161
|
+
> on an unparseable challenge. (`fetch({ autoRoute:true })` is the one place a plan turns into a
|
|
162
|
+
> THROWN `PaymentDeclinedError` — refusing before any send when nothing is settleable.)
|
|
152
163
|
|
|
153
164
|
### 6. Affordability converges on one error, by two mechanisms
|
|
154
165
|
|
|
@@ -194,9 +205,10 @@ the reader, full raw detail for the debugger — both, always. Chains with no re
|
|
|
194
205
|
|
|
195
206
|
## 7. Registry / loader pattern
|
|
196
207
|
|
|
197
|
-
- EVM is registered eagerly (`viem` is the one hard peer dep).
|
|
198
|
-
lazily via a single dynamic
|
|
199
|
-
first time
|
|
208
|
+
- EVM is registered eagerly (`viem` is the one hard peer dep). Every non-EVM family (Solana,
|
|
209
|
+
TON, Tron, NEAR, Sui, Stellar, XRPL, Aptos, Algorand) mounts lazily via a single dynamic
|
|
210
|
+
`import()` in [`drivers/index.ts`](src/drivers/index.ts) the first time its `chain` is
|
|
211
|
+
named — no setup call.
|
|
200
212
|
- A failed lazy `import()` → `MissingDriverError` naming the exact `npm install` + `{ cause }`.
|
|
201
213
|
The in-flight promise isn't cached on failure, so a later call can retry.
|
|
202
214
|
- No driver for the family, or `resolve()` → `null` → `UnsupportedNetworkError`.
|
package/README.md
CHANGED
|
@@ -77,13 +77,41 @@ client.spent() // → { count, byAsset: [{ symbol:'USDC', totalFormatted:'0.05',
|
|
|
77
77
|
|
|
78
78
|
**Know the gas before you pay.** `client.estimateCost(url)` returns the quote **and** a `CostEstimate` — the network fee in the chain's **native coin** (you pay in USDC but burn ETH / SOL / TON / XLM / XRP / TRX on gas, a separate balance the agent must keep topped up). It's best-effort and labelled (`cost.basis`): a live-RPC read where cheap (`'estimated'` — EVM gas price, XRPL fee), a typical-cost constant otherwise (`'heuristic'`), and it never throws. Most valuable on **Tron**, where a USD₮ transfer can cost real TRX. So an agent can budget the *total* — payment **+** gas — before any funds move. Every driver implements it; the math is extracted per-chain and shaped uniformly by one shared `nativeCost()` helper.
|
|
79
79
|
|
|
80
|
+
### Plan before you pay — `planPayment()` (never fumble a payment)
|
|
81
|
+
|
|
82
|
+
`quote()` tells you the price and `estimateCost()` the gas — **`planPayment(url)`** closes the loop: **one read-only call** that checks, against your wallet's *own* holdings, whether a 402 will actually go through — and if not, exactly what to fix. No funds move.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const plan = await client.planPayment(url)
|
|
86
|
+
|
|
87
|
+
if (plan?.payable) {
|
|
88
|
+
await client.fetch(url, { autoRoute: true }) // pays plan.best — the cheapest rail you can settle
|
|
89
|
+
} else {
|
|
90
|
+
console.log(plan?.fundingHint)
|
|
91
|
+
// "Have the USDC, but need ~0.000021 ETH for gas on base (have 0)."
|
|
92
|
+
// "Recipient 2OT6…GC5E4 can't receive on algorand yet — must opt into the USDC ASA."
|
|
93
|
+
// "Top up 0.04 USDC on base (have 0.01)."
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For **every rail the 402 offers on your chain**, the plan reads **token balance + native-coin gas + recipient-readiness** (trustline / ATA / `storage_deposit` / ASA opt-in / activation) and returns:
|
|
98
|
+
- **`payable`** + **`best`** — the cheapest rail you can actually settle (recipient confirmed able to receive);
|
|
99
|
+
- **`options[]`** — each rail with typed **`blockers`** (`INSUFFICIENT_TOKEN` · `INSUFFICIENT_GAS` · `RECIPIENT_NOT_READY` · `OUTSIDE_POLICY`), soft **`warnings`** (`SYMBOL_MISMATCH`, `THIN_GAS_MARGIN`, `BALANCE_UNREADABLE`, …), a **`shortfall`**, the live **`balance`**, and **`recipient.fix`**;
|
|
100
|
+
- **`fundingHint`** — one human sentence on exactly what to top up.
|
|
101
|
+
|
|
102
|
+
**Why it's the agent unlock.** The official x402 client picks `accepts[0]` blind and learns it can't pay only when the broadcast reverts (no token, no gas) or the transfer silently strands (recipient not set up to receive). `planPayment` turns those runtime failures into a pre-checked decision — and on the no-facilitator path *you* pay your own gas, so "I hold USDC but no ETH" is a first-class answer, not a crash. It **never throws for a read hiccup** (a throttled RPC surfaces as `state: 'unknown'` + a warning, never a false "broke"), returns `null` when the URL isn't gated, and *explains* "this is offered on solana, base — you're on xrpl" instead of erroring. `client.canAfford(url)` is the one-boolean convenience.
|
|
103
|
+
|
|
104
|
+
**Auto-route (opt-in).** `new PipRailClient({ autoRoute: true })` (or `fetch(url, { autoRoute: true })`) makes `fetch` pay the cheapest *settleable* rail instead of the first policy-passing one — refusing with `PaymentDeclinedError` + the funding hint before any send. **Default off; the zero-config path is unchanged.**
|
|
105
|
+
|
|
106
|
+
**Across chains.** A client is bound to one chain; **`planAcross([baseClient, solanaClient, …], url)`** runs each plan in parallel and merges them payable-first, so an agent holding funds on several chains learns which to use. (No price oracle — cross-coin ties break on the order you list the clients.)
|
|
107
|
+
|
|
80
108
|
### Hand an LLM a budget-bound wallet
|
|
81
109
|
|
|
82
110
|
`paymentTools(client)` returns framework-agnostic tool descriptors (name + description + JSON Schema + `invoke`) — drop them into MCP, the Vercel AI SDK, OpenAI/Anthropic function-calling, or LangChain in a couple of lines. The budget rides on the client, so the model can't overspend.
|
|
83
111
|
|
|
84
112
|
```ts
|
|
85
113
|
import { paymentTools } from '@piprail/sdk'
|
|
86
|
-
const tools = paymentTools(client) // → [piprail_quote_payment, piprail_pay_request]
|
|
114
|
+
const tools = paymentTools(client) // → [piprail_quote_payment, piprail_plan_payment, piprail_pay_request]
|
|
87
115
|
```
|
|
88
116
|
|
|
89
117
|
See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK wiring.
|
|
@@ -248,6 +276,12 @@ requirePayment({ chain: 'near', token: { contractId: 'token.near', decimals: 6 }
|
|
|
248
276
|
|
|
249
277
|
// On Sui, a custom coin is { coinType, decimals }:
|
|
250
278
|
requirePayment({ chain: 'sui', token: { coinType: '0x…::usdc::USDC', decimals: 6 }, amount: '0.05', payTo })
|
|
279
|
+
|
|
280
|
+
// On Aptos, a custom Fungible Asset is { metadata, decimals }:
|
|
281
|
+
requirePayment({ chain: 'aptos', token: { metadata: '0x…', decimals: 6 }, amount: '0.05', payTo })
|
|
282
|
+
|
|
283
|
+
// On Algorand, a custom ASA is { assetId, decimals }:
|
|
284
|
+
requirePayment({ chain: 'algorand', token: { assetId: 12345678, decimals: 6 }, amount: '0.05', payTo })
|
|
251
285
|
```
|
|
252
286
|
|
|
253
287
|
> **Production:** the built-in chains use public RPCs (rate-limited). Pass your own `rpcUrl` for real traffic.
|
|
@@ -477,7 +511,7 @@ The SDK is browser-clean (no Node-only globals in the protocol layer), so a plai
|
|
|
477
511
|
Two layers, one contract. Worth knowing if you're extending the SDK or auditing it.
|
|
478
512
|
|
|
479
513
|
- **The protocol layer is chain-agnostic.** `server.ts` (`requirePayment`/`createPaymentGate`), `client.ts` (`PipRailClient`), `x402.ts` (wire envelopes), `policy.ts`, `ledger.ts`, and `agent.ts` depend **only** on the `PaymentDriver` contract in `drivers/types.ts` — zero `viem`, zero `@solana/web3.js`, zero chain SDK. The chain is data the caller passes, not an allowlist the SDK ships.
|
|
480
|
-
- **The `PaymentDriver` contract.** `resolve(chain)` → a bound `ResolvedNetwork` exposing `resolveToken` · `describeAsset` · `assertValidPayTo` · `bindWallet` · `send` · `confirm` · `estimateCost` · `verify`. That's the entire boundary every family implements and the protocol layer ever sees.
|
|
514
|
+
- **The `PaymentDriver` contract.** `resolve(chain)` → a bound `ResolvedNetwork` exposing `resolveToken` · `describeAsset` · `assertValidPayTo` · `bindWallet` · `send` · `confirm` · `estimateCost` · `balanceOf` · `recipientReady` · `verify`. That's the entire boundary every family implements and the protocol layer ever sees.
|
|
481
515
|
- **Families mirror each other file-for-file.** Each lives in `drivers/<family>/` as `chains` · `wallet` · `pay` · `verify` · `index`, with family-suffixed functions (`payEvm`/`paySui`/…, `verifyEvm`/`verifyNear`/…). Ten today: `evm`, `solana`, `ton`, `stellar`, `xrpl`, `tron`, `near`, `sui`, `aptos`, `algorand`. Adding one = copy the five files, implement the contract, `registerDriver` — the protocol layer never changes.
|
|
482
516
|
- **Routing + lazy auto-mount.** `registry.ts` maps a `chain` value to its family synchronously (`familyForChain`). EVM is always present (viem is a hard peer); every non-EVM family **loads itself on first use** via one dynamic `import()`, so a pure-EVM install never downloads `@solana`/`@ton`/`@stellar`/`xrpl`/`tronweb`/`near-api-js`/`@mysten/sui`/`@aptos-labs/ts-sdk`/`algosdk`. A build-time invariant asserts the main bundle has **zero** static imports of those libs — only per-family lazy chunks.
|
|
483
517
|
- **Two verification templates.** *Template A (memo-bound)* — Stellar, XRPL, TON, NEAR, Algorand — carries the challenge nonce inside the transfer (memo / tag / comment / note), so the proof is cryptographically bound to its challenge. *Template B (digest-bound)* — EVM, Solana, Tron, Sui, Aptos — binds via a single-use proof set + recipient + amount + a tight recency window (use a persistent `isUsed`/`markUsed` store in production).
|
|
@@ -556,7 +590,7 @@ Provide **either** `chain` + `token` + `amount` (single) **or** a non-empty `acc
|
|
|
556
590
|
| `retryTimeoutMs` | `30000` | Timeout for the retry leg after broadcast |
|
|
557
591
|
| `onEvent` | — | `(event) => void` observability: `payment-required` · `payment-broadcast` · `payment-confirmed` · `payment-unconfirmed` (broadcast OK, local confirm timed out → deferring to server) · `payment-settled` · `payment-failed` |
|
|
558
592
|
|
|
559
|
-
Methods: `fetch` · `get` · `post` (return the gated `Response` after settlement) · **`quote(url)`** (price without paying → `PipRailQuote \| null`) · **`estimateCost(url)`** (price **+** native-coin gas estimate → `PipRailCostQuote \| null`) · **`spent()`** (per-asset ledger snapshot).
|
|
593
|
+
Methods: `fetch` · `get` · `post` (return the gated `Response` after settlement) · **`quote(url)`** (price without paying → `PipRailQuote \| null`) · **`estimateCost(url)`** (price **+** native-coin gas estimate → `PipRailCostQuote \| null`) · **`planPayment(url)`** (affordability + recipient-readiness across the offered rails → `PaymentPlan \| null`) · **`canAfford(url)`** (→ `boolean`) · **`spent()`** (per-asset ledger snapshot). Pass `{ autoRoute: true }` to `fetch` (or set it on the client) to pay the cheapest *settleable* rail. Module-level **`planAcross(clients, url)`** plans across chains.
|
|
560
594
|
|
|
561
595
|
**Wallets by family** — the `chain` selector routes; each driver validates its own key format (a mismatch throws `WrongFamilyError`):
|
|
562
596
|
|
|
@@ -575,7 +609,7 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
|
|
|
575
609
|
|
|
576
610
|
**Hand an LLM a wallet:** `paymentTools(client)` → framework-agnostic tool descriptors (MCP / AI SDK / function-calling), budget enforced by the client.
|
|
577
611
|
|
|
578
|
-
**Bring your own chain family:** the SDK is built on a tiny `PaymentDriver` contract — `resolve(chain)` returns a bound network with `resolveToken` / `describeAsset` / `assertValidPayTo` / `bindWallet` / `send` / `confirm` / `estimateCost` / `verify`. Register your own with `registerDriver(...)`; the protocol layer never changes (see [Architecture](#architecture-under-the-hood)).
|
|
612
|
+
**Bring your own chain family:** the SDK is built on a tiny `PaymentDriver` contract — `resolve(chain)` returns a bound network with `resolveToken` / `describeAsset` / `assertValidPayTo` / `bindWallet` / `send` / `confirm` / `estimateCost` / `balanceOf` / `recipientReady` / `verify`. Register your own with `registerDriver(...)`; the protocol layer never changes (see [Architecture](#architecture-under-the-hood)).
|
|
579
613
|
|
|
580
614
|
**Universal x402 (experimental):** building blocks to pay servers on the mainstream x402 `exact` scheme (EIP-3009 + facilitator) — `parseExactRequirements`, `buildExactAuthorization`, `encodeXPaymentHeader`. EVM-only; validate against your target facilitator before production.
|
|
581
615
|
|
|
@@ -314,6 +314,40 @@ function makeAlgorandNetwork(preset, algodUrl) {
|
|
|
314
314
|
detail: "min fee 1000 \xB5Algos (1 transaction)"
|
|
315
315
|
});
|
|
316
316
|
},
|
|
317
|
+
async balanceOf(wallet, asset) {
|
|
318
|
+
let owner;
|
|
319
|
+
try {
|
|
320
|
+
owner = resolveAlgorandWallet(wallet._native).addr;
|
|
321
|
+
} catch {
|
|
322
|
+
return { token: null, native: null };
|
|
323
|
+
}
|
|
324
|
+
let info;
|
|
325
|
+
try {
|
|
326
|
+
info = await algod.accountInformation(owner).do();
|
|
327
|
+
} catch {
|
|
328
|
+
return { token: null, native: null };
|
|
329
|
+
}
|
|
330
|
+
const native = info.amount != null ? BigInt(info.amount) : null;
|
|
331
|
+
if (asset === "native") return { token: native, native };
|
|
332
|
+
const assetId = parseAlgorandAssetId(asset);
|
|
333
|
+
const holding = (info.assets ?? []).find((a) => Number(a.assetId) === assetId);
|
|
334
|
+
return { token: holding ? BigInt(holding.amount) : 0n, native };
|
|
335
|
+
},
|
|
336
|
+
async recipientReady(payTo, asset) {
|
|
337
|
+
if (asset === "native") return { ready: "n/a" };
|
|
338
|
+
const assetId = parseAlgorandAssetId(asset);
|
|
339
|
+
if (assetId == null) return { ready: "unknown" };
|
|
340
|
+
try {
|
|
341
|
+
const info = await algod.accountInformation(payTo).do();
|
|
342
|
+
const optedIn = (info.assets ?? []).some((a) => Number(a.assetId) === assetId);
|
|
343
|
+
return optedIn ? { ready: true } : { ready: false, reason: "NOT_OPTED_IN" };
|
|
344
|
+
} catch (e) {
|
|
345
|
+
if (/does not exist|no accounts found|404|account not found/i.test(String(e?.message ?? e))) {
|
|
346
|
+
return { ready: false, reason: "NOT_OPTED_IN" };
|
|
347
|
+
}
|
|
348
|
+
return { ready: "unknown" };
|
|
349
|
+
}
|
|
350
|
+
},
|
|
317
351
|
async verify(_ref, accept) {
|
|
318
352
|
return verifyAlgorand({ reader, accept });
|
|
319
353
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
@@ -89,7 +89,7 @@ async function verifyAlgorand(params) {
|
|
|
89
89
|
let txs;
|
|
90
90
|
try {
|
|
91
91
|
txs = await reader.transactionsForAccount(accept.payTo, 50);
|
|
92
|
-
} catch (
|
|
92
|
+
} catch (e2) {
|
|
93
93
|
return rpcFailed(nonce);
|
|
94
94
|
}
|
|
95
95
|
const tx = txs.find((t) => typeof t.note === "string" && t.note === nonce);
|
|
@@ -117,7 +117,7 @@ async function verifyAlgorand(params) {
|
|
|
117
117
|
let paid = 0n;
|
|
118
118
|
try {
|
|
119
119
|
paid = tx.amount ? BigInt(tx.amount) : 0n;
|
|
120
|
-
} catch (
|
|
120
|
+
} catch (e3) {
|
|
121
121
|
paid = 0n;
|
|
122
122
|
}
|
|
123
123
|
if (paid < required) {
|
|
@@ -314,6 +314,40 @@ function makeAlgorandNetwork(preset, algodUrl) {
|
|
|
314
314
|
detail: "min fee 1000 \xB5Algos (1 transaction)"
|
|
315
315
|
});
|
|
316
316
|
},
|
|
317
|
+
async balanceOf(wallet, asset) {
|
|
318
|
+
let owner;
|
|
319
|
+
try {
|
|
320
|
+
owner = resolveAlgorandWallet(wallet._native).addr;
|
|
321
|
+
} catch (e4) {
|
|
322
|
+
return { token: null, native: null };
|
|
323
|
+
}
|
|
324
|
+
let info;
|
|
325
|
+
try {
|
|
326
|
+
info = await algod.accountInformation(owner).do();
|
|
327
|
+
} catch (e5) {
|
|
328
|
+
return { token: null, native: null };
|
|
329
|
+
}
|
|
330
|
+
const native = info.amount != null ? BigInt(info.amount) : null;
|
|
331
|
+
if (asset === "native") return { token: native, native };
|
|
332
|
+
const assetId = parseAlgorandAssetId(asset);
|
|
333
|
+
const holding = (_nullishCoalesce(info.assets, () => ( []))).find((a) => Number(a.assetId) === assetId);
|
|
334
|
+
return { token: holding ? BigInt(holding.amount) : 0n, native };
|
|
335
|
+
},
|
|
336
|
+
async recipientReady(payTo, asset) {
|
|
337
|
+
if (asset === "native") return { ready: "n/a" };
|
|
338
|
+
const assetId = parseAlgorandAssetId(asset);
|
|
339
|
+
if (assetId == null) return { ready: "unknown" };
|
|
340
|
+
try {
|
|
341
|
+
const info = await algod.accountInformation(payTo).do();
|
|
342
|
+
const optedIn = (_nullishCoalesce(info.assets, () => ( []))).some((a) => Number(a.assetId) === assetId);
|
|
343
|
+
return optedIn ? { ready: true } : { ready: false, reason: "NOT_OPTED_IN" };
|
|
344
|
+
} catch (e) {
|
|
345
|
+
if (/does not exist|no accounts found|404|account not found/i.test(String(_nullishCoalesce(_optionalChain([e, 'optionalAccess', _ => _.message]), () => ( e))))) {
|
|
346
|
+
return { ready: false, reason: "NOT_OPTED_IN" };
|
|
347
|
+
}
|
|
348
|
+
return { ready: "unknown" };
|
|
349
|
+
}
|
|
350
|
+
},
|
|
317
351
|
async verify(_ref, accept) {
|
|
318
352
|
return verifyAlgorand({ reader, accept });
|
|
319
353
|
}
|
|
@@ -354,7 +388,7 @@ function adaptTxn(raw) {
|
|
|
354
388
|
function decodeNote(bytes) {
|
|
355
389
|
try {
|
|
356
390
|
return new TextDecoder().decode(bytes);
|
|
357
|
-
} catch (
|
|
391
|
+
} catch (e6) {
|
|
358
392
|
return void 0;
|
|
359
393
|
}
|
|
360
394
|
}
|
|
@@ -322,6 +322,34 @@ function makeAptosNetwork(preset, rpcUrl) {
|
|
|
322
322
|
detail: "\u22480.001 APT (a simple Fungible-Asset transfer; Aptos gas is sub-cent)"
|
|
323
323
|
});
|
|
324
324
|
},
|
|
325
|
+
async balanceOf(wallet, asset) {
|
|
326
|
+
let owner;
|
|
327
|
+
try {
|
|
328
|
+
owner = resolveAptosAccount(wallet._native).accountAddress.toString();
|
|
329
|
+
} catch (e7) {
|
|
330
|
+
return { token: null, native: null };
|
|
331
|
+
}
|
|
332
|
+
const native = await aptos.getAccountAPTAmount({ accountAddress: owner }).then((n) => BigInt(n)).catch(() => null);
|
|
333
|
+
if (asset === "native") return { token: native, native };
|
|
334
|
+
let token = null;
|
|
335
|
+
try {
|
|
336
|
+
const [bal] = await aptos.view({
|
|
337
|
+
payload: {
|
|
338
|
+
function: "0x1::primary_fungible_store::balance",
|
|
339
|
+
typeArguments: ["0x1::fungible_asset::Metadata"],
|
|
340
|
+
functionArguments: [owner, asset]
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
token = BigInt(String(bal));
|
|
344
|
+
} catch (e8) {
|
|
345
|
+
token = null;
|
|
346
|
+
}
|
|
347
|
+
return { token, native };
|
|
348
|
+
},
|
|
349
|
+
// No receive prerequisite — the recipient's primary FA store auto-creates on receipt.
|
|
350
|
+
async recipientReady() {
|
|
351
|
+
return { ready: "n/a" };
|
|
352
|
+
},
|
|
325
353
|
async verify(ref, accept) {
|
|
326
354
|
return verifyAptos({ reader, hash: ref, accept });
|
|
327
355
|
}
|
|
@@ -322,6 +322,34 @@ function makeAptosNetwork(preset, rpcUrl) {
|
|
|
322
322
|
detail: "\u22480.001 APT (a simple Fungible-Asset transfer; Aptos gas is sub-cent)"
|
|
323
323
|
});
|
|
324
324
|
},
|
|
325
|
+
async balanceOf(wallet, asset) {
|
|
326
|
+
let owner;
|
|
327
|
+
try {
|
|
328
|
+
owner = resolveAptosAccount(wallet._native).accountAddress.toString();
|
|
329
|
+
} catch {
|
|
330
|
+
return { token: null, native: null };
|
|
331
|
+
}
|
|
332
|
+
const native = await aptos.getAccountAPTAmount({ accountAddress: owner }).then((n) => BigInt(n)).catch(() => null);
|
|
333
|
+
if (asset === "native") return { token: native, native };
|
|
334
|
+
let token = null;
|
|
335
|
+
try {
|
|
336
|
+
const [bal] = await aptos.view({
|
|
337
|
+
payload: {
|
|
338
|
+
function: "0x1::primary_fungible_store::balance",
|
|
339
|
+
typeArguments: ["0x1::fungible_asset::Metadata"],
|
|
340
|
+
functionArguments: [owner, asset]
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
token = BigInt(String(bal));
|
|
344
|
+
} catch {
|
|
345
|
+
token = null;
|
|
346
|
+
}
|
|
347
|
+
return { token, native };
|
|
348
|
+
},
|
|
349
|
+
// No receive prerequisite — the recipient's primary FA store auto-creates on receipt.
|
|
350
|
+
async recipientReady() {
|
|
351
|
+
return { ready: "n/a" };
|
|
352
|
+
},
|
|
325
353
|
async verify(ref, accept) {
|
|
326
354
|
return verifyAptos({ reader, hash: ref, accept });
|
|
327
355
|
}
|