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