|
|
|
@@ -3,6 +3,8 @@
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { NativeEventEmitter, NativeModules } from '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 { LIBP2P_CONFIG } from '../utils/constants';
|
|
|
|
import type { ConnectionStatusEvent, PeerDiscoveredEvent, PeerInfoEvent } from '../utils/types';
|
|
|
|
import type { ConnectionStatusEvent, PeerDiscoveredEvent, PeerInfoEvent } from '../utils/types';
|
|
|
|
import type {
|
|
|
|
import type {
|
|
|
|
@@ -15,6 +17,9 @@ import type {
|
|
|
|
PeerInfo,
|
|
|
|
PeerInfo,
|
|
|
|
} from '../interfaces/ILibp2pComponent';
|
|
|
|
} from '../interfaces/ILibp2pComponent';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create logger instance for this module
|
|
|
|
|
|
|
|
const logger = new LoggerComponent('Libp2pComponent');
|
|
|
|
|
|
|
|
|
|
|
|
// Helper class to create PeerId-like objects from strings
|
|
|
|
// Helper class to create PeerId-like objects from strings
|
|
|
|
export class SimplePeerId implements PeerId {
|
|
|
|
export class SimplePeerId implements PeerId {
|
|
|
|
constructor(private id: string) {}
|
|
|
|
constructor(private id: string) {}
|
|
|
|
@@ -74,8 +79,13 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
private _peerId?: PeerId;
|
|
|
|
private _peerId?: PeerId;
|
|
|
|
private _multiaddrs: Multiaddr[] = [];
|
|
|
|
private _multiaddrs: Multiaddr[] = [];
|
|
|
|
private _started: boolean = false;
|
|
|
|
private _started: boolean = false;
|
|
|
|
|
|
|
|
private _starting: boolean = false;
|
|
|
|
|
|
|
|
private _error: string | null = null;
|
|
|
|
private cachedConnections: Connection[] = [];
|
|
|
|
private cachedConnections: Connection[] = [];
|
|
|
|
private options: Libp2pOptions;
|
|
|
|
private options: Libp2pOptions;
|
|
|
|
|
|
|
|
private nodeStartedListener: any = null;
|
|
|
|
|
|
|
|
private nodeStoppedListener: any = null;
|
|
|
|
|
|
|
|
private errorListener: any = null;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(options?: Libp2pOptions, nativeModules?: typeof NativeModules) {
|
|
|
|
constructor(options?: Libp2pOptions, nativeModules?: typeof NativeModules) {
|
|
|
|
// Allow dependency injection of NativeModules for testing
|
|
|
|
// Allow dependency injection of NativeModules for testing
|
|
|
|
@@ -90,6 +100,7 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
|
|
|
|
|
|
|
|
this.setupNativeEventListeners();
|
|
|
|
this.setupNativeEventListeners();
|
|
|
|
this.setupProtocolHandlers();
|
|
|
|
this.setupProtocolHandlers();
|
|
|
|
|
|
|
|
this.setupStateListeners();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get peerId(): PeerId | null {
|
|
|
|
get peerId(): PeerId | null {
|
|
|
|
@@ -100,6 +111,14 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
return this._multiaddrs;
|
|
|
|
return this._multiaddrs;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get isStarting(): boolean {
|
|
|
|
|
|
|
|
return this._starting;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get error(): string | null {
|
|
|
|
|
|
|
|
return this._error;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private setupNativeEventListeners(): void {
|
|
|
|
private setupNativeEventListeners(): void {
|
|
|
|
// Map native events to js-libp2p style events
|
|
|
|
// Map native events to js-libp2p style events
|
|
|
|
|
|
|
|
|
|
|
|
@@ -134,6 +153,15 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
// Connection events
|
|
|
|
this.eventEmitter.addListener(
|
|
|
|
this.eventEmitter.addListener(
|
|
|
|
'onConnectionStatus',
|
|
|
|
'onConnectionStatus',
|
|
|
|
@@ -195,6 +223,27 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<K extends keyof Libp2pEvents>(event: K, detail: unknown): void {
|
|
|
|
private emit<K extends keyof Libp2pEvents>(event: K, detail: unknown): void {
|
|
|
|
const handlers = this.eventHandlers.get(event);
|
|
|
|
const handlers = this.eventHandlers.get(event);
|
|
|
|
if (handlers) {
|
|
|
|
if (handlers) {
|
|
|
|
@@ -206,34 +255,95 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async start(): Promise<void> {
|
|
|
|
async start(): Promise<void> {
|
|
|
|
// Register protocols if any
|
|
|
|
logger.info('[TypeScript] start() called, _started:', this._started, '_starting:', this._starting);
|
|
|
|
if (this.options.protocols && this.nativeModule.registerProtocolHandler) {
|
|
|
|
|
|
|
|
for (const protocol of this.options.protocols) {
|
|
|
|
if (this._started || this._starting) {
|
|
|
|
await this.nativeModule.registerProtocolHandler(protocol.protocolId);
|
|
|
|
logger.info('[TypeScript] Already started or starting, returning early');
|
|
|
|
}
|
|
|
|
return; // Already started or starting
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Pass configuration options to native module including keypair
|
|
|
|
this._starting = true;
|
|
|
|
const config = {
|
|
|
|
this._error = null;
|
|
|
|
tcpPort: this.options.config?.tcpPort,
|
|
|
|
logger.info('[TypeScript] Setting _starting to true, proceeding with start');
|
|
|
|
wsPort: this.options.config?.wsPort,
|
|
|
|
|
|
|
|
// Convert Uint8Array to base64 for passing to native module
|
|
|
|
|
|
|
|
keypair: this.options.keypair
|
|
|
|
|
|
|
|
? {
|
|
|
|
|
|
|
|
privateKey: btoa(String.fromCharCode(...this.options.keypair.privateKey)),
|
|
|
|
|
|
|
|
publicKey: btoa(String.fromCharCode(...this.options.keypair.publicKey)),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await this.nativeModule.startLibp2p(config);
|
|
|
|
try {
|
|
|
|
this._started = true;
|
|
|
|
// 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<void> {
|
|
|
|
async stop(): Promise<void> {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await this.nativeModule.stopLibp2p();
|
|
|
|
await this.nativeModule.stopLibp2p();
|
|
|
|
this._started = false;
|
|
|
|
this._started = false;
|
|
|
|
|
|
|
|
this._peerId = undefined;
|
|
|
|
|
|
|
|
this._multiaddrs = [];
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
// If libp2p wasn't running, that's okay - just update our state
|
|
|
|
// If libp2p wasn't running, that's okay - just update our state
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
|
@@ -247,23 +357,65 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async dial(multiaddr: string): Promise<Connection> {
|
|
|
|
cleanup(): void {
|
|
|
|
await this.nativeModule.connectToPeer(multiaddr);
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create connection object
|
|
|
|
async dial(multiaddr: string): Promise<Connection> {
|
|
|
|
const peerId = multiaddr.match(/\/p2p\/([^/]+)/)?.[1] || '';
|
|
|
|
console.log(`[Libp2pComponent] dial() called with multiaddr: ${multiaddr}`);
|
|
|
|
return {
|
|
|
|
|
|
|
|
id: `${peerId}-${Date.now()}`,
|
|
|
|
try {
|
|
|
|
remotePeer: new SimplePeerId(peerId),
|
|
|
|
const result = await this.nativeModule.connectToPeer(multiaddr);
|
|
|
|
remoteAddr: new SimpleMultiaddr(multiaddr),
|
|
|
|
console.log('[Libp2pComponent] connectToPeer result:', result);
|
|
|
|
stat: {
|
|
|
|
|
|
|
|
direction: 'outbound',
|
|
|
|
// Create connection object
|
|
|
|
status: 'open',
|
|
|
|
// Try to extract peer ID from result or multiaddr
|
|
|
|
timeline: {
|
|
|
|
let peerId = result?.peer_id || result?.peerId || '';
|
|
|
|
open: Date.now(),
|
|
|
|
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<void> {
|
|
|
|
async hangUp(peerId: string): Promise<void> {
|
|
|
|
@@ -370,6 +522,20 @@ export class Libp2pComponent implements ILibp2pComponent {
|
|
|
|
throw new Error('Protocol sending not supported on this platform');
|
|
|
|
throw new Error('Protocol sending not supported on this platform');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async acceptConnection(peerId: string): Promise<void> {
|
|
|
|
|
|
|
|
if (!this.nativeModule.acceptConnection) {
|
|
|
|
|
|
|
|
throw new Error('acceptConnection not supported by native module');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.nativeModule.acceptConnection(peerId);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async rejectConnection(peerId: string): Promise<void> {
|
|
|
|
|
|
|
|
if (!this.nativeModule.rejectConnection) {
|
|
|
|
|
|
|
|
throw new Error('rejectConnection not supported by native module');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.nativeModule.rejectConnection(peerId);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Type definition for CustomEventInit
|
|
|
|
// Type definition for CustomEventInit
|
|
|
|
|