/** * Native libp2p bridge component for React Native */ import { NativeEventEmitter, NativeModules } from 'react-native'; 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'; // 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 cachedConnections: Connection[] = []; private options: Libp2pOptions; 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(); } get peerId(): PeerId | null { return this._peerId || null; } get multiaddrs(): Multiaddr[] { return this._multiaddrs; } 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 }: PeerDiscoveredEvent) => { const addrs = (multiaddrs || addresses || []).map( (addr: string) => new SimpleMultiaddr(addr), ); this.emit('peer:discovery', { id: new SimplePeerId(peerId), multiaddrs: addrs, }); }, ); // Peer lost (for mDNS service lost events) this.eventEmitter.addListener('onPeerLost', ({ peerId }: { peerId: string }) => { this.emit('peer:lost', { id: new SimplePeerId(peerId), }); }); // 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 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 { // Register protocols if any if (this.options.protocols && this.nativeModule.registerProtocolHandler) { for (const protocol of this.options.protocols) { await this.nativeModule.registerProtocolHandler(protocol.protocolId); } } // Pass configuration options to native module including keypair const config = { tcpPort: this.options.config?.tcpPort, wsPort: this.options.config?.wsPort, // Convert Uint8Array to base64 for passing to native module // Using Buffer.from for proper encoding of binary data privateKey: this.options.keypair?.privateKey ? Buffer.from(this.options.keypair.privateKey).toString('base64') : undefined, }; await this.nativeModule.startLibp2p(config); this._started = true; } async stop(): Promise { try { await this.nativeModule.stopLibp2p(); this._started = false; } 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; } } } async dial(multiaddr: string): Promise { await this.nativeModule.connectToPeer(multiaddr); // Create connection object const peerId = multiaddr.match(/\/p2p\/([^/]+)/)?.[1] || ''; return { id: `${peerId}-${Date.now()}`, remotePeer: new SimplePeerId(peerId), remoteAddr: new SimpleMultiaddr(multiaddr), stat: { direction: 'outbound', status: 'open', timeline: { open: Date.now(), }, }, }; } 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'); } } } // 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; }