@toon-protocol/client 0.14.3 → 0.14.5

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/README.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  The **client library** for TOON Protocol — _pay-to-write Nostr over Interledger (ILP)_. Use it to **pay for and publish** writes to a network of service nodes. **Reads are free; writes cost a signed EIP-712 payment-channel claim** against an on-chain deposit.
4
4
 
5
- > **`client` vs `townhouse`.** This package (`@toon-protocol/client`) is what an _app or end user_ uses to **pay** and publish. It does **not** run any relay or node. The nodes are operated separately by **`@toon-protocol/townhouse`** (the operator product, which runs an _apex_ connector plus `town` / `mill` / `dvm` children). Don't confuse the **client** (pays) with **townhouse** (operates), or **`town`** (a single Nostr-relay node) with **townhouse** (the whole operator stack).
5
+ > **`client` vs `relay`.** This package (`@toon-protocol/client`) is what an _app or end user_ uses to **pay** and publish. It does **not** run any relay or node. The nodes are operated separately by **`@toon-protocol/relay`** (the operator product, which runs an _apex_ connector plus `town` / `swap` / `store` children). Don't confuse the **client** (pays) with **relay** (operates), or **`town`** (a single Nostr-relay node) with **relay** (the whole operator stack).
6
6
 
7
7
  ## Which call pays which node
8
8
 
9
- Every write is an ILP packet carrying a signed payment-channel claim. The client reaches all node types **through a townhouse apex** (`g.townhouse`): the apex validates the claim, takes its fee, and forwards the packet to the destination node, which returns FULFILL (accepted) or REJECT. The method you call determines which node type you pay:
9
+ Every write is an ILP packet carrying a signed payment-channel claim. The client reaches all node types **through a relay apex** (`g.proxy`): the apex validates the claim, takes its fee, and forwards the packet to the destination node, which returns FULFILL (accepted) or REJECT. The method you call determines which node type you pay:
10
10
 
11
11
  | Client call | Node type | What it does |
12
12
  | --------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
13
  | `client.publishEvent(event)` | **town** | Publish a Nostr event (e.g. `kind:1`) to the relay. |
14
- | `requestBlobStorage(client, …)` | **dvm** | NIP-90 compute/storage. Builds and publishes a `kind:5094` event that uploads a blob to Arweave — the job request **is** the payment — and decodes the Arweave tx ID from the FULFILL response. |
15
- | `client.sendSwapPacket(…)` | **mill** | Multi-chain token swap (low-level). Most callers use the higher-level `streamSwap()` from `@toon-protocol/sdk`, which is built on `sendSwapPacket`. |
14
+ | `requestBlobStorage(client, …)` | **store** | NIP-90 compute/storage. Builds and publishes a `kind:5094` event that uploads a blob to Arweave — the job request **is** the payment — and decodes the Arweave tx ID from the FULFILL response. |
15
+ | `client.sendSwapPacket(…)` | **swap** | Multi-chain token swap (low-level). Most callers use the higher-level `streamSwap()` from `@toon-protocol/sdk`, which is built on `sendSwapPacket`. |
16
16
 
17
17
  ## What It Does
18
18
 
@@ -21,7 +21,7 @@ This client handles:
21
21
  - **ILP Micropayments**: Pay to publish Nostr events (reads are free)
22
22
  - **Payment Channels**: Automatic on-chain channel creation with off-chain settlement via signed balance proofs
23
23
  - **Unified Identity**: One Nostr key = one EVM address (both secp256k1, derived automatically) — or a single BIP-39 **mnemonic** to derive a full multi-chain identity
24
- - **Multi-Chain Settlement**: Sign payment-channel claims on EVM (EIP-712), Solana (Ed25519), and Mina (Pallas) from one mnemonic. A Townhouse apex validates the claim and redeems it on-chain on the matching chain (EVM/Solana credit the recipient; Mina redeems each claim on-chain with recipient credit-at-close deferred — see [Multi-Chain Settlement notes](#identity--multi-chain-settlement))
24
+ - **Multi-Chain Settlement**: Sign payment-channel claims on EVM (EIP-712), Solana (Ed25519), and Mina (Pallas) from one mnemonic. A relay apex validates the claim and redeems it on-chain on the matching chain (EVM/Solana credit the recipient; Mina redeems each claim on-chain with recipient credit-at-close deferred — see [Multi-Chain Settlement notes](#identity--multi-chain-settlement))
25
25
  - **Multi-Hop Routing**: Publish to any destination address (`destination` / `destinationAddress`), not just your direct peer
26
26
  - **Network Bootstrap**: Automatically discover and register with ILP peers via NIP-02 follow lists
27
27
  - **TOON Encoding**: Native binary format for agent-friendly event encoding
@@ -41,7 +41,7 @@ pnpm add mina-signer
41
41
 
42
42
  - **Node.js ≥ 20** — these packages are ESM.
43
43
  - **A TOON apex to pay.** You don't run any node yourself; you connect to a running
44
- [`@toon-protocol/townhouse`](https://www.npmjs.com/package/@toon-protocol/townhouse) apex (or any
44
+ [`@toon-protocol/relay`](https://www.npmjs.com/package/@toon-protocol/relay) apex (or any
45
45
  TOON connector) and pay it. From its operator you need:
46
46
  - a **connector endpoint** — an HTTP `connectorUrl` and/or a BTP WebSocket `btpUrl`;
47
47
  - a **settlement-chain RPC URL** and a **funded key** on that chain, so the client can open a
@@ -167,13 +167,13 @@ console.log('Mina: ', client.getMinaAddress()); // base58, after start() (nee
167
167
  - **Mina is optional**: it requires the `mina-signer` peer dependency (see Installation). Without it, the client still works for Nostr/EVM/Solana and `getMinaAddress()` returns `undefined`.
168
168
  - **Security**: JavaScript strings can't be zeroed from memory, so a `mnemonic` may linger in the heap. For high-security contexts, derive keys yourself (e.g. via `KeyManager`) and pass a pre-derived `secretKey`.
169
169
  - **Per-chain claim formats.** Each publish carries a balance-proof claim in the format that destination chain's connector verifier expects — EVM via EIP-712, Solana as a raw Ed25519 message over the on-chain payment-channel message (`channel_pda ‖ nonce ‖ transferredAmount`), Mina as a Pallas-Schnorr claim over a Poseidon `balanceCommitment`. `ToonClient` selects the right signer for the negotiated channel automatically; you do not pick the format. Canonical layouts live in `@toon-protocol/core` (`packages/core/src/settlement/`) so client signers and connector verifiers cannot drift.
170
- - **On-chain redemption is automatic, and driven by the apex — not the client.** You sign off-chain claims; the Townhouse apex validates them, fulfills, and (once a per-channel threshold is crossed) submits the on-chain redemption itself. EVM and Solana credit the recipient on-chain (Solana at channel close, `SETTLE_CHANNEL`). On **Mina** each paid publish redeems on-chain (`claimFromChannel`, the apex co-signs the counterparty signature; the zkApp nonce and balance commitment advance), and **the recipient's tokens are credited at channel close** via the Story 34.4 fund-custody zkApp (`@toon-protocol/connector` ≥3.10.0): the deposit is escrowed on the zkApp account and `settle()` drains it to the participants (recipient + depositor refund). Verified against `@toon-protocol/connector` 3.10.0.
170
+ - **On-chain redemption is automatic, and driven by the apex — not the client.** You sign off-chain claims; the relay apex validates them, fulfills, and (once a per-channel threshold is crossed) submits the on-chain redemption itself. EVM and Solana credit the recipient on-chain (Solana at channel close, `SETTLE_CHANNEL`). On **Mina** each paid publish redeems on-chain (`claimFromChannel`, the apex co-signs the counterparty signature; the zkApp nonce and balance commitment advance), and **the recipient's tokens are credited at channel close** via the Story 34.4 fund-custody zkApp (`@toon-protocol/connector` ≥3.10.0): the deposit is escrowed on the zkApp account and `settle()` drains it to the participants (recipient + depositor refund). Verified against `@toon-protocol/connector` 3.10.0.
171
171
 
172
172
  ---
173
173
 
174
174
  ## Uploading a blob to a DVM (Arweave storage)
175
175
 
176
- To store a blob permanently on Arweave, pay a **dvm** node with a `kind:5094` NIP-90 request. The `requestBlobStorage` helper builds the signed event, publishes it through your `ToonClient` (reusing its claim/channel plumbing), and decodes the Arweave transaction ID from the FULFILL response:
176
+ To store a blob permanently on Arweave, pay a **store** node with a `kind:5094` NIP-90 request. The `requestBlobStorage` helper builds the signed event, publishes it through your `ToonClient` (reusing its claim/channel plumbing), and decodes the Arweave transaction ID from the FULFILL response:
177
177
 
178
178
  ```typescript
179
179
  import { ToonClient, requestBlobStorage } from '@toon-protocol/client';
@@ -183,7 +183,7 @@ const result = await requestBlobStorage(client, secretKey, {
183
183
  blobData: new Uint8Array([1, 2, 3, 4]),
184
184
  contentType: 'application/octet-stream',
185
185
  ilpAmount: 50_000n, // USDC micro-units; also used as the event's `bid` if `bid` is omitted
186
- destination: 'g.toon.peer1', // the DVM's ILP address (defaults to the client's destinationAddress)
186
+ destination: 'g.toon.peer1', // the store node's ILP address (defaults to the client's destinationAddress)
187
187
  });
188
188
 
189
189
  if (result.success) {
@@ -239,7 +239,7 @@ await client.publishEvent(event, { claim });
239
239
  2. **Channel Creation**: Opens an on-chain payment channel on the negotiated chain — using your derived EVM address (EVM), the Ed25519 channel PDA (Solana), or the deployed zkApp account (Mina) when the matching `solanaChannel` / `minaChannel` config is provided
240
240
  3. **Off-chain Payments**: Signed balance proofs (chain-appropriate format) settle payments off-chain
241
241
  4. **Auto-tracking**: ChannelManager automatically tracks channels and increments nonces
242
- 5. **On-chain redemption**: A Townhouse apex auto-redeems claims on-chain once a per-channel threshold is crossed (see [Multi-Chain Settlement](#identity--multi-chain-settlement) for the EVM/Solana/Mina specifics and the Mina credit-at-close deferral)
242
+ 5. **On-chain redemption**: A relay apex auto-redeems claims on-chain once a per-channel threshold is crossed (see [Multi-Chain Settlement](#identity--multi-chain-settlement) for the EVM/Solana/Mina specifics and the Mina credit-at-close deferral)
243
243
 
244
244
  ### Using a Separate EVM Key (Advanced)
245
245
 
@@ -304,10 +304,9 @@ See [examples/client-example/](../../examples/client-example/) for standalone cl
304
304
  ## Related Packages
305
305
 
306
306
  - **[@toon-protocol/core](../core/)** — Core protocol (peer discovery, bootstrap, `buildBlobStorageRequest`)
307
- - **[@toon-protocol/relay](../relay/)** — Nostr relay with ILP payment gating (`encodeEventToToon` / `decodeEventFromToon`)
308
- - **[@toon-protocol/sdk](../sdk/)** — Higher-level helpers including `streamSwap()` for multi-chain swaps via a **mill**
307
+ - **[@toon-protocol/relay](../relay/)** — Operator product running the apex connector plus town/swap/store nodes; also exports `encodeEventToToon` / `decodeEventFromToon` for event encoding
308
+ - **[@toon-protocol/sdk](../sdk/)** — Higher-level helpers including `streamSwap()` for multi-chain swaps via a **swap**
309
309
  - **[@toon-protocol/bls](../bls/)** — Business Logic Server (pricing, validation, storage)
310
- - **[@toon-protocol/townhouse](../townhouse/)** — The operator product that runs the apex + town/mill/dvm nodes you pay
311
310
 
312
311
  ---
313
312
 
package/dist/index.d.ts CHANGED
@@ -5,6 +5,17 @@ import { NostrEvent, EventTemplate } from 'nostr-tools/pure';
5
5
  import { PrivateKeyAccount } from 'viem/accounts';
6
6
  export { A2uiDecision, ConsentDecision, ConsentRequest, DispatchGuardInfo, DispatchInput, GenerateContext, GeneratedRenderer, GenerativeDecision, GenerativeFallbackOptions, GenerativeFallbackRenderer, GenerativeFallbackResult, GuardedDispatchInput, IntentClassification, KindRegistry, MIME_A2UI, MIME_MCP_APP, McpUiDecision, NativeDecision, PublishBackOptions, RenderBranch, RenderDecision, RenderTrust, RendererGenerator, RendererPin, RendererPinStore, RendererPublisher, RendererSigner, ResolvedCoordinate, SwapApproval, SwapDecision, SwapRejection, SwapRejectionReason, UiResource, VerifyRendererInput, WidgetIntent, buildConsentRequest, buildRendererEventTemplate, classifyIntent, deterministicGenerator, extractUiResource, guardedRenderDispatch, isTrustDowngrade, publishBackCoordinate, renderDeterministicHtml, renderDispatch, resolveRendererMime, resolveUiCoordinate, resolveUiRenderer, verifyRendererTrust } from './render/index.js';
7
7
 
8
+ /** One on-chain wallet token balance. `amount` is base-unit integer, decimal. */
9
+ interface WalletBalance {
10
+ chain: 'evm' | 'solana' | 'mina';
11
+ address: string;
12
+ amount: string;
13
+ /** Token symbol, when resolved (e.g. `'USDC'`, `'MINA'`). */
14
+ asset?: string;
15
+ /** Token decimals, when resolved. */
16
+ assetScale?: number;
17
+ }
18
+
8
19
  /**
9
20
  * Solana payment-channel parameters supplied via `ToonClientConfig.solanaChannel`.
10
21
  *
@@ -172,7 +183,7 @@ interface ToonClientConfig {
172
183
  * preferred tokens, EVM TokenNetwork addresses, and the Solana/Mina channel
173
184
  * params (programId / zkApp) — from the shared core presets
174
185
  * (`resolveClientNetwork`), so the caller no longer hand-wires every address.
175
- * This is the client-side mirror of the townhouse node's `network` selector;
186
+ * This is the client-side mirror of the relay node's `network` selector;
176
187
  * both resolve the SAME deployed contracts.
177
188
  *
178
189
  * Precedence: any explicit per-chain field (`supportedChains`,
@@ -1013,7 +1024,7 @@ declare class ToonClient {
1013
1024
  }): Promise<RequestBlobStorageResult>;
1014
1025
  /**
1015
1026
  * Per-chain settlement readiness for the configured `network` tier, mirroring
1016
- * the townhouse node's status. Returns `undefined` when no named `network` is
1027
+ * the relay node's status. Returns `undefined` when no named `network` is
1017
1028
  * set (or `network: 'custom'`), since there is no preset tier to report on.
1018
1029
  */
1019
1030
  getNetworkStatus(): NetworkFamilyStatus | undefined;
@@ -1099,7 +1110,7 @@ declare class ToonClient {
1099
1110
  */
1100
1111
  h402Fetch(url: string, opts?: H402FetchOptions): Promise<Response>;
1101
1112
  /**
1102
- * Sends a raw swap ILP packet (Story 12.5) to a Mill peer with an attached
1113
+ * Sends a raw swap ILP packet (Story 12.5) to a swap peer with an attached
1103
1114
  * balance-proof claim. This is a lower-level surface than `publishEvent`:
1104
1115
  * it forwards the raw `IlpSendResult` so the sender (`streamSwap()`) can
1105
1116
  * decode FULFILL metadata itself.
@@ -1210,6 +1221,20 @@ declare class ToonClient {
1210
1221
  * Gets the cumulative transferred amount for a tracked channel.
1211
1222
  */
1212
1223
  getChannelCumulativeAmount(channelId: string): bigint;
1224
+ /**
1225
+ * Gets the on-chain deposit total (locked collateral) for a tracked channel.
1226
+ * The available (spendable) balance is this minus the cumulative spent amount.
1227
+ */
1228
+ getChannelDepositTotal(channelId: string): bigint;
1229
+ /**
1230
+ * Read the on-chain settlement-token balance of this client's OWN wallet on
1231
+ * each configured chain (EVM token, Solana SPL, native MINA). A free read — no
1232
+ * signing, no payment. Best-effort per chain: a chain whose config is absent or
1233
+ * whose RPC read fails is omitted rather than failing the whole result, so the
1234
+ * wallet view degrades gracefully. Available after `start()` (Solana/Mina keys
1235
+ * are derived there).
1236
+ */
1237
+ getBalances(): Promise<WalletBalance[]>;
1213
1238
  /**
1214
1239
  * Resolves an ILP destination address to a peer ID.
1215
1240
  * Convention: destination "g.toon.peer1" → peerId "peer1" (last segment).
@@ -1864,7 +1889,7 @@ declare class EvmSigner {
1864
1889
  * is what makes a client-issued Solana payment-channel claim (paying the apex
1865
1890
  * to write) acceptable on connector 3.9.0.
1866
1891
  *
1867
- * NOTE: this is a DIFFERENT message from the Mill ↔ sender swap-claim wire
1892
+ * NOTE: this is a DIFFERENT message from the swap peer ↔ sender swap-claim wire
1868
1893
  * contract (`balanceProofHashSolana`, SDK `verifyEd25519Signature`). The client
1869
1894
  * here is paying a payment-channel claim to the apex, not issuing a swap claim,
1870
1895
  * so it must sign the connector's on-chain payment-channel message. `channelId`
@@ -1934,7 +1959,7 @@ interface MinaSignerOptions {
1934
1959
  * off-chain `verifyBalanceProof` accepts EITHER, so off-chain store/FULFILL works
1935
1960
  * in both cases — only on-chain settle requires the participant form.
1936
1961
  *
1937
- * NOTE: this is a DIFFERENT message + format from the Mill ↔ sender swap-claim
1962
+ * NOTE: this is a DIFFERENT message + format from the swap peer ↔ sender swap-claim
1938
1963
  * wire contract (`balanceProofFieldsMina` in `@toon-protocol/core`, verified by
1939
1964
  * the SDK's `verifyMinaSignature`). The client here pays a payment-channel claim
1940
1965
  * to the apex, so it signs the connector's on-chain payment-channel scheme; the
@@ -2274,6 +2299,12 @@ declare class ChannelManager {
2274
2299
  * Gets the cumulative transferred amount for a tracked channel.
2275
2300
  */
2276
2301
  getCumulativeAmount(channelId: string): bigint;
2302
+ /**
2303
+ * Gets the on-chain deposit total (collateral locked at open / via deposits)
2304
+ * for a tracked channel, or `0n` when none was captured. The available
2305
+ * (spendable) balance is `depositTotal - cumulativeAmount`.
2306
+ */
2307
+ getDepositTotal(channelId: string): bigint;
2277
2308
  /**
2278
2309
  * Gets all tracked channel IDs.
2279
2310
  */
@@ -2406,7 +2437,7 @@ declare function buildStoreWriteEnvelope(event: NostrEvent, requestTarget?: stri
2406
2437
  * dependency-free helper that returns the status code + raw body string, which
2407
2438
  * is all the publish/upload paths need.
2408
2439
  *
2409
- * DEFENSIVE: not every FULFILL is HTTP-enveloped (e.g. Mill-swap raw-TOON
2440
+ * DEFENSIVE: not every FULFILL is HTTP-enveloped (e.g. swap peer raw-TOON
2410
2441
  * FULFILLs go through `sendSwapPacket`, not these paths). If the decoded data
2411
2442
  * does not begin with an `HTTP/<v>` status line, `isHttp` is `false` and the
2412
2443
  * caller should fall back to its prior (non-HTTP) interpretation rather than
@@ -2467,7 +2498,7 @@ interface ClientSettlementInfo {
2467
2498
  declare function applyNetworkPresets(config: ToonClientConfig): ToonClientConfig;
2468
2499
  /**
2469
2500
  * Returns per-chain settlement readiness for the configured `network` tier,
2470
- * mirroring the townhouse node's status. Returns `undefined` when `network` is
2501
+ * mirroring the relay node's status. Returns `undefined` when `network` is
2471
2502
  * unset or `'custom'` (no preset tier to report on).
2472
2503
  */
2473
2504
  declare function getNetworkStatus(config: ToonClientConfig): NetworkFamilyStatus | undefined;
@@ -3286,7 +3317,7 @@ declare function isPrfSupported(): boolean;
3286
3317
  /**
3287
3318
  * Node-only encrypted mnemonic keystore for @toon-protocol/client.
3288
3319
  *
3289
- * Mirrors the Townhouse node wallet crypto (`packages/townhouse/src/wallet/
3320
+ * Mirrors the relay node wallet crypto (`packages/relay/src/wallet/
3290
3321
  * crypto.ts`): a BIP-39 mnemonic is encrypted at rest with scrypt (KDF) +
3291
3322
  * AES-256-GCM (authenticated encryption), serialized as JSON, and written to
3292
3323
  * disk with mode 0o600. Decryption requires the operator password; a wrong
@@ -3301,7 +3332,7 @@ declare function isPrfSupported(): boolean;
3301
3332
  */
3302
3333
  /**
3303
3334
  * Encrypted keystore file format (JSON, all binary fields base64-encoded).
3304
- * Wire-compatible with Townhouse's `EncryptedWallet`.
3335
+ * Wire-compatible with the relay node's `EncryptedWallet`.
3305
3336
  */
3306
3337
  interface EncryptedKeystore {
3307
3338
  /** scrypt salt (base64). */
@@ -3352,7 +3383,7 @@ declare function importKeystore(path: string, mnemonic: string, password: string
3352
3383
  declare function loadKeystore(path: string, password: string): string;
3353
3384
  /**
3354
3385
  * Serialize and write an encrypted keystore to disk with mode 0o600
3355
- * (owner read/write only), mirroring the Townhouse wallet file permissions.
3386
+ * (owner read/write only), mirroring the relay node wallet file permissions.
3356
3387
  */
3357
3388
  declare function writeKeystoreFile(path: string, keystore: EncryptedKeystore): void;
3358
3389
 
package/dist/index.js CHANGED
@@ -1640,17 +1640,17 @@ function readDiscoveredIlpPeer(peer) {
1640
1640
  }
1641
1641
  function selectIlpTransport(peer, options = {}) {
1642
1642
  const needsDuplex = options.needsDuplex ?? false;
1643
- const http2 = peer.httpEndpoint?.trim() || void 0;
1643
+ const http3 = peer.httpEndpoint?.trim() || void 0;
1644
1644
  const btp = peer.btpEndpoint?.trim() || void 0;
1645
1645
  const canUpgrade = peer.supportsUpgrade === true;
1646
1646
  if (needsDuplex) {
1647
1647
  if (btp) return { kind: "btp", btpEndpoint: btp };
1648
- if (http2 && canUpgrade) return { kind: "http-upgradable", httpEndpoint: http2 };
1648
+ if (http3 && canUpgrade) return { kind: "http-upgradable", httpEndpoint: http3 };
1649
1649
  throw new Error(
1650
1650
  "Duplex transport required but peer exposes neither a btpEndpoint nor an upgradable httpEndpoint"
1651
1651
  );
1652
1652
  }
1653
- if (http2) return { kind: "http", httpEndpoint: http2, canUpgrade };
1653
+ if (http3) return { kind: "http", httpEndpoint: http3, canUpgrade };
1654
1654
  if (btp) return { kind: "btp", btpEndpoint: btp };
1655
1655
  throw new Error("Peer exposes neither an httpEndpoint nor a btpEndpoint");
1656
1656
  }
@@ -3489,6 +3489,18 @@ var ChannelManager = class {
3489
3489
  }
3490
3490
  return tracking.cumulativeAmount;
3491
3491
  }
3492
+ /**
3493
+ * Gets the on-chain deposit total (collateral locked at open / via deposits)
3494
+ * for a tracked channel, or `0n` when none was captured. The available
3495
+ * (spendable) balance is `depositTotal - cumulativeAmount`.
3496
+ */
3497
+ getDepositTotal(channelId) {
3498
+ const tracking = this.channels.get(channelId);
3499
+ if (!tracking) {
3500
+ throw new Error(`Channel "${channelId}" is not being tracked.`);
3501
+ }
3502
+ return tracking.depositTotal ?? 0n;
3503
+ }
3492
3504
  /**
3493
3505
  * Gets all tracked channel IDs.
3494
3506
  */
@@ -3547,6 +3559,91 @@ var JsonFileChannelStore = class {
3547
3559
  }
3548
3560
  };
3549
3561
 
3562
+ // src/balance/WalletBalanceReader.ts
3563
+ import { createPublicClient as createPublicClient2, http as http2, defineChain as defineChain2 } from "viem";
3564
+ var ERC20_READ_ABI = [
3565
+ { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }] },
3566
+ { name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
3567
+ { name: "symbol", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] }
3568
+ ];
3569
+ function parseEvmChainId(chainKey) {
3570
+ const parts = chainKey.split(":");
3571
+ const idStr = parts.length >= 3 ? parts[2] : parts[1];
3572
+ const id = Number.parseInt(idStr ?? "", 10);
3573
+ if (!Number.isFinite(id)) throw new Error(`Invalid EVM chain key "${chainKey}".`);
3574
+ return id;
3575
+ }
3576
+ async function readEvmTokenBalance(opts) {
3577
+ const chainId = parseEvmChainId(opts.chainKey);
3578
+ const client = createPublicClient2({
3579
+ transport: http2(opts.rpcUrl),
3580
+ chain: defineChain2({
3581
+ id: chainId,
3582
+ name: opts.chainKey,
3583
+ nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
3584
+ rpcUrls: { default: { http: [opts.rpcUrl] } }
3585
+ })
3586
+ });
3587
+ const token = opts.tokenAddress;
3588
+ const owner = opts.owner;
3589
+ const [amount, decimals, symbol] = await Promise.all([
3590
+ client.readContract({ address: token, abi: ERC20_READ_ABI, functionName: "balanceOf", args: [owner] }),
3591
+ client.readContract({ address: token, abi: ERC20_READ_ABI, functionName: "decimals" }).catch(() => void 0),
3592
+ client.readContract({ address: token, abi: ERC20_READ_ABI, functionName: "symbol" }).catch(() => void 0)
3593
+ ]);
3594
+ const out = { chain: "evm", address: opts.owner, amount: amount.toString() };
3595
+ if (typeof symbol === "string" && symbol) out.asset = symbol;
3596
+ if (decimals !== void 0) out.assetScale = Number(decimals);
3597
+ return out;
3598
+ }
3599
+ async function readSolanaTokenBalance(opts) {
3600
+ const fetchImpl = opts.fetchImpl ?? fetch;
3601
+ const res = await fetchImpl(opts.rpcUrl, {
3602
+ method: "POST",
3603
+ headers: { "content-type": "application/json" },
3604
+ body: JSON.stringify({
3605
+ jsonrpc: "2.0",
3606
+ id: 1,
3607
+ method: "getTokenAccountsByOwner",
3608
+ params: [opts.owner, { mint: opts.mint }, { encoding: "jsonParsed", commitment: "confirmed" }]
3609
+ })
3610
+ });
3611
+ if (!res.ok) throw new Error(`Solana RPC request failed: HTTP ${res.status}`);
3612
+ const json = await res.json();
3613
+ if (json.error) throw new Error(`Solana RPC error: ${json.error.message ?? "unknown"}`);
3614
+ let amount = 0n;
3615
+ let decimals;
3616
+ for (const acc of json.result?.value ?? []) {
3617
+ const ta = acc.account?.data?.parsed?.info?.tokenAmount;
3618
+ if (ta?.amount) amount += BigInt(ta.amount);
3619
+ if (ta?.decimals !== void 0) decimals = ta.decimals;
3620
+ }
3621
+ const out = { chain: "solana", address: opts.owner, amount: amount.toString() };
3622
+ if (decimals !== void 0) out.assetScale = decimals;
3623
+ return out;
3624
+ }
3625
+ async function readMinaBalance(opts) {
3626
+ const fetchImpl = opts.fetchImpl ?? fetch;
3627
+ const query = "query($pk:String!){account(publicKey:$pk){balance{total}}}";
3628
+ const res = await fetchImpl(opts.graphqlUrl, {
3629
+ method: "POST",
3630
+ headers: { "content-type": "application/json" },
3631
+ body: JSON.stringify({ query, variables: { pk: opts.owner } })
3632
+ });
3633
+ if (!res.ok) throw new Error(`Mina GraphQL request failed: HTTP ${res.status}`);
3634
+ const json = await res.json();
3635
+ if (json.errors && json.errors.length > 0) {
3636
+ throw new Error(`Mina GraphQL error: ${json.errors[0]?.message ?? "unknown"}`);
3637
+ }
3638
+ return {
3639
+ chain: "mina",
3640
+ address: opts.owner,
3641
+ amount: String(json.data?.account?.balance?.total ?? "0"),
3642
+ asset: "MINA",
3643
+ assetScale: 9
3644
+ };
3645
+ }
3646
+
3550
3647
  // src/blob-storage.ts
3551
3648
  import { buildBlobStorageRequest } from "@toon-protocol/core";
3552
3649
  var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
@@ -3617,8 +3714,8 @@ async function requestBlobStorage(client, secretKey, params) {
3617
3714
  };
3618
3715
  }
3619
3716
  function extractArweaveTxId(base64Data) {
3620
- const http2 = parseFulfillHttp(base64Data);
3621
- if (!http2.isHttp) {
3717
+ const http3 = parseFulfillHttp(base64Data);
3718
+ if (!http3.isHttp) {
3622
3719
  const legacy = decodeUtf8(fromBase64(base64Data));
3623
3720
  if (!ARWEAVE_TX_ID_REGEX.test(legacy)) {
3624
3721
  throw new Error(
@@ -3627,18 +3724,18 @@ function extractArweaveTxId(base64Data) {
3627
3724
  }
3628
3725
  return legacy;
3629
3726
  }
3630
- if (http2.status < 200 || http2.status >= 300) {
3631
- const detail = http2.body ? ` - ${http2.body}` : "";
3727
+ if (http3.status < 200 || http3.status >= 300) {
3728
+ const detail = http3.body ? ` - ${http3.body}` : "";
3632
3729
  throw new Error(
3633
- `Blob upload failed: DVM returned HTTP ${http2.status} ${http2.statusText}`.trimEnd() + detail
3730
+ `Blob upload failed: DVM returned HTTP ${http3.status} ${http3.statusText}`.trimEnd() + detail
3634
3731
  );
3635
3732
  }
3636
3733
  let parsed;
3637
3734
  try {
3638
- parsed = JSON.parse(http2.body);
3735
+ parsed = JSON.parse(http3.body);
3639
3736
  } catch {
3640
3737
  throw new Error(
3641
- `Blob upload response body was not valid JSON: "${http2.body}"`
3738
+ `Blob upload response body was not valid JSON: "${http3.body}"`
3642
3739
  );
3643
3740
  }
3644
3741
  const body = parsed;
@@ -3656,7 +3753,7 @@ function extractArweaveTxId(base64Data) {
3656
3753
  }
3657
3754
  }
3658
3755
  throw new Error(
3659
- `Blob upload response did not contain a valid Arweave tx ID: "${http2.body}"`
3756
+ `Blob upload response did not contain a valid Arweave tx ID: "${http3.body}"`
3660
3757
  );
3661
3758
  }
3662
3759
 
@@ -3987,7 +4084,7 @@ var ToonClient = class {
3987
4084
  }
3988
4085
  /**
3989
4086
  * Per-chain settlement readiness for the configured `network` tier, mirroring
3990
- * the townhouse node's status. Returns `undefined` when no named `network` is
4087
+ * the relay node's status. Returns `undefined` when no named `network` is
3991
4088
  * set (or `network: 'custom'`), since there is no preset tier to report on.
3992
4089
  */
3993
4090
  getNetworkStatus() {
@@ -4305,7 +4402,7 @@ var ToonClient = class {
4305
4402
  return client.fetch(url, opts);
4306
4403
  }
4307
4404
  /**
4308
- * Sends a raw swap ILP packet (Story 12.5) to a Mill peer with an attached
4405
+ * Sends a raw swap ILP packet (Story 12.5) to a swap peer with an attached
4309
4406
  * balance-proof claim. This is a lower-level surface than `publishEvent`:
4310
4407
  * it forwards the raw `IlpSendResult` so the sender (`streamSwap()`) can
4311
4408
  * decode FULFILL metadata itself.
@@ -4533,6 +4630,59 @@ var ToonClient = class {
4533
4630
  if (!this.channelManager) throw new Error("ChannelManager not initialized");
4534
4631
  return this.channelManager.getCumulativeAmount(channelId);
4535
4632
  }
4633
+ /**
4634
+ * Gets the on-chain deposit total (locked collateral) for a tracked channel.
4635
+ * The available (spendable) balance is this minus the cumulative spent amount.
4636
+ */
4637
+ getChannelDepositTotal(channelId) {
4638
+ if (!this.channelManager) throw new Error("ChannelManager not initialized");
4639
+ return this.channelManager.getDepositTotal(channelId);
4640
+ }
4641
+ /**
4642
+ * Read the on-chain settlement-token balance of this client's OWN wallet on
4643
+ * each configured chain (EVM token, Solana SPL, native MINA). A free read — no
4644
+ * signing, no payment. Best-effort per chain: a chain whose config is absent or
4645
+ * whose RPC read fails is omitted rather than failing the whole result, so the
4646
+ * wallet view degrades gracefully. Available after `start()` (Solana/Mina keys
4647
+ * are derived there).
4648
+ */
4649
+ async getBalances() {
4650
+ const out = [];
4651
+ const evmAddress = this.getEvmAddress();
4652
+ const rpcUrls = this.config.chainRpcUrls;
4653
+ const tokens = this.config.preferredTokens;
4654
+ if (evmAddress && rpcUrls && tokens) {
4655
+ const chainKeys = this.config.supportedChains ?? Object.keys(rpcUrls);
4656
+ const chainKey = chainKeys.find((c) => c.startsWith("evm") && rpcUrls[c] && tokens[c]);
4657
+ const rpcUrl = chainKey ? rpcUrls[chainKey] : void 0;
4658
+ const tokenAddress = chainKey ? tokens[chainKey] : void 0;
4659
+ if (chainKey && rpcUrl && tokenAddress) {
4660
+ try {
4661
+ out.push(await readEvmTokenBalance({ rpcUrl, chainKey, tokenAddress, owner: evmAddress }));
4662
+ } catch {
4663
+ }
4664
+ }
4665
+ }
4666
+ const solAddress = this.getSolanaAddress();
4667
+ const sol = this.config.solanaChannel;
4668
+ if (solAddress && sol?.rpcUrl && sol.tokenMint) {
4669
+ try {
4670
+ out.push(
4671
+ await readSolanaTokenBalance({ rpcUrl: sol.rpcUrl, mint: sol.tokenMint, owner: solAddress })
4672
+ );
4673
+ } catch {
4674
+ }
4675
+ }
4676
+ const minaAddress = this.getMinaAddress();
4677
+ const mina = this.config.minaChannel;
4678
+ if (minaAddress && mina?.graphqlUrl) {
4679
+ try {
4680
+ out.push(await readMinaBalance({ graphqlUrl: mina.graphqlUrl, owner: minaAddress }));
4681
+ } catch {
4682
+ }
4683
+ }
4684
+ return out;
4685
+ }
4536
4686
  /**
4537
4687
  * Resolves an ILP destination address to a peer ID.
4538
4688
  * Convention: destination "g.toon.peer1" → peerId "peer1" (last segment).