@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/README.md +422 -0
- package/dist/channels/index.d.ts +1560 -0
- package/dist/channels/index.js +1135 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3482 -0
- package/dist/index.js.map +1 -0
- package/dist/settlement/index.d.ts +867 -0
- package/dist/settlement/index.js +1030 -0
- package/dist/settlement/index.js.map +1 -0
- package/dist/streaming/index.d.ts +1004 -0
- package/dist/streaming/index.js +1321 -0
- package/dist/streaming/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -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
|