@t402/streaming-payments 1.0.0-beta.1

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.
@@ -0,0 +1,1135 @@
1
+ // src/channels/types.ts
2
+ import { z } from "zod";
3
+ var ChannelState = z.enum([
4
+ "created",
5
+ // Channel created but not funded
6
+ "funding",
7
+ // Waiting for funding transaction
8
+ "open",
9
+ // Channel is active and streaming
10
+ "paused",
11
+ // Channel temporarily paused
12
+ "closing",
13
+ // Channel close initiated
14
+ "disputing",
15
+ // Dispute in progress
16
+ "closed",
17
+ // Channel fully closed
18
+ "expired"
19
+ // Channel expired without proper close
20
+ ]);
21
+ var ChannelConfig = z.object({
22
+ // Funding requirements
23
+ minDeposit: z.string().default("1000000"),
24
+ // Minimum deposit (in smallest units)
25
+ maxDeposit: z.string().optional(),
26
+ // Timing
27
+ challengePeriod: z.number().default(86400),
28
+ // Dispute window in seconds (24h default)
29
+ expirationTime: z.number().optional(),
30
+ // Optional expiration timestamp
31
+ // Checkpointing
32
+ checkpointInterval: z.number().default(3600),
33
+ // Checkpoint every hour
34
+ minCheckpointAmount: z.string().default("100000"),
35
+ // Min amount change to checkpoint
36
+ // Fees
37
+ channelFee: z.string().default("0"),
38
+ // One-time channel opening fee
39
+ settlementFee: z.string().default("0")
40
+ // Fee for final settlement
41
+ });
42
+ var ChannelParticipant = z.object({
43
+ address: z.string(),
44
+ role: z.enum(["payer", "payee"]),
45
+ publicKey: z.string().optional()
46
+ // For off-chain signature verification
47
+ });
48
+ var ChannelBalance = z.object({
49
+ payer: z.string(),
50
+ // Payer's remaining balance
51
+ payee: z.string(),
52
+ // Payee's accrued balance
53
+ total: z.string(),
54
+ // Total channel capacity
55
+ locked: z.string().default("0")
56
+ // Amount locked in disputes
57
+ });
58
+ var ChannelCheckpoint = z.object({
59
+ channelId: z.string(),
60
+ sequence: z.number(),
61
+ // Monotonically increasing sequence number
62
+ timestamp: z.number(),
63
+ balance: ChannelBalance,
64
+ amountStreamed: z.string(),
65
+ // Total amount streamed so far
66
+ payerSignature: z.string(),
67
+ payeeSignature: z.string().optional(),
68
+ // Payee signature for mutual checkpoints
69
+ stateRoot: z.string().optional()
70
+ // Merkle root of channel state
71
+ });
72
+ var StreamingChannel = z.object({
73
+ id: z.string(),
74
+ state: ChannelState,
75
+ chain: z.string(),
76
+ // CAIP-2 chain ID
77
+ asset: z.string(),
78
+ // Token contract address
79
+ // Participants
80
+ payer: ChannelParticipant,
81
+ payee: ChannelParticipant,
82
+ // Financial state
83
+ balance: ChannelBalance,
84
+ // Streaming parameters
85
+ ratePerSecond: z.string(),
86
+ // Amount per second
87
+ startTime: z.number().optional(),
88
+ // When streaming started
89
+ pausedAt: z.number().optional(),
90
+ // When paused (if paused)
91
+ // Configuration
92
+ config: ChannelConfig,
93
+ // On-chain references
94
+ contractAddress: z.string().optional(),
95
+ // Payment channel contract
96
+ fundingTxHash: z.string().optional(),
97
+ closingTxHash: z.string().optional(),
98
+ // Checkpointing
99
+ checkpoints: z.array(ChannelCheckpoint),
100
+ latestCheckpoint: ChannelCheckpoint.optional(),
101
+ // Timestamps
102
+ createdAt: z.number(),
103
+ updatedAt: z.number(),
104
+ closedAt: z.number().optional()
105
+ });
106
+ var ChannelCreateRequest = z.object({
107
+ chain: z.string(),
108
+ asset: z.string(),
109
+ payerAddress: z.string(),
110
+ payeeAddress: z.string(),
111
+ depositAmount: z.string(),
112
+ ratePerSecond: z.string(),
113
+ config: ChannelConfig.optional()
114
+ });
115
+ var FundingTransaction = z.object({
116
+ channelId: z.string(),
117
+ txHash: z.string(),
118
+ amount: z.string(),
119
+ sender: z.string(),
120
+ blockNumber: z.number().optional(),
121
+ timestamp: z.number(),
122
+ confirmed: z.boolean()
123
+ });
124
+ var ChannelCloseRequest = z.object({
125
+ channelId: z.string(),
126
+ initiator: z.string(),
127
+ // Address of closer
128
+ reason: z.enum(["mutual", "unilateral", "timeout", "dispute"]),
129
+ finalCheckpoint: ChannelCheckpoint.optional(),
130
+ signature: z.string()
131
+ });
132
+ var ChannelDispute = z.object({
133
+ channelId: z.string(),
134
+ disputeId: z.string(),
135
+ initiator: z.string(),
136
+ reason: z.string(),
137
+ claimedBalance: ChannelBalance,
138
+ evidence: z.array(z.object({
139
+ type: z.enum(["checkpoint", "signature", "transaction"]),
140
+ data: z.string(),
141
+ timestamp: z.number()
142
+ })),
143
+ status: z.enum(["pending", "resolved", "rejected"]),
144
+ resolution: z.object({
145
+ winner: z.string().optional(),
146
+ finalBalance: ChannelBalance.optional(),
147
+ timestamp: z.number().optional()
148
+ }).optional(),
149
+ createdAt: z.number(),
150
+ expiresAt: z.number()
151
+ });
152
+ var StateTransition = z.object({
153
+ from: ChannelState,
154
+ to: ChannelState,
155
+ trigger: z.string(),
156
+ timestamp: z.number(),
157
+ metadata: z.record(z.unknown()).optional()
158
+ });
159
+ var RecoveryData = z.object({
160
+ channelId: z.string(),
161
+ checkpoints: z.array(ChannelCheckpoint),
162
+ transactions: z.array(FundingTransaction),
163
+ disputes: z.array(ChannelDispute),
164
+ stateHistory: z.array(StateTransition),
165
+ recoveredAt: z.number()
166
+ });
167
+
168
+ // src/channels/state.ts
169
+ var STATE_TRANSITIONS = {
170
+ created: ["funding", "closed"],
171
+ funding: ["open", "closed", "expired"],
172
+ open: ["paused", "closing", "disputing"],
173
+ paused: ["open", "closing", "disputing"],
174
+ closing: ["closed", "disputing"],
175
+ disputing: ["closing", "closed"],
176
+ closed: [],
177
+ expired: []
178
+ };
179
+ var ChannelStateMachine = class {
180
+ channel;
181
+ transitions = [];
182
+ listeners = /* @__PURE__ */ new Map();
183
+ constructor(channel) {
184
+ this.channel = { ...channel };
185
+ }
186
+ /**
187
+ * Get current channel state
188
+ */
189
+ getChannel() {
190
+ return { ...this.channel };
191
+ }
192
+ /**
193
+ * Get current state
194
+ */
195
+ getState() {
196
+ return this.channel.state;
197
+ }
198
+ /**
199
+ * Get state history
200
+ */
201
+ getTransitions() {
202
+ return [...this.transitions];
203
+ }
204
+ /**
205
+ * Check if a transition is valid
206
+ */
207
+ canTransition(to) {
208
+ const allowedTransitions = STATE_TRANSITIONS[this.channel.state];
209
+ return allowedTransitions.includes(to);
210
+ }
211
+ /**
212
+ * Get allowed transitions from current state
213
+ */
214
+ getAllowedTransitions() {
215
+ return STATE_TRANSITIONS[this.channel.state];
216
+ }
217
+ /**
218
+ * Process a state event
219
+ */
220
+ process(event) {
221
+ const currentState = this.channel.state;
222
+ let nextState = null;
223
+ let metadata = {};
224
+ switch (event.type) {
225
+ case "FUND":
226
+ if (currentState !== "created") {
227
+ return { success: false, error: "Channel must be in created state to fund" };
228
+ }
229
+ nextState = "funding";
230
+ metadata = { txHash: event.txHash, amount: event.amount };
231
+ this.channel.fundingTxHash = event.txHash;
232
+ break;
233
+ case "CONFIRM_FUNDING":
234
+ if (currentState !== "funding") {
235
+ return { success: false, error: "Channel must be in funding state" };
236
+ }
237
+ nextState = "open";
238
+ this.channel.startTime = Date.now();
239
+ break;
240
+ case "START_STREAMING":
241
+ if (currentState !== "open" && currentState !== "paused") {
242
+ return { success: false, error: "Channel must be open or paused to stream" };
243
+ }
244
+ if (currentState === "paused") {
245
+ nextState = "open";
246
+ this.channel.pausedAt = void 0;
247
+ }
248
+ break;
249
+ case "PAUSE":
250
+ if (currentState !== "open") {
251
+ return { success: false, error: "Channel must be open to pause" };
252
+ }
253
+ nextState = "paused";
254
+ this.channel.pausedAt = Date.now();
255
+ break;
256
+ case "RESUME":
257
+ if (currentState !== "paused") {
258
+ return { success: false, error: "Channel must be paused to resume" };
259
+ }
260
+ nextState = "open";
261
+ this.channel.pausedAt = void 0;
262
+ break;
263
+ case "INITIATE_CLOSE":
264
+ if (currentState !== "open" && currentState !== "paused") {
265
+ return { success: false, error: "Channel must be open or paused to close" };
266
+ }
267
+ nextState = "closing";
268
+ metadata = { initiator: event.initiator };
269
+ break;
270
+ case "MUTUAL_CLOSE":
271
+ if (currentState !== "closing") {
272
+ return { success: false, error: "Channel must be in closing state" };
273
+ }
274
+ nextState = "closed";
275
+ this.channel.closedAt = Date.now();
276
+ metadata = { signature: event.signature };
277
+ break;
278
+ case "DISPUTE":
279
+ if (!["open", "paused", "closing"].includes(currentState)) {
280
+ return { success: false, error: "Cannot dispute in current state" };
281
+ }
282
+ nextState = "disputing";
283
+ metadata = { reason: event.reason };
284
+ break;
285
+ case "RESOLVE_DISPUTE":
286
+ if (currentState !== "disputing") {
287
+ return { success: false, error: "No dispute to resolve" };
288
+ }
289
+ nextState = "closing";
290
+ metadata = { winner: event.winner };
291
+ break;
292
+ case "FINALIZE":
293
+ if (currentState !== "closing") {
294
+ return { success: false, error: "Channel must be in closing state" };
295
+ }
296
+ nextState = "closed";
297
+ this.channel.closedAt = Date.now();
298
+ break;
299
+ case "TIMEOUT":
300
+ if (currentState === "funding") {
301
+ nextState = "expired";
302
+ } else if (currentState === "disputing") {
303
+ nextState = "closed";
304
+ this.channel.closedAt = Date.now();
305
+ } else {
306
+ return { success: false, error: "Timeout not applicable in current state" };
307
+ }
308
+ break;
309
+ default:
310
+ return { success: false, error: "Unknown event type" };
311
+ }
312
+ if (nextState && nextState !== currentState) {
313
+ if (!this.canTransition(nextState)) {
314
+ return { success: false, error: `Invalid transition from ${currentState} to ${nextState}` };
315
+ }
316
+ const transition = {
317
+ from: currentState,
318
+ to: nextState,
319
+ trigger: event.type,
320
+ timestamp: Date.now(),
321
+ metadata
322
+ };
323
+ this.channel.state = nextState;
324
+ this.channel.updatedAt = Date.now();
325
+ this.transitions.push(transition);
326
+ this.notifyListeners(nextState);
327
+ }
328
+ return { success: true };
329
+ }
330
+ /**
331
+ * Update channel balance
332
+ */
333
+ updateBalance(payerBalance, payeeBalance) {
334
+ this.channel.balance = {
335
+ ...this.channel.balance,
336
+ payer: payerBalance,
337
+ payee: payeeBalance
338
+ };
339
+ this.channel.updatedAt = Date.now();
340
+ }
341
+ /**
342
+ * Add checkpoint
343
+ */
344
+ addCheckpoint(checkpoint) {
345
+ this.channel.checkpoints.push(checkpoint);
346
+ this.channel.latestCheckpoint = checkpoint;
347
+ this.channel.updatedAt = Date.now();
348
+ }
349
+ /**
350
+ * Get current streamed amount
351
+ */
352
+ getCurrentStreamedAmount() {
353
+ if (!this.channel.startTime) return "0";
354
+ if (this.channel.state !== "open" && this.channel.state !== "paused") {
355
+ return this.channel.balance.payee;
356
+ }
357
+ const now = this.channel.pausedAt ?? Date.now();
358
+ const elapsed = Math.floor((now - this.channel.startTime) / 1e3);
359
+ const streamed = BigInt(this.channel.ratePerSecond) * BigInt(elapsed);
360
+ const maxStreamed = BigInt(this.channel.balance.total);
361
+ return (streamed > maxStreamed ? maxStreamed : streamed).toString();
362
+ }
363
+ /**
364
+ * Calculate remaining balance
365
+ */
366
+ getRemainingBalance() {
367
+ const total = BigInt(this.channel.balance.total);
368
+ const streamed = BigInt(this.getCurrentStreamedAmount());
369
+ return (total - streamed).toString();
370
+ }
371
+ /**
372
+ * Check if channel needs checkpoint
373
+ */
374
+ needsCheckpoint() {
375
+ const config = this.channel.config;
376
+ const lastCheckpoint = this.channel.latestCheckpoint;
377
+ if (!lastCheckpoint) return true;
378
+ const timeSinceCheckpoint = Date.now() - lastCheckpoint.timestamp;
379
+ if (timeSinceCheckpoint >= config.checkpointInterval * 1e3) return true;
380
+ const currentStreamed = BigInt(this.getCurrentStreamedAmount());
381
+ const lastStreamed = BigInt(lastCheckpoint.amountStreamed);
382
+ const amountChange = currentStreamed - lastStreamed;
383
+ if (amountChange >= BigInt(config.minCheckpointAmount)) return true;
384
+ return false;
385
+ }
386
+ /**
387
+ * Subscribe to state changes
388
+ */
389
+ onStateChange(state, callback) {
390
+ const key = state;
391
+ if (!this.listeners.has(key)) {
392
+ this.listeners.set(key, /* @__PURE__ */ new Set());
393
+ }
394
+ this.listeners.get(key).add(callback);
395
+ return () => {
396
+ this.listeners.get(key)?.delete(callback);
397
+ };
398
+ }
399
+ notifyListeners(newState) {
400
+ this.listeners.get(newState)?.forEach((cb) => cb(this.channel));
401
+ this.listeners.get("*")?.forEach((cb) => cb(this.channel));
402
+ }
403
+ };
404
+ function isChannelActive(channel) {
405
+ return channel.state === "open";
406
+ }
407
+ function isChannelTerminal(channel) {
408
+ return channel.state === "closed" || channel.state === "expired";
409
+ }
410
+ function getChannelUtilization(channel) {
411
+ const total = parseFloat(channel.balance.total);
412
+ if (total === 0) return 0;
413
+ const payee = parseFloat(channel.balance.payee);
414
+ return payee / total * 100;
415
+ }
416
+
417
+ // src/channels/opening.ts
418
+ var ChannelOpener = class {
419
+ config;
420
+ constructor(config = {}) {
421
+ this.config = {
422
+ fundingTimeout: config.fundingTimeout ?? 36e5,
423
+ // 1 hour
424
+ confirmations: config.confirmations ?? 1,
425
+ contractAddress: config.contractAddress ?? ""
426
+ };
427
+ }
428
+ /**
429
+ * Create a new channel
430
+ */
431
+ create(request) {
432
+ this.validateCreateRequest(request);
433
+ const channelConfig = request.config ?? ChannelConfig.parse({});
434
+ const now = Date.now();
435
+ const channelId = this.generateChannelId(request, now);
436
+ const payer = {
437
+ address: request.payerAddress,
438
+ role: "payer"
439
+ };
440
+ const payee = {
441
+ address: request.payeeAddress,
442
+ role: "payee"
443
+ };
444
+ const balance = {
445
+ payer: request.depositAmount,
446
+ payee: "0",
447
+ total: request.depositAmount,
448
+ locked: "0"
449
+ };
450
+ const channel = {
451
+ id: channelId,
452
+ state: "created",
453
+ chain: request.chain,
454
+ asset: request.asset,
455
+ payer,
456
+ payee,
457
+ balance,
458
+ ratePerSecond: request.ratePerSecond,
459
+ config: channelConfig,
460
+ contractAddress: this.config.contractAddress || void 0,
461
+ checkpoints: [],
462
+ createdAt: now,
463
+ updatedAt: now
464
+ };
465
+ const stateMachine = new ChannelStateMachine(channel);
466
+ const fundingRequired = this.calculateFundingRequirements(channel);
467
+ return {
468
+ channel: stateMachine.getChannel(),
469
+ stateMachine,
470
+ fundingRequired
471
+ };
472
+ }
473
+ /**
474
+ * Process funding transaction
475
+ */
476
+ processFunding(stateMachine, txHash, amount) {
477
+ const channel = stateMachine.getChannel();
478
+ if (BigInt(amount) < BigInt(channel.config.minDeposit)) {
479
+ return { success: false, error: "Funding amount below minimum deposit" };
480
+ }
481
+ if (channel.config.maxDeposit && BigInt(amount) > BigInt(channel.config.maxDeposit)) {
482
+ return { success: false, error: "Funding amount exceeds maximum deposit" };
483
+ }
484
+ const result = stateMachine.process({
485
+ type: "FUND",
486
+ txHash,
487
+ amount
488
+ });
489
+ if (!result.success) {
490
+ return result;
491
+ }
492
+ stateMachine.updateBalance(amount, "0");
493
+ return { success: true };
494
+ }
495
+ /**
496
+ * Confirm funding (after required confirmations)
497
+ */
498
+ confirmFunding(stateMachine, _confirmation) {
499
+ return stateMachine.process({ type: "CONFIRM_FUNDING" });
500
+ }
501
+ /**
502
+ * Handle funding timeout
503
+ */
504
+ handleTimeout(stateMachine) {
505
+ const channel = stateMachine.getChannel();
506
+ if (channel.state !== "funding") {
507
+ return { success: false, expired: false };
508
+ }
509
+ const elapsed = Date.now() - channel.createdAt;
510
+ if (elapsed < this.config.fundingTimeout) {
511
+ return { success: false, expired: false };
512
+ }
513
+ const result = stateMachine.process({ type: "TIMEOUT" });
514
+ return { success: result.success, expired: result.success };
515
+ }
516
+ /**
517
+ * Validate channel create request
518
+ */
519
+ validateCreateRequest(request) {
520
+ ChannelCreateRequest.parse(request);
521
+ if (request.payerAddress === request.payeeAddress) {
522
+ throw new Error("Payer and payee must be different addresses");
523
+ }
524
+ if (BigInt(request.depositAmount) <= 0n) {
525
+ throw new Error("Deposit amount must be positive");
526
+ }
527
+ if (BigInt(request.ratePerSecond) <= 0n) {
528
+ throw new Error("Rate per second must be positive");
529
+ }
530
+ const depositBigInt = BigInt(request.depositAmount);
531
+ const rateBigInt = BigInt(request.ratePerSecond);
532
+ const minDuration = depositBigInt / rateBigInt;
533
+ if (minDuration < 60n) {
534
+ throw new Error("Channel would be exhausted in less than 60 seconds");
535
+ }
536
+ }
537
+ /**
538
+ * Generate unique channel ID
539
+ */
540
+ generateChannelId(request, timestamp) {
541
+ const data = `${request.chain}:${request.asset}:${request.payerAddress}:${request.payeeAddress}:${timestamp}`;
542
+ let hash = 0;
543
+ for (let i = 0; i < data.length; i++) {
544
+ const char = data.charCodeAt(i);
545
+ hash = (hash << 5) - hash + char;
546
+ hash = hash & hash;
547
+ }
548
+ return `ch_${Math.abs(hash).toString(16).padStart(16, "0")}`;
549
+ }
550
+ /**
551
+ * Calculate funding requirements for channel
552
+ */
553
+ calculateFundingRequirements(channel) {
554
+ const totalRequired = BigInt(channel.balance.total) + BigInt(channel.config.channelFee);
555
+ return {
556
+ amount: totalRequired.toString(),
557
+ to: channel.contractAddress ?? channel.payee.address,
558
+ data: this.encodeFundingData(channel)
559
+ };
560
+ }
561
+ /**
562
+ * Encode funding transaction data
563
+ */
564
+ encodeFundingData(channel) {
565
+ const methodId = "0x" + "initChannel".split("").map((c) => c.charCodeAt(0).toString(16)).join("").slice(0, 8);
566
+ return `${methodId}${channel.id.replace("ch_", "").padStart(64, "0")}`;
567
+ }
568
+ };
569
+ function estimateChannelDuration(depositAmount, ratePerSecond) {
570
+ const deposit = BigInt(depositAmount);
571
+ const rate = BigInt(ratePerSecond);
572
+ if (rate === 0n) {
573
+ return { seconds: Infinity, formatted: "infinite" };
574
+ }
575
+ const seconds = Number(deposit / rate);
576
+ if (seconds < 60) {
577
+ return { seconds, formatted: `${seconds} seconds` };
578
+ } else if (seconds < 3600) {
579
+ const minutes = Math.floor(seconds / 60);
580
+ return { seconds, formatted: `${minutes} minutes` };
581
+ } else if (seconds < 86400) {
582
+ const hours = Math.floor(seconds / 3600);
583
+ return { seconds, formatted: `${hours} hours` };
584
+ } else {
585
+ const days = Math.floor(seconds / 86400);
586
+ return { seconds, formatted: `${days} days` };
587
+ }
588
+ }
589
+ function calculateRequiredDeposit(ratePerSecond, durationSeconds) {
590
+ const rate = BigInt(ratePerSecond);
591
+ const duration = BigInt(durationSeconds);
592
+ return (rate * duration).toString();
593
+ }
594
+
595
+ // src/channels/closing.ts
596
+ var ChannelCloser = class {
597
+ config;
598
+ constructor(config = {}) {
599
+ this.config = {
600
+ challengePeriod: config.challengePeriod ?? 86400,
601
+ // 24 hours
602
+ gracePeriod: config.gracePeriod ?? 3600,
603
+ // 1 hour
604
+ autoFinalize: config.autoFinalize ?? true
605
+ };
606
+ }
607
+ /**
608
+ * Initiate channel close
609
+ */
610
+ initiateClose(stateMachine, initiator, signature) {
611
+ const channel = stateMachine.getChannel();
612
+ if (initiator !== channel.payer.address && initiator !== channel.payee.address) {
613
+ return { success: false, error: "Initiator must be a channel participant" };
614
+ }
615
+ const currentStreamed = stateMachine.getCurrentStreamedAmount();
616
+ const finalBalance = this.calculateFinalBalance(channel, currentStreamed);
617
+ const finalCheckpoint = this.createFinalCheckpoint(channel, finalBalance, signature);
618
+ const result = stateMachine.process({
619
+ type: "INITIATE_CLOSE",
620
+ initiator
621
+ });
622
+ if (!result.success) {
623
+ return { success: false, error: result.error };
624
+ }
625
+ stateMachine.addCheckpoint(finalCheckpoint);
626
+ const challengeDeadline = Date.now() + this.config.challengePeriod * 1e3;
627
+ return {
628
+ success: true,
629
+ challengeDeadline,
630
+ finalCheckpoint,
631
+ settlementAmount: {
632
+ payer: finalBalance.payer,
633
+ payee: finalBalance.payee
634
+ }
635
+ };
636
+ }
637
+ /**
638
+ * Complete mutual close (both parties agree)
639
+ */
640
+ mutualClose(stateMachine, payerSignature, payeeSignature) {
641
+ const channel = stateMachine.getChannel();
642
+ const currentStreamed = stateMachine.getCurrentStreamedAmount();
643
+ const finalBalance = this.calculateFinalBalance(channel, currentStreamed);
644
+ const finalCheckpoint = {
645
+ channelId: channel.id,
646
+ sequence: channel.checkpoints.length,
647
+ timestamp: Date.now(),
648
+ balance: finalBalance,
649
+ amountStreamed: currentStreamed,
650
+ payerSignature,
651
+ payeeSignature
652
+ };
653
+ let result = stateMachine.process({
654
+ type: "INITIATE_CLOSE",
655
+ initiator: channel.payer.address
656
+ });
657
+ if (!result.success) {
658
+ return { success: false, error: result.error };
659
+ }
660
+ result = stateMachine.process({
661
+ type: "MUTUAL_CLOSE",
662
+ signature: `${payerSignature}:${payeeSignature}`
663
+ });
664
+ if (!result.success) {
665
+ return { success: false, error: result.error };
666
+ }
667
+ stateMachine.addCheckpoint(finalCheckpoint);
668
+ return {
669
+ success: true,
670
+ finalCheckpoint,
671
+ settlementAmount: {
672
+ payer: finalBalance.payer,
673
+ payee: finalBalance.payee
674
+ }
675
+ };
676
+ }
677
+ /**
678
+ * Finalize channel after challenge period
679
+ */
680
+ finalize(stateMachine) {
681
+ const channel = stateMachine.getChannel();
682
+ if (channel.state !== "closing") {
683
+ return {
684
+ success: false,
685
+ error: "Channel must be in closing state",
686
+ finalBalance: channel.balance
687
+ };
688
+ }
689
+ const latestCheckpoint = channel.latestCheckpoint;
690
+ if (latestCheckpoint) {
691
+ const elapsed = Date.now() - latestCheckpoint.timestamp;
692
+ if (elapsed < this.config.challengePeriod * 1e3) {
693
+ return {
694
+ success: false,
695
+ error: "Challenge period not yet elapsed",
696
+ finalBalance: channel.balance
697
+ };
698
+ }
699
+ }
700
+ const result = stateMachine.process({ type: "FINALIZE" });
701
+ if (!result.success) {
702
+ return {
703
+ success: false,
704
+ error: result.error,
705
+ finalBalance: channel.balance
706
+ };
707
+ }
708
+ const finalChannel = stateMachine.getChannel();
709
+ return {
710
+ success: true,
711
+ finalBalance: finalChannel.balance
712
+ };
713
+ }
714
+ /**
715
+ * Validate close request
716
+ */
717
+ validateCloseRequest(channel, request) {
718
+ const errors = [];
719
+ if (request.channelId !== channel.id) {
720
+ errors.push("Channel ID mismatch");
721
+ }
722
+ if (request.initiator !== channel.payer.address && request.initiator !== channel.payee.address) {
723
+ errors.push("Initiator is not a channel participant");
724
+ }
725
+ if (channel.state !== "open" && channel.state !== "paused") {
726
+ errors.push(`Cannot close channel in ${channel.state} state`);
727
+ }
728
+ if (request.finalCheckpoint) {
729
+ const cpErrors = this.validateCheckpoint(channel, request.finalCheckpoint);
730
+ errors.push(...cpErrors);
731
+ }
732
+ return { valid: errors.length === 0, errors };
733
+ }
734
+ /**
735
+ * Get settlement amounts for on-chain execution
736
+ */
737
+ getSettlementAmounts(channel) {
738
+ const finalBalance = channel.latestCheckpoint?.balance ?? channel.balance;
739
+ const fee = channel.config.settlementFee;
740
+ const payerAmount = BigInt(finalBalance.payer) - BigInt(fee);
741
+ return {
742
+ payer: payerAmount > 0n ? payerAmount.toString() : "0",
743
+ payee: finalBalance.payee,
744
+ fee
745
+ };
746
+ }
747
+ /**
748
+ * Calculate time until channel can be finalized
749
+ */
750
+ getTimeUntilFinalization(channel) {
751
+ if (channel.state !== "closing") {
752
+ return -1;
753
+ }
754
+ const latestCheckpoint = channel.latestCheckpoint;
755
+ if (!latestCheckpoint) {
756
+ return 0;
757
+ }
758
+ const elapsed = Date.now() - latestCheckpoint.timestamp;
759
+ const remaining = this.config.challengePeriod * 1e3 - elapsed;
760
+ return Math.max(0, remaining);
761
+ }
762
+ /**
763
+ * Check if channel close can be challenged
764
+ */
765
+ canChallenge(channel) {
766
+ if (channel.state !== "closing") {
767
+ return false;
768
+ }
769
+ return this.getTimeUntilFinalization(channel) > 0;
770
+ }
771
+ calculateFinalBalance(channel, amountStreamed) {
772
+ const total = BigInt(channel.balance.total);
773
+ const streamed = BigInt(amountStreamed);
774
+ const remaining = total - streamed;
775
+ return {
776
+ payer: remaining.toString(),
777
+ payee: streamed.toString(),
778
+ total: total.toString(),
779
+ locked: "0"
780
+ };
781
+ }
782
+ createFinalCheckpoint(channel, balance, signature) {
783
+ return {
784
+ channelId: channel.id,
785
+ sequence: channel.checkpoints.length,
786
+ timestamp: Date.now(),
787
+ balance,
788
+ amountStreamed: balance.payee,
789
+ payerSignature: signature
790
+ };
791
+ }
792
+ validateCheckpoint(channel, checkpoint) {
793
+ const errors = [];
794
+ const expectedSequence = channel.checkpoints.length;
795
+ if (checkpoint.sequence !== expectedSequence) {
796
+ errors.push(`Invalid sequence: expected ${expectedSequence}, got ${checkpoint.sequence}`);
797
+ }
798
+ const totalBalance = BigInt(checkpoint.balance.payer) + BigInt(checkpoint.balance.payee);
799
+ if (totalBalance !== BigInt(channel.balance.total)) {
800
+ errors.push("Balance inconsistency: payer + payee does not equal total");
801
+ }
802
+ if (checkpoint.timestamp > Date.now()) {
803
+ errors.push("Checkpoint timestamp is in the future");
804
+ }
805
+ return errors;
806
+ }
807
+ };
808
+ function buildCloseTransactionData(channel, checkpoint) {
809
+ const methodId = "0x" + "closeChannel".split("").map((c) => c.charCodeAt(0).toString(16)).join("").slice(0, 8);
810
+ const data = [
811
+ methodId,
812
+ channel.id.replace("ch_", "").padStart(64, "0"),
813
+ checkpoint.sequence.toString(16).padStart(64, "0"),
814
+ checkpoint.payerSignature.replace("0x", "").padStart(128, "0")
815
+ ].join("");
816
+ return {
817
+ to: channel.contractAddress ?? channel.payee.address,
818
+ data,
819
+ value: "0"
820
+ };
821
+ }
822
+
823
+ // src/channels/recovery.ts
824
+ var ChannelRecovery = class {
825
+ config;
826
+ storage;
827
+ constructor(config = {}, storage) {
828
+ this.config = {
829
+ maxCheckpointsToKeep: config.maxCheckpointsToKeep ?? 100,
830
+ verifySignatures: config.verifySignatures ?? true,
831
+ onChainFallback: config.onChainFallback ?? true
832
+ };
833
+ this.storage = storage;
834
+ }
835
+ /**
836
+ * Recover channel from checkpoints
837
+ */
838
+ recoverFromCheckpoints(baseChannel, checkpoints) {
839
+ if (checkpoints.length === 0) {
840
+ return {
841
+ success: true,
842
+ channel: baseChannel,
843
+ stateMachine: new ChannelStateMachine(baseChannel),
844
+ dataLoss: false
845
+ };
846
+ }
847
+ const sortedCheckpoints = [...checkpoints].sort((a, b) => a.sequence - b.sequence);
848
+ let latestValid;
849
+ for (let i = sortedCheckpoints.length - 1; i >= 0; i--) {
850
+ const checkpoint = sortedCheckpoints[i];
851
+ if (this.isCheckpointValid(baseChannel, checkpoint)) {
852
+ latestValid = checkpoint;
853
+ break;
854
+ }
855
+ }
856
+ if (!latestValid) {
857
+ return {
858
+ success: false,
859
+ error: "No valid checkpoints found",
860
+ dataLoss: true
861
+ };
862
+ }
863
+ const recoveredChannel = this.reconstructFromCheckpoint(baseChannel, latestValid, sortedCheckpoints);
864
+ return {
865
+ success: true,
866
+ channel: recoveredChannel,
867
+ stateMachine: new ChannelStateMachine(recoveredChannel),
868
+ recoveredFromCheckpoint: latestValid,
869
+ dataLoss: sortedCheckpoints[sortedCheckpoints.length - 1].sequence > latestValid.sequence
870
+ };
871
+ }
872
+ /**
873
+ * Full recovery from storage
874
+ */
875
+ async recoverFromStorage(channelId) {
876
+ if (!this.storage) {
877
+ return {
878
+ success: false,
879
+ error: "No storage provider configured"
880
+ };
881
+ }
882
+ try {
883
+ const [channel, checkpoints, transactions, disputes] = await Promise.all([
884
+ this.storage.getChannel(channelId),
885
+ this.storage.getCheckpoints(channelId),
886
+ this.storage.getTransactions(channelId),
887
+ this.storage.getDisputes(channelId)
888
+ ]);
889
+ if (!channel) {
890
+ return {
891
+ success: false,
892
+ error: "Channel not found in storage"
893
+ };
894
+ }
895
+ const result = this.recoverFromCheckpoints(channel, checkpoints);
896
+ if (!result.success) {
897
+ if (this.config.onChainFallback) {
898
+ return this.recoverFromOnChain(channel, transactions, disputes);
899
+ }
900
+ return result;
901
+ }
902
+ return result;
903
+ } catch (error) {
904
+ return {
905
+ success: false,
906
+ error: `Storage recovery failed: ${error instanceof Error ? error.message : "Unknown error"}`
907
+ };
908
+ }
909
+ }
910
+ /**
911
+ * Export recovery data for backup
912
+ */
913
+ exportRecoveryData(channel, stateMachine) {
914
+ return {
915
+ channelId: channel.id,
916
+ checkpoints: channel.checkpoints,
917
+ transactions: [],
918
+ // Would be populated from transaction history
919
+ disputes: [],
920
+ // Would be populated from dispute history
921
+ stateHistory: stateMachine.getTransitions(),
922
+ recoveredAt: Date.now()
923
+ };
924
+ }
925
+ /**
926
+ * Import recovery data
927
+ */
928
+ importRecoveryData(data) {
929
+ if (!data.channelId || !data.checkpoints) {
930
+ return {
931
+ success: false,
932
+ error: "Invalid recovery data format"
933
+ };
934
+ }
935
+ if (data.checkpoints.length === 0) {
936
+ return {
937
+ success: false,
938
+ error: "No checkpoints in recovery data"
939
+ };
940
+ }
941
+ const baseChannel = this.createBaseChannelFromRecoveryData(data);
942
+ return this.recoverFromCheckpoints(baseChannel, data.checkpoints);
943
+ }
944
+ /**
945
+ * Verify checkpoint integrity
946
+ */
947
+ verifyCheckpointChain(checkpoints) {
948
+ const errors = [];
949
+ if (checkpoints.length === 0) {
950
+ return { valid: true, errors: [] };
951
+ }
952
+ const sorted = [...checkpoints].sort((a, b) => a.sequence - b.sequence);
953
+ for (let i = 0; i < sorted.length; i++) {
954
+ const current = sorted[i];
955
+ if (current.sequence !== i) {
956
+ errors.push(`Sequence gap: expected ${i}, got ${current.sequence}`);
957
+ }
958
+ if (i > 0 && current.timestamp < sorted[i - 1].timestamp) {
959
+ errors.push(`Timestamp ordering violation at sequence ${current.sequence}`);
960
+ }
961
+ if (i > 0) {
962
+ const prevStreamed = BigInt(sorted[i - 1].amountStreamed);
963
+ const currStreamed = BigInt(current.amountStreamed);
964
+ if (currStreamed < prevStreamed) {
965
+ errors.push(`Amount decreased at sequence ${current.sequence}`);
966
+ }
967
+ }
968
+ }
969
+ return { valid: errors.length === 0, errors };
970
+ }
971
+ /**
972
+ * Prune old checkpoints keeping only recent ones
973
+ */
974
+ pruneCheckpoints(checkpoints) {
975
+ if (checkpoints.length <= this.config.maxCheckpointsToKeep) {
976
+ return checkpoints;
977
+ }
978
+ const sorted = [...checkpoints].sort((a, b) => a.sequence - b.sequence);
979
+ const genesis = sorted[0];
980
+ const recent = sorted.slice(-(this.config.maxCheckpointsToKeep - 1));
981
+ return [genesis, ...recent];
982
+ }
983
+ isCheckpointValid(channel, checkpoint) {
984
+ if (checkpoint.channelId !== channel.id) {
985
+ return false;
986
+ }
987
+ const total = BigInt(checkpoint.balance.payer) + BigInt(checkpoint.balance.payee);
988
+ if (total !== BigInt(channel.balance.total)) {
989
+ return false;
990
+ }
991
+ if (!checkpoint.payerSignature) {
992
+ return false;
993
+ }
994
+ if (this.config.verifySignatures) {
995
+ }
996
+ return true;
997
+ }
998
+ reconstructFromCheckpoint(baseChannel, checkpoint, allCheckpoints) {
999
+ const validCheckpoints = allCheckpoints.filter((cp) => cp.sequence <= checkpoint.sequence);
1000
+ let state = "open";
1001
+ if (checkpoint.payeeSignature) {
1002
+ state = "open";
1003
+ }
1004
+ return {
1005
+ ...baseChannel,
1006
+ state,
1007
+ balance: checkpoint.balance,
1008
+ checkpoints: validCheckpoints,
1009
+ latestCheckpoint: checkpoint,
1010
+ updatedAt: checkpoint.timestamp
1011
+ };
1012
+ }
1013
+ recoverFromOnChain(channel, transactions, disputes) {
1014
+ const confirmedTxs = transactions.filter((tx) => tx.confirmed);
1015
+ if (confirmedTxs.length === 0) {
1016
+ return {
1017
+ success: false,
1018
+ error: "No confirmed transactions found",
1019
+ dataLoss: true
1020
+ };
1021
+ }
1022
+ const totalFunded = confirmedTxs.reduce(
1023
+ (sum, tx) => sum + BigInt(tx.amount),
1024
+ 0n
1025
+ );
1026
+ const activeDispute = disputes.find((d) => d.status === "pending");
1027
+ let state = "open";
1028
+ if (activeDispute) {
1029
+ state = "disputing";
1030
+ }
1031
+ const recoveredChannel = {
1032
+ ...channel,
1033
+ state,
1034
+ balance: {
1035
+ ...channel.balance,
1036
+ total: totalFunded.toString(),
1037
+ payer: totalFunded.toString(),
1038
+ payee: "0"
1039
+ },
1040
+ fundingTxHash: confirmedTxs[0].txHash,
1041
+ checkpoints: [],
1042
+ updatedAt: Date.now()
1043
+ };
1044
+ return {
1045
+ success: true,
1046
+ channel: recoveredChannel,
1047
+ stateMachine: new ChannelStateMachine(recoveredChannel),
1048
+ dataLoss: true
1049
+ };
1050
+ }
1051
+ createBaseChannelFromRecoveryData(data) {
1052
+ const firstCheckpoint = data.checkpoints[0];
1053
+ return {
1054
+ id: data.channelId,
1055
+ state: "open",
1056
+ chain: "",
1057
+ asset: "",
1058
+ payer: { address: "", role: "payer" },
1059
+ payee: { address: "", role: "payee" },
1060
+ balance: firstCheckpoint.balance,
1061
+ ratePerSecond: "0",
1062
+ config: {
1063
+ minDeposit: "0",
1064
+ challengePeriod: 86400,
1065
+ checkpointInterval: 3600,
1066
+ minCheckpointAmount: "0",
1067
+ channelFee: "0",
1068
+ settlementFee: "0"
1069
+ },
1070
+ checkpoints: [],
1071
+ createdAt: firstCheckpoint.timestamp,
1072
+ updatedAt: firstCheckpoint.timestamp
1073
+ };
1074
+ }
1075
+ };
1076
+ function createStateSnapshot(_channel, stateMachine) {
1077
+ const snapshot = {
1078
+ version: 1,
1079
+ channel: stateMachine.getChannel(),
1080
+ transitions: stateMachine.getTransitions(),
1081
+ timestamp: Date.now()
1082
+ };
1083
+ return JSON.stringify(snapshot);
1084
+ }
1085
+ function restoreFromSnapshot(snapshotJson) {
1086
+ try {
1087
+ const snapshot = JSON.parse(snapshotJson);
1088
+ if (snapshot.version !== 1) {
1089
+ return {
1090
+ success: false,
1091
+ error: "Unsupported snapshot version"
1092
+ };
1093
+ }
1094
+ const channel = snapshot.channel;
1095
+ const stateMachine = new ChannelStateMachine(channel);
1096
+ return {
1097
+ success: true,
1098
+ channel,
1099
+ stateMachine,
1100
+ dataLoss: false
1101
+ };
1102
+ } catch (error) {
1103
+ return {
1104
+ success: false,
1105
+ error: `Failed to parse snapshot: ${error instanceof Error ? error.message : "Unknown error"}`
1106
+ };
1107
+ }
1108
+ }
1109
+ export {
1110
+ ChannelBalance,
1111
+ ChannelCheckpoint,
1112
+ ChannelCloseRequest,
1113
+ ChannelCloser,
1114
+ ChannelConfig,
1115
+ ChannelCreateRequest,
1116
+ ChannelDispute,
1117
+ ChannelOpener,
1118
+ ChannelParticipant,
1119
+ ChannelRecovery,
1120
+ ChannelState,
1121
+ ChannelStateMachine,
1122
+ FundingTransaction,
1123
+ RecoveryData,
1124
+ StateTransition,
1125
+ StreamingChannel,
1126
+ buildCloseTransactionData,
1127
+ calculateRequiredDeposit,
1128
+ createStateSnapshot,
1129
+ estimateChannelDuration,
1130
+ getChannelUtilization,
1131
+ isChannelActive,
1132
+ isChannelTerminal,
1133
+ restoreFromSnapshot
1134
+ };
1135
+ //# sourceMappingURL=index.js.map