@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,3482 @@
|
|
|
1
|
+
// src/channels/types.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var ChannelState = z.enum([
|
|
4
|
+
"created",
|
|
5
|
+
// Channel created but not funded
|
|
6
|
+
"funding",
|
|
7
|
+
// Waiting for funding transaction
|
|
8
|
+
"open",
|
|
9
|
+
// Channel is active and streaming
|
|
10
|
+
"paused",
|
|
11
|
+
// Channel temporarily paused
|
|
12
|
+
"closing",
|
|
13
|
+
// Channel close initiated
|
|
14
|
+
"disputing",
|
|
15
|
+
// Dispute in progress
|
|
16
|
+
"closed",
|
|
17
|
+
// Channel fully closed
|
|
18
|
+
"expired"
|
|
19
|
+
// Channel expired without proper close
|
|
20
|
+
]);
|
|
21
|
+
var ChannelConfig = z.object({
|
|
22
|
+
// Funding requirements
|
|
23
|
+
minDeposit: z.string().default("1000000"),
|
|
24
|
+
// Minimum deposit (in smallest units)
|
|
25
|
+
maxDeposit: z.string().optional(),
|
|
26
|
+
// Timing
|
|
27
|
+
challengePeriod: z.number().default(86400),
|
|
28
|
+
// Dispute window in seconds (24h default)
|
|
29
|
+
expirationTime: z.number().optional(),
|
|
30
|
+
// Optional expiration timestamp
|
|
31
|
+
// Checkpointing
|
|
32
|
+
checkpointInterval: z.number().default(3600),
|
|
33
|
+
// Checkpoint every hour
|
|
34
|
+
minCheckpointAmount: z.string().default("100000"),
|
|
35
|
+
// Min amount change to checkpoint
|
|
36
|
+
// Fees
|
|
37
|
+
channelFee: z.string().default("0"),
|
|
38
|
+
// One-time channel opening fee
|
|
39
|
+
settlementFee: z.string().default("0")
|
|
40
|
+
// Fee for final settlement
|
|
41
|
+
});
|
|
42
|
+
var ChannelParticipant = z.object({
|
|
43
|
+
address: z.string(),
|
|
44
|
+
role: z.enum(["payer", "payee"]),
|
|
45
|
+
publicKey: z.string().optional()
|
|
46
|
+
// For off-chain signature verification
|
|
47
|
+
});
|
|
48
|
+
var ChannelBalance = z.object({
|
|
49
|
+
payer: z.string(),
|
|
50
|
+
// Payer's remaining balance
|
|
51
|
+
payee: z.string(),
|
|
52
|
+
// Payee's accrued balance
|
|
53
|
+
total: z.string(),
|
|
54
|
+
// Total channel capacity
|
|
55
|
+
locked: z.string().default("0")
|
|
56
|
+
// Amount locked in disputes
|
|
57
|
+
});
|
|
58
|
+
var ChannelCheckpoint = z.object({
|
|
59
|
+
channelId: z.string(),
|
|
60
|
+
sequence: z.number(),
|
|
61
|
+
// Monotonically increasing sequence number
|
|
62
|
+
timestamp: z.number(),
|
|
63
|
+
balance: ChannelBalance,
|
|
64
|
+
amountStreamed: z.string(),
|
|
65
|
+
// Total amount streamed so far
|
|
66
|
+
payerSignature: z.string(),
|
|
67
|
+
payeeSignature: z.string().optional(),
|
|
68
|
+
// Payee signature for mutual checkpoints
|
|
69
|
+
stateRoot: z.string().optional()
|
|
70
|
+
// Merkle root of channel state
|
|
71
|
+
});
|
|
72
|
+
var StreamingChannel = z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
state: ChannelState,
|
|
75
|
+
chain: z.string(),
|
|
76
|
+
// CAIP-2 chain ID
|
|
77
|
+
asset: z.string(),
|
|
78
|
+
// Token contract address
|
|
79
|
+
// Participants
|
|
80
|
+
payer: ChannelParticipant,
|
|
81
|
+
payee: ChannelParticipant,
|
|
82
|
+
// Financial state
|
|
83
|
+
balance: ChannelBalance,
|
|
84
|
+
// Streaming parameters
|
|
85
|
+
ratePerSecond: z.string(),
|
|
86
|
+
// Amount per second
|
|
87
|
+
startTime: z.number().optional(),
|
|
88
|
+
// When streaming started
|
|
89
|
+
pausedAt: z.number().optional(),
|
|
90
|
+
// When paused (if paused)
|
|
91
|
+
// Configuration
|
|
92
|
+
config: ChannelConfig,
|
|
93
|
+
// On-chain references
|
|
94
|
+
contractAddress: z.string().optional(),
|
|
95
|
+
// Payment channel contract
|
|
96
|
+
fundingTxHash: z.string().optional(),
|
|
97
|
+
closingTxHash: z.string().optional(),
|
|
98
|
+
// Checkpointing
|
|
99
|
+
checkpoints: z.array(ChannelCheckpoint),
|
|
100
|
+
latestCheckpoint: ChannelCheckpoint.optional(),
|
|
101
|
+
// Timestamps
|
|
102
|
+
createdAt: z.number(),
|
|
103
|
+
updatedAt: z.number(),
|
|
104
|
+
closedAt: z.number().optional()
|
|
105
|
+
});
|
|
106
|
+
var ChannelCreateRequest = z.object({
|
|
107
|
+
chain: z.string(),
|
|
108
|
+
asset: z.string(),
|
|
109
|
+
payerAddress: z.string(),
|
|
110
|
+
payeeAddress: z.string(),
|
|
111
|
+
depositAmount: z.string(),
|
|
112
|
+
ratePerSecond: z.string(),
|
|
113
|
+
config: ChannelConfig.optional()
|
|
114
|
+
});
|
|
115
|
+
var FundingTransaction = z.object({
|
|
116
|
+
channelId: z.string(),
|
|
117
|
+
txHash: z.string(),
|
|
118
|
+
amount: z.string(),
|
|
119
|
+
sender: z.string(),
|
|
120
|
+
blockNumber: z.number().optional(),
|
|
121
|
+
timestamp: z.number(),
|
|
122
|
+
confirmed: z.boolean()
|
|
123
|
+
});
|
|
124
|
+
var ChannelCloseRequest = z.object({
|
|
125
|
+
channelId: z.string(),
|
|
126
|
+
initiator: z.string(),
|
|
127
|
+
// Address of closer
|
|
128
|
+
reason: z.enum(["mutual", "unilateral", "timeout", "dispute"]),
|
|
129
|
+
finalCheckpoint: ChannelCheckpoint.optional(),
|
|
130
|
+
signature: z.string()
|
|
131
|
+
});
|
|
132
|
+
var ChannelDispute = z.object({
|
|
133
|
+
channelId: z.string(),
|
|
134
|
+
disputeId: z.string(),
|
|
135
|
+
initiator: z.string(),
|
|
136
|
+
reason: z.string(),
|
|
137
|
+
claimedBalance: ChannelBalance,
|
|
138
|
+
evidence: z.array(z.object({
|
|
139
|
+
type: z.enum(["checkpoint", "signature", "transaction"]),
|
|
140
|
+
data: z.string(),
|
|
141
|
+
timestamp: z.number()
|
|
142
|
+
})),
|
|
143
|
+
status: z.enum(["pending", "resolved", "rejected"]),
|
|
144
|
+
resolution: z.object({
|
|
145
|
+
winner: z.string().optional(),
|
|
146
|
+
finalBalance: ChannelBalance.optional(),
|
|
147
|
+
timestamp: z.number().optional()
|
|
148
|
+
}).optional(),
|
|
149
|
+
createdAt: z.number(),
|
|
150
|
+
expiresAt: z.number()
|
|
151
|
+
});
|
|
152
|
+
var StateTransition = z.object({
|
|
153
|
+
from: ChannelState,
|
|
154
|
+
to: ChannelState,
|
|
155
|
+
trigger: z.string(),
|
|
156
|
+
timestamp: z.number(),
|
|
157
|
+
metadata: z.record(z.unknown()).optional()
|
|
158
|
+
});
|
|
159
|
+
var RecoveryData = z.object({
|
|
160
|
+
channelId: z.string(),
|
|
161
|
+
checkpoints: z.array(ChannelCheckpoint),
|
|
162
|
+
transactions: z.array(FundingTransaction),
|
|
163
|
+
disputes: z.array(ChannelDispute),
|
|
164
|
+
stateHistory: z.array(StateTransition),
|
|
165
|
+
recoveredAt: z.number()
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// src/channels/state.ts
|
|
169
|
+
var STATE_TRANSITIONS = {
|
|
170
|
+
created: ["funding", "closed"],
|
|
171
|
+
funding: ["open", "closed", "expired"],
|
|
172
|
+
open: ["paused", "closing", "disputing"],
|
|
173
|
+
paused: ["open", "closing", "disputing"],
|
|
174
|
+
closing: ["closed", "disputing"],
|
|
175
|
+
disputing: ["closing", "closed"],
|
|
176
|
+
closed: [],
|
|
177
|
+
expired: []
|
|
178
|
+
};
|
|
179
|
+
var ChannelStateMachine = class {
|
|
180
|
+
channel;
|
|
181
|
+
transitions = [];
|
|
182
|
+
listeners = /* @__PURE__ */ new Map();
|
|
183
|
+
constructor(channel) {
|
|
184
|
+
this.channel = { ...channel };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get current channel state
|
|
188
|
+
*/
|
|
189
|
+
getChannel() {
|
|
190
|
+
return { ...this.channel };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get current state
|
|
194
|
+
*/
|
|
195
|
+
getState() {
|
|
196
|
+
return this.channel.state;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get state history
|
|
200
|
+
*/
|
|
201
|
+
getTransitions() {
|
|
202
|
+
return [...this.transitions];
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Check if a transition is valid
|
|
206
|
+
*/
|
|
207
|
+
canTransition(to) {
|
|
208
|
+
const allowedTransitions = STATE_TRANSITIONS[this.channel.state];
|
|
209
|
+
return allowedTransitions.includes(to);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get allowed transitions from current state
|
|
213
|
+
*/
|
|
214
|
+
getAllowedTransitions() {
|
|
215
|
+
return STATE_TRANSITIONS[this.channel.state];
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Process a state event
|
|
219
|
+
*/
|
|
220
|
+
process(event) {
|
|
221
|
+
const currentState = this.channel.state;
|
|
222
|
+
let nextState = null;
|
|
223
|
+
let metadata = {};
|
|
224
|
+
switch (event.type) {
|
|
225
|
+
case "FUND":
|
|
226
|
+
if (currentState !== "created") {
|
|
227
|
+
return { success: false, error: "Channel must be in created state to fund" };
|
|
228
|
+
}
|
|
229
|
+
nextState = "funding";
|
|
230
|
+
metadata = { txHash: event.txHash, amount: event.amount };
|
|
231
|
+
this.channel.fundingTxHash = event.txHash;
|
|
232
|
+
break;
|
|
233
|
+
case "CONFIRM_FUNDING":
|
|
234
|
+
if (currentState !== "funding") {
|
|
235
|
+
return { success: false, error: "Channel must be in funding state" };
|
|
236
|
+
}
|
|
237
|
+
nextState = "open";
|
|
238
|
+
this.channel.startTime = Date.now();
|
|
239
|
+
break;
|
|
240
|
+
case "START_STREAMING":
|
|
241
|
+
if (currentState !== "open" && currentState !== "paused") {
|
|
242
|
+
return { success: false, error: "Channel must be open or paused to stream" };
|
|
243
|
+
}
|
|
244
|
+
if (currentState === "paused") {
|
|
245
|
+
nextState = "open";
|
|
246
|
+
this.channel.pausedAt = void 0;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case "PAUSE":
|
|
250
|
+
if (currentState !== "open") {
|
|
251
|
+
return { success: false, error: "Channel must be open to pause" };
|
|
252
|
+
}
|
|
253
|
+
nextState = "paused";
|
|
254
|
+
this.channel.pausedAt = Date.now();
|
|
255
|
+
break;
|
|
256
|
+
case "RESUME":
|
|
257
|
+
if (currentState !== "paused") {
|
|
258
|
+
return { success: false, error: "Channel must be paused to resume" };
|
|
259
|
+
}
|
|
260
|
+
nextState = "open";
|
|
261
|
+
this.channel.pausedAt = void 0;
|
|
262
|
+
break;
|
|
263
|
+
case "INITIATE_CLOSE":
|
|
264
|
+
if (currentState !== "open" && currentState !== "paused") {
|
|
265
|
+
return { success: false, error: "Channel must be open or paused to close" };
|
|
266
|
+
}
|
|
267
|
+
nextState = "closing";
|
|
268
|
+
metadata = { initiator: event.initiator };
|
|
269
|
+
break;
|
|
270
|
+
case "MUTUAL_CLOSE":
|
|
271
|
+
if (currentState !== "closing") {
|
|
272
|
+
return { success: false, error: "Channel must be in closing state" };
|
|
273
|
+
}
|
|
274
|
+
nextState = "closed";
|
|
275
|
+
this.channel.closedAt = Date.now();
|
|
276
|
+
metadata = { signature: event.signature };
|
|
277
|
+
break;
|
|
278
|
+
case "DISPUTE":
|
|
279
|
+
if (!["open", "paused", "closing"].includes(currentState)) {
|
|
280
|
+
return { success: false, error: "Cannot dispute in current state" };
|
|
281
|
+
}
|
|
282
|
+
nextState = "disputing";
|
|
283
|
+
metadata = { reason: event.reason };
|
|
284
|
+
break;
|
|
285
|
+
case "RESOLVE_DISPUTE":
|
|
286
|
+
if (currentState !== "disputing") {
|
|
287
|
+
return { success: false, error: "No dispute to resolve" };
|
|
288
|
+
}
|
|
289
|
+
nextState = "closing";
|
|
290
|
+
metadata = { winner: event.winner };
|
|
291
|
+
break;
|
|
292
|
+
case "FINALIZE":
|
|
293
|
+
if (currentState !== "closing") {
|
|
294
|
+
return { success: false, error: "Channel must be in closing state" };
|
|
295
|
+
}
|
|
296
|
+
nextState = "closed";
|
|
297
|
+
this.channel.closedAt = Date.now();
|
|
298
|
+
break;
|
|
299
|
+
case "TIMEOUT":
|
|
300
|
+
if (currentState === "funding") {
|
|
301
|
+
nextState = "expired";
|
|
302
|
+
} else if (currentState === "disputing") {
|
|
303
|
+
nextState = "closed";
|
|
304
|
+
this.channel.closedAt = Date.now();
|
|
305
|
+
} else {
|
|
306
|
+
return { success: false, error: "Timeout not applicable in current state" };
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
default:
|
|
310
|
+
return { success: false, error: "Unknown event type" };
|
|
311
|
+
}
|
|
312
|
+
if (nextState && nextState !== currentState) {
|
|
313
|
+
if (!this.canTransition(nextState)) {
|
|
314
|
+
return { success: false, error: `Invalid transition from ${currentState} to ${nextState}` };
|
|
315
|
+
}
|
|
316
|
+
const transition = {
|
|
317
|
+
from: currentState,
|
|
318
|
+
to: nextState,
|
|
319
|
+
trigger: event.type,
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
metadata
|
|
322
|
+
};
|
|
323
|
+
this.channel.state = nextState;
|
|
324
|
+
this.channel.updatedAt = Date.now();
|
|
325
|
+
this.transitions.push(transition);
|
|
326
|
+
this.notifyListeners(nextState);
|
|
327
|
+
}
|
|
328
|
+
return { success: true };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Update channel balance
|
|
332
|
+
*/
|
|
333
|
+
updateBalance(payerBalance, payeeBalance) {
|
|
334
|
+
this.channel.balance = {
|
|
335
|
+
...this.channel.balance,
|
|
336
|
+
payer: payerBalance,
|
|
337
|
+
payee: payeeBalance
|
|
338
|
+
};
|
|
339
|
+
this.channel.updatedAt = Date.now();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Add checkpoint
|
|
343
|
+
*/
|
|
344
|
+
addCheckpoint(checkpoint) {
|
|
345
|
+
this.channel.checkpoints.push(checkpoint);
|
|
346
|
+
this.channel.latestCheckpoint = checkpoint;
|
|
347
|
+
this.channel.updatedAt = Date.now();
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get current streamed amount
|
|
351
|
+
*/
|
|
352
|
+
getCurrentStreamedAmount() {
|
|
353
|
+
if (!this.channel.startTime) return "0";
|
|
354
|
+
if (this.channel.state !== "open" && this.channel.state !== "paused") {
|
|
355
|
+
return this.channel.balance.payee;
|
|
356
|
+
}
|
|
357
|
+
const now = this.channel.pausedAt ?? Date.now();
|
|
358
|
+
const elapsed = Math.floor((now - this.channel.startTime) / 1e3);
|
|
359
|
+
const streamed = BigInt(this.channel.ratePerSecond) * BigInt(elapsed);
|
|
360
|
+
const maxStreamed = BigInt(this.channel.balance.total);
|
|
361
|
+
return (streamed > maxStreamed ? maxStreamed : streamed).toString();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Calculate remaining balance
|
|
365
|
+
*/
|
|
366
|
+
getRemainingBalance() {
|
|
367
|
+
const total = BigInt(this.channel.balance.total);
|
|
368
|
+
const streamed = BigInt(this.getCurrentStreamedAmount());
|
|
369
|
+
return (total - streamed).toString();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Check if channel needs checkpoint
|
|
373
|
+
*/
|
|
374
|
+
needsCheckpoint() {
|
|
375
|
+
const config = this.channel.config;
|
|
376
|
+
const lastCheckpoint = this.channel.latestCheckpoint;
|
|
377
|
+
if (!lastCheckpoint) return true;
|
|
378
|
+
const timeSinceCheckpoint = Date.now() - lastCheckpoint.timestamp;
|
|
379
|
+
if (timeSinceCheckpoint >= config.checkpointInterval * 1e3) return true;
|
|
380
|
+
const currentStreamed = BigInt(this.getCurrentStreamedAmount());
|
|
381
|
+
const lastStreamed = BigInt(lastCheckpoint.amountStreamed);
|
|
382
|
+
const amountChange = currentStreamed - lastStreamed;
|
|
383
|
+
if (amountChange >= BigInt(config.minCheckpointAmount)) return true;
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Subscribe to state changes
|
|
388
|
+
*/
|
|
389
|
+
onStateChange(state, callback) {
|
|
390
|
+
const key = state;
|
|
391
|
+
if (!this.listeners.has(key)) {
|
|
392
|
+
this.listeners.set(key, /* @__PURE__ */ new Set());
|
|
393
|
+
}
|
|
394
|
+
this.listeners.get(key).add(callback);
|
|
395
|
+
return () => {
|
|
396
|
+
this.listeners.get(key)?.delete(callback);
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
notifyListeners(newState) {
|
|
400
|
+
this.listeners.get(newState)?.forEach((cb) => cb(this.channel));
|
|
401
|
+
this.listeners.get("*")?.forEach((cb) => cb(this.channel));
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function isChannelActive(channel) {
|
|
405
|
+
return channel.state === "open";
|
|
406
|
+
}
|
|
407
|
+
function isChannelTerminal(channel) {
|
|
408
|
+
return channel.state === "closed" || channel.state === "expired";
|
|
409
|
+
}
|
|
410
|
+
function getChannelUtilization(channel) {
|
|
411
|
+
const total = parseFloat(channel.balance.total);
|
|
412
|
+
if (total === 0) return 0;
|
|
413
|
+
const payee = parseFloat(channel.balance.payee);
|
|
414
|
+
return payee / total * 100;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/channels/opening.ts
|
|
418
|
+
var ChannelOpener = class {
|
|
419
|
+
config;
|
|
420
|
+
constructor(config = {}) {
|
|
421
|
+
this.config = {
|
|
422
|
+
fundingTimeout: config.fundingTimeout ?? 36e5,
|
|
423
|
+
// 1 hour
|
|
424
|
+
confirmations: config.confirmations ?? 1,
|
|
425
|
+
contractAddress: config.contractAddress ?? ""
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Create a new channel
|
|
430
|
+
*/
|
|
431
|
+
create(request) {
|
|
432
|
+
this.validateCreateRequest(request);
|
|
433
|
+
const channelConfig = request.config ?? ChannelConfig.parse({});
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
const channelId = this.generateChannelId(request, now);
|
|
436
|
+
const payer = {
|
|
437
|
+
address: request.payerAddress,
|
|
438
|
+
role: "payer"
|
|
439
|
+
};
|
|
440
|
+
const payee = {
|
|
441
|
+
address: request.payeeAddress,
|
|
442
|
+
role: "payee"
|
|
443
|
+
};
|
|
444
|
+
const balance = {
|
|
445
|
+
payer: request.depositAmount,
|
|
446
|
+
payee: "0",
|
|
447
|
+
total: request.depositAmount,
|
|
448
|
+
locked: "0"
|
|
449
|
+
};
|
|
450
|
+
const channel = {
|
|
451
|
+
id: channelId,
|
|
452
|
+
state: "created",
|
|
453
|
+
chain: request.chain,
|
|
454
|
+
asset: request.asset,
|
|
455
|
+
payer,
|
|
456
|
+
payee,
|
|
457
|
+
balance,
|
|
458
|
+
ratePerSecond: request.ratePerSecond,
|
|
459
|
+
config: channelConfig,
|
|
460
|
+
contractAddress: this.config.contractAddress || void 0,
|
|
461
|
+
checkpoints: [],
|
|
462
|
+
createdAt: now,
|
|
463
|
+
updatedAt: now
|
|
464
|
+
};
|
|
465
|
+
const stateMachine = new ChannelStateMachine(channel);
|
|
466
|
+
const fundingRequired = this.calculateFundingRequirements(channel);
|
|
467
|
+
return {
|
|
468
|
+
channel: stateMachine.getChannel(),
|
|
469
|
+
stateMachine,
|
|
470
|
+
fundingRequired
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Process funding transaction
|
|
475
|
+
*/
|
|
476
|
+
processFunding(stateMachine, txHash, amount) {
|
|
477
|
+
const channel = stateMachine.getChannel();
|
|
478
|
+
if (BigInt(amount) < BigInt(channel.config.minDeposit)) {
|
|
479
|
+
return { success: false, error: "Funding amount below minimum deposit" };
|
|
480
|
+
}
|
|
481
|
+
if (channel.config.maxDeposit && BigInt(amount) > BigInt(channel.config.maxDeposit)) {
|
|
482
|
+
return { success: false, error: "Funding amount exceeds maximum deposit" };
|
|
483
|
+
}
|
|
484
|
+
const result = stateMachine.process({
|
|
485
|
+
type: "FUND",
|
|
486
|
+
txHash,
|
|
487
|
+
amount
|
|
488
|
+
});
|
|
489
|
+
if (!result.success) {
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
stateMachine.updateBalance(amount, "0");
|
|
493
|
+
return { success: true };
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Confirm funding (after required confirmations)
|
|
497
|
+
*/
|
|
498
|
+
confirmFunding(stateMachine, _confirmation) {
|
|
499
|
+
return stateMachine.process({ type: "CONFIRM_FUNDING" });
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Handle funding timeout
|
|
503
|
+
*/
|
|
504
|
+
handleTimeout(stateMachine) {
|
|
505
|
+
const channel = stateMachine.getChannel();
|
|
506
|
+
if (channel.state !== "funding") {
|
|
507
|
+
return { success: false, expired: false };
|
|
508
|
+
}
|
|
509
|
+
const elapsed = Date.now() - channel.createdAt;
|
|
510
|
+
if (elapsed < this.config.fundingTimeout) {
|
|
511
|
+
return { success: false, expired: false };
|
|
512
|
+
}
|
|
513
|
+
const result = stateMachine.process({ type: "TIMEOUT" });
|
|
514
|
+
return { success: result.success, expired: result.success };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Validate channel create request
|
|
518
|
+
*/
|
|
519
|
+
validateCreateRequest(request) {
|
|
520
|
+
ChannelCreateRequest.parse(request);
|
|
521
|
+
if (request.payerAddress === request.payeeAddress) {
|
|
522
|
+
throw new Error("Payer and payee must be different addresses");
|
|
523
|
+
}
|
|
524
|
+
if (BigInt(request.depositAmount) <= 0n) {
|
|
525
|
+
throw new Error("Deposit amount must be positive");
|
|
526
|
+
}
|
|
527
|
+
if (BigInt(request.ratePerSecond) <= 0n) {
|
|
528
|
+
throw new Error("Rate per second must be positive");
|
|
529
|
+
}
|
|
530
|
+
const depositBigInt = BigInt(request.depositAmount);
|
|
531
|
+
const rateBigInt = BigInt(request.ratePerSecond);
|
|
532
|
+
const minDuration = depositBigInt / rateBigInt;
|
|
533
|
+
if (minDuration < 60n) {
|
|
534
|
+
throw new Error("Channel would be exhausted in less than 60 seconds");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Generate unique channel ID
|
|
539
|
+
*/
|
|
540
|
+
generateChannelId(request, timestamp) {
|
|
541
|
+
const data = `${request.chain}:${request.asset}:${request.payerAddress}:${request.payeeAddress}:${timestamp}`;
|
|
542
|
+
let hash = 0;
|
|
543
|
+
for (let i = 0; i < data.length; i++) {
|
|
544
|
+
const char = data.charCodeAt(i);
|
|
545
|
+
hash = (hash << 5) - hash + char;
|
|
546
|
+
hash = hash & hash;
|
|
547
|
+
}
|
|
548
|
+
return `ch_${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Calculate funding requirements for channel
|
|
552
|
+
*/
|
|
553
|
+
calculateFundingRequirements(channel) {
|
|
554
|
+
const totalRequired = BigInt(channel.balance.total) + BigInt(channel.config.channelFee);
|
|
555
|
+
return {
|
|
556
|
+
amount: totalRequired.toString(),
|
|
557
|
+
to: channel.contractAddress ?? channel.payee.address,
|
|
558
|
+
data: this.encodeFundingData(channel)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Encode funding transaction data
|
|
563
|
+
*/
|
|
564
|
+
encodeFundingData(channel) {
|
|
565
|
+
const methodId = "0x" + "initChannel".split("").map((c) => c.charCodeAt(0).toString(16)).join("").slice(0, 8);
|
|
566
|
+
return `${methodId}${channel.id.replace("ch_", "").padStart(64, "0")}`;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
function estimateChannelDuration(depositAmount, ratePerSecond) {
|
|
570
|
+
const deposit = BigInt(depositAmount);
|
|
571
|
+
const rate = BigInt(ratePerSecond);
|
|
572
|
+
if (rate === 0n) {
|
|
573
|
+
return { seconds: Infinity, formatted: "infinite" };
|
|
574
|
+
}
|
|
575
|
+
const seconds = Number(deposit / rate);
|
|
576
|
+
if (seconds < 60) {
|
|
577
|
+
return { seconds, formatted: `${seconds} seconds` };
|
|
578
|
+
} else if (seconds < 3600) {
|
|
579
|
+
const minutes = Math.floor(seconds / 60);
|
|
580
|
+
return { seconds, formatted: `${minutes} minutes` };
|
|
581
|
+
} else if (seconds < 86400) {
|
|
582
|
+
const hours = Math.floor(seconds / 3600);
|
|
583
|
+
return { seconds, formatted: `${hours} hours` };
|
|
584
|
+
} else {
|
|
585
|
+
const days = Math.floor(seconds / 86400);
|
|
586
|
+
return { seconds, formatted: `${days} days` };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function calculateRequiredDeposit(ratePerSecond, durationSeconds) {
|
|
590
|
+
const rate = BigInt(ratePerSecond);
|
|
591
|
+
const duration = BigInt(durationSeconds);
|
|
592
|
+
return (rate * duration).toString();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/channels/closing.ts
|
|
596
|
+
var ChannelCloser = class {
|
|
597
|
+
config;
|
|
598
|
+
constructor(config = {}) {
|
|
599
|
+
this.config = {
|
|
600
|
+
challengePeriod: config.challengePeriod ?? 86400,
|
|
601
|
+
// 24 hours
|
|
602
|
+
gracePeriod: config.gracePeriod ?? 3600,
|
|
603
|
+
// 1 hour
|
|
604
|
+
autoFinalize: config.autoFinalize ?? true
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Initiate channel close
|
|
609
|
+
*/
|
|
610
|
+
initiateClose(stateMachine, initiator, signature) {
|
|
611
|
+
const channel = stateMachine.getChannel();
|
|
612
|
+
if (initiator !== channel.payer.address && initiator !== channel.payee.address) {
|
|
613
|
+
return { success: false, error: "Initiator must be a channel participant" };
|
|
614
|
+
}
|
|
615
|
+
const currentStreamed = stateMachine.getCurrentStreamedAmount();
|
|
616
|
+
const finalBalance = this.calculateFinalBalance(channel, currentStreamed);
|
|
617
|
+
const finalCheckpoint = this.createFinalCheckpoint(channel, finalBalance, signature);
|
|
618
|
+
const result = stateMachine.process({
|
|
619
|
+
type: "INITIATE_CLOSE",
|
|
620
|
+
initiator
|
|
621
|
+
});
|
|
622
|
+
if (!result.success) {
|
|
623
|
+
return { success: false, error: result.error };
|
|
624
|
+
}
|
|
625
|
+
stateMachine.addCheckpoint(finalCheckpoint);
|
|
626
|
+
const challengeDeadline = Date.now() + this.config.challengePeriod * 1e3;
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
challengeDeadline,
|
|
630
|
+
finalCheckpoint,
|
|
631
|
+
settlementAmount: {
|
|
632
|
+
payer: finalBalance.payer,
|
|
633
|
+
payee: finalBalance.payee
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Complete mutual close (both parties agree)
|
|
639
|
+
*/
|
|
640
|
+
mutualClose(stateMachine, payerSignature, payeeSignature) {
|
|
641
|
+
const channel = stateMachine.getChannel();
|
|
642
|
+
const currentStreamed = stateMachine.getCurrentStreamedAmount();
|
|
643
|
+
const finalBalance = this.calculateFinalBalance(channel, currentStreamed);
|
|
644
|
+
const finalCheckpoint = {
|
|
645
|
+
channelId: channel.id,
|
|
646
|
+
sequence: channel.checkpoints.length,
|
|
647
|
+
timestamp: Date.now(),
|
|
648
|
+
balance: finalBalance,
|
|
649
|
+
amountStreamed: currentStreamed,
|
|
650
|
+
payerSignature,
|
|
651
|
+
payeeSignature
|
|
652
|
+
};
|
|
653
|
+
let result = stateMachine.process({
|
|
654
|
+
type: "INITIATE_CLOSE",
|
|
655
|
+
initiator: channel.payer.address
|
|
656
|
+
});
|
|
657
|
+
if (!result.success) {
|
|
658
|
+
return { success: false, error: result.error };
|
|
659
|
+
}
|
|
660
|
+
result = stateMachine.process({
|
|
661
|
+
type: "MUTUAL_CLOSE",
|
|
662
|
+
signature: `${payerSignature}:${payeeSignature}`
|
|
663
|
+
});
|
|
664
|
+
if (!result.success) {
|
|
665
|
+
return { success: false, error: result.error };
|
|
666
|
+
}
|
|
667
|
+
stateMachine.addCheckpoint(finalCheckpoint);
|
|
668
|
+
return {
|
|
669
|
+
success: true,
|
|
670
|
+
finalCheckpoint,
|
|
671
|
+
settlementAmount: {
|
|
672
|
+
payer: finalBalance.payer,
|
|
673
|
+
payee: finalBalance.payee
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Finalize channel after challenge period
|
|
679
|
+
*/
|
|
680
|
+
finalize(stateMachine) {
|
|
681
|
+
const channel = stateMachine.getChannel();
|
|
682
|
+
if (channel.state !== "closing") {
|
|
683
|
+
return {
|
|
684
|
+
success: false,
|
|
685
|
+
error: "Channel must be in closing state",
|
|
686
|
+
finalBalance: channel.balance
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const latestCheckpoint = channel.latestCheckpoint;
|
|
690
|
+
if (latestCheckpoint) {
|
|
691
|
+
const elapsed = Date.now() - latestCheckpoint.timestamp;
|
|
692
|
+
if (elapsed < this.config.challengePeriod * 1e3) {
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
error: "Challenge period not yet elapsed",
|
|
696
|
+
finalBalance: channel.balance
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const result = stateMachine.process({ type: "FINALIZE" });
|
|
701
|
+
if (!result.success) {
|
|
702
|
+
return {
|
|
703
|
+
success: false,
|
|
704
|
+
error: result.error,
|
|
705
|
+
finalBalance: channel.balance
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const finalChannel = stateMachine.getChannel();
|
|
709
|
+
return {
|
|
710
|
+
success: true,
|
|
711
|
+
finalBalance: finalChannel.balance
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Validate close request
|
|
716
|
+
*/
|
|
717
|
+
validateCloseRequest(channel, request) {
|
|
718
|
+
const errors = [];
|
|
719
|
+
if (request.channelId !== channel.id) {
|
|
720
|
+
errors.push("Channel ID mismatch");
|
|
721
|
+
}
|
|
722
|
+
if (request.initiator !== channel.payer.address && request.initiator !== channel.payee.address) {
|
|
723
|
+
errors.push("Initiator is not a channel participant");
|
|
724
|
+
}
|
|
725
|
+
if (channel.state !== "open" && channel.state !== "paused") {
|
|
726
|
+
errors.push(`Cannot close channel in ${channel.state} state`);
|
|
727
|
+
}
|
|
728
|
+
if (request.finalCheckpoint) {
|
|
729
|
+
const cpErrors = this.validateCheckpoint(channel, request.finalCheckpoint);
|
|
730
|
+
errors.push(...cpErrors);
|
|
731
|
+
}
|
|
732
|
+
return { valid: errors.length === 0, errors };
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Get settlement amounts for on-chain execution
|
|
736
|
+
*/
|
|
737
|
+
getSettlementAmounts(channel) {
|
|
738
|
+
const finalBalance = channel.latestCheckpoint?.balance ?? channel.balance;
|
|
739
|
+
const fee = channel.config.settlementFee;
|
|
740
|
+
const payerAmount = BigInt(finalBalance.payer) - BigInt(fee);
|
|
741
|
+
return {
|
|
742
|
+
payer: payerAmount > 0n ? payerAmount.toString() : "0",
|
|
743
|
+
payee: finalBalance.payee,
|
|
744
|
+
fee
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Calculate time until channel can be finalized
|
|
749
|
+
*/
|
|
750
|
+
getTimeUntilFinalization(channel) {
|
|
751
|
+
if (channel.state !== "closing") {
|
|
752
|
+
return -1;
|
|
753
|
+
}
|
|
754
|
+
const latestCheckpoint = channel.latestCheckpoint;
|
|
755
|
+
if (!latestCheckpoint) {
|
|
756
|
+
return 0;
|
|
757
|
+
}
|
|
758
|
+
const elapsed = Date.now() - latestCheckpoint.timestamp;
|
|
759
|
+
const remaining = this.config.challengePeriod * 1e3 - elapsed;
|
|
760
|
+
return Math.max(0, remaining);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Check if channel close can be challenged
|
|
764
|
+
*/
|
|
765
|
+
canChallenge(channel) {
|
|
766
|
+
if (channel.state !== "closing") {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
return this.getTimeUntilFinalization(channel) > 0;
|
|
770
|
+
}
|
|
771
|
+
calculateFinalBalance(channel, amountStreamed) {
|
|
772
|
+
const total = BigInt(channel.balance.total);
|
|
773
|
+
const streamed = BigInt(amountStreamed);
|
|
774
|
+
const remaining = total - streamed;
|
|
775
|
+
return {
|
|
776
|
+
payer: remaining.toString(),
|
|
777
|
+
payee: streamed.toString(),
|
|
778
|
+
total: total.toString(),
|
|
779
|
+
locked: "0"
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
createFinalCheckpoint(channel, balance, signature) {
|
|
783
|
+
return {
|
|
784
|
+
channelId: channel.id,
|
|
785
|
+
sequence: channel.checkpoints.length,
|
|
786
|
+
timestamp: Date.now(),
|
|
787
|
+
balance,
|
|
788
|
+
amountStreamed: balance.payee,
|
|
789
|
+
payerSignature: signature
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
validateCheckpoint(channel, checkpoint) {
|
|
793
|
+
const errors = [];
|
|
794
|
+
const expectedSequence = channel.checkpoints.length;
|
|
795
|
+
if (checkpoint.sequence !== expectedSequence) {
|
|
796
|
+
errors.push(`Invalid sequence: expected ${expectedSequence}, got ${checkpoint.sequence}`);
|
|
797
|
+
}
|
|
798
|
+
const totalBalance = BigInt(checkpoint.balance.payer) + BigInt(checkpoint.balance.payee);
|
|
799
|
+
if (totalBalance !== BigInt(channel.balance.total)) {
|
|
800
|
+
errors.push("Balance inconsistency: payer + payee does not equal total");
|
|
801
|
+
}
|
|
802
|
+
if (checkpoint.timestamp > Date.now()) {
|
|
803
|
+
errors.push("Checkpoint timestamp is in the future");
|
|
804
|
+
}
|
|
805
|
+
return errors;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
function buildCloseTransactionData(channel, checkpoint) {
|
|
809
|
+
const methodId = "0x" + "closeChannel".split("").map((c) => c.charCodeAt(0).toString(16)).join("").slice(0, 8);
|
|
810
|
+
const data = [
|
|
811
|
+
methodId,
|
|
812
|
+
channel.id.replace("ch_", "").padStart(64, "0"),
|
|
813
|
+
checkpoint.sequence.toString(16).padStart(64, "0"),
|
|
814
|
+
checkpoint.payerSignature.replace("0x", "").padStart(128, "0")
|
|
815
|
+
].join("");
|
|
816
|
+
return {
|
|
817
|
+
to: channel.contractAddress ?? channel.payee.address,
|
|
818
|
+
data,
|
|
819
|
+
value: "0"
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/channels/recovery.ts
|
|
824
|
+
var ChannelRecovery = class {
|
|
825
|
+
config;
|
|
826
|
+
storage;
|
|
827
|
+
constructor(config = {}, storage) {
|
|
828
|
+
this.config = {
|
|
829
|
+
maxCheckpointsToKeep: config.maxCheckpointsToKeep ?? 100,
|
|
830
|
+
verifySignatures: config.verifySignatures ?? true,
|
|
831
|
+
onChainFallback: config.onChainFallback ?? true
|
|
832
|
+
};
|
|
833
|
+
this.storage = storage;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Recover channel from checkpoints
|
|
837
|
+
*/
|
|
838
|
+
recoverFromCheckpoints(baseChannel, checkpoints) {
|
|
839
|
+
if (checkpoints.length === 0) {
|
|
840
|
+
return {
|
|
841
|
+
success: true,
|
|
842
|
+
channel: baseChannel,
|
|
843
|
+
stateMachine: new ChannelStateMachine(baseChannel),
|
|
844
|
+
dataLoss: false
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
const sortedCheckpoints = [...checkpoints].sort((a, b) => a.sequence - b.sequence);
|
|
848
|
+
let latestValid;
|
|
849
|
+
for (let i = sortedCheckpoints.length - 1; i >= 0; i--) {
|
|
850
|
+
const checkpoint = sortedCheckpoints[i];
|
|
851
|
+
if (this.isCheckpointValid(baseChannel, checkpoint)) {
|
|
852
|
+
latestValid = checkpoint;
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (!latestValid) {
|
|
857
|
+
return {
|
|
858
|
+
success: false,
|
|
859
|
+
error: "No valid checkpoints found",
|
|
860
|
+
dataLoss: true
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const recoveredChannel = this.reconstructFromCheckpoint(baseChannel, latestValid, sortedCheckpoints);
|
|
864
|
+
return {
|
|
865
|
+
success: true,
|
|
866
|
+
channel: recoveredChannel,
|
|
867
|
+
stateMachine: new ChannelStateMachine(recoveredChannel),
|
|
868
|
+
recoveredFromCheckpoint: latestValid,
|
|
869
|
+
dataLoss: sortedCheckpoints[sortedCheckpoints.length - 1].sequence > latestValid.sequence
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Full recovery from storage
|
|
874
|
+
*/
|
|
875
|
+
async recoverFromStorage(channelId) {
|
|
876
|
+
if (!this.storage) {
|
|
877
|
+
return {
|
|
878
|
+
success: false,
|
|
879
|
+
error: "No storage provider configured"
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
const [channel, checkpoints, transactions, disputes] = await Promise.all([
|
|
884
|
+
this.storage.getChannel(channelId),
|
|
885
|
+
this.storage.getCheckpoints(channelId),
|
|
886
|
+
this.storage.getTransactions(channelId),
|
|
887
|
+
this.storage.getDisputes(channelId)
|
|
888
|
+
]);
|
|
889
|
+
if (!channel) {
|
|
890
|
+
return {
|
|
891
|
+
success: false,
|
|
892
|
+
error: "Channel not found in storage"
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const result = this.recoverFromCheckpoints(channel, checkpoints);
|
|
896
|
+
if (!result.success) {
|
|
897
|
+
if (this.config.onChainFallback) {
|
|
898
|
+
return this.recoverFromOnChain(channel, transactions, disputes);
|
|
899
|
+
}
|
|
900
|
+
return result;
|
|
901
|
+
}
|
|
902
|
+
return result;
|
|
903
|
+
} catch (error) {
|
|
904
|
+
return {
|
|
905
|
+
success: false,
|
|
906
|
+
error: `Storage recovery failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Export recovery data for backup
|
|
912
|
+
*/
|
|
913
|
+
exportRecoveryData(channel, stateMachine) {
|
|
914
|
+
return {
|
|
915
|
+
channelId: channel.id,
|
|
916
|
+
checkpoints: channel.checkpoints,
|
|
917
|
+
transactions: [],
|
|
918
|
+
// Would be populated from transaction history
|
|
919
|
+
disputes: [],
|
|
920
|
+
// Would be populated from dispute history
|
|
921
|
+
stateHistory: stateMachine.getTransitions(),
|
|
922
|
+
recoveredAt: Date.now()
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Import recovery data
|
|
927
|
+
*/
|
|
928
|
+
importRecoveryData(data) {
|
|
929
|
+
if (!data.channelId || !data.checkpoints) {
|
|
930
|
+
return {
|
|
931
|
+
success: false,
|
|
932
|
+
error: "Invalid recovery data format"
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (data.checkpoints.length === 0) {
|
|
936
|
+
return {
|
|
937
|
+
success: false,
|
|
938
|
+
error: "No checkpoints in recovery data"
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
const baseChannel = this.createBaseChannelFromRecoveryData(data);
|
|
942
|
+
return this.recoverFromCheckpoints(baseChannel, data.checkpoints);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Verify checkpoint integrity
|
|
946
|
+
*/
|
|
947
|
+
verifyCheckpointChain(checkpoints) {
|
|
948
|
+
const errors = [];
|
|
949
|
+
if (checkpoints.length === 0) {
|
|
950
|
+
return { valid: true, errors: [] };
|
|
951
|
+
}
|
|
952
|
+
const sorted = [...checkpoints].sort((a, b) => a.sequence - b.sequence);
|
|
953
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
954
|
+
const current = sorted[i];
|
|
955
|
+
if (current.sequence !== i) {
|
|
956
|
+
errors.push(`Sequence gap: expected ${i}, got ${current.sequence}`);
|
|
957
|
+
}
|
|
958
|
+
if (i > 0 && current.timestamp < sorted[i - 1].timestamp) {
|
|
959
|
+
errors.push(`Timestamp ordering violation at sequence ${current.sequence}`);
|
|
960
|
+
}
|
|
961
|
+
if (i > 0) {
|
|
962
|
+
const prevStreamed = BigInt(sorted[i - 1].amountStreamed);
|
|
963
|
+
const currStreamed = BigInt(current.amountStreamed);
|
|
964
|
+
if (currStreamed < prevStreamed) {
|
|
965
|
+
errors.push(`Amount decreased at sequence ${current.sequence}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return { valid: errors.length === 0, errors };
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Prune old checkpoints keeping only recent ones
|
|
973
|
+
*/
|
|
974
|
+
pruneCheckpoints(checkpoints) {
|
|
975
|
+
if (checkpoints.length <= this.config.maxCheckpointsToKeep) {
|
|
976
|
+
return checkpoints;
|
|
977
|
+
}
|
|
978
|
+
const sorted = [...checkpoints].sort((a, b) => a.sequence - b.sequence);
|
|
979
|
+
const genesis = sorted[0];
|
|
980
|
+
const recent = sorted.slice(-(this.config.maxCheckpointsToKeep - 1));
|
|
981
|
+
return [genesis, ...recent];
|
|
982
|
+
}
|
|
983
|
+
isCheckpointValid(channel, checkpoint) {
|
|
984
|
+
if (checkpoint.channelId !== channel.id) {
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
const total = BigInt(checkpoint.balance.payer) + BigInt(checkpoint.balance.payee);
|
|
988
|
+
if (total !== BigInt(channel.balance.total)) {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
if (!checkpoint.payerSignature) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if (this.config.verifySignatures) {
|
|
995
|
+
}
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
reconstructFromCheckpoint(baseChannel, checkpoint, allCheckpoints) {
|
|
999
|
+
const validCheckpoints = allCheckpoints.filter((cp) => cp.sequence <= checkpoint.sequence);
|
|
1000
|
+
let state = "open";
|
|
1001
|
+
if (checkpoint.payeeSignature) {
|
|
1002
|
+
state = "open";
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
...baseChannel,
|
|
1006
|
+
state,
|
|
1007
|
+
balance: checkpoint.balance,
|
|
1008
|
+
checkpoints: validCheckpoints,
|
|
1009
|
+
latestCheckpoint: checkpoint,
|
|
1010
|
+
updatedAt: checkpoint.timestamp
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
recoverFromOnChain(channel, transactions, disputes) {
|
|
1014
|
+
const confirmedTxs = transactions.filter((tx) => tx.confirmed);
|
|
1015
|
+
if (confirmedTxs.length === 0) {
|
|
1016
|
+
return {
|
|
1017
|
+
success: false,
|
|
1018
|
+
error: "No confirmed transactions found",
|
|
1019
|
+
dataLoss: true
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const totalFunded = confirmedTxs.reduce(
|
|
1023
|
+
(sum, tx) => sum + BigInt(tx.amount),
|
|
1024
|
+
0n
|
|
1025
|
+
);
|
|
1026
|
+
const activeDispute = disputes.find((d) => d.status === "pending");
|
|
1027
|
+
let state = "open";
|
|
1028
|
+
if (activeDispute) {
|
|
1029
|
+
state = "disputing";
|
|
1030
|
+
}
|
|
1031
|
+
const recoveredChannel = {
|
|
1032
|
+
...channel,
|
|
1033
|
+
state,
|
|
1034
|
+
balance: {
|
|
1035
|
+
...channel.balance,
|
|
1036
|
+
total: totalFunded.toString(),
|
|
1037
|
+
payer: totalFunded.toString(),
|
|
1038
|
+
payee: "0"
|
|
1039
|
+
},
|
|
1040
|
+
fundingTxHash: confirmedTxs[0].txHash,
|
|
1041
|
+
checkpoints: [],
|
|
1042
|
+
updatedAt: Date.now()
|
|
1043
|
+
};
|
|
1044
|
+
return {
|
|
1045
|
+
success: true,
|
|
1046
|
+
channel: recoveredChannel,
|
|
1047
|
+
stateMachine: new ChannelStateMachine(recoveredChannel),
|
|
1048
|
+
dataLoss: true
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
createBaseChannelFromRecoveryData(data) {
|
|
1052
|
+
const firstCheckpoint = data.checkpoints[0];
|
|
1053
|
+
return {
|
|
1054
|
+
id: data.channelId,
|
|
1055
|
+
state: "open",
|
|
1056
|
+
chain: "",
|
|
1057
|
+
asset: "",
|
|
1058
|
+
payer: { address: "", role: "payer" },
|
|
1059
|
+
payee: { address: "", role: "payee" },
|
|
1060
|
+
balance: firstCheckpoint.balance,
|
|
1061
|
+
ratePerSecond: "0",
|
|
1062
|
+
config: {
|
|
1063
|
+
minDeposit: "0",
|
|
1064
|
+
challengePeriod: 86400,
|
|
1065
|
+
checkpointInterval: 3600,
|
|
1066
|
+
minCheckpointAmount: "0",
|
|
1067
|
+
channelFee: "0",
|
|
1068
|
+
settlementFee: "0"
|
|
1069
|
+
},
|
|
1070
|
+
checkpoints: [],
|
|
1071
|
+
createdAt: firstCheckpoint.timestamp,
|
|
1072
|
+
updatedAt: firstCheckpoint.timestamp
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
function createStateSnapshot(_channel, stateMachine) {
|
|
1077
|
+
const snapshot = {
|
|
1078
|
+
version: 1,
|
|
1079
|
+
channel: stateMachine.getChannel(),
|
|
1080
|
+
transitions: stateMachine.getTransitions(),
|
|
1081
|
+
timestamp: Date.now()
|
|
1082
|
+
};
|
|
1083
|
+
return JSON.stringify(snapshot);
|
|
1084
|
+
}
|
|
1085
|
+
function restoreFromSnapshot(snapshotJson) {
|
|
1086
|
+
try {
|
|
1087
|
+
const snapshot = JSON.parse(snapshotJson);
|
|
1088
|
+
if (snapshot.version !== 1) {
|
|
1089
|
+
return {
|
|
1090
|
+
success: false,
|
|
1091
|
+
error: "Unsupported snapshot version"
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
const channel = snapshot.channel;
|
|
1095
|
+
const stateMachine = new ChannelStateMachine(channel);
|
|
1096
|
+
return {
|
|
1097
|
+
success: true,
|
|
1098
|
+
channel,
|
|
1099
|
+
stateMachine,
|
|
1100
|
+
dataLoss: false
|
|
1101
|
+
};
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
return {
|
|
1104
|
+
success: false,
|
|
1105
|
+
error: `Failed to parse snapshot: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/streaming/types.ts
|
|
1111
|
+
import { z as z2 } from "zod";
|
|
1112
|
+
var StreamState = z2.enum([
|
|
1113
|
+
"idle",
|
|
1114
|
+
// Stream not started
|
|
1115
|
+
"active",
|
|
1116
|
+
// Streaming in progress
|
|
1117
|
+
"paused",
|
|
1118
|
+
// Temporarily paused
|
|
1119
|
+
"completed",
|
|
1120
|
+
// Stream completed (exhausted)
|
|
1121
|
+
"cancelled"
|
|
1122
|
+
// Stream cancelled
|
|
1123
|
+
]);
|
|
1124
|
+
var RateType = z2.enum([
|
|
1125
|
+
"fixed",
|
|
1126
|
+
// Fixed rate per second
|
|
1127
|
+
"variable",
|
|
1128
|
+
// Rate can change
|
|
1129
|
+
"tiered",
|
|
1130
|
+
// Different rates based on usage
|
|
1131
|
+
"dynamic"
|
|
1132
|
+
// Demand-based pricing
|
|
1133
|
+
]);
|
|
1134
|
+
var StreamRate = z2.object({
|
|
1135
|
+
type: RateType,
|
|
1136
|
+
baseRate: z2.string(),
|
|
1137
|
+
// Base rate per second
|
|
1138
|
+
minRate: z2.string().optional(),
|
|
1139
|
+
// Minimum rate
|
|
1140
|
+
maxRate: z2.string().optional(),
|
|
1141
|
+
// Maximum rate
|
|
1142
|
+
// For tiered rates
|
|
1143
|
+
tiers: z2.array(z2.object({
|
|
1144
|
+
threshold: z2.string(),
|
|
1145
|
+
// Usage threshold
|
|
1146
|
+
rate: z2.string()
|
|
1147
|
+
// Rate after threshold
|
|
1148
|
+
})).optional(),
|
|
1149
|
+
// For dynamic rates
|
|
1150
|
+
adjustmentInterval: z2.number().optional(),
|
|
1151
|
+
// How often to adjust (seconds)
|
|
1152
|
+
adjustmentFactor: z2.number().optional()
|
|
1153
|
+
// Max adjustment per interval
|
|
1154
|
+
});
|
|
1155
|
+
var UsageMetrics = z2.object({
|
|
1156
|
+
totalSeconds: z2.number(),
|
|
1157
|
+
totalAmount: z2.string(),
|
|
1158
|
+
averageRate: z2.string(),
|
|
1159
|
+
peakRate: z2.string(),
|
|
1160
|
+
startTime: z2.number(),
|
|
1161
|
+
endTime: z2.number().optional(),
|
|
1162
|
+
// Breakdown by period
|
|
1163
|
+
hourly: z2.array(z2.object({
|
|
1164
|
+
hour: z2.number(),
|
|
1165
|
+
amount: z2.string(),
|
|
1166
|
+
seconds: z2.number()
|
|
1167
|
+
})).optional()
|
|
1168
|
+
});
|
|
1169
|
+
var MeteringRecord = z2.object({
|
|
1170
|
+
timestamp: z2.number(),
|
|
1171
|
+
duration: z2.number(),
|
|
1172
|
+
// Seconds since last record
|
|
1173
|
+
amount: z2.string(),
|
|
1174
|
+
// Amount for this period
|
|
1175
|
+
rate: z2.string(),
|
|
1176
|
+
// Rate applied
|
|
1177
|
+
cumulative: z2.string(),
|
|
1178
|
+
// Cumulative total
|
|
1179
|
+
metadata: z2.record(z2.unknown()).optional()
|
|
1180
|
+
});
|
|
1181
|
+
var BillingPeriod = z2.enum([
|
|
1182
|
+
"realtime",
|
|
1183
|
+
// Continuous real-time billing
|
|
1184
|
+
"second",
|
|
1185
|
+
// Per-second
|
|
1186
|
+
"minute",
|
|
1187
|
+
// Per-minute
|
|
1188
|
+
"hour",
|
|
1189
|
+
// Per-hour
|
|
1190
|
+
"day"
|
|
1191
|
+
// Per-day
|
|
1192
|
+
]);
|
|
1193
|
+
var BillingConfig = z2.object({
|
|
1194
|
+
period: BillingPeriod,
|
|
1195
|
+
minimumCharge: z2.string().default("0"),
|
|
1196
|
+
roundingMode: z2.enum(["floor", "ceil", "round"]).default("floor"),
|
|
1197
|
+
gracePeriod: z2.number().default(0),
|
|
1198
|
+
// Seconds of free usage
|
|
1199
|
+
invoiceInterval: z2.number().optional()
|
|
1200
|
+
// Generate invoice every N seconds
|
|
1201
|
+
});
|
|
1202
|
+
var InvoiceItem = z2.object({
|
|
1203
|
+
description: z2.string(),
|
|
1204
|
+
quantity: z2.number(),
|
|
1205
|
+
// Duration in billing periods
|
|
1206
|
+
rate: z2.string(),
|
|
1207
|
+
amount: z2.string(),
|
|
1208
|
+
startTime: z2.number(),
|
|
1209
|
+
endTime: z2.number()
|
|
1210
|
+
});
|
|
1211
|
+
var Invoice = z2.object({
|
|
1212
|
+
id: z2.string(),
|
|
1213
|
+
channelId: z2.string(),
|
|
1214
|
+
payer: z2.string(),
|
|
1215
|
+
payee: z2.string(),
|
|
1216
|
+
items: z2.array(InvoiceItem),
|
|
1217
|
+
subtotal: z2.string(),
|
|
1218
|
+
fees: z2.string(),
|
|
1219
|
+
total: z2.string(),
|
|
1220
|
+
currency: z2.string(),
|
|
1221
|
+
status: z2.enum(["pending", "paid", "settled", "disputed"]),
|
|
1222
|
+
createdAt: z2.number(),
|
|
1223
|
+
dueAt: z2.number().optional(),
|
|
1224
|
+
paidAt: z2.number().optional()
|
|
1225
|
+
});
|
|
1226
|
+
var StreamSession = z2.object({
|
|
1227
|
+
id: z2.string(),
|
|
1228
|
+
channelId: z2.string(),
|
|
1229
|
+
state: StreamState,
|
|
1230
|
+
rate: StreamRate,
|
|
1231
|
+
startedAt: z2.number().optional(),
|
|
1232
|
+
pausedAt: z2.number().optional(),
|
|
1233
|
+
endedAt: z2.number().optional(),
|
|
1234
|
+
totalDuration: z2.number(),
|
|
1235
|
+
// Total streaming seconds
|
|
1236
|
+
totalAmount: z2.string(),
|
|
1237
|
+
// Total amount streamed
|
|
1238
|
+
meteringRecords: z2.array(MeteringRecord),
|
|
1239
|
+
billingConfig: BillingConfig,
|
|
1240
|
+
invoices: z2.array(z2.string())
|
|
1241
|
+
// Invoice IDs
|
|
1242
|
+
});
|
|
1243
|
+
var RateAdjustmentRequest = z2.object({
|
|
1244
|
+
sessionId: z2.string(),
|
|
1245
|
+
newRate: z2.string(),
|
|
1246
|
+
reason: z2.string(),
|
|
1247
|
+
effectiveFrom: z2.number().optional(),
|
|
1248
|
+
// Timestamp, default now
|
|
1249
|
+
signature: z2.string().optional()
|
|
1250
|
+
// For mutual rate changes
|
|
1251
|
+
});
|
|
1252
|
+
var StreamEvent = z2.discriminatedUnion("type", [
|
|
1253
|
+
z2.object({
|
|
1254
|
+
type: z2.literal("started"),
|
|
1255
|
+
sessionId: z2.string(),
|
|
1256
|
+
timestamp: z2.number(),
|
|
1257
|
+
rate: z2.string()
|
|
1258
|
+
}),
|
|
1259
|
+
z2.object({
|
|
1260
|
+
type: z2.literal("paused"),
|
|
1261
|
+
sessionId: z2.string(),
|
|
1262
|
+
timestamp: z2.number(),
|
|
1263
|
+
totalStreamed: z2.string()
|
|
1264
|
+
}),
|
|
1265
|
+
z2.object({
|
|
1266
|
+
type: z2.literal("resumed"),
|
|
1267
|
+
sessionId: z2.string(),
|
|
1268
|
+
timestamp: z2.number()
|
|
1269
|
+
}),
|
|
1270
|
+
z2.object({
|
|
1271
|
+
type: z2.literal("rate_changed"),
|
|
1272
|
+
sessionId: z2.string(),
|
|
1273
|
+
timestamp: z2.number(),
|
|
1274
|
+
oldRate: z2.string(),
|
|
1275
|
+
newRate: z2.string()
|
|
1276
|
+
}),
|
|
1277
|
+
z2.object({
|
|
1278
|
+
type: z2.literal("checkpoint"),
|
|
1279
|
+
sessionId: z2.string(),
|
|
1280
|
+
timestamp: z2.number(),
|
|
1281
|
+
amount: z2.string(),
|
|
1282
|
+
checkpointId: z2.string()
|
|
1283
|
+
}),
|
|
1284
|
+
z2.object({
|
|
1285
|
+
type: z2.literal("completed"),
|
|
1286
|
+
sessionId: z2.string(),
|
|
1287
|
+
timestamp: z2.number(),
|
|
1288
|
+
totalAmount: z2.string(),
|
|
1289
|
+
totalDuration: z2.number()
|
|
1290
|
+
}),
|
|
1291
|
+
z2.object({
|
|
1292
|
+
type: z2.literal("cancelled"),
|
|
1293
|
+
sessionId: z2.string(),
|
|
1294
|
+
timestamp: z2.number(),
|
|
1295
|
+
reason: z2.string()
|
|
1296
|
+
})
|
|
1297
|
+
]);
|
|
1298
|
+
|
|
1299
|
+
// src/streaming/flow.ts
|
|
1300
|
+
var FlowController = class {
|
|
1301
|
+
session;
|
|
1302
|
+
config;
|
|
1303
|
+
updateTimer;
|
|
1304
|
+
checkpointTimer;
|
|
1305
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
1306
|
+
lastUpdateTime = 0;
|
|
1307
|
+
constructor(channelId, rate, billingConfig, config = {}) {
|
|
1308
|
+
this.config = {
|
|
1309
|
+
updateInterval: config.updateInterval ?? 1e3,
|
|
1310
|
+
bufferTime: config.bufferTime ?? 60,
|
|
1311
|
+
autoCheckpoint: config.autoCheckpoint ?? true,
|
|
1312
|
+
checkpointInterval: config.checkpointInterval ?? 3600
|
|
1313
|
+
};
|
|
1314
|
+
this.session = {
|
|
1315
|
+
id: this.generateSessionId(),
|
|
1316
|
+
channelId,
|
|
1317
|
+
state: "idle",
|
|
1318
|
+
rate,
|
|
1319
|
+
totalDuration: 0,
|
|
1320
|
+
totalAmount: "0",
|
|
1321
|
+
meteringRecords: [],
|
|
1322
|
+
billingConfig,
|
|
1323
|
+
invoices: []
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Get current session state
|
|
1328
|
+
*/
|
|
1329
|
+
getSession() {
|
|
1330
|
+
return { ...this.session };
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Get current state
|
|
1334
|
+
*/
|
|
1335
|
+
getState() {
|
|
1336
|
+
return this.session.state;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Start streaming
|
|
1340
|
+
*/
|
|
1341
|
+
start() {
|
|
1342
|
+
if (this.session.state !== "idle" && this.session.state !== "paused") {
|
|
1343
|
+
return { success: false, error: "Stream must be idle or paused to start" };
|
|
1344
|
+
}
|
|
1345
|
+
const now = Date.now();
|
|
1346
|
+
if (this.session.state === "idle") {
|
|
1347
|
+
this.session.startedAt = now;
|
|
1348
|
+
} else {
|
|
1349
|
+
const pauseDuration = now - (this.session.pausedAt ?? now);
|
|
1350
|
+
this.session.pausedAt = void 0;
|
|
1351
|
+
this.addMeteringRecord({
|
|
1352
|
+
timestamp: now,
|
|
1353
|
+
duration: 0,
|
|
1354
|
+
amount: "0",
|
|
1355
|
+
rate: "0",
|
|
1356
|
+
cumulative: this.session.totalAmount,
|
|
1357
|
+
metadata: { event: "resume", pauseDuration }
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
this.session.state = "active";
|
|
1361
|
+
this.lastUpdateTime = now;
|
|
1362
|
+
this.startUpdateTimer();
|
|
1363
|
+
if (this.config.autoCheckpoint) {
|
|
1364
|
+
this.startCheckpointTimer();
|
|
1365
|
+
}
|
|
1366
|
+
this.emitEvent({
|
|
1367
|
+
type: "started",
|
|
1368
|
+
sessionId: this.session.id,
|
|
1369
|
+
timestamp: now,
|
|
1370
|
+
rate: this.session.rate.baseRate
|
|
1371
|
+
});
|
|
1372
|
+
return { success: true };
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Pause streaming
|
|
1376
|
+
*/
|
|
1377
|
+
pause() {
|
|
1378
|
+
if (this.session.state !== "active") {
|
|
1379
|
+
return { success: false, error: "Stream must be active to pause" };
|
|
1380
|
+
}
|
|
1381
|
+
const now = Date.now();
|
|
1382
|
+
this.updateTotals(now);
|
|
1383
|
+
this.session.state = "paused";
|
|
1384
|
+
this.session.pausedAt = now;
|
|
1385
|
+
this.stopTimers();
|
|
1386
|
+
this.emitEvent({
|
|
1387
|
+
type: "paused",
|
|
1388
|
+
sessionId: this.session.id,
|
|
1389
|
+
timestamp: now,
|
|
1390
|
+
totalStreamed: this.session.totalAmount
|
|
1391
|
+
});
|
|
1392
|
+
return { success: true };
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Resume streaming (alias for start when paused)
|
|
1396
|
+
*/
|
|
1397
|
+
resume() {
|
|
1398
|
+
if (this.session.state !== "paused") {
|
|
1399
|
+
return { success: false, error: "Stream must be paused to resume" };
|
|
1400
|
+
}
|
|
1401
|
+
const result = this.start();
|
|
1402
|
+
if (result.success) {
|
|
1403
|
+
this.emitEvent({
|
|
1404
|
+
type: "resumed",
|
|
1405
|
+
sessionId: this.session.id,
|
|
1406
|
+
timestamp: Date.now()
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
return result;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Stop streaming (complete)
|
|
1413
|
+
*/
|
|
1414
|
+
stop() {
|
|
1415
|
+
const now = Date.now();
|
|
1416
|
+
if (this.session.state === "active") {
|
|
1417
|
+
this.updateTotals(now);
|
|
1418
|
+
}
|
|
1419
|
+
this.stopTimers();
|
|
1420
|
+
this.session.state = "completed";
|
|
1421
|
+
this.session.endedAt = now;
|
|
1422
|
+
this.emitEvent({
|
|
1423
|
+
type: "completed",
|
|
1424
|
+
sessionId: this.session.id,
|
|
1425
|
+
timestamp: now,
|
|
1426
|
+
totalAmount: this.session.totalAmount,
|
|
1427
|
+
totalDuration: this.session.totalDuration
|
|
1428
|
+
});
|
|
1429
|
+
return {
|
|
1430
|
+
success: true,
|
|
1431
|
+
finalAmount: this.session.totalAmount
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Cancel streaming
|
|
1436
|
+
*/
|
|
1437
|
+
cancel(reason) {
|
|
1438
|
+
const now = Date.now();
|
|
1439
|
+
if (this.session.state === "active") {
|
|
1440
|
+
this.updateTotals(now);
|
|
1441
|
+
}
|
|
1442
|
+
this.stopTimers();
|
|
1443
|
+
this.session.state = "cancelled";
|
|
1444
|
+
this.session.endedAt = now;
|
|
1445
|
+
this.emitEvent({
|
|
1446
|
+
type: "cancelled",
|
|
1447
|
+
sessionId: this.session.id,
|
|
1448
|
+
timestamp: now,
|
|
1449
|
+
reason
|
|
1450
|
+
});
|
|
1451
|
+
return { success: true };
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Get current streamed amount
|
|
1455
|
+
*/
|
|
1456
|
+
getCurrentAmount() {
|
|
1457
|
+
if (this.session.state !== "active") {
|
|
1458
|
+
return this.session.totalAmount;
|
|
1459
|
+
}
|
|
1460
|
+
const now = Date.now();
|
|
1461
|
+
const elapsed = Math.floor((now - this.lastUpdateTime) / 1e3);
|
|
1462
|
+
const additionalAmount = this.calculateAmount(elapsed, this.session.rate);
|
|
1463
|
+
return (BigInt(this.session.totalAmount) + BigInt(additionalAmount)).toString();
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Get current rate
|
|
1467
|
+
*/
|
|
1468
|
+
getCurrentRate() {
|
|
1469
|
+
return this.getEffectiveRate(this.session.rate, this.session.totalAmount);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Get time until exhaustion (returns -1 if infinite)
|
|
1473
|
+
*/
|
|
1474
|
+
getTimeUntilExhaustion(channelCapacity) {
|
|
1475
|
+
if (this.session.state !== "active") {
|
|
1476
|
+
return -1;
|
|
1477
|
+
}
|
|
1478
|
+
const remaining = BigInt(channelCapacity) - BigInt(this.getCurrentAmount());
|
|
1479
|
+
if (remaining <= 0n) {
|
|
1480
|
+
return 0;
|
|
1481
|
+
}
|
|
1482
|
+
const rate = BigInt(this.getCurrentRate());
|
|
1483
|
+
if (rate <= 0n) {
|
|
1484
|
+
return -1;
|
|
1485
|
+
}
|
|
1486
|
+
return Number(remaining / rate);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Check if stream is near exhaustion
|
|
1490
|
+
*/
|
|
1491
|
+
isNearExhaustion(channelCapacity) {
|
|
1492
|
+
const remaining = this.getTimeUntilExhaustion(channelCapacity);
|
|
1493
|
+
return remaining >= 0 && remaining <= this.config.bufferTime;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Create manual checkpoint
|
|
1497
|
+
*/
|
|
1498
|
+
createCheckpoint() {
|
|
1499
|
+
const now = Date.now();
|
|
1500
|
+
const amount = this.getCurrentAmount();
|
|
1501
|
+
const checkpointId = `cp_${this.session.id}_${now}`;
|
|
1502
|
+
this.emitEvent({
|
|
1503
|
+
type: "checkpoint",
|
|
1504
|
+
sessionId: this.session.id,
|
|
1505
|
+
timestamp: now,
|
|
1506
|
+
amount,
|
|
1507
|
+
checkpointId
|
|
1508
|
+
});
|
|
1509
|
+
return { id: checkpointId, amount, timestamp: now };
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Subscribe to stream events
|
|
1513
|
+
*/
|
|
1514
|
+
onEvent(callback) {
|
|
1515
|
+
this.eventListeners.add(callback);
|
|
1516
|
+
return () => this.eventListeners.delete(callback);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Clean up resources
|
|
1520
|
+
*/
|
|
1521
|
+
destroy() {
|
|
1522
|
+
this.stopTimers();
|
|
1523
|
+
this.eventListeners.clear();
|
|
1524
|
+
}
|
|
1525
|
+
startUpdateTimer() {
|
|
1526
|
+
this.updateTimer = setInterval(() => {
|
|
1527
|
+
if (this.session.state === "active") {
|
|
1528
|
+
this.updateTotals(Date.now());
|
|
1529
|
+
}
|
|
1530
|
+
}, this.config.updateInterval);
|
|
1531
|
+
}
|
|
1532
|
+
startCheckpointTimer() {
|
|
1533
|
+
this.checkpointTimer = setInterval(() => {
|
|
1534
|
+
if (this.session.state === "active") {
|
|
1535
|
+
this.createCheckpoint();
|
|
1536
|
+
}
|
|
1537
|
+
}, this.config.checkpointInterval * 1e3);
|
|
1538
|
+
}
|
|
1539
|
+
stopTimers() {
|
|
1540
|
+
if (this.updateTimer) {
|
|
1541
|
+
clearInterval(this.updateTimer);
|
|
1542
|
+
this.updateTimer = void 0;
|
|
1543
|
+
}
|
|
1544
|
+
if (this.checkpointTimer) {
|
|
1545
|
+
clearInterval(this.checkpointTimer);
|
|
1546
|
+
this.checkpointTimer = void 0;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
updateTotals(now) {
|
|
1550
|
+
const elapsed = Math.floor((now - this.lastUpdateTime) / 1e3);
|
|
1551
|
+
if (elapsed <= 0) return;
|
|
1552
|
+
const amount = this.calculateAmount(elapsed, this.session.rate);
|
|
1553
|
+
const newTotal = BigInt(this.session.totalAmount) + BigInt(amount);
|
|
1554
|
+
this.session.totalDuration += elapsed;
|
|
1555
|
+
this.session.totalAmount = newTotal.toString();
|
|
1556
|
+
this.lastUpdateTime = now;
|
|
1557
|
+
this.addMeteringRecord({
|
|
1558
|
+
timestamp: now,
|
|
1559
|
+
duration: elapsed,
|
|
1560
|
+
amount,
|
|
1561
|
+
rate: this.getCurrentRate(),
|
|
1562
|
+
cumulative: this.session.totalAmount
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
calculateAmount(seconds, rate) {
|
|
1566
|
+
const effectiveRate = this.getEffectiveRate(rate, this.session.totalAmount);
|
|
1567
|
+
return (BigInt(effectiveRate) * BigInt(seconds)).toString();
|
|
1568
|
+
}
|
|
1569
|
+
getEffectiveRate(rate, totalAmount) {
|
|
1570
|
+
if (rate.type === "fixed") {
|
|
1571
|
+
return rate.baseRate;
|
|
1572
|
+
}
|
|
1573
|
+
if (rate.type === "tiered" && rate.tiers) {
|
|
1574
|
+
const amount = BigInt(totalAmount);
|
|
1575
|
+
let applicableRate = rate.baseRate;
|
|
1576
|
+
for (const tier of rate.tiers) {
|
|
1577
|
+
if (amount >= BigInt(tier.threshold)) {
|
|
1578
|
+
applicableRate = tier.rate;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return applicableRate;
|
|
1582
|
+
}
|
|
1583
|
+
return rate.baseRate;
|
|
1584
|
+
}
|
|
1585
|
+
addMeteringRecord(record) {
|
|
1586
|
+
this.session.meteringRecords.push(record);
|
|
1587
|
+
if (this.session.meteringRecords.length > 1e3) {
|
|
1588
|
+
this.session.meteringRecords = this.session.meteringRecords.slice(-1e3);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
emitEvent(event) {
|
|
1592
|
+
this.eventListeners.forEach((callback) => {
|
|
1593
|
+
try {
|
|
1594
|
+
callback(event);
|
|
1595
|
+
} catch {
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
generateSessionId() {
|
|
1600
|
+
return `ss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
function createFixedRateFlow(channelId, ratePerSecond, config) {
|
|
1604
|
+
const rate = {
|
|
1605
|
+
type: "fixed",
|
|
1606
|
+
baseRate: ratePerSecond
|
|
1607
|
+
};
|
|
1608
|
+
const billingConfig = {
|
|
1609
|
+
period: "realtime",
|
|
1610
|
+
minimumCharge: "0",
|
|
1611
|
+
roundingMode: "floor",
|
|
1612
|
+
gracePeriod: 0
|
|
1613
|
+
};
|
|
1614
|
+
return new FlowController(channelId, rate, billingConfig, config);
|
|
1615
|
+
}
|
|
1616
|
+
function createTieredRateFlow(channelId, baseRate, tiers, config) {
|
|
1617
|
+
const rate = {
|
|
1618
|
+
type: "tiered",
|
|
1619
|
+
baseRate,
|
|
1620
|
+
tiers
|
|
1621
|
+
};
|
|
1622
|
+
const billingConfig = {
|
|
1623
|
+
period: "realtime",
|
|
1624
|
+
minimumCharge: "0",
|
|
1625
|
+
roundingMode: "floor",
|
|
1626
|
+
gracePeriod: 0
|
|
1627
|
+
};
|
|
1628
|
+
return new FlowController(channelId, rate, billingConfig, config);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/streaming/rate.ts
|
|
1632
|
+
var RateController = class {
|
|
1633
|
+
currentRate;
|
|
1634
|
+
config;
|
|
1635
|
+
history = [];
|
|
1636
|
+
lastChangeTime = 0;
|
|
1637
|
+
constructor(initialRate, config = {}) {
|
|
1638
|
+
this.currentRate = { ...initialRate };
|
|
1639
|
+
this.config = {
|
|
1640
|
+
maxChangePercent: config.maxChangePercent ?? 50,
|
|
1641
|
+
minChangeInterval: config.minChangeInterval ?? 60,
|
|
1642
|
+
smoothingFactor: config.smoothingFactor ?? 0.3
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Get current rate configuration
|
|
1647
|
+
*/
|
|
1648
|
+
getRate() {
|
|
1649
|
+
return { ...this.currentRate };
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Get current effective rate
|
|
1653
|
+
*/
|
|
1654
|
+
getEffectiveRate(totalUsage) {
|
|
1655
|
+
if (this.currentRate.type === "fixed") {
|
|
1656
|
+
return this.currentRate.baseRate;
|
|
1657
|
+
}
|
|
1658
|
+
if (this.currentRate.type === "tiered" && this.currentRate.tiers && totalUsage) {
|
|
1659
|
+
return this.calculateTieredRate(totalUsage);
|
|
1660
|
+
}
|
|
1661
|
+
return this.currentRate.baseRate;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Request rate adjustment
|
|
1665
|
+
*/
|
|
1666
|
+
adjustRate(request) {
|
|
1667
|
+
const now = Date.now();
|
|
1668
|
+
const timeSinceLastChange = (now - this.lastChangeTime) / 1e3;
|
|
1669
|
+
if (timeSinceLastChange < this.config.minChangeInterval) {
|
|
1670
|
+
return {
|
|
1671
|
+
success: false,
|
|
1672
|
+
error: `Rate can only be changed every ${this.config.minChangeInterval} seconds`
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
const newRateBigInt = BigInt(request.newRate);
|
|
1676
|
+
if (newRateBigInt <= 0n) {
|
|
1677
|
+
return { success: false, error: "Rate must be positive" };
|
|
1678
|
+
}
|
|
1679
|
+
if (this.currentRate.minRate && newRateBigInt < BigInt(this.currentRate.minRate)) {
|
|
1680
|
+
return {
|
|
1681
|
+
success: false,
|
|
1682
|
+
error: `Rate cannot be below minimum: ${this.currentRate.minRate}`
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
if (this.currentRate.maxRate && newRateBigInt > BigInt(this.currentRate.maxRate)) {
|
|
1686
|
+
return {
|
|
1687
|
+
success: false,
|
|
1688
|
+
error: `Rate cannot exceed maximum: ${this.currentRate.maxRate}`
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const currentRateBigInt = BigInt(this.currentRate.baseRate);
|
|
1692
|
+
const changePercent = this.calculateChangePercent(currentRateBigInt, newRateBigInt);
|
|
1693
|
+
let adjustedRate = request.newRate;
|
|
1694
|
+
if (changePercent > this.config.maxChangePercent) {
|
|
1695
|
+
adjustedRate = this.applyMaxChange(
|
|
1696
|
+
currentRateBigInt,
|
|
1697
|
+
newRateBigInt,
|
|
1698
|
+
this.config.maxChangePercent
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
this.history.push({
|
|
1702
|
+
timestamp: now,
|
|
1703
|
+
rate: adjustedRate,
|
|
1704
|
+
reason: request.reason,
|
|
1705
|
+
previousRate: this.currentRate.baseRate
|
|
1706
|
+
});
|
|
1707
|
+
this.currentRate.baseRate = adjustedRate;
|
|
1708
|
+
this.lastChangeTime = now;
|
|
1709
|
+
return {
|
|
1710
|
+
success: true,
|
|
1711
|
+
newRate: adjustedRate,
|
|
1712
|
+
adjustedAmount: adjustedRate !== request.newRate ? adjustedRate : void 0
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Set rate bounds
|
|
1717
|
+
*/
|
|
1718
|
+
setBounds(minRate, maxRate) {
|
|
1719
|
+
if (minRate !== void 0) {
|
|
1720
|
+
this.currentRate.minRate = minRate;
|
|
1721
|
+
}
|
|
1722
|
+
if (maxRate !== void 0) {
|
|
1723
|
+
this.currentRate.maxRate = maxRate;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Add or update a tier
|
|
1728
|
+
*/
|
|
1729
|
+
addTier(threshold, rate) {
|
|
1730
|
+
if (this.currentRate.type !== "tiered") {
|
|
1731
|
+
this.currentRate.type = "tiered";
|
|
1732
|
+
this.currentRate.tiers = [];
|
|
1733
|
+
}
|
|
1734
|
+
const tiers = this.currentRate.tiers ?? [];
|
|
1735
|
+
const existingIndex = tiers.findIndex((t) => t.threshold === threshold);
|
|
1736
|
+
if (existingIndex >= 0) {
|
|
1737
|
+
tiers[existingIndex].rate = rate;
|
|
1738
|
+
} else {
|
|
1739
|
+
tiers.push({ threshold, rate });
|
|
1740
|
+
tiers.sort((a, b) => {
|
|
1741
|
+
return BigInt(a.threshold) < BigInt(b.threshold) ? -1 : 1;
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
this.currentRate.tiers = tiers;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Remove a tier
|
|
1748
|
+
*/
|
|
1749
|
+
removeTier(threshold) {
|
|
1750
|
+
if (!this.currentRate.tiers) return false;
|
|
1751
|
+
const initialLength = this.currentRate.tiers.length;
|
|
1752
|
+
this.currentRate.tiers = this.currentRate.tiers.filter(
|
|
1753
|
+
(t) => t.threshold !== threshold
|
|
1754
|
+
);
|
|
1755
|
+
return this.currentRate.tiers.length < initialLength;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Get rate history
|
|
1759
|
+
*/
|
|
1760
|
+
getHistory() {
|
|
1761
|
+
return [...this.history];
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Calculate average rate over time period
|
|
1765
|
+
*/
|
|
1766
|
+
getAverageRate(startTime, endTime) {
|
|
1767
|
+
const relevantHistory = this.history.filter(
|
|
1768
|
+
(h) => h.timestamp >= startTime && h.timestamp <= endTime
|
|
1769
|
+
);
|
|
1770
|
+
if (relevantHistory.length === 0) {
|
|
1771
|
+
return this.currentRate.baseRate;
|
|
1772
|
+
}
|
|
1773
|
+
let totalWeight = 0n;
|
|
1774
|
+
let weightedSum = 0n;
|
|
1775
|
+
let prevTime = startTime;
|
|
1776
|
+
for (const entry of relevantHistory) {
|
|
1777
|
+
const duration = BigInt(entry.timestamp - prevTime);
|
|
1778
|
+
weightedSum += BigInt(entry.previousRate) * duration;
|
|
1779
|
+
totalWeight += duration;
|
|
1780
|
+
prevTime = entry.timestamp;
|
|
1781
|
+
}
|
|
1782
|
+
const finalDuration = BigInt(endTime - prevTime);
|
|
1783
|
+
weightedSum += BigInt(this.currentRate.baseRate) * finalDuration;
|
|
1784
|
+
totalWeight += finalDuration;
|
|
1785
|
+
if (totalWeight === 0n) {
|
|
1786
|
+
return this.currentRate.baseRate;
|
|
1787
|
+
}
|
|
1788
|
+
return (weightedSum / totalWeight).toString();
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Calculate rate for dynamic pricing based on demand
|
|
1792
|
+
*/
|
|
1793
|
+
calculateDynamicRate(demand, _baseRate) {
|
|
1794
|
+
const base = BigInt(this.currentRate.baseRate);
|
|
1795
|
+
const min = this.currentRate.minRate ? BigInt(this.currentRate.minRate) : base / 2n;
|
|
1796
|
+
const max = this.currentRate.maxRate ? BigInt(this.currentRate.maxRate) : base * 2n;
|
|
1797
|
+
const range = max - min;
|
|
1798
|
+
const adjustment = BigInt(Math.floor(Number(range) * demand));
|
|
1799
|
+
return (min + adjustment).toString();
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Apply exponential smoothing for rate changes
|
|
1803
|
+
*/
|
|
1804
|
+
smoothRate(targetRate) {
|
|
1805
|
+
const current = BigInt(this.currentRate.baseRate);
|
|
1806
|
+
const target = BigInt(targetRate);
|
|
1807
|
+
const alpha = this.config.smoothingFactor;
|
|
1808
|
+
const smoothed = BigInt(Math.floor(
|
|
1809
|
+
alpha * Number(target) + (1 - alpha) * Number(current)
|
|
1810
|
+
));
|
|
1811
|
+
return smoothed.toString();
|
|
1812
|
+
}
|
|
1813
|
+
calculateTieredRate(totalUsage) {
|
|
1814
|
+
const usage = BigInt(totalUsage);
|
|
1815
|
+
const tiers = this.currentRate.tiers ?? [];
|
|
1816
|
+
let applicableRate = this.currentRate.baseRate;
|
|
1817
|
+
for (const tier of tiers) {
|
|
1818
|
+
if (usage >= BigInt(tier.threshold)) {
|
|
1819
|
+
applicableRate = tier.rate;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return applicableRate;
|
|
1823
|
+
}
|
|
1824
|
+
calculateChangePercent(from, to) {
|
|
1825
|
+
if (from === 0n) return 100;
|
|
1826
|
+
const diff = to > from ? to - from : from - to;
|
|
1827
|
+
return Number(diff * 100n / from);
|
|
1828
|
+
}
|
|
1829
|
+
applyMaxChange(from, to, maxPercent) {
|
|
1830
|
+
const maxChange = from * BigInt(maxPercent) / 100n;
|
|
1831
|
+
if (to > from) {
|
|
1832
|
+
return (from + maxChange).toString();
|
|
1833
|
+
} else {
|
|
1834
|
+
return (from - maxChange).toString();
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
var RateLimiter = class {
|
|
1839
|
+
requests = /* @__PURE__ */ new Map();
|
|
1840
|
+
maxRequests;
|
|
1841
|
+
windowMs;
|
|
1842
|
+
constructor(maxRequests = 10, windowMs = 6e4) {
|
|
1843
|
+
this.maxRequests = maxRequests;
|
|
1844
|
+
this.windowMs = windowMs;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Check if request is allowed
|
|
1848
|
+
*/
|
|
1849
|
+
isAllowed(key) {
|
|
1850
|
+
const now = Date.now();
|
|
1851
|
+
const windowStart = now - this.windowMs;
|
|
1852
|
+
let requests = this.requests.get(key) ?? [];
|
|
1853
|
+
requests = requests.filter((t) => t > windowStart);
|
|
1854
|
+
if (requests.length >= this.maxRequests) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
requests.push(now);
|
|
1858
|
+
this.requests.set(key, requests);
|
|
1859
|
+
return true;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Get remaining requests in window
|
|
1863
|
+
*/
|
|
1864
|
+
getRemainingRequests(key) {
|
|
1865
|
+
const now = Date.now();
|
|
1866
|
+
const windowStart = now - this.windowMs;
|
|
1867
|
+
const requests = (this.requests.get(key) ?? []).filter((t) => t > windowStart);
|
|
1868
|
+
return Math.max(0, this.maxRequests - requests.length);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Reset limits for a key
|
|
1872
|
+
*/
|
|
1873
|
+
reset(key) {
|
|
1874
|
+
this.requests.delete(key);
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Clear all limits
|
|
1878
|
+
*/
|
|
1879
|
+
clear() {
|
|
1880
|
+
this.requests.clear();
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
function calculateOptimalRate(channelCapacity, desiredDurationSeconds, bufferPercent = 10) {
|
|
1884
|
+
const capacity = BigInt(channelCapacity);
|
|
1885
|
+
const duration = BigInt(desiredDurationSeconds);
|
|
1886
|
+
if (duration === 0n) {
|
|
1887
|
+
return "0";
|
|
1888
|
+
}
|
|
1889
|
+
const effectiveCapacity = capacity * BigInt(100 - bufferPercent) / 100n;
|
|
1890
|
+
return (effectiveCapacity / duration).toString();
|
|
1891
|
+
}
|
|
1892
|
+
function convertRate(rate, fromUnit, toUnit) {
|
|
1893
|
+
const unitToSeconds = {
|
|
1894
|
+
second: 1,
|
|
1895
|
+
minute: 60,
|
|
1896
|
+
hour: 3600,
|
|
1897
|
+
day: 86400
|
|
1898
|
+
};
|
|
1899
|
+
const fromSeconds = unitToSeconds[fromUnit];
|
|
1900
|
+
const toSeconds = unitToSeconds[toUnit];
|
|
1901
|
+
const rateBigInt = BigInt(rate);
|
|
1902
|
+
const perSecond = rateBigInt / BigInt(fromSeconds);
|
|
1903
|
+
return (perSecond * BigInt(toSeconds)).toString();
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/streaming/metering.ts
|
|
1907
|
+
var MeteringManager = class {
|
|
1908
|
+
records = [];
|
|
1909
|
+
config;
|
|
1910
|
+
sessionId;
|
|
1911
|
+
startTime;
|
|
1912
|
+
constructor(sessionId, config = {}) {
|
|
1913
|
+
this.sessionId = sessionId;
|
|
1914
|
+
this.startTime = Date.now();
|
|
1915
|
+
this.config = {
|
|
1916
|
+
recordInterval: config.recordInterval ?? 1,
|
|
1917
|
+
aggregationInterval: config.aggregationInterval ?? 3600,
|
|
1918
|
+
maxRecords: config.maxRecords ?? 1e4,
|
|
1919
|
+
precision: config.precision ?? 18
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Record usage
|
|
1924
|
+
*/
|
|
1925
|
+
record(duration, amount, rate, metadata) {
|
|
1926
|
+
const cumulative = this.calculateCumulative(amount);
|
|
1927
|
+
const record = {
|
|
1928
|
+
timestamp: Date.now(),
|
|
1929
|
+
duration,
|
|
1930
|
+
amount,
|
|
1931
|
+
rate,
|
|
1932
|
+
cumulative,
|
|
1933
|
+
metadata
|
|
1934
|
+
};
|
|
1935
|
+
this.records.push(record);
|
|
1936
|
+
if (this.records.length > this.config.maxRecords) {
|
|
1937
|
+
this.pruneRecords();
|
|
1938
|
+
}
|
|
1939
|
+
return record;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Get all records
|
|
1943
|
+
*/
|
|
1944
|
+
getRecords() {
|
|
1945
|
+
return [...this.records];
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Get records in time range
|
|
1949
|
+
*/
|
|
1950
|
+
getRecordsInRange(startTime, endTime) {
|
|
1951
|
+
return this.records.filter(
|
|
1952
|
+
(r) => r.timestamp >= startTime && r.timestamp <= endTime
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Calculate usage metrics
|
|
1957
|
+
*/
|
|
1958
|
+
getMetrics() {
|
|
1959
|
+
if (this.records.length === 0) {
|
|
1960
|
+
return {
|
|
1961
|
+
totalSeconds: 0,
|
|
1962
|
+
totalAmount: "0",
|
|
1963
|
+
averageRate: "0",
|
|
1964
|
+
peakRate: "0",
|
|
1965
|
+
startTime: this.startTime
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
const totalSeconds = this.records.reduce((sum, r) => sum + r.duration, 0);
|
|
1969
|
+
const totalAmount = this.records[this.records.length - 1].cumulative;
|
|
1970
|
+
const rates = this.records.map((r) => BigInt(r.rate));
|
|
1971
|
+
const peakRate = rates.length > 0 ? rates.reduce((max, r) => r > max ? r : max, 0n) : 0n;
|
|
1972
|
+
const averageRate = totalSeconds > 0 ? (BigInt(totalAmount) / BigInt(totalSeconds)).toString() : "0";
|
|
1973
|
+
return {
|
|
1974
|
+
totalSeconds,
|
|
1975
|
+
totalAmount,
|
|
1976
|
+
averageRate,
|
|
1977
|
+
peakRate: peakRate.toString(),
|
|
1978
|
+
startTime: this.startTime,
|
|
1979
|
+
endTime: this.records[this.records.length - 1].timestamp,
|
|
1980
|
+
hourly: this.getHourlyBreakdown()
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Get aggregated usage by period
|
|
1985
|
+
*/
|
|
1986
|
+
aggregate(intervalSeconds = 3600) {
|
|
1987
|
+
if (this.records.length === 0) return [];
|
|
1988
|
+
const aggregated = [];
|
|
1989
|
+
const intervalMs = intervalSeconds * 1e3;
|
|
1990
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1991
|
+
for (const record of this.records) {
|
|
1992
|
+
const periodStart = Math.floor(record.timestamp / intervalMs) * intervalMs;
|
|
1993
|
+
const existing = groups.get(periodStart) ?? [];
|
|
1994
|
+
existing.push(record);
|
|
1995
|
+
groups.set(periodStart, existing);
|
|
1996
|
+
}
|
|
1997
|
+
for (const [periodStart, records] of groups) {
|
|
1998
|
+
const totalAmount = records.reduce(
|
|
1999
|
+
(sum, r) => sum + BigInt(r.amount),
|
|
2000
|
+
0n
|
|
2001
|
+
);
|
|
2002
|
+
const totalDuration = records.reduce((sum, r) => sum + r.duration, 0);
|
|
2003
|
+
const averageRate = totalDuration > 0 ? (totalAmount / BigInt(totalDuration)).toString() : "0";
|
|
2004
|
+
aggregated.push({
|
|
2005
|
+
period: new Date(periodStart).toISOString(),
|
|
2006
|
+
totalAmount: totalAmount.toString(),
|
|
2007
|
+
totalDuration,
|
|
2008
|
+
averageRate,
|
|
2009
|
+
recordCount: records.length
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
return aggregated.sort((a, b) => a.period.localeCompare(b.period));
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Get cumulative amount at a point in time
|
|
2016
|
+
*/
|
|
2017
|
+
getCumulativeAt(timestamp) {
|
|
2018
|
+
for (let i = this.records.length - 1; i >= 0; i--) {
|
|
2019
|
+
if (this.records[i].timestamp <= timestamp) {
|
|
2020
|
+
return this.records[i].cumulative;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return "0";
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Calculate usage for billing period
|
|
2027
|
+
*/
|
|
2028
|
+
getUsageForBillingPeriod(startTime, endTime) {
|
|
2029
|
+
const periodRecords = this.getRecordsInRange(startTime, endTime);
|
|
2030
|
+
const amount = periodRecords.reduce(
|
|
2031
|
+
(sum, r) => sum + BigInt(r.amount),
|
|
2032
|
+
0n
|
|
2033
|
+
);
|
|
2034
|
+
const duration = periodRecords.reduce((sum, r) => sum + r.duration, 0);
|
|
2035
|
+
return {
|
|
2036
|
+
amount: amount.toString(),
|
|
2037
|
+
duration,
|
|
2038
|
+
records: periodRecords.length
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Export records for backup/audit
|
|
2043
|
+
*/
|
|
2044
|
+
export() {
|
|
2045
|
+
return JSON.stringify({
|
|
2046
|
+
sessionId: this.sessionId,
|
|
2047
|
+
startTime: this.startTime,
|
|
2048
|
+
records: this.records,
|
|
2049
|
+
exportedAt: Date.now()
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Import records from backup
|
|
2054
|
+
*/
|
|
2055
|
+
import(data) {
|
|
2056
|
+
try {
|
|
2057
|
+
const parsed = JSON.parse(data);
|
|
2058
|
+
if (parsed.sessionId !== this.sessionId) {
|
|
2059
|
+
return { success: false, recordsImported: 0 };
|
|
2060
|
+
}
|
|
2061
|
+
const importedRecords = parsed.records;
|
|
2062
|
+
let importedCount = 0;
|
|
2063
|
+
for (const record of importedRecords) {
|
|
2064
|
+
const exists = this.records.some((r) => r.timestamp === record.timestamp);
|
|
2065
|
+
if (!exists) {
|
|
2066
|
+
this.records.push(record);
|
|
2067
|
+
importedCount++;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
this.records.sort((a, b) => a.timestamp - b.timestamp);
|
|
2071
|
+
return { success: true, recordsImported: importedCount };
|
|
2072
|
+
} catch {
|
|
2073
|
+
return { success: false, recordsImported: 0 };
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Clear all records
|
|
2078
|
+
*/
|
|
2079
|
+
clear() {
|
|
2080
|
+
this.records = [];
|
|
2081
|
+
}
|
|
2082
|
+
calculateCumulative(newAmount) {
|
|
2083
|
+
if (this.records.length === 0) {
|
|
2084
|
+
return newAmount;
|
|
2085
|
+
}
|
|
2086
|
+
const lastCumulative = BigInt(this.records[this.records.length - 1].cumulative);
|
|
2087
|
+
return (lastCumulative + BigInt(newAmount)).toString();
|
|
2088
|
+
}
|
|
2089
|
+
pruneRecords() {
|
|
2090
|
+
const keepCount = this.config.maxRecords;
|
|
2091
|
+
if (this.records.length <= keepCount) return;
|
|
2092
|
+
const first = this.records[0];
|
|
2093
|
+
const recent = this.records.slice(-(keepCount - 1));
|
|
2094
|
+
this.records = [first, ...recent];
|
|
2095
|
+
}
|
|
2096
|
+
getHourlyBreakdown() {
|
|
2097
|
+
const hourlyMap = /* @__PURE__ */ new Map();
|
|
2098
|
+
for (const record of this.records) {
|
|
2099
|
+
const hour = new Date(record.timestamp).getUTCHours();
|
|
2100
|
+
const existing = hourlyMap.get(hour) ?? { amount: 0n, seconds: 0 };
|
|
2101
|
+
existing.amount += BigInt(record.amount);
|
|
2102
|
+
existing.seconds += record.duration;
|
|
2103
|
+
hourlyMap.set(hour, existing);
|
|
2104
|
+
}
|
|
2105
|
+
return Array.from(hourlyMap.entries()).map(([hour, data]) => ({
|
|
2106
|
+
hour,
|
|
2107
|
+
amount: data.amount.toString(),
|
|
2108
|
+
seconds: data.seconds
|
|
2109
|
+
}));
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
function calculateProRatedUsage(fullPeriodAmount, fullPeriodSeconds, actualSeconds) {
|
|
2113
|
+
if (fullPeriodSeconds === 0) return "0";
|
|
2114
|
+
const amount = BigInt(fullPeriodAmount);
|
|
2115
|
+
return (amount * BigInt(actualSeconds) / BigInt(fullPeriodSeconds)).toString();
|
|
2116
|
+
}
|
|
2117
|
+
function estimateUsage(metrics, futureSeconds) {
|
|
2118
|
+
if (metrics.totalSeconds === 0) return "0";
|
|
2119
|
+
const avgRate = BigInt(metrics.averageRate);
|
|
2120
|
+
return (avgRate * BigInt(futureSeconds)).toString();
|
|
2121
|
+
}
|
|
2122
|
+
function compareUsage(current, previous) {
|
|
2123
|
+
const currentAmount = BigInt(current.totalAmount);
|
|
2124
|
+
const previousAmount = BigInt(previous.totalAmount);
|
|
2125
|
+
const amountChange = currentAmount - previousAmount;
|
|
2126
|
+
const amountChangePercent = previousAmount > 0n ? Number(amountChange * 100n / previousAmount) : 0;
|
|
2127
|
+
const currentRate = BigInt(current.averageRate);
|
|
2128
|
+
const previousRate = BigInt(previous.averageRate);
|
|
2129
|
+
const rateChange = currentRate - previousRate;
|
|
2130
|
+
const rateChangePercent = previousRate > 0n ? Number(rateChange * 100n / previousRate) : 0;
|
|
2131
|
+
return {
|
|
2132
|
+
amountChange: amountChange.toString(),
|
|
2133
|
+
amountChangePercent,
|
|
2134
|
+
rateChange: rateChange.toString(),
|
|
2135
|
+
rateChangePercent
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// src/streaming/billing.ts
|
|
2140
|
+
var BillingManager = class {
|
|
2141
|
+
config;
|
|
2142
|
+
billingConfig;
|
|
2143
|
+
invoices = [];
|
|
2144
|
+
channelId;
|
|
2145
|
+
payer;
|
|
2146
|
+
payee;
|
|
2147
|
+
lastInvoiceTime;
|
|
2148
|
+
constructor(channelId, payer, payee, billingConfig, config = {}) {
|
|
2149
|
+
this.channelId = channelId;
|
|
2150
|
+
this.payer = payer;
|
|
2151
|
+
this.payee = payee;
|
|
2152
|
+
this.billingConfig = billingConfig;
|
|
2153
|
+
this.lastInvoiceTime = Date.now();
|
|
2154
|
+
this.config = {
|
|
2155
|
+
autoInvoice: config.autoInvoice ?? false,
|
|
2156
|
+
invoiceInterval: config.invoiceInterval ?? 86400,
|
|
2157
|
+
currency: config.currency ?? "USDT",
|
|
2158
|
+
taxRate: config.taxRate ?? 0
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Generate invoice from metering records
|
|
2163
|
+
*/
|
|
2164
|
+
generateInvoice(records, startTime, endTime) {
|
|
2165
|
+
const items = this.createInvoiceItems(records, startTime, endTime);
|
|
2166
|
+
const subtotal = this.calculateSubtotal(items);
|
|
2167
|
+
const fees = this.calculateFees(subtotal);
|
|
2168
|
+
const total = (BigInt(subtotal) + BigInt(fees)).toString();
|
|
2169
|
+
const invoice = {
|
|
2170
|
+
id: this.generateInvoiceId(),
|
|
2171
|
+
channelId: this.channelId,
|
|
2172
|
+
payer: this.payer,
|
|
2173
|
+
payee: this.payee,
|
|
2174
|
+
items,
|
|
2175
|
+
subtotal,
|
|
2176
|
+
fees,
|
|
2177
|
+
total,
|
|
2178
|
+
currency: this.config.currency,
|
|
2179
|
+
status: "pending",
|
|
2180
|
+
createdAt: Date.now(),
|
|
2181
|
+
dueAt: Date.now() + 864e5
|
|
2182
|
+
// Due in 24 hours
|
|
2183
|
+
};
|
|
2184
|
+
this.invoices.push(invoice);
|
|
2185
|
+
this.lastInvoiceTime = endTime;
|
|
2186
|
+
return invoice;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Get all invoices
|
|
2190
|
+
*/
|
|
2191
|
+
getInvoices() {
|
|
2192
|
+
return [...this.invoices];
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Get invoice by ID
|
|
2196
|
+
*/
|
|
2197
|
+
getInvoice(id) {
|
|
2198
|
+
return this.invoices.find((inv) => inv.id === id);
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Mark invoice as paid
|
|
2202
|
+
*/
|
|
2203
|
+
markPaid(invoiceId) {
|
|
2204
|
+
const invoice = this.invoices.find((inv) => inv.id === invoiceId);
|
|
2205
|
+
if (!invoice || invoice.status !== "pending") {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
invoice.status = "paid";
|
|
2209
|
+
invoice.paidAt = Date.now();
|
|
2210
|
+
return true;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Mark invoice as settled
|
|
2214
|
+
*/
|
|
2215
|
+
markSettled(invoiceId) {
|
|
2216
|
+
const invoice = this.invoices.find((inv) => inv.id === invoiceId);
|
|
2217
|
+
if (!invoice) return false;
|
|
2218
|
+
invoice.status = "settled";
|
|
2219
|
+
return true;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Get pending amount
|
|
2223
|
+
*/
|
|
2224
|
+
getPendingAmount() {
|
|
2225
|
+
return this.invoices.filter((inv) => inv.status === "pending").reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Get total billed amount
|
|
2229
|
+
*/
|
|
2230
|
+
getTotalBilled() {
|
|
2231
|
+
return this.invoices.reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Check if new invoice is due
|
|
2235
|
+
*/
|
|
2236
|
+
isInvoiceDue(currentTime) {
|
|
2237
|
+
const elapsed = currentTime - this.lastInvoiceTime;
|
|
2238
|
+
return elapsed >= this.config.invoiceInterval * 1e3;
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Calculate amount for billing period
|
|
2242
|
+
*/
|
|
2243
|
+
calculatePeriodAmount(rate, durationSeconds) {
|
|
2244
|
+
const periodSeconds = this.getPeriodSeconds(this.billingConfig.period);
|
|
2245
|
+
const periods = durationSeconds / periodSeconds;
|
|
2246
|
+
const amount = BigInt(rate) * BigInt(Math.floor(periods * periodSeconds));
|
|
2247
|
+
return this.applyRounding(amount.toString());
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Apply minimum charge
|
|
2251
|
+
*/
|
|
2252
|
+
applyMinimumCharge(amount) {
|
|
2253
|
+
const minCharge = BigInt(this.billingConfig.minimumCharge);
|
|
2254
|
+
const actualAmount = BigInt(amount);
|
|
2255
|
+
return (actualAmount < minCharge ? minCharge : actualAmount).toString();
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Calculate grace period savings
|
|
2259
|
+
*/
|
|
2260
|
+
calculateGracePeriodSavings(rate, totalDuration) {
|
|
2261
|
+
const gracePeriod = this.billingConfig.gracePeriod;
|
|
2262
|
+
if (gracePeriod <= 0 || totalDuration <= gracePeriod) {
|
|
2263
|
+
return "0";
|
|
2264
|
+
}
|
|
2265
|
+
return (BigInt(rate) * BigInt(Math.min(gracePeriod, totalDuration))).toString();
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Get billing summary
|
|
2269
|
+
*/
|
|
2270
|
+
getSummary() {
|
|
2271
|
+
const totalBilled = this.getTotalBilled();
|
|
2272
|
+
const totalPaid = this.invoices.filter((inv) => inv.status === "paid" || inv.status === "settled").reduce((sum, inv) => sum + BigInt(inv.total), 0n).toString();
|
|
2273
|
+
const totalPending = this.getPendingAmount();
|
|
2274
|
+
const averageInvoice = this.invoices.length > 0 ? (BigInt(totalBilled) / BigInt(this.invoices.length)).toString() : "0";
|
|
2275
|
+
return {
|
|
2276
|
+
totalInvoices: this.invoices.length,
|
|
2277
|
+
totalBilled,
|
|
2278
|
+
totalPaid,
|
|
2279
|
+
totalPending,
|
|
2280
|
+
averageInvoice
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Export billing data
|
|
2285
|
+
*/
|
|
2286
|
+
export() {
|
|
2287
|
+
return JSON.stringify({
|
|
2288
|
+
channelId: this.channelId,
|
|
2289
|
+
invoices: this.invoices,
|
|
2290
|
+
exportedAt: Date.now()
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
createInvoiceItems(records, startTime, endTime) {
|
|
2294
|
+
if (records.length === 0) {
|
|
2295
|
+
return [];
|
|
2296
|
+
}
|
|
2297
|
+
const rateGroups = /* @__PURE__ */ new Map();
|
|
2298
|
+
for (const record of records) {
|
|
2299
|
+
const existing = rateGroups.get(record.rate) ?? [];
|
|
2300
|
+
existing.push(record);
|
|
2301
|
+
rateGroups.set(record.rate, existing);
|
|
2302
|
+
}
|
|
2303
|
+
const items = [];
|
|
2304
|
+
for (const [rate, groupRecords] of rateGroups) {
|
|
2305
|
+
const totalDuration = groupRecords.reduce((sum, r) => sum + r.duration, 0);
|
|
2306
|
+
const totalAmount = groupRecords.reduce(
|
|
2307
|
+
(sum, r) => sum + BigInt(r.amount),
|
|
2308
|
+
0n
|
|
2309
|
+
);
|
|
2310
|
+
const periodName = this.getPeriodName(this.billingConfig.period);
|
|
2311
|
+
const quantity = this.calculateQuantity(totalDuration);
|
|
2312
|
+
items.push({
|
|
2313
|
+
description: `Streaming usage at ${rate} per ${periodName}`,
|
|
2314
|
+
quantity,
|
|
2315
|
+
rate,
|
|
2316
|
+
amount: totalAmount.toString(),
|
|
2317
|
+
startTime,
|
|
2318
|
+
endTime
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return items;
|
|
2322
|
+
}
|
|
2323
|
+
calculateSubtotal(items) {
|
|
2324
|
+
return items.reduce((sum, item) => sum + BigInt(item.amount), 0n).toString();
|
|
2325
|
+
}
|
|
2326
|
+
calculateFees(subtotal) {
|
|
2327
|
+
const amount = BigInt(subtotal);
|
|
2328
|
+
const taxRate = this.config.taxRate;
|
|
2329
|
+
if (taxRate <= 0) return "0";
|
|
2330
|
+
return BigInt(Math.floor(Number(amount) * taxRate)).toString();
|
|
2331
|
+
}
|
|
2332
|
+
applyRounding(amount) {
|
|
2333
|
+
const value = BigInt(amount);
|
|
2334
|
+
const mode = this.billingConfig.roundingMode;
|
|
2335
|
+
switch (mode) {
|
|
2336
|
+
case "floor":
|
|
2337
|
+
return value.toString();
|
|
2338
|
+
case "ceil":
|
|
2339
|
+
return value.toString();
|
|
2340
|
+
case "round":
|
|
2341
|
+
return value.toString();
|
|
2342
|
+
default:
|
|
2343
|
+
return value.toString();
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
getPeriodSeconds(period) {
|
|
2347
|
+
switch (period) {
|
|
2348
|
+
case "realtime":
|
|
2349
|
+
case "second":
|
|
2350
|
+
return 1;
|
|
2351
|
+
case "minute":
|
|
2352
|
+
return 60;
|
|
2353
|
+
case "hour":
|
|
2354
|
+
return 3600;
|
|
2355
|
+
case "day":
|
|
2356
|
+
return 86400;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
getPeriodName(period) {
|
|
2360
|
+
switch (period) {
|
|
2361
|
+
case "realtime":
|
|
2362
|
+
case "second":
|
|
2363
|
+
return "second";
|
|
2364
|
+
case "minute":
|
|
2365
|
+
return "minute";
|
|
2366
|
+
case "hour":
|
|
2367
|
+
return "hour";
|
|
2368
|
+
case "day":
|
|
2369
|
+
return "day";
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
calculateQuantity(totalSeconds) {
|
|
2373
|
+
const periodSeconds = this.getPeriodSeconds(this.billingConfig.period);
|
|
2374
|
+
return totalSeconds / periodSeconds;
|
|
2375
|
+
}
|
|
2376
|
+
generateInvoiceId() {
|
|
2377
|
+
return `inv_${this.channelId.slice(-8)}_${Date.now().toString(36)}`;
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
function formatCurrencyAmount(amount, decimals = 6, symbol = "USDT") {
|
|
2381
|
+
const value = BigInt(amount);
|
|
2382
|
+
const divisor = BigInt(10 ** decimals);
|
|
2383
|
+
const wholePart = value / divisor;
|
|
2384
|
+
const fractionalPart = value % divisor;
|
|
2385
|
+
const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
|
|
2386
|
+
const trimmedFractional = fractionalStr.replace(/0+$/, "") || "0";
|
|
2387
|
+
return `${wholePart}.${trimmedFractional} ${symbol}`;
|
|
2388
|
+
}
|
|
2389
|
+
function parseCurrencyAmount(display, decimals = 6) {
|
|
2390
|
+
const cleaned = display.replace(/[^\d.]/g, "");
|
|
2391
|
+
const [whole, fractional = ""] = cleaned.split(".");
|
|
2392
|
+
const paddedFractional = fractional.slice(0, decimals).padEnd(decimals, "0");
|
|
2393
|
+
const combined = whole + paddedFractional;
|
|
2394
|
+
return BigInt(combined).toString();
|
|
2395
|
+
}
|
|
2396
|
+
function estimateFutureBill(currentRate, durationSeconds, minimumCharge = "0") {
|
|
2397
|
+
const estimated = BigInt(currentRate) * BigInt(durationSeconds);
|
|
2398
|
+
const minimum = BigInt(minimumCharge);
|
|
2399
|
+
return (estimated < minimum ? minimum : estimated).toString();
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// src/settlement/types.ts
|
|
2403
|
+
import { z as z3 } from "zod";
|
|
2404
|
+
var SettlementState = z3.enum([
|
|
2405
|
+
"pending",
|
|
2406
|
+
// Settlement not yet initiated
|
|
2407
|
+
"in_progress",
|
|
2408
|
+
// Settlement process started
|
|
2409
|
+
"challenging",
|
|
2410
|
+
// In challenge period
|
|
2411
|
+
"disputed",
|
|
2412
|
+
// Dispute raised
|
|
2413
|
+
"finalizing",
|
|
2414
|
+
// Finalizing on-chain
|
|
2415
|
+
"completed",
|
|
2416
|
+
// Successfully settled
|
|
2417
|
+
"failed"
|
|
2418
|
+
// Settlement failed
|
|
2419
|
+
]);
|
|
2420
|
+
var CheckpointType = z3.enum([
|
|
2421
|
+
"periodic",
|
|
2422
|
+
// Regular interval checkpoint
|
|
2423
|
+
"manual",
|
|
2424
|
+
// Manually triggered
|
|
2425
|
+
"balance",
|
|
2426
|
+
// Triggered by balance threshold
|
|
2427
|
+
"pre_close",
|
|
2428
|
+
// Before channel close
|
|
2429
|
+
"dispute"
|
|
2430
|
+
// Checkpoint for dispute
|
|
2431
|
+
]);
|
|
2432
|
+
var SettlementCheckpoint = z3.object({
|
|
2433
|
+
id: z3.string(),
|
|
2434
|
+
channelId: z3.string(),
|
|
2435
|
+
sequence: z3.number(),
|
|
2436
|
+
type: CheckpointType,
|
|
2437
|
+
// Balances
|
|
2438
|
+
payerBalance: z3.string(),
|
|
2439
|
+
payeeBalance: z3.string(),
|
|
2440
|
+
totalStreamed: z3.string(),
|
|
2441
|
+
// Signatures
|
|
2442
|
+
payerSignature: z3.string(),
|
|
2443
|
+
payeeSignature: z3.string().optional(),
|
|
2444
|
+
// Verification
|
|
2445
|
+
stateHash: z3.string(),
|
|
2446
|
+
merkleRoot: z3.string().optional(),
|
|
2447
|
+
// Timing
|
|
2448
|
+
createdAt: z3.number(),
|
|
2449
|
+
expiresAt: z3.number().optional(),
|
|
2450
|
+
// On-chain reference
|
|
2451
|
+
txHash: z3.string().optional(),
|
|
2452
|
+
blockNumber: z3.number().optional()
|
|
2453
|
+
});
|
|
2454
|
+
var SettlementRequest = z3.object({
|
|
2455
|
+
channelId: z3.string(),
|
|
2456
|
+
initiator: z3.string(),
|
|
2457
|
+
finalCheckpoint: SettlementCheckpoint,
|
|
2458
|
+
reason: z3.enum(["mutual", "unilateral", "timeout", "dispute_resolution"]),
|
|
2459
|
+
signature: z3.string(),
|
|
2460
|
+
metadata: z3.record(z3.unknown()).optional()
|
|
2461
|
+
});
|
|
2462
|
+
var SettlementResult = z3.object({
|
|
2463
|
+
success: z3.boolean(),
|
|
2464
|
+
settlementId: z3.string().optional(),
|
|
2465
|
+
error: z3.string().optional(),
|
|
2466
|
+
finalBalances: z3.object({
|
|
2467
|
+
payer: z3.string(),
|
|
2468
|
+
payee: z3.string()
|
|
2469
|
+
}).optional(),
|
|
2470
|
+
txHash: z3.string().optional(),
|
|
2471
|
+
timestamp: z3.number()
|
|
2472
|
+
});
|
|
2473
|
+
var DisputeReason = z3.enum([
|
|
2474
|
+
"invalid_checkpoint",
|
|
2475
|
+
// Checkpoint signature or data invalid
|
|
2476
|
+
"stale_state",
|
|
2477
|
+
// Challenger has newer valid state
|
|
2478
|
+
"balance_mismatch",
|
|
2479
|
+
// Balance doesn't match expected
|
|
2480
|
+
"unauthorized_close",
|
|
2481
|
+
// Unauthorized party initiated close
|
|
2482
|
+
"fraud",
|
|
2483
|
+
// Fraudulent activity detected
|
|
2484
|
+
"other"
|
|
2485
|
+
// Other reason
|
|
2486
|
+
]);
|
|
2487
|
+
var DisputeEvidence = z3.object({
|
|
2488
|
+
type: z3.enum(["checkpoint", "signature", "transaction", "state_proof"]),
|
|
2489
|
+
data: z3.string(),
|
|
2490
|
+
description: z3.string(),
|
|
2491
|
+
timestamp: z3.number(),
|
|
2492
|
+
verified: z3.boolean().default(false)
|
|
2493
|
+
});
|
|
2494
|
+
var Dispute = z3.object({
|
|
2495
|
+
id: z3.string(),
|
|
2496
|
+
channelId: z3.string(),
|
|
2497
|
+
initiator: z3.string(),
|
|
2498
|
+
respondent: z3.string(),
|
|
2499
|
+
reason: DisputeReason,
|
|
2500
|
+
description: z3.string(),
|
|
2501
|
+
// Claimed state
|
|
2502
|
+
claimedPayerBalance: z3.string(),
|
|
2503
|
+
claimedPayeeBalance: z3.string(),
|
|
2504
|
+
claimedCheckpoint: SettlementCheckpoint.optional(),
|
|
2505
|
+
// Evidence
|
|
2506
|
+
evidence: z3.array(DisputeEvidence),
|
|
2507
|
+
responseEvidence: z3.array(DisputeEvidence).optional(),
|
|
2508
|
+
// Resolution
|
|
2509
|
+
status: z3.enum(["pending", "under_review", "resolved", "rejected", "timeout"]),
|
|
2510
|
+
resolution: z3.object({
|
|
2511
|
+
winner: z3.string().optional(),
|
|
2512
|
+
finalPayerBalance: z3.string(),
|
|
2513
|
+
finalPayeeBalance: z3.string(),
|
|
2514
|
+
reason: z3.string(),
|
|
2515
|
+
timestamp: z3.number()
|
|
2516
|
+
}).optional(),
|
|
2517
|
+
// Timing
|
|
2518
|
+
createdAt: z3.number(),
|
|
2519
|
+
responseDeadline: z3.number(),
|
|
2520
|
+
resolutionDeadline: z3.number()
|
|
2521
|
+
});
|
|
2522
|
+
var SettlementConfig = z3.object({
|
|
2523
|
+
challengePeriod: z3.number().default(86400),
|
|
2524
|
+
// 24 hours
|
|
2525
|
+
disputeResponsePeriod: z3.number().default(43200),
|
|
2526
|
+
// 12 hours
|
|
2527
|
+
disputeResolutionPeriod: z3.number().default(172800),
|
|
2528
|
+
// 48 hours
|
|
2529
|
+
minCheckpointInterval: z3.number().default(60),
|
|
2530
|
+
// 60 seconds
|
|
2531
|
+
maxCheckpointsStored: z3.number().default(100),
|
|
2532
|
+
settlementFee: z3.string().default("0"),
|
|
2533
|
+
disputeBond: z3.string().default("0")
|
|
2534
|
+
// Bond required to raise dispute
|
|
2535
|
+
});
|
|
2536
|
+
var OnChainSettlement = z3.object({
|
|
2537
|
+
channelId: z3.string(),
|
|
2538
|
+
settlementId: z3.string(),
|
|
2539
|
+
txHash: z3.string(),
|
|
2540
|
+
blockNumber: z3.number(),
|
|
2541
|
+
payerReceived: z3.string(),
|
|
2542
|
+
payeeReceived: z3.string(),
|
|
2543
|
+
fee: z3.string(),
|
|
2544
|
+
timestamp: z3.number(),
|
|
2545
|
+
finalized: z3.boolean()
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2548
|
+
// src/settlement/checkpoint.ts
|
|
2549
|
+
var CheckpointManager = class {
|
|
2550
|
+
checkpoints = /* @__PURE__ */ new Map();
|
|
2551
|
+
config;
|
|
2552
|
+
settlementConfig;
|
|
2553
|
+
lastCheckpointTime = /* @__PURE__ */ new Map();
|
|
2554
|
+
constructor(settlementConfig, config = {}) {
|
|
2555
|
+
this.settlementConfig = settlementConfig;
|
|
2556
|
+
this.config = {
|
|
2557
|
+
autoCheckpoint: config.autoCheckpoint ?? false,
|
|
2558
|
+
intervalSeconds: config.intervalSeconds ?? 3600,
|
|
2559
|
+
balanceThreshold: config.balanceThreshold ?? "1000000",
|
|
2560
|
+
onCheckpoint: config.onCheckpoint ?? (() => {
|
|
2561
|
+
})
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Create a new checkpoint
|
|
2566
|
+
*/
|
|
2567
|
+
create(request) {
|
|
2568
|
+
const now = Date.now();
|
|
2569
|
+
const channelCheckpoints = this.checkpoints.get(request.channelId) ?? [];
|
|
2570
|
+
const expectedSequence = channelCheckpoints.length;
|
|
2571
|
+
if (request.sequence !== expectedSequence) {
|
|
2572
|
+
throw new Error(`Invalid sequence: expected ${expectedSequence}, got ${request.sequence}`);
|
|
2573
|
+
}
|
|
2574
|
+
const lastTime = this.lastCheckpointTime.get(request.channelId) ?? 0;
|
|
2575
|
+
const elapsed = (now - lastTime) / 1e3;
|
|
2576
|
+
if (elapsed < this.settlementConfig.minCheckpointInterval && channelCheckpoints.length > 0) {
|
|
2577
|
+
throw new Error(`Checkpoint interval too short: ${elapsed}s < ${this.settlementConfig.minCheckpointInterval}s`);
|
|
2578
|
+
}
|
|
2579
|
+
const stateHash = this.generateStateHash(request);
|
|
2580
|
+
const checkpoint = {
|
|
2581
|
+
id: this.generateCheckpointId(request.channelId, request.sequence),
|
|
2582
|
+
channelId: request.channelId,
|
|
2583
|
+
sequence: request.sequence,
|
|
2584
|
+
type: request.type ?? "manual",
|
|
2585
|
+
payerBalance: request.payerBalance,
|
|
2586
|
+
payeeBalance: request.payeeBalance,
|
|
2587
|
+
totalStreamed: request.totalStreamed,
|
|
2588
|
+
payerSignature: request.payerSignature,
|
|
2589
|
+
payeeSignature: request.payeeSignature,
|
|
2590
|
+
stateHash,
|
|
2591
|
+
createdAt: now
|
|
2592
|
+
};
|
|
2593
|
+
channelCheckpoints.push(checkpoint);
|
|
2594
|
+
this.checkpoints.set(request.channelId, channelCheckpoints);
|
|
2595
|
+
this.lastCheckpointTime.set(request.channelId, now);
|
|
2596
|
+
this.pruneCheckpoints(request.channelId);
|
|
2597
|
+
this.config.onCheckpoint(checkpoint);
|
|
2598
|
+
return checkpoint;
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Get latest checkpoint for channel
|
|
2602
|
+
*/
|
|
2603
|
+
getLatest(channelId) {
|
|
2604
|
+
const checkpoints = this.checkpoints.get(channelId);
|
|
2605
|
+
if (!checkpoints || checkpoints.length === 0) return void 0;
|
|
2606
|
+
return checkpoints[checkpoints.length - 1];
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Get checkpoint by sequence
|
|
2610
|
+
*/
|
|
2611
|
+
getBySequence(channelId, sequence) {
|
|
2612
|
+
const checkpoints = this.checkpoints.get(channelId);
|
|
2613
|
+
if (!checkpoints) return void 0;
|
|
2614
|
+
return checkpoints.find((cp) => cp.sequence === sequence);
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Get all checkpoints for channel
|
|
2618
|
+
*/
|
|
2619
|
+
getAll(channelId) {
|
|
2620
|
+
return [...this.checkpoints.get(channelId) ?? []];
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Validate checkpoint
|
|
2624
|
+
*/
|
|
2625
|
+
validate(checkpoint) {
|
|
2626
|
+
const errors = [];
|
|
2627
|
+
const payerBalance = BigInt(checkpoint.payerBalance);
|
|
2628
|
+
const payeeBalance = BigInt(checkpoint.payeeBalance);
|
|
2629
|
+
const totalStreamed = BigInt(checkpoint.totalStreamed);
|
|
2630
|
+
if (payerBalance < 0n) {
|
|
2631
|
+
errors.push("Payer balance cannot be negative");
|
|
2632
|
+
}
|
|
2633
|
+
if (payeeBalance < 0n) {
|
|
2634
|
+
errors.push("Payee balance cannot be negative");
|
|
2635
|
+
}
|
|
2636
|
+
if (totalStreamed < 0n) {
|
|
2637
|
+
errors.push("Total streamed cannot be negative");
|
|
2638
|
+
}
|
|
2639
|
+
if (payeeBalance !== totalStreamed) {
|
|
2640
|
+
errors.push("Total streamed should equal payee balance");
|
|
2641
|
+
}
|
|
2642
|
+
if (!checkpoint.payerSignature) {
|
|
2643
|
+
errors.push("Payer signature is required");
|
|
2644
|
+
}
|
|
2645
|
+
const expectedHash = this.generateStateHash({
|
|
2646
|
+
channelId: checkpoint.channelId,
|
|
2647
|
+
sequence: checkpoint.sequence,
|
|
2648
|
+
payerBalance: checkpoint.payerBalance,
|
|
2649
|
+
payeeBalance: checkpoint.payeeBalance,
|
|
2650
|
+
totalStreamed: checkpoint.totalStreamed,
|
|
2651
|
+
payerSignature: checkpoint.payerSignature
|
|
2652
|
+
});
|
|
2653
|
+
if (checkpoint.stateHash !== expectedHash) {
|
|
2654
|
+
errors.push("State hash mismatch");
|
|
2655
|
+
}
|
|
2656
|
+
if (checkpoint.createdAt > Date.now()) {
|
|
2657
|
+
errors.push("Checkpoint timestamp is in the future");
|
|
2658
|
+
}
|
|
2659
|
+
return { valid: errors.length === 0, errors };
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Compare two checkpoints
|
|
2663
|
+
*/
|
|
2664
|
+
compare(a, b) {
|
|
2665
|
+
const isNewer = a.sequence > b.sequence || a.sequence === b.sequence && a.createdAt > b.createdAt;
|
|
2666
|
+
const payerDiff = BigInt(a.payerBalance) - BigInt(b.payerBalance);
|
|
2667
|
+
const payeeDiff = BigInt(a.payeeBalance) - BigInt(b.payeeBalance);
|
|
2668
|
+
return {
|
|
2669
|
+
isNewer,
|
|
2670
|
+
balanceDifference: {
|
|
2671
|
+
payer: payerDiff.toString(),
|
|
2672
|
+
payee: payeeDiff.toString()
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Check if checkpoint is needed
|
|
2678
|
+
*/
|
|
2679
|
+
needsCheckpoint(channelId, currentPayeeBalance) {
|
|
2680
|
+
const lastCheckpoint = this.getLatest(channelId);
|
|
2681
|
+
const now = Date.now();
|
|
2682
|
+
if (!lastCheckpoint) {
|
|
2683
|
+
return { needed: true, reason: "No existing checkpoint" };
|
|
2684
|
+
}
|
|
2685
|
+
const elapsed = (now - lastCheckpoint.createdAt) / 1e3;
|
|
2686
|
+
if (elapsed >= this.config.intervalSeconds) {
|
|
2687
|
+
return { needed: true, reason: "Interval elapsed" };
|
|
2688
|
+
}
|
|
2689
|
+
const balanceChange = BigInt(currentPayeeBalance) - BigInt(lastCheckpoint.payeeBalance);
|
|
2690
|
+
if (balanceChange >= BigInt(this.config.balanceThreshold)) {
|
|
2691
|
+
return { needed: true, reason: "Balance threshold exceeded" };
|
|
2692
|
+
}
|
|
2693
|
+
return { needed: false, reason: "" };
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Build merkle root from checkpoint history
|
|
2697
|
+
*/
|
|
2698
|
+
buildMerkleRoot(channelId) {
|
|
2699
|
+
const checkpoints = this.checkpoints.get(channelId);
|
|
2700
|
+
if (!checkpoints || checkpoints.length === 0) {
|
|
2701
|
+
return "0x" + "0".repeat(64);
|
|
2702
|
+
}
|
|
2703
|
+
const hashes = checkpoints.map((cp) => cp.stateHash);
|
|
2704
|
+
return this.hashArray(hashes);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Verify checkpoint is in merkle tree
|
|
2708
|
+
*/
|
|
2709
|
+
verifyMerkleProof(checkpoint, merkleRoot, _proof) {
|
|
2710
|
+
const channelRoot = this.buildMerkleRoot(checkpoint.channelId);
|
|
2711
|
+
return channelRoot === merkleRoot;
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Export checkpoints for backup
|
|
2715
|
+
*/
|
|
2716
|
+
export(channelId) {
|
|
2717
|
+
const checkpoints = this.checkpoints.get(channelId) ?? [];
|
|
2718
|
+
return JSON.stringify({
|
|
2719
|
+
channelId,
|
|
2720
|
+
checkpoints,
|
|
2721
|
+
exportedAt: Date.now()
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Import checkpoints from backup
|
|
2726
|
+
*/
|
|
2727
|
+
import(data) {
|
|
2728
|
+
try {
|
|
2729
|
+
const parsed = JSON.parse(data);
|
|
2730
|
+
const { channelId, checkpoints } = parsed;
|
|
2731
|
+
let imported = 0;
|
|
2732
|
+
const existing = this.checkpoints.get(channelId) ?? [];
|
|
2733
|
+
for (const cp of checkpoints) {
|
|
2734
|
+
const validation = this.validate(cp);
|
|
2735
|
+
if (validation.valid) {
|
|
2736
|
+
const exists = existing.some((e) => e.sequence === cp.sequence);
|
|
2737
|
+
if (!exists) {
|
|
2738
|
+
existing.push(cp);
|
|
2739
|
+
imported++;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
existing.sort((a, b) => a.sequence - b.sequence);
|
|
2744
|
+
this.checkpoints.set(channelId, existing);
|
|
2745
|
+
return { success: true, imported };
|
|
2746
|
+
} catch {
|
|
2747
|
+
return { success: false, imported: 0 };
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Clear checkpoints for channel
|
|
2752
|
+
*/
|
|
2753
|
+
clear(channelId) {
|
|
2754
|
+
this.checkpoints.delete(channelId);
|
|
2755
|
+
this.lastCheckpointTime.delete(channelId);
|
|
2756
|
+
}
|
|
2757
|
+
generateCheckpointId(channelId, sequence) {
|
|
2758
|
+
return `cp_${channelId.slice(-8)}_${sequence}_${Date.now().toString(36)}`;
|
|
2759
|
+
}
|
|
2760
|
+
generateStateHash(request) {
|
|
2761
|
+
const data = [
|
|
2762
|
+
request.channelId,
|
|
2763
|
+
request.sequence.toString(),
|
|
2764
|
+
request.payerBalance,
|
|
2765
|
+
request.payeeBalance,
|
|
2766
|
+
request.totalStreamed
|
|
2767
|
+
].join(":");
|
|
2768
|
+
let hash = 0;
|
|
2769
|
+
for (let i = 0; i < data.length; i++) {
|
|
2770
|
+
const char = data.charCodeAt(i);
|
|
2771
|
+
hash = (hash << 5) - hash + char;
|
|
2772
|
+
hash = hash & hash;
|
|
2773
|
+
}
|
|
2774
|
+
return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
|
|
2775
|
+
}
|
|
2776
|
+
hashArray(hashes) {
|
|
2777
|
+
const combined = hashes.join("");
|
|
2778
|
+
let hash = 0;
|
|
2779
|
+
for (let i = 0; i < combined.length; i++) {
|
|
2780
|
+
const char = combined.charCodeAt(i);
|
|
2781
|
+
hash = (hash << 5) - hash + char;
|
|
2782
|
+
hash = hash & hash;
|
|
2783
|
+
}
|
|
2784
|
+
return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
|
|
2785
|
+
}
|
|
2786
|
+
pruneCheckpoints(channelId) {
|
|
2787
|
+
const checkpoints = this.checkpoints.get(channelId);
|
|
2788
|
+
if (!checkpoints) return;
|
|
2789
|
+
const maxStored = this.settlementConfig.maxCheckpointsStored;
|
|
2790
|
+
if (checkpoints.length <= maxStored) return;
|
|
2791
|
+
const first = checkpoints[0];
|
|
2792
|
+
const recent = checkpoints.slice(-(maxStored - 1));
|
|
2793
|
+
this.checkpoints.set(channelId, [first, ...recent]);
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
function signCheckpoint(checkpoint, _privateKey) {
|
|
2797
|
+
const dataToSign = [
|
|
2798
|
+
checkpoint.channelId,
|
|
2799
|
+
checkpoint.sequence.toString(),
|
|
2800
|
+
checkpoint.payerBalance,
|
|
2801
|
+
checkpoint.payeeBalance,
|
|
2802
|
+
checkpoint.totalStreamed,
|
|
2803
|
+
checkpoint.createdAt.toString()
|
|
2804
|
+
].join(":");
|
|
2805
|
+
let hash = 0;
|
|
2806
|
+
for (let i = 0; i < dataToSign.length; i++) {
|
|
2807
|
+
const char = dataToSign.charCodeAt(i);
|
|
2808
|
+
hash = (hash << 5) - hash + char;
|
|
2809
|
+
hash = hash & hash;
|
|
2810
|
+
}
|
|
2811
|
+
return "0x" + Math.abs(hash).toString(16).padStart(128, "0");
|
|
2812
|
+
}
|
|
2813
|
+
function verifyCheckpointSignature(_checkpoint, signature, _publicKey) {
|
|
2814
|
+
return signature.startsWith("0x") && signature.length === 130;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/settlement/final.ts
|
|
2818
|
+
var FinalSettlementManager = class {
|
|
2819
|
+
settlements = /* @__PURE__ */ new Map();
|
|
2820
|
+
checkpointManager;
|
|
2821
|
+
settlementConfig;
|
|
2822
|
+
onChainSettlements = /* @__PURE__ */ new Map();
|
|
2823
|
+
constructor(checkpointManager, settlementConfig, _config = {}) {
|
|
2824
|
+
this.checkpointManager = checkpointManager;
|
|
2825
|
+
this.settlementConfig = settlementConfig;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Initiate settlement
|
|
2829
|
+
*/
|
|
2830
|
+
initiate(request) {
|
|
2831
|
+
const now = Date.now();
|
|
2832
|
+
const validation = this.validateRequest(request);
|
|
2833
|
+
if (!validation.valid) {
|
|
2834
|
+
return {
|
|
2835
|
+
success: false,
|
|
2836
|
+
error: validation.errors.join(", "),
|
|
2837
|
+
timestamp: now
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
const existing = this.settlements.get(request.channelId);
|
|
2841
|
+
if (existing && existing.state !== "completed" && existing.state !== "failed") {
|
|
2842
|
+
return {
|
|
2843
|
+
success: false,
|
|
2844
|
+
error: "Settlement already in progress",
|
|
2845
|
+
timestamp: now
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
const settlementId = this.generateSettlementId(request.channelId);
|
|
2849
|
+
const challengeDeadline = now + this.settlementConfig.challengePeriod * 1e3;
|
|
2850
|
+
const status = {
|
|
2851
|
+
state: "pending",
|
|
2852
|
+
channelId: request.channelId,
|
|
2853
|
+
initiator: request.initiator,
|
|
2854
|
+
checkpoint: request.finalCheckpoint,
|
|
2855
|
+
challengeDeadline
|
|
2856
|
+
};
|
|
2857
|
+
this.settlements.set(request.channelId, status);
|
|
2858
|
+
this.transitionState(request.channelId, "in_progress");
|
|
2859
|
+
return {
|
|
2860
|
+
success: true,
|
|
2861
|
+
settlementId,
|
|
2862
|
+
finalBalances: {
|
|
2863
|
+
payer: request.finalCheckpoint.payerBalance,
|
|
2864
|
+
payee: request.finalCheckpoint.payeeBalance
|
|
2865
|
+
},
|
|
2866
|
+
timestamp: now
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
/**
|
|
2870
|
+
* Process mutual settlement (both parties agree)
|
|
2871
|
+
*/
|
|
2872
|
+
processMutual(channelId, payerSignature, payeeSignature, finalCheckpoint) {
|
|
2873
|
+
const now = Date.now();
|
|
2874
|
+
if (!payerSignature || !payeeSignature) {
|
|
2875
|
+
return {
|
|
2876
|
+
success: false,
|
|
2877
|
+
error: "Both signatures required for mutual settlement",
|
|
2878
|
+
timestamp: now
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
const mutualCheckpoint = {
|
|
2882
|
+
...finalCheckpoint,
|
|
2883
|
+
payerSignature,
|
|
2884
|
+
payeeSignature
|
|
2885
|
+
};
|
|
2886
|
+
const status = {
|
|
2887
|
+
state: "finalizing",
|
|
2888
|
+
channelId,
|
|
2889
|
+
initiator: "mutual",
|
|
2890
|
+
checkpoint: mutualCheckpoint,
|
|
2891
|
+
challengeDeadline: now
|
|
2892
|
+
// No challenge period for mutual
|
|
2893
|
+
};
|
|
2894
|
+
this.settlements.set(channelId, status);
|
|
2895
|
+
return this.finalize(channelId);
|
|
2896
|
+
}
|
|
2897
|
+
/**
|
|
2898
|
+
* Check if challenge period has elapsed
|
|
2899
|
+
*/
|
|
2900
|
+
canFinalize(channelId) {
|
|
2901
|
+
const status = this.settlements.get(channelId);
|
|
2902
|
+
if (!status) {
|
|
2903
|
+
return { canFinalize: false, timeRemaining: -1 };
|
|
2904
|
+
}
|
|
2905
|
+
if (status.state === "completed" || status.state === "failed") {
|
|
2906
|
+
return { canFinalize: false, timeRemaining: 0 };
|
|
2907
|
+
}
|
|
2908
|
+
if (status.state === "disputed") {
|
|
2909
|
+
return { canFinalize: false, timeRemaining: -1 };
|
|
2910
|
+
}
|
|
2911
|
+
const now = Date.now();
|
|
2912
|
+
const timeRemaining = Math.max(0, status.challengeDeadline - now);
|
|
2913
|
+
return {
|
|
2914
|
+
canFinalize: timeRemaining === 0,
|
|
2915
|
+
timeRemaining
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Finalize settlement after challenge period
|
|
2920
|
+
*/
|
|
2921
|
+
finalize(channelId) {
|
|
2922
|
+
const now = Date.now();
|
|
2923
|
+
const status = this.settlements.get(channelId);
|
|
2924
|
+
if (!status) {
|
|
2925
|
+
return {
|
|
2926
|
+
success: false,
|
|
2927
|
+
error: "No settlement found",
|
|
2928
|
+
timestamp: now
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
const { canFinalize: canFinalizeNow, timeRemaining } = this.canFinalize(channelId);
|
|
2932
|
+
if (!canFinalizeNow && status.state !== "finalizing") {
|
|
2933
|
+
return {
|
|
2934
|
+
success: false,
|
|
2935
|
+
error: `Cannot finalize: ${timeRemaining}ms remaining in challenge period`,
|
|
2936
|
+
timestamp: now
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
this.transitionState(channelId, "finalizing");
|
|
2940
|
+
const settlementId = this.generateSettlementId(channelId);
|
|
2941
|
+
const mockTxHash = this.generateMockTxHash(channelId);
|
|
2942
|
+
const onChainSettlement = {
|
|
2943
|
+
channelId,
|
|
2944
|
+
settlementId,
|
|
2945
|
+
txHash: mockTxHash,
|
|
2946
|
+
blockNumber: Math.floor(Date.now() / 1e3),
|
|
2947
|
+
payerReceived: status.checkpoint.payerBalance,
|
|
2948
|
+
payeeReceived: status.checkpoint.payeeBalance,
|
|
2949
|
+
fee: this.settlementConfig.settlementFee,
|
|
2950
|
+
timestamp: now,
|
|
2951
|
+
finalized: true
|
|
2952
|
+
};
|
|
2953
|
+
this.onChainSettlements.set(channelId, onChainSettlement);
|
|
2954
|
+
this.transitionState(channelId, "completed");
|
|
2955
|
+
status.finalizedAt = now;
|
|
2956
|
+
status.txHash = mockTxHash;
|
|
2957
|
+
return {
|
|
2958
|
+
success: true,
|
|
2959
|
+
settlementId,
|
|
2960
|
+
finalBalances: {
|
|
2961
|
+
payer: status.checkpoint.payerBalance,
|
|
2962
|
+
payee: status.checkpoint.payeeBalance
|
|
2963
|
+
},
|
|
2964
|
+
txHash: mockTxHash,
|
|
2965
|
+
timestamp: now
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Get settlement status
|
|
2970
|
+
*/
|
|
2971
|
+
getStatus(channelId) {
|
|
2972
|
+
return this.settlements.get(channelId);
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Get on-chain settlement
|
|
2976
|
+
*/
|
|
2977
|
+
getOnChainSettlement(channelId) {
|
|
2978
|
+
return this.onChainSettlements.get(channelId);
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* Cancel pending settlement (before challenge period ends)
|
|
2982
|
+
*/
|
|
2983
|
+
cancel(channelId, canceller, reason) {
|
|
2984
|
+
const status = this.settlements.get(channelId);
|
|
2985
|
+
if (!status) {
|
|
2986
|
+
return { success: false, error: "No settlement found" };
|
|
2987
|
+
}
|
|
2988
|
+
if (status.state !== "pending" && status.state !== "in_progress" && status.state !== "challenging") {
|
|
2989
|
+
return { success: false, error: "Cannot cancel settlement in current state" };
|
|
2990
|
+
}
|
|
2991
|
+
if (status.initiator !== canceller && status.initiator !== "mutual") {
|
|
2992
|
+
return { success: false, error: "Only initiator can cancel" };
|
|
2993
|
+
}
|
|
2994
|
+
this.transitionState(channelId, "failed");
|
|
2995
|
+
status.error = `Cancelled: ${reason}`;
|
|
2996
|
+
return { success: true };
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Calculate settlement amounts
|
|
3000
|
+
*/
|
|
3001
|
+
calculateSettlementAmounts(checkpoint) {
|
|
3002
|
+
const payerBalance = BigInt(checkpoint.payerBalance);
|
|
3003
|
+
const payeeBalance = BigInt(checkpoint.payeeBalance);
|
|
3004
|
+
const fee = BigInt(this.settlementConfig.settlementFee);
|
|
3005
|
+
const payerReceives = payerBalance > fee ? payerBalance - fee : 0n;
|
|
3006
|
+
const payeeReceives = payeeBalance;
|
|
3007
|
+
const total = payerReceives + payeeReceives + fee;
|
|
3008
|
+
return {
|
|
3009
|
+
payerReceives: payerReceives.toString(),
|
|
3010
|
+
payeeReceives: payeeReceives.toString(),
|
|
3011
|
+
fee: fee.toString(),
|
|
3012
|
+
total: total.toString()
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Build settlement transaction data
|
|
3017
|
+
*/
|
|
3018
|
+
buildSettlementTransaction(channelId) {
|
|
3019
|
+
const status = this.settlements.get(channelId);
|
|
3020
|
+
if (!status) return null;
|
|
3021
|
+
const amounts = this.calculateSettlementAmounts(status.checkpoint);
|
|
3022
|
+
const methodId = "0x" + "settle".split("").map((c) => c.charCodeAt(0).toString(16)).join("").slice(0, 8);
|
|
3023
|
+
const data = [
|
|
3024
|
+
methodId,
|
|
3025
|
+
channelId.replace(/^ch_/, "").padStart(64, "0"),
|
|
3026
|
+
amounts.payerReceives.padStart(64, "0"),
|
|
3027
|
+
amounts.payeeReceives.padStart(64, "0"),
|
|
3028
|
+
status.checkpoint.payerSignature.replace("0x", "").padStart(128, "0")
|
|
3029
|
+
].join("");
|
|
3030
|
+
return {
|
|
3031
|
+
to: "0x" + "0".repeat(40),
|
|
3032
|
+
// Contract address placeholder
|
|
3033
|
+
data,
|
|
3034
|
+
value: "0"
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
validateRequest(request) {
|
|
3038
|
+
const errors = [];
|
|
3039
|
+
const cpValidation = this.checkpointManager.validate(request.finalCheckpoint);
|
|
3040
|
+
if (!cpValidation.valid) {
|
|
3041
|
+
errors.push(...cpValidation.errors);
|
|
3042
|
+
}
|
|
3043
|
+
if (!request.signature) {
|
|
3044
|
+
errors.push("Settlement signature is required");
|
|
3045
|
+
}
|
|
3046
|
+
const validReasons = ["mutual", "unilateral", "timeout", "dispute_resolution"];
|
|
3047
|
+
if (!validReasons.includes(request.reason)) {
|
|
3048
|
+
errors.push("Invalid settlement reason");
|
|
3049
|
+
}
|
|
3050
|
+
return { valid: errors.length === 0, errors };
|
|
3051
|
+
}
|
|
3052
|
+
transitionState(channelId, newState) {
|
|
3053
|
+
const status = this.settlements.get(channelId);
|
|
3054
|
+
if (status) {
|
|
3055
|
+
status.state = newState;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
generateSettlementId(channelId) {
|
|
3059
|
+
return `stl_${channelId.slice(-8)}_${Date.now().toString(36)}`;
|
|
3060
|
+
}
|
|
3061
|
+
generateMockTxHash(channelId) {
|
|
3062
|
+
const data = channelId + Date.now().toString();
|
|
3063
|
+
let hash = 0;
|
|
3064
|
+
for (let i = 0; i < data.length; i++) {
|
|
3065
|
+
const char = data.charCodeAt(i);
|
|
3066
|
+
hash = (hash << 5) - hash + char;
|
|
3067
|
+
hash = hash & hash;
|
|
3068
|
+
}
|
|
3069
|
+
return "0x" + Math.abs(hash).toString(16).padStart(64, "0");
|
|
3070
|
+
}
|
|
3071
|
+
};
|
|
3072
|
+
function estimateSettlementGas(hasDispute, checkpointCount) {
|
|
3073
|
+
const baseGas = 100000n;
|
|
3074
|
+
const disputeGas = hasDispute ? 50000n : 0n;
|
|
3075
|
+
const checkpointGas = BigInt(checkpointCount) * 5000n;
|
|
3076
|
+
return (baseGas + disputeGas + checkpointGas).toString();
|
|
3077
|
+
}
|
|
3078
|
+
function verifySettlementOnChain(settlement) {
|
|
3079
|
+
if (!settlement.txHash || !settlement.txHash.startsWith("0x")) {
|
|
3080
|
+
return { verified: false, error: "Invalid transaction hash" };
|
|
3081
|
+
}
|
|
3082
|
+
if (!settlement.finalized) {
|
|
3083
|
+
return { verified: false, error: "Settlement not finalized" };
|
|
3084
|
+
}
|
|
3085
|
+
return { verified: true };
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
// src/settlement/dispute.ts
|
|
3089
|
+
var DisputeManager = class {
|
|
3090
|
+
disputes = /* @__PURE__ */ new Map();
|
|
3091
|
+
config;
|
|
3092
|
+
settlementConfig;
|
|
3093
|
+
checkpointManager;
|
|
3094
|
+
constructor(checkpointManager, settlementConfig, config = {}) {
|
|
3095
|
+
this.checkpointManager = checkpointManager;
|
|
3096
|
+
this.settlementConfig = settlementConfig;
|
|
3097
|
+
this.config = {
|
|
3098
|
+
requireBond: config.requireBond ?? true,
|
|
3099
|
+
autoResolve: config.autoResolve ?? false,
|
|
3100
|
+
notifyParties: config.notifyParties ?? (() => {
|
|
3101
|
+
})
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Raise a dispute
|
|
3106
|
+
*/
|
|
3107
|
+
raise(request) {
|
|
3108
|
+
const now = Date.now();
|
|
3109
|
+
const validation = this.validateRequest(request);
|
|
3110
|
+
if (!validation.valid) {
|
|
3111
|
+
return { success: false, error: validation.errors.join(", ") };
|
|
3112
|
+
}
|
|
3113
|
+
const existingDispute = this.getByChannel(request.channelId);
|
|
3114
|
+
if (existingDispute && existingDispute.status === "pending") {
|
|
3115
|
+
return { success: false, error: "Dispute already pending for this channel" };
|
|
3116
|
+
}
|
|
3117
|
+
if (this.config.requireBond && this.settlementConfig.disputeBond !== "0") {
|
|
3118
|
+
if (!request.bond || BigInt(request.bond) < BigInt(this.settlementConfig.disputeBond)) {
|
|
3119
|
+
return {
|
|
3120
|
+
success: false,
|
|
3121
|
+
error: `Dispute bond of ${this.settlementConfig.disputeBond} required`
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
const disputeId = this.generateDisputeId(request.channelId);
|
|
3126
|
+
const responseDeadline = now + this.settlementConfig.disputeResponsePeriod * 1e3;
|
|
3127
|
+
const resolutionDeadline = now + this.settlementConfig.disputeResolutionPeriod * 1e3;
|
|
3128
|
+
const dispute = {
|
|
3129
|
+
id: disputeId,
|
|
3130
|
+
channelId: request.channelId,
|
|
3131
|
+
initiator: request.initiator,
|
|
3132
|
+
respondent: request.respondent,
|
|
3133
|
+
reason: request.reason,
|
|
3134
|
+
description: request.description,
|
|
3135
|
+
claimedPayerBalance: request.claimedPayerBalance,
|
|
3136
|
+
claimedPayeeBalance: request.claimedPayeeBalance,
|
|
3137
|
+
claimedCheckpoint: request.claimedCheckpoint,
|
|
3138
|
+
evidence: request.evidence,
|
|
3139
|
+
status: "pending",
|
|
3140
|
+
createdAt: now,
|
|
3141
|
+
responseDeadline,
|
|
3142
|
+
resolutionDeadline
|
|
3143
|
+
};
|
|
3144
|
+
this.disputes.set(disputeId, dispute);
|
|
3145
|
+
this.config.notifyParties(dispute, "raised");
|
|
3146
|
+
return { success: true, dispute };
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Respond to a dispute
|
|
3150
|
+
*/
|
|
3151
|
+
respond(response) {
|
|
3152
|
+
const dispute = this.disputes.get(response.disputeId);
|
|
3153
|
+
if (!dispute) {
|
|
3154
|
+
return { success: false, error: "Dispute not found" };
|
|
3155
|
+
}
|
|
3156
|
+
if (dispute.status !== "pending") {
|
|
3157
|
+
return { success: false, error: "Dispute is not pending" };
|
|
3158
|
+
}
|
|
3159
|
+
if (response.responder !== dispute.respondent) {
|
|
3160
|
+
return { success: false, error: "Only the respondent can respond" };
|
|
3161
|
+
}
|
|
3162
|
+
const now = Date.now();
|
|
3163
|
+
if (now > dispute.responseDeadline) {
|
|
3164
|
+
return { success: false, error: "Response deadline has passed" };
|
|
3165
|
+
}
|
|
3166
|
+
dispute.responseEvidence = response.evidence;
|
|
3167
|
+
dispute.status = "under_review";
|
|
3168
|
+
this.config.notifyParties(dispute, "responded");
|
|
3169
|
+
return { success: true };
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Resolve a dispute
|
|
3173
|
+
*/
|
|
3174
|
+
resolve(disputeId, winner, finalPayerBalance, finalPayeeBalance, reason) {
|
|
3175
|
+
const dispute = this.disputes.get(disputeId);
|
|
3176
|
+
if (!dispute) {
|
|
3177
|
+
return { success: false, error: "Dispute not found" };
|
|
3178
|
+
}
|
|
3179
|
+
if (dispute.status === "resolved" || dispute.status === "rejected") {
|
|
3180
|
+
return { success: false, error: "Dispute already resolved" };
|
|
3181
|
+
}
|
|
3182
|
+
const now = Date.now();
|
|
3183
|
+
dispute.resolution = {
|
|
3184
|
+
winner,
|
|
3185
|
+
finalPayerBalance,
|
|
3186
|
+
finalPayeeBalance,
|
|
3187
|
+
reason,
|
|
3188
|
+
timestamp: now
|
|
3189
|
+
};
|
|
3190
|
+
dispute.status = "resolved";
|
|
3191
|
+
this.config.notifyParties(dispute, "resolved");
|
|
3192
|
+
return { success: true };
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Reject a dispute
|
|
3196
|
+
*/
|
|
3197
|
+
reject(disputeId, reason) {
|
|
3198
|
+
const dispute = this.disputes.get(disputeId);
|
|
3199
|
+
if (!dispute) {
|
|
3200
|
+
return { success: false, error: "Dispute not found" };
|
|
3201
|
+
}
|
|
3202
|
+
if (dispute.status === "resolved" || dispute.status === "rejected") {
|
|
3203
|
+
return { success: false, error: "Dispute already resolved" };
|
|
3204
|
+
}
|
|
3205
|
+
dispute.status = "rejected";
|
|
3206
|
+
dispute.resolution = {
|
|
3207
|
+
finalPayerBalance: dispute.claimedPayerBalance,
|
|
3208
|
+
finalPayeeBalance: dispute.claimedPayeeBalance,
|
|
3209
|
+
reason: `Rejected: ${reason}`,
|
|
3210
|
+
timestamp: Date.now()
|
|
3211
|
+
};
|
|
3212
|
+
this.config.notifyParties(dispute, "rejected");
|
|
3213
|
+
return { success: true };
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Handle timeout (no response)
|
|
3217
|
+
*/
|
|
3218
|
+
handleTimeout(disputeId) {
|
|
3219
|
+
const dispute = this.disputes.get(disputeId);
|
|
3220
|
+
if (!dispute) {
|
|
3221
|
+
return { success: false };
|
|
3222
|
+
}
|
|
3223
|
+
const now = Date.now();
|
|
3224
|
+
if (dispute.status === "pending" && now > dispute.responseDeadline) {
|
|
3225
|
+
return this.resolve(
|
|
3226
|
+
disputeId,
|
|
3227
|
+
dispute.initiator,
|
|
3228
|
+
dispute.claimedPayerBalance,
|
|
3229
|
+
dispute.claimedPayeeBalance,
|
|
3230
|
+
"Timeout: No response from respondent"
|
|
3231
|
+
).success ? { success: true, resolution: dispute.resolution } : { success: false };
|
|
3232
|
+
}
|
|
3233
|
+
if (dispute.status === "under_review" && now > dispute.resolutionDeadline) {
|
|
3234
|
+
dispute.status = "timeout";
|
|
3235
|
+
return { success: true };
|
|
3236
|
+
}
|
|
3237
|
+
return { success: false };
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Get dispute by ID
|
|
3241
|
+
*/
|
|
3242
|
+
get(disputeId) {
|
|
3243
|
+
return this.disputes.get(disputeId);
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Get dispute by channel ID
|
|
3247
|
+
*/
|
|
3248
|
+
getByChannel(channelId) {
|
|
3249
|
+
for (const dispute of this.disputes.values()) {
|
|
3250
|
+
if (dispute.channelId === channelId && dispute.status !== "resolved" && dispute.status !== "rejected") {
|
|
3251
|
+
return dispute;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
return void 0;
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Get all disputes
|
|
3258
|
+
*/
|
|
3259
|
+
getAll() {
|
|
3260
|
+
return Array.from(this.disputes.values());
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Get disputes by status
|
|
3264
|
+
*/
|
|
3265
|
+
getByStatus(status) {
|
|
3266
|
+
return Array.from(this.disputes.values()).filter((d) => d.status === status);
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Add evidence to existing dispute
|
|
3270
|
+
*/
|
|
3271
|
+
addEvidence(disputeId, party, evidence) {
|
|
3272
|
+
const dispute = this.disputes.get(disputeId);
|
|
3273
|
+
if (!dispute) {
|
|
3274
|
+
return { success: false, error: "Dispute not found" };
|
|
3275
|
+
}
|
|
3276
|
+
if (dispute.status !== "pending" && dispute.status !== "under_review") {
|
|
3277
|
+
return { success: false, error: "Cannot add evidence to resolved dispute" };
|
|
3278
|
+
}
|
|
3279
|
+
if (party !== dispute.initiator && party !== dispute.respondent) {
|
|
3280
|
+
return { success: false, error: "Only parties can add evidence" };
|
|
3281
|
+
}
|
|
3282
|
+
if (party === dispute.initiator) {
|
|
3283
|
+
dispute.evidence.push(evidence);
|
|
3284
|
+
} else {
|
|
3285
|
+
dispute.responseEvidence = dispute.responseEvidence ?? [];
|
|
3286
|
+
dispute.responseEvidence.push(evidence);
|
|
3287
|
+
}
|
|
3288
|
+
return { success: true };
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* Evaluate dispute based on evidence
|
|
3292
|
+
*/
|
|
3293
|
+
evaluateEvidence(disputeId) {
|
|
3294
|
+
const dispute = this.disputes.get(disputeId);
|
|
3295
|
+
if (!dispute) {
|
|
3296
|
+
return { initiatorScore: 0, respondentScore: 0, recommendation: "Dispute not found" };
|
|
3297
|
+
}
|
|
3298
|
+
let initiatorScore = 0;
|
|
3299
|
+
let respondentScore = 0;
|
|
3300
|
+
for (const evidence of dispute.evidence) {
|
|
3301
|
+
if (evidence.verified) {
|
|
3302
|
+
initiatorScore += this.getEvidenceWeight(evidence.type);
|
|
3303
|
+
} else {
|
|
3304
|
+
initiatorScore += this.getEvidenceWeight(evidence.type) * 0.5;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
for (const evidence of dispute.responseEvidence ?? []) {
|
|
3308
|
+
if (evidence.verified) {
|
|
3309
|
+
respondentScore += this.getEvidenceWeight(evidence.type);
|
|
3310
|
+
} else {
|
|
3311
|
+
respondentScore += this.getEvidenceWeight(evidence.type) * 0.5;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
if (dispute.claimedCheckpoint) {
|
|
3315
|
+
const latestCheckpoint = this.checkpointManager.getLatest(dispute.channelId);
|
|
3316
|
+
if (latestCheckpoint) {
|
|
3317
|
+
const comparison = this.checkpointManager.compare(
|
|
3318
|
+
dispute.claimedCheckpoint,
|
|
3319
|
+
latestCheckpoint
|
|
3320
|
+
);
|
|
3321
|
+
if (comparison.isNewer) {
|
|
3322
|
+
initiatorScore += 10;
|
|
3323
|
+
} else {
|
|
3324
|
+
respondentScore += 10;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
let recommendation;
|
|
3329
|
+
if (initiatorScore > respondentScore * 1.5) {
|
|
3330
|
+
recommendation = "Initiator has stronger evidence";
|
|
3331
|
+
} else if (respondentScore > initiatorScore * 1.5) {
|
|
3332
|
+
recommendation = "Respondent has stronger evidence";
|
|
3333
|
+
} else {
|
|
3334
|
+
recommendation = "Evidence is inconclusive - may need arbitration";
|
|
3335
|
+
}
|
|
3336
|
+
return { initiatorScore, respondentScore, recommendation };
|
|
3337
|
+
}
|
|
3338
|
+
validateRequest(request) {
|
|
3339
|
+
const errors = [];
|
|
3340
|
+
if (!request.channelId) {
|
|
3341
|
+
errors.push("Channel ID is required");
|
|
3342
|
+
}
|
|
3343
|
+
if (!request.initiator) {
|
|
3344
|
+
errors.push("Initiator address is required");
|
|
3345
|
+
}
|
|
3346
|
+
if (!request.respondent) {
|
|
3347
|
+
errors.push("Respondent address is required");
|
|
3348
|
+
}
|
|
3349
|
+
if (request.initiator === request.respondent) {
|
|
3350
|
+
errors.push("Initiator and respondent must be different");
|
|
3351
|
+
}
|
|
3352
|
+
if (!request.reason) {
|
|
3353
|
+
errors.push("Dispute reason is required");
|
|
3354
|
+
}
|
|
3355
|
+
if (!request.description || request.description.length < 10) {
|
|
3356
|
+
errors.push("Description must be at least 10 characters");
|
|
3357
|
+
}
|
|
3358
|
+
if (request.evidence.length === 0) {
|
|
3359
|
+
errors.push("At least one piece of evidence is required");
|
|
3360
|
+
}
|
|
3361
|
+
if (BigInt(request.claimedPayerBalance) < 0n) {
|
|
3362
|
+
errors.push("Claimed payer balance cannot be negative");
|
|
3363
|
+
}
|
|
3364
|
+
if (BigInt(request.claimedPayeeBalance) < 0n) {
|
|
3365
|
+
errors.push("Claimed payee balance cannot be negative");
|
|
3366
|
+
}
|
|
3367
|
+
return { valid: errors.length === 0, errors };
|
|
3368
|
+
}
|
|
3369
|
+
getEvidenceWeight(type) {
|
|
3370
|
+
const weights = {
|
|
3371
|
+
checkpoint: 10,
|
|
3372
|
+
signature: 8,
|
|
3373
|
+
transaction: 9,
|
|
3374
|
+
state_proof: 10
|
|
3375
|
+
};
|
|
3376
|
+
return weights[type] ?? 5;
|
|
3377
|
+
}
|
|
3378
|
+
generateDisputeId(channelId) {
|
|
3379
|
+
return `dsp_${channelId.slice(-8)}_${Date.now().toString(36)}`;
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
function createCheckpointEvidence(checkpoint, description) {
|
|
3383
|
+
return {
|
|
3384
|
+
type: "checkpoint",
|
|
3385
|
+
data: JSON.stringify(checkpoint),
|
|
3386
|
+
description,
|
|
3387
|
+
timestamp: Date.now(),
|
|
3388
|
+
verified: false
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
function createSignatureEvidence(signature, signedData, description) {
|
|
3392
|
+
return {
|
|
3393
|
+
type: "signature",
|
|
3394
|
+
data: JSON.stringify({ signature, signedData }),
|
|
3395
|
+
description,
|
|
3396
|
+
timestamp: Date.now(),
|
|
3397
|
+
verified: false
|
|
3398
|
+
};
|
|
3399
|
+
}
|
|
3400
|
+
function createTransactionEvidence(txHash, description) {
|
|
3401
|
+
return {
|
|
3402
|
+
type: "transaction",
|
|
3403
|
+
data: txHash,
|
|
3404
|
+
description,
|
|
3405
|
+
timestamp: Date.now(),
|
|
3406
|
+
verified: false
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
export {
|
|
3410
|
+
BillingConfig,
|
|
3411
|
+
BillingManager,
|
|
3412
|
+
BillingPeriod,
|
|
3413
|
+
ChannelBalance,
|
|
3414
|
+
ChannelCheckpoint,
|
|
3415
|
+
ChannelCloseRequest,
|
|
3416
|
+
ChannelCloser,
|
|
3417
|
+
ChannelConfig,
|
|
3418
|
+
ChannelCreateRequest,
|
|
3419
|
+
ChannelDispute,
|
|
3420
|
+
ChannelOpener,
|
|
3421
|
+
ChannelParticipant,
|
|
3422
|
+
ChannelRecovery,
|
|
3423
|
+
ChannelState,
|
|
3424
|
+
ChannelStateMachine,
|
|
3425
|
+
CheckpointManager,
|
|
3426
|
+
CheckpointType,
|
|
3427
|
+
Dispute,
|
|
3428
|
+
DisputeEvidence,
|
|
3429
|
+
DisputeManager,
|
|
3430
|
+
DisputeReason,
|
|
3431
|
+
FinalSettlementManager,
|
|
3432
|
+
FlowController,
|
|
3433
|
+
FundingTransaction,
|
|
3434
|
+
Invoice,
|
|
3435
|
+
InvoiceItem,
|
|
3436
|
+
MeteringManager,
|
|
3437
|
+
MeteringRecord,
|
|
3438
|
+
OnChainSettlement,
|
|
3439
|
+
RateAdjustmentRequest,
|
|
3440
|
+
RateController,
|
|
3441
|
+
RateLimiter,
|
|
3442
|
+
RateType,
|
|
3443
|
+
RecoveryData,
|
|
3444
|
+
SettlementCheckpoint,
|
|
3445
|
+
SettlementConfig,
|
|
3446
|
+
SettlementRequest,
|
|
3447
|
+
SettlementResult,
|
|
3448
|
+
SettlementState,
|
|
3449
|
+
StateTransition,
|
|
3450
|
+
StreamEvent,
|
|
3451
|
+
StreamRate,
|
|
3452
|
+
StreamSession,
|
|
3453
|
+
StreamState,
|
|
3454
|
+
StreamingChannel,
|
|
3455
|
+
UsageMetrics,
|
|
3456
|
+
buildCloseTransactionData,
|
|
3457
|
+
calculateOptimalRate,
|
|
3458
|
+
calculateProRatedUsage,
|
|
3459
|
+
calculateRequiredDeposit,
|
|
3460
|
+
compareUsage,
|
|
3461
|
+
convertRate,
|
|
3462
|
+
createCheckpointEvidence,
|
|
3463
|
+
createFixedRateFlow,
|
|
3464
|
+
createSignatureEvidence,
|
|
3465
|
+
createStateSnapshot,
|
|
3466
|
+
createTieredRateFlow,
|
|
3467
|
+
createTransactionEvidence,
|
|
3468
|
+
estimateChannelDuration,
|
|
3469
|
+
estimateFutureBill,
|
|
3470
|
+
estimateSettlementGas,
|
|
3471
|
+
estimateUsage,
|
|
3472
|
+
formatCurrencyAmount,
|
|
3473
|
+
getChannelUtilization,
|
|
3474
|
+
isChannelActive,
|
|
3475
|
+
isChannelTerminal,
|
|
3476
|
+
parseCurrencyAmount,
|
|
3477
|
+
restoreFromSnapshot,
|
|
3478
|
+
signCheckpoint,
|
|
3479
|
+
verifyCheckpointSignature,
|
|
3480
|
+
verifySettlementOnChain
|
|
3481
|
+
};
|
|
3482
|
+
//# sourceMappingURL=index.js.map
|