/** * Native libp2p bridge component for React Native */ import { NativeEventEmitter, NativeModules } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { LoggerComponent } from 'ior:gitea:gitea.metatrom.net:universal-components/logger@1.0.0'; import { LIBP2P_CONFIG } from '../utils/constants'; import type { ConnectionStatusEvent, PeerDiscoveredEvent, PeerInfoEvent } from '../utils/types'; import type { Connection, ILibp2pComponent, Libp2pEvents, Libp2pOptions, Multiaddr, PeerId, PeerInfo, } from '../interfaces/ILibp2pComponent'; // Create logger instance for this module const logger = new LoggerComponent('Libp2pComponent'); // Helper class to create PeerId-like objects from strings export class SimplePeerId implements PeerId { constructor(private id: string) {} toString(): string { return this.id; } toBytes(): Uint8Array { return new TextEncoder().encode(this.id); } equals(other: PeerId): boolean { return this.toString() === other.toString(); } } // Helper class to create Multiaddr-like objects from strings export class SimpleMultiaddr implements Multiaddr { bytes: Uint8Array; constructor(private addr: string) { this.bytes = new TextEncoder().encode(addr); } toString(): string { return this.addr; } protos(): Array<{ code: number; name: string }> { // Simple parsing of multiaddr components const parts = this.addr.split('/').filter((p) => p); const protos = []; for (let i = 0; i < parts.length; i += 2) { const name = parts[i]; protos.push({ code: 0, name }); // Simplified - real implementation would have proper codes } return protos; } getPeerId(): string | null { const match = this.addr.match(/\/p2p\/([^/]+)/); return match ? match[1] : null; } } // Type for event handler functions type EventHandler = (evt: T) => void; // Native implementation that wraps our iOS/Android modules export class Libp2pComponent implements ILibp2pComponent { private nativeModule: any; private eventEmitter: NativeEventEmitter; private eventHandlers: Map> = new Map(); private _peerId?: PeerId; private _multiaddrs: Multiaddr[] = []; private _started: boolean = false; private _starting: boolean = false; private _error: string | null = null; private cachedConnections: Connection[] = []; private options: Libp2pOptions; private nodeStartedListener: any = null; private nodeStoppedListener: any = null; private errorListener: any = null; constructor(options?: Libp2pOptions, nativeModules?: typeof NativeModules) { // Allow dependency injection of NativeModules for testing const modules = nativeModules || NativeModules; this.nativeModule = modules.Libp2pModule; this.eventEmitter = new NativeEventEmitter(this.nativeModule); this.options = options || {}; if (!this.nativeModule) { throw new Error('Libp2p native module not found'); } this.setupNativeEventListeners(); this.setupProtocolHandlers(); this.setupStateListeners(); } get peerId(): PeerId | null { return this._peerId || null; } get multiaddrs(): Multiaddr[] { return this._multiaddrs; } get isStarting(): boolean { return this._starting; } get error(): string | null { return this._error; } private setupNativeEventListeners(): void { // Map native events to js-libp2p style events // Peer info update this.eventEmitter.addListener('onPeerInfo', ({ peerId, multiaddrs }: PeerInfoEvent) => { this._peerId = new SimplePeerId(peerId); this._multiaddrs = multiaddrs.map((addr: string) => new SimpleMultiaddr(addr)); this.emit('self:peer:update', { peerId: this._peerId, multiaddrs: this._multiaddrs, }); }); // Peer discovery this.eventEmitter.addListener( 'onPeerDiscovered', ({ peerId, addresses, multiaddrs, metadata }: PeerDiscoveredEvent) => { const addrs = (multiaddrs || addresses || []).map( (addr: string) => new SimpleMultiaddr(addr), ); this.emit('peer:discovery', { id: new SimplePeerId(peerId), multiaddrs: addrs, metadata: metadata, // Pass metadata through }); }, ); // Peer lost (for mDNS service lost events) this.eventEmitter.addListener('onPeerLost', ({ peerId }: { peerId: string }) => { this.emit('peer:lost', { id: new SimplePeerId(peerId), }); }); // Multiaddresses changed event this.eventEmitter.addListener('onMultiaddressesChanged', ({ multiaddrs }: { multiaddrs: string[] }) => { logger.debug('[Libp2pComponent] Multiaddresses changed:', multiaddrs); this._multiaddrs = multiaddrs.map((addr: string) => new SimpleMultiaddr(addr)); this.emit('multiaddresses:changed', { multiaddrs: this._multiaddrs, }); }); // Connection events this.eventEmitter.addListener( 'onConnectionStatus', ({ peerId, status, direction, multiaddr }: ConnectionStatusEvent) => { const connection: Connection = { id: `${peerId}-${Date.now()}`, remotePeer: new SimplePeerId(peerId), remoteAddr: new SimpleMultiaddr(multiaddr || ''), stat: { direction: direction || 'outbound', status: status === LIBP2P_CONFIG.CONNECTION_STATUS.CONNECTED ? 'open' : status === LIBP2P_CONFIG.CONNECTION_STATUS.PENDING ? 'pending' : status === LIBP2P_CONFIG.CONNECTION_STATUS.DISCONNECTED ? 'closed' : 'closing', timeline: { open: Date.now(), }, }, }; if ( status === LIBP2P_CONFIG.CONNECTION_STATUS.CONNECTED || status === LIBP2P_CONFIG.CONNECTION_STATUS.PENDING ) { // Emit peer:connect for both pending and connected states // The UI will differentiate based on connection.stat.status this.emit('peer:connect', connection); this.emit('connection:open', connection); } else if ( status === LIBP2P_CONFIG.CONNECTION_STATUS.DISCONNECTED || status === LIBP2P_CONFIG.CONNECTION_STATUS.FAILED ) { connection.stat.timeline.close = Date.now(); connection.stat.status = 'closed'; this.emit('peer:disconnect', connection); this.emit('connection:close', connection); } }, ); } private setupProtocolHandlers(): void { // Listen for protocol data events this.eventEmitter.addListener( 'onProtocolData', (data: { protocolId: string; peerId: string; data?: number[] }) => { // Find matching protocol handler const handler = this.options.protocols?.find((p) => p.protocolId === data.protocolId); if (handler) { // Convert number array to Uint8Array if needed const uint8Data = data.data ? new Uint8Array(data.data) : undefined; handler.handler({ peerId: data.peerId, data: uint8Data }); } }, ); } private setupStateListeners(): void { // Listen for node started event this.nodeStartedListener = this.eventEmitter.addListener('onNodeStarted', () => { this._started = true; this._starting = false; this._error = null; }); // Listen for node stopped event this.nodeStoppedListener = this.eventEmitter.addListener('onNodeStopped', () => { this._started = false; this._starting = false; }); // Listen for error events this.errorListener = this.eventEmitter.addListener('onError', (event: any) => { this._error = event.error || 'Unknown error'; this._starting = false; }); } private emit(event: K, detail: unknown): void { const handlers = this.eventHandlers.get(event); if (handlers) { const customEvent = new CustomEvent(event, { detail }); for (const handler of handlers) { handler(customEvent as Libp2pEvents[K]); } } } async start(): Promise { logger.info('[TypeScript] start() called, _started:', this._started, '_starting:', this._starting); if (this._started || this._starting) { logger.info('[TypeScript] Already started or starting, returning early'); return; // Already started or starting } this._starting = true; this._error = null; logger.info('[TypeScript] Setting _starting to true, proceeding with start'); try { // Register protocols if any if (this.options.protocols && this.nativeModule.registerProtocolHandler) { for (const protocol of this.options.protocols) { await this.nativeModule.registerProtocolHandler(protocol.protocolId); } } // Load stored private key if available and not provided let privateKeyBytes = this.options.keypair?.privateKey; if (!privateKeyBytes) { try { const storedKey = await AsyncStorage.getItem('libp2p_private_key'); if (storedKey) { privateKeyBytes = new Uint8Array(JSON.parse(storedKey)); } } catch (err) { // Failed to load stored key, will generate new one } } // Pass configuration options to native module including keypair const config: any = { tcpPort: this.options.config?.tcpPort, wsPort: this.options.config?.wsPort, }; if (privateKeyBytes) { // Convert Uint8Array to base64 for passing to native module config.privateKey = Buffer.from(privateKeyBytes).toString('base64'); } logger.debug('[Libp2pComponent] About to call nativeModule.startLibp2p with config:', config); logger.debug('[Libp2pComponent] nativeModule exists:', !!this.nativeModule); logger.debug('[Libp2pComponent] nativeModule.startLibp2p exists:', !!this.nativeModule?.startLibp2p); let result; try { result = await this.nativeModule.startLibp2p(config); logger.info('[Libp2pComponent] startLibp2p returned:', result); } catch (error) { logger.error('[Libp2pComponent] startLibp2p failed:', error); throw error; } // Update internal state from result if (result.peerId) { this._peerId = new SimplePeerId(result.peerId); } if (result.multiaddrs) { this._multiaddrs = result.multiaddrs.map((addr: string) => new SimpleMultiaddr(addr)); } // Store the private key if we generated a new one if (result.privateKey && !privateKeyBytes) { try { const keyBytes = Buffer.from(result.privateKey, 'base64'); await AsyncStorage.setItem('libp2p_private_key', JSON.stringify(Array.from(keyBytes))); } catch (err) { // Failed to store key, but continue } } this._started = true; this._starting = false; } catch (error) { this._starting = false; this._error = error instanceof Error ? error.message : 'Failed to start'; throw error; } } async stop(): Promise { try { await this.nativeModule.stopLibp2p(); this._started = false; this._peerId = undefined; this._multiaddrs = []; } catch (error) { // If libp2p wasn't running, that's okay - just update our state const errorMessage = error instanceof Error ? error.message : String(error); const errorCode = (error as { code?: string })?.code; if (errorMessage.includes('not running') || errorCode === 'LIBP2P_ERROR') { this._started = false; } else { // Re-throw other errors throw error; } } } cleanup(): void { // Remove state listeners if (this.nodeStartedListener) { this.nodeStartedListener.remove(); this.nodeStartedListener = null; } if (this.nodeStoppedListener) { this.nodeStoppedListener.remove(); this.nodeStoppedListener = null; } if (this.errorListener) { this.errorListener.remove(); this.errorListener = null; } // Clear event handlers this.eventHandlers.clear(); } async dial(multiaddr: string): Promise { console.log(`[Libp2pComponent] dial() called with multiaddr: ${multiaddr}`); try { const result = await this.nativeModule.connectToPeer(multiaddr); console.log('[Libp2pComponent] connectToPeer result:', result); // Create connection object // Try to extract peer ID from result or multiaddr let peerId = result?.peer_id || result?.peerId || ''; if (!peerId) { // Try to extract from multiaddr if not in result peerId = multiaddr.match(/\/p2p\/([^/]+)/)?.[1] || ''; } // If still no peer ID, this might be a connection without known peer ID // The real peer ID will be determined during handshake if (!peerId || peerId === 'pending') { console.log('[Libp2pComponent] Dialing without known peer ID, will be determined during handshake'); peerId = 'pending'; } const connection = { id: `${peerId}-${Date.now()}`, remotePeer: new SimplePeerId(peerId), remoteAddr: new SimpleMultiaddr(multiaddr), stat: { direction: 'outbound' as const, status: 'open' as const, timeline: { open: Date.now(), }, }, }; console.log('[Libp2pComponent] Returning connection:', connection); return connection; } catch (error) { console.error('[Libp2pComponent] dial error:', error); throw error; } } async hangUp(peerId: string): Promise { const result = await this.nativeModule.hangUp(peerId); if (!result?.success) { // No active connections to peer - this is okay } } async getConnections(peerId?: string): Promise { try { const rawConnections = await this.nativeModule.getConnections(); // Transform native connections to match the Connection interface this.cachedConnections = (rawConnections || []).map((conn: any) => { // Ensure the connection has the expected structure // Handle different formats from native modules let peerIdStr = conn.peerId || 'unknown'; if (conn.remotePeer) { // If remotePeer is an object with toString property (from native module) if (typeof conn.remotePeer.toString === 'string') { peerIdStr = conn.remotePeer.toString; } // If remotePeer is a PeerId object with toString method else if (typeof conn.remotePeer.toString === 'function') { peerIdStr = conn.remotePeer.toString(); } } const connection: Connection = { id: conn.id || `${peerIdStr}-${Date.now()}`, remotePeer: new SimplePeerId(peerIdStr), remoteAddr: conn.remoteAddr?.toString ? new SimpleMultiaddr( typeof conn.remoteAddr.toString === 'string' ? conn.remoteAddr.toString : conn.remoteAddr.toString(), ) : new SimpleMultiaddr('/unknown'), stat: conn.stat || { direction: conn.direction || 'outbound', status: conn.status || 'open', timeline: conn.timeline || { open: Date.now(), }, }, }; return connection; }); // Filter by peerId if provided if (peerId) { return this.cachedConnections.filter((conn) => conn.remotePeer.toString() === peerId); } return this.cachedConnections; } catch (_error) { // Failed to get connections return []; } } addEventListener( event: K, handler: (evt: Libp2pEvents[K]) => void, ): void { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, new Set()); } this.eventHandlers.get(event)?.add(handler as EventHandler); } removeEventListener( event: K, handler: (evt: Libp2pEvents[K]) => void, ): void { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.delete(handler as EventHandler); } } async refreshDiscovery(): Promise { if (this.nativeModule.refreshDiscovery) { await this.nativeModule.refreshDiscovery(); } } async pingPeer(peerId: string): Promise<{ success: boolean; rtt?: number; peerId: string }> { if (this.nativeModule.pingPeer) { return await this.nativeModule.pingPeer(peerId); } else { throw new Error('Ping not supported on this platform'); } } async sendProtocolData(peerId: string, protocolId: string, data: Uint8Array): Promise { if (this.nativeModule.sendProtocolData) { // Convert Uint8Array to regular array for native module const dataArray = Array.from(data); await this.nativeModule.sendProtocolData(peerId, protocolId, dataArray); } else { throw new Error('Protocol sending not supported on this platform'); } } async acceptConnection(peerId: string): Promise { if (!this.nativeModule.acceptConnection) { throw new Error('acceptConnection not supported by native module'); } await this.nativeModule.acceptConnection(peerId); } async rejectConnection(peerId: string): Promise { if (!this.nativeModule.rejectConnection) { throw new Error('rejectConnection not supported by native module'); } await this.nativeModule.rejectConnection(peerId); } } // Type definition for CustomEventInit interface CustomEventInit { detail?: T; bubbles?: boolean; cancelable?: boolean; } // Polyfill CustomEvent for React Native if (typeof CustomEvent === 'undefined') { const globalObj = global as typeof globalThis & { CustomEvent: typeof CustomEvent }; globalObj.CustomEvent = class CustomEvent { readonly type: string; readonly detail: T; readonly bubbles: boolean; readonly cancelable: boolean; constructor(type: string, eventInitDict?: CustomEventInit) { this.type = type; this.detail = eventInitDict?.detail as T; this.bubbles = eventInitDict?.bubbles || false; this.cancelable = eventInitDict?.cancelable || false; } preventDefault() {} stopPropagation() {} stopImmediatePropagation() {} } as any; }