@toon-protocol/client 0.14.6 → 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 +89 -0
- package/dist/index.js +300 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
@@ -2165,6 +2189,13 @@ declare class OnChainChannelClient implements ConnectorChannelClient {
|
|
|
2165
2189
|
txHash?: string;
|
|
2166
2190
|
depositTotal: bigint;
|
|
2167
2191
|
}>;
|
|
2192
|
+
/**
|
|
2193
|
+
* Solana deposit: fire the standalone `deposit` instruction against the channel
|
|
2194
|
+
* vault. Incremental on-chain (the program adds `amount`), so the new total is
|
|
2195
|
+
* the caller-tracked current plus the delta. Requires the funded payer token
|
|
2196
|
+
* account (the funded ATA) from the Solana channel config.
|
|
2197
|
+
*/
|
|
2198
|
+
private depositSolana;
|
|
2168
2199
|
/**
|
|
2169
2200
|
* EVM deposit: approve the token-network for the delta if the allowance is
|
|
2170
2201
|
* short, then `setTotalDeposit(channelId, participant, current + delta)` —
|
|
@@ -2172,6 +2203,41 @@ declare class OnChainChannelClient implements ConnectorChannelClient {
|
|
|
2172
2203
|
* caller-supplied current locked amount.
|
|
2173
2204
|
*/
|
|
2174
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;
|
|
2175
2241
|
/**
|
|
2176
2242
|
* Opens a REAL on-chain Solana payment channel.
|
|
2177
2243
|
*
|
|
@@ -2235,6 +2301,12 @@ declare class OnChainChannelClient implements ConnectorChannelClient {
|
|
|
2235
2301
|
interface ChannelStoreEntry {
|
|
2236
2302
|
nonce: number;
|
|
2237
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;
|
|
2238
2310
|
}
|
|
2239
2311
|
/**
|
|
2240
2312
|
* Persistence interface for payment channel nonce/amount state.
|
|
@@ -2348,6 +2420,23 @@ declare class ChannelManager {
|
|
|
2348
2420
|
* collateral on the next read.
|
|
2349
2421
|
*/
|
|
2350
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';
|
|
2351
2440
|
/**
|
|
2352
2441
|
* Gets all tracked channel IDs.
|
|
2353
2442
|
*/
|
package/dist/index.js
CHANGED
|
@@ -2035,29 +2035,44 @@ async function openSolanaChannel(params) {
|
|
|
2035
2035
|
]);
|
|
2036
2036
|
let depositTxSignature;
|
|
2037
2037
|
if (params.deposit && params.deposit.amount > 0n) {
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
pubkey: params.deposit.payerTokenAccount,
|
|
2048
|
-
isSigner: false,
|
|
2049
|
-
isWritable: true
|
|
2050
|
-
},
|
|
2051
|
-
{ pubkey: vaultPDA, isSigner: false, isWritable: true },
|
|
2052
|
-
{ pubkey: channelPDA, isSigner: false, isWritable: true },
|
|
2053
|
-
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
|
|
2054
|
-
],
|
|
2055
|
-
data: depositData
|
|
2056
|
-
}
|
|
2057
|
-
]);
|
|
2038
|
+
({ depositTxSignature } = await depositSolanaChannel({
|
|
2039
|
+
rpcUrl,
|
|
2040
|
+
programId,
|
|
2041
|
+
channelPDA,
|
|
2042
|
+
payerSeed,
|
|
2043
|
+
payerPubkey,
|
|
2044
|
+
payerTokenAccount: params.deposit.payerTokenAccount,
|
|
2045
|
+
amount: params.deposit.amount
|
|
2046
|
+
}));
|
|
2058
2047
|
}
|
|
2059
2048
|
return { channelPDA, opened: true, initTxSignature, depositTxSignature };
|
|
2060
2049
|
}
|
|
2050
|
+
async function depositSolanaChannel(params) {
|
|
2051
|
+
const { rpcUrl, programId, channelPDA, payerSeed, payerPubkey, payerTokenAccount, amount } = params;
|
|
2052
|
+
if (amount <= 0n) throw new Error("Solana deposit amount must be positive.");
|
|
2053
|
+
const payer = {
|
|
2054
|
+
publicKey: padTo32(base58Decode(payerPubkey)),
|
|
2055
|
+
privateKey: payerSeed
|
|
2056
|
+
};
|
|
2057
|
+
const { pda: vaultPDA } = deriveVaultPDA(channelPDA, programId);
|
|
2058
|
+
const depositData = new Uint8Array(16);
|
|
2059
|
+
depositData.set(IX_DEPOSIT, 0);
|
|
2060
|
+
writeU64LE(depositData, 8, amount);
|
|
2061
|
+
const depositTxSignature = await buildAndSendTransaction(rpcUrl, payer, [
|
|
2062
|
+
{
|
|
2063
|
+
programId,
|
|
2064
|
+
keys: [
|
|
2065
|
+
{ pubkey: payerPubkey, isSigner: true, isWritable: false },
|
|
2066
|
+
{ pubkey: payerTokenAccount, isSigner: false, isWritable: true },
|
|
2067
|
+
{ pubkey: vaultPDA, isSigner: false, isWritable: true },
|
|
2068
|
+
{ pubkey: channelPDA, isSigner: false, isWritable: true },
|
|
2069
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
|
|
2070
|
+
],
|
|
2071
|
+
data: depositData
|
|
2072
|
+
}
|
|
2073
|
+
]);
|
|
2074
|
+
return { depositTxSignature };
|
|
2075
|
+
}
|
|
2061
2076
|
|
|
2062
2077
|
// src/channel/mina-channel-open.ts
|
|
2063
2078
|
import { hexToMinaBase58PrivateKey as hexToMinaBase58PrivateKey2 } from "@toon-protocol/core";
|
|
@@ -2257,6 +2272,20 @@ var TOKEN_NETWORK_ABI = [
|
|
|
2257
2272
|
],
|
|
2258
2273
|
outputs: []
|
|
2259
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
|
+
},
|
|
2260
2289
|
{
|
|
2261
2290
|
name: "channels",
|
|
2262
2291
|
type: "function",
|
|
@@ -2429,12 +2458,43 @@ var OnChainChannelClient = class {
|
|
|
2429
2458
|
);
|
|
2430
2459
|
}
|
|
2431
2460
|
const chainPrefix = ctx.chain.split(":")[0];
|
|
2432
|
-
if (chainPrefix === "solana"
|
|
2461
|
+
if (chainPrefix === "solana") {
|
|
2462
|
+
return this.depositSolana(channelId, amount, opts.currentDeposit);
|
|
2463
|
+
}
|
|
2464
|
+
if (chainPrefix === "mina") {
|
|
2465
|
+
throw new Error("Deposit on mina is not yet supported (EVM + Solana today; Mina follow-up).");
|
|
2466
|
+
}
|
|
2467
|
+
return this.depositEvm(channelId, amount, opts.currentDeposit, ctx);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Solana deposit: fire the standalone `deposit` instruction against the channel
|
|
2471
|
+
* vault. Incremental on-chain (the program adds `amount`), so the new total is
|
|
2472
|
+
* the caller-tracked current plus the delta. Requires the funded payer token
|
|
2473
|
+
* account (the funded ATA) from the Solana channel config.
|
|
2474
|
+
*/
|
|
2475
|
+
async depositSolana(channelId, amount, currentDeposit) {
|
|
2476
|
+
if (!this.solanaConfig) {
|
|
2477
|
+
throw new Error("Solana channel config not set \u2014 cannot deposit.");
|
|
2478
|
+
}
|
|
2479
|
+
const cfg = this.solanaConfig;
|
|
2480
|
+
const payerTokenAccount = cfg.deposit?.payerTokenAccount;
|
|
2481
|
+
if (!payerTokenAccount) {
|
|
2433
2482
|
throw new Error(
|
|
2434
|
-
|
|
2483
|
+
"Solana deposit requires solanaConfig.deposit.payerTokenAccount (the funded SPL token account)."
|
|
2435
2484
|
);
|
|
2436
2485
|
}
|
|
2437
|
-
|
|
2486
|
+
const payerSeed = cfg.keypair.slice(0, 32);
|
|
2487
|
+
const payerPubkey = base58Encode2(new Uint8Array(ed255192.getPublicKey(payerSeed)));
|
|
2488
|
+
const { depositTxSignature } = await depositSolanaChannel({
|
|
2489
|
+
rpcUrl: cfg.rpcUrl,
|
|
2490
|
+
programId: cfg.programId,
|
|
2491
|
+
channelPDA: channelId,
|
|
2492
|
+
payerSeed,
|
|
2493
|
+
payerPubkey,
|
|
2494
|
+
payerTokenAccount,
|
|
2495
|
+
amount
|
|
2496
|
+
});
|
|
2497
|
+
return { txHash: depositTxSignature, depositTotal: currentDeposit + amount };
|
|
2438
2498
|
}
|
|
2439
2499
|
/**
|
|
2440
2500
|
* EVM deposit: approve the token-network for the delta if the allowance is
|
|
@@ -2474,6 +2534,103 @@ var OnChainChannelClient = class {
|
|
|
2474
2534
|
await publicClient.waitForTransactionReceipt({ hash: depositHash });
|
|
2475
2535
|
return { txHash: depositHash, depositTotal: newTotal };
|
|
2476
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
|
+
}
|
|
2477
2634
|
/**
|
|
2478
2635
|
* Opens a REAL on-chain Solana payment channel.
|
|
2479
2636
|
*
|
|
@@ -3438,7 +3595,12 @@ var ChannelManager = class {
|
|
|
3438
3595
|
tokenNetworkAddress: tnAddr,
|
|
3439
3596
|
tokenAddress: chainContext?.tokenAddress,
|
|
3440
3597
|
recipient: chainContext?.recipient,
|
|
3441
|
-
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 } : {}
|
|
3442
3604
|
});
|
|
3443
3605
|
return;
|
|
3444
3606
|
}
|
|
@@ -3473,12 +3635,7 @@ var ChannelManager = class {
|
|
|
3473
3635
|
}
|
|
3474
3636
|
tracking.nonce += 1;
|
|
3475
3637
|
tracking.cumulativeAmount += additionalAmount;
|
|
3476
|
-
|
|
3477
|
-
this.store.save(channelId, {
|
|
3478
|
-
nonce: tracking.nonce,
|
|
3479
|
-
cumulativeAmount: tracking.cumulativeAmount
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3638
|
+
this.persist(channelId);
|
|
3482
3639
|
const signer = this.chainSigners.get(tracking.chainType);
|
|
3483
3640
|
if (signer && tracking.chainType !== "evm") {
|
|
3484
3641
|
if (!tracking.recipient) {
|
|
@@ -3578,6 +3735,57 @@ var ChannelManager = class {
|
|
|
3578
3735
|
}
|
|
3579
3736
|
tracking.depositTotal = total;
|
|
3580
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
|
+
}
|
|
3581
3789
|
/**
|
|
3582
3790
|
* Gets all tracked channel IDs.
|
|
3583
3791
|
*/
|
|
@@ -3603,7 +3811,10 @@ var JsonFileChannelStore = class {
|
|
|
3603
3811
|
const data = this.readFile();
|
|
3604
3812
|
data[channelId] = {
|
|
3605
3813
|
nonce: tracking.nonce,
|
|
3606
|
-
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() } : {}
|
|
3607
3818
|
};
|
|
3608
3819
|
this.writeFile(data);
|
|
3609
3820
|
}
|
|
@@ -3613,7 +3824,10 @@ var JsonFileChannelStore = class {
|
|
|
3613
3824
|
if (!entry) return void 0;
|
|
3614
3825
|
return {
|
|
3615
3826
|
nonce: entry.nonce,
|
|
3616
|
-
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) } : {}
|
|
3617
3831
|
};
|
|
3618
3832
|
}
|
|
3619
3833
|
list() {
|
|
@@ -4743,6 +4957,60 @@ var ToonClient = class {
|
|
|
4743
4957
|
depositTotal: result.depositTotal.toString()
|
|
4744
4958
|
};
|
|
4745
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
|
+
}
|
|
4746
5014
|
/**
|
|
4747
5015
|
* Read the on-chain settlement-token balance of this client's OWN wallet on
|
|
4748
5016
|
* each configured chain (EVM token, Solana SPL, native MINA). A free read — no
|