@ruvector/edge-net 0.4.5 → 0.4.6
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/credits.js +631 -0
- package/package.json +9 -1
- package/task-execution-handler.js +868 -0
- package/tests/webrtc-datachannel-e2e-test.js +1081 -0
package/credits.js
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/edge-net Credit System MVP
|
|
3
|
+
*
|
|
4
|
+
* Simple credit accounting for distributed task execution:
|
|
5
|
+
* - Nodes earn credits when executing tasks for others
|
|
6
|
+
* - Nodes spend credits when submitting tasks
|
|
7
|
+
* - Credits stored in CRDT ledger for conflict-free replication
|
|
8
|
+
* - Persisted to Firebase for cross-session continuity
|
|
9
|
+
*
|
|
10
|
+
* @module @ruvector/edge-net/credits
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import { Ledger } from './ledger.js';
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// CREDIT CONFIGURATION
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default credit values for operations
|
|
22
|
+
*/
|
|
23
|
+
export const CREDIT_CONFIG = {
|
|
24
|
+
// Base credit cost per task submission
|
|
25
|
+
taskSubmissionCost: 1,
|
|
26
|
+
|
|
27
|
+
// Credits earned per task completion (base rate)
|
|
28
|
+
taskCompletionReward: 1,
|
|
29
|
+
|
|
30
|
+
// Multipliers for task types
|
|
31
|
+
taskTypeMultipliers: {
|
|
32
|
+
embed: 1.0,
|
|
33
|
+
process: 1.0,
|
|
34
|
+
analyze: 1.5,
|
|
35
|
+
transform: 1.0,
|
|
36
|
+
compute: 2.0,
|
|
37
|
+
aggregate: 1.5,
|
|
38
|
+
custom: 1.0,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Priority multipliers (higher priority = higher cost/reward)
|
|
42
|
+
priorityMultipliers: {
|
|
43
|
+
low: 0.5,
|
|
44
|
+
medium: 1.0,
|
|
45
|
+
high: 1.5,
|
|
46
|
+
critical: 2.0,
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Initial credits for new nodes (bootstrap)
|
|
50
|
+
initialCredits: 10,
|
|
51
|
+
|
|
52
|
+
// Minimum balance required to submit tasks (0 = no minimum)
|
|
53
|
+
minimumBalance: 0,
|
|
54
|
+
|
|
55
|
+
// Maximum transaction history to keep per node
|
|
56
|
+
maxTransactionHistory: 1000,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ============================================
|
|
60
|
+
// CREDIT SYSTEM
|
|
61
|
+
// ============================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* CreditSystem - Manages credit accounting for distributed task execution
|
|
65
|
+
*
|
|
66
|
+
* Integrates with:
|
|
67
|
+
* - Ledger (CRDT) for conflict-free credit tracking
|
|
68
|
+
* - TaskExecutionHandler for automatic credit operations
|
|
69
|
+
* - FirebaseLedgerSync for persistence
|
|
70
|
+
*/
|
|
71
|
+
export class CreditSystem extends EventEmitter {
|
|
72
|
+
/**
|
|
73
|
+
* @param {Object} options
|
|
74
|
+
* @param {string} options.nodeId - This node's identifier
|
|
75
|
+
* @param {Ledger} options.ledger - CRDT ledger instance (will create if not provided)
|
|
76
|
+
* @param {Object} options.config - Credit configuration overrides
|
|
77
|
+
*/
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
super();
|
|
80
|
+
|
|
81
|
+
this.nodeId = options.nodeId;
|
|
82
|
+
this.config = { ...CREDIT_CONFIG, ...options.config };
|
|
83
|
+
|
|
84
|
+
// Use provided ledger or create new one
|
|
85
|
+
this.ledger = options.ledger || new Ledger({
|
|
86
|
+
nodeId: this.nodeId,
|
|
87
|
+
maxTransactions: this.config.maxTransactionHistory,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Transaction tracking by taskId (for deduplication)
|
|
91
|
+
this.processedTasks = new Map(); // taskId -> { type, timestamp }
|
|
92
|
+
|
|
93
|
+
// Stats
|
|
94
|
+
this.stats = {
|
|
95
|
+
creditsEarned: 0,
|
|
96
|
+
creditsSpent: 0,
|
|
97
|
+
tasksExecuted: 0,
|
|
98
|
+
tasksSubmitted: 0,
|
|
99
|
+
insufficientFunds: 0,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.initialized = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Initialize credit system
|
|
107
|
+
*/
|
|
108
|
+
async initialize() {
|
|
109
|
+
// Initialize ledger
|
|
110
|
+
if (!this.ledger.initialized) {
|
|
111
|
+
await this.ledger.initialize();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Grant initial credits if balance is zero (new node)
|
|
115
|
+
if (this.ledger.balance() === 0 && this.config.initialCredits > 0) {
|
|
116
|
+
this.ledger.credit(this.config.initialCredits, 'Initial bootstrap credits');
|
|
117
|
+
console.log(`[Credits] Granted ${this.config.initialCredits} initial credits`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.initialized = true;
|
|
121
|
+
this.emit('initialized', { balance: this.getBalance() });
|
|
122
|
+
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================
|
|
127
|
+
// CREDIT OPERATIONS
|
|
128
|
+
// ============================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Earn credits when completing a task for another node
|
|
132
|
+
*
|
|
133
|
+
* @param {string} nodeId - The node that earned credits (usually this node)
|
|
134
|
+
* @param {number} amount - Credit amount (will be adjusted by multipliers)
|
|
135
|
+
* @param {string} taskId - Task identifier
|
|
136
|
+
* @param {Object} taskInfo - Task details for calculating multipliers
|
|
137
|
+
* @returns {Object} Transaction record
|
|
138
|
+
*/
|
|
139
|
+
earnCredits(nodeId, amount, taskId, taskInfo = {}) {
|
|
140
|
+
// Only process for this node
|
|
141
|
+
if (nodeId !== this.nodeId) {
|
|
142
|
+
console.warn(`[Credits] Ignoring earnCredits for different node: ${nodeId}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check for duplicate processing
|
|
147
|
+
if (this.processedTasks.has(`earn:${taskId}`)) {
|
|
148
|
+
console.warn(`[Credits] Task ${taskId} already credited`);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Calculate final amount with multipliers
|
|
153
|
+
const finalAmount = this._calculateAmount(amount, taskInfo);
|
|
154
|
+
|
|
155
|
+
// Record transaction in ledger
|
|
156
|
+
const tx = this.ledger.credit(finalAmount, JSON.stringify({
|
|
157
|
+
taskId,
|
|
158
|
+
type: 'task_completion',
|
|
159
|
+
taskType: taskInfo.type,
|
|
160
|
+
submitter: taskInfo.submitter,
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
// Mark as processed
|
|
164
|
+
this.processedTasks.set(`earn:${taskId}`, {
|
|
165
|
+
type: 'earn',
|
|
166
|
+
amount: finalAmount,
|
|
167
|
+
timestamp: Date.now(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Update stats
|
|
171
|
+
this.stats.creditsEarned += finalAmount;
|
|
172
|
+
this.stats.tasksExecuted++;
|
|
173
|
+
|
|
174
|
+
// Prune old processed tasks (keep last 10000)
|
|
175
|
+
this._pruneProcessedTasks();
|
|
176
|
+
|
|
177
|
+
this.emit('credits-earned', {
|
|
178
|
+
nodeId,
|
|
179
|
+
amount: finalAmount,
|
|
180
|
+
taskId,
|
|
181
|
+
balance: this.getBalance(),
|
|
182
|
+
tx,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
console.log(`[Credits] Earned ${finalAmount} credits for task ${taskId.slice(0, 8)}...`);
|
|
186
|
+
|
|
187
|
+
return tx;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Spend credits when submitting a task
|
|
192
|
+
*
|
|
193
|
+
* @param {string} nodeId - The node spending credits (usually this node)
|
|
194
|
+
* @param {number} amount - Credit amount (will be adjusted by multipliers)
|
|
195
|
+
* @param {string} taskId - Task identifier
|
|
196
|
+
* @param {Object} taskInfo - Task details for calculating cost
|
|
197
|
+
* @returns {Object|null} Transaction record or null if insufficient funds
|
|
198
|
+
*/
|
|
199
|
+
spendCredits(nodeId, amount, taskId, taskInfo = {}) {
|
|
200
|
+
// Only process for this node
|
|
201
|
+
if (nodeId !== this.nodeId) {
|
|
202
|
+
console.warn(`[Credits] Ignoring spendCredits for different node: ${nodeId}`);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check for duplicate processing
|
|
207
|
+
if (this.processedTasks.has(`spend:${taskId}`)) {
|
|
208
|
+
console.warn(`[Credits] Task ${taskId} already charged`);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Calculate final amount with multipliers
|
|
213
|
+
const finalAmount = this._calculateAmount(amount, taskInfo);
|
|
214
|
+
|
|
215
|
+
// Check balance
|
|
216
|
+
const balance = this.getBalance();
|
|
217
|
+
if (balance < finalAmount) {
|
|
218
|
+
this.stats.insufficientFunds++;
|
|
219
|
+
this.emit('insufficient-funds', {
|
|
220
|
+
nodeId,
|
|
221
|
+
required: finalAmount,
|
|
222
|
+
available: balance,
|
|
223
|
+
taskId,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// In MVP, we allow tasks even with insufficient funds
|
|
227
|
+
// (can be enforced later)
|
|
228
|
+
if (this.config.minimumBalance > 0 && balance < this.config.minimumBalance) {
|
|
229
|
+
console.warn(`[Credits] Insufficient funds: ${balance} < ${finalAmount}`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Record transaction in ledger
|
|
235
|
+
let tx;
|
|
236
|
+
try {
|
|
237
|
+
tx = this.ledger.debit(finalAmount, JSON.stringify({
|
|
238
|
+
taskId,
|
|
239
|
+
type: 'task_submission',
|
|
240
|
+
taskType: taskInfo.type,
|
|
241
|
+
targetPeer: taskInfo.targetPeer,
|
|
242
|
+
}));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
// Debit failed (insufficient balance in strict mode)
|
|
245
|
+
console.warn(`[Credits] Debit failed: ${error.message}`);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Mark as processed
|
|
250
|
+
this.processedTasks.set(`spend:${taskId}`, {
|
|
251
|
+
type: 'spend',
|
|
252
|
+
amount: finalAmount,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Update stats
|
|
257
|
+
this.stats.creditsSpent += finalAmount;
|
|
258
|
+
this.stats.tasksSubmitted++;
|
|
259
|
+
|
|
260
|
+
this._pruneProcessedTasks();
|
|
261
|
+
|
|
262
|
+
this.emit('credits-spent', {
|
|
263
|
+
nodeId,
|
|
264
|
+
amount: finalAmount,
|
|
265
|
+
taskId,
|
|
266
|
+
balance: this.getBalance(),
|
|
267
|
+
tx,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
console.log(`[Credits] Spent ${finalAmount} credits for task ${taskId.slice(0, 8)}...`);
|
|
271
|
+
|
|
272
|
+
return tx;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get current credit balance
|
|
277
|
+
*
|
|
278
|
+
* @param {string} nodeId - Node to check (defaults to this node)
|
|
279
|
+
* @returns {number} Current balance
|
|
280
|
+
*/
|
|
281
|
+
getBalance(nodeId = null) {
|
|
282
|
+
// For MVP, only track this node's balance
|
|
283
|
+
if (nodeId && nodeId !== this.nodeId) {
|
|
284
|
+
// Would need network query for other nodes
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
return this.ledger.balance();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get transaction history
|
|
292
|
+
*
|
|
293
|
+
* @param {string} nodeId - Node to get history for (defaults to this node)
|
|
294
|
+
* @param {number} limit - Maximum transactions to return
|
|
295
|
+
* @returns {Array} Transaction history
|
|
296
|
+
*/
|
|
297
|
+
getTransactionHistory(nodeId = null, limit = 50) {
|
|
298
|
+
// For MVP, only track this node's history
|
|
299
|
+
if (nodeId && nodeId !== this.nodeId) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const transactions = this.ledger.getTransactions(limit);
|
|
304
|
+
|
|
305
|
+
// Parse memo JSON and add readable info
|
|
306
|
+
return transactions.map(tx => {
|
|
307
|
+
let details = {};
|
|
308
|
+
try {
|
|
309
|
+
details = JSON.parse(tx.memo || '{}');
|
|
310
|
+
} catch {
|
|
311
|
+
details = { memo: tx.memo };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
id: tx.id,
|
|
316
|
+
type: tx.type, // 'credit' or 'debit'
|
|
317
|
+
amount: tx.amount,
|
|
318
|
+
timestamp: tx.timestamp,
|
|
319
|
+
date: new Date(tx.timestamp).toISOString(),
|
|
320
|
+
...details,
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if node has sufficient credits for a task
|
|
327
|
+
*
|
|
328
|
+
* @param {number} amount - Base amount
|
|
329
|
+
* @param {Object} taskInfo - Task info for multipliers
|
|
330
|
+
* @returns {boolean} True if sufficient
|
|
331
|
+
*/
|
|
332
|
+
hasSufficientCredits(amount, taskInfo = {}) {
|
|
333
|
+
const required = this._calculateAmount(amount, taskInfo);
|
|
334
|
+
return this.getBalance() >= required;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================
|
|
338
|
+
// CALCULATION HELPERS
|
|
339
|
+
// ============================================
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Calculate final credit amount with multipliers
|
|
343
|
+
*/
|
|
344
|
+
_calculateAmount(baseAmount, taskInfo = {}) {
|
|
345
|
+
let amount = baseAmount;
|
|
346
|
+
|
|
347
|
+
// Apply task type multiplier
|
|
348
|
+
if (taskInfo.type && this.config.taskTypeMultipliers[taskInfo.type]) {
|
|
349
|
+
amount *= this.config.taskTypeMultipliers[taskInfo.type];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Apply priority multiplier
|
|
353
|
+
if (taskInfo.priority && this.config.priorityMultipliers[taskInfo.priority]) {
|
|
354
|
+
amount *= this.config.priorityMultipliers[taskInfo.priority];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Round to 2 decimal places
|
|
358
|
+
return Math.round(amount * 100) / 100;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Prune old processed task records
|
|
363
|
+
*/
|
|
364
|
+
_pruneProcessedTasks() {
|
|
365
|
+
if (this.processedTasks.size > 10000) {
|
|
366
|
+
// Remove oldest entries
|
|
367
|
+
const entries = Array.from(this.processedTasks.entries())
|
|
368
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
369
|
+
|
|
370
|
+
const toRemove = entries.slice(0, 5000);
|
|
371
|
+
for (const [key] of toRemove) {
|
|
372
|
+
this.processedTasks.delete(key);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================
|
|
378
|
+
// INTEGRATION METHODS
|
|
379
|
+
// ============================================
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Wire to TaskExecutionHandler for automatic credit operations
|
|
383
|
+
*
|
|
384
|
+
* @param {TaskExecutionHandler} handler - Task execution handler
|
|
385
|
+
*/
|
|
386
|
+
wireToTaskHandler(handler) {
|
|
387
|
+
// Auto-credit when we complete a task
|
|
388
|
+
handler.on('task-complete', ({ taskId, from, duration, result }) => {
|
|
389
|
+
this.earnCredits(
|
|
390
|
+
this.nodeId,
|
|
391
|
+
this.config.taskCompletionReward,
|
|
392
|
+
taskId,
|
|
393
|
+
{
|
|
394
|
+
type: result?.taskType || 'compute',
|
|
395
|
+
submitter: from,
|
|
396
|
+
duration,
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Could also track task submissions if handler emits that event
|
|
402
|
+
handler.on('task-submitted', ({ taskId, to, task }) => {
|
|
403
|
+
this.spendCredits(
|
|
404
|
+
this.nodeId,
|
|
405
|
+
this.config.taskSubmissionCost,
|
|
406
|
+
taskId,
|
|
407
|
+
{
|
|
408
|
+
type: task?.type || 'compute',
|
|
409
|
+
priority: task?.priority,
|
|
410
|
+
targetPeer: to,
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
console.log('[Credits] Wired to TaskExecutionHandler');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get credit system summary
|
|
420
|
+
*/
|
|
421
|
+
getSummary() {
|
|
422
|
+
return {
|
|
423
|
+
nodeId: this.nodeId,
|
|
424
|
+
balance: this.getBalance(),
|
|
425
|
+
totalEarned: this.ledger.totalEarned(),
|
|
426
|
+
totalSpent: this.ledger.totalSpent(),
|
|
427
|
+
stats: { ...this.stats },
|
|
428
|
+
initialized: this.initialized,
|
|
429
|
+
recentTransactions: this.getTransactionHistory(null, 5),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Export ledger state for sync
|
|
435
|
+
*/
|
|
436
|
+
export() {
|
|
437
|
+
return this.ledger.export();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Merge with remote ledger state (CRDT)
|
|
442
|
+
*/
|
|
443
|
+
merge(remoteState) {
|
|
444
|
+
this.ledger.merge(remoteState);
|
|
445
|
+
this.emit('merged', { balance: this.getBalance() });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Shutdown credit system
|
|
450
|
+
*/
|
|
451
|
+
async shutdown() {
|
|
452
|
+
await this.ledger.shutdown();
|
|
453
|
+
this.initialized = false;
|
|
454
|
+
this.emit('shutdown');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ============================================
|
|
459
|
+
// FIREBASE CREDIT SYNC
|
|
460
|
+
// ============================================
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Syncs credits to Firebase for persistence and cross-node visibility
|
|
464
|
+
*/
|
|
465
|
+
export class FirebaseCreditSync extends EventEmitter {
|
|
466
|
+
/**
|
|
467
|
+
* @param {CreditSystem} creditSystem - Credit system to sync
|
|
468
|
+
* @param {Object} options
|
|
469
|
+
* @param {Object} options.firebaseConfig - Firebase configuration
|
|
470
|
+
* @param {number} options.syncInterval - Sync interval in ms
|
|
471
|
+
*/
|
|
472
|
+
constructor(creditSystem, options = {}) {
|
|
473
|
+
super();
|
|
474
|
+
|
|
475
|
+
this.credits = creditSystem;
|
|
476
|
+
this.config = options.firebaseConfig;
|
|
477
|
+
this.syncInterval = options.syncInterval || 30000;
|
|
478
|
+
|
|
479
|
+
// Firebase instances
|
|
480
|
+
this.db = null;
|
|
481
|
+
this.firebase = null;
|
|
482
|
+
this.syncTimer = null;
|
|
483
|
+
this.unsubscribers = [];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Start Firebase sync
|
|
488
|
+
*/
|
|
489
|
+
async start() {
|
|
490
|
+
if (!this.config || !this.config.apiKey || !this.config.projectId) {
|
|
491
|
+
console.log('[FirebaseCreditSync] No Firebase config, skipping sync');
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const { initializeApp, getApps } = await import('firebase/app');
|
|
497
|
+
const { getFirestore, doc, setDoc, onSnapshot, getDoc, collection } = await import('firebase/firestore');
|
|
498
|
+
|
|
499
|
+
this.firebase = { doc, setDoc, onSnapshot, getDoc, collection };
|
|
500
|
+
|
|
501
|
+
const apps = getApps();
|
|
502
|
+
const app = apps.length ? apps[0] : initializeApp(this.config);
|
|
503
|
+
this.db = getFirestore(app);
|
|
504
|
+
|
|
505
|
+
// Initial sync
|
|
506
|
+
await this.pull();
|
|
507
|
+
|
|
508
|
+
// Subscribe to updates
|
|
509
|
+
this.subscribe();
|
|
510
|
+
|
|
511
|
+
// Periodic push
|
|
512
|
+
this.syncTimer = setInterval(() => this.push(), this.syncInterval);
|
|
513
|
+
|
|
514
|
+
console.log('[FirebaseCreditSync] Started');
|
|
515
|
+
return true;
|
|
516
|
+
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.log('[FirebaseCreditSync] Failed to start:', error.message);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Pull credit state from Firebase
|
|
525
|
+
*/
|
|
526
|
+
async pull() {
|
|
527
|
+
const { doc, getDoc } = this.firebase;
|
|
528
|
+
|
|
529
|
+
const creditRef = doc(this.db, 'edgenet_credits', this.credits.nodeId);
|
|
530
|
+
const snapshot = await getDoc(creditRef);
|
|
531
|
+
|
|
532
|
+
if (snapshot.exists()) {
|
|
533
|
+
const remoteState = snapshot.data();
|
|
534
|
+
if (remoteState.ledgerState) {
|
|
535
|
+
this.credits.merge(remoteState.ledgerState);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Push credit state to Firebase
|
|
542
|
+
*/
|
|
543
|
+
async push() {
|
|
544
|
+
const { doc, setDoc } = this.firebase;
|
|
545
|
+
|
|
546
|
+
const creditRef = doc(this.db, 'edgenet_credits', this.credits.nodeId);
|
|
547
|
+
|
|
548
|
+
await setDoc(creditRef, {
|
|
549
|
+
nodeId: this.credits.nodeId,
|
|
550
|
+
balance: this.credits.getBalance(),
|
|
551
|
+
totalEarned: this.credits.ledger.totalEarned(),
|
|
552
|
+
totalSpent: this.credits.ledger.totalSpent(),
|
|
553
|
+
ledgerState: this.credits.export(),
|
|
554
|
+
updatedAt: Date.now(),
|
|
555
|
+
}, { merge: true });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Subscribe to credit updates from Firebase
|
|
560
|
+
*/
|
|
561
|
+
subscribe() {
|
|
562
|
+
const { doc, onSnapshot } = this.firebase;
|
|
563
|
+
|
|
564
|
+
const creditRef = doc(this.db, 'edgenet_credits', this.credits.nodeId);
|
|
565
|
+
|
|
566
|
+
const unsubscribe = onSnapshot(creditRef, (snapshot) => {
|
|
567
|
+
if (snapshot.exists()) {
|
|
568
|
+
const data = snapshot.data();
|
|
569
|
+
if (data.ledgerState) {
|
|
570
|
+
this.credits.merge(data.ledgerState);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
this.unsubscribers.push(unsubscribe);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Stop sync
|
|
580
|
+
*/
|
|
581
|
+
stop() {
|
|
582
|
+
if (this.syncTimer) {
|
|
583
|
+
clearInterval(this.syncTimer);
|
|
584
|
+
this.syncTimer = null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (const unsub of this.unsubscribers) {
|
|
588
|
+
if (typeof unsub === 'function') unsub();
|
|
589
|
+
}
|
|
590
|
+
this.unsubscribers = [];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ============================================
|
|
595
|
+
// CONVENIENCE FACTORY
|
|
596
|
+
// ============================================
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Create and initialize a complete credit system with optional Firebase sync
|
|
600
|
+
*
|
|
601
|
+
* @param {Object} options
|
|
602
|
+
* @param {string} options.nodeId - Node identifier
|
|
603
|
+
* @param {Ledger} options.ledger - Existing ledger (optional)
|
|
604
|
+
* @param {Object} options.firebaseConfig - Firebase config for sync
|
|
605
|
+
* @param {Object} options.config - Credit configuration overrides
|
|
606
|
+
* @returns {Promise<CreditSystem>} Initialized credit system
|
|
607
|
+
*/
|
|
608
|
+
export async function createCreditSystem(options = {}) {
|
|
609
|
+
const system = new CreditSystem(options);
|
|
610
|
+
await system.initialize();
|
|
611
|
+
|
|
612
|
+
// Start Firebase sync if configured
|
|
613
|
+
if (options.firebaseConfig) {
|
|
614
|
+
const sync = new FirebaseCreditSync(system, {
|
|
615
|
+
firebaseConfig: options.firebaseConfig,
|
|
616
|
+
syncInterval: options.syncInterval,
|
|
617
|
+
});
|
|
618
|
+
await sync.start();
|
|
619
|
+
|
|
620
|
+
// Attach sync to system for cleanup
|
|
621
|
+
system._firebaseSync = sync;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return system;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ============================================
|
|
628
|
+
// EXPORTS
|
|
629
|
+
// ============================================
|
|
630
|
+
|
|
631
|
+
export default CreditSystem;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruvector/edge-net",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Distributed compute intelligence network with WASM cryptographic security - contribute browser compute, spawn distributed AI agents, earn credits. Features Ed25519 signing, PiKey identity, Time Crystal coordination, Neural DAG attention, P2P swarm intelligence, ONNX inference, WebRTC signaling, CRDT ledger, and multi-agent workflows.",
|
|
6
6
|
"main": "ruvector_edge_net.js",
|
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
"firebase-signaling.js",
|
|
104
104
|
"firebase-setup.js",
|
|
105
105
|
"secure-access.js",
|
|
106
|
+
"credits.js",
|
|
107
|
+
"task-execution-handler.js",
|
|
106
108
|
"README.md",
|
|
107
109
|
"LICENSE"
|
|
108
110
|
],
|
|
@@ -170,6 +172,12 @@
|
|
|
170
172
|
},
|
|
171
173
|
"./secure-access": {
|
|
172
174
|
"import": "./secure-access.js"
|
|
175
|
+
},
|
|
176
|
+
"./credits": {
|
|
177
|
+
"import": "./credits.js"
|
|
178
|
+
},
|
|
179
|
+
"./task-execution": {
|
|
180
|
+
"import": "./task-execution-handler.js"
|
|
173
181
|
}
|
|
174
182
|
},
|
|
175
183
|
"sideEffects": [
|