@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 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.5",
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": [