@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/README.md +119 -0
- package/cli.js +287 -108
- package/index.js +104 -0
- package/join.html +985 -0
- package/join.js +1333 -0
- package/network.js +820 -0
- package/networks.js +817 -0
- package/node/ruvector_edge_net.cjs +8126 -0
- package/node/ruvector_edge_net.d.ts +2289 -0
- package/node/ruvector_edge_net_bg.wasm +0 -0
- package/node/ruvector_edge_net_bg.wasm.d.ts +625 -0
- package/package.json +17 -3
- package/webrtc.js +964 -0
package/networks.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Edge-Net Multi-Network Module
|
|
4
|
+
*
|
|
5
|
+
* Enables creation, discovery, and contribution to multiple edge networks.
|
|
6
|
+
* Each network is cryptographically isolated with its own:
|
|
7
|
+
* - Genesis block and network ID
|
|
8
|
+
* - QDAG ledger
|
|
9
|
+
* - Peer registry
|
|
10
|
+
* - Access control (public/private/invite-only)
|
|
11
|
+
*
|
|
12
|
+
* Security Features:
|
|
13
|
+
* - Network ID derived from genesis hash (tamper-evident)
|
|
14
|
+
* - Ed25519 signatures for network announcements
|
|
15
|
+
* - Optional invite codes for private networks
|
|
16
|
+
* - Cryptographic proof of network membership
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createHash, randomBytes } from 'crypto';
|
|
20
|
+
import { promises as fs } from 'fs';
|
|
21
|
+
import { homedir } from 'os';
|
|
22
|
+
import { join, dirname } from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = dirname(__filename);
|
|
28
|
+
|
|
29
|
+
// ANSI colors
|
|
30
|
+
const colors = {
|
|
31
|
+
reset: '\x1b[0m',
|
|
32
|
+
bold: '\x1b[1m',
|
|
33
|
+
dim: '\x1b[2m',
|
|
34
|
+
cyan: '\x1b[36m',
|
|
35
|
+
green: '\x1b[32m',
|
|
36
|
+
yellow: '\x1b[33m',
|
|
37
|
+
blue: '\x1b[34m',
|
|
38
|
+
magenta: '\x1b[35m',
|
|
39
|
+
red: '\x1b[31m',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
|
|
43
|
+
|
|
44
|
+
// Network types
|
|
45
|
+
const NetworkType = {
|
|
46
|
+
PUBLIC: 'public', // Anyone can join and discover
|
|
47
|
+
PRIVATE: 'private', // Requires invite code to join
|
|
48
|
+
CONSORTIUM: 'consortium', // Requires approval from existing members
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Well-known public networks (bootstrap)
|
|
52
|
+
const WELL_KNOWN_NETWORKS = [
|
|
53
|
+
{
|
|
54
|
+
id: 'mainnet',
|
|
55
|
+
name: 'Edge-Net Mainnet',
|
|
56
|
+
description: 'Primary public compute network',
|
|
57
|
+
type: NetworkType.PUBLIC,
|
|
58
|
+
genesisHash: 'edgenet-mainnet-genesis-v1',
|
|
59
|
+
bootstrapNodes: ['edge-net.ruvector.dev:9000'],
|
|
60
|
+
created: '2024-01-01T00:00:00Z',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'testnet',
|
|
64
|
+
name: 'Edge-Net Testnet',
|
|
65
|
+
description: 'Testing and development network',
|
|
66
|
+
type: NetworkType.PUBLIC,
|
|
67
|
+
genesisHash: 'edgenet-testnet-genesis-v1',
|
|
68
|
+
bootstrapNodes: ['testnet.ruvector.dev:9000'],
|
|
69
|
+
created: '2024-01-01T00:00:00Z',
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// Directory structure
|
|
74
|
+
function getNetworksDir() {
|
|
75
|
+
const dir = join(homedir(), '.ruvector', 'networks');
|
|
76
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
77
|
+
return dir;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getRegistryFile() {
|
|
81
|
+
return join(getNetworksDir(), 'registry.json');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getNetworkDir(networkId) {
|
|
85
|
+
const dir = join(getNetworksDir(), networkId);
|
|
86
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
87
|
+
return dir;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Network Genesis - defines a network's identity
|
|
92
|
+
*/
|
|
93
|
+
export class NetworkGenesis {
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.version = 1;
|
|
96
|
+
this.name = options.name || 'Custom Network';
|
|
97
|
+
this.description = options.description || 'A custom edge-net network';
|
|
98
|
+
this.type = options.type || NetworkType.PUBLIC;
|
|
99
|
+
this.creator = options.creator || null; // Creator's public key
|
|
100
|
+
this.creatorSiteId = options.creatorSiteId || 'anonymous';
|
|
101
|
+
this.created = options.created || new Date().toISOString();
|
|
102
|
+
this.parameters = {
|
|
103
|
+
minContributors: options.minContributors || 1,
|
|
104
|
+
confirmationThreshold: options.confirmationThreshold || 3,
|
|
105
|
+
creditMultiplier: options.creditMultiplier || 1.0,
|
|
106
|
+
maxPeers: options.maxPeers || 100,
|
|
107
|
+
...options.parameters,
|
|
108
|
+
};
|
|
109
|
+
this.inviteRequired = this.type !== NetworkType.PUBLIC;
|
|
110
|
+
this.approvers = options.approvers || []; // For consortium networks
|
|
111
|
+
this.nonce = options.nonce || randomBytes(16).toString('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compute network ID from genesis hash
|
|
116
|
+
*/
|
|
117
|
+
computeNetworkId() {
|
|
118
|
+
const data = JSON.stringify({
|
|
119
|
+
version: this.version,
|
|
120
|
+
name: this.name,
|
|
121
|
+
type: this.type,
|
|
122
|
+
creator: this.creator,
|
|
123
|
+
created: this.created,
|
|
124
|
+
parameters: this.parameters,
|
|
125
|
+
nonce: this.nonce,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const hash = createHash('sha256').update(data).digest('hex');
|
|
129
|
+
return `net-${hash.slice(0, 16)}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create signed genesis block
|
|
134
|
+
*/
|
|
135
|
+
createSignedGenesis(signFn) {
|
|
136
|
+
const genesis = {
|
|
137
|
+
...this,
|
|
138
|
+
networkId: this.computeNetworkId(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (signFn) {
|
|
142
|
+
const dataToSign = JSON.stringify(genesis);
|
|
143
|
+
genesis.signature = signFn(dataToSign);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return genesis;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate invite code for private networks
|
|
151
|
+
*/
|
|
152
|
+
generateInviteCode() {
|
|
153
|
+
if (this.type === NetworkType.PUBLIC) {
|
|
154
|
+
throw new Error('Public networks do not require invite codes');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const networkId = this.computeNetworkId();
|
|
158
|
+
const secret = randomBytes(16).toString('hex');
|
|
159
|
+
const code = Buffer.from(`${networkId}:${secret}`).toString('base64url');
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
code,
|
|
163
|
+
networkId,
|
|
164
|
+
validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Network Registry - manages known networks
|
|
171
|
+
*/
|
|
172
|
+
export class NetworkRegistry {
|
|
173
|
+
constructor() {
|
|
174
|
+
this.networks = new Map();
|
|
175
|
+
this.activeNetwork = null;
|
|
176
|
+
this.loaded = false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async load() {
|
|
180
|
+
try {
|
|
181
|
+
// Load well-known networks
|
|
182
|
+
for (const network of WELL_KNOWN_NETWORKS) {
|
|
183
|
+
this.networks.set(network.id, {
|
|
184
|
+
...network,
|
|
185
|
+
isWellKnown: true,
|
|
186
|
+
joined: false,
|
|
187
|
+
stats: null,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Load user's network registry
|
|
192
|
+
if (existsSync(getRegistryFile())) {
|
|
193
|
+
const data = JSON.parse(await fs.readFile(getRegistryFile(), 'utf-8'));
|
|
194
|
+
|
|
195
|
+
for (const network of data.networks || []) {
|
|
196
|
+
this.networks.set(network.id, {
|
|
197
|
+
...network,
|
|
198
|
+
isWellKnown: false,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.activeNetwork = data.activeNetwork || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.loaded = true;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error('Failed to load network registry:', err.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async save() {
|
|
212
|
+
const data = {
|
|
213
|
+
version: 1,
|
|
214
|
+
activeNetwork: this.activeNetwork,
|
|
215
|
+
networks: Array.from(this.networks.values()).filter(n => !n.isWellKnown),
|
|
216
|
+
savedAt: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
await fs.writeFile(getRegistryFile(), JSON.stringify(data, null, 2));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a new network
|
|
224
|
+
*/
|
|
225
|
+
async createNetwork(options, identity) {
|
|
226
|
+
const genesis = new NetworkGenesis({
|
|
227
|
+
...options,
|
|
228
|
+
creator: identity?.publicKey,
|
|
229
|
+
creatorSiteId: identity?.siteId,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const networkId = genesis.computeNetworkId();
|
|
233
|
+
|
|
234
|
+
// Create network directory structure
|
|
235
|
+
const networkDir = getNetworkDir(networkId);
|
|
236
|
+
await fs.mkdir(join(networkDir, 'peers'), { recursive: true });
|
|
237
|
+
|
|
238
|
+
// Save genesis block
|
|
239
|
+
const genesisData = genesis.createSignedGenesis(
|
|
240
|
+
identity?.sign ? (data) => identity.sign(data) : null
|
|
241
|
+
);
|
|
242
|
+
await fs.writeFile(
|
|
243
|
+
join(networkDir, 'genesis.json'),
|
|
244
|
+
JSON.stringify(genesisData, null, 2)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Initialize QDAG for this network
|
|
248
|
+
const qdag = {
|
|
249
|
+
networkId,
|
|
250
|
+
nodes: [{
|
|
251
|
+
id: 'genesis',
|
|
252
|
+
type: 'genesis',
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
message: `Genesis: ${genesis.name}`,
|
|
255
|
+
parents: [],
|
|
256
|
+
weight: 1,
|
|
257
|
+
confirmations: 0,
|
|
258
|
+
}],
|
|
259
|
+
tips: ['genesis'],
|
|
260
|
+
confirmed: ['genesis'],
|
|
261
|
+
createdAt: Date.now(),
|
|
262
|
+
};
|
|
263
|
+
await fs.writeFile(
|
|
264
|
+
join(networkDir, 'qdag.json'),
|
|
265
|
+
JSON.stringify(qdag, null, 2)
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Initialize peer list
|
|
269
|
+
await fs.writeFile(
|
|
270
|
+
join(networkDir, 'peers.json'),
|
|
271
|
+
JSON.stringify([], null, 2)
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Register network
|
|
275
|
+
const networkEntry = {
|
|
276
|
+
id: networkId,
|
|
277
|
+
name: genesis.name,
|
|
278
|
+
description: genesis.description,
|
|
279
|
+
type: genesis.type,
|
|
280
|
+
creator: genesis.creator,
|
|
281
|
+
creatorSiteId: genesis.creatorSiteId,
|
|
282
|
+
created: genesis.created,
|
|
283
|
+
parameters: genesis.parameters,
|
|
284
|
+
genesisHash: createHash('sha256')
|
|
285
|
+
.update(JSON.stringify(genesisData))
|
|
286
|
+
.digest('hex')
|
|
287
|
+
.slice(0, 32),
|
|
288
|
+
joined: true,
|
|
289
|
+
isOwner: true,
|
|
290
|
+
stats: { nodes: 1, contributors: 0, credits: 0 },
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
this.networks.set(networkId, networkEntry);
|
|
294
|
+
await this.save();
|
|
295
|
+
|
|
296
|
+
// Generate invite codes if private
|
|
297
|
+
let inviteCodes = null;
|
|
298
|
+
if (genesis.type !== NetworkType.PUBLIC) {
|
|
299
|
+
inviteCodes = [];
|
|
300
|
+
for (let i = 0; i < 5; i++) {
|
|
301
|
+
inviteCodes.push(genesis.generateInviteCode());
|
|
302
|
+
}
|
|
303
|
+
await fs.writeFile(
|
|
304
|
+
join(networkDir, 'invites.json'),
|
|
305
|
+
JSON.stringify(inviteCodes, null, 2)
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { networkId, genesis: genesisData, inviteCodes };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Join an existing network
|
|
314
|
+
*/
|
|
315
|
+
async joinNetwork(networkId, inviteCode = null) {
|
|
316
|
+
const network = this.networks.get(networkId);
|
|
317
|
+
|
|
318
|
+
if (!network) {
|
|
319
|
+
throw new Error(`Network not found: ${networkId}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (network.joined) {
|
|
323
|
+
return { alreadyJoined: true, network };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Verify invite code for private networks
|
|
327
|
+
if (network.type === NetworkType.PRIVATE) {
|
|
328
|
+
if (!inviteCode) {
|
|
329
|
+
throw new Error('Private network requires invite code');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const isValid = await this.verifyInviteCode(networkId, inviteCode);
|
|
333
|
+
if (!isValid) {
|
|
334
|
+
throw new Error('Invalid or expired invite code');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Create local network directory
|
|
339
|
+
const networkDir = getNetworkDir(networkId);
|
|
340
|
+
|
|
341
|
+
// For well-known networks, create initial structure
|
|
342
|
+
if (network.isWellKnown) {
|
|
343
|
+
const qdag = {
|
|
344
|
+
networkId,
|
|
345
|
+
nodes: [{
|
|
346
|
+
id: 'genesis',
|
|
347
|
+
type: 'genesis',
|
|
348
|
+
timestamp: Date.now(),
|
|
349
|
+
message: `Joined: ${network.name}`,
|
|
350
|
+
parents: [],
|
|
351
|
+
weight: 1,
|
|
352
|
+
confirmations: 0,
|
|
353
|
+
}],
|
|
354
|
+
tips: ['genesis'],
|
|
355
|
+
confirmed: ['genesis'],
|
|
356
|
+
createdAt: Date.now(),
|
|
357
|
+
};
|
|
358
|
+
await fs.writeFile(
|
|
359
|
+
join(networkDir, 'qdag.json'),
|
|
360
|
+
JSON.stringify(qdag, null, 2)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
await fs.writeFile(
|
|
364
|
+
join(networkDir, 'peers.json'),
|
|
365
|
+
JSON.stringify([], null, 2)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
network.joined = true;
|
|
370
|
+
network.joinedAt = new Date().toISOString();
|
|
371
|
+
await this.save();
|
|
372
|
+
|
|
373
|
+
return { joined: true, network };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Verify invite code
|
|
378
|
+
*/
|
|
379
|
+
async verifyInviteCode(networkId, code) {
|
|
380
|
+
try {
|
|
381
|
+
const decoded = Buffer.from(code, 'base64url').toString();
|
|
382
|
+
const [codeNetworkId, secret] = decoded.split(':');
|
|
383
|
+
|
|
384
|
+
if (codeNetworkId !== networkId) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// In production, verify against network's invite registry
|
|
389
|
+
// For local simulation, accept any properly formatted code
|
|
390
|
+
return secret && secret.length === 32;
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Discover networks from DHT/registry
|
|
398
|
+
*/
|
|
399
|
+
async discoverNetworks(options = {}) {
|
|
400
|
+
const discovered = [];
|
|
401
|
+
|
|
402
|
+
// Always include well-known networks
|
|
403
|
+
for (const network of WELL_KNOWN_NETWORKS) {
|
|
404
|
+
const existing = this.networks.get(network.id);
|
|
405
|
+
discovered.push({
|
|
406
|
+
...network,
|
|
407
|
+
joined: existing?.joined || false,
|
|
408
|
+
source: 'well-known',
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Scan for locally known networks
|
|
413
|
+
try {
|
|
414
|
+
const networksDir = getNetworksDir();
|
|
415
|
+
const dirs = await fs.readdir(networksDir);
|
|
416
|
+
|
|
417
|
+
for (const dir of dirs) {
|
|
418
|
+
if (dir === 'registry.json') continue;
|
|
419
|
+
|
|
420
|
+
const genesisPath = join(networksDir, dir, 'genesis.json');
|
|
421
|
+
if (existsSync(genesisPath)) {
|
|
422
|
+
try {
|
|
423
|
+
const genesis = JSON.parse(await fs.readFile(genesisPath, 'utf-8'));
|
|
424
|
+
const existing = this.networks.get(genesis.networkId || dir);
|
|
425
|
+
|
|
426
|
+
if (!existing?.isWellKnown) {
|
|
427
|
+
discovered.push({
|
|
428
|
+
id: genesis.networkId || dir,
|
|
429
|
+
name: genesis.name,
|
|
430
|
+
description: genesis.description,
|
|
431
|
+
type: genesis.type,
|
|
432
|
+
creator: genesis.creatorSiteId,
|
|
433
|
+
created: genesis.created,
|
|
434
|
+
joined: existing?.joined || false,
|
|
435
|
+
source: 'local',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
} catch (e) {
|
|
439
|
+
// Skip invalid genesis files
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch (err) {
|
|
444
|
+
// Networks directory doesn't exist yet
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// In production: Query DHT/bootstrap nodes for public networks
|
|
448
|
+
// This is simulated here
|
|
449
|
+
|
|
450
|
+
return discovered;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Set active network for contributions
|
|
455
|
+
*/
|
|
456
|
+
async setActiveNetwork(networkId) {
|
|
457
|
+
const network = this.networks.get(networkId);
|
|
458
|
+
|
|
459
|
+
if (!network) {
|
|
460
|
+
throw new Error(`Network not found: ${networkId}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!network.joined) {
|
|
464
|
+
throw new Error(`Must join network first: ${networkId}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.activeNetwork = networkId;
|
|
468
|
+
await this.save();
|
|
469
|
+
|
|
470
|
+
return network;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Get network info
|
|
475
|
+
*/
|
|
476
|
+
getNetwork(networkId) {
|
|
477
|
+
return this.networks.get(networkId);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get active network
|
|
482
|
+
*/
|
|
483
|
+
getActiveNetwork() {
|
|
484
|
+
if (!this.activeNetwork) return null;
|
|
485
|
+
return this.networks.get(this.activeNetwork);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Get all joined networks
|
|
490
|
+
*/
|
|
491
|
+
getJoinedNetworks() {
|
|
492
|
+
return Array.from(this.networks.values()).filter(n => n.joined);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Get network statistics
|
|
497
|
+
*/
|
|
498
|
+
async getNetworkStats(networkId) {
|
|
499
|
+
const networkDir = getNetworkDir(networkId);
|
|
500
|
+
const qdagPath = join(networkDir, 'qdag.json');
|
|
501
|
+
const peersPath = join(networkDir, 'peers.json');
|
|
502
|
+
|
|
503
|
+
const stats = {
|
|
504
|
+
nodes: 0,
|
|
505
|
+
contributions: 0,
|
|
506
|
+
contributors: 0,
|
|
507
|
+
credits: 0,
|
|
508
|
+
peers: 0,
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
if (existsSync(qdagPath)) {
|
|
513
|
+
const qdag = JSON.parse(await fs.readFile(qdagPath, 'utf-8'));
|
|
514
|
+
const contributions = (qdag.nodes || []).filter(n => n.type === 'contribution');
|
|
515
|
+
|
|
516
|
+
stats.nodes = qdag.nodes?.length || 0;
|
|
517
|
+
stats.contributions = contributions.length;
|
|
518
|
+
stats.contributors = new Set(contributions.map(c => c.contributor)).size;
|
|
519
|
+
stats.credits = contributions.reduce((sum, c) => sum + (c.credits || 0), 0);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (existsSync(peersPath)) {
|
|
523
|
+
const peers = JSON.parse(await fs.readFile(peersPath, 'utf-8'));
|
|
524
|
+
stats.peers = peers.length;
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
// Stats not available
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return stats;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* List all networks
|
|
535
|
+
*/
|
|
536
|
+
listNetworks() {
|
|
537
|
+
return Array.from(this.networks.values());
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Multi-Network Manager - coordinates contributions across networks
|
|
543
|
+
*/
|
|
544
|
+
export class MultiNetworkManager {
|
|
545
|
+
constructor(identity) {
|
|
546
|
+
this.identity = identity;
|
|
547
|
+
this.registry = new NetworkRegistry();
|
|
548
|
+
this.activeConnections = new Map();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async initialize() {
|
|
552
|
+
await this.registry.load();
|
|
553
|
+
return this;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Create a new network
|
|
558
|
+
*/
|
|
559
|
+
async createNetwork(options) {
|
|
560
|
+
console.log(`\n${c('cyan', 'Creating new network...')}\n`);
|
|
561
|
+
|
|
562
|
+
const result = await this.registry.createNetwork(options, this.identity);
|
|
563
|
+
|
|
564
|
+
console.log(`${c('green', '✓')} Network created successfully!`);
|
|
565
|
+
console.log(` ${c('cyan', 'Network ID:')} ${result.networkId}`);
|
|
566
|
+
console.log(` ${c('cyan', 'Name:')} ${options.name}`);
|
|
567
|
+
console.log(` ${c('cyan', 'Type:')} ${options.type}`);
|
|
568
|
+
console.log(` ${c('cyan', 'Description:')} ${options.description || 'N/A'}`);
|
|
569
|
+
|
|
570
|
+
if (result.inviteCodes) {
|
|
571
|
+
console.log(`\n${c('bold', 'Invite Codes (share these to invite members):')}`);
|
|
572
|
+
for (const invite of result.inviteCodes.slice(0, 3)) {
|
|
573
|
+
console.log(` ${c('yellow', invite.code)}`);
|
|
574
|
+
}
|
|
575
|
+
console.log(` ${c('dim', `(${result.inviteCodes.length} codes saved to network directory)`)}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
console.log(`\n${c('dim', 'Network directory:')} ~/.ruvector/networks/${result.networkId}`);
|
|
579
|
+
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Discover available networks
|
|
585
|
+
*/
|
|
586
|
+
async discoverNetworks() {
|
|
587
|
+
console.log(`\n${c('cyan', 'Discovering networks...')}\n`);
|
|
588
|
+
|
|
589
|
+
const networks = await this.registry.discoverNetworks();
|
|
590
|
+
|
|
591
|
+
if (networks.length === 0) {
|
|
592
|
+
console.log(` ${c('dim', 'No networks found.')}`);
|
|
593
|
+
return networks;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
console.log(`${c('bold', 'Available Networks:')}\n`);
|
|
597
|
+
|
|
598
|
+
for (const network of networks) {
|
|
599
|
+
const status = network.joined ? c('green', '● Joined') : c('dim', '○ Not joined');
|
|
600
|
+
const typeIcon = network.type === NetworkType.PUBLIC ? '🌐' :
|
|
601
|
+
network.type === NetworkType.PRIVATE ? '🔒' : '🏢';
|
|
602
|
+
|
|
603
|
+
console.log(` ${status} ${typeIcon} ${c('bold', network.name)}`);
|
|
604
|
+
console.log(` ${c('dim', 'ID:')} ${network.id}`);
|
|
605
|
+
console.log(` ${c('dim', 'Type:')} ${network.type}`);
|
|
606
|
+
console.log(` ${c('dim', 'Description:')} ${network.description || 'N/A'}`);
|
|
607
|
+
console.log(` ${c('dim', 'Source:')} ${network.source}`);
|
|
608
|
+
console.log('');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return networks;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Join a network
|
|
616
|
+
*/
|
|
617
|
+
async joinNetwork(networkId, inviteCode = null) {
|
|
618
|
+
console.log(`\n${c('cyan', `Joining network ${networkId}...`)}\n`);
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const result = await this.registry.joinNetwork(networkId, inviteCode);
|
|
622
|
+
|
|
623
|
+
if (result.alreadyJoined) {
|
|
624
|
+
console.log(`${c('yellow', '⚠')} Already joined network: ${result.network.name}`);
|
|
625
|
+
} else {
|
|
626
|
+
console.log(`${c('green', '✓')} Successfully joined: ${result.network.name}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Set as active if it's the only joined network
|
|
630
|
+
const joinedNetworks = this.registry.getJoinedNetworks();
|
|
631
|
+
if (joinedNetworks.length === 1) {
|
|
632
|
+
await this.registry.setActiveNetwork(networkId);
|
|
633
|
+
console.log(` ${c('dim', 'Set as active network')}`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return result;
|
|
637
|
+
} catch (err) {
|
|
638
|
+
console.log(`${c('red', '✗')} Failed to join: ${err.message}`);
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Switch active network
|
|
645
|
+
*/
|
|
646
|
+
async switchNetwork(networkId) {
|
|
647
|
+
const network = await this.registry.setActiveNetwork(networkId);
|
|
648
|
+
console.log(`${c('green', '✓')} Active network: ${network.name} (${networkId})`);
|
|
649
|
+
return network;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Show network status
|
|
654
|
+
*/
|
|
655
|
+
async showStatus() {
|
|
656
|
+
const active = this.registry.getActiveNetwork();
|
|
657
|
+
const joined = this.registry.getJoinedNetworks();
|
|
658
|
+
|
|
659
|
+
console.log(`\n${c('bold', 'NETWORK STATUS:')}\n`);
|
|
660
|
+
|
|
661
|
+
if (!active) {
|
|
662
|
+
console.log(` ${c('yellow', '⚠')} No active network`);
|
|
663
|
+
console.log(` ${c('dim', 'Join a network to start contributing')}\n`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const stats = await this.registry.getNetworkStats(active.id);
|
|
668
|
+
|
|
669
|
+
console.log(`${c('bold', 'Active Network:')}`);
|
|
670
|
+
console.log(` ${c('cyan', 'Name:')} ${active.name}`);
|
|
671
|
+
console.log(` ${c('cyan', 'ID:')} ${active.id}`);
|
|
672
|
+
console.log(` ${c('cyan', 'Type:')} ${active.type}`);
|
|
673
|
+
console.log(` ${c('cyan', 'QDAG Nodes:')} ${stats.nodes}`);
|
|
674
|
+
console.log(` ${c('cyan', 'Contributions:')} ${stats.contributions}`);
|
|
675
|
+
console.log(` ${c('cyan', 'Contributors:')} ${stats.contributors}`);
|
|
676
|
+
console.log(` ${c('cyan', 'Total Credits:')} ${stats.credits}`);
|
|
677
|
+
console.log(` ${c('cyan', 'Connected Peers:')} ${stats.peers}`);
|
|
678
|
+
|
|
679
|
+
if (joined.length > 1) {
|
|
680
|
+
console.log(`\n${c('bold', 'Other Joined Networks:')}`);
|
|
681
|
+
for (const network of joined) {
|
|
682
|
+
if (network.id !== active.id) {
|
|
683
|
+
console.log(` ${c('dim', '○')} ${network.name} (${network.id})`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
console.log('');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Get active network directory for contributions
|
|
693
|
+
*/
|
|
694
|
+
getActiveNetworkDir() {
|
|
695
|
+
const active = this.registry.getActiveNetwork();
|
|
696
|
+
if (!active) return null;
|
|
697
|
+
return getNetworkDir(active.id);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// CLI interface
|
|
702
|
+
async function main() {
|
|
703
|
+
const args = process.argv.slice(2);
|
|
704
|
+
const command = args[0];
|
|
705
|
+
|
|
706
|
+
const registry = new NetworkRegistry();
|
|
707
|
+
await registry.load();
|
|
708
|
+
|
|
709
|
+
if (command === 'list' || command === 'ls') {
|
|
710
|
+
console.log(`\n${c('bold', 'NETWORKS:')}\n`);
|
|
711
|
+
|
|
712
|
+
const networks = registry.listNetworks();
|
|
713
|
+
const active = registry.activeNetwork;
|
|
714
|
+
|
|
715
|
+
for (const network of networks) {
|
|
716
|
+
const isActive = network.id === active;
|
|
717
|
+
const status = network.joined ?
|
|
718
|
+
(isActive ? c('green', '● Active') : c('cyan', '○ Joined')) :
|
|
719
|
+
c('dim', ' Available');
|
|
720
|
+
const typeIcon = network.type === NetworkType.PUBLIC ? '🌐' :
|
|
721
|
+
network.type === NetworkType.PRIVATE ? '🔒' : '🏢';
|
|
722
|
+
|
|
723
|
+
console.log(` ${status} ${typeIcon} ${c('bold', network.name)}`);
|
|
724
|
+
console.log(` ${c('dim', 'ID:')} ${network.id}`);
|
|
725
|
+
if (network.description) {
|
|
726
|
+
console.log(` ${c('dim', network.description)}`);
|
|
727
|
+
}
|
|
728
|
+
console.log('');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
} else if (command === 'discover') {
|
|
732
|
+
const manager = new MultiNetworkManager(null);
|
|
733
|
+
await manager.initialize();
|
|
734
|
+
await manager.discoverNetworks();
|
|
735
|
+
|
|
736
|
+
} else if (command === 'create') {
|
|
737
|
+
const name = args[1] || 'My Network';
|
|
738
|
+
const type = args.includes('--private') ? NetworkType.PRIVATE :
|
|
739
|
+
args.includes('--consortium') ? NetworkType.CONSORTIUM :
|
|
740
|
+
NetworkType.PUBLIC;
|
|
741
|
+
const description = args.find((a, i) => args[i - 1] === '--desc') || '';
|
|
742
|
+
|
|
743
|
+
const manager = new MultiNetworkManager(null);
|
|
744
|
+
await manager.initialize();
|
|
745
|
+
await manager.createNetwork({ name, type, description });
|
|
746
|
+
|
|
747
|
+
} else if (command === 'join') {
|
|
748
|
+
const networkId = args[1];
|
|
749
|
+
const inviteCode = args.find((a, i) => args[i - 1] === '--invite');
|
|
750
|
+
|
|
751
|
+
if (!networkId) {
|
|
752
|
+
console.log(`${c('red', '✗')} Usage: networks join <network-id> [--invite <code>]`);
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const manager = new MultiNetworkManager(null);
|
|
757
|
+
await manager.initialize();
|
|
758
|
+
await manager.joinNetwork(networkId, inviteCode);
|
|
759
|
+
|
|
760
|
+
} else if (command === 'switch' || command === 'use') {
|
|
761
|
+
const networkId = args[1];
|
|
762
|
+
|
|
763
|
+
if (!networkId) {
|
|
764
|
+
console.log(`${c('red', '✗')} Usage: networks switch <network-id>`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const manager = new MultiNetworkManager(null);
|
|
769
|
+
await manager.initialize();
|
|
770
|
+
await manager.switchNetwork(networkId);
|
|
771
|
+
|
|
772
|
+
} else if (command === 'status') {
|
|
773
|
+
const manager = new MultiNetworkManager(null);
|
|
774
|
+
await manager.initialize();
|
|
775
|
+
await manager.showStatus();
|
|
776
|
+
|
|
777
|
+
} else if (command === 'help' || !command) {
|
|
778
|
+
console.log(`
|
|
779
|
+
${c('bold', 'Edge-Net Multi-Network Manager')}
|
|
780
|
+
|
|
781
|
+
${c('bold', 'COMMANDS:')}
|
|
782
|
+
${c('green', 'list')} List all known networks
|
|
783
|
+
${c('green', 'discover')} Discover available networks
|
|
784
|
+
${c('green', 'create')} Create a new network
|
|
785
|
+
${c('green', 'join')} Join an existing network
|
|
786
|
+
${c('green', 'switch')} Switch active network
|
|
787
|
+
${c('green', 'status')} Show current network status
|
|
788
|
+
${c('green', 'help')} Show this help
|
|
789
|
+
|
|
790
|
+
${c('bold', 'EXAMPLES:')}
|
|
791
|
+
${c('dim', '# List networks')}
|
|
792
|
+
$ node networks.js list
|
|
793
|
+
|
|
794
|
+
${c('dim', '# Create a public network')}
|
|
795
|
+
$ node networks.js create "My Research Network" --desc "For ML research"
|
|
796
|
+
|
|
797
|
+
${c('dim', '# Create a private network')}
|
|
798
|
+
$ node networks.js create "Team Network" --private
|
|
799
|
+
|
|
800
|
+
${c('dim', '# Join a network')}
|
|
801
|
+
$ node networks.js join net-abc123def456
|
|
802
|
+
|
|
803
|
+
${c('dim', '# Join a private network with invite')}
|
|
804
|
+
$ node networks.js join net-xyz789 --invite <invite-code>
|
|
805
|
+
|
|
806
|
+
${c('dim', '# Switch active network')}
|
|
807
|
+
$ node networks.js switch net-abc123def456
|
|
808
|
+
|
|
809
|
+
${c('bold', 'NETWORK TYPES:')}
|
|
810
|
+
${c('cyan', '🌐 Public')} Anyone can join and discover
|
|
811
|
+
${c('cyan', '🔒 Private')} Requires invite code to join
|
|
812
|
+
${c('cyan', '🏢 Consortium')} Requires approval from members
|
|
813
|
+
`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
main().catch(console.error);
|