@toon-protocol/client 0.14.7 → 0.14.8

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
@@ -1240,6 +1240,30 @@ declare class ToonClient {
1240
1240
  txHash?: string;
1241
1241
  depositTotal: string;
1242
1242
  }>;
1243
+ /**
1244
+ * Close a channel to begin the settlement grace period (first half of
1245
+ * withdraw). Records `closedAt`/`settleableAt` (unix seconds) on the tracked
1246
+ * channel — persisted, so the grace timer survives a daemon restart. Spends
1247
+ * on-chain. EVM today; Solana/Mina are follow-ups.
1248
+ */
1249
+ closeChannel(channelId: string): Promise<{
1250
+ channelId: string;
1251
+ txHash?: string;
1252
+ closedAt: string;
1253
+ settleableAt: string;
1254
+ }>;
1255
+ /**
1256
+ * Settle a closed channel to release collateral (second half of withdraw).
1257
+ * THE time guard: never settle before `settleableAt`. A too-early call throws
1258
+ * a retryable error (carrying the remaining seconds) BEFORE spending gas — the
1259
+ * contract would revert anyway. Spends on-chain. EVM today.
1260
+ */
1261
+ settleChannel(channelId: string): Promise<{
1262
+ channelId: string;
1263
+ txHash?: string;
1264
+ }>;
1265
+ /** Where a tracked channel sits in the withdraw journey. */
1266
+ getChannelCloseState(channelId: string): 'open' | 'closing' | 'settleable' | 'settled';
1243
1267
  /**
1244
1268
  * Read the on-chain settlement-token balance of this client's OWN wallet on
1245
1269
  * each configured chain (EVM token, Solana SPL, native MINA). A free read — no
@@ -2179,6 +2203,41 @@ declare class OnChainChannelClient implements ConnectorChannelClient {
2179
2203
  * caller-supplied current locked amount.
2180
2204
  */
2181
2205
  private depositEvm;
2206
+ /**
2207
+ * Close a channel to begin the settlement grace period. Dispatches by the
2208
+ * channel's cached chain context. EVM `closeChannel` is unilateral (channelId
2209
+ * only); after it confirms we read the `channels()` view for the AUTHORITATIVE
2210
+ * `closedAt` + `settlementTimeout` (block-timestamp seconds) and compute
2211
+ * `settleableAt = closedAt + settlementTimeout`. Solana/Mina are follow-ups.
2212
+ */
2213
+ closeChannel(channelId: string): Promise<{
2214
+ txHash?: string;
2215
+ closedAt: bigint;
2216
+ settlementTimeout: bigint;
2217
+ settleableAt: bigint;
2218
+ }>;
2219
+ /**
2220
+ * Settle a closed channel after its grace period to release collateral. EVM
2221
+ * `settleChannel` is unilateral (channelId only); the contract itself reverts
2222
+ * before `closedAt + settlementTimeout`, so an early call surfaces as a tx
2223
+ * revert here — but the caller (ToonClient/daemon) enforces the time guard
2224
+ * BEFORE spending gas. Solana/Mina are follow-ups.
2225
+ */
2226
+ settleChannel(channelId: string): Promise<{
2227
+ txHash?: string;
2228
+ }>;
2229
+ /**
2230
+ * Read the EVM channel's close-relevant fields so a restarted daemon can
2231
+ * recompute the grace timer from chain (chain is authoritative). EVM-only.
2232
+ */
2233
+ getChannelCloseInfo(channelId: string): Promise<{
2234
+ status: ChannelState['status'];
2235
+ closedAt: bigint;
2236
+ settlementTimeout: bigint;
2237
+ settleableAt: bigint;
2238
+ }>;
2239
+ /** Read + destructure the EVM `channels(bytes32)` view. */
2240
+ private readEvmChannel;
2182
2241
  /**
2183
2242
  * Opens a REAL on-chain Solana payment channel.
2184
2243
  *
@@ -2242,6 +2301,12 @@ declare class OnChainChannelClient implements ConnectorChannelClient {
2242
2301
  interface ChannelStoreEntry {
2243
2302
  nonce: number;
2244
2303
  cumulativeAmount: bigint;
2304
+ /** Unix SECONDS when close was initiated (withdraw flow). */
2305
+ closedAt?: bigint;
2306
+ /** Unix SECONDS the channel becomes settleable (= closedAt + settlementTimeout). */
2307
+ settleableAt?: bigint;
2308
+ /** Unix SECONDS the channel was settled (collateral released). */
2309
+ settledAt?: bigint;
2245
2310
  }
2246
2311
  /**
2247
2312
  * Persistence interface for payment channel nonce/amount state.
@@ -2355,6 +2420,23 @@ declare class ChannelManager {
2355
2420
  * collateral on the next read.
2356
2421
  */
2357
2422
  setDepositTotal(channelId: string, total: bigint): void;
2423
+ /** Persist a channel's full nonce/amount + withdraw-timer state to the store. */
2424
+ private persist;
2425
+ /**
2426
+ * Record that a channel was closed (withdraw flow): stores `closedAt` +
2427
+ * `settleableAt` (unix SECONDS) so the grace timer survives a daemon restart.
2428
+ */
2429
+ setChannelClosed(channelId: string, closedAt: bigint, settleableAt: bigint): void;
2430
+ /** Record that a channel was settled (collateral released). */
2431
+ setChannelSettled(channelId: string, settledAt: bigint): void;
2432
+ /** The `settleableAt` timestamp (unix seconds) for a closed channel, if set. */
2433
+ getSettleableAt(channelId: string): bigint | undefined;
2434
+ /**
2435
+ * Where a channel sits in the withdraw journey, from the tracked timers:
2436
+ * `open` (never closed) → `closing` (closed, grace not elapsed) →
2437
+ * `settleable` (grace elapsed) → `settled`. `nowSec` is injectable for tests.
2438
+ */
2439
+ getChannelCloseState(channelId: string, nowSec?: bigint): 'open' | 'closing' | 'settleable' | 'settled';
2358
2440
  /**
2359
2441
  * Gets all tracked channel IDs.
2360
2442
  */
package/dist/index.js CHANGED
@@ -2272,6 +2272,20 @@ var TOKEN_NETWORK_ABI = [
2272
2272
  ],
2273
2273
  outputs: []
2274
2274
  },
2275
+ {
2276
+ name: "closeChannel",
2277
+ type: "function",
2278
+ stateMutability: "nonpayable",
2279
+ inputs: [{ name: "channelId", type: "bytes32" }],
2280
+ outputs: []
2281
+ },
2282
+ {
2283
+ name: "settleChannel",
2284
+ type: "function",
2285
+ stateMutability: "nonpayable",
2286
+ inputs: [{ name: "channelId", type: "bytes32" }],
2287
+ outputs: []
2288
+ },
2275
2289
  {
2276
2290
  name: "channels",
2277
2291
  type: "function",
@@ -2520,6 +2534,103 @@ var OnChainChannelClient = class {
2520
2534
  await publicClient.waitForTransactionReceipt({ hash: depositHash });
2521
2535
  return { txHash: depositHash, depositTotal: newTotal };
2522
2536
  }
2537
+ /**
2538
+ * Close a channel to begin the settlement grace period. Dispatches by the
2539
+ * channel's cached chain context. EVM `closeChannel` is unilateral (channelId
2540
+ * only); after it confirms we read the `channels()` view for the AUTHORITATIVE
2541
+ * `closedAt` + `settlementTimeout` (block-timestamp seconds) and compute
2542
+ * `settleableAt = closedAt + settlementTimeout`. Solana/Mina are follow-ups.
2543
+ */
2544
+ async closeChannel(channelId) {
2545
+ const ctx = this.channelContext.get(channelId);
2546
+ if (!ctx) {
2547
+ throw new Error(
2548
+ `No on-chain context for channel "${channelId}" \u2014 it must be opened by this client first.`
2549
+ );
2550
+ }
2551
+ const chainPrefix = ctx.chain.split(":")[0];
2552
+ if (chainPrefix === "solana" || chainPrefix === "mina") {
2553
+ throw new Error(
2554
+ `Close on ${chainPrefix} is not yet supported (EVM today; Solana/Mina follow-up).`
2555
+ );
2556
+ }
2557
+ const { publicClient, walletClient } = this.createClients(ctx.chain);
2558
+ const tokenNetworkAddr = ctx.tokenNetworkAddress;
2559
+ const closeHash = await walletClient.writeContract({
2560
+ address: tokenNetworkAddr,
2561
+ abi: TOKEN_NETWORK_ABI,
2562
+ functionName: "closeChannel",
2563
+ args: [channelId]
2564
+ });
2565
+ await publicClient.waitForTransactionReceipt({ hash: closeHash });
2566
+ const info = await this.readEvmChannel(publicClient, tokenNetworkAddr, channelId);
2567
+ return {
2568
+ txHash: closeHash,
2569
+ closedAt: info.closedAt,
2570
+ settlementTimeout: info.settlementTimeout,
2571
+ settleableAt: info.closedAt + info.settlementTimeout
2572
+ };
2573
+ }
2574
+ /**
2575
+ * Settle a closed channel after its grace period to release collateral. EVM
2576
+ * `settleChannel` is unilateral (channelId only); the contract itself reverts
2577
+ * before `closedAt + settlementTimeout`, so an early call surfaces as a tx
2578
+ * revert here — but the caller (ToonClient/daemon) enforces the time guard
2579
+ * BEFORE spending gas. Solana/Mina are follow-ups.
2580
+ */
2581
+ async settleChannel(channelId) {
2582
+ const ctx = this.channelContext.get(channelId);
2583
+ if (!ctx) {
2584
+ throw new Error(
2585
+ `No on-chain context for channel "${channelId}" \u2014 it must be opened by this client first.`
2586
+ );
2587
+ }
2588
+ const chainPrefix = ctx.chain.split(":")[0];
2589
+ if (chainPrefix === "solana" || chainPrefix === "mina") {
2590
+ throw new Error(
2591
+ `Settle on ${chainPrefix} is not yet supported (EVM today; Solana/Mina follow-up).`
2592
+ );
2593
+ }
2594
+ const { publicClient, walletClient } = this.createClients(ctx.chain);
2595
+ const settleHash = await walletClient.writeContract({
2596
+ address: ctx.tokenNetworkAddress,
2597
+ abi: TOKEN_NETWORK_ABI,
2598
+ functionName: "settleChannel",
2599
+ args: [channelId]
2600
+ });
2601
+ await publicClient.waitForTransactionReceipt({ hash: settleHash });
2602
+ return { txHash: settleHash };
2603
+ }
2604
+ /**
2605
+ * Read the EVM channel's close-relevant fields so a restarted daemon can
2606
+ * recompute the grace timer from chain (chain is authoritative). EVM-only.
2607
+ */
2608
+ async getChannelCloseInfo(channelId) {
2609
+ const ctx = this.channelContext.get(channelId);
2610
+ if (!ctx) throw new Error(`No on-chain context for channel "${channelId}".`);
2611
+ const chainPrefix = ctx.chain.split(":")[0];
2612
+ if (chainPrefix === "solana" || chainPrefix === "mina") {
2613
+ throw new Error(`getChannelCloseInfo on ${chainPrefix} is not supported.`);
2614
+ }
2615
+ const { publicClient } = this.createClients(ctx.chain);
2616
+ const info = await this.readEvmChannel(publicClient, ctx.tokenNetworkAddress, channelId);
2617
+ return {
2618
+ status: STATE_MAP2[info.state] ?? "open",
2619
+ closedAt: info.closedAt,
2620
+ settlementTimeout: info.settlementTimeout,
2621
+ settleableAt: info.closedAt + info.settlementTimeout
2622
+ };
2623
+ }
2624
+ /** Read + destructure the EVM `channels(bytes32)` view. */
2625
+ async readEvmChannel(publicClient, tokenNetworkAddr, channelId) {
2626
+ const res = await publicClient.readContract({
2627
+ address: tokenNetworkAddr,
2628
+ abi: TOKEN_NETWORK_ABI,
2629
+ functionName: "channels",
2630
+ args: [channelId]
2631
+ });
2632
+ return { settlementTimeout: res[0], state: Number(res[1]), closedAt: res[2] };
2633
+ }
2523
2634
  /**
2524
2635
  * Opens a REAL on-chain Solana payment channel.
2525
2636
  *
@@ -3484,7 +3595,12 @@ var ChannelManager = class {
3484
3595
  tokenNetworkAddress: tnAddr,
3485
3596
  tokenAddress: chainContext?.tokenAddress,
3486
3597
  recipient: chainContext?.recipient,
3487
- depositTotal: chainContext?.depositTotal
3598
+ depositTotal: chainContext?.depositTotal,
3599
+ // Resume the withdraw-flow timers so a daemon restart mid-grace
3600
+ // doesn't strand funds (the gate can't be evaluated without them).
3601
+ ...persisted.closedAt !== void 0 ? { closedAt: persisted.closedAt } : {},
3602
+ ...persisted.settleableAt !== void 0 ? { settleableAt: persisted.settleableAt } : {},
3603
+ ...persisted.settledAt !== void 0 ? { settledAt: persisted.settledAt } : {}
3488
3604
  });
3489
3605
  return;
3490
3606
  }
@@ -3519,12 +3635,7 @@ var ChannelManager = class {
3519
3635
  }
3520
3636
  tracking.nonce += 1;
3521
3637
  tracking.cumulativeAmount += additionalAmount;
3522
- if (this.store) {
3523
- this.store.save(channelId, {
3524
- nonce: tracking.nonce,
3525
- cumulativeAmount: tracking.cumulativeAmount
3526
- });
3527
- }
3638
+ this.persist(channelId);
3528
3639
  const signer = this.chainSigners.get(tracking.chainType);
3529
3640
  if (signer && tracking.chainType !== "evm") {
3530
3641
  if (!tracking.recipient) {
@@ -3624,6 +3735,57 @@ var ChannelManager = class {
3624
3735
  }
3625
3736
  tracking.depositTotal = total;
3626
3737
  }
3738
+ /** Persist a channel's full nonce/amount + withdraw-timer state to the store. */
3739
+ persist(channelId) {
3740
+ if (!this.store) return;
3741
+ const t = this.channels.get(channelId);
3742
+ if (!t) return;
3743
+ this.store.save(channelId, {
3744
+ nonce: t.nonce,
3745
+ cumulativeAmount: t.cumulativeAmount,
3746
+ ...t.closedAt !== void 0 ? { closedAt: t.closedAt } : {},
3747
+ ...t.settleableAt !== void 0 ? { settleableAt: t.settleableAt } : {},
3748
+ ...t.settledAt !== void 0 ? { settledAt: t.settledAt } : {}
3749
+ });
3750
+ }
3751
+ /**
3752
+ * Record that a channel was closed (withdraw flow): stores `closedAt` +
3753
+ * `settleableAt` (unix SECONDS) so the grace timer survives a daemon restart.
3754
+ */
3755
+ setChannelClosed(channelId, closedAt, settleableAt) {
3756
+ const tracking = this.channels.get(channelId);
3757
+ if (!tracking) {
3758
+ throw new Error(`Channel "${channelId}" is not being tracked.`);
3759
+ }
3760
+ tracking.closedAt = closedAt;
3761
+ tracking.settleableAt = settleableAt;
3762
+ this.persist(channelId);
3763
+ }
3764
+ /** Record that a channel was settled (collateral released). */
3765
+ setChannelSettled(channelId, settledAt) {
3766
+ const tracking = this.channels.get(channelId);
3767
+ if (!tracking) {
3768
+ throw new Error(`Channel "${channelId}" is not being tracked.`);
3769
+ }
3770
+ tracking.settledAt = settledAt;
3771
+ this.persist(channelId);
3772
+ }
3773
+ /** The `settleableAt` timestamp (unix seconds) for a closed channel, if set. */
3774
+ getSettleableAt(channelId) {
3775
+ return this.channels.get(channelId)?.settleableAt;
3776
+ }
3777
+ /**
3778
+ * Where a channel sits in the withdraw journey, from the tracked timers:
3779
+ * `open` (never closed) → `closing` (closed, grace not elapsed) →
3780
+ * `settleable` (grace elapsed) → `settled`. `nowSec` is injectable for tests.
3781
+ */
3782
+ getChannelCloseState(channelId, nowSec = BigInt(Math.floor(Date.now() / 1e3))) {
3783
+ const t = this.channels.get(channelId);
3784
+ if (!t || t.closedAt === void 0) return "open";
3785
+ if (t.settledAt !== void 0) return "settled";
3786
+ if (t.settleableAt !== void 0 && nowSec >= t.settleableAt) return "settleable";
3787
+ return "closing";
3788
+ }
3627
3789
  /**
3628
3790
  * Gets all tracked channel IDs.
3629
3791
  */
@@ -3649,7 +3811,10 @@ var JsonFileChannelStore = class {
3649
3811
  const data = this.readFile();
3650
3812
  data[channelId] = {
3651
3813
  nonce: tracking.nonce,
3652
- cumulativeAmount: tracking.cumulativeAmount.toString()
3814
+ cumulativeAmount: tracking.cumulativeAmount.toString(),
3815
+ ...tracking.closedAt !== void 0 ? { closedAt: tracking.closedAt.toString() } : {},
3816
+ ...tracking.settleableAt !== void 0 ? { settleableAt: tracking.settleableAt.toString() } : {},
3817
+ ...tracking.settledAt !== void 0 ? { settledAt: tracking.settledAt.toString() } : {}
3653
3818
  };
3654
3819
  this.writeFile(data);
3655
3820
  }
@@ -3659,7 +3824,10 @@ var JsonFileChannelStore = class {
3659
3824
  if (!entry) return void 0;
3660
3825
  return {
3661
3826
  nonce: entry.nonce,
3662
- cumulativeAmount: BigInt(entry.cumulativeAmount)
3827
+ cumulativeAmount: BigInt(entry.cumulativeAmount),
3828
+ ...entry.closedAt !== void 0 ? { closedAt: BigInt(entry.closedAt) } : {},
3829
+ ...entry.settleableAt !== void 0 ? { settleableAt: BigInt(entry.settleableAt) } : {},
3830
+ ...entry.settledAt !== void 0 ? { settledAt: BigInt(entry.settledAt) } : {}
3663
3831
  };
3664
3832
  }
3665
3833
  list() {
@@ -4789,6 +4957,60 @@ var ToonClient = class {
4789
4957
  depositTotal: result.depositTotal.toString()
4790
4958
  };
4791
4959
  }
4960
+ /**
4961
+ * Close a channel to begin the settlement grace period (first half of
4962
+ * withdraw). Records `closedAt`/`settleableAt` (unix seconds) on the tracked
4963
+ * channel — persisted, so the grace timer survives a daemon restart. Spends
4964
+ * on-chain. EVM today; Solana/Mina are follow-ups.
4965
+ */
4966
+ async closeChannel(channelId) {
4967
+ if (!this.channelManager) throw new Error("ChannelManager not initialized");
4968
+ if (!this.onChainChannelClient) {
4969
+ throw new Error("On-chain channel client not configured (no chainRpcUrls).");
4970
+ }
4971
+ const r = await this.onChainChannelClient.closeChannel(channelId);
4972
+ this.channelManager.setChannelClosed(channelId, r.closedAt, r.settleableAt);
4973
+ return {
4974
+ channelId,
4975
+ ...r.txHash ? { txHash: r.txHash } : {},
4976
+ closedAt: r.closedAt.toString(),
4977
+ settleableAt: r.settleableAt.toString()
4978
+ };
4979
+ }
4980
+ /**
4981
+ * Settle a closed channel to release collateral (second half of withdraw).
4982
+ * THE time guard: never settle before `settleableAt`. A too-early call throws
4983
+ * a retryable error (carrying the remaining seconds) BEFORE spending gas — the
4984
+ * contract would revert anyway. Spends on-chain. EVM today.
4985
+ */
4986
+ async settleChannel(channelId) {
4987
+ if (!this.channelManager) throw new Error("ChannelManager not initialized");
4988
+ if (!this.onChainChannelClient) {
4989
+ throw new Error("On-chain channel client not configured (no chainRpcUrls).");
4990
+ }
4991
+ const settleableAt = this.channelManager.getSettleableAt(channelId);
4992
+ if (settleableAt === void 0) {
4993
+ throw new Error(`Channel "${channelId}" is not closed; call closeChannel first.`);
4994
+ }
4995
+ const nowSec = BigInt(Math.floor(Date.now() / 1e3));
4996
+ if (nowSec < settleableAt) {
4997
+ const remaining = settleableAt - nowSec;
4998
+ throw Object.assign(
4999
+ new Error(
5000
+ `Channel "${channelId}" is not settleable yet \u2014 ${remaining}s remain (settleable at ${settleableAt}).`
5001
+ ),
5002
+ { name: "SettleTooEarlyError", retryable: true, settleableAt: settleableAt.toString() }
5003
+ );
5004
+ }
5005
+ const r = await this.onChainChannelClient.settleChannel(channelId);
5006
+ this.channelManager.setChannelSettled(channelId, nowSec);
5007
+ return { channelId, ...r.txHash ? { txHash: r.txHash } : {} };
5008
+ }
5009
+ /** Where a tracked channel sits in the withdraw journey. */
5010
+ getChannelCloseState(channelId) {
5011
+ if (!this.channelManager) throw new Error("ChannelManager not initialized");
5012
+ return this.channelManager.getChannelCloseState(channelId);
5013
+ }
4792
5014
  /**
4793
5015
  * Read the on-chain settlement-token balance of this client's OWN wallet on
4794
5016
  * each configured chain (EVM token, Solana SPL, native MINA). A free read — no