@gethashd/bytecave-browser 1.0.1

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/src/client.ts ADDED
@@ -0,0 +1,876 @@
1
+ /**
2
+ * ByteCave Browser Client
3
+ *
4
+ * WebRTC P2P client for connecting browsers directly to ByteCave storage nodes.
5
+ */
6
+
7
+ import { createLibp2p, Libp2p } from 'libp2p';
8
+ import { webRTC } from '@libp2p/webrtc';
9
+ import { webSockets } from '@libp2p/websockets';
10
+ import { noise } from '@chainsafe/libp2p-noise';
11
+ import { yamux } from '@chainsafe/libp2p-yamux';
12
+ import { floodsub } from '@libp2p/floodsub';
13
+ import { identify } from '@libp2p/identify';
14
+ import { bootstrap } from '@libp2p/bootstrap';
15
+ import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
16
+ import { multiaddr } from '@multiformats/multiaddr';
17
+ import { peerIdFromString } from '@libp2p/peer-id';
18
+ import { fromString, toString } from 'uint8arrays';
19
+ import { ethers } from 'ethers';
20
+ import { ContractDiscovery } from './discovery.js';
21
+ import { p2pProtocolClient } from './p2p-protocols.js';
22
+ import { CONTENT_REGISTRY_ABI } from './contracts/ContentRegistry.js';
23
+ import type {
24
+ ByteCaveConfig,
25
+ PeerInfo,
26
+ StoreResult,
27
+ RetrieveResult,
28
+ ConnectionState,
29
+ SignalingMessage
30
+ } from './types.js';
31
+
32
+ const ANNOUNCE_TOPIC = 'bytecave-announce';
33
+ const SIGNALING_TOPIC_PREFIX = 'bytecave-signaling-';
34
+
35
+ export class ByteCaveClient {
36
+ private node: Libp2p | null = null;
37
+ private discovery?: ContractDiscovery; // Optional - only if contract address provided
38
+ private config: ByteCaveConfig;
39
+ private knownPeers: Map<string, PeerInfo> = new Map();
40
+ private connectionState: ConnectionState = 'disconnected';
41
+ private eventListeners: Map<string, Set<Function>> = new Map();
42
+
43
+ constructor(config: ByteCaveConfig) {
44
+ this.config = {
45
+ maxPeers: 10,
46
+ connectionTimeout: 30000,
47
+ ...config
48
+ };
49
+ // Only initialize contract discovery if contract address is provided
50
+ if (config.vaultNodeRegistryAddress && config.rpcUrl) {
51
+ this.discovery = new ContractDiscovery(config.vaultNodeRegistryAddress, config.rpcUrl);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Initialize and start the P2P client
57
+ */
58
+ async start(): Promise<void> {
59
+ if (this.node) {
60
+ console.warn('ByteCave client already started');
61
+ return;
62
+ }
63
+
64
+ this.setConnectionState('connecting');
65
+ console.log('[ByteCave] Starting P2P client...');
66
+
67
+ try {
68
+ const bootstrapPeers: string[] = [];
69
+
70
+ // Add direct node addresses if provided (for direct WebRTC connections)
71
+ if (this.config.directNodeAddrs && this.config.directNodeAddrs.length > 0) {
72
+ console.log('[ByteCave] Using direct node addresses:', this.config.directNodeAddrs);
73
+ bootstrapPeers.push(...this.config.directNodeAddrs);
74
+ }
75
+
76
+ // Use relay peers for fallback (circuit relay connections)
77
+ if (this.config.relayPeers && this.config.relayPeers.length > 0) {
78
+ console.log('[ByteCave] Using relay peers as fallback:', this.config.relayPeers);
79
+ bootstrapPeers.push(...this.config.relayPeers);
80
+ }
81
+
82
+ if (bootstrapPeers.length === 0) {
83
+ console.warn('[ByteCave] No peers configured - will rely on contract discovery only');
84
+ }
85
+
86
+ console.log('[ByteCave] Bootstrap peers:', bootstrapPeers);
87
+
88
+ // Create libp2p node with WebRTC transport
89
+ this.node = await createLibp2p({
90
+ transports: [
91
+ webRTC() as any,
92
+ webSockets() as any,
93
+ circuitRelayTransport() as any
94
+ ],
95
+ connectionEncrypters: [noise()],
96
+ streamMuxers: [yamux()],
97
+ connectionGater: {
98
+ denyDialMultiaddr: () => false,
99
+ denyDialPeer: () => false,
100
+ denyInboundConnection: () => false,
101
+ denyOutboundConnection: () => false,
102
+ denyInboundEncryptedConnection: () => false,
103
+ denyOutboundEncryptedConnection: () => false,
104
+ denyInboundUpgradedConnection: () => false,
105
+ denyOutboundUpgradedConnection: () => false,
106
+ filterMultiaddrForPeer: () => true
107
+ },
108
+ services: {
109
+ identify: identify(),
110
+ pubsub: floodsub()
111
+ },
112
+ peerDiscovery: bootstrapPeers.length > 0 ? [
113
+ bootstrap({ list: bootstrapPeers })
114
+ ] : undefined
115
+ });
116
+
117
+ // Set up event listeners
118
+ this.setupEventListeners();
119
+
120
+ // Set up pubsub
121
+ await this.setupPubsub();
122
+
123
+ // Start the node
124
+ await this.node.start();
125
+ console.log('[ByteCave] Node started with peerId:', this.node.peerId.toString());
126
+
127
+ // Dial relay peers to bootstrap P2P discovery
128
+ console.log('[ByteCave] Attempting to dial relay peers:', bootstrapPeers);
129
+
130
+ for (const addr of bootstrapPeers) {
131
+ try {
132
+ console.log('[ByteCave] Dialing relay:', addr);
133
+ const ma = multiaddr(addr);
134
+ const connection = await this.node.dial(ma as any);
135
+ console.log('[ByteCave] ✓ Connected to relay:', addr, 'remotePeer:', connection.remotePeer.toString());
136
+ } catch (err: any) {
137
+ console.error('[ByteCave] ✗ Failed to dial relay:', addr, 'Error:', err.message || err);
138
+ }
139
+ }
140
+
141
+ const connectedPeers = this.node.getPeers();
142
+ console.log('[ByteCave] Connected peers after relay dial:', connectedPeers.length, connectedPeers.map(p => p.toString()));
143
+
144
+ // Initialize P2P protocol client with the libp2p node
145
+ p2pProtocolClient.setNode(this.node);
146
+
147
+ // Fast discovery: Query relay for peer directory
148
+ console.log('[ByteCave] Querying relay for peer directory...');
149
+ for (const relayAddr of bootstrapPeers) {
150
+ try {
151
+ // Extract relay peer ID from multiaddr
152
+ const parts = relayAddr.split('/p2p/');
153
+ if (parts.length < 2) continue;
154
+ const relayPeerId = parts[parts.length - 1];
155
+
156
+ const directory = await p2pProtocolClient.getPeerDirectoryFromRelay(relayPeerId);
157
+ if (directory && directory.peers.length > 0) {
158
+ console.log('[ByteCave] Got', directory.peers.length, 'peers from relay directory');
159
+
160
+ // Dial each peer and fetch health data
161
+ for (const peer of directory.peers) {
162
+ try {
163
+ console.log('[ByteCave] Dialing peer from directory:', peer.peerId.slice(0, 12) + '...');
164
+
165
+ // Try to dial using circuit relay multiaddr
166
+ let connected = false;
167
+ for (const addr of peer.multiaddrs) {
168
+ try {
169
+ console.log('[ByteCave] Trying multiaddr:', addr);
170
+ const ma = multiaddr(addr);
171
+ await this.node.dial(ma as any);
172
+ connected = true;
173
+ console.log('[ByteCave] ✓ Connected via:', addr);
174
+ break;
175
+ } catch (dialErr: any) {
176
+ console.warn('[ByteCave] Failed to dial via', addr.slice(0, 50) + '...', dialErr.message);
177
+ }
178
+ }
179
+
180
+ if (!connected) {
181
+ console.warn('[ByteCave] Could not connect to peer via any multiaddr');
182
+ continue;
183
+ }
184
+
185
+ // Fetch health data
186
+ const health = await p2pProtocolClient.getHealthFromPeer(peer.peerId);
187
+ if (health) {
188
+ this.knownPeers.set(peer.peerId, {
189
+ peerId: peer.peerId,
190
+ publicKey: health.publicKey || '',
191
+ contentTypes: health.contentTypes || 'all',
192
+ connected: true,
193
+ nodeId: health.nodeId
194
+ });
195
+ console.log('[ByteCave] ✓ Discovered peer:', health.nodeId || peer.peerId.slice(0, 12));
196
+ }
197
+ } catch (err: any) {
198
+ console.warn('[ByteCave] Failed to process peer from directory:', peer.peerId.slice(0, 12), err.message);
199
+ }
200
+ }
201
+
202
+ break; // Successfully got directory from one relay
203
+ }
204
+ } catch (err: any) {
205
+ console.warn('[ByteCave] Failed to get directory from relay:', err.message);
206
+ }
207
+ }
208
+
209
+ this.setConnectionState('connected');
210
+ console.log('[ByteCave] Client started', {
211
+ peerId: this.node.peerId.toString(),
212
+ relayPeers: bootstrapPeers.length,
213
+ connectedPeers: connectedPeers.length,
214
+ discoveredPeers: this.knownPeers.size
215
+ });
216
+
217
+ } catch (error) {
218
+ this.setConnectionState('error');
219
+ console.error('Failed to start ByteCave client:', error);
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Stop the P2P client
226
+ */
227
+ async stop(): Promise<void> {
228
+ if (!this.node) return;
229
+
230
+ await this.node.stop();
231
+ this.node = null;
232
+ this.knownPeers.clear();
233
+ this.setConnectionState('disconnected');
234
+ console.log('ByteCave client stopped');
235
+ }
236
+
237
+ /**
238
+ * Refresh peer directory from relay
239
+ * This rediscovers nodes that may have reconnected or restarted
240
+ */
241
+ async refreshPeerDirectory(): Promise<void> {
242
+ if (!this.node) {
243
+ console.warn('[ByteCave] Cannot refresh - node not initialized');
244
+ return;
245
+ }
246
+
247
+ const bootstrapPeers = [
248
+ ...(this.config.directNodeAddrs || []),
249
+ ...(this.config.relayPeers || [])
250
+ ];
251
+
252
+ console.log('[ByteCave] Refreshing peer directory from relays...');
253
+
254
+ for (const relayAddr of bootstrapPeers) {
255
+ try {
256
+ // Extract relay peer ID from multiaddr
257
+ const parts = relayAddr.split('/p2p/');
258
+ if (parts.length < 2) continue;
259
+ const relayPeerId = parts[parts.length - 1];
260
+
261
+ const directory = await p2pProtocolClient.getPeerDirectoryFromRelay(relayPeerId);
262
+ if (directory && directory.peers.length > 0) {
263
+ console.log('[ByteCave] Refresh: Got', directory.peers.length, 'peers from relay directory');
264
+
265
+ // Check each peer and reconnect if disconnected
266
+ for (const peer of directory.peers) {
267
+ const isConnected = this.node.getPeers().some(p => p.toString() === peer.peerId);
268
+ const knownPeer = this.knownPeers.get(peer.peerId);
269
+
270
+ if (!isConnected || !knownPeer) {
271
+ console.log('[ByteCave] Refresh: Reconnecting to peer:', peer.peerId.slice(0, 12) + '...');
272
+
273
+ // Try to dial using circuit relay multiaddr
274
+ let connected = false;
275
+ for (const addr of peer.multiaddrs) {
276
+ try {
277
+ const ma = multiaddr(addr);
278
+ await this.node.dial(ma as any);
279
+ connected = true;
280
+ console.log('[ByteCave] Refresh: ✓ Reconnected via:', addr.slice(0, 60) + '...');
281
+ break;
282
+ } catch (dialErr: any) {
283
+ console.debug('[ByteCave] Refresh: Failed to dial via', addr.slice(0, 50) + '...', dialErr.message);
284
+ }
285
+ }
286
+
287
+ if (connected) {
288
+ // Fetch health data
289
+ try {
290
+ const health = await p2pProtocolClient.getHealthFromPeer(peer.peerId);
291
+ if (health) {
292
+ this.knownPeers.set(peer.peerId, {
293
+ peerId: peer.peerId,
294
+ publicKey: health.publicKey || '',
295
+ contentTypes: health.contentTypes || 'all',
296
+ connected: true,
297
+ nodeId: health.nodeId
298
+ });
299
+ console.log('[ByteCave] Refresh: ✓ Updated peer info:', health.nodeId || peer.peerId.slice(0, 12));
300
+ }
301
+ } catch (err: any) {
302
+ console.warn('[ByteCave] Refresh: Failed to get health from peer:', peer.peerId.slice(0, 12), err.message);
303
+ }
304
+ }
305
+ } else {
306
+ console.debug('[ByteCave] Refresh: Peer already connected:', peer.peerId.slice(0, 12) + '...');
307
+ }
308
+ }
309
+
310
+ break; // Successfully refreshed from one relay
311
+ }
312
+ } catch (err: any) {
313
+ console.warn('[ByteCave] Refresh: Failed to get directory from relay:', err.message);
314
+ }
315
+ }
316
+
317
+ console.log('[ByteCave] Refresh complete. Connected peers:', this.node.getPeers().length, 'Known peers:', this.knownPeers.size);
318
+ }
319
+
320
+ /**
321
+ * Store data on the network
322
+ * Uses getPeers() directly for fast peer access
323
+ *
324
+ * @param data - Data to store
325
+ * @param mimeType - MIME type (optional, defaults to 'application/octet-stream')
326
+ * @param signer - Ethers signer for authorization (optional, but required for most nodes)
327
+ */
328
+ async store(data: Uint8Array | ArrayBuffer, mimeType?: string, signer?: any): Promise<StoreResult> {
329
+ if (!this.node) {
330
+ return { success: false, error: 'P2P node not initialized' };
331
+ }
332
+
333
+ // Get all connected peers (excluding relay)
334
+ const allPeers = this.node.getPeers();
335
+ const relayPeerIds = new Set(
336
+ this.config.relayPeers?.map(addr => addr.split('/p2p/').pop()) || []
337
+ );
338
+
339
+ const connectedPeerIds = allPeers
340
+ .map(p => p.toString())
341
+ .filter(peerId => !relayPeerIds.has(peerId));
342
+
343
+ console.log('[ByteCave] Store - connected storage peers:', connectedPeerIds.length);
344
+ console.log('[ByteCave] Store - knownPeers with registration info:', this.knownPeers.size);
345
+
346
+ if (connectedPeerIds.length === 0) {
347
+ return { success: false, error: 'No storage peers available' };
348
+ }
349
+
350
+ // Prioritize registered peers from knownPeers (populated via floodsub announcements)
351
+ // If no registered peers known yet, use all connected peers
352
+ const registeredPeerIds = Array.from(this.knownPeers.values())
353
+ .filter(p => p.isRegistered && connectedPeerIds.includes(p.peerId))
354
+ .map(p => p.peerId);
355
+
356
+ const storagePeerIds = registeredPeerIds.length > 0
357
+ ? [...registeredPeerIds, ...connectedPeerIds.filter(id => !registeredPeerIds.includes(id))]
358
+ : connectedPeerIds;
359
+
360
+ console.log('[ByteCave] Store - peer order (registered first):',
361
+ storagePeerIds.map(id => id.slice(0, 12)).join(', '),
362
+ '(registered:', registeredPeerIds.length, ')');
363
+
364
+ const dataArray = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
365
+
366
+ // Validate file size (5MB limit)
367
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB in bytes
368
+ if (dataArray.length > MAX_FILE_SIZE) {
369
+ const sizeMB = (dataArray.length / (1024 * 1024)).toFixed(2);
370
+ return {
371
+ success: false,
372
+ error: `File size (${sizeMB}MB) exceeds maximum allowed size of 5MB`
373
+ };
374
+ }
375
+
376
+ // Create authorization if signer is provided
377
+ let authorization: any = undefined;
378
+ if (signer) {
379
+ try {
380
+ // Import ethers dynamically to avoid bundling issues
381
+ const { ethers } = await import('ethers');
382
+
383
+ const sender = await signer.getAddress();
384
+ const contentHash = ethers.keccak256(dataArray);
385
+ const timestamp = Date.now();
386
+ const nonce = Math.random().toString(36).substring(2, 15) +
387
+ Math.random().toString(36).substring(2, 15);
388
+
389
+ const message = `ByteCave Storage Request for:
390
+ Content Hash: ${contentHash}
391
+ App ID: ${this.config.appId}
392
+ Timestamp: ${timestamp}
393
+ Nonce: ${nonce}`;
394
+ const signature = await signer.signMessage(message);
395
+
396
+ authorization = {
397
+ sender,
398
+ signature,
399
+ timestamp,
400
+ nonce,
401
+ contentHash,
402
+ appId: this.config.appId
403
+ };
404
+
405
+ console.log('[ByteCave] Created authorization for storage request');
406
+ } catch (err: any) {
407
+ console.warn('[ByteCave] Failed to create authorization:', err.message);
408
+ }
409
+ }
410
+
411
+ // Try each storage peer until one succeeds
412
+ const errors: string[] = [];
413
+ for (const peerId of storagePeerIds) {
414
+ console.log('[ByteCave] Attempting P2P store to peer:', peerId.slice(0, 12) + '...');
415
+
416
+ try {
417
+ const result = await p2pProtocolClient.storeToPeer(
418
+ peerId,
419
+ dataArray,
420
+ mimeType || 'application/octet-stream',
421
+ authorization,
422
+ false // shouldVerifyOnChain - false for browser test storage
423
+ );
424
+
425
+ if (result.success && result.cid) {
426
+ console.log('[ByteCave] ✓ P2P store successful:', result.cid);
427
+ return {
428
+ success: true,
429
+ cid: result.cid,
430
+ peerId
431
+ };
432
+ }
433
+
434
+ const errorMsg = `${peerId.slice(0, 12)}: ${result.error}`;
435
+ console.warn('[ByteCave] ✗ P2P store failed:', errorMsg);
436
+ errors.push(errorMsg);
437
+ } catch (err: any) {
438
+ const errorMsg = `${peerId.slice(0, 12)}: ${err.message}`;
439
+ console.error('[ByteCave] ✗ P2P store exception:', errorMsg);
440
+ errors.push(errorMsg);
441
+ }
442
+ }
443
+
444
+ console.error('[ByteCave] All storage peers failed. Errors:', errors);
445
+ return { success: false, error: `All storage peers failed: ${errors.join('; ')}` };
446
+ }
447
+
448
+ /**
449
+ * Retrieve ciphertext from a node via P2P only (no HTTP fallback)
450
+ */
451
+ async retrieve(cid: string): Promise<RetrieveResult> {
452
+ if (!this.node) {
453
+ console.error('[ByteCave] Retrieve failed: P2P node not initialized');
454
+ return { success: false, error: 'P2P node not initialized' };
455
+ }
456
+
457
+ const libp2pPeers = this.node.getPeers();
458
+ console.log('[ByteCave] Retrieve - libp2p peers:', libp2pPeers.length);
459
+ console.log('[ByteCave] Retrieve - known peers:', this.knownPeers.size);
460
+ console.log('[ByteCave] Retrieve - node status:', this.node.status);
461
+
462
+ if (libp2pPeers.length === 0) {
463
+ console.warn('[ByteCave] Retrieve failed: No libp2p peers connected, but have', this.knownPeers.size, 'known peers');
464
+ return { success: false, error: 'No connected peers available' };
465
+ }
466
+
467
+ // Find which peers have this CID
468
+ const peersWithCid: string[] = [];
469
+
470
+ for (const peerId of libp2pPeers) {
471
+ const peerIdStr = peerId.toString();
472
+
473
+ try {
474
+ const hasCid = await p2pProtocolClient.peerHasCid(peerIdStr, cid);
475
+
476
+ if (hasCid) {
477
+ peersWithCid.push(peerIdStr);
478
+ }
479
+ } catch (error: any) {
480
+ // Skip peers that don't support the protocol
481
+ }
482
+ }
483
+
484
+ if (peersWithCid.length === 0) {
485
+ return { success: false, error: 'Blob not found on any connected peer' };
486
+ }
487
+
488
+ // Try to retrieve from peers that have the CID
489
+ for (const peerId of peersWithCid) {
490
+ try {
491
+ const timeoutPromise = new Promise<null>((_, reject) =>
492
+ setTimeout(() => reject(new Error('Retrieval timeout after 10s')), 10000)
493
+ );
494
+
495
+ const result = await Promise.race([
496
+ p2pProtocolClient.retrieveFromPeer(peerId, cid),
497
+ timeoutPromise
498
+ ]);
499
+
500
+ if (result) {
501
+ return { success: true, data: result.data, peerId };
502
+ }
503
+ } catch (error: any) {
504
+ // Continue to next peer
505
+ }
506
+ }
507
+
508
+ return { success: false, error: 'Failed to retrieve blob from peers that have it' };
509
+ }
510
+
511
+ /**
512
+ * Register content in ContentRegistry contract (on-chain)
513
+ * This must be called before storing content that requires on-chain verification
514
+ */
515
+ async registerContent(
516
+ cid: string,
517
+ appId: string,
518
+ signer: ethers.Signer
519
+ ): Promise<{ success: boolean; txHash?: string; error?: string }> {
520
+ if (!this.config.contentRegistryAddress) {
521
+ return {
522
+ success: false,
523
+ error: 'ContentRegistry address not configured'
524
+ };
525
+ }
526
+
527
+ if (!this.config.rpcUrl) {
528
+ return {
529
+ success: false,
530
+ error: 'RPC URL not configured'
531
+ };
532
+ }
533
+
534
+ try {
535
+ console.log('[ByteCave] Registering content in ContentRegistry:', { cid, appId });
536
+
537
+ const contract = new ethers.Contract(
538
+ this.config.contentRegistryAddress,
539
+ CONTENT_REGISTRY_ABI,
540
+ signer
541
+ );
542
+
543
+ const tx = await contract.registerContent(cid, appId);
544
+ console.log('[ByteCave] ContentRegistry transaction sent:', tx.hash);
545
+
546
+ const receipt = await tx.wait();
547
+ console.log('[ByteCave] ContentRegistry registration confirmed:', receipt.hash);
548
+
549
+ return {
550
+ success: true,
551
+ txHash: receipt.hash
552
+ };
553
+ } catch (error: any) {
554
+ console.error('[ByteCave] ContentRegistry registration failed:', error);
555
+ return {
556
+ success: false,
557
+ error: error.message || 'Registration failed'
558
+ };
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Check if content is registered in ContentRegistry
564
+ */
565
+ async isContentRegistered(cid: string): Promise<boolean> {
566
+ if (!this.config.contentRegistryAddress || !this.config.rpcUrl) {
567
+ return false;
568
+ }
569
+
570
+ try {
571
+ const provider = new ethers.JsonRpcProvider(this.config.rpcUrl);
572
+ const contract = new ethers.Contract(
573
+ this.config.contentRegistryAddress,
574
+ CONTENT_REGISTRY_ABI,
575
+ provider
576
+ );
577
+
578
+ return await contract.isContentRegistered(cid);
579
+ } catch (error: any) {
580
+ console.error('[ByteCave] Failed to check content registration:', error);
581
+ return false;
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Get list of known peers (includes both announced peers and connected libp2p peers)
587
+ */
588
+ async getPeers(): Promise<PeerInfo[]> {
589
+ // Get the set of currently connected peer IDs
590
+ const connectedPeerIds = new Set<string>();
591
+ if (this.node) {
592
+ const libp2pPeers = this.node.getPeers();
593
+ console.log('[ByteCave] getPeers called - node exists:', !!this.node, 'libp2p peers:', libp2pPeers.length);
594
+ for (const peerId of libp2pPeers) {
595
+ connectedPeerIds.add(peerId.toString());
596
+ }
597
+ } else {
598
+ console.log('[ByteCave] getPeers called - NO NODE!');
599
+ }
600
+
601
+ // Build result from known peers, marking connected status
602
+ const result: PeerInfo[] = [];
603
+
604
+ // Add all known peers with updated connected status
605
+ for (const [peerIdStr, peer] of this.knownPeers) {
606
+ result.push({
607
+ ...peer,
608
+ connected: connectedPeerIds.has(peerIdStr)
609
+ });
610
+ connectedPeerIds.delete(peerIdStr); // Remove so we don't add twice
611
+ }
612
+
613
+ // Add any connected peers not in knownPeers
614
+ for (const peerIdStr of connectedPeerIds) {
615
+ result.push({
616
+ peerId: peerIdStr,
617
+ publicKey: '',
618
+ contentTypes: 'all',
619
+ connected: true
620
+ });
621
+ }
622
+
623
+ console.log('[ByteCave] getPeers - returning:', result.length, 'peers, connected:', result.filter(p => p.connected).length);
624
+ return result;
625
+ }
626
+
627
+ /**
628
+ * Get count of connected peers
629
+ */
630
+ getConnectedPeerCount(): number {
631
+ return this.node ? this.node.getPeers().length : 0;
632
+ }
633
+
634
+ /**
635
+ * Get node info from a peer via P2P stream (for registration)
636
+ */
637
+ async getNodeInfo(peerId: string): Promise<{ publicKey: string; ownerAddress?: string; peerId: string } | null> {
638
+ try {
639
+ const info = await p2pProtocolClient.getInfoFromPeer(peerId);
640
+ return info;
641
+ } catch (error: any) {
642
+ console.warn('[ByteCave] Failed to get node info via P2P:', error.message);
643
+ return null;
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Get health info from a peer via P2P stream
649
+ */
650
+ async getNodeHealth(peerId: string): Promise<{
651
+ status: string;
652
+ blobCount: number;
653
+ storageUsed: number;
654
+ uptime: number;
655
+ } | null> {
656
+ try {
657
+ // Check if we have relay addresses for this peer
658
+ const peerInfo = this.knownPeers.get(peerId);
659
+ const relayAddrs = (peerInfo as any)?.relayAddrs;
660
+
661
+ // If we have relay addresses, dial through relay first
662
+ if (relayAddrs && relayAddrs.length > 0 && this.node) {
663
+ console.log('[ByteCave] Dialing peer through relay:', peerId.slice(0, 12), relayAddrs[0]);
664
+ try {
665
+ const ma = multiaddr(relayAddrs[0]);
666
+ await this.node.dial(ma as any);
667
+ console.log('[ByteCave] Successfully dialed peer through relay');
668
+ } catch (dialError) {
669
+ console.warn('[ByteCave] Failed to dial through relay:', dialError);
670
+ }
671
+ }
672
+
673
+ const health = await p2pProtocolClient.getHealthFromPeer(peerId);
674
+ return health;
675
+ } catch (error: any) {
676
+ console.warn('[ByteCave] Failed to get node health via P2P:', error.message);
677
+ return null;
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Get current connection state
683
+ */
684
+ getConnectionState(): ConnectionState {
685
+ return this.connectionState;
686
+ }
687
+
688
+ /**
689
+ * Subscribe to events
690
+ */
691
+ on(event: string, callback: Function): void {
692
+ if (!this.eventListeners.has(event)) {
693
+ this.eventListeners.set(event, new Set());
694
+ }
695
+ this.eventListeners.get(event)!.add(callback);
696
+ }
697
+
698
+ /**
699
+ * Unsubscribe from events
700
+ */
701
+ off(event: string, callback: Function): void {
702
+ this.eventListeners.get(event)?.delete(callback);
703
+ }
704
+
705
+ private emit(event: string, data?: any): void {
706
+ this.eventListeners.get(event)?.forEach(cb => cb(data));
707
+ }
708
+
709
+ private setConnectionState(state: ConnectionState): void {
710
+ this.connectionState = state;
711
+ this.emit('connectionStateChange', state);
712
+ }
713
+
714
+ private setupEventListeners(): void {
715
+ if (!this.node) return;
716
+
717
+ this.node.addEventListener('peer:connect', (event) => {
718
+ const peerId = event.detail.toString();
719
+ console.log('[ByteCave] Peer connected:', peerId, 'Total now:', this.node?.getPeers().length);
720
+ this.emit('peerConnect', peerId);
721
+ });
722
+
723
+ this.node.addEventListener('peer:disconnect', (event) => {
724
+ const peerId = event.detail.toString();
725
+ console.log('[ByteCave] Peer disconnected:', peerId);
726
+ console.log('[ByteCave] Remaining peers:', this.node?.getPeers().length);
727
+
728
+ const peer = this.knownPeers.get(peerId);
729
+ if (peer) {
730
+ peer.connected = false;
731
+ }
732
+ this.emit('peerDisconnect', peerId);
733
+ });
734
+ }
735
+
736
+ private async setupPubsub(): Promise<void> {
737
+ if (!this.node) return;
738
+
739
+ const pubsub = this.node.services.pubsub as any;
740
+ if (!pubsub) return;
741
+
742
+ // Subscribe to announcement topic
743
+ pubsub.subscribe(ANNOUNCE_TOPIC);
744
+
745
+ // Subscribe to our own signaling topic
746
+ const mySignalingTopic = `${SIGNALING_TOPIC_PREFIX}${this.node.peerId.toString()}`;
747
+ pubsub.subscribe(mySignalingTopic);
748
+
749
+ pubsub.addEventListener('message', (event: any) => {
750
+ const topic = event.detail.topic;
751
+ console.log('[ByteCave] Received floodsub message on topic:', topic);
752
+
753
+ if (topic === ANNOUNCE_TOPIC) {
754
+ try {
755
+ const data = toString(event.detail.data);
756
+ const announcement = JSON.parse(data);
757
+ console.log('[ByteCave] Received peer announcement:', announcement.peerId?.slice(0, 12));
758
+ this.handlePeerAnnouncement(announcement);
759
+ } catch (error) {
760
+ console.warn('Failed to parse announcement:', error);
761
+ }
762
+ }
763
+
764
+ if (topic === mySignalingTopic) {
765
+ try {
766
+ const data = toString(event.detail.data);
767
+ const signal: SignalingMessage = JSON.parse(data);
768
+ this.handleSignalingMessage(signal);
769
+ } catch (error) {
770
+ console.warn('Failed to parse signaling message:', error);
771
+ }
772
+ }
773
+ });
774
+ }
775
+
776
+ private async handlePeerAnnouncement(announcement: {
777
+ peerId: string;
778
+ timestamp?: number;
779
+ relayAddrs?: string[];
780
+ contentTypes?: string[] | 'all';
781
+ }): Promise<void> {
782
+ console.log('[ByteCave] Received announcement from peer:', announcement.peerId.slice(0, 12), announcement);
783
+
784
+ const existing = this.knownPeers.get(announcement.peerId);
785
+
786
+ const peerInfo: PeerInfo = {
787
+ peerId: announcement.peerId,
788
+ publicKey: existing?.publicKey || '',
789
+ contentTypes: announcement.contentTypes || 'all',
790
+ connected: this.node?.getPeers().some(p => p.toString() === announcement.peerId) || false
791
+ };
792
+
793
+ // Preserve relay addresses from existing peer info if new announcement doesn't have them
794
+ if (announcement.relayAddrs && announcement.relayAddrs.length > 0) {
795
+ (peerInfo as any).relayAddrs = announcement.relayAddrs;
796
+ } else if (existing && (existing as any).relayAddrs) {
797
+ (peerInfo as any).relayAddrs = (existing as any).relayAddrs;
798
+ }
799
+
800
+ this.knownPeers.set(announcement.peerId, peerInfo);
801
+
802
+ this.emit('peerAnnounce', peerInfo);
803
+ }
804
+
805
+
806
+ /**
807
+ * Check if a nodeId is registered in the on-chain registry
808
+ */
809
+ private async checkNodeRegistration(nodeId: string): Promise<boolean> {
810
+ if (!this.discovery) {
811
+ // No contract configured - skip registration check
812
+ return true;
813
+ }
814
+
815
+ try {
816
+ const registeredNodes = await this.discovery.getActiveNodes();
817
+ return registeredNodes.some(node => node.nodeId === nodeId);
818
+ } catch (error) {
819
+ console.warn('[ByteCave] Failed to check node registration:', error);
820
+ return false;
821
+ }
822
+ }
823
+
824
+ private handleSignalingMessage(signal: SignalingMessage): void {
825
+ console.log('Received signaling message:', signal.type, 'from:', signal.from);
826
+ this.emit('signaling', signal);
827
+ }
828
+
829
+ /**
830
+ * Send signaling message to a peer for WebRTC negotiation
831
+ */
832
+ async sendSignalingMessage(targetPeerId: string, signal: Omit<SignalingMessage, 'from'>): Promise<void> {
833
+ if (!this.node) return;
834
+
835
+ const pubsub = this.node.services.pubsub as any;
836
+ if (!pubsub) return;
837
+
838
+ const targetTopic = `${SIGNALING_TOPIC_PREFIX}${targetPeerId}`;
839
+ const message: SignalingMessage = {
840
+ ...signal,
841
+ from: this.node.peerId.toString()
842
+ };
843
+
844
+ try {
845
+ await pubsub.publish(targetTopic, fromString(JSON.stringify(message)));
846
+ } catch (error) {
847
+ console.warn('Failed to send signaling message:', error);
848
+ }
849
+ }
850
+
851
+ private findPeerForContentType(contentType: string): PeerInfo | null {
852
+ // First pass: find registered peers that accept this content type
853
+ for (const peer of this.knownPeers.values()) {
854
+ if (!peer.connected) continue;
855
+ if (!peer.isRegistered) continue; // Prefer registered nodes for storage
856
+ if (peer.contentTypes === 'all') return peer;
857
+ if (Array.isArray(peer.contentTypes) && peer.contentTypes.includes(contentType)) {
858
+ return peer;
859
+ }
860
+ }
861
+
862
+ // Second pass: fall back to any connected peer (for local-only storage)
863
+ // This allows personal nodes to work, but data won't replicate
864
+ for (const peer of this.knownPeers.values()) {
865
+ if (!peer.connected) continue;
866
+ if (peer.contentTypes === 'all') return peer;
867
+ if (Array.isArray(peer.contentTypes) && peer.contentTypes.includes(contentType)) {
868
+ console.warn('[ByteCave] No registered peers available, using unregistered peer. Data will NOT replicate.');
869
+ return peer;
870
+ }
871
+ }
872
+ return null;
873
+ }
874
+
875
+
876
+ }