@toon-protocol/client 0.14.4 → 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/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
  *
@@ -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).
@@ -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
  */
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
 
@@ -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).