@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.
package/dist/index.js ADDED
@@ -0,0 +1,3482 @@
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
+
1110
+ // src/streaming/types.ts
1111
+ import { z as z2 } from "zod";
1112
+ var StreamState = z2.enum([
1113
+ "idle",
1114
+ // Stream not started
1115
+ "active",
1116
+ // Streaming in progress
1117
+ "paused",
1118
+ // Temporarily paused
1119
+ "completed",
1120
+ // Stream completed (exhausted)
1121
+ "cancelled"
1122
+ // Stream cancelled
1123
+ ]);
1124
+ var RateType = z2.enum([
1125
+ "fixed",
1126
+ // Fixed rate per second
1127
+ "variable",
1128
+ // Rate can change
1129
+ "tiered",
1130
+ // Different rates based on usage
1131
+ "dynamic"
1132
+ // Demand-based pricing
1133
+ ]);
1134
+ var StreamRate = z2.object({
1135
+ type: RateType,
1136
+ baseRate: z2.string(),
1137
+ // Base rate per second
1138
+ minRate: z2.string().optional(),
1139
+ // Minimum rate
1140
+ maxRate: z2.string().optional(),
1141
+ // Maximum rate
1142
+ // For tiered rates
1143
+ tiers: z2.array(z2.object({
1144
+ threshold: z2.string(),
1145
+ // Usage threshold
1146
+ rate: z2.string()
1147
+ // Rate after threshold
1148
+ })).optional(),
1149
+ // For dynamic rates
1150
+ adjustmentInterval: z2.number().optional(),
1151
+ // How often to adjust (seconds)
1152
+ adjustmentFactor: z2.number().optional()
1153
+ // Max adjustment per interval
1154
+ });
1155
+ var UsageMetrics = z2.object({
1156
+ totalSeconds: z2.number(),
1157
+ totalAmount: z2.string(),
1158
+ averageRate: z2.string(),
1159
+ peakRate: z2.string(),
1160
+ startTime: z2.number(),
1161
+ endTime: z2.number().optional(),
1162
+ // Breakdown by period
1163
+ hourly: z2.array(z2.object({
1164
+ hour: z2.number(),
1165
+ amount: z2.string(),
1166
+ seconds: z2.number()
1167
+ })).optional()
1168
+ });
1169
+ var MeteringRecord = z2.object({
1170
+ timestamp: z2.number(),
1171
+ duration: z2.number(),
1172
+ // Seconds since last record
1173
+ amount: z2.string(),
1174
+ // Amount for this period
1175
+ rate: z2.string(),
1176
+ // Rate applied
1177
+ cumulative: z2.string(),
1178
+ // Cumulative total
1179
+ metadata: z2.record(z2.unknown()).optional()
1180
+ });
1181
+ var BillingPeriod = z2.enum([
1182
+ "realtime",
1183
+ // Continuous real-time billing
1184
+ "second",
1185
+ // Per-second
1186
+ "minute",
1187
+ // Per-minute
1188
+ "hour",
1189
+ // Per-hour
1190
+ "day"
1191
+ // Per-day
1192
+ ]);
1193
+ var BillingConfig = z2.object({
1194
+ period: BillingPeriod,
1195
+ minimumCharge: z2.string().default("0"),
1196
+ roundingMode: z2.enum(["floor", "ceil", "round"]).default("floor"),
1197
+ gracePeriod: z2.number().default(0),
1198
+ // Seconds of free usage
1199
+ invoiceInterval: z2.number().optional()
1200
+ // Generate invoice every N seconds
1201
+ });
1202
+ var InvoiceItem = z2.object({
1203
+ description: z2.string(),
1204
+ quantity: z2.number(),
1205
+ // Duration in billing periods
1206
+ rate: z2.string(),
1207
+ amount: z2.string(),
1208
+ startTime: z2.number(),
1209
+ endTime: z2.number()
1210
+ });
1211
+ var Invoice = z2.object({
1212
+ id: z2.string(),
1213
+ channelId: z2.string(),
1214
+ payer: z2.string(),
1215
+ payee: z2.string(),
1216
+ items: z2.array(InvoiceItem),
1217
+ subtotal: z2.string(),
1218
+ fees: z2.string(),
1219
+ total: z2.string(),
1220
+ currency: z2.string(),
1221
+ status: z2.enum(["pending", "paid", "settled", "disputed"]),
1222
+ createdAt: z2.number(),
1223
+ dueAt: z2.number().optional(),
1224
+ paidAt: z2.number().optional()
1225
+ });
1226
+ var StreamSession = z2.object({
1227
+ id: z2.string(),
1228
+ channelId: z2.string(),
1229
+ state: StreamState,
1230
+ rate: StreamRate,
1231
+ startedAt: z2.number().optional(),
1232
+ pausedAt: z2.number().optional(),
1233
+ endedAt: z2.number().optional(),
1234
+ totalDuration: z2.number(),
1235
+ // Total streaming seconds
1236
+ totalAmount: z2.string(),
1237
+ // Total amount streamed
1238
+ meteringRecords: z2.array(MeteringRecord),
1239
+ billingConfig: BillingConfig,
1240
+ invoices: z2.array(z2.string())
1241
+ // Invoice IDs
1242
+ });
1243
+ var RateAdjustmentRequest = z2.object({
1244
+ sessionId: z2.string(),
1245
+ newRate: z2.string(),
1246
+ reason: z2.string(),
1247
+ effectiveFrom: z2.number().optional(),
1248
+ // Timestamp, default now
1249
+ signature: z2.string().optional()
1250
+ // For mutual rate changes
1251
+ });
1252
+ var StreamEvent = z2.discriminatedUnion("type", [
1253
+ z2.object({
1254
+ type: z2.literal("started"),
1255
+ sessionId: z2.string(),
1256
+ timestamp: z2.number(),
1257
+ rate: z2.string()
1258
+ }),
1259
+ z2.object({
1260
+ type: z2.literal("paused"),
1261
+ sessionId: z2.string(),
1262
+ timestamp: z2.number(),
1263
+ totalStreamed: z2.string()
1264
+ }),
1265
+ z2.object({
1266
+ type: z2.literal("resumed"),
1267
+ sessionId: z2.string(),
1268
+ timestamp: z2.number()
1269
+ }),
1270
+ z2.object({
1271
+ type: z2.literal("rate_changed"),
1272
+ sessionId: z2.string(),
1273
+ timestamp: z2.number(),
1274
+ oldRate: z2.string(),
1275
+ newRate: z2.string()
1276
+ }),
1277
+ z2.object({
1278
+ type: z2.literal("checkpoint"),
1279
+ sessionId: z2.string(),
1280
+ timestamp: z2.number(),
1281
+ amount: z2.string(),
1282
+ checkpointId: z2.string()
1283
+ }),
1284
+ z2.object({
1285
+ type: z2.literal("completed"),
1286
+ sessionId: z2.string(),
1287
+ timestamp: z2.number(),
1288
+ totalAmount: z2.string(),
1289
+ totalDuration: z2.number()
1290
+ }),
1291
+ z2.object({
1292
+ type: z2.literal("cancelled"),
1293
+ sessionId: z2.string(),
1294
+ timestamp: z2.number(),
1295
+ reason: z2.string()
1296
+ })
1297
+ ]);
1298
+
1299
+ // src/streaming/flow.ts
1300
+ var FlowController = class {
1301
+ session;
1302
+ config;
1303
+ updateTimer;
1304
+ checkpointTimer;
1305
+ eventListeners = /* @__PURE__ */ new Set();
1306
+ lastUpdateTime = 0;
1307
+ constructor(channelId, rate, billingConfig, config = {}) {
1308
+ this.config = {
1309
+ updateInterval: config.updateInterval ?? 1e3,
1310
+ bufferTime: config.bufferTime ?? 60,
1311
+ autoCheckpoint: config.autoCheckpoint ?? true,
1312
+ checkpointInterval: config.checkpointInterval ?? 3600
1313
+ };
1314
+ this.session = {
1315
+ id: this.generateSessionId(),
1316
+ channelId,
1317
+ state: "idle",
1318
+ rate,
1319
+ totalDuration: 0,
1320
+ totalAmount: "0",
1321
+ meteringRecords: [],
1322
+ billingConfig,
1323
+ invoices: []
1324
+ };
1325
+ }
1326
+ /**
1327
+ * Get current session state
1328
+ */
1329
+ getSession() {
1330
+ return { ...this.session };
1331
+ }
1332
+ /**
1333
+ * Get current state
1334
+ */
1335
+ getState() {
1336
+ return this.session.state;
1337
+ }
1338
+ /**
1339
+ * Start streaming
1340
+ */
1341
+ start() {
1342
+ if (this.session.state !== "idle" && this.session.state !== "paused") {
1343
+ return { success: false, error: "Stream must be idle or paused to start" };
1344
+ }
1345
+ const now = Date.now();
1346
+ if (this.session.state === "idle") {
1347
+ this.session.startedAt = now;
1348
+ } else {
1349
+ const pauseDuration = now - (this.session.pausedAt ?? now);
1350
+ this.session.pausedAt = void 0;
1351
+ this.addMeteringRecord({
1352
+ timestamp: now,
1353
+ duration: 0,
1354
+ amount: "0",
1355
+ rate: "0",
1356
+ cumulative: this.session.totalAmount,
1357
+ metadata: { event: "resume", pauseDuration }
1358
+ });
1359
+ }
1360
+ this.session.state = "active";
1361
+ this.lastUpdateTime = now;
1362
+ this.startUpdateTimer();
1363
+ if (this.config.autoCheckpoint) {
1364
+ this.startCheckpointTimer();
1365
+ }
1366
+ this.emitEvent({
1367
+ type: "started",
1368
+ sessionId: this.session.id,
1369
+ timestamp: now,
1370
+ rate: this.session.rate.baseRate
1371
+ });
1372
+ return { success: true };
1373
+ }
1374
+ /**
1375
+ * Pause streaming
1376
+ */
1377
+ pause() {
1378
+ if (this.session.state !== "active") {
1379
+ return { success: false, error: "Stream must be active to pause" };
1380
+ }
1381
+ const now = Date.now();
1382
+ this.updateTotals(now);
1383
+ this.session.state = "paused";
1384
+ this.session.pausedAt = now;
1385
+ this.stopTimers();
1386
+ this.emitEvent({
1387
+ type: "paused",
1388
+ sessionId: this.session.id,
1389
+ timestamp: now,
1390
+ totalStreamed: this.session.totalAmount
1391
+ });
1392
+ return { success: true };
1393
+ }
1394
+ /**
1395
+ * Resume streaming (alias for start when paused)
1396
+ */
1397
+ resume() {
1398
+ if (this.session.state !== "paused") {
1399
+ return { success: false, error: "Stream must be paused to resume" };
1400
+ }
1401
+ const result = this.start();
1402
+ if (result.success) {
1403
+ this.emitEvent({
1404
+ type: "resumed",
1405
+ sessionId: this.session.id,
1406
+ timestamp: Date.now()
1407
+ });
1408
+ }
1409
+ return result;
1410
+ }
1411
+ /**
1412
+ * Stop streaming (complete)
1413
+ */
1414
+ stop() {
1415
+ const now = Date.now();
1416
+ if (this.session.state === "active") {
1417
+ this.updateTotals(now);
1418
+ }
1419
+ this.stopTimers();
1420
+ this.session.state = "completed";
1421
+ this.session.endedAt = now;
1422
+ this.emitEvent({
1423
+ type: "completed",
1424
+ sessionId: this.session.id,
1425
+ timestamp: now,
1426
+ totalAmount: this.session.totalAmount,
1427
+ totalDuration: this.session.totalDuration
1428
+ });
1429
+ return {
1430
+ success: true,
1431
+ finalAmount: this.session.totalAmount
1432
+ };
1433
+ }
1434
+ /**
1435
+ * Cancel streaming
1436
+ */
1437
+ cancel(reason) {
1438
+ const now = Date.now();
1439
+ if (this.session.state === "active") {
1440
+ this.updateTotals(now);
1441
+ }
1442
+ this.stopTimers();
1443
+ this.session.state = "cancelled";
1444
+ this.session.endedAt = now;
1445
+ this.emitEvent({
1446
+ type: "cancelled",
1447
+ sessionId: this.session.id,
1448
+ timestamp: now,
1449
+ reason
1450
+ });
1451
+ return { success: true };
1452
+ }
1453
+ /**
1454
+ * Get current streamed amount
1455
+ */
1456
+ getCurrentAmount() {
1457
+ if (this.session.state !== "active") {
1458
+ return this.session.totalAmount;
1459
+ }
1460
+ const now = Date.now();
1461
+ const elapsed = Math.floor((now - this.lastUpdateTime) / 1e3);
1462
+ const additionalAmount = this.calculateAmount(elapsed, this.session.rate);
1463
+ return (BigInt(this.session.totalAmount) + BigInt(additionalAmount)).toString();
1464
+ }
1465
+ /**
1466
+ * Get current rate
1467
+ */
1468
+ getCurrentRate() {
1469
+ return this.getEffectiveRate(this.session.rate, this.session.totalAmount);
1470
+ }
1471
+ /**
1472
+ * Get time until exhaustion (returns -1 if infinite)
1473
+ */
1474
+ getTimeUntilExhaustion(channelCapacity) {
1475
+ if (this.session.state !== "active") {
1476
+ return -1;
1477
+ }
1478
+ const remaining = BigInt(channelCapacity) - BigInt(this.getCurrentAmount());
1479
+ if (remaining <= 0n) {
1480
+ return 0;
1481
+ }
1482
+ const rate = BigInt(this.getCurrentRate());
1483
+ if (rate <= 0n) {
1484
+ return -1;
1485
+ }
1486
+ return Number(remaining / rate);
1487
+ }
1488
+ /**
1489
+ * Check if stream is near exhaustion
1490
+ */
1491
+ isNearExhaustion(channelCapacity) {
1492
+ const remaining = this.getTimeUntilExhaustion(channelCapacity);
1493
+ return remaining >= 0 && remaining <= this.config.bufferTime;
1494
+ }
1495
+ /**
1496
+ * Create manual checkpoint
1497
+ */
1498
+ createCheckpoint() {
1499
+ const now = Date.now();
1500
+ const amount = this.getCurrentAmount();
1501
+ const checkpointId = `cp_${this.session.id}_${now}`;
1502
+ this.emitEvent({
1503
+ type: "checkpoint",
1504
+ sessionId: this.session.id,
1505
+ timestamp: now,
1506
+ amount,
1507
+ checkpointId
1508
+ });
1509
+ return { id: checkpointId, amount, timestamp: now };
1510
+ }
1511
+ /**
1512
+ * Subscribe to stream events
1513
+ */
1514
+ onEvent(callback) {
1515
+ this.eventListeners.add(callback);
1516
+ return () => this.eventListeners.delete(callback);
1517
+ }
1518
+ /**
1519
+ * Clean up resources
1520
+ */
1521
+ destroy() {
1522
+ this.stopTimers();
1523
+ this.eventListeners.clear();
1524
+ }
1525
+ startUpdateTimer() {
1526
+ this.updateTimer = setInterval(() => {
1527
+ if (this.session.state === "active") {
1528
+ this.updateTotals(Date.now());
1529
+ }
1530
+ }, this.config.updateInterval);
1531
+ }
1532
+ startCheckpointTimer() {
1533
+ this.checkpointTimer = setInterval(() => {
1534
+ if (this.session.state === "active") {
1535
+ this.createCheckpoint();
1536
+ }
1537
+ }, this.config.checkpointInterval * 1e3);
1538
+ }
1539
+ stopTimers() {
1540
+ if (this.updateTimer) {
1541
+ clearInterval(this.updateTimer);
1542
+ this.updateTimer = void 0;
1543
+ }
1544
+ if (this.checkpointTimer) {
1545
+ clearInterval(this.checkpointTimer);
1546
+ this.checkpointTimer = void 0;
1547
+ }
1548
+ }
1549
+ updateTotals(now) {
1550
+ const elapsed = Math.floor((now - this.lastUpdateTime) / 1e3);
1551
+ if (elapsed <= 0) return;
1552
+ const amount = this.calculateAmount(elapsed, this.session.rate);
1553
+ const newTotal = BigInt(this.session.totalAmount) + BigInt(amount);
1554
+ this.session.totalDuration += elapsed;
1555
+ this.session.totalAmount = newTotal.toString();
1556
+ this.lastUpdateTime = now;
1557
+ this.addMeteringRecord({
1558
+ timestamp: now,
1559
+ duration: elapsed,
1560
+ amount,
1561
+ rate: this.getCurrentRate(),
1562
+ cumulative: this.session.totalAmount
1563
+ });
1564
+ }
1565
+ calculateAmount(seconds, rate) {
1566
+ const effectiveRate = this.getEffectiveRate(rate, this.session.totalAmount);
1567
+ return (BigInt(effectiveRate) * BigInt(seconds)).toString();
1568
+ }
1569
+ getEffectiveRate(rate, totalAmount) {
1570
+ if (rate.type === "fixed") {
1571
+ return rate.baseRate;
1572
+ }
1573
+ if (rate.type === "tiered" && rate.tiers) {
1574
+ const amount = BigInt(totalAmount);
1575
+ let applicableRate = rate.baseRate;
1576
+ for (const tier of rate.tiers) {
1577
+ if (amount >= BigInt(tier.threshold)) {
1578
+ applicableRate = tier.rate;
1579
+ }
1580
+ }
1581
+ return applicableRate;
1582
+ }
1583
+ return rate.baseRate;
1584
+ }
1585
+ addMeteringRecord(record) {
1586
+ this.session.meteringRecords.push(record);
1587
+ if (this.session.meteringRecords.length > 1e3) {
1588
+ this.session.meteringRecords = this.session.meteringRecords.slice(-1e3);
1589
+ }
1590
+ }
1591
+ emitEvent(event) {
1592
+ this.eventListeners.forEach((callback) => {
1593
+ try {
1594
+ callback(event);
1595
+ } catch {
1596
+ }
1597
+ });
1598
+ }
1599
+ generateSessionId() {
1600
+ return `ss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1601
+ }
1602
+ };
1603
+ function createFixedRateFlow(channelId, ratePerSecond, config) {
1604
+ const rate = {
1605
+ type: "fixed",
1606
+ baseRate: ratePerSecond
1607
+ };
1608
+ const billingConfig = {
1609
+ period: "realtime",
1610
+ minimumCharge: "0",
1611
+ roundingMode: "floor",
1612
+ gracePeriod: 0
1613
+ };
1614
+ return new FlowController(channelId, rate, billingConfig, config);
1615
+ }
1616
+ function createTieredRateFlow(channelId, baseRate, tiers, config) {
1617
+ const rate = {
1618
+ type: "tiered",
1619
+ baseRate,
1620
+ tiers
1621
+ };
1622
+ const billingConfig = {
1623
+ period: "realtime",
1624
+ minimumCharge: "0",
1625
+ roundingMode: "floor",
1626
+ gracePeriod: 0
1627
+ };
1628
+ return new FlowController(channelId, rate, billingConfig, config);
1629
+ }
1630
+
1631
+ // src/streaming/rate.ts
1632
+ var RateController = class {
1633
+ currentRate;
1634
+ config;
1635
+ history = [];
1636
+ lastChangeTime = 0;
1637
+ constructor(initialRate, config = {}) {
1638
+ this.currentRate = { ...initialRate };
1639
+ this.config = {
1640
+ maxChangePercent: config.maxChangePercent ?? 50,
1641
+ minChangeInterval: config.minChangeInterval ?? 60,
1642
+ smoothingFactor: config.smoothingFactor ?? 0.3
1643
+ };
1644
+ }
1645
+ /**
1646
+ * Get current rate configuration
1647
+ */
1648
+ getRate() {
1649
+ return { ...this.currentRate };
1650
+ }
1651
+ /**
1652
+ * Get current effective rate
1653
+ */
1654
+ getEffectiveRate(totalUsage) {
1655
+ if (this.currentRate.type === "fixed") {
1656
+ return this.currentRate.baseRate;
1657
+ }
1658
+ if (this.currentRate.type === "tiered" && this.currentRate.tiers && totalUsage) {
1659
+ return this.calculateTieredRate(totalUsage);
1660
+ }
1661
+ return this.currentRate.baseRate;
1662
+ }
1663
+ /**
1664
+ * Request rate adjustment
1665
+ */
1666
+ adjustRate(request) {
1667
+ const now = Date.now();
1668
+ const timeSinceLastChange = (now - this.lastChangeTime) / 1e3;
1669
+ if (timeSinceLastChange < this.config.minChangeInterval) {
1670
+ return {
1671
+ success: false,
1672
+ error: `Rate can only be changed every ${this.config.minChangeInterval} seconds`
1673
+ };
1674
+ }
1675
+ const newRateBigInt = BigInt(request.newRate);
1676
+ if (newRateBigInt <= 0n) {
1677
+ return { success: false, error: "Rate must be positive" };
1678
+ }
1679
+ if (this.currentRate.minRate && newRateBigInt < BigInt(this.currentRate.minRate)) {
1680
+ return {
1681
+ success: false,
1682
+ error: `Rate cannot be below minimum: ${this.currentRate.minRate}`
1683
+ };
1684
+ }
1685
+ if (this.currentRate.maxRate && newRateBigInt > BigInt(this.currentRate.maxRate)) {
1686
+ return {
1687
+ success: false,
1688
+ error: `Rate cannot exceed maximum: ${this.currentRate.maxRate}`
1689
+ };
1690
+ }
1691
+ const currentRateBigInt = BigInt(this.currentRate.baseRate);
1692
+ const changePercent = this.calculateChangePercent(currentRateBigInt, newRateBigInt);
1693
+ let adjustedRate = request.newRate;
1694
+ if (changePercent > this.config.maxChangePercent) {
1695
+ adjustedRate = this.applyMaxChange(
1696
+ currentRateBigInt,
1697
+ newRateBigInt,
1698
+ this.config.maxChangePercent
1699
+ );
1700
+ }
1701
+ this.history.push({
1702
+ timestamp: now,
1703
+ rate: adjustedRate,
1704
+ reason: request.reason,
1705
+ previousRate: this.currentRate.baseRate
1706
+ });
1707
+ this.currentRate.baseRate = adjustedRate;
1708
+ this.lastChangeTime = now;
1709
+ return {
1710
+ success: true,
1711
+ newRate: adjustedRate,
1712
+ adjustedAmount: adjustedRate !== request.newRate ? adjustedRate : void 0
1713
+ };
1714
+ }
1715
+ /**
1716
+ * Set rate bounds
1717
+ */
1718
+ setBounds(minRate, maxRate) {
1719
+ if (minRate !== void 0) {
1720
+ this.currentRate.minRate = minRate;
1721
+ }
1722
+ if (maxRate !== void 0) {
1723
+ this.currentRate.maxRate = maxRate;
1724
+ }
1725
+ }
1726
+ /**
1727
+ * Add or update a tier
1728
+ */
1729
+ addTier(threshold, rate) {
1730
+ if (this.currentRate.type !== "tiered") {
1731
+ this.currentRate.type = "tiered";
1732
+ this.currentRate.tiers = [];
1733
+ }
1734
+ const tiers = this.currentRate.tiers ?? [];
1735
+ const existingIndex = tiers.findIndex((t) => t.threshold === threshold);
1736
+ if (existingIndex >= 0) {
1737
+ tiers[existingIndex].rate = rate;
1738
+ } else {
1739
+ tiers.push({ threshold, rate });
1740
+ tiers.sort((a, b) => {
1741
+ return BigInt(a.threshold) < BigInt(b.threshold) ? -1 : 1;
1742
+ });
1743
+ }
1744
+ this.currentRate.tiers = tiers;
1745
+ }
1746
+ /**
1747
+ * Remove a tier
1748
+ */
1749
+ removeTier(threshold) {
1750
+ if (!this.currentRate.tiers) return false;
1751
+ const initialLength = this.currentRate.tiers.length;
1752
+ this.currentRate.tiers = this.currentRate.tiers.filter(
1753
+ (t) => t.threshold !== threshold
1754
+ );
1755
+ return this.currentRate.tiers.length < initialLength;
1756
+ }
1757
+ /**
1758
+ * Get rate history
1759
+ */
1760
+ getHistory() {
1761
+ return [...this.history];
1762
+ }
1763
+ /**
1764
+ * Calculate average rate over time period
1765
+ */
1766
+ getAverageRate(startTime, endTime) {
1767
+ const relevantHistory = this.history.filter(
1768
+ (h) => h.timestamp >= startTime && h.timestamp <= endTime
1769
+ );
1770
+ if (relevantHistory.length === 0) {
1771
+ return this.currentRate.baseRate;
1772
+ }
1773
+ let totalWeight = 0n;
1774
+ let weightedSum = 0n;
1775
+ let prevTime = startTime;
1776
+ for (const entry of relevantHistory) {
1777
+ const duration = BigInt(entry.timestamp - prevTime);
1778
+ weightedSum += BigInt(entry.previousRate) * duration;
1779
+ totalWeight += duration;
1780
+ prevTime = entry.timestamp;
1781
+ }
1782
+ const finalDuration = BigInt(endTime - prevTime);
1783
+ weightedSum += BigInt(this.currentRate.baseRate) * finalDuration;
1784
+ totalWeight += finalDuration;
1785
+ if (totalWeight === 0n) {
1786
+ return this.currentRate.baseRate;
1787
+ }
1788
+ return (weightedSum / totalWeight).toString();
1789
+ }
1790
+ /**
1791
+ * Calculate rate for dynamic pricing based on demand
1792
+ */
1793
+ calculateDynamicRate(demand, _baseRate) {
1794
+ const base = BigInt(this.currentRate.baseRate);
1795
+ const min = this.currentRate.minRate ? BigInt(this.currentRate.minRate) : base / 2n;
1796
+ const max = this.currentRate.maxRate ? BigInt(this.currentRate.maxRate) : base * 2n;
1797
+ const range = max - min;
1798
+ const adjustment = BigInt(Math.floor(Number(range) * demand));
1799
+ return (min + adjustment).toString();
1800
+ }
1801
+ /**
1802
+ * Apply exponential smoothing for rate changes
1803
+ */
1804
+ smoothRate(targetRate) {
1805
+ const current = BigInt(this.currentRate.baseRate);
1806
+ const target = BigInt(targetRate);
1807
+ const alpha = this.config.smoothingFactor;
1808
+ const smoothed = BigInt(Math.floor(
1809
+ alpha * Number(target) + (1 - alpha) * Number(current)
1810
+ ));
1811
+ return smoothed.toString();
1812
+ }
1813
+ calculateTieredRate(totalUsage) {
1814
+ const usage = BigInt(totalUsage);
1815
+ const tiers = this.currentRate.tiers ?? [];
1816
+ let applicableRate = this.currentRate.baseRate;
1817
+ for (const tier of tiers) {
1818
+ if (usage >= BigInt(tier.threshold)) {
1819
+ applicableRate = tier.rate;
1820
+ }
1821
+ }
1822
+ return applicableRate;
1823
+ }
1824
+ calculateChangePercent(from, to) {
1825
+ if (from === 0n) return 100;
1826
+ const diff = to > from ? to - from : from - to;
1827
+ return Number(diff * 100n / from);
1828
+ }
1829
+ applyMaxChange(from, to, maxPercent) {
1830
+ const maxChange = from * BigInt(maxPercent) / 100n;
1831
+ if (to > from) {
1832
+ return (from + maxChange).toString();
1833
+ } else {
1834
+ return (from - maxChange).toString();
1835
+ }
1836
+ }
1837
+ };
1838
+ var RateLimiter = class {
1839
+ requests = /* @__PURE__ */ new Map();
1840
+ maxRequests;
1841
+ windowMs;
1842
+ constructor(maxRequests = 10, windowMs = 6e4) {
1843
+ this.maxRequests = maxRequests;
1844
+ this.windowMs = windowMs;
1845
+ }
1846
+ /**
1847
+ * Check if request is allowed
1848
+ */
1849
+ isAllowed(key) {
1850
+ const now = Date.now();
1851
+ const windowStart = now - this.windowMs;
1852
+ let requests = this.requests.get(key) ?? [];
1853
+ requests = requests.filter((t) => t > windowStart);
1854
+ if (requests.length >= this.maxRequests) {
1855
+ return false;
1856
+ }
1857
+ requests.push(now);
1858
+ this.requests.set(key, requests);
1859
+ return true;
1860
+ }
1861
+ /**
1862
+ * Get remaining requests in window
1863
+ */
1864
+ getRemainingRequests(key) {
1865
+ const now = Date.now();
1866
+ const windowStart = now - this.windowMs;
1867
+ const requests = (this.requests.get(key) ?? []).filter((t) => t > windowStart);
1868
+ return Math.max(0, this.maxRequests - requests.length);
1869
+ }
1870
+ /**
1871
+ * Reset limits for a key
1872
+ */
1873
+ reset(key) {
1874
+ this.requests.delete(key);
1875
+ }
1876
+ /**
1877
+ * Clear all limits
1878
+ */
1879
+ clear() {
1880
+ this.requests.clear();
1881
+ }
1882
+ };
1883
+ function calculateOptimalRate(channelCapacity, desiredDurationSeconds, bufferPercent = 10) {
1884
+ const capacity = BigInt(channelCapacity);
1885
+ const duration = BigInt(desiredDurationSeconds);
1886
+ if (duration === 0n) {
1887
+ return "0";
1888
+ }
1889
+ const effectiveCapacity = capacity * BigInt(100 - bufferPercent) / 100n;
1890
+ return (effectiveCapacity / duration).toString();
1891
+ }
1892
+ function convertRate(rate, fromUnit, toUnit) {
1893
+ const unitToSeconds = {
1894
+ second: 1,
1895
+ minute: 60,
1896
+ hour: 3600,
1897
+ day: 86400
1898
+ };
1899
+ const fromSeconds = unitToSeconds[fromUnit];
1900
+ const toSeconds = unitToSeconds[toUnit];
1901
+ const rateBigInt = BigInt(rate);
1902
+ const perSecond = rateBigInt / BigInt(fromSeconds);
1903
+ return (perSecond * BigInt(toSeconds)).toString();
1904
+ }
1905
+
1906
+ // src/streaming/metering.ts
1907
+ var MeteringManager = class {
1908
+ records = [];
1909
+ config;
1910
+ sessionId;
1911
+ startTime;
1912
+ constructor(sessionId, config = {}) {
1913
+ this.sessionId = sessionId;
1914
+ this.startTime = Date.now();
1915
+ this.config = {
1916
+ recordInterval: config.recordInterval ?? 1,
1917
+ aggregationInterval: config.aggregationInterval ?? 3600,
1918
+ maxRecords: config.maxRecords ?? 1e4,
1919
+ precision: config.precision ?? 18
1920
+ };
1921
+ }
1922
+ /**
1923
+ * Record usage
1924
+ */
1925
+ record(duration, amount, rate, metadata) {
1926
+ const cumulative = this.calculateCumulative(amount);
1927
+ const record = {
1928
+ timestamp: Date.now(),
1929
+ duration,
1930
+ amount,
1931
+ rate,
1932
+ cumulative,
1933
+ metadata
1934
+ };
1935
+ this.records.push(record);
1936
+ if (this.records.length > this.config.maxRecords) {
1937
+ this.pruneRecords();
1938
+ }
1939
+ return record;
1940
+ }
1941
+ /**
1942
+ * Get all records
1943
+ */
1944
+ getRecords() {
1945
+ return [...this.records];
1946
+ }
1947
+ /**
1948
+ * Get records in time range
1949
+ */
1950
+ getRecordsInRange(startTime, endTime) {
1951
+ return this.records.filter(
1952
+ (r) => r.timestamp >= startTime && r.timestamp <= endTime
1953
+ );
1954
+ }
1955
+ /**
1956
+ * Calculate usage metrics
1957
+ */
1958
+ getMetrics() {
1959
+ if (this.records.length === 0) {
1960
+ return {
1961
+ totalSeconds: 0,
1962
+ totalAmount: "0",
1963
+ averageRate: "0",
1964
+ peakRate: "0",
1965
+ startTime: this.startTime
1966
+ };
1967
+ }
1968
+ const totalSeconds = this.records.reduce((sum, r) => sum + r.duration, 0);
1969
+ const totalAmount = this.records[this.records.length - 1].cumulative;
1970
+ const rates = this.records.map((r) => BigInt(r.rate));
1971
+ const peakRate = rates.length > 0 ? rates.reduce((max, r) => r > max ? r : max, 0n) : 0n;
1972
+ const averageRate = totalSeconds > 0 ? (BigInt(totalAmount) / BigInt(totalSeconds)).toString() : "0";
1973
+ return {
1974
+ totalSeconds,
1975
+ totalAmount,
1976
+ averageRate,
1977
+ peakRate: peakRate.toString(),
1978
+ startTime: this.startTime,
1979
+ endTime: this.records[this.records.length - 1].timestamp,
1980
+ hourly: this.getHourlyBreakdown()
1981
+ };
1982
+ }
1983
+ /**
1984
+ * Get aggregated usage by period
1985
+ */
1986
+ aggregate(intervalSeconds = 3600) {
1987
+ if (this.records.length === 0) return [];
1988
+ const aggregated = [];
1989
+ const intervalMs = intervalSeconds * 1e3;
1990
+ const groups = /* @__PURE__ */ new Map();
1991
+ for (const record of this.records) {
1992
+ const periodStart = Math.floor(record.timestamp / intervalMs) * intervalMs;
1993
+ const existing = groups.get(periodStart) ?? [];
1994
+ existing.push(record);
1995
+ groups.set(periodStart, existing);
1996
+ }
1997
+ for (const [periodStart, records] of groups) {
1998
+ const totalAmount = records.reduce(
1999
+ (sum, r) => sum + BigInt(r.amount),
2000
+ 0n
2001
+ );
2002
+ const totalDuration = records.reduce((sum, r) => sum + r.duration, 0);
2003
+ const averageRate = totalDuration > 0 ? (totalAmount / BigInt(totalDuration)).toString() : "0";
2004
+ aggregated.push({
2005
+ period: new Date(periodStart).toISOString(),
2006
+ totalAmount: totalAmount.toString(),
2007
+ totalDuration,
2008
+ averageRate,
2009
+ recordCount: records.length
2010
+ });
2011
+ }
2012
+ return aggregated.sort((a, b) => a.period.localeCompare(b.period));
2013
+ }
2014
+ /**
2015
+ * Get cumulative amount at a point in time
2016
+ */
2017
+ getCumulativeAt(timestamp) {
2018
+ for (let i = this.records.length - 1; i >= 0; i--) {
2019
+ if (this.records[i].timestamp <= timestamp) {
2020
+ return this.records[i].cumulative;
2021
+ }
2022
+ }
2023
+ return "0";
2024
+ }
2025
+ /**
2026
+ * Calculate usage for billing period
2027
+ */
2028
+ getUsageForBillingPeriod(startTime, endTime) {
2029
+ const periodRecords = this.getRecordsInRange(startTime, endTime);
2030
+ const amount = periodRecords.reduce(
2031
+ (sum, r) => sum + BigInt(r.amount),
2032
+ 0n
2033
+ );
2034
+ const duration = periodRecords.reduce((sum, r) => sum + r.duration, 0);
2035
+ return {
2036
+ amount: amount.toString(),
2037
+ duration,
2038
+ records: periodRecords.length
2039
+ };
2040
+ }
2041
+ /**
2042
+ * Export records for backup/audit
2043
+ */
2044
+ export() {
2045
+ return JSON.stringify({
2046
+ sessionId: this.sessionId,
2047
+ startTime: this.startTime,
2048
+ records: this.records,
2049
+ exportedAt: Date.now()
2050
+ });
2051
+ }
2052
+ /**
2053
+ * Import records from backup
2054
+ */
2055
+ import(data) {
2056
+ try {
2057
+ const parsed = JSON.parse(data);
2058
+ if (parsed.sessionId !== this.sessionId) {
2059
+ return { success: false, recordsImported: 0 };
2060
+ }
2061
+ const importedRecords = parsed.records;
2062
+ let importedCount = 0;
2063
+ for (const record of importedRecords) {
2064
+ const exists = this.records.some((r) => r.timestamp === record.timestamp);
2065
+ if (!exists) {
2066
+ this.records.push(record);
2067
+ importedCount++;
2068
+ }
2069
+ }
2070
+ this.records.sort((a, b) => a.timestamp - b.timestamp);
2071
+ return { success: true, recordsImported: importedCount };
2072
+ } catch {
2073
+ return { success: false, recordsImported: 0 };
2074
+ }
2075
+ }
2076
+ /**
2077
+ * Clear all records
2078
+ */
2079
+ clear() {
2080
+ this.records = [];
2081
+ }
2082
+ calculateCumulative(newAmount) {
2083
+ if (this.records.length === 0) {
2084
+ return newAmount;
2085
+ }
2086
+ const lastCumulative = BigInt(this.records[this.records.length - 1].cumulative);
2087
+ return (lastCumulative + BigInt(newAmount)).toString();
2088
+ }
2089
+ pruneRecords() {
2090
+ const keepCount = this.config.maxRecords;
2091
+ if (this.records.length <= keepCount) return;
2092
+ const first = this.records[0];
2093
+ const recent = this.records.slice(-(keepCount - 1));
2094
+ this.records = [first, ...recent];
2095
+ }
2096
+ getHourlyBreakdown() {
2097
+ const hourlyMap = /* @__PURE__ */ new Map();
2098
+ for (const record of this.records) {
2099
+ const hour = new Date(record.timestamp).getUTCHours();
2100
+ const existing = hourlyMap.get(hour) ?? { amount: 0n, seconds: 0 };
2101
+ existing.amount += BigInt(record.amount);
2102
+ existing.seconds += record.duration;
2103
+ hourlyMap.set(hour, existing);
2104
+ }
2105
+ return Array.from(hourlyMap.entries()).map(([hour, data]) => ({
2106
+ hour,
2107
+ amount: data.amount.toString(),
2108
+ seconds: data.seconds
2109
+ }));
2110
+ }
2111
+ };
2112
+ function calculateProRatedUsage(fullPeriodAmount, fullPeriodSeconds, actualSeconds) {
2113
+ if (fullPeriodSeconds === 0) return "0";
2114
+ const amount = BigInt(fullPeriodAmount);
2115
+ return (amount * BigInt(actualSeconds) / BigInt(fullPeriodSeconds)).toString();
2116
+ }
2117
+ function estimateUsage(metrics, futureSeconds) {
2118
+ if (metrics.totalSeconds === 0) return "0";
2119
+ const avgRate = BigInt(metrics.averageRate);
2120
+ return (avgRate * BigInt(futureSeconds)).toString();
2121
+ }
2122
+ function compareUsage(current, previous) {
2123
+ const currentAmount = BigInt(current.totalAmount);
2124
+ const previousAmount = BigInt(previous.totalAmount);
2125
+ const amountChange = currentAmount - previousAmount;
2126
+ const amountChangePercent = previousAmount > 0n ? Number(amountChange * 100n / previousAmount) : 0;
2127
+ const currentRate = BigInt(current.averageRate);
2128
+ const previousRate = BigInt(previous.averageRate);
2129
+ const rateChange = currentRate - previousRate;
2130
+ const rateChangePercent = previousRate > 0n ? Number(rateChange * 100n / previousRate) : 0;
2131
+ return {
2132
+ amountChange: amountChange.toString(),
2133
+ amountChangePercent,
2134
+ rateChange: rateChange.toString(),
2135
+ rateChangePercent
2136
+ };
2137
+ }
2138
+
2139
+ // src/streaming/billing.ts
2140
+ var BillingManager = class {
2141
+ config;
2142
+ billingConfig;
2143
+ invoices = [];
2144
+ channelId;
2145
+ payer;
2146
+ payee;
2147
+ lastInvoiceTime;
2148
+ constructor(channelId, payer, payee, billingConfig, config = {}) {
2149
+ this.channelId = channelId;
2150
+ this.payer = payer;
2151
+ this.payee = payee;
2152
+ this.billingConfig = billingConfig;
2153
+ this.lastInvoiceTime = Date.now();
2154
+ this.config = {
2155
+ autoInvoice: config.autoInvoice ?? false,
2156
+ invoiceInterval: config.invoiceInterval ?? 86400,
2157
+ currency: config.currency ?? "USDT",
2158
+ taxRate: config.taxRate ?? 0
2159
+ };
2160
+ }
2161
+ /**
2162
+ * Generate invoice from metering records
2163
+ */
2164
+ generateInvoice(records, startTime, endTime) {
2165
+ const items = this.createInvoiceItems(records, startTime, endTime);
2166
+ const subtotal = this.calculateSubtotal(items);
2167
+ const fees = this.calculateFees(subtotal);
2168
+ const total = (BigInt(subtotal) + BigInt(fees)).toString();
2169
+ const invoice = {
2170
+ id: this.generateInvoiceId(),
2171
+ channelId: this.channelId,
2172
+ payer: this.payer,
2173
+ payee: this.payee,
2174
+ items,
2175
+ subtotal,
2176
+ fees,
2177
+ total,
2178
+ currency: this.config.currency,
2179
+ status: "pending",
2180
+ createdAt: Date.now(),
2181
+ dueAt: Date.now() + 864e5
2182
+ // Due in 24 hours
2183
+ };
2184
+ this.invoices.push(invoice);
2185
+ this.lastInvoiceTime = endTime;
2186
+ return invoice;
2187
+ }
2188
+ /**
2189
+ * Get all invoices
2190
+ */
2191
+ getInvoices() {
2192
+ return [...this.invoices];
2193
+ }
2194
+ /**
2195
+ * Get invoice by ID
2196
+ */
2197
+ getInvoice(id) {
2198
+ return this.invoices.find((inv) => inv.id === id);
2199
+ }
2200
+ /**
2201
+ * Mark invoice as paid
2202
+ */
2203
+ markPaid(invoiceId) {
2204
+ const invoice = this.invoices.find((inv) => inv.id === invoiceId);
2205
+ if (!invoice || invoice.status !== "pending") {
2206
+ return false;
2207
+ }
2208
+ invoice.status = "paid";
2209
+ invoice.paidAt = Date.now();
2210
+ return true;
2211
+ }
2212
+ /**
2213
+ * Mark invoice as settled
2214
+ */
2215
+ markSettled(invoiceId) {
2216
+ const invoice = this.invoices.find((inv) => inv.id === invoiceId);
2217
+ if (!invoice) return false;
2218
+ invoice.status = "settled";
2219
+ return true;
2220
+ }
2221
+ /**
2222
+ * Get pending amount
2223
+ */
2224
+ getPendingAmount() {
2225
+ return this.invoices.filter((inv) => inv.status === "pending").reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
2226
+ }
2227
+ /**
2228
+ * Get total billed amount
2229
+ */
2230
+ getTotalBilled() {
2231
+ return this.invoices.reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
2232
+ }
2233
+ /**
2234
+ * Check if new invoice is due
2235
+ */
2236
+ isInvoiceDue(currentTime) {
2237
+ const elapsed = currentTime - this.lastInvoiceTime;
2238
+ return elapsed >= this.config.invoiceInterval * 1e3;
2239
+ }
2240
+ /**
2241
+ * Calculate amount for billing period
2242
+ */
2243
+ calculatePeriodAmount(rate, durationSeconds) {
2244
+ const periodSeconds = this.getPeriodSeconds(this.billingConfig.period);
2245
+ const periods = durationSeconds / periodSeconds;
2246
+ const amount = BigInt(rate) * BigInt(Math.floor(periods * periodSeconds));
2247
+ return this.applyRounding(amount.toString());
2248
+ }
2249
+ /**
2250
+ * Apply minimum charge
2251
+ */
2252
+ applyMinimumCharge(amount) {
2253
+ const minCharge = BigInt(this.billingConfig.minimumCharge);
2254
+ const actualAmount = BigInt(amount);
2255
+ return (actualAmount < minCharge ? minCharge : actualAmount).toString();
2256
+ }
2257
+ /**
2258
+ * Calculate grace period savings
2259
+ */
2260
+ calculateGracePeriodSavings(rate, totalDuration) {
2261
+ const gracePeriod = this.billingConfig.gracePeriod;
2262
+ if (gracePeriod <= 0 || totalDuration <= gracePeriod) {
2263
+ return "0";
2264
+ }
2265
+ return (BigInt(rate) * BigInt(Math.min(gracePeriod, totalDuration))).toString();
2266
+ }
2267
+ /**
2268
+ * Get billing summary
2269
+ */
2270
+ getSummary() {
2271
+ const totalBilled = this.getTotalBilled();
2272
+ const totalPaid = this.invoices.filter((inv) => inv.status === "paid" || inv.status === "settled").reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
2273
+ const totalPending = this.getPendingAmount();
2274
+ const averageInvoice = this.invoices.length > 0 ? (BigInt(totalBilled) / BigInt(this.invoices.length)).toString() : "0";
2275
+ return {
2276
+ totalInvoices: this.invoices.length,
2277
+ totalBilled,
2278
+ totalPaid,
2279
+ totalPending,
2280
+ averageInvoice
2281
+ };
2282
+ }
2283
+ /**
2284
+ * Export billing data
2285
+ */
2286
+ export() {
2287
+ return JSON.stringify({
2288
+ channelId: this.channelId,
2289
+ invoices: this.invoices,
2290
+ exportedAt: Date.now()
2291
+ });
2292
+ }
2293
+ createInvoiceItems(records, startTime, endTime) {
2294
+ if (records.length === 0) {
2295
+ return [];
2296
+ }
2297
+ const rateGroups = /* @__PURE__ */ new Map();
2298
+ for (const record of records) {
2299
+ const existing = rateGroups.get(record.rate) ?? [];
2300
+ existing.push(record);
2301
+ rateGroups.set(record.rate, existing);
2302
+ }
2303
+ const items = [];
2304
+ for (const [rate, groupRecords] of rateGroups) {
2305
+ const totalDuration = groupRecords.reduce((sum, r) => sum + r.duration, 0);
2306
+ const totalAmount = groupRecords.reduce(
2307
+ (sum, r) => sum + BigInt(r.amount),
2308
+ 0n
2309
+ );
2310
+ const periodName = this.getPeriodName(this.billingConfig.period);
2311
+ const quantity = this.calculateQuantity(totalDuration);
2312
+ items.push({
2313
+ description: `Streaming usage at ${rate} per ${periodName}`,
2314
+ quantity,
2315
+ rate,
2316
+ amount: totalAmount.toString(),
2317
+ startTime,
2318
+ endTime
2319
+ });
2320
+ }
2321
+ return items;
2322
+ }
2323
+ calculateSubtotal(items) {
2324
+ return items.reduce((sum, item) => sum + BigInt(item.amount), 0n).toString();
2325
+ }
2326
+ calculateFees(subtotal) {
2327
+ const amount = BigInt(subtotal);
2328
+ const taxRate = this.config.taxRate;
2329
+ if (taxRate <= 0) return "0";
2330
+ return BigInt(Math.floor(Number(amount) * taxRate)).toString();
2331
+ }
2332
+ applyRounding(amount) {
2333
+ const value = BigInt(amount);
2334
+ const mode = this.billingConfig.roundingMode;
2335
+ switch (mode) {
2336
+ case "floor":
2337
+ return value.toString();
2338
+ case "ceil":
2339
+ return value.toString();
2340
+ case "round":
2341
+ return value.toString();
2342
+ default:
2343
+ return value.toString();
2344
+ }
2345
+ }
2346
+ getPeriodSeconds(period) {
2347
+ switch (period) {
2348
+ case "realtime":
2349
+ case "second":
2350
+ return 1;
2351
+ case "minute":
2352
+ return 60;
2353
+ case "hour":
2354
+ return 3600;
2355
+ case "day":
2356
+ return 86400;
2357
+ }
2358
+ }
2359
+ getPeriodName(period) {
2360
+ switch (period) {
2361
+ case "realtime":
2362
+ case "second":
2363
+ return "second";
2364
+ case "minute":
2365
+ return "minute";
2366
+ case "hour":
2367
+ return "hour";
2368
+ case "day":
2369
+ return "day";
2370
+ }
2371
+ }
2372
+ calculateQuantity(totalSeconds) {
2373
+ const periodSeconds = this.getPeriodSeconds(this.billingConfig.period);
2374
+ return totalSeconds / periodSeconds;
2375
+ }
2376
+ generateInvoiceId() {
2377
+ return `inv_${this.channelId.slice(-8)}_${Date.now().toString(36)}`;
2378
+ }
2379
+ };
2380
+ function formatCurrencyAmount(amount, decimals = 6, symbol = "USDT") {
2381
+ const value = BigInt(amount);
2382
+ const divisor = BigInt(10 ** decimals);
2383
+ const wholePart = value / divisor;
2384
+ const fractionalPart = value % divisor;
2385
+ const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
2386
+ const trimmedFractional = fractionalStr.replace(/0+$/, "") || "0";
2387
+ return `${wholePart}.${trimmedFractional} ${symbol}`;
2388
+ }
2389
+ function parseCurrencyAmount(display, decimals = 6) {
2390
+ const cleaned = display.replace(/[^\d.]/g, "");
2391
+ const [whole, fractional = ""] = cleaned.split(".");
2392
+ const paddedFractional = fractional.slice(0, decimals).padEnd(decimals, "0");
2393
+ const combined = whole + paddedFractional;
2394
+ return BigInt(combined).toString();
2395
+ }
2396
+ function estimateFutureBill(currentRate, durationSeconds, minimumCharge = "0") {
2397
+ const estimated = BigInt(currentRate) * BigInt(durationSeconds);
2398
+ const minimum = BigInt(minimumCharge);
2399
+ return (estimated < minimum ? minimum : estimated).toString();
2400
+ }
2401
+
2402
+ // src/settlement/types.ts
2403
+ import { z as z3 } from "zod";
2404
+ var SettlementState = z3.enum([
2405
+ "pending",
2406
+ // Settlement not yet initiated
2407
+ "in_progress",
2408
+ // Settlement process started
2409
+ "challenging",
2410
+ // In challenge period
2411
+ "disputed",
2412
+ // Dispute raised
2413
+ "finalizing",
2414
+ // Finalizing on-chain
2415
+ "completed",
2416
+ // Successfully settled
2417
+ "failed"
2418
+ // Settlement failed
2419
+ ]);
2420
+ var CheckpointType = z3.enum([
2421
+ "periodic",
2422
+ // Regular interval checkpoint
2423
+ "manual",
2424
+ // Manually triggered
2425
+ "balance",
2426
+ // Triggered by balance threshold
2427
+ "pre_close",
2428
+ // Before channel close
2429
+ "dispute"
2430
+ // Checkpoint for dispute
2431
+ ]);
2432
+ var SettlementCheckpoint = z3.object({
2433
+ id: z3.string(),
2434
+ channelId: z3.string(),
2435
+ sequence: z3.number(),
2436
+ type: CheckpointType,
2437
+ // Balances
2438
+ payerBalance: z3.string(),
2439
+ payeeBalance: z3.string(),
2440
+ totalStreamed: z3.string(),
2441
+ // Signatures
2442
+ payerSignature: z3.string(),
2443
+ payeeSignature: z3.string().optional(),
2444
+ // Verification
2445
+ stateHash: z3.string(),
2446
+ merkleRoot: z3.string().optional(),
2447
+ // Timing
2448
+ createdAt: z3.number(),
2449
+ expiresAt: z3.number().optional(),
2450
+ // On-chain reference
2451
+ txHash: z3.string().optional(),
2452
+ blockNumber: z3.number().optional()
2453
+ });
2454
+ var SettlementRequest = z3.object({
2455
+ channelId: z3.string(),
2456
+ initiator: z3.string(),
2457
+ finalCheckpoint: SettlementCheckpoint,
2458
+ reason: z3.enum(["mutual", "unilateral", "timeout", "dispute_resolution"]),
2459
+ signature: z3.string(),
2460
+ metadata: z3.record(z3.unknown()).optional()
2461
+ });
2462
+ var SettlementResult = z3.object({
2463
+ success: z3.boolean(),
2464
+ settlementId: z3.string().optional(),
2465
+ error: z3.string().optional(),
2466
+ finalBalances: z3.object({
2467
+ payer: z3.string(),
2468
+ payee: z3.string()
2469
+ }).optional(),
2470
+ txHash: z3.string().optional(),
2471
+ timestamp: z3.number()
2472
+ });
2473
+ var DisputeReason = z3.enum([
2474
+ "invalid_checkpoint",
2475
+ // Checkpoint signature or data invalid
2476
+ "stale_state",
2477
+ // Challenger has newer valid state
2478
+ "balance_mismatch",
2479
+ // Balance doesn't match expected
2480
+ "unauthorized_close",
2481
+ // Unauthorized party initiated close
2482
+ "fraud",
2483
+ // Fraudulent activity detected
2484
+ "other"
2485
+ // Other reason
2486
+ ]);
2487
+ var DisputeEvidence = z3.object({
2488
+ type: z3.enum(["checkpoint", "signature", "transaction", "state_proof"]),
2489
+ data: z3.string(),
2490
+ description: z3.string(),
2491
+ timestamp: z3.number(),
2492
+ verified: z3.boolean().default(false)
2493
+ });
2494
+ var Dispute = z3.object({
2495
+ id: z3.string(),
2496
+ channelId: z3.string(),
2497
+ initiator: z3.string(),
2498
+ respondent: z3.string(),
2499
+ reason: DisputeReason,
2500
+ description: z3.string(),
2501
+ // Claimed state
2502
+ claimedPayerBalance: z3.string(),
2503
+ claimedPayeeBalance: z3.string(),
2504
+ claimedCheckpoint: SettlementCheckpoint.optional(),
2505
+ // Evidence
2506
+ evidence: z3.array(DisputeEvidence),
2507
+ responseEvidence: z3.array(DisputeEvidence).optional(),
2508
+ // Resolution
2509
+ status: z3.enum(["pending", "under_review", "resolved", "rejected", "timeout"]),
2510
+ resolution: z3.object({
2511
+ winner: z3.string().optional(),
2512
+ finalPayerBalance: z3.string(),
2513
+ finalPayeeBalance: z3.string(),
2514
+ reason: z3.string(),
2515
+ timestamp: z3.number()
2516
+ }).optional(),
2517
+ // Timing
2518
+ createdAt: z3.number(),
2519
+ responseDeadline: z3.number(),
2520
+ resolutionDeadline: z3.number()
2521
+ });
2522
+ var SettlementConfig = z3.object({
2523
+ challengePeriod: z3.number().default(86400),
2524
+ // 24 hours
2525
+ disputeResponsePeriod: z3.number().default(43200),
2526
+ // 12 hours
2527
+ disputeResolutionPeriod: z3.number().default(172800),
2528
+ // 48 hours
2529
+ minCheckpointInterval: z3.number().default(60),
2530
+ // 60 seconds
2531
+ maxCheckpointsStored: z3.number().default(100),
2532
+ settlementFee: z3.string().default("0"),
2533
+ disputeBond: z3.string().default("0")
2534
+ // Bond required to raise dispute
2535
+ });
2536
+ var OnChainSettlement = z3.object({
2537
+ channelId: z3.string(),
2538
+ settlementId: z3.string(),
2539
+ txHash: z3.string(),
2540
+ blockNumber: z3.number(),
2541
+ payerReceived: z3.string(),
2542
+ payeeReceived: z3.string(),
2543
+ fee: z3.string(),
2544
+ timestamp: z3.number(),
2545
+ finalized: z3.boolean()
2546
+ });
2547
+
2548
+ // src/settlement/checkpoint.ts
2549
+ var CheckpointManager = class {
2550
+ checkpoints = /* @__PURE__ */ new Map();
2551
+ config;
2552
+ settlementConfig;
2553
+ lastCheckpointTime = /* @__PURE__ */ new Map();
2554
+ constructor(settlementConfig, config = {}) {
2555
+ this.settlementConfig = settlementConfig;
2556
+ this.config = {
2557
+ autoCheckpoint: config.autoCheckpoint ?? false,
2558
+ intervalSeconds: config.intervalSeconds ?? 3600,
2559
+ balanceThreshold: config.balanceThreshold ?? "1000000",
2560
+ onCheckpoint: config.onCheckpoint ?? (() => {
2561
+ })
2562
+ };
2563
+ }
2564
+ /**
2565
+ * Create a new checkpoint
2566
+ */
2567
+ create(request) {
2568
+ const now = Date.now();
2569
+ const channelCheckpoints = this.checkpoints.get(request.channelId) ?? [];
2570
+ const expectedSequence = channelCheckpoints.length;
2571
+ if (request.sequence !== expectedSequence) {
2572
+ throw new Error(`Invalid sequence: expected ${expectedSequence}, got ${request.sequence}`);
2573
+ }
2574
+ const lastTime = this.lastCheckpointTime.get(request.channelId) ?? 0;
2575
+ const elapsed = (now - lastTime) / 1e3;
2576
+ if (elapsed < this.settlementConfig.minCheckpointInterval && channelCheckpoints.length > 0) {
2577
+ throw new Error(`Checkpoint interval too short: ${elapsed}s < ${this.settlementConfig.minCheckpointInterval}s`);
2578
+ }
2579
+ const stateHash = this.generateStateHash(request);
2580
+ const checkpoint = {
2581
+ id: this.generateCheckpointId(request.channelId, request.sequence),
2582
+ channelId: request.channelId,
2583
+ sequence: request.sequence,
2584
+ type: request.type ?? "manual",
2585
+ payerBalance: request.payerBalance,
2586
+ payeeBalance: request.payeeBalance,
2587
+ totalStreamed: request.totalStreamed,
2588
+ payerSignature: request.payerSignature,
2589
+ payeeSignature: request.payeeSignature,
2590
+ stateHash,
2591
+ createdAt: now
2592
+ };
2593
+ channelCheckpoints.push(checkpoint);
2594
+ this.checkpoints.set(request.channelId, channelCheckpoints);
2595
+ this.lastCheckpointTime.set(request.channelId, now);
2596
+ this.pruneCheckpoints(request.channelId);
2597
+ this.config.onCheckpoint(checkpoint);
2598
+ return checkpoint;
2599
+ }
2600
+ /**
2601
+ * Get latest checkpoint for channel
2602
+ */
2603
+ getLatest(channelId) {
2604
+ const checkpoints = this.checkpoints.get(channelId);
2605
+ if (!checkpoints || checkpoints.length === 0) return void 0;
2606
+ return checkpoints[checkpoints.length - 1];
2607
+ }
2608
+ /**
2609
+ * Get checkpoint by sequence
2610
+ */
2611
+ getBySequence(channelId, sequence) {
2612
+ const checkpoints = this.checkpoints.get(channelId);
2613
+ if (!checkpoints) return void 0;
2614
+ return checkpoints.find((cp) => cp.sequence === sequence);
2615
+ }
2616
+ /**
2617
+ * Get all checkpoints for channel
2618
+ */
2619
+ getAll(channelId) {
2620
+ return [...this.checkpoints.get(channelId) ?? []];
2621
+ }
2622
+ /**
2623
+ * Validate checkpoint
2624
+ */
2625
+ validate(checkpoint) {
2626
+ const errors = [];
2627
+ const payerBalance = BigInt(checkpoint.payerBalance);
2628
+ const payeeBalance = BigInt(checkpoint.payeeBalance);
2629
+ const totalStreamed = BigInt(checkpoint.totalStreamed);
2630
+ if (payerBalance < 0n) {
2631
+ errors.push("Payer balance cannot be negative");
2632
+ }
2633
+ if (payeeBalance < 0n) {
2634
+ errors.push("Payee balance cannot be negative");
2635
+ }
2636
+ if (totalStreamed < 0n) {
2637
+ errors.push("Total streamed cannot be negative");
2638
+ }
2639
+ if (payeeBalance !== totalStreamed) {
2640
+ errors.push("Total streamed should equal payee balance");
2641
+ }
2642
+ if (!checkpoint.payerSignature) {
2643
+ errors.push("Payer signature is required");
2644
+ }
2645
+ const expectedHash = this.generateStateHash({
2646
+ channelId: checkpoint.channelId,
2647
+ sequence: checkpoint.sequence,
2648
+ payerBalance: checkpoint.payerBalance,
2649
+ payeeBalance: checkpoint.payeeBalance,
2650
+ totalStreamed: checkpoint.totalStreamed,
2651
+ payerSignature: checkpoint.payerSignature
2652
+ });
2653
+ if (checkpoint.stateHash !== expectedHash) {
2654
+ errors.push("State hash mismatch");
2655
+ }
2656
+ if (checkpoint.createdAt > Date.now()) {
2657
+ errors.push("Checkpoint timestamp is in the future");
2658
+ }
2659
+ return { valid: errors.length === 0, errors };
2660
+ }
2661
+ /**
2662
+ * Compare two checkpoints
2663
+ */
2664
+ compare(a, b) {
2665
+ const isNewer = a.sequence > b.sequence || a.sequence === b.sequence && a.createdAt > b.createdAt;
2666
+ const payerDiff = BigInt(a.payerBalance) - BigInt(b.payerBalance);
2667
+ const payeeDiff = BigInt(a.payeeBalance) - BigInt(b.payeeBalance);
2668
+ return {
2669
+ isNewer,
2670
+ balanceDifference: {
2671
+ payer: payerDiff.toString(),
2672
+ payee: payeeDiff.toString()
2673
+ }
2674
+ };
2675
+ }
2676
+ /**
2677
+ * Check if checkpoint is needed
2678
+ */
2679
+ needsCheckpoint(channelId, currentPayeeBalance) {
2680
+ const lastCheckpoint = this.getLatest(channelId);
2681
+ const now = Date.now();
2682
+ if (!lastCheckpoint) {
2683
+ return { needed: true, reason: "No existing checkpoint" };
2684
+ }
2685
+ const elapsed = (now - lastCheckpoint.createdAt) / 1e3;
2686
+ if (elapsed >= this.config.intervalSeconds) {
2687
+ return { needed: true, reason: "Interval elapsed" };
2688
+ }
2689
+ const balanceChange = BigInt(currentPayeeBalance) - BigInt(lastCheckpoint.payeeBalance);
2690
+ if (balanceChange >= BigInt(this.config.balanceThreshold)) {
2691
+ return { needed: true, reason: "Balance threshold exceeded" };
2692
+ }
2693
+ return { needed: false, reason: "" };
2694
+ }
2695
+ /**
2696
+ * Build merkle root from checkpoint history
2697
+ */
2698
+ buildMerkleRoot(channelId) {
2699
+ const checkpoints = this.checkpoints.get(channelId);
2700
+ if (!checkpoints || checkpoints.length === 0) {
2701
+ return "0x" + "0".repeat(64);
2702
+ }
2703
+ const hashes = checkpoints.map((cp) => cp.stateHash);
2704
+ return this.hashArray(hashes);
2705
+ }
2706
+ /**
2707
+ * Verify checkpoint is in merkle tree
2708
+ */
2709
+ verifyMerkleProof(checkpoint, merkleRoot, _proof) {
2710
+ const channelRoot = this.buildMerkleRoot(checkpoint.channelId);
2711
+ return channelRoot === merkleRoot;
2712
+ }
2713
+ /**
2714
+ * Export checkpoints for backup
2715
+ */
2716
+ export(channelId) {
2717
+ const checkpoints = this.checkpoints.get(channelId) ?? [];
2718
+ return JSON.stringify({
2719
+ channelId,
2720
+ checkpoints,
2721
+ exportedAt: Date.now()
2722
+ });
2723
+ }
2724
+ /**
2725
+ * Import checkpoints from backup
2726
+ */
2727
+ import(data) {
2728
+ try {
2729
+ const parsed = JSON.parse(data);
2730
+ const { channelId, checkpoints } = parsed;
2731
+ let imported = 0;
2732
+ const existing = this.checkpoints.get(channelId) ?? [];
2733
+ for (const cp of checkpoints) {
2734
+ const validation = this.validate(cp);
2735
+ if (validation.valid) {
2736
+ const exists = existing.some((e) => e.sequence === cp.sequence);
2737
+ if (!exists) {
2738
+ existing.push(cp);
2739
+ imported++;
2740
+ }
2741
+ }
2742
+ }
2743
+ existing.sort((a, b) => a.sequence - b.sequence);
2744
+ this.checkpoints.set(channelId, existing);
2745
+ return { success: true, imported };
2746
+ } catch {
2747
+ return { success: false, imported: 0 };
2748
+ }
2749
+ }
2750
+ /**
2751
+ * Clear checkpoints for channel
2752
+ */
2753
+ clear(channelId) {
2754
+ this.checkpoints.delete(channelId);
2755
+ this.lastCheckpointTime.delete(channelId);
2756
+ }
2757
+ generateCheckpointId(channelId, sequence) {
2758
+ return `cp_${channelId.slice(-8)}_${sequence}_${Date.now().toString(36)}`;
2759
+ }
2760
+ generateStateHash(request) {
2761
+ const data = [
2762
+ request.channelId,
2763
+ request.sequence.toString(),
2764
+ request.payerBalance,
2765
+ request.payeeBalance,
2766
+ request.totalStreamed
2767
+ ].join(":");
2768
+ let hash = 0;
2769
+ for (let i = 0; i < data.length; i++) {
2770
+ const char = data.charCodeAt(i);
2771
+ hash = (hash << 5) - hash + char;
2772
+ hash = hash & hash;
2773
+ }
2774
+ return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
2775
+ }
2776
+ hashArray(hashes) {
2777
+ const combined = hashes.join("");
2778
+ let hash = 0;
2779
+ for (let i = 0; i < combined.length; i++) {
2780
+ const char = combined.charCodeAt(i);
2781
+ hash = (hash << 5) - hash + char;
2782
+ hash = hash & hash;
2783
+ }
2784
+ return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
2785
+ }
2786
+ pruneCheckpoints(channelId) {
2787
+ const checkpoints = this.checkpoints.get(channelId);
2788
+ if (!checkpoints) return;
2789
+ const maxStored = this.settlementConfig.maxCheckpointsStored;
2790
+ if (checkpoints.length <= maxStored) return;
2791
+ const first = checkpoints[0];
2792
+ const recent = checkpoints.slice(-(maxStored - 1));
2793
+ this.checkpoints.set(channelId, [first, ...recent]);
2794
+ }
2795
+ };
2796
+ function signCheckpoint(checkpoint, _privateKey) {
2797
+ const dataToSign = [
2798
+ checkpoint.channelId,
2799
+ checkpoint.sequence.toString(),
2800
+ checkpoint.payerBalance,
2801
+ checkpoint.payeeBalance,
2802
+ checkpoint.totalStreamed,
2803
+ checkpoint.createdAt.toString()
2804
+ ].join(":");
2805
+ let hash = 0;
2806
+ for (let i = 0; i < dataToSign.length; i++) {
2807
+ const char = dataToSign.charCodeAt(i);
2808
+ hash = (hash << 5) - hash + char;
2809
+ hash = hash & hash;
2810
+ }
2811
+ return "0x" + Math.abs(hash).toString(16).padStart(128, "0");
2812
+ }
2813
+ function verifyCheckpointSignature(_checkpoint, signature, _publicKey) {
2814
+ return signature.startsWith("0x") && signature.length === 130;
2815
+ }
2816
+
2817
+ // src/settlement/final.ts
2818
+ var FinalSettlementManager = class {
2819
+ settlements = /* @__PURE__ */ new Map();
2820
+ checkpointManager;
2821
+ settlementConfig;
2822
+ onChainSettlements = /* @__PURE__ */ new Map();
2823
+ constructor(checkpointManager, settlementConfig, _config = {}) {
2824
+ this.checkpointManager = checkpointManager;
2825
+ this.settlementConfig = settlementConfig;
2826
+ }
2827
+ /**
2828
+ * Initiate settlement
2829
+ */
2830
+ initiate(request) {
2831
+ const now = Date.now();
2832
+ const validation = this.validateRequest(request);
2833
+ if (!validation.valid) {
2834
+ return {
2835
+ success: false,
2836
+ error: validation.errors.join(", "),
2837
+ timestamp: now
2838
+ };
2839
+ }
2840
+ const existing = this.settlements.get(request.channelId);
2841
+ if (existing && existing.state !== "completed" && existing.state !== "failed") {
2842
+ return {
2843
+ success: false,
2844
+ error: "Settlement already in progress",
2845
+ timestamp: now
2846
+ };
2847
+ }
2848
+ const settlementId = this.generateSettlementId(request.channelId);
2849
+ const challengeDeadline = now + this.settlementConfig.challengePeriod * 1e3;
2850
+ const status = {
2851
+ state: "pending",
2852
+ channelId: request.channelId,
2853
+ initiator: request.initiator,
2854
+ checkpoint: request.finalCheckpoint,
2855
+ challengeDeadline
2856
+ };
2857
+ this.settlements.set(request.channelId, status);
2858
+ this.transitionState(request.channelId, "in_progress");
2859
+ return {
2860
+ success: true,
2861
+ settlementId,
2862
+ finalBalances: {
2863
+ payer: request.finalCheckpoint.payerBalance,
2864
+ payee: request.finalCheckpoint.payeeBalance
2865
+ },
2866
+ timestamp: now
2867
+ };
2868
+ }
2869
+ /**
2870
+ * Process mutual settlement (both parties agree)
2871
+ */
2872
+ processMutual(channelId, payerSignature, payeeSignature, finalCheckpoint) {
2873
+ const now = Date.now();
2874
+ if (!payerSignature || !payeeSignature) {
2875
+ return {
2876
+ success: false,
2877
+ error: "Both signatures required for mutual settlement",
2878
+ timestamp: now
2879
+ };
2880
+ }
2881
+ const mutualCheckpoint = {
2882
+ ...finalCheckpoint,
2883
+ payerSignature,
2884
+ payeeSignature
2885
+ };
2886
+ const status = {
2887
+ state: "finalizing",
2888
+ channelId,
2889
+ initiator: "mutual",
2890
+ checkpoint: mutualCheckpoint,
2891
+ challengeDeadline: now
2892
+ // No challenge period for mutual
2893
+ };
2894
+ this.settlements.set(channelId, status);
2895
+ return this.finalize(channelId);
2896
+ }
2897
+ /**
2898
+ * Check if challenge period has elapsed
2899
+ */
2900
+ canFinalize(channelId) {
2901
+ const status = this.settlements.get(channelId);
2902
+ if (!status) {
2903
+ return { canFinalize: false, timeRemaining: -1 };
2904
+ }
2905
+ if (status.state === "completed" || status.state === "failed") {
2906
+ return { canFinalize: false, timeRemaining: 0 };
2907
+ }
2908
+ if (status.state === "disputed") {
2909
+ return { canFinalize: false, timeRemaining: -1 };
2910
+ }
2911
+ const now = Date.now();
2912
+ const timeRemaining = Math.max(0, status.challengeDeadline - now);
2913
+ return {
2914
+ canFinalize: timeRemaining === 0,
2915
+ timeRemaining
2916
+ };
2917
+ }
2918
+ /**
2919
+ * Finalize settlement after challenge period
2920
+ */
2921
+ finalize(channelId) {
2922
+ const now = Date.now();
2923
+ const status = this.settlements.get(channelId);
2924
+ if (!status) {
2925
+ return {
2926
+ success: false,
2927
+ error: "No settlement found",
2928
+ timestamp: now
2929
+ };
2930
+ }
2931
+ const { canFinalize: canFinalizeNow, timeRemaining } = this.canFinalize(channelId);
2932
+ if (!canFinalizeNow && status.state !== "finalizing") {
2933
+ return {
2934
+ success: false,
2935
+ error: `Cannot finalize: ${timeRemaining}ms remaining in challenge period`,
2936
+ timestamp: now
2937
+ };
2938
+ }
2939
+ this.transitionState(channelId, "finalizing");
2940
+ const settlementId = this.generateSettlementId(channelId);
2941
+ const mockTxHash = this.generateMockTxHash(channelId);
2942
+ const onChainSettlement = {
2943
+ channelId,
2944
+ settlementId,
2945
+ txHash: mockTxHash,
2946
+ blockNumber: Math.floor(Date.now() / 1e3),
2947
+ payerReceived: status.checkpoint.payerBalance,
2948
+ payeeReceived: status.checkpoint.payeeBalance,
2949
+ fee: this.settlementConfig.settlementFee,
2950
+ timestamp: now,
2951
+ finalized: true
2952
+ };
2953
+ this.onChainSettlements.set(channelId, onChainSettlement);
2954
+ this.transitionState(channelId, "completed");
2955
+ status.finalizedAt = now;
2956
+ status.txHash = mockTxHash;
2957
+ return {
2958
+ success: true,
2959
+ settlementId,
2960
+ finalBalances: {
2961
+ payer: status.checkpoint.payerBalance,
2962
+ payee: status.checkpoint.payeeBalance
2963
+ },
2964
+ txHash: mockTxHash,
2965
+ timestamp: now
2966
+ };
2967
+ }
2968
+ /**
2969
+ * Get settlement status
2970
+ */
2971
+ getStatus(channelId) {
2972
+ return this.settlements.get(channelId);
2973
+ }
2974
+ /**
2975
+ * Get on-chain settlement
2976
+ */
2977
+ getOnChainSettlement(channelId) {
2978
+ return this.onChainSettlements.get(channelId);
2979
+ }
2980
+ /**
2981
+ * Cancel pending settlement (before challenge period ends)
2982
+ */
2983
+ cancel(channelId, canceller, reason) {
2984
+ const status = this.settlements.get(channelId);
2985
+ if (!status) {
2986
+ return { success: false, error: "No settlement found" };
2987
+ }
2988
+ if (status.state !== "pending" && status.state !== "in_progress" && status.state !== "challenging") {
2989
+ return { success: false, error: "Cannot cancel settlement in current state" };
2990
+ }
2991
+ if (status.initiator !== canceller && status.initiator !== "mutual") {
2992
+ return { success: false, error: "Only initiator can cancel" };
2993
+ }
2994
+ this.transitionState(channelId, "failed");
2995
+ status.error = `Cancelled: ${reason}`;
2996
+ return { success: true };
2997
+ }
2998
+ /**
2999
+ * Calculate settlement amounts
3000
+ */
3001
+ calculateSettlementAmounts(checkpoint) {
3002
+ const payerBalance = BigInt(checkpoint.payerBalance);
3003
+ const payeeBalance = BigInt(checkpoint.payeeBalance);
3004
+ const fee = BigInt(this.settlementConfig.settlementFee);
3005
+ const payerReceives = payerBalance > fee ? payerBalance - fee : 0n;
3006
+ const payeeReceives = payeeBalance;
3007
+ const total = payerReceives + payeeReceives + fee;
3008
+ return {
3009
+ payerReceives: payerReceives.toString(),
3010
+ payeeReceives: payeeReceives.toString(),
3011
+ fee: fee.toString(),
3012
+ total: total.toString()
3013
+ };
3014
+ }
3015
+ /**
3016
+ * Build settlement transaction data
3017
+ */
3018
+ buildSettlementTransaction(channelId) {
3019
+ const status = this.settlements.get(channelId);
3020
+ if (!status) return null;
3021
+ const amounts = this.calculateSettlementAmounts(status.checkpoint);
3022
+ const methodId = "0x" + "settle".split("").map((c) => c.charCodeAt(0).toString(16)).join("").slice(0, 8);
3023
+ const data = [
3024
+ methodId,
3025
+ channelId.replace(/^ch_/, "").padStart(64, "0"),
3026
+ amounts.payerReceives.padStart(64, "0"),
3027
+ amounts.payeeReceives.padStart(64, "0"),
3028
+ status.checkpoint.payerSignature.replace("0x", "").padStart(128, "0")
3029
+ ].join("");
3030
+ return {
3031
+ to: "0x" + "0".repeat(40),
3032
+ // Contract address placeholder
3033
+ data,
3034
+ value: "0"
3035
+ };
3036
+ }
3037
+ validateRequest(request) {
3038
+ const errors = [];
3039
+ const cpValidation = this.checkpointManager.validate(request.finalCheckpoint);
3040
+ if (!cpValidation.valid) {
3041
+ errors.push(...cpValidation.errors);
3042
+ }
3043
+ if (!request.signature) {
3044
+ errors.push("Settlement signature is required");
3045
+ }
3046
+ const validReasons = ["mutual", "unilateral", "timeout", "dispute_resolution"];
3047
+ if (!validReasons.includes(request.reason)) {
3048
+ errors.push("Invalid settlement reason");
3049
+ }
3050
+ return { valid: errors.length === 0, errors };
3051
+ }
3052
+ transitionState(channelId, newState) {
3053
+ const status = this.settlements.get(channelId);
3054
+ if (status) {
3055
+ status.state = newState;
3056
+ }
3057
+ }
3058
+ generateSettlementId(channelId) {
3059
+ return `stl_${channelId.slice(-8)}_${Date.now().toString(36)}`;
3060
+ }
3061
+ generateMockTxHash(channelId) {
3062
+ const data = channelId + Date.now().toString();
3063
+ let hash = 0;
3064
+ for (let i = 0; i < data.length; i++) {
3065
+ const char = data.charCodeAt(i);
3066
+ hash = (hash << 5) - hash + char;
3067
+ hash = hash & hash;
3068
+ }
3069
+ return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
3070
+ }
3071
+ };
3072
+ function estimateSettlementGas(hasDispute, checkpointCount) {
3073
+ const baseGas = 100000n;
3074
+ const disputeGas = hasDispute ? 50000n : 0n;
3075
+ const checkpointGas = BigInt(checkpointCount) * 5000n;
3076
+ return (baseGas + disputeGas + checkpointGas).toString();
3077
+ }
3078
+ function verifySettlementOnChain(settlement) {
3079
+ if (!settlement.txHash || !settlement.txHash.startsWith("0x")) {
3080
+ return { verified: false, error: "Invalid transaction hash" };
3081
+ }
3082
+ if (!settlement.finalized) {
3083
+ return { verified: false, error: "Settlement not finalized" };
3084
+ }
3085
+ return { verified: true };
3086
+ }
3087
+
3088
+ // src/settlement/dispute.ts
3089
+ var DisputeManager = class {
3090
+ disputes = /* @__PURE__ */ new Map();
3091
+ config;
3092
+ settlementConfig;
3093
+ checkpointManager;
3094
+ constructor(checkpointManager, settlementConfig, config = {}) {
3095
+ this.checkpointManager = checkpointManager;
3096
+ this.settlementConfig = settlementConfig;
3097
+ this.config = {
3098
+ requireBond: config.requireBond ?? true,
3099
+ autoResolve: config.autoResolve ?? false,
3100
+ notifyParties: config.notifyParties ?? (() => {
3101
+ })
3102
+ };
3103
+ }
3104
+ /**
3105
+ * Raise a dispute
3106
+ */
3107
+ raise(request) {
3108
+ const now = Date.now();
3109
+ const validation = this.validateRequest(request);
3110
+ if (!validation.valid) {
3111
+ return { success: false, error: validation.errors.join(", ") };
3112
+ }
3113
+ const existingDispute = this.getByChannel(request.channelId);
3114
+ if (existingDispute && existingDispute.status === "pending") {
3115
+ return { success: false, error: "Dispute already pending for this channel" };
3116
+ }
3117
+ if (this.config.requireBond && this.settlementConfig.disputeBond !== "0") {
3118
+ if (!request.bond || BigInt(request.bond) < BigInt(this.settlementConfig.disputeBond)) {
3119
+ return {
3120
+ success: false,
3121
+ error: `Dispute bond of ${this.settlementConfig.disputeBond} required`
3122
+ };
3123
+ }
3124
+ }
3125
+ const disputeId = this.generateDisputeId(request.channelId);
3126
+ const responseDeadline = now + this.settlementConfig.disputeResponsePeriod * 1e3;
3127
+ const resolutionDeadline = now + this.settlementConfig.disputeResolutionPeriod * 1e3;
3128
+ const dispute = {
3129
+ id: disputeId,
3130
+ channelId: request.channelId,
3131
+ initiator: request.initiator,
3132
+ respondent: request.respondent,
3133
+ reason: request.reason,
3134
+ description: request.description,
3135
+ claimedPayerBalance: request.claimedPayerBalance,
3136
+ claimedPayeeBalance: request.claimedPayeeBalance,
3137
+ claimedCheckpoint: request.claimedCheckpoint,
3138
+ evidence: request.evidence,
3139
+ status: "pending",
3140
+ createdAt: now,
3141
+ responseDeadline,
3142
+ resolutionDeadline
3143
+ };
3144
+ this.disputes.set(disputeId, dispute);
3145
+ this.config.notifyParties(dispute, "raised");
3146
+ return { success: true, dispute };
3147
+ }
3148
+ /**
3149
+ * Respond to a dispute
3150
+ */
3151
+ respond(response) {
3152
+ const dispute = this.disputes.get(response.disputeId);
3153
+ if (!dispute) {
3154
+ return { success: false, error: "Dispute not found" };
3155
+ }
3156
+ if (dispute.status !== "pending") {
3157
+ return { success: false, error: "Dispute is not pending" };
3158
+ }
3159
+ if (response.responder !== dispute.respondent) {
3160
+ return { success: false, error: "Only the respondent can respond" };
3161
+ }
3162
+ const now = Date.now();
3163
+ if (now > dispute.responseDeadline) {
3164
+ return { success: false, error: "Response deadline has passed" };
3165
+ }
3166
+ dispute.responseEvidence = response.evidence;
3167
+ dispute.status = "under_review";
3168
+ this.config.notifyParties(dispute, "responded");
3169
+ return { success: true };
3170
+ }
3171
+ /**
3172
+ * Resolve a dispute
3173
+ */
3174
+ resolve(disputeId, winner, finalPayerBalance, finalPayeeBalance, reason) {
3175
+ const dispute = this.disputes.get(disputeId);
3176
+ if (!dispute) {
3177
+ return { success: false, error: "Dispute not found" };
3178
+ }
3179
+ if (dispute.status === "resolved" || dispute.status === "rejected") {
3180
+ return { success: false, error: "Dispute already resolved" };
3181
+ }
3182
+ const now = Date.now();
3183
+ dispute.resolution = {
3184
+ winner,
3185
+ finalPayerBalance,
3186
+ finalPayeeBalance,
3187
+ reason,
3188
+ timestamp: now
3189
+ };
3190
+ dispute.status = "resolved";
3191
+ this.config.notifyParties(dispute, "resolved");
3192
+ return { success: true };
3193
+ }
3194
+ /**
3195
+ * Reject a dispute
3196
+ */
3197
+ reject(disputeId, reason) {
3198
+ const dispute = this.disputes.get(disputeId);
3199
+ if (!dispute) {
3200
+ return { success: false, error: "Dispute not found" };
3201
+ }
3202
+ if (dispute.status === "resolved" || dispute.status === "rejected") {
3203
+ return { success: false, error: "Dispute already resolved" };
3204
+ }
3205
+ dispute.status = "rejected";
3206
+ dispute.resolution = {
3207
+ finalPayerBalance: dispute.claimedPayerBalance,
3208
+ finalPayeeBalance: dispute.claimedPayeeBalance,
3209
+ reason: `Rejected: ${reason}`,
3210
+ timestamp: Date.now()
3211
+ };
3212
+ this.config.notifyParties(dispute, "rejected");
3213
+ return { success: true };
3214
+ }
3215
+ /**
3216
+ * Handle timeout (no response)
3217
+ */
3218
+ handleTimeout(disputeId) {
3219
+ const dispute = this.disputes.get(disputeId);
3220
+ if (!dispute) {
3221
+ return { success: false };
3222
+ }
3223
+ const now = Date.now();
3224
+ if (dispute.status === "pending" && now > dispute.responseDeadline) {
3225
+ return this.resolve(
3226
+ disputeId,
3227
+ dispute.initiator,
3228
+ dispute.claimedPayerBalance,
3229
+ dispute.claimedPayeeBalance,
3230
+ "Timeout: No response from respondent"
3231
+ ).success ? { success: true, resolution: dispute.resolution } : { success: false };
3232
+ }
3233
+ if (dispute.status === "under_review" && now > dispute.resolutionDeadline) {
3234
+ dispute.status = "timeout";
3235
+ return { success: true };
3236
+ }
3237
+ return { success: false };
3238
+ }
3239
+ /**
3240
+ * Get dispute by ID
3241
+ */
3242
+ get(disputeId) {
3243
+ return this.disputes.get(disputeId);
3244
+ }
3245
+ /**
3246
+ * Get dispute by channel ID
3247
+ */
3248
+ getByChannel(channelId) {
3249
+ for (const dispute of this.disputes.values()) {
3250
+ if (dispute.channelId === channelId && dispute.status !== "resolved" && dispute.status !== "rejected") {
3251
+ return dispute;
3252
+ }
3253
+ }
3254
+ return void 0;
3255
+ }
3256
+ /**
3257
+ * Get all disputes
3258
+ */
3259
+ getAll() {
3260
+ return Array.from(this.disputes.values());
3261
+ }
3262
+ /**
3263
+ * Get disputes by status
3264
+ */
3265
+ getByStatus(status) {
3266
+ return Array.from(this.disputes.values()).filter((d) => d.status === status);
3267
+ }
3268
+ /**
3269
+ * Add evidence to existing dispute
3270
+ */
3271
+ addEvidence(disputeId, party, evidence) {
3272
+ const dispute = this.disputes.get(disputeId);
3273
+ if (!dispute) {
3274
+ return { success: false, error: "Dispute not found" };
3275
+ }
3276
+ if (dispute.status !== "pending" && dispute.status !== "under_review") {
3277
+ return { success: false, error: "Cannot add evidence to resolved dispute" };
3278
+ }
3279
+ if (party !== dispute.initiator && party !== dispute.respondent) {
3280
+ return { success: false, error: "Only parties can add evidence" };
3281
+ }
3282
+ if (party === dispute.initiator) {
3283
+ dispute.evidence.push(evidence);
3284
+ } else {
3285
+ dispute.responseEvidence = dispute.responseEvidence ?? [];
3286
+ dispute.responseEvidence.push(evidence);
3287
+ }
3288
+ return { success: true };
3289
+ }
3290
+ /**
3291
+ * Evaluate dispute based on evidence
3292
+ */
3293
+ evaluateEvidence(disputeId) {
3294
+ const dispute = this.disputes.get(disputeId);
3295
+ if (!dispute) {
3296
+ return { initiatorScore: 0, respondentScore: 0, recommendation: "Dispute not found" };
3297
+ }
3298
+ let initiatorScore = 0;
3299
+ let respondentScore = 0;
3300
+ for (const evidence of dispute.evidence) {
3301
+ if (evidence.verified) {
3302
+ initiatorScore += this.getEvidenceWeight(evidence.type);
3303
+ } else {
3304
+ initiatorScore += this.getEvidenceWeight(evidence.type) * 0.5;
3305
+ }
3306
+ }
3307
+ for (const evidence of dispute.responseEvidence ?? []) {
3308
+ if (evidence.verified) {
3309
+ respondentScore += this.getEvidenceWeight(evidence.type);
3310
+ } else {
3311
+ respondentScore += this.getEvidenceWeight(evidence.type) * 0.5;
3312
+ }
3313
+ }
3314
+ if (dispute.claimedCheckpoint) {
3315
+ const latestCheckpoint = this.checkpointManager.getLatest(dispute.channelId);
3316
+ if (latestCheckpoint) {
3317
+ const comparison = this.checkpointManager.compare(
3318
+ dispute.claimedCheckpoint,
3319
+ latestCheckpoint
3320
+ );
3321
+ if (comparison.isNewer) {
3322
+ initiatorScore += 10;
3323
+ } else {
3324
+ respondentScore += 10;
3325
+ }
3326
+ }
3327
+ }
3328
+ let recommendation;
3329
+ if (initiatorScore > respondentScore * 1.5) {
3330
+ recommendation = "Initiator has stronger evidence";
3331
+ } else if (respondentScore > initiatorScore * 1.5) {
3332
+ recommendation = "Respondent has stronger evidence";
3333
+ } else {
3334
+ recommendation = "Evidence is inconclusive - may need arbitration";
3335
+ }
3336
+ return { initiatorScore, respondentScore, recommendation };
3337
+ }
3338
+ validateRequest(request) {
3339
+ const errors = [];
3340
+ if (!request.channelId) {
3341
+ errors.push("Channel ID is required");
3342
+ }
3343
+ if (!request.initiator) {
3344
+ errors.push("Initiator address is required");
3345
+ }
3346
+ if (!request.respondent) {
3347
+ errors.push("Respondent address is required");
3348
+ }
3349
+ if (request.initiator === request.respondent) {
3350
+ errors.push("Initiator and respondent must be different");
3351
+ }
3352
+ if (!request.reason) {
3353
+ errors.push("Dispute reason is required");
3354
+ }
3355
+ if (!request.description || request.description.length < 10) {
3356
+ errors.push("Description must be at least 10 characters");
3357
+ }
3358
+ if (request.evidence.length === 0) {
3359
+ errors.push("At least one piece of evidence is required");
3360
+ }
3361
+ if (BigInt(request.claimedPayerBalance) < 0n) {
3362
+ errors.push("Claimed payer balance cannot be negative");
3363
+ }
3364
+ if (BigInt(request.claimedPayeeBalance) < 0n) {
3365
+ errors.push("Claimed payee balance cannot be negative");
3366
+ }
3367
+ return { valid: errors.length === 0, errors };
3368
+ }
3369
+ getEvidenceWeight(type) {
3370
+ const weights = {
3371
+ checkpoint: 10,
3372
+ signature: 8,
3373
+ transaction: 9,
3374
+ state_proof: 10
3375
+ };
3376
+ return weights[type] ?? 5;
3377
+ }
3378
+ generateDisputeId(channelId) {
3379
+ return `dsp_${channelId.slice(-8)}_${Date.now().toString(36)}`;
3380
+ }
3381
+ };
3382
+ function createCheckpointEvidence(checkpoint, description) {
3383
+ return {
3384
+ type: "checkpoint",
3385
+ data: JSON.stringify(checkpoint),
3386
+ description,
3387
+ timestamp: Date.now(),
3388
+ verified: false
3389
+ };
3390
+ }
3391
+ function createSignatureEvidence(signature, signedData, description) {
3392
+ return {
3393
+ type: "signature",
3394
+ data: JSON.stringify({ signature, signedData }),
3395
+ description,
3396
+ timestamp: Date.now(),
3397
+ verified: false
3398
+ };
3399
+ }
3400
+ function createTransactionEvidence(txHash, description) {
3401
+ return {
3402
+ type: "transaction",
3403
+ data: txHash,
3404
+ description,
3405
+ timestamp: Date.now(),
3406
+ verified: false
3407
+ };
3408
+ }
3409
+ export {
3410
+ BillingConfig,
3411
+ BillingManager,
3412
+ BillingPeriod,
3413
+ ChannelBalance,
3414
+ ChannelCheckpoint,
3415
+ ChannelCloseRequest,
3416
+ ChannelCloser,
3417
+ ChannelConfig,
3418
+ ChannelCreateRequest,
3419
+ ChannelDispute,
3420
+ ChannelOpener,
3421
+ ChannelParticipant,
3422
+ ChannelRecovery,
3423
+ ChannelState,
3424
+ ChannelStateMachine,
3425
+ CheckpointManager,
3426
+ CheckpointType,
3427
+ Dispute,
3428
+ DisputeEvidence,
3429
+ DisputeManager,
3430
+ DisputeReason,
3431
+ FinalSettlementManager,
3432
+ FlowController,
3433
+ FundingTransaction,
3434
+ Invoice,
3435
+ InvoiceItem,
3436
+ MeteringManager,
3437
+ MeteringRecord,
3438
+ OnChainSettlement,
3439
+ RateAdjustmentRequest,
3440
+ RateController,
3441
+ RateLimiter,
3442
+ RateType,
3443
+ RecoveryData,
3444
+ SettlementCheckpoint,
3445
+ SettlementConfig,
3446
+ SettlementRequest,
3447
+ SettlementResult,
3448
+ SettlementState,
3449
+ StateTransition,
3450
+ StreamEvent,
3451
+ StreamRate,
3452
+ StreamSession,
3453
+ StreamState,
3454
+ StreamingChannel,
3455
+ UsageMetrics,
3456
+ buildCloseTransactionData,
3457
+ calculateOptimalRate,
3458
+ calculateProRatedUsage,
3459
+ calculateRequiredDeposit,
3460
+ compareUsage,
3461
+ convertRate,
3462
+ createCheckpointEvidence,
3463
+ createFixedRateFlow,
3464
+ createSignatureEvidence,
3465
+ createStateSnapshot,
3466
+ createTieredRateFlow,
3467
+ createTransactionEvidence,
3468
+ estimateChannelDuration,
3469
+ estimateFutureBill,
3470
+ estimateSettlementGas,
3471
+ estimateUsage,
3472
+ formatCurrencyAmount,
3473
+ getChannelUtilization,
3474
+ isChannelActive,
3475
+ isChannelTerminal,
3476
+ parseCurrencyAmount,
3477
+ restoreFromSnapshot,
3478
+ signCheckpoint,
3479
+ verifyCheckpointSignature,
3480
+ verifySettlementOnChain
3481
+ };
3482
+ //# sourceMappingURL=index.js.map