@ruvector/edge-net 0.1.0 → 0.1.2

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/network.js ADDED
@@ -0,0 +1,820 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Edge-Net Network Module
4
+ *
5
+ * Handles:
6
+ * - Bootstrap node discovery
7
+ * - Peer announcement protocol
8
+ * - QDAG contribution recording
9
+ * - Contribution verification
10
+ * - P2P message routing
11
+ */
12
+
13
+ import { createHash, randomBytes } from 'crypto';
14
+ import { promises as fs } from 'fs';
15
+ import { homedir } from 'os';
16
+ import { join, dirname } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ // Network configuration
23
+ const NETWORK_CONFIG = {
24
+ // Genesis nodes on Google Cloud (multi-region)
25
+ bootstrapNodes: [
26
+ { id: 'genesis-us-central1', host: 'edge-net-genesis-us.ruvector.dev', port: 9000, region: 'us-central1', cloud: 'gcp' },
27
+ { id: 'genesis-europe-west1', host: 'edge-net-genesis-eu.ruvector.dev', port: 9000, region: 'europe-west1', cloud: 'gcp' },
28
+ { id: 'genesis-asia-east1', host: 'edge-net-genesis-asia.ruvector.dev', port: 9000, region: 'asia-east1', cloud: 'gcp' },
29
+ ],
30
+ // Local network simulation for offline/testing
31
+ localSimulation: true,
32
+ // Peer discovery interval (ms)
33
+ discoveryInterval: 30000,
34
+ // Heartbeat interval (ms)
35
+ heartbeatInterval: 10000,
36
+ // Max peers per node
37
+ maxPeers: 50,
38
+ // QDAG sync interval (ms)
39
+ qdagSyncInterval: 5000,
40
+ };
41
+
42
+ // Data directories
43
+ function getNetworkDir() {
44
+ return join(homedir(), '.ruvector', 'network');
45
+ }
46
+
47
+ function getPeersFile() {
48
+ return join(getNetworkDir(), 'peers.json');
49
+ }
50
+
51
+ function getQDAGFile() {
52
+ return join(getNetworkDir(), 'qdag.json');
53
+ }
54
+
55
+ // Ensure directories exist
56
+ async function ensureDirectories() {
57
+ await fs.mkdir(getNetworkDir(), { recursive: true });
58
+ }
59
+
60
+ /**
61
+ * Peer Discovery and Management
62
+ */
63
+ export class PeerManager {
64
+ constructor(localIdentity) {
65
+ this.localIdentity = localIdentity;
66
+ this.peers = new Map();
67
+ this.bootstrapNodes = NETWORK_CONFIG.bootstrapNodes;
68
+ this.discoveryInterval = null;
69
+ this.heartbeatInterval = null;
70
+ }
71
+
72
+ async initialize() {
73
+ await ensureDirectories();
74
+ await this.loadPeers();
75
+
76
+ // Start discovery and heartbeat
77
+ if (!NETWORK_CONFIG.localSimulation) {
78
+ this.startDiscovery();
79
+ this.startHeartbeat();
80
+ }
81
+
82
+ return this;
83
+ }
84
+
85
+ async loadPeers() {
86
+ try {
87
+ const data = await fs.readFile(getPeersFile(), 'utf-8');
88
+ const peers = JSON.parse(data);
89
+ for (const peer of peers) {
90
+ this.peers.set(peer.piKey, peer);
91
+ }
92
+ console.log(` šŸ“” Loaded ${this.peers.size} known peers`);
93
+ } catch (err) {
94
+ // No peers file yet
95
+ console.log(' šŸ“” Starting fresh peer list');
96
+ }
97
+ }
98
+
99
+ async savePeers() {
100
+ const peers = Array.from(this.peers.values());
101
+ await fs.writeFile(getPeersFile(), JSON.stringify(peers, null, 2));
102
+ }
103
+
104
+ /**
105
+ * Announce this node to the network
106
+ */
107
+ async announce() {
108
+ const announcement = {
109
+ type: 'announce',
110
+ piKey: this.localIdentity.piKey,
111
+ publicKey: this.localIdentity.publicKey,
112
+ siteId: this.localIdentity.siteId,
113
+ timestamp: Date.now(),
114
+ capabilities: ['compute', 'storage', 'verify'],
115
+ version: '0.1.1',
116
+ };
117
+
118
+ // Sign the announcement
119
+ announcement.signature = this.signMessage(JSON.stringify(announcement));
120
+
121
+ // In local simulation, just record ourselves
122
+ if (NETWORK_CONFIG.localSimulation) {
123
+ await this.registerPeer({
124
+ ...announcement,
125
+ lastSeen: Date.now(),
126
+ verified: true,
127
+ });
128
+ return announcement;
129
+ }
130
+
131
+ // In production, broadcast to bootstrap nodes
132
+ for (const bootstrap of this.bootstrapNodes) {
133
+ try {
134
+ await this.sendToNode(bootstrap, announcement);
135
+ } catch (err) {
136
+ // Bootstrap node unreachable
137
+ }
138
+ }
139
+
140
+ return announcement;
141
+ }
142
+
143
+ /**
144
+ * Register a peer in the local peer table
145
+ */
146
+ async registerPeer(peer) {
147
+ const existing = this.peers.get(peer.piKey);
148
+
149
+ if (existing) {
150
+ // Update last seen
151
+ existing.lastSeen = Date.now();
152
+ existing.verified = peer.verified || existing.verified;
153
+ } else {
154
+ // New peer
155
+ this.peers.set(peer.piKey, {
156
+ piKey: peer.piKey,
157
+ publicKey: peer.publicKey,
158
+ siteId: peer.siteId,
159
+ capabilities: peer.capabilities || [],
160
+ firstSeen: Date.now(),
161
+ lastSeen: Date.now(),
162
+ verified: peer.verified || false,
163
+ contributions: 0,
164
+ });
165
+ console.log(` šŸ†• New peer: ${peer.siteId} (Ļ€:${peer.piKey.slice(0, 8)})`);
166
+ }
167
+
168
+ await this.savePeers();
169
+ }
170
+
171
+ /**
172
+ * Get active peers (seen in last 5 minutes)
173
+ */
174
+ getActivePeers() {
175
+ const cutoff = Date.now() - 300000; // 5 minutes
176
+ return Array.from(this.peers.values()).filter(p => p.lastSeen > cutoff);
177
+ }
178
+
179
+ /**
180
+ * Get all known peers
181
+ */
182
+ getAllPeers() {
183
+ return Array.from(this.peers.values());
184
+ }
185
+
186
+ /**
187
+ * Verify a peer's identity
188
+ */
189
+ async verifyPeer(peer) {
190
+ // Request identity proof
191
+ const challenge = randomBytes(32).toString('hex');
192
+ const response = await this.requestProof(peer, challenge);
193
+
194
+ if (response && this.verifyProof(peer.publicKey, challenge, response)) {
195
+ peer.verified = true;
196
+ await this.savePeers();
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+
202
+ /**
203
+ * Sign a message with local identity
204
+ */
205
+ signMessage(message) {
206
+ // Simplified signing (in production uses Ed25519)
207
+ const hash = createHash('sha256')
208
+ .update(this.localIdentity.piKey)
209
+ .update(message)
210
+ .digest('hex');
211
+ return hash;
212
+ }
213
+
214
+ /**
215
+ * Verify a signature
216
+ */
217
+ verifySignature(publicKey, message, signature) {
218
+ // Simplified verification
219
+ return signature && signature.length === 64;
220
+ }
221
+
222
+ startDiscovery() {
223
+ this.discoveryInterval = setInterval(async () => {
224
+ await this.discoverPeers();
225
+ }, NETWORK_CONFIG.discoveryInterval);
226
+ }
227
+
228
+ startHeartbeat() {
229
+ this.heartbeatInterval = setInterval(async () => {
230
+ await this.announce();
231
+ }, NETWORK_CONFIG.heartbeatInterval);
232
+ }
233
+
234
+ async discoverPeers() {
235
+ // Request peer lists from known peers
236
+ for (const peer of this.getActivePeers()) {
237
+ try {
238
+ const newPeers = await this.requestPeerList(peer);
239
+ for (const newPeer of newPeers) {
240
+ await this.registerPeer(newPeer);
241
+ }
242
+ } catch (err) {
243
+ // Peer unreachable
244
+ }
245
+ }
246
+ }
247
+
248
+ // Placeholder network methods (implemented in production with WebRTC/WebSocket)
249
+ async sendToNode(node, message) {
250
+ // In production: WebSocket/WebRTC connection
251
+ return { ok: true };
252
+ }
253
+
254
+ async requestProof(peer, challenge) {
255
+ // In production: Request signed proof
256
+ return this.signMessage(challenge);
257
+ }
258
+
259
+ verifyProof(publicKey, challenge, response) {
260
+ return response && response.length > 0;
261
+ }
262
+
263
+ async requestPeerList(peer) {
264
+ return [];
265
+ }
266
+
267
+ stop() {
268
+ if (this.discoveryInterval) clearInterval(this.discoveryInterval);
269
+ if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * QDAG (Quantum DAG) Contribution Ledger
275
+ *
276
+ * A directed acyclic graph that records all contributions
277
+ * with cryptographic verification and consensus
278
+ */
279
+ export class QDAGLedger {
280
+ constructor(peerManager) {
281
+ this.peerManager = peerManager;
282
+ this.nodes = new Map(); // DAG nodes
283
+ this.tips = new Set(); // Current tips (unconfirmed)
284
+ this.confirmed = new Set(); // Confirmed nodes
285
+ this.pendingContributions = [];
286
+ this.syncInterval = null;
287
+ }
288
+
289
+ async initialize() {
290
+ await this.loadLedger();
291
+
292
+ if (!NETWORK_CONFIG.localSimulation) {
293
+ this.startSync();
294
+ }
295
+
296
+ return this;
297
+ }
298
+
299
+ async loadLedger() {
300
+ try {
301
+ const data = await fs.readFile(getQDAGFile(), 'utf-8');
302
+ const ledger = JSON.parse(data);
303
+
304
+ for (const node of ledger.nodes || []) {
305
+ this.nodes.set(node.id, node);
306
+ }
307
+ this.tips = new Set(ledger.tips || []);
308
+ this.confirmed = new Set(ledger.confirmed || []);
309
+
310
+ console.log(` šŸ“Š Loaded QDAG: ${this.nodes.size} nodes, ${this.confirmed.size} confirmed`);
311
+ } catch (err) {
312
+ // Create genesis node
313
+ const genesis = this.createNode({
314
+ type: 'genesis',
315
+ timestamp: Date.now(),
316
+ message: 'Edge-Net QDAG Genesis',
317
+ }, []);
318
+
319
+ this.nodes.set(genesis.id, genesis);
320
+ this.tips.add(genesis.id);
321
+ this.confirmed.add(genesis.id);
322
+
323
+ await this.saveLedger();
324
+ console.log(' šŸ“Š Created QDAG genesis block');
325
+ }
326
+ }
327
+
328
+ async saveLedger() {
329
+ const ledger = {
330
+ nodes: Array.from(this.nodes.values()),
331
+ tips: Array.from(this.tips),
332
+ confirmed: Array.from(this.confirmed),
333
+ savedAt: Date.now(),
334
+ };
335
+ await fs.writeFile(getQDAGFile(), JSON.stringify(ledger, null, 2));
336
+ }
337
+
338
+ /**
339
+ * Create a new QDAG node
340
+ */
341
+ createNode(data, parents) {
342
+ const nodeData = {
343
+ ...data,
344
+ parents: parents,
345
+ timestamp: Date.now(),
346
+ };
347
+
348
+ const id = createHash('sha256')
349
+ .update(JSON.stringify(nodeData))
350
+ .digest('hex')
351
+ .slice(0, 16);
352
+
353
+ return {
354
+ id,
355
+ ...nodeData,
356
+ weight: 1,
357
+ confirmations: 0,
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Record a contribution to the QDAG
363
+ */
364
+ async recordContribution(contribution) {
365
+ // Select parent tips (2 parents for DAG structure)
366
+ const parents = this.selectTips(2);
367
+
368
+ // Create contribution node
369
+ const node = this.createNode({
370
+ type: 'contribution',
371
+ contributor: contribution.piKey,
372
+ siteId: contribution.siteId,
373
+ taskId: contribution.taskId,
374
+ computeUnits: contribution.computeUnits,
375
+ credits: contribution.credits,
376
+ signature: contribution.signature,
377
+ }, parents);
378
+
379
+ // Add to DAG
380
+ this.nodes.set(node.id, node);
381
+
382
+ // Update tips
383
+ for (const parent of parents) {
384
+ this.tips.delete(parent);
385
+ }
386
+ this.tips.add(node.id);
387
+
388
+ // Update parent weights (confirm path)
389
+ await this.updateWeights(node.id);
390
+
391
+ await this.saveLedger();
392
+
393
+ console.log(` šŸ“ Recorded contribution ${node.id}: +${contribution.credits} credits`);
394
+
395
+ return node;
396
+ }
397
+
398
+ /**
399
+ * Select tips for new node parents
400
+ */
401
+ selectTips(count) {
402
+ const tips = Array.from(this.tips);
403
+ if (tips.length <= count) return tips;
404
+
405
+ // Weighted random selection based on age
406
+ const selected = [];
407
+ const available = [...tips];
408
+
409
+ while (selected.length < count && available.length > 0) {
410
+ const idx = Math.floor(Math.random() * available.length);
411
+ selected.push(available[idx]);
412
+ available.splice(idx, 1);
413
+ }
414
+
415
+ return selected;
416
+ }
417
+
418
+ /**
419
+ * Update weights along the path to genesis
420
+ */
421
+ async updateWeights(nodeId) {
422
+ const visited = new Set();
423
+ const queue = [nodeId];
424
+
425
+ while (queue.length > 0) {
426
+ const id = queue.shift();
427
+ if (visited.has(id)) continue;
428
+ visited.add(id);
429
+
430
+ const node = this.nodes.get(id);
431
+ if (!node) continue;
432
+
433
+ node.weight = (node.weight || 0) + 1;
434
+ node.confirmations = (node.confirmations || 0) + 1;
435
+
436
+ // Check for confirmation threshold
437
+ if (node.confirmations >= 3 && !this.confirmed.has(id)) {
438
+ this.confirmed.add(id);
439
+ }
440
+
441
+ // Add parents to queue
442
+ for (const parentId of node.parents || []) {
443
+ queue.push(parentId);
444
+ }
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Get contribution stats for a contributor
450
+ */
451
+ getContributorStats(piKey) {
452
+ const contributions = Array.from(this.nodes.values())
453
+ .filter(n => n.type === 'contribution' && n.contributor === piKey);
454
+
455
+ return {
456
+ totalContributions: contributions.length,
457
+ confirmedContributions: contributions.filter(c => this.confirmed.has(c.id)).length,
458
+ totalCredits: contributions.reduce((sum, c) => sum + (c.credits || 0), 0),
459
+ totalComputeUnits: contributions.reduce((sum, c) => sum + (c.computeUnits || 0), 0),
460
+ firstContribution: contributions.length > 0
461
+ ? Math.min(...contributions.map(c => c.timestamp))
462
+ : null,
463
+ lastContribution: contributions.length > 0
464
+ ? Math.max(...contributions.map(c => c.timestamp))
465
+ : null,
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Get network-wide stats
471
+ */
472
+ getNetworkStats() {
473
+ const contributions = Array.from(this.nodes.values())
474
+ .filter(n => n.type === 'contribution');
475
+
476
+ const contributors = new Set(contributions.map(c => c.contributor));
477
+
478
+ return {
479
+ totalNodes: this.nodes.size,
480
+ totalContributions: contributions.length,
481
+ confirmedNodes: this.confirmed.size,
482
+ uniqueContributors: contributors.size,
483
+ totalCredits: contributions.reduce((sum, c) => sum + (c.credits || 0), 0),
484
+ totalComputeUnits: contributions.reduce((sum, c) => sum + (c.computeUnits || 0), 0),
485
+ currentTips: this.tips.size,
486
+ };
487
+ }
488
+
489
+ /**
490
+ * Verify contribution integrity
491
+ */
492
+ async verifyContribution(nodeId) {
493
+ const node = this.nodes.get(nodeId);
494
+ if (!node) return { valid: false, reason: 'Node not found' };
495
+
496
+ // Verify parents exist
497
+ for (const parentId of node.parents || []) {
498
+ if (!this.nodes.has(parentId)) {
499
+ return { valid: false, reason: `Missing parent: ${parentId}` };
500
+ }
501
+ }
502
+
503
+ // Verify signature (if peer available)
504
+ const peer = this.peerManager.peers.get(node.contributor);
505
+ if (peer && node.signature) {
506
+ const dataToVerify = JSON.stringify({
507
+ contributor: node.contributor,
508
+ taskId: node.taskId,
509
+ computeUnits: node.computeUnits,
510
+ credits: node.credits,
511
+ });
512
+
513
+ if (!this.peerManager.verifySignature(peer.publicKey, dataToVerify, node.signature)) {
514
+ return { valid: false, reason: 'Invalid signature' };
515
+ }
516
+ }
517
+
518
+ return { valid: true, confirmations: node.confirmations };
519
+ }
520
+
521
+ /**
522
+ * Sync QDAG with peers
523
+ */
524
+ startSync() {
525
+ this.syncInterval = setInterval(async () => {
526
+ await this.syncWithPeers();
527
+ }, NETWORK_CONFIG.qdagSyncInterval);
528
+ }
529
+
530
+ async syncWithPeers() {
531
+ const activePeers = this.peerManager.getActivePeers();
532
+
533
+ for (const peer of activePeers.slice(0, 3)) {
534
+ try {
535
+ // Request missing nodes from peer
536
+ const peerTips = await this.requestTips(peer);
537
+ for (const tipId of peerTips) {
538
+ if (!this.nodes.has(tipId)) {
539
+ const node = await this.requestNode(peer, tipId);
540
+ if (node) {
541
+ await this.mergeNode(node);
542
+ }
543
+ }
544
+ }
545
+ } catch (err) {
546
+ // Peer sync failed
547
+ }
548
+ }
549
+ }
550
+
551
+ async requestTips(peer) {
552
+ // In production: Request tips via P2P
553
+ return [];
554
+ }
555
+
556
+ async requestNode(peer, nodeId) {
557
+ // In production: Request specific node via P2P
558
+ return null;
559
+ }
560
+
561
+ async mergeNode(node) {
562
+ if (this.nodes.has(node.id)) return;
563
+
564
+ // Verify node before merging
565
+ const verification = await this.verifyContribution(node.id);
566
+ if (!verification.valid) return;
567
+
568
+ this.nodes.set(node.id, node);
569
+ await this.updateWeights(node.id);
570
+ await this.saveLedger();
571
+ }
572
+
573
+ stop() {
574
+ if (this.syncInterval) clearInterval(this.syncInterval);
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Contribution Verifier
580
+ *
581
+ * Cross-verifies contributions between peers
582
+ */
583
+ export class ContributionVerifier {
584
+ constructor(peerManager, qdagLedger) {
585
+ this.peerManager = peerManager;
586
+ this.qdag = qdagLedger;
587
+ this.verificationQueue = [];
588
+ }
589
+
590
+ /**
591
+ * Submit contribution for verification
592
+ */
593
+ async submitContribution(contribution) {
594
+ // Sign the contribution
595
+ contribution.signature = this.peerManager.signMessage(
596
+ JSON.stringify({
597
+ contributor: contribution.piKey,
598
+ taskId: contribution.taskId,
599
+ computeUnits: contribution.computeUnits,
600
+ credits: contribution.credits,
601
+ })
602
+ );
603
+
604
+ // Record to local QDAG
605
+ const node = await this.qdag.recordContribution(contribution);
606
+
607
+ // In local simulation, self-verify
608
+ if (NETWORK_CONFIG.localSimulation) {
609
+ return {
610
+ nodeId: node.id,
611
+ verified: true,
612
+ confirmations: 1,
613
+ };
614
+ }
615
+
616
+ // In production, broadcast for peer verification
617
+ const verifications = await this.broadcastForVerification(node);
618
+
619
+ return {
620
+ nodeId: node.id,
621
+ verified: verifications.filter(v => v.valid).length >= 2,
622
+ confirmations: verifications.length,
623
+ };
624
+ }
625
+
626
+ /**
627
+ * Broadcast contribution for peer verification
628
+ */
629
+ async broadcastForVerification(node) {
630
+ const activePeers = this.peerManager.getActivePeers();
631
+ const verifications = [];
632
+
633
+ for (const peer of activePeers.slice(0, 5)) {
634
+ try {
635
+ const verification = await this.requestVerification(peer, node);
636
+ verifications.push(verification);
637
+ } catch (err) {
638
+ // Peer verification failed
639
+ }
640
+ }
641
+
642
+ return verifications;
643
+ }
644
+
645
+ async requestVerification(peer, node) {
646
+ // In production: Request verification via P2P
647
+ return { valid: true, peerId: peer.piKey };
648
+ }
649
+
650
+ /**
651
+ * Verify a contribution from another peer
652
+ */
653
+ async verifyFromPeer(contribution, requestingPeer) {
654
+ // Verify signature
655
+ const valid = this.peerManager.verifySignature(
656
+ requestingPeer.publicKey,
657
+ JSON.stringify({
658
+ contributor: contribution.contributor,
659
+ taskId: contribution.taskId,
660
+ computeUnits: contribution.computeUnits,
661
+ credits: contribution.credits,
662
+ }),
663
+ contribution.signature
664
+ );
665
+
666
+ // Verify compute units are reasonable
667
+ const reasonable = contribution.computeUnits > 0 &&
668
+ contribution.computeUnits < 1000000 &&
669
+ contribution.credits === Math.floor(contribution.computeUnits / 100);
670
+
671
+ return {
672
+ valid: valid && reasonable,
673
+ reason: !valid ? 'Invalid signature' : (!reasonable ? 'Unreasonable values' : 'OK'),
674
+ };
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Network Manager - High-level API
680
+ */
681
+ export class NetworkManager {
682
+ constructor(identity) {
683
+ this.identity = identity;
684
+ this.peerManager = new PeerManager(identity);
685
+ this.qdag = null;
686
+ this.verifier = null;
687
+ this.initialized = false;
688
+ }
689
+
690
+ async initialize() {
691
+ console.log('\n🌐 Initializing Edge-Net Network...');
692
+
693
+ await this.peerManager.initialize();
694
+
695
+ this.qdag = new QDAGLedger(this.peerManager);
696
+ await this.qdag.initialize();
697
+
698
+ this.verifier = new ContributionVerifier(this.peerManager, this.qdag);
699
+
700
+ // Announce to network
701
+ await this.peerManager.announce();
702
+
703
+ this.initialized = true;
704
+ console.log('āœ… Network initialized\n');
705
+
706
+ return this;
707
+ }
708
+
709
+ /**
710
+ * Record a compute contribution
711
+ */
712
+ async recordContribution(taskId, computeUnits) {
713
+ const credits = Math.floor(computeUnits / 100);
714
+
715
+ const contribution = {
716
+ piKey: this.identity.piKey,
717
+ siteId: this.identity.siteId,
718
+ taskId,
719
+ computeUnits,
720
+ credits,
721
+ timestamp: Date.now(),
722
+ };
723
+
724
+ return await this.verifier.submitContribution(contribution);
725
+ }
726
+
727
+ /**
728
+ * Get stats for this contributor
729
+ */
730
+ getMyStats() {
731
+ return this.qdag.getContributorStats(this.identity.piKey);
732
+ }
733
+
734
+ /**
735
+ * Get network-wide stats
736
+ */
737
+ getNetworkStats() {
738
+ return this.qdag.getNetworkStats();
739
+ }
740
+
741
+ /**
742
+ * Get connected peers
743
+ */
744
+ getPeers() {
745
+ return this.peerManager.getAllPeers();
746
+ }
747
+
748
+ /**
749
+ * Stop network services
750
+ */
751
+ stop() {
752
+ this.peerManager.stop();
753
+ this.qdag.stop();
754
+ }
755
+ }
756
+
757
+ // CLI interface
758
+ async function main() {
759
+ const args = process.argv.slice(2);
760
+ const command = args[0];
761
+
762
+ if (command === 'stats') {
763
+ // Show network stats
764
+ await ensureDirectories();
765
+
766
+ try {
767
+ const data = await fs.readFile(getQDAGFile(), 'utf-8');
768
+ const ledger = JSON.parse(data);
769
+
770
+ console.log('\nšŸ“Š Edge-Net Network Statistics\n');
771
+ console.log(` Total Nodes: ${ledger.nodes?.length || 0}`);
772
+ console.log(` Confirmed: ${ledger.confirmed?.length || 0}`);
773
+ console.log(` Current Tips: ${ledger.tips?.length || 0}`);
774
+
775
+ const contributions = (ledger.nodes || []).filter(n => n.type === 'contribution');
776
+ const contributors = new Set(contributions.map(c => c.contributor));
777
+
778
+ console.log(` Contributions: ${contributions.length}`);
779
+ console.log(` Contributors: ${contributors.size}`);
780
+ console.log(` Total Credits: ${contributions.reduce((s, c) => s + (c.credits || 0), 0)}`);
781
+ console.log();
782
+ } catch (err) {
783
+ console.log('No QDAG data found. Start contributing to initialize the network.');
784
+ }
785
+ } else if (command === 'peers') {
786
+ // Show known peers
787
+ await ensureDirectories();
788
+
789
+ try {
790
+ const data = await fs.readFile(getPeersFile(), 'utf-8');
791
+ const peers = JSON.parse(data);
792
+
793
+ console.log('\nšŸ‘„ Known Peers\n');
794
+ for (const peer of peers) {
795
+ const status = (Date.now() - peer.lastSeen) < 300000 ? '🟢' : '⚪';
796
+ console.log(` ${status} ${peer.siteId} (Ļ€:${peer.piKey.slice(0, 8)})`);
797
+ console.log(` First seen: ${new Date(peer.firstSeen).toLocaleString()}`);
798
+ console.log(` Last seen: ${new Date(peer.lastSeen).toLocaleString()}`);
799
+ console.log(` Verified: ${peer.verified ? 'āœ…' : 'āŒ'}`);
800
+ console.log();
801
+ }
802
+ } catch (err) {
803
+ console.log('No peers found. Join the network to discover peers.');
804
+ }
805
+ } else if (command === 'help' || !command) {
806
+ console.log(`
807
+ Edge-Net Network Module
808
+
809
+ Commands:
810
+ stats Show network statistics
811
+ peers Show known peers
812
+ help Show this help
813
+
814
+ The network module is used internally by the join CLI.
815
+ To join the network: npx edge-net-join --generate
816
+ `);
817
+ }
818
+ }
819
+
820
+ main().catch(console.error);