@t402/streaming-payments 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1030 @@
1
+ // src/settlement/types.ts
2
+ import { z } from "zod";
3
+ var SettlementState = z.enum([
4
+ "pending",
5
+ // Settlement not yet initiated
6
+ "in_progress",
7
+ // Settlement process started
8
+ "challenging",
9
+ // In challenge period
10
+ "disputed",
11
+ // Dispute raised
12
+ "finalizing",
13
+ // Finalizing on-chain
14
+ "completed",
15
+ // Successfully settled
16
+ "failed"
17
+ // Settlement failed
18
+ ]);
19
+ var CheckpointType = z.enum([
20
+ "periodic",
21
+ // Regular interval checkpoint
22
+ "manual",
23
+ // Manually triggered
24
+ "balance",
25
+ // Triggered by balance threshold
26
+ "pre_close",
27
+ // Before channel close
28
+ "dispute"
29
+ // Checkpoint for dispute
30
+ ]);
31
+ var SettlementCheckpoint = z.object({
32
+ id: z.string(),
33
+ channelId: z.string(),
34
+ sequence: z.number(),
35
+ type: CheckpointType,
36
+ // Balances
37
+ payerBalance: z.string(),
38
+ payeeBalance: z.string(),
39
+ totalStreamed: z.string(),
40
+ // Signatures
41
+ payerSignature: z.string(),
42
+ payeeSignature: z.string().optional(),
43
+ // Verification
44
+ stateHash: z.string(),
45
+ merkleRoot: z.string().optional(),
46
+ // Timing
47
+ createdAt: z.number(),
48
+ expiresAt: z.number().optional(),
49
+ // On-chain reference
50
+ txHash: z.string().optional(),
51
+ blockNumber: z.number().optional()
52
+ });
53
+ var SettlementRequest = z.object({
54
+ channelId: z.string(),
55
+ initiator: z.string(),
56
+ finalCheckpoint: SettlementCheckpoint,
57
+ reason: z.enum(["mutual", "unilateral", "timeout", "dispute_resolution"]),
58
+ signature: z.string(),
59
+ metadata: z.record(z.unknown()).optional()
60
+ });
61
+ var SettlementResult = z.object({
62
+ success: z.boolean(),
63
+ settlementId: z.string().optional(),
64
+ error: z.string().optional(),
65
+ finalBalances: z.object({
66
+ payer: z.string(),
67
+ payee: z.string()
68
+ }).optional(),
69
+ txHash: z.string().optional(),
70
+ timestamp: z.number()
71
+ });
72
+ var DisputeReason = z.enum([
73
+ "invalid_checkpoint",
74
+ // Checkpoint signature or data invalid
75
+ "stale_state",
76
+ // Challenger has newer valid state
77
+ "balance_mismatch",
78
+ // Balance doesn't match expected
79
+ "unauthorized_close",
80
+ // Unauthorized party initiated close
81
+ "fraud",
82
+ // Fraudulent activity detected
83
+ "other"
84
+ // Other reason
85
+ ]);
86
+ var DisputeEvidence = z.object({
87
+ type: z.enum(["checkpoint", "signature", "transaction", "state_proof"]),
88
+ data: z.string(),
89
+ description: z.string(),
90
+ timestamp: z.number(),
91
+ verified: z.boolean().default(false)
92
+ });
93
+ var Dispute = z.object({
94
+ id: z.string(),
95
+ channelId: z.string(),
96
+ initiator: z.string(),
97
+ respondent: z.string(),
98
+ reason: DisputeReason,
99
+ description: z.string(),
100
+ // Claimed state
101
+ claimedPayerBalance: z.string(),
102
+ claimedPayeeBalance: z.string(),
103
+ claimedCheckpoint: SettlementCheckpoint.optional(),
104
+ // Evidence
105
+ evidence: z.array(DisputeEvidence),
106
+ responseEvidence: z.array(DisputeEvidence).optional(),
107
+ // Resolution
108
+ status: z.enum(["pending", "under_review", "resolved", "rejected", "timeout"]),
109
+ resolution: z.object({
110
+ winner: z.string().optional(),
111
+ finalPayerBalance: z.string(),
112
+ finalPayeeBalance: z.string(),
113
+ reason: z.string(),
114
+ timestamp: z.number()
115
+ }).optional(),
116
+ // Timing
117
+ createdAt: z.number(),
118
+ responseDeadline: z.number(),
119
+ resolutionDeadline: z.number()
120
+ });
121
+ var SettlementConfig = z.object({
122
+ challengePeriod: z.number().default(86400),
123
+ // 24 hours
124
+ disputeResponsePeriod: z.number().default(43200),
125
+ // 12 hours
126
+ disputeResolutionPeriod: z.number().default(172800),
127
+ // 48 hours
128
+ minCheckpointInterval: z.number().default(60),
129
+ // 60 seconds
130
+ maxCheckpointsStored: z.number().default(100),
131
+ settlementFee: z.string().default("0"),
132
+ disputeBond: z.string().default("0")
133
+ // Bond required to raise dispute
134
+ });
135
+ var OnChainSettlement = z.object({
136
+ channelId: z.string(),
137
+ settlementId: z.string(),
138
+ txHash: z.string(),
139
+ blockNumber: z.number(),
140
+ payerReceived: z.string(),
141
+ payeeReceived: z.string(),
142
+ fee: z.string(),
143
+ timestamp: z.number(),
144
+ finalized: z.boolean()
145
+ });
146
+
147
+ // src/settlement/checkpoint.ts
148
+ var CheckpointManager = class {
149
+ checkpoints = /* @__PURE__ */ new Map();
150
+ config;
151
+ settlementConfig;
152
+ lastCheckpointTime = /* @__PURE__ */ new Map();
153
+ constructor(settlementConfig, config = {}) {
154
+ this.settlementConfig = settlementConfig;
155
+ this.config = {
156
+ autoCheckpoint: config.autoCheckpoint ?? false,
157
+ intervalSeconds: config.intervalSeconds ?? 3600,
158
+ balanceThreshold: config.balanceThreshold ?? "1000000",
159
+ onCheckpoint: config.onCheckpoint ?? (() => {
160
+ })
161
+ };
162
+ }
163
+ /**
164
+ * Create a new checkpoint
165
+ */
166
+ create(request) {
167
+ const now = Date.now();
168
+ const channelCheckpoints = this.checkpoints.get(request.channelId) ?? [];
169
+ const expectedSequence = channelCheckpoints.length;
170
+ if (request.sequence !== expectedSequence) {
171
+ throw new Error(`Invalid sequence: expected ${expectedSequence}, got ${request.sequence}`);
172
+ }
173
+ const lastTime = this.lastCheckpointTime.get(request.channelId) ?? 0;
174
+ const elapsed = (now - lastTime) / 1e3;
175
+ if (elapsed < this.settlementConfig.minCheckpointInterval && channelCheckpoints.length > 0) {
176
+ throw new Error(`Checkpoint interval too short: ${elapsed}s < ${this.settlementConfig.minCheckpointInterval}s`);
177
+ }
178
+ const stateHash = this.generateStateHash(request);
179
+ const checkpoint = {
180
+ id: this.generateCheckpointId(request.channelId, request.sequence),
181
+ channelId: request.channelId,
182
+ sequence: request.sequence,
183
+ type: request.type ?? "manual",
184
+ payerBalance: request.payerBalance,
185
+ payeeBalance: request.payeeBalance,
186
+ totalStreamed: request.totalStreamed,
187
+ payerSignature: request.payerSignature,
188
+ payeeSignature: request.payeeSignature,
189
+ stateHash,
190
+ createdAt: now
191
+ };
192
+ channelCheckpoints.push(checkpoint);
193
+ this.checkpoints.set(request.channelId, channelCheckpoints);
194
+ this.lastCheckpointTime.set(request.channelId, now);
195
+ this.pruneCheckpoints(request.channelId);
196
+ this.config.onCheckpoint(checkpoint);
197
+ return checkpoint;
198
+ }
199
+ /**
200
+ * Get latest checkpoint for channel
201
+ */
202
+ getLatest(channelId) {
203
+ const checkpoints = this.checkpoints.get(channelId);
204
+ if (!checkpoints || checkpoints.length === 0) return void 0;
205
+ return checkpoints[checkpoints.length - 1];
206
+ }
207
+ /**
208
+ * Get checkpoint by sequence
209
+ */
210
+ getBySequence(channelId, sequence) {
211
+ const checkpoints = this.checkpoints.get(channelId);
212
+ if (!checkpoints) return void 0;
213
+ return checkpoints.find((cp) => cp.sequence === sequence);
214
+ }
215
+ /**
216
+ * Get all checkpoints for channel
217
+ */
218
+ getAll(channelId) {
219
+ return [...this.checkpoints.get(channelId) ?? []];
220
+ }
221
+ /**
222
+ * Validate checkpoint
223
+ */
224
+ validate(checkpoint) {
225
+ const errors = [];
226
+ const payerBalance = BigInt(checkpoint.payerBalance);
227
+ const payeeBalance = BigInt(checkpoint.payeeBalance);
228
+ const totalStreamed = BigInt(checkpoint.totalStreamed);
229
+ if (payerBalance < 0n) {
230
+ errors.push("Payer balance cannot be negative");
231
+ }
232
+ if (payeeBalance < 0n) {
233
+ errors.push("Payee balance cannot be negative");
234
+ }
235
+ if (totalStreamed < 0n) {
236
+ errors.push("Total streamed cannot be negative");
237
+ }
238
+ if (payeeBalance !== totalStreamed) {
239
+ errors.push("Total streamed should equal payee balance");
240
+ }
241
+ if (!checkpoint.payerSignature) {
242
+ errors.push("Payer signature is required");
243
+ }
244
+ const expectedHash = this.generateStateHash({
245
+ channelId: checkpoint.channelId,
246
+ sequence: checkpoint.sequence,
247
+ payerBalance: checkpoint.payerBalance,
248
+ payeeBalance: checkpoint.payeeBalance,
249
+ totalStreamed: checkpoint.totalStreamed,
250
+ payerSignature: checkpoint.payerSignature
251
+ });
252
+ if (checkpoint.stateHash !== expectedHash) {
253
+ errors.push("State hash mismatch");
254
+ }
255
+ if (checkpoint.createdAt > Date.now()) {
256
+ errors.push("Checkpoint timestamp is in the future");
257
+ }
258
+ return { valid: errors.length === 0, errors };
259
+ }
260
+ /**
261
+ * Compare two checkpoints
262
+ */
263
+ compare(a, b) {
264
+ const isNewer = a.sequence > b.sequence || a.sequence === b.sequence && a.createdAt > b.createdAt;
265
+ const payerDiff = BigInt(a.payerBalance) - BigInt(b.payerBalance);
266
+ const payeeDiff = BigInt(a.payeeBalance) - BigInt(b.payeeBalance);
267
+ return {
268
+ isNewer,
269
+ balanceDifference: {
270
+ payer: payerDiff.toString(),
271
+ payee: payeeDiff.toString()
272
+ }
273
+ };
274
+ }
275
+ /**
276
+ * Check if checkpoint is needed
277
+ */
278
+ needsCheckpoint(channelId, currentPayeeBalance) {
279
+ const lastCheckpoint = this.getLatest(channelId);
280
+ const now = Date.now();
281
+ if (!lastCheckpoint) {
282
+ return { needed: true, reason: "No existing checkpoint" };
283
+ }
284
+ const elapsed = (now - lastCheckpoint.createdAt) / 1e3;
285
+ if (elapsed >= this.config.intervalSeconds) {
286
+ return { needed: true, reason: "Interval elapsed" };
287
+ }
288
+ const balanceChange = BigInt(currentPayeeBalance) - BigInt(lastCheckpoint.payeeBalance);
289
+ if (balanceChange >= BigInt(this.config.balanceThreshold)) {
290
+ return { needed: true, reason: "Balance threshold exceeded" };
291
+ }
292
+ return { needed: false, reason: "" };
293
+ }
294
+ /**
295
+ * Build merkle root from checkpoint history
296
+ */
297
+ buildMerkleRoot(channelId) {
298
+ const checkpoints = this.checkpoints.get(channelId);
299
+ if (!checkpoints || checkpoints.length === 0) {
300
+ return "0x" + "0".repeat(64);
301
+ }
302
+ const hashes = checkpoints.map((cp) => cp.stateHash);
303
+ return this.hashArray(hashes);
304
+ }
305
+ /**
306
+ * Verify checkpoint is in merkle tree
307
+ */
308
+ verifyMerkleProof(checkpoint, merkleRoot, _proof) {
309
+ const channelRoot = this.buildMerkleRoot(checkpoint.channelId);
310
+ return channelRoot === merkleRoot;
311
+ }
312
+ /**
313
+ * Export checkpoints for backup
314
+ */
315
+ export(channelId) {
316
+ const checkpoints = this.checkpoints.get(channelId) ?? [];
317
+ return JSON.stringify({
318
+ channelId,
319
+ checkpoints,
320
+ exportedAt: Date.now()
321
+ });
322
+ }
323
+ /**
324
+ * Import checkpoints from backup
325
+ */
326
+ import(data) {
327
+ try {
328
+ const parsed = JSON.parse(data);
329
+ const { channelId, checkpoints } = parsed;
330
+ let imported = 0;
331
+ const existing = this.checkpoints.get(channelId) ?? [];
332
+ for (const cp of checkpoints) {
333
+ const validation = this.validate(cp);
334
+ if (validation.valid) {
335
+ const exists = existing.some((e) => e.sequence === cp.sequence);
336
+ if (!exists) {
337
+ existing.push(cp);
338
+ imported++;
339
+ }
340
+ }
341
+ }
342
+ existing.sort((a, b) => a.sequence - b.sequence);
343
+ this.checkpoints.set(channelId, existing);
344
+ return { success: true, imported };
345
+ } catch {
346
+ return { success: false, imported: 0 };
347
+ }
348
+ }
349
+ /**
350
+ * Clear checkpoints for channel
351
+ */
352
+ clear(channelId) {
353
+ this.checkpoints.delete(channelId);
354
+ this.lastCheckpointTime.delete(channelId);
355
+ }
356
+ generateCheckpointId(channelId, sequence) {
357
+ return `cp_${channelId.slice(-8)}_${sequence}_${Date.now().toString(36)}`;
358
+ }
359
+ generateStateHash(request) {
360
+ const data = [
361
+ request.channelId,
362
+ request.sequence.toString(),
363
+ request.payerBalance,
364
+ request.payeeBalance,
365
+ request.totalStreamed
366
+ ].join(":");
367
+ let hash = 0;
368
+ for (let i = 0; i < data.length; i++) {
369
+ const char = data.charCodeAt(i);
370
+ hash = (hash << 5) - hash + char;
371
+ hash = hash & hash;
372
+ }
373
+ return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
374
+ }
375
+ hashArray(hashes) {
376
+ const combined = hashes.join("");
377
+ let hash = 0;
378
+ for (let i = 0; i < combined.length; i++) {
379
+ const char = combined.charCodeAt(i);
380
+ hash = (hash << 5) - hash + char;
381
+ hash = hash & hash;
382
+ }
383
+ return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
384
+ }
385
+ pruneCheckpoints(channelId) {
386
+ const checkpoints = this.checkpoints.get(channelId);
387
+ if (!checkpoints) return;
388
+ const maxStored = this.settlementConfig.maxCheckpointsStored;
389
+ if (checkpoints.length <= maxStored) return;
390
+ const first = checkpoints[0];
391
+ const recent = checkpoints.slice(-(maxStored - 1));
392
+ this.checkpoints.set(channelId, [first, ...recent]);
393
+ }
394
+ };
395
+ function signCheckpoint(checkpoint, _privateKey) {
396
+ const dataToSign = [
397
+ checkpoint.channelId,
398
+ checkpoint.sequence.toString(),
399
+ checkpoint.payerBalance,
400
+ checkpoint.payeeBalance,
401
+ checkpoint.totalStreamed,
402
+ checkpoint.createdAt.toString()
403
+ ].join(":");
404
+ let hash = 0;
405
+ for (let i = 0; i < dataToSign.length; i++) {
406
+ const char = dataToSign.charCodeAt(i);
407
+ hash = (hash << 5) - hash + char;
408
+ hash = hash & hash;
409
+ }
410
+ return "0x" + Math.abs(hash).toString(16).padStart(128, "0");
411
+ }
412
+ function verifyCheckpointSignature(_checkpoint, signature, _publicKey) {
413
+ return signature.startsWith("0x") && signature.length === 130;
414
+ }
415
+
416
+ // src/settlement/final.ts
417
+ var FinalSettlementManager = class {
418
+ settlements = /* @__PURE__ */ new Map();
419
+ checkpointManager;
420
+ settlementConfig;
421
+ onChainSettlements = /* @__PURE__ */ new Map();
422
+ constructor(checkpointManager, settlementConfig, _config = {}) {
423
+ this.checkpointManager = checkpointManager;
424
+ this.settlementConfig = settlementConfig;
425
+ }
426
+ /**
427
+ * Initiate settlement
428
+ */
429
+ initiate(request) {
430
+ const now = Date.now();
431
+ const validation = this.validateRequest(request);
432
+ if (!validation.valid) {
433
+ return {
434
+ success: false,
435
+ error: validation.errors.join(", "),
436
+ timestamp: now
437
+ };
438
+ }
439
+ const existing = this.settlements.get(request.channelId);
440
+ if (existing && existing.state !== "completed" && existing.state !== "failed") {
441
+ return {
442
+ success: false,
443
+ error: "Settlement already in progress",
444
+ timestamp: now
445
+ };
446
+ }
447
+ const settlementId = this.generateSettlementId(request.channelId);
448
+ const challengeDeadline = now + this.settlementConfig.challengePeriod * 1e3;
449
+ const status = {
450
+ state: "pending",
451
+ channelId: request.channelId,
452
+ initiator: request.initiator,
453
+ checkpoint: request.finalCheckpoint,
454
+ challengeDeadline
455
+ };
456
+ this.settlements.set(request.channelId, status);
457
+ this.transitionState(request.channelId, "in_progress");
458
+ return {
459
+ success: true,
460
+ settlementId,
461
+ finalBalances: {
462
+ payer: request.finalCheckpoint.payerBalance,
463
+ payee: request.finalCheckpoint.payeeBalance
464
+ },
465
+ timestamp: now
466
+ };
467
+ }
468
+ /**
469
+ * Process mutual settlement (both parties agree)
470
+ */
471
+ processMutual(channelId, payerSignature, payeeSignature, finalCheckpoint) {
472
+ const now = Date.now();
473
+ if (!payerSignature || !payeeSignature) {
474
+ return {
475
+ success: false,
476
+ error: "Both signatures required for mutual settlement",
477
+ timestamp: now
478
+ };
479
+ }
480
+ const mutualCheckpoint = {
481
+ ...finalCheckpoint,
482
+ payerSignature,
483
+ payeeSignature
484
+ };
485
+ const status = {
486
+ state: "finalizing",
487
+ channelId,
488
+ initiator: "mutual",
489
+ checkpoint: mutualCheckpoint,
490
+ challengeDeadline: now
491
+ // No challenge period for mutual
492
+ };
493
+ this.settlements.set(channelId, status);
494
+ return this.finalize(channelId);
495
+ }
496
+ /**
497
+ * Check if challenge period has elapsed
498
+ */
499
+ canFinalize(channelId) {
500
+ const status = this.settlements.get(channelId);
501
+ if (!status) {
502
+ return { canFinalize: false, timeRemaining: -1 };
503
+ }
504
+ if (status.state === "completed" || status.state === "failed") {
505
+ return { canFinalize: false, timeRemaining: 0 };
506
+ }
507
+ if (status.state === "disputed") {
508
+ return { canFinalize: false, timeRemaining: -1 };
509
+ }
510
+ const now = Date.now();
511
+ const timeRemaining = Math.max(0, status.challengeDeadline - now);
512
+ return {
513
+ canFinalize: timeRemaining === 0,
514
+ timeRemaining
515
+ };
516
+ }
517
+ /**
518
+ * Finalize settlement after challenge period
519
+ */
520
+ finalize(channelId) {
521
+ const now = Date.now();
522
+ const status = this.settlements.get(channelId);
523
+ if (!status) {
524
+ return {
525
+ success: false,
526
+ error: "No settlement found",
527
+ timestamp: now
528
+ };
529
+ }
530
+ const { canFinalize: canFinalizeNow, timeRemaining } = this.canFinalize(channelId);
531
+ if (!canFinalizeNow && status.state !== "finalizing") {
532
+ return {
533
+ success: false,
534
+ error: `Cannot finalize: ${timeRemaining}ms remaining in challenge period`,
535
+ timestamp: now
536
+ };
537
+ }
538
+ this.transitionState(channelId, "finalizing");
539
+ const settlementId = this.generateSettlementId(channelId);
540
+ const mockTxHash = this.generateMockTxHash(channelId);
541
+ const onChainSettlement = {
542
+ channelId,
543
+ settlementId,
544
+ txHash: mockTxHash,
545
+ blockNumber: Math.floor(Date.now() / 1e3),
546
+ payerReceived: status.checkpoint.payerBalance,
547
+ payeeReceived: status.checkpoint.payeeBalance,
548
+ fee: this.settlementConfig.settlementFee,
549
+ timestamp: now,
550
+ finalized: true
551
+ };
552
+ this.onChainSettlements.set(channelId, onChainSettlement);
553
+ this.transitionState(channelId, "completed");
554
+ status.finalizedAt = now;
555
+ status.txHash = mockTxHash;
556
+ return {
557
+ success: true,
558
+ settlementId,
559
+ finalBalances: {
560
+ payer: status.checkpoint.payerBalance,
561
+ payee: status.checkpoint.payeeBalance
562
+ },
563
+ txHash: mockTxHash,
564
+ timestamp: now
565
+ };
566
+ }
567
+ /**
568
+ * Get settlement status
569
+ */
570
+ getStatus(channelId) {
571
+ return this.settlements.get(channelId);
572
+ }
573
+ /**
574
+ * Get on-chain settlement
575
+ */
576
+ getOnChainSettlement(channelId) {
577
+ return this.onChainSettlements.get(channelId);
578
+ }
579
+ /**
580
+ * Cancel pending settlement (before challenge period ends)
581
+ */
582
+ cancel(channelId, canceller, reason) {
583
+ const status = this.settlements.get(channelId);
584
+ if (!status) {
585
+ return { success: false, error: "No settlement found" };
586
+ }
587
+ if (status.state !== "pending" && status.state !== "in_progress" && status.state !== "challenging") {
588
+ return { success: false, error: "Cannot cancel settlement in current state" };
589
+ }
590
+ if (status.initiator !== canceller && status.initiator !== "mutual") {
591
+ return { success: false, error: "Only initiator can cancel" };
592
+ }
593
+ this.transitionState(channelId, "failed");
594
+ status.error = `Cancelled: ${reason}`;
595
+ return { success: true };
596
+ }
597
+ /**
598
+ * Calculate settlement amounts
599
+ */
600
+ calculateSettlementAmounts(checkpoint) {
601
+ const payerBalance = BigInt(checkpoint.payerBalance);
602
+ const payeeBalance = BigInt(checkpoint.payeeBalance);
603
+ const fee = BigInt(this.settlementConfig.settlementFee);
604
+ const payerReceives = payerBalance > fee ? payerBalance - fee : 0n;
605
+ const payeeReceives = payeeBalance;
606
+ const total = payerReceives + payeeReceives + fee;
607
+ return {
608
+ payerReceives: payerReceives.toString(),
609
+ payeeReceives: payeeReceives.toString(),
610
+ fee: fee.toString(),
611
+ total: total.toString()
612
+ };
613
+ }
614
+ /**
615
+ * Build settlement transaction data
616
+ */
617
+ buildSettlementTransaction(channelId) {
618
+ const status = this.settlements.get(channelId);
619
+ if (!status) return null;
620
+ const amounts = this.calculateSettlementAmounts(status.checkpoint);
621
+ const methodId = "0x" + "settle".split("").map((c) => c.charCodeAt(0).toString(16)).join("").slice(0, 8);
622
+ const data = [
623
+ methodId,
624
+ channelId.replace(/^ch_/, "").padStart(64, "0"),
625
+ amounts.payerReceives.padStart(64, "0"),
626
+ amounts.payeeReceives.padStart(64, "0"),
627
+ status.checkpoint.payerSignature.replace("0x", "").padStart(128, "0")
628
+ ].join("");
629
+ return {
630
+ to: "0x" + "0".repeat(40),
631
+ // Contract address placeholder
632
+ data,
633
+ value: "0"
634
+ };
635
+ }
636
+ validateRequest(request) {
637
+ const errors = [];
638
+ const cpValidation = this.checkpointManager.validate(request.finalCheckpoint);
639
+ if (!cpValidation.valid) {
640
+ errors.push(...cpValidation.errors);
641
+ }
642
+ if (!request.signature) {
643
+ errors.push("Settlement signature is required");
644
+ }
645
+ const validReasons = ["mutual", "unilateral", "timeout", "dispute_resolution"];
646
+ if (!validReasons.includes(request.reason)) {
647
+ errors.push("Invalid settlement reason");
648
+ }
649
+ return { valid: errors.length === 0, errors };
650
+ }
651
+ transitionState(channelId, newState) {
652
+ const status = this.settlements.get(channelId);
653
+ if (status) {
654
+ status.state = newState;
655
+ }
656
+ }
657
+ generateSettlementId(channelId) {
658
+ return `stl_${channelId.slice(-8)}_${Date.now().toString(36)}`;
659
+ }
660
+ generateMockTxHash(channelId) {
661
+ const data = channelId + Date.now().toString();
662
+ let hash = 0;
663
+ for (let i = 0; i < data.length; i++) {
664
+ const char = data.charCodeAt(i);
665
+ hash = (hash << 5) - hash + char;
666
+ hash = hash & hash;
667
+ }
668
+ return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
669
+ }
670
+ };
671
+ function estimateSettlementGas(hasDispute, checkpointCount) {
672
+ const baseGas = 100000n;
673
+ const disputeGas = hasDispute ? 50000n : 0n;
674
+ const checkpointGas = BigInt(checkpointCount) * 5000n;
675
+ return (baseGas + disputeGas + checkpointGas).toString();
676
+ }
677
+ function verifySettlementOnChain(settlement) {
678
+ if (!settlement.txHash || !settlement.txHash.startsWith("0x")) {
679
+ return { verified: false, error: "Invalid transaction hash" };
680
+ }
681
+ if (!settlement.finalized) {
682
+ return { verified: false, error: "Settlement not finalized" };
683
+ }
684
+ return { verified: true };
685
+ }
686
+
687
+ // src/settlement/dispute.ts
688
+ var DisputeManager = class {
689
+ disputes = /* @__PURE__ */ new Map();
690
+ config;
691
+ settlementConfig;
692
+ checkpointManager;
693
+ constructor(checkpointManager, settlementConfig, config = {}) {
694
+ this.checkpointManager = checkpointManager;
695
+ this.settlementConfig = settlementConfig;
696
+ this.config = {
697
+ requireBond: config.requireBond ?? true,
698
+ autoResolve: config.autoResolve ?? false,
699
+ notifyParties: config.notifyParties ?? (() => {
700
+ })
701
+ };
702
+ }
703
+ /**
704
+ * Raise a dispute
705
+ */
706
+ raise(request) {
707
+ const now = Date.now();
708
+ const validation = this.validateRequest(request);
709
+ if (!validation.valid) {
710
+ return { success: false, error: validation.errors.join(", ") };
711
+ }
712
+ const existingDispute = this.getByChannel(request.channelId);
713
+ if (existingDispute && existingDispute.status === "pending") {
714
+ return { success: false, error: "Dispute already pending for this channel" };
715
+ }
716
+ if (this.config.requireBond && this.settlementConfig.disputeBond !== "0") {
717
+ if (!request.bond || BigInt(request.bond) < BigInt(this.settlementConfig.disputeBond)) {
718
+ return {
719
+ success: false,
720
+ error: `Dispute bond of ${this.settlementConfig.disputeBond} required`
721
+ };
722
+ }
723
+ }
724
+ const disputeId = this.generateDisputeId(request.channelId);
725
+ const responseDeadline = now + this.settlementConfig.disputeResponsePeriod * 1e3;
726
+ const resolutionDeadline = now + this.settlementConfig.disputeResolutionPeriod * 1e3;
727
+ const dispute = {
728
+ id: disputeId,
729
+ channelId: request.channelId,
730
+ initiator: request.initiator,
731
+ respondent: request.respondent,
732
+ reason: request.reason,
733
+ description: request.description,
734
+ claimedPayerBalance: request.claimedPayerBalance,
735
+ claimedPayeeBalance: request.claimedPayeeBalance,
736
+ claimedCheckpoint: request.claimedCheckpoint,
737
+ evidence: request.evidence,
738
+ status: "pending",
739
+ createdAt: now,
740
+ responseDeadline,
741
+ resolutionDeadline
742
+ };
743
+ this.disputes.set(disputeId, dispute);
744
+ this.config.notifyParties(dispute, "raised");
745
+ return { success: true, dispute };
746
+ }
747
+ /**
748
+ * Respond to a dispute
749
+ */
750
+ respond(response) {
751
+ const dispute = this.disputes.get(response.disputeId);
752
+ if (!dispute) {
753
+ return { success: false, error: "Dispute not found" };
754
+ }
755
+ if (dispute.status !== "pending") {
756
+ return { success: false, error: "Dispute is not pending" };
757
+ }
758
+ if (response.responder !== dispute.respondent) {
759
+ return { success: false, error: "Only the respondent can respond" };
760
+ }
761
+ const now = Date.now();
762
+ if (now > dispute.responseDeadline) {
763
+ return { success: false, error: "Response deadline has passed" };
764
+ }
765
+ dispute.responseEvidence = response.evidence;
766
+ dispute.status = "under_review";
767
+ this.config.notifyParties(dispute, "responded");
768
+ return { success: true };
769
+ }
770
+ /**
771
+ * Resolve a dispute
772
+ */
773
+ resolve(disputeId, winner, finalPayerBalance, finalPayeeBalance, reason) {
774
+ const dispute = this.disputes.get(disputeId);
775
+ if (!dispute) {
776
+ return { success: false, error: "Dispute not found" };
777
+ }
778
+ if (dispute.status === "resolved" || dispute.status === "rejected") {
779
+ return { success: false, error: "Dispute already resolved" };
780
+ }
781
+ const now = Date.now();
782
+ dispute.resolution = {
783
+ winner,
784
+ finalPayerBalance,
785
+ finalPayeeBalance,
786
+ reason,
787
+ timestamp: now
788
+ };
789
+ dispute.status = "resolved";
790
+ this.config.notifyParties(dispute, "resolved");
791
+ return { success: true };
792
+ }
793
+ /**
794
+ * Reject a dispute
795
+ */
796
+ reject(disputeId, reason) {
797
+ const dispute = this.disputes.get(disputeId);
798
+ if (!dispute) {
799
+ return { success: false, error: "Dispute not found" };
800
+ }
801
+ if (dispute.status === "resolved" || dispute.status === "rejected") {
802
+ return { success: false, error: "Dispute already resolved" };
803
+ }
804
+ dispute.status = "rejected";
805
+ dispute.resolution = {
806
+ finalPayerBalance: dispute.claimedPayerBalance,
807
+ finalPayeeBalance: dispute.claimedPayeeBalance,
808
+ reason: `Rejected: ${reason}`,
809
+ timestamp: Date.now()
810
+ };
811
+ this.config.notifyParties(dispute, "rejected");
812
+ return { success: true };
813
+ }
814
+ /**
815
+ * Handle timeout (no response)
816
+ */
817
+ handleTimeout(disputeId) {
818
+ const dispute = this.disputes.get(disputeId);
819
+ if (!dispute) {
820
+ return { success: false };
821
+ }
822
+ const now = Date.now();
823
+ if (dispute.status === "pending" && now > dispute.responseDeadline) {
824
+ return this.resolve(
825
+ disputeId,
826
+ dispute.initiator,
827
+ dispute.claimedPayerBalance,
828
+ dispute.claimedPayeeBalance,
829
+ "Timeout: No response from respondent"
830
+ ).success ? { success: true, resolution: dispute.resolution } : { success: false };
831
+ }
832
+ if (dispute.status === "under_review" && now > dispute.resolutionDeadline) {
833
+ dispute.status = "timeout";
834
+ return { success: true };
835
+ }
836
+ return { success: false };
837
+ }
838
+ /**
839
+ * Get dispute by ID
840
+ */
841
+ get(disputeId) {
842
+ return this.disputes.get(disputeId);
843
+ }
844
+ /**
845
+ * Get dispute by channel ID
846
+ */
847
+ getByChannel(channelId) {
848
+ for (const dispute of this.disputes.values()) {
849
+ if (dispute.channelId === channelId && dispute.status !== "resolved" && dispute.status !== "rejected") {
850
+ return dispute;
851
+ }
852
+ }
853
+ return void 0;
854
+ }
855
+ /**
856
+ * Get all disputes
857
+ */
858
+ getAll() {
859
+ return Array.from(this.disputes.values());
860
+ }
861
+ /**
862
+ * Get disputes by status
863
+ */
864
+ getByStatus(status) {
865
+ return Array.from(this.disputes.values()).filter((d) => d.status === status);
866
+ }
867
+ /**
868
+ * Add evidence to existing dispute
869
+ */
870
+ addEvidence(disputeId, party, evidence) {
871
+ const dispute = this.disputes.get(disputeId);
872
+ if (!dispute) {
873
+ return { success: false, error: "Dispute not found" };
874
+ }
875
+ if (dispute.status !== "pending" && dispute.status !== "under_review") {
876
+ return { success: false, error: "Cannot add evidence to resolved dispute" };
877
+ }
878
+ if (party !== dispute.initiator && party !== dispute.respondent) {
879
+ return { success: false, error: "Only parties can add evidence" };
880
+ }
881
+ if (party === dispute.initiator) {
882
+ dispute.evidence.push(evidence);
883
+ } else {
884
+ dispute.responseEvidence = dispute.responseEvidence ?? [];
885
+ dispute.responseEvidence.push(evidence);
886
+ }
887
+ return { success: true };
888
+ }
889
+ /**
890
+ * Evaluate dispute based on evidence
891
+ */
892
+ evaluateEvidence(disputeId) {
893
+ const dispute = this.disputes.get(disputeId);
894
+ if (!dispute) {
895
+ return { initiatorScore: 0, respondentScore: 0, recommendation: "Dispute not found" };
896
+ }
897
+ let initiatorScore = 0;
898
+ let respondentScore = 0;
899
+ for (const evidence of dispute.evidence) {
900
+ if (evidence.verified) {
901
+ initiatorScore += this.getEvidenceWeight(evidence.type);
902
+ } else {
903
+ initiatorScore += this.getEvidenceWeight(evidence.type) * 0.5;
904
+ }
905
+ }
906
+ for (const evidence of dispute.responseEvidence ?? []) {
907
+ if (evidence.verified) {
908
+ respondentScore += this.getEvidenceWeight(evidence.type);
909
+ } else {
910
+ respondentScore += this.getEvidenceWeight(evidence.type) * 0.5;
911
+ }
912
+ }
913
+ if (dispute.claimedCheckpoint) {
914
+ const latestCheckpoint = this.checkpointManager.getLatest(dispute.channelId);
915
+ if (latestCheckpoint) {
916
+ const comparison = this.checkpointManager.compare(
917
+ dispute.claimedCheckpoint,
918
+ latestCheckpoint
919
+ );
920
+ if (comparison.isNewer) {
921
+ initiatorScore += 10;
922
+ } else {
923
+ respondentScore += 10;
924
+ }
925
+ }
926
+ }
927
+ let recommendation;
928
+ if (initiatorScore > respondentScore * 1.5) {
929
+ recommendation = "Initiator has stronger evidence";
930
+ } else if (respondentScore > initiatorScore * 1.5) {
931
+ recommendation = "Respondent has stronger evidence";
932
+ } else {
933
+ recommendation = "Evidence is inconclusive - may need arbitration";
934
+ }
935
+ return { initiatorScore, respondentScore, recommendation };
936
+ }
937
+ validateRequest(request) {
938
+ const errors = [];
939
+ if (!request.channelId) {
940
+ errors.push("Channel ID is required");
941
+ }
942
+ if (!request.initiator) {
943
+ errors.push("Initiator address is required");
944
+ }
945
+ if (!request.respondent) {
946
+ errors.push("Respondent address is required");
947
+ }
948
+ if (request.initiator === request.respondent) {
949
+ errors.push("Initiator and respondent must be different");
950
+ }
951
+ if (!request.reason) {
952
+ errors.push("Dispute reason is required");
953
+ }
954
+ if (!request.description || request.description.length < 10) {
955
+ errors.push("Description must be at least 10 characters");
956
+ }
957
+ if (request.evidence.length === 0) {
958
+ errors.push("At least one piece of evidence is required");
959
+ }
960
+ if (BigInt(request.claimedPayerBalance) < 0n) {
961
+ errors.push("Claimed payer balance cannot be negative");
962
+ }
963
+ if (BigInt(request.claimedPayeeBalance) < 0n) {
964
+ errors.push("Claimed payee balance cannot be negative");
965
+ }
966
+ return { valid: errors.length === 0, errors };
967
+ }
968
+ getEvidenceWeight(type) {
969
+ const weights = {
970
+ checkpoint: 10,
971
+ signature: 8,
972
+ transaction: 9,
973
+ state_proof: 10
974
+ };
975
+ return weights[type] ?? 5;
976
+ }
977
+ generateDisputeId(channelId) {
978
+ return `dsp_${channelId.slice(-8)}_${Date.now().toString(36)}`;
979
+ }
980
+ };
981
+ function createCheckpointEvidence(checkpoint, description) {
982
+ return {
983
+ type: "checkpoint",
984
+ data: JSON.stringify(checkpoint),
985
+ description,
986
+ timestamp: Date.now(),
987
+ verified: false
988
+ };
989
+ }
990
+ function createSignatureEvidence(signature, signedData, description) {
991
+ return {
992
+ type: "signature",
993
+ data: JSON.stringify({ signature, signedData }),
994
+ description,
995
+ timestamp: Date.now(),
996
+ verified: false
997
+ };
998
+ }
999
+ function createTransactionEvidence(txHash, description) {
1000
+ return {
1001
+ type: "transaction",
1002
+ data: txHash,
1003
+ description,
1004
+ timestamp: Date.now(),
1005
+ verified: false
1006
+ };
1007
+ }
1008
+ export {
1009
+ CheckpointManager,
1010
+ CheckpointType,
1011
+ Dispute,
1012
+ DisputeEvidence,
1013
+ DisputeManager,
1014
+ DisputeReason,
1015
+ FinalSettlementManager,
1016
+ OnChainSettlement,
1017
+ SettlementCheckpoint,
1018
+ SettlementConfig,
1019
+ SettlementRequest,
1020
+ SettlementResult,
1021
+ SettlementState,
1022
+ createCheckpointEvidence,
1023
+ createSignatureEvidence,
1024
+ createTransactionEvidence,
1025
+ estimateSettlementGas,
1026
+ signCheckpoint,
1027
+ verifyCheckpointSignature,
1028
+ verifySettlementOnChain
1029
+ };
1030
+ //# sourceMappingURL=index.js.map