@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 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 Sei** (USDC only there).
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). Solana / TON / Stellar mount
198
- lazily via a single dynamic `import()` in [`drivers/index.ts`](src/drivers/index.ts) the
199
- first time their `chain` is named — no setup call.
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 (e) {
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 (e2) {
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 (e3) {
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
  }