@ruvector/edge-net 0.1.1 → 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 +17 -0
- package/join.html +985 -0
- package/join.js +1333 -0
- package/network.js +820 -0
- package/networks.js +817 -0
- package/package.json +15 -3
- package/webrtc.js +964 -0
package/join.html
ADDED
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Join Edge-Net | RuVector Distributed Compute</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0a0a0f;
|
|
10
|
+
--surface: #12121a;
|
|
11
|
+
--border: #2a2a3a;
|
|
12
|
+
--primary: #6366f1;
|
|
13
|
+
--primary-hover: #818cf8;
|
|
14
|
+
--success: #22c55e;
|
|
15
|
+
--warning: #f59e0b;
|
|
16
|
+
--text: #e2e8f0;
|
|
17
|
+
--text-muted: #94a3b8;
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20
|
+
body {
|
|
21
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
22
|
+
background: var(--bg);
|
|
23
|
+
color: var(--text);
|
|
24
|
+
min-height: 100vh;
|
|
25
|
+
padding: 2rem;
|
|
26
|
+
}
|
|
27
|
+
.container {
|
|
28
|
+
max-width: 800px;
|
|
29
|
+
margin: 0 auto;
|
|
30
|
+
}
|
|
31
|
+
header {
|
|
32
|
+
text-align: center;
|
|
33
|
+
margin-bottom: 2rem;
|
|
34
|
+
}
|
|
35
|
+
h1 {
|
|
36
|
+
font-size: 2rem;
|
|
37
|
+
background: linear-gradient(135deg, var(--primary), var(--success));
|
|
38
|
+
-webkit-background-clip: text;
|
|
39
|
+
-webkit-text-fill-color: transparent;
|
|
40
|
+
margin-bottom: 0.5rem;
|
|
41
|
+
}
|
|
42
|
+
.subtitle {
|
|
43
|
+
color: var(--text-muted);
|
|
44
|
+
font-size: 0.9rem;
|
|
45
|
+
}
|
|
46
|
+
.card {
|
|
47
|
+
background: var(--surface);
|
|
48
|
+
border: 1px solid var(--border);
|
|
49
|
+
border-radius: 12px;
|
|
50
|
+
padding: 1.5rem;
|
|
51
|
+
margin-bottom: 1.5rem;
|
|
52
|
+
}
|
|
53
|
+
.card h2 {
|
|
54
|
+
font-size: 1rem;
|
|
55
|
+
color: var(--primary);
|
|
56
|
+
margin-bottom: 1rem;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 0.5rem;
|
|
60
|
+
}
|
|
61
|
+
.form-group {
|
|
62
|
+
margin-bottom: 1rem;
|
|
63
|
+
}
|
|
64
|
+
label {
|
|
65
|
+
display: block;
|
|
66
|
+
font-size: 0.85rem;
|
|
67
|
+
color: var(--text-muted);
|
|
68
|
+
margin-bottom: 0.5rem;
|
|
69
|
+
}
|
|
70
|
+
input[type="text"], input[type="password"] {
|
|
71
|
+
width: 100%;
|
|
72
|
+
padding: 0.75rem;
|
|
73
|
+
background: var(--bg);
|
|
74
|
+
border: 1px solid var(--border);
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
color: var(--text);
|
|
77
|
+
font-family: inherit;
|
|
78
|
+
font-size: 0.9rem;
|
|
79
|
+
}
|
|
80
|
+
input:focus {
|
|
81
|
+
outline: none;
|
|
82
|
+
border-color: var(--primary);
|
|
83
|
+
}
|
|
84
|
+
.btn {
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0.5rem;
|
|
88
|
+
padding: 0.75rem 1.5rem;
|
|
89
|
+
background: var(--primary);
|
|
90
|
+
color: white;
|
|
91
|
+
border: none;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
font-family: inherit;
|
|
94
|
+
font-size: 0.9rem;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
transition: background 0.2s;
|
|
97
|
+
}
|
|
98
|
+
.btn:hover { background: var(--primary-hover); }
|
|
99
|
+
.btn:disabled {
|
|
100
|
+
opacity: 0.5;
|
|
101
|
+
cursor: not-allowed;
|
|
102
|
+
}
|
|
103
|
+
.btn-secondary {
|
|
104
|
+
background: transparent;
|
|
105
|
+
border: 1px solid var(--border);
|
|
106
|
+
}
|
|
107
|
+
.btn-secondary:hover {
|
|
108
|
+
background: var(--surface);
|
|
109
|
+
}
|
|
110
|
+
.identity-display {
|
|
111
|
+
background: var(--bg);
|
|
112
|
+
border: 1px solid var(--border);
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
padding: 1rem;
|
|
115
|
+
font-size: 0.85rem;
|
|
116
|
+
}
|
|
117
|
+
.identity-row {
|
|
118
|
+
display: flex;
|
|
119
|
+
justify-content: space-between;
|
|
120
|
+
padding: 0.5rem 0;
|
|
121
|
+
border-bottom: 1px solid var(--border);
|
|
122
|
+
}
|
|
123
|
+
.identity-row:last-child { border-bottom: none; }
|
|
124
|
+
.identity-label { color: var(--text-muted); }
|
|
125
|
+
.identity-value {
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
word-break: break-all;
|
|
128
|
+
}
|
|
129
|
+
.pi-key { color: var(--success); }
|
|
130
|
+
.status {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 0.5rem;
|
|
134
|
+
padding: 0.75rem 1rem;
|
|
135
|
+
border-radius: 8px;
|
|
136
|
+
margin-bottom: 1rem;
|
|
137
|
+
font-size: 0.85rem;
|
|
138
|
+
}
|
|
139
|
+
.status.info { background: rgba(99, 102, 241, 0.1); border: 1px solid var(--primary); }
|
|
140
|
+
.status.success { background: rgba(34, 197, 94, 0.1); border: 1px solid var(--success); }
|
|
141
|
+
.status.warning { background: rgba(245, 158, 11, 0.1); border: 1px solid var(--warning); }
|
|
142
|
+
.network-stats {
|
|
143
|
+
display: grid;
|
|
144
|
+
grid-template-columns: repeat(3, 1fr);
|
|
145
|
+
gap: 1rem;
|
|
146
|
+
margin-top: 1rem;
|
|
147
|
+
}
|
|
148
|
+
.stat {
|
|
149
|
+
text-align: center;
|
|
150
|
+
padding: 1rem;
|
|
151
|
+
background: var(--bg);
|
|
152
|
+
border-radius: 8px;
|
|
153
|
+
}
|
|
154
|
+
.stat-value {
|
|
155
|
+
font-size: 1.5rem;
|
|
156
|
+
font-weight: bold;
|
|
157
|
+
color: var(--primary);
|
|
158
|
+
}
|
|
159
|
+
.stat-label {
|
|
160
|
+
font-size: 0.75rem;
|
|
161
|
+
color: var(--text-muted);
|
|
162
|
+
margin-top: 0.25rem;
|
|
163
|
+
}
|
|
164
|
+
.contribution-log {
|
|
165
|
+
max-height: 200px;
|
|
166
|
+
overflow-y: auto;
|
|
167
|
+
background: var(--bg);
|
|
168
|
+
border-radius: 8px;
|
|
169
|
+
padding: 1rem;
|
|
170
|
+
font-size: 0.8rem;
|
|
171
|
+
}
|
|
172
|
+
.log-entry {
|
|
173
|
+
padding: 0.25rem 0;
|
|
174
|
+
color: var(--text-muted);
|
|
175
|
+
}
|
|
176
|
+
.log-entry.success { color: var(--success); }
|
|
177
|
+
.log-entry.highlight { color: var(--primary); }
|
|
178
|
+
.hidden { display: none; }
|
|
179
|
+
.actions {
|
|
180
|
+
display: flex;
|
|
181
|
+
gap: 1rem;
|
|
182
|
+
flex-wrap: wrap;
|
|
183
|
+
}
|
|
184
|
+
#qr-code {
|
|
185
|
+
display: flex;
|
|
186
|
+
justify-content: center;
|
|
187
|
+
padding: 1rem;
|
|
188
|
+
background: white;
|
|
189
|
+
border-radius: 8px;
|
|
190
|
+
margin-top: 1rem;
|
|
191
|
+
}
|
|
192
|
+
.crypto-badge {
|
|
193
|
+
display: inline-flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
gap: 0.25rem;
|
|
196
|
+
padding: 0.25rem 0.5rem;
|
|
197
|
+
background: rgba(34, 197, 94, 0.1);
|
|
198
|
+
border: 1px solid var(--success);
|
|
199
|
+
border-radius: 4px;
|
|
200
|
+
font-size: 0.7rem;
|
|
201
|
+
color: var(--success);
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
204
|
+
</head>
|
|
205
|
+
<body>
|
|
206
|
+
<div class="container">
|
|
207
|
+
<header>
|
|
208
|
+
<h1>🌐 Edge-Net Join</h1>
|
|
209
|
+
<p class="subtitle">Contribute browser compute, earn credits</p>
|
|
210
|
+
<div style="margin-top: 0.5rem;">
|
|
211
|
+
<span class="crypto-badge">🔐 Ed25519</span>
|
|
212
|
+
<span class="crypto-badge">🛡️ Argon2id</span>
|
|
213
|
+
<span class="crypto-badge">🔒 AES-256-GCM</span>
|
|
214
|
+
</div>
|
|
215
|
+
</header>
|
|
216
|
+
|
|
217
|
+
<!-- Step 1: Generate or Restore Identity -->
|
|
218
|
+
<div class="card" id="identity-section">
|
|
219
|
+
<h2>🔑 Your Identity</h2>
|
|
220
|
+
|
|
221
|
+
<div id="no-identity">
|
|
222
|
+
<div class="status info">
|
|
223
|
+
<span>ℹ️</span>
|
|
224
|
+
<span>Create a new identity or restore an existing one to join the network.</span>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="form-group">
|
|
228
|
+
<label for="site-id">Site ID (your unique identifier)</label>
|
|
229
|
+
<input type="text" id="site-id" placeholder="e.g., alice, bob, node-42" />
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="form-group">
|
|
233
|
+
<label for="password">Password (for encrypted backup)</label>
|
|
234
|
+
<input type="password" id="password" placeholder="Strong password for identity encryption" />
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div class="actions">
|
|
238
|
+
<button class="btn" id="generate-btn" onclick="generateIdentity()">
|
|
239
|
+
<span>✨</span> Generate New Identity
|
|
240
|
+
</button>
|
|
241
|
+
<button class="btn btn-secondary" onclick="document.getElementById('restore-file').click()">
|
|
242
|
+
<span>📥</span> Restore from Backup
|
|
243
|
+
</button>
|
|
244
|
+
<input type="file" id="restore-file" class="hidden" accept=".identity" onchange="restoreIdentity(event)" />
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div id="has-identity" class="hidden">
|
|
249
|
+
<div class="status success">
|
|
250
|
+
<span>✅</span>
|
|
251
|
+
<span>Identity active and connected to network</span>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div class="identity-display">
|
|
255
|
+
<div class="identity-row">
|
|
256
|
+
<span class="identity-label">Site ID</span>
|
|
257
|
+
<span class="identity-value" id="display-site-id">-</span>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="identity-row">
|
|
260
|
+
<span class="identity-label">Pi-Key</span>
|
|
261
|
+
<span class="identity-value pi-key" id="display-pi-key">-</span>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="identity-row">
|
|
264
|
+
<span class="identity-label">Public Key</span>
|
|
265
|
+
<span class="identity-value" id="display-pubkey">-</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="identity-row">
|
|
268
|
+
<span class="identity-label">Created</span>
|
|
269
|
+
<span class="identity-value" id="display-created">-</span>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div class="actions" style="margin-top: 1rem;">
|
|
274
|
+
<button class="btn btn-secondary" onclick="exportIdentity()">
|
|
275
|
+
<span>📤</span> Export Backup
|
|
276
|
+
</button>
|
|
277
|
+
<button class="btn btn-secondary" onclick="copyPublicKey()">
|
|
278
|
+
<span>📋</span> Copy Public Key
|
|
279
|
+
</button>
|
|
280
|
+
<button class="btn btn-secondary" onclick="showQR()">
|
|
281
|
+
<span>📱</span> Show QR
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
<div id="qr-code" class="hidden"></div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<!-- Step 2: Network Status -->
|
|
289
|
+
<div class="card" id="network-section">
|
|
290
|
+
<h2>📡 Network Status</h2>
|
|
291
|
+
|
|
292
|
+
<div class="network-stats">
|
|
293
|
+
<div class="stat">
|
|
294
|
+
<div class="stat-value" id="stat-peers">0</div>
|
|
295
|
+
<div class="stat-label">Connected Peers</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="stat">
|
|
298
|
+
<div class="stat-value" id="stat-contributions">0</div>
|
|
299
|
+
<div class="stat-label">Contributions</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="stat">
|
|
302
|
+
<div class="stat-value" id="stat-credits">0</div>
|
|
303
|
+
<div class="stat-label">Credits Earned</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="contribution-log" id="contribution-log">
|
|
308
|
+
<div class="log-entry">Waiting for identity...</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<!-- Step 3: Contribute -->
|
|
313
|
+
<div class="card" id="contribute-section">
|
|
314
|
+
<h2>⚡ Contribute Compute</h2>
|
|
315
|
+
|
|
316
|
+
<div class="status warning" id="contribute-status">
|
|
317
|
+
<span>⏳</span>
|
|
318
|
+
<span>Generate or restore identity to start contributing</span>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="actions">
|
|
322
|
+
<button class="btn" id="start-btn" disabled onclick="startContributing()">
|
|
323
|
+
<span>▶️</span> Start Contributing
|
|
324
|
+
</button>
|
|
325
|
+
<button class="btn btn-secondary" id="stop-btn" disabled onclick="stopContributing()">
|
|
326
|
+
<span>⏹️</span> Stop
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<script type="module">
|
|
333
|
+
// Import WASM module
|
|
334
|
+
import init, * as wasm from './ruvector_edge_net.js';
|
|
335
|
+
|
|
336
|
+
let wasmModule = null;
|
|
337
|
+
let identity = null;
|
|
338
|
+
let contributing = false;
|
|
339
|
+
let contributionCount = 0;
|
|
340
|
+
let creditsEarned = 0;
|
|
341
|
+
let peerCount = 0;
|
|
342
|
+
|
|
343
|
+
// Initialize WASM
|
|
344
|
+
async function initWasm() {
|
|
345
|
+
try {
|
|
346
|
+
await init();
|
|
347
|
+
wasmModule = wasm;
|
|
348
|
+
log('WASM module loaded', 'success');
|
|
349
|
+
checkStoredIdentity();
|
|
350
|
+
} catch (err) {
|
|
351
|
+
log('Failed to load WASM: ' + err.message, 'error');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check for stored identity in localStorage
|
|
356
|
+
function checkStoredIdentity() {
|
|
357
|
+
const stored = localStorage.getItem('edge-net-identity');
|
|
358
|
+
if (stored) {
|
|
359
|
+
try {
|
|
360
|
+
identity = JSON.parse(stored);
|
|
361
|
+
showIdentity();
|
|
362
|
+
log('Identity restored from storage', 'success');
|
|
363
|
+
} catch (e) {
|
|
364
|
+
log('Stored identity corrupted', 'error');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Generate new identity
|
|
370
|
+
window.generateIdentity = async function() {
|
|
371
|
+
const siteId = document.getElementById('site-id').value.trim();
|
|
372
|
+
const password = document.getElementById('password').value;
|
|
373
|
+
|
|
374
|
+
if (!siteId) {
|
|
375
|
+
alert('Please enter a Site ID');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (password.length < 8) {
|
|
379
|
+
alert('Password must be at least 8 characters');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
document.getElementById('generate-btn').disabled = true;
|
|
384
|
+
log('Generating identity...', 'highlight');
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// Generate Pi-Key identity using WASM
|
|
388
|
+
const piKeyData = wasmModule.generate_pi_key();
|
|
389
|
+
|
|
390
|
+
// Create identity object
|
|
391
|
+
identity = {
|
|
392
|
+
siteId: siteId,
|
|
393
|
+
piKey: arrayToHex(piKeyData.pi_key).slice(0, 20),
|
|
394
|
+
publicKey: arrayToHex(piKeyData.public_key),
|
|
395
|
+
created: new Date().toISOString(),
|
|
396
|
+
sessions: 1,
|
|
397
|
+
contributions: [],
|
|
398
|
+
// Store encrypted private key for backup
|
|
399
|
+
encryptedPrivateKey: await encryptData(piKeyData.private_key, password)
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Save to localStorage
|
|
403
|
+
localStorage.setItem('edge-net-identity', JSON.stringify(identity));
|
|
404
|
+
localStorage.setItem('edge-net-password-hint', password.length.toString());
|
|
405
|
+
|
|
406
|
+
showIdentity();
|
|
407
|
+
log('Identity generated: π:' + identity.piKey, 'success');
|
|
408
|
+
|
|
409
|
+
// Announce to network
|
|
410
|
+
announceToNetwork();
|
|
411
|
+
|
|
412
|
+
} catch (err) {
|
|
413
|
+
log('Generation failed: ' + err.message, 'error');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
document.getElementById('generate-btn').disabled = false;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Show identity UI
|
|
420
|
+
function showIdentity() {
|
|
421
|
+
document.getElementById('no-identity').classList.add('hidden');
|
|
422
|
+
document.getElementById('has-identity').classList.remove('hidden');
|
|
423
|
+
|
|
424
|
+
document.getElementById('display-site-id').textContent = identity.siteId;
|
|
425
|
+
document.getElementById('display-pi-key').textContent = 'π:' + identity.piKey;
|
|
426
|
+
document.getElementById('display-pubkey').textContent = identity.publicKey.slice(0, 16) + '...';
|
|
427
|
+
document.getElementById('display-created').textContent = new Date(identity.created).toLocaleDateString();
|
|
428
|
+
|
|
429
|
+
document.getElementById('start-btn').disabled = false;
|
|
430
|
+
document.getElementById('contribute-status').innerHTML = '<span>✅</span><span>Ready to contribute compute to the network</span>';
|
|
431
|
+
document.getElementById('contribute-status').className = 'status success';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Export encrypted identity backup
|
|
435
|
+
window.exportIdentity = async function() {
|
|
436
|
+
const password = prompt('Enter password to encrypt backup:');
|
|
437
|
+
if (!password) return;
|
|
438
|
+
|
|
439
|
+
const backup = {
|
|
440
|
+
version: 1,
|
|
441
|
+
identity: identity,
|
|
442
|
+
exported: new Date().toISOString()
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const encrypted = await encryptData(JSON.stringify(backup), password);
|
|
446
|
+
const blob = new Blob([encrypted], { type: 'application/octet-stream' });
|
|
447
|
+
const url = URL.createObjectURL(blob);
|
|
448
|
+
|
|
449
|
+
const a = document.createElement('a');
|
|
450
|
+
a.href = url;
|
|
451
|
+
a.download = `${identity.siteId}.identity`;
|
|
452
|
+
a.click();
|
|
453
|
+
|
|
454
|
+
URL.revokeObjectURL(url);
|
|
455
|
+
log('Identity exported to ' + identity.siteId + '.identity', 'success');
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// Restore identity from backup
|
|
459
|
+
window.restoreIdentity = async function(event) {
|
|
460
|
+
const file = event.target.files[0];
|
|
461
|
+
if (!file) return;
|
|
462
|
+
|
|
463
|
+
const password = prompt('Enter backup password:');
|
|
464
|
+
if (!password) return;
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const encrypted = await file.text();
|
|
468
|
+
const decrypted = await decryptData(encrypted, password);
|
|
469
|
+
const backup = JSON.parse(decrypted);
|
|
470
|
+
|
|
471
|
+
identity = backup.identity;
|
|
472
|
+
identity.sessions = (identity.sessions || 0) + 1;
|
|
473
|
+
|
|
474
|
+
localStorage.setItem('edge-net-identity', JSON.stringify(identity));
|
|
475
|
+
showIdentity();
|
|
476
|
+
|
|
477
|
+
log('Identity restored: π:' + identity.piKey, 'success');
|
|
478
|
+
announceToNetwork();
|
|
479
|
+
|
|
480
|
+
} catch (err) {
|
|
481
|
+
alert('Failed to restore: ' + err.message);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// Copy public key
|
|
486
|
+
window.copyPublicKey = function() {
|
|
487
|
+
navigator.clipboard.writeText(identity.publicKey);
|
|
488
|
+
log('Public key copied to clipboard', 'success');
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// Show QR code
|
|
492
|
+
window.showQR = function() {
|
|
493
|
+
const qrDiv = document.getElementById('qr-code');
|
|
494
|
+
if (qrDiv.classList.contains('hidden')) {
|
|
495
|
+
// Simple text QR representation (in production, use a QR library)
|
|
496
|
+
qrDiv.innerHTML = `<div style="text-align: center; color: #000;">
|
|
497
|
+
<div style="font-size: 0.8rem; margin-bottom: 0.5rem;">Scan to verify</div>
|
|
498
|
+
<div style="font-family: monospace; font-size: 0.7rem; word-break: break-all; max-width: 200px;">
|
|
499
|
+
${identity.publicKey}
|
|
500
|
+
</div>
|
|
501
|
+
</div>`;
|
|
502
|
+
qrDiv.classList.remove('hidden');
|
|
503
|
+
} else {
|
|
504
|
+
qrDiv.classList.add('hidden');
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Start contributing compute
|
|
509
|
+
window.startContributing = function() {
|
|
510
|
+
if (!identity) return;
|
|
511
|
+
|
|
512
|
+
contributing = true;
|
|
513
|
+
document.getElementById('start-btn').disabled = true;
|
|
514
|
+
document.getElementById('stop-btn').disabled = false;
|
|
515
|
+
|
|
516
|
+
log('Starting compute contribution...', 'highlight');
|
|
517
|
+
contributeLoop();
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Stop contributing
|
|
521
|
+
window.stopContributing = function() {
|
|
522
|
+
contributing = false;
|
|
523
|
+
document.getElementById('start-btn').disabled = false;
|
|
524
|
+
document.getElementById('stop-btn').disabled = true;
|
|
525
|
+
log('Compute contribution stopped', 'warning');
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Contribution loop
|
|
529
|
+
async function contributeLoop() {
|
|
530
|
+
while (contributing) {
|
|
531
|
+
try {
|
|
532
|
+
// Simulate compute task
|
|
533
|
+
const taskId = Math.random().toString(36).slice(2, 10);
|
|
534
|
+
log(`Processing task ${taskId}...`);
|
|
535
|
+
|
|
536
|
+
// Do actual WASM computation
|
|
537
|
+
const start = performance.now();
|
|
538
|
+
|
|
539
|
+
// Vector computation task
|
|
540
|
+
const vectors = [];
|
|
541
|
+
for (let i = 0; i < 100; i++) {
|
|
542
|
+
vectors.push(new Float32Array(128).map(() => Math.random()));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Compute similarities (actual work)
|
|
546
|
+
let computed = 0;
|
|
547
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
548
|
+
for (let j = i + 1; j < vectors.length; j++) {
|
|
549
|
+
dotProduct(vectors[i], vectors[j]);
|
|
550
|
+
computed++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const elapsed = performance.now() - start;
|
|
555
|
+
|
|
556
|
+
// Record contribution
|
|
557
|
+
contributionCount++;
|
|
558
|
+
const credits = Math.floor(computed / 100);
|
|
559
|
+
creditsEarned += credits;
|
|
560
|
+
|
|
561
|
+
// Update stats
|
|
562
|
+
document.getElementById('stat-contributions').textContent = contributionCount;
|
|
563
|
+
document.getElementById('stat-credits').textContent = creditsEarned;
|
|
564
|
+
|
|
565
|
+
// Save contribution
|
|
566
|
+
identity.contributions.push({
|
|
567
|
+
taskId,
|
|
568
|
+
computed,
|
|
569
|
+
credits,
|
|
570
|
+
timestamp: Date.now()
|
|
571
|
+
});
|
|
572
|
+
localStorage.setItem('edge-net-identity', JSON.stringify(identity));
|
|
573
|
+
|
|
574
|
+
log(`Task ${taskId} complete: ${computed} ops, +${credits} credits (${elapsed.toFixed(1)}ms)`, 'success');
|
|
575
|
+
|
|
576
|
+
// Wait before next task
|
|
577
|
+
await sleep(2000);
|
|
578
|
+
|
|
579
|
+
} catch (err) {
|
|
580
|
+
log('Task error: ' + err.message, 'error');
|
|
581
|
+
await sleep(5000);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ========================================
|
|
587
|
+
// Real WebRTC P2P Implementation
|
|
588
|
+
// ========================================
|
|
589
|
+
|
|
590
|
+
// WebRTC Configuration
|
|
591
|
+
const WEBRTC_CONFIG = {
|
|
592
|
+
iceServers: [
|
|
593
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
594
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
595
|
+
]
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Relay server for signaling
|
|
599
|
+
const RELAY_URL = window.location.hostname === 'localhost'
|
|
600
|
+
? 'ws://localhost:8080'
|
|
601
|
+
: 'wss://edge-net-relay.ruvector.dev';
|
|
602
|
+
|
|
603
|
+
let signalingSocket = null;
|
|
604
|
+
const peerConnections = new Map(); // peerId -> RTCPeerConnection
|
|
605
|
+
const dataChannels = new Map(); // peerId -> RTCDataChannel
|
|
606
|
+
|
|
607
|
+
// Connect to signaling server and announce
|
|
608
|
+
async function announceToNetwork() {
|
|
609
|
+
log('Connecting to network...', 'highlight');
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
signalingSocket = new WebSocket(RELAY_URL);
|
|
613
|
+
|
|
614
|
+
signalingSocket.onopen = () => {
|
|
615
|
+
log('Connected to relay server', 'success');
|
|
616
|
+
|
|
617
|
+
// Register with network
|
|
618
|
+
signalingSocket.send(JSON.stringify({
|
|
619
|
+
type: 'register',
|
|
620
|
+
nodeId: identity.piKey,
|
|
621
|
+
publicKey: identity.publicKey,
|
|
622
|
+
siteId: identity.siteId,
|
|
623
|
+
}));
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
signalingSocket.onmessage = async (event) => {
|
|
627
|
+
try {
|
|
628
|
+
const message = JSON.parse(event.data);
|
|
629
|
+
await handleSignalingMessage(message);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
console.error('Message error:', err);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
signalingSocket.onclose = () => {
|
|
636
|
+
log('Disconnected from relay', 'warning');
|
|
637
|
+
// Attempt reconnection after delay
|
|
638
|
+
setTimeout(announceToNetwork, 5000);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
signalingSocket.onerror = (err) => {
|
|
642
|
+
log('Relay connection error', 'error');
|
|
643
|
+
console.error('WebSocket error:', err);
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
} catch (err) {
|
|
647
|
+
log('Failed to connect: ' + err.message, 'error');
|
|
648
|
+
// Fallback to simulation
|
|
649
|
+
simulatePeers();
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Handle signaling messages
|
|
654
|
+
async function handleSignalingMessage(message) {
|
|
655
|
+
switch (message.type) {
|
|
656
|
+
case 'welcome':
|
|
657
|
+
log(`Registered as ${message.nodeId}`, 'success');
|
|
658
|
+
peerCount = message.peers?.length || 0;
|
|
659
|
+
document.getElementById('stat-peers').textContent = peerCount;
|
|
660
|
+
|
|
661
|
+
// Connect to existing peers
|
|
662
|
+
if (message.peers) {
|
|
663
|
+
for (const peerId of message.peers) {
|
|
664
|
+
if (identity.piKey > peerId) {
|
|
665
|
+
await initiatePeerConnection(peerId);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
break;
|
|
670
|
+
|
|
671
|
+
case 'node_joined':
|
|
672
|
+
log(`Peer joined: ${message.nodeId.slice(0, 8)}...`, 'success');
|
|
673
|
+
peerCount++;
|
|
674
|
+
document.getElementById('stat-peers').textContent = peerCount;
|
|
675
|
+
|
|
676
|
+
// Initiate connection if we have higher ID
|
|
677
|
+
if (identity.piKey > message.nodeId) {
|
|
678
|
+
await initiatePeerConnection(message.nodeId);
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
|
|
682
|
+
case 'node_left':
|
|
683
|
+
log(`Peer left: ${message.nodeId.slice(0, 8)}...`);
|
|
684
|
+
closePeerConnection(message.nodeId);
|
|
685
|
+
peerCount = Math.max(0, peerCount - 1);
|
|
686
|
+
document.getElementById('stat-peers').textContent = peerCount;
|
|
687
|
+
break;
|
|
688
|
+
|
|
689
|
+
case 'webrtc_offer':
|
|
690
|
+
await handleOffer(message.from, message.offer);
|
|
691
|
+
break;
|
|
692
|
+
|
|
693
|
+
case 'webrtc_answer':
|
|
694
|
+
await handleAnswer(message.from, message.answer);
|
|
695
|
+
break;
|
|
696
|
+
|
|
697
|
+
case 'webrtc_ice':
|
|
698
|
+
await handleIceCandidate(message.from, message.candidate);
|
|
699
|
+
break;
|
|
700
|
+
|
|
701
|
+
case 'webrtc_disconnect':
|
|
702
|
+
closePeerConnection(message.from);
|
|
703
|
+
break;
|
|
704
|
+
|
|
705
|
+
case 'time_crystal_sync':
|
|
706
|
+
// Update network phase
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Initiate WebRTC connection to peer
|
|
712
|
+
async function initiatePeerConnection(peerId) {
|
|
713
|
+
if (peerConnections.has(peerId)) return;
|
|
714
|
+
|
|
715
|
+
log(`Connecting to ${peerId.slice(0, 8)}...`);
|
|
716
|
+
|
|
717
|
+
const pc = new RTCPeerConnection(WEBRTC_CONFIG);
|
|
718
|
+
peerConnections.set(peerId, pc);
|
|
719
|
+
|
|
720
|
+
setupPeerConnection(pc, peerId);
|
|
721
|
+
|
|
722
|
+
// Create data channel
|
|
723
|
+
const channel = pc.createDataChannel('edge-net', {
|
|
724
|
+
ordered: true,
|
|
725
|
+
maxRetransmits: 3,
|
|
726
|
+
});
|
|
727
|
+
setupDataChannel(channel, peerId);
|
|
728
|
+
|
|
729
|
+
// Create and send offer
|
|
730
|
+
const offer = await pc.createOffer();
|
|
731
|
+
await pc.setLocalDescription(offer);
|
|
732
|
+
|
|
733
|
+
signalingSocket.send(JSON.stringify({
|
|
734
|
+
type: 'webrtc_offer',
|
|
735
|
+
targetId: peerId,
|
|
736
|
+
offer: offer,
|
|
737
|
+
}));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Handle incoming offer
|
|
741
|
+
async function handleOffer(peerId, offer) {
|
|
742
|
+
log(`Offer from ${peerId.slice(0, 8)}...`);
|
|
743
|
+
|
|
744
|
+
const pc = new RTCPeerConnection(WEBRTC_CONFIG);
|
|
745
|
+
peerConnections.set(peerId, pc);
|
|
746
|
+
|
|
747
|
+
setupPeerConnection(pc, peerId);
|
|
748
|
+
|
|
749
|
+
// Handle incoming data channel
|
|
750
|
+
pc.ondatachannel = (event) => {
|
|
751
|
+
setupDataChannel(event.channel, peerId);
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
|
755
|
+
const answer = await pc.createAnswer();
|
|
756
|
+
await pc.setLocalDescription(answer);
|
|
757
|
+
|
|
758
|
+
signalingSocket.send(JSON.stringify({
|
|
759
|
+
type: 'webrtc_answer',
|
|
760
|
+
targetId: peerId,
|
|
761
|
+
answer: answer,
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Handle incoming answer
|
|
766
|
+
async function handleAnswer(peerId, answer) {
|
|
767
|
+
const pc = peerConnections.get(peerId);
|
|
768
|
+
if (pc) {
|
|
769
|
+
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Handle ICE candidate
|
|
774
|
+
async function handleIceCandidate(peerId, candidate) {
|
|
775
|
+
const pc = peerConnections.get(peerId);
|
|
776
|
+
if (pc && candidate) {
|
|
777
|
+
try {
|
|
778
|
+
await pc.addIceCandidate(new RTCIceCandidate(candidate));
|
|
779
|
+
} catch (err) {
|
|
780
|
+
console.warn('ICE candidate error:', err);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Setup peer connection event handlers
|
|
786
|
+
function setupPeerConnection(pc, peerId) {
|
|
787
|
+
pc.onicecandidate = (event) => {
|
|
788
|
+
if (event.candidate && signalingSocket?.readyState === WebSocket.OPEN) {
|
|
789
|
+
signalingSocket.send(JSON.stringify({
|
|
790
|
+
type: 'webrtc_ice',
|
|
791
|
+
targetId: peerId,
|
|
792
|
+
candidate: event.candidate,
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
pc.oniceconnectionstatechange = () => {
|
|
798
|
+
const state = pc.iceConnectionState;
|
|
799
|
+
if (state === 'connected') {
|
|
800
|
+
log(`P2P connected: ${peerId.slice(0, 8)}...`, 'success');
|
|
801
|
+
} else if (state === 'disconnected' || state === 'failed') {
|
|
802
|
+
closePeerConnection(peerId);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Setup data channel event handlers
|
|
808
|
+
function setupDataChannel(channel, peerId) {
|
|
809
|
+
dataChannels.set(peerId, channel);
|
|
810
|
+
|
|
811
|
+
channel.onopen = () => {
|
|
812
|
+
log(`Data channel open: ${peerId.slice(0, 8)}...`, 'success');
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
channel.onmessage = (event) => {
|
|
816
|
+
try {
|
|
817
|
+
const message = JSON.parse(event.data);
|
|
818
|
+
handleP2PMessage(peerId, message);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
console.error('P2P message error:', err);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
channel.onclose = () => {
|
|
825
|
+
dataChannels.delete(peerId);
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Handle P2P messages over data channel
|
|
830
|
+
function handleP2PMessage(peerId, message) {
|
|
831
|
+
if (message.type === 'task') {
|
|
832
|
+
log(`Task from ${peerId.slice(0, 8)}: ${message.taskId}`);
|
|
833
|
+
} else if (message.type === 'result') {
|
|
834
|
+
log(`Result from ${peerId.slice(0, 8)}: ${message.taskId}`, 'success');
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Send message to peer via data channel
|
|
839
|
+
function sendToPeer(peerId, message) {
|
|
840
|
+
const channel = dataChannels.get(peerId);
|
|
841
|
+
if (channel && channel.readyState === 'open') {
|
|
842
|
+
channel.send(JSON.stringify(message));
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Broadcast to all connected peers
|
|
849
|
+
function broadcastToPeers(message) {
|
|
850
|
+
let sent = 0;
|
|
851
|
+
for (const [peerId, channel] of dataChannels) {
|
|
852
|
+
if (channel.readyState === 'open') {
|
|
853
|
+
channel.send(JSON.stringify(message));
|
|
854
|
+
sent++;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return sent;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Close peer connection
|
|
861
|
+
function closePeerConnection(peerId) {
|
|
862
|
+
const channel = dataChannels.get(peerId);
|
|
863
|
+
if (channel) {
|
|
864
|
+
channel.close();
|
|
865
|
+
dataChannels.delete(peerId);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const pc = peerConnections.get(peerId);
|
|
869
|
+
if (pc) {
|
|
870
|
+
pc.close();
|
|
871
|
+
peerConnections.delete(peerId);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Fallback simulation mode
|
|
876
|
+
function simulatePeers() {
|
|
877
|
+
log('Using simulation mode (offline)', 'warning');
|
|
878
|
+
peerCount = 3;
|
|
879
|
+
document.getElementById('stat-peers').textContent = peerCount;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Utility functions
|
|
883
|
+
function log(message, type = '') {
|
|
884
|
+
const logDiv = document.getElementById('contribution-log');
|
|
885
|
+
const entry = document.createElement('div');
|
|
886
|
+
entry.className = 'log-entry ' + type;
|
|
887
|
+
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
888
|
+
logDiv.insertBefore(entry, logDiv.firstChild);
|
|
889
|
+
|
|
890
|
+
// Keep only last 50 entries
|
|
891
|
+
while (logDiv.children.length > 50) {
|
|
892
|
+
logDiv.removeChild(logDiv.lastChild);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function arrayToHex(arr) {
|
|
897
|
+
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function dotProduct(a, b) {
|
|
901
|
+
let sum = 0;
|
|
902
|
+
for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
|
|
903
|
+
return sum;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function sleep(ms) {
|
|
907
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Simple encryption (in production, use Web Crypto API with Argon2)
|
|
911
|
+
async function encryptData(data, password) {
|
|
912
|
+
const encoder = new TextEncoder();
|
|
913
|
+
const dataBytes = typeof data === 'string' ? encoder.encode(data) : data;
|
|
914
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
915
|
+
'raw',
|
|
916
|
+
encoder.encode(password),
|
|
917
|
+
'PBKDF2',
|
|
918
|
+
false,
|
|
919
|
+
['deriveBits', 'deriveKey']
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
923
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
924
|
+
|
|
925
|
+
const key = await crypto.subtle.deriveKey(
|
|
926
|
+
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
|
|
927
|
+
keyMaterial,
|
|
928
|
+
{ name: 'AES-GCM', length: 256 },
|
|
929
|
+
false,
|
|
930
|
+
['encrypt']
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
934
|
+
{ name: 'AES-GCM', iv },
|
|
935
|
+
key,
|
|
936
|
+
dataBytes
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
// Combine salt + iv + encrypted
|
|
940
|
+
const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
|
|
941
|
+
result.set(salt, 0);
|
|
942
|
+
result.set(iv, salt.length);
|
|
943
|
+
result.set(new Uint8Array(encrypted), salt.length + iv.length);
|
|
944
|
+
|
|
945
|
+
return btoa(String.fromCharCode(...result));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function decryptData(encryptedBase64, password) {
|
|
949
|
+
const encoder = new TextEncoder();
|
|
950
|
+
const encrypted = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
|
|
951
|
+
|
|
952
|
+
const salt = encrypted.slice(0, 16);
|
|
953
|
+
const iv = encrypted.slice(16, 28);
|
|
954
|
+
const data = encrypted.slice(28);
|
|
955
|
+
|
|
956
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
957
|
+
'raw',
|
|
958
|
+
encoder.encode(password),
|
|
959
|
+
'PBKDF2',
|
|
960
|
+
false,
|
|
961
|
+
['deriveBits', 'deriveKey']
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
const key = await crypto.subtle.deriveKey(
|
|
965
|
+
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
|
|
966
|
+
keyMaterial,
|
|
967
|
+
{ name: 'AES-GCM', length: 256 },
|
|
968
|
+
false,
|
|
969
|
+
['decrypt']
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
973
|
+
{ name: 'AES-GCM', iv },
|
|
974
|
+
key,
|
|
975
|
+
data
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
return new TextDecoder().decode(decrypted);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Initialize on load
|
|
982
|
+
initWasm();
|
|
983
|
+
</script>
|
|
984
|
+
</body>
|
|
985
|
+
</html>
|