@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 +12 -13
- package/dist/index.d.ts +41 -10
- package/dist/index.js +163 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 `
|
|
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
|
|
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, …)` | **
|
|
15
|
-
| `client.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
|
|
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/
|
|
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
|
|
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 **
|
|
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
|
|
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
|
|
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/)** —
|
|
308
|
-
- **[@toon-protocol/sdk](../sdk/)** — Higher-level helpers including `streamSwap()` for multi-chain swaps via a **
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
3621
|
-
if (!
|
|
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 (
|
|
3631
|
-
const detail =
|
|
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 ${
|
|
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(
|
|
3735
|
+
parsed = JSON.parse(http3.body);
|
|
3639
3736
|
} catch {
|
|
3640
3737
|
throw new Error(
|
|
3641
|
-
`Blob upload response body was not valid JSON: "${
|
|
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: "${
|
|
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
|
|
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
|
|
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).
|