diff --git a/packages/interfaces/src/index.ts b/packages/interfaces/src/index.ts index ac79a2ed34..f7739613b6 100644 --- a/packages/interfaces/src/index.ts +++ b/packages/interfaces/src/index.ts @@ -18,3 +18,4 @@ export * from "./constants.js"; export * from "./sharding.js"; export * from "./health_status.js"; export * from "./discovery.js"; +export * from "./webrtc.js"; diff --git a/packages/interfaces/src/waku.ts b/packages/interfaces/src/waku.ts index 71914755f7..00f79ef656 100644 --- a/packages/interfaces/src/waku.ts +++ b/packages/interfaces/src/waku.ts @@ -15,6 +15,7 @@ import type { Protocols } from "./protocols.js"; import type { IRelay } from "./relay.js"; import type { ShardId } from "./sharding.js"; import type { IStore } from "./store.js"; +import type { IWebRTC } from "./webrtc.js"; export type CreateDecoderParams = { contentTopic: string; @@ -62,6 +63,7 @@ export interface IWaku { store?: IStore; filter?: IFilter; lightPush?: ILightPush; + webRTC?: IWebRTC; /** * Emits events related to the Waku node. @@ -272,6 +274,7 @@ export interface LightNode extends IWaku { store: IStore; filter: IFilter; lightPush: ILightPush; + webRTC: IWebRTC; } export interface RelayNode extends IWaku { diff --git a/packages/interfaces/src/webrtc.ts b/packages/interfaces/src/webrtc.ts new file mode 100644 index 0000000000..61de12764d --- /dev/null +++ b/packages/interfaces/src/webrtc.ts @@ -0,0 +1,93 @@ +import type { PeerId, TypedEventEmitter } from "@libp2p/interface"; + +export enum WebRTCEvent { + InboundRequest = "webRTC:inbound-request", + Connected = "webRTC:connected", + Closed = "webRTC:closed", + Rejected = "webRTC:rejected" +} + +export interface IWebRTCEvents { + /** + * Used to listen to incoming WebRTC connection request. + * + * @example + * ```typescript + * waku.addEventListener(WebRTCEvent.Inbound, (event) => { + * const requesterPeerId = event.detail; + * + * if (requesterPeerId.equals(expectedPeerId)) { + * waku.webRTC.accept(requesterPeerId); + * } else { + * waku.webRTC.hangUp(requesterPeerId); + * } + * }); + */ + [WebRTCEvent.InboundRequest]: CustomEvent; + + /** + * Used to listen to get notified when a WebRTC connection is established. + * + * @example + * ```typescript + * waku.addEventListener(WebRTCEvent.Connected, (event) => { + * const connection = event.detail; // RTCPeerConnection + * }); + * ``` + */ + [WebRTCEvent.Connected]: CustomEvent; +} + +export type PeerIdOrString = PeerId | string; + +export type WebRTCDialOptions = { + peerId: PeerIdOrString; + timeoutMs?: number; +}; + +export interface IWebRTC { + /** + * Used to listen to incoming WebRTC connection request or progress of established connections. + */ + events: TypedEventEmitter; + + /** + * Starts the listening to incoming WebRTC connection requests. + */ + start(): Promise; + + /** + * Stops the listening to incoming WebRTC connection requests. + */ + stop(): Promise; + + /** + * Dials a peer using Waku WebRTC protocol. + */ + dial(options: WebRTCDialOptions): Promise; + + /** + * Accepts a WebRTC connection request from a peer. + */ + accept(peerId: PeerIdOrString): void; + + /** + * Hang up a WebRTC connection to a peer or incoming connection request. + */ + hangUp(peerId: PeerIdOrString): void; + + /** + * Checks if a WebRTC connection is established to a peer. + */ + isConnected(peerId: PeerIdOrString): boolean; + + /** + * Gets the list of connected peers using Waku WebRTC protocol. + */ + getConnectedPeers(): PeerId[]; + + /** + * Gets map of WebRTC connections by peer ID. + */ + getConnections(): Record; +} diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index 7336b06df7..8fdcd49eb8 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -20,6 +20,7 @@ import type { IStore, IWaku, IWakuEventEmitter, + IWebRTC, Libp2p, NetworkConfig } from "@waku/interfaces"; @@ -35,6 +36,7 @@ import { HealthIndicator } from "../health_indicator/index.js"; import { LightPush } from "../light_push/index.js"; import { PeerManager } from "../peer_manager/index.js"; import { Store } from "../store/index.js"; +import { WebRTC } from "../webrtc/index.js"; import { waitForRemotePeer } from "./wait_for_remote_peer.js"; @@ -52,6 +54,7 @@ export class WakuNode implements IWaku { public store?: IStore; public filter?: IFilter; public lightPush?: ILightPush; + public webRTC?: IWebRTC; public readonly events: IWakuEventEmitter = new TypedEventEmitter(); @@ -126,6 +129,21 @@ export class WakuNode implements IWaku { }); } + if (this.lightPush && this.filter) { + const webRtcContentTopic = WebRTC.buildContentTopic(this.libp2p.peerId); + + this.webRTC = new WebRTC({ + lightPush: this.lightPush, + filter: this.filter, + encoder: this.createEncoder({ + contentTopic: webRtcContentTopic + }), + decoder: this.createDecoder({ + contentTopic: webRtcContentTopic + }) + }); + } + log.info( "Waku node created", peerId, diff --git a/packages/sdk/src/webrtc/index.ts b/packages/sdk/src/webrtc/index.ts new file mode 100644 index 0000000000..6eca21c36f --- /dev/null +++ b/packages/sdk/src/webrtc/index.ts @@ -0,0 +1 @@ +export { WebRTC } from "./webrtc.js"; diff --git a/packages/sdk/src/webrtc/webrtc.ts b/packages/sdk/src/webrtc/webrtc.ts new file mode 100644 index 0000000000..71057f9f07 --- /dev/null +++ b/packages/sdk/src/webrtc/webrtc.ts @@ -0,0 +1,121 @@ +import { PeerId, TypedEventEmitter } from "@libp2p/interface"; +import type { + IDecodedMessage, + IDecoder, + IEncoder, + IFilter, + ILightPush, + IWebRTC, + IWebRTCEvents, + PeerIdOrString, + WebRTCDialOptions +} from "@waku/interfaces"; + +type WebRTCConstructorOptions = { + lightPush: ILightPush; + filter: IFilter; + decoder: IDecoder; + encoder: IEncoder; +}; + +export class WebRTC implements IWebRTC { + private readonly lightPush: ILightPush; + private readonly filter: IFilter; + + private readonly decoder: IDecoder; + private readonly encoder: IEncoder; + + public readonly events: TypedEventEmitter = + new TypedEventEmitter(); + + private isStarted = false; + + public static buildContentTopic(peerId: PeerId): string { + return `/js-waku-webrtc/1/${peerId.toString()}/proto`; + } + + public constructor(options: WebRTCConstructorOptions) { + this.lightPush = options.lightPush; + this.filter = options.filter; + + this.decoder = options.decoder; + this.encoder = options.encoder; + + this.handleInboundRequest = this.handleInboundRequest.bind(this); + } + + public async start(): Promise { + if (this.isStarted) { + return; + } + + this.isStarted = true; + await this.subscribeToInboundRequests(); + } + + public async stop(): Promise { + if (!this.isStarted) { + return; + } + + this.isStarted = false; + await this.unsubscribeFromInboundRequests(); + } + + public async dial(options: WebRTCDialOptions): Promise { + // TODO: implement + } + + public accept(peerId: PeerIdOrString): void { + // TODO: implement + } + + public hangUp(peerId: PeerIdOrString): void { + // TODO: implement + } + + public isConnected(peerId: PeerIdOrString): boolean { + // TODO: implement + return false; + } + + public getConnectedPeers(): PeerId[] { + // TODO: implement + return []; + } + + public getConnections(): Record { + // TODO: implement + return {}; + } + + private async subscribeToInboundRequests(): Promise { + await this.filter.subscribe(this.decoder, this.handleInboundRequest); + } + + private async unsubscribeFromInboundRequests(): Promise { + await this.filter.unsubscribe(this.decoder); + } + + private handleInboundRequest(message: IDecodedMessage): void { + /* + const decryptedMessage = decrypt(message.payload, this.privateKey); + switch (decryptedMessage.type) { + case "dial": + break; + case "ack": + break; + case "answer": + break; + case "reject": + break; + case "candidate": + break; + case "close": + break; + default: + break; + } + */ + } +}