mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 13:53:12 +00:00
* implement proto * implement filter v2 * add tests * minor improvements - make unsubscribe functions private in filter - enable all tests * enable all tests * readd multiaddrinput * address comment removals * unsubscribe based on contentFilters passed * update unsubscribe function parameters in test * reset interfaces & filter v1 * refactor filterv2 into 2 classes - removes generics from types on filter which means manual typecasting to filter version is required on consumer side - defaults to filterv2 - splits filterv2 into 2 classes: - one to create the subscription object with a peer which returns the second class - the other to manage all subscription functions * updates filter tests for the new API - also fixes an interface import * update `toAsyncIterator` test for Filter V1 * implement IReceiver on FilterV2 * remove return values from subscription functions * update `to_async_iterator` * address variable naming * add tsdoc comments for hidden function * address minor comments * update docs to default to filter v2 * address comments * rename `wakuFilter` to `wakuFilterV1` * chore: Remove static variables (#1371) * chore: Remove static variables - Remove internal types from `@core/interfaces` - Remove data being redundantly stored (pubsub topic) - Remove usage of static variables - Clean up callbacks and decoders when using `unsubscribe` - Clean up callbacks and decoders when using `unsubscribeAll` * fix setting activeSubscription --------- Co-authored-by: danisharora099 <danisharora099@gmail.com> * make activeSub getter and setter private * update size-limit --------- Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com>
394 lines
10 KiB
TypeScript
394 lines
10 KiB
TypeScript
import type { PeerId } from "@libp2p/interface-peer-id";
|
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
import { Multiaddr, multiaddr } from "@multiformats/multiaddr";
|
|
import { DefaultPubSubTopic } from "@waku/core";
|
|
import { isDefined } from "@waku/utils";
|
|
import { bytesToHex, hexToBytes } from "@waku/utils/bytes";
|
|
import debug from "debug";
|
|
import portfinder from "portfinder";
|
|
|
|
import { existsAsync, mkdirAsync, openAsync } from "../async_fs.js";
|
|
import { delay } from "../delay.js";
|
|
import waitForLine from "../log_file.js";
|
|
|
|
import Dockerode from "./dockerode.js";
|
|
import {
|
|
Args,
|
|
KeyPair,
|
|
LogLevel,
|
|
MessageRpcQuery,
|
|
MessageRpcResponse,
|
|
} from "./interfaces.js";
|
|
|
|
const log = debug("waku:node");
|
|
|
|
const WAKU_SERVICE_NODE_PARAMS =
|
|
process.env.WAKU_SERVICE_NODE_PARAMS ?? undefined;
|
|
const NODE_READY_LOG_LINE = "Node setup complete";
|
|
|
|
const DOCKER_IMAGE_NAME =
|
|
process.env.WAKUNODE_IMAGE || "statusteam/nim-waku:v0.17.0";
|
|
|
|
const isGoWaku = DOCKER_IMAGE_NAME.includes("go-waku");
|
|
|
|
const LOG_DIR = "./log";
|
|
|
|
const OneMillion = BigInt(1_000_000);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
BigInt.prototype.toJSON = function toJSON() {
|
|
return Number(this);
|
|
};
|
|
|
|
export class NimGoNode {
|
|
private docker?: Dockerode;
|
|
private peerId?: PeerId;
|
|
private multiaddrWithId?: Multiaddr;
|
|
private websocketPort?: number;
|
|
private readonly logPath: string;
|
|
private rpcPort?: number;
|
|
|
|
/**
|
|
* Convert a [[WakuMessage]] to a [[WakuRelayMessage]]. The latter is used
|
|
* by the nwaku JSON-RPC API.
|
|
*/
|
|
static toMessageRpcQuery(message: {
|
|
payload: Uint8Array;
|
|
contentTopic: string;
|
|
timestamp?: Date;
|
|
}): MessageRpcQuery {
|
|
if (!message.payload) {
|
|
throw "Attempting to convert empty message";
|
|
}
|
|
|
|
let timestamp;
|
|
if (message.timestamp) {
|
|
timestamp = BigInt(message.timestamp.valueOf()) * OneMillion;
|
|
}
|
|
|
|
return {
|
|
payload: Buffer.from(message.payload).toString("base64"),
|
|
contentTopic: message.contentTopic,
|
|
timestamp,
|
|
};
|
|
}
|
|
|
|
constructor(logName: string) {
|
|
this.logPath = `${LOG_DIR}/wakunode_${logName}.log`;
|
|
}
|
|
|
|
type(): "go-waku" | "nwaku" {
|
|
return isGoWaku ? "go-waku" : "nwaku";
|
|
}
|
|
|
|
get nodeType(): "go-waku" | "nwaku" {
|
|
return isGoWaku ? "go-waku" : "nwaku";
|
|
}
|
|
|
|
async start(args: Args = {}): Promise<void> {
|
|
this.docker = await Dockerode.createInstance(DOCKER_IMAGE_NAME);
|
|
try {
|
|
await existsAsync(LOG_DIR);
|
|
} catch (e) {
|
|
try {
|
|
await mkdirAsync(LOG_DIR);
|
|
} catch (e) {
|
|
// Looks like 2 tests tried to create the director at the same time,
|
|
// it can be ignored
|
|
}
|
|
}
|
|
|
|
await openAsync(this.logPath, "w");
|
|
|
|
const mergedArgs = defaultArgs();
|
|
|
|
// waku nodes takes some time to bind port so to decrease chances of conflict
|
|
// we also randomize the first port that portfinder will try
|
|
const startPort = Math.floor(Math.random() * (65535 - 1025) + 1025);
|
|
|
|
const ports: number[] = await new Promise((resolve, reject) => {
|
|
portfinder.getPorts(4, { port: startPort }, (err, ports) => {
|
|
if (err) reject(err);
|
|
resolve(ports);
|
|
});
|
|
});
|
|
|
|
if (isGoWaku && !args.logLevel) {
|
|
args.logLevel = LogLevel.Debug;
|
|
}
|
|
|
|
const [rpcPort, tcpPort, websocketPort, discv5UdpPort] = ports;
|
|
this.rpcPort = rpcPort;
|
|
this.websocketPort = websocketPort;
|
|
|
|
// Object.assign overrides the properties with the source (if there are conflicts)
|
|
Object.assign(
|
|
mergedArgs,
|
|
{
|
|
rpcPort,
|
|
tcpPort,
|
|
websocketPort,
|
|
...(args?.peerExchange && { discv5UdpPort }),
|
|
...(isGoWaku && { minRelayPeersToPublish: 0 }),
|
|
},
|
|
{ rpcAddress: "0.0.0.0" },
|
|
args
|
|
);
|
|
|
|
process.env.WAKUNODE2_STORE_MESSAGE_DB_URL = "";
|
|
|
|
if (this.docker.container) {
|
|
await this.docker.stop();
|
|
}
|
|
|
|
await this.docker.startContainer(
|
|
ports,
|
|
mergedArgs,
|
|
this.logPath,
|
|
WAKU_SERVICE_NODE_PARAMS
|
|
);
|
|
|
|
try {
|
|
log(`Waiting to see '${NODE_READY_LOG_LINE}' in ${this.type} logs`);
|
|
await this.waitForLog(NODE_READY_LOG_LINE, 15000);
|
|
if (process.env.CI) await delay(100);
|
|
log(`${this.type} node has been started`);
|
|
} catch (error) {
|
|
log(`Error starting ${this.type}: ${error}`);
|
|
if (this.docker.container) await this.docker.stop();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async stop(): Promise<void> {
|
|
this.docker?.stop();
|
|
}
|
|
|
|
async waitForLog(msg: string, timeout: number): Promise<void> {
|
|
return waitForLine(this.logPath, msg, timeout);
|
|
}
|
|
|
|
/** Calls nwaku JSON-RPC API `get_waku_v2_admin_v1_peers` to check
|
|
* for known peers
|
|
* @throws if WakuNode isn't started.
|
|
*/
|
|
async peers(): Promise<string[]> {
|
|
this.checkProcess();
|
|
|
|
return this.rpcCall<string[]>("get_waku_v2_admin_v1_peers", []);
|
|
}
|
|
|
|
async info(): Promise<RpcInfoResponse> {
|
|
this.checkProcess();
|
|
|
|
return this.rpcCall<RpcInfoResponse>("get_waku_v2_debug_v1_info", []);
|
|
}
|
|
|
|
async sendMessage(
|
|
message: MessageRpcQuery,
|
|
pubSubTopic: string = DefaultPubSubTopic
|
|
): Promise<boolean> {
|
|
this.checkProcess();
|
|
|
|
if (typeof message.timestamp === "undefined") {
|
|
message.timestamp = BigInt(new Date().valueOf()) * OneMillion;
|
|
}
|
|
|
|
return this.rpcCall<boolean>("post_waku_v2_relay_v1_message", [
|
|
pubSubTopic,
|
|
message,
|
|
]);
|
|
}
|
|
|
|
async messages(
|
|
pubsubTopic: string = DefaultPubSubTopic
|
|
): Promise<MessageRpcResponse[]> {
|
|
this.checkProcess();
|
|
|
|
const msgs = await this.rpcCall<MessageRpcResponse[]>(
|
|
"get_waku_v2_relay_v1_messages",
|
|
[pubsubTopic]
|
|
);
|
|
|
|
return msgs.filter(isDefined);
|
|
}
|
|
|
|
async getAsymmetricKeyPair(): Promise<KeyPair> {
|
|
this.checkProcess();
|
|
|
|
const { privateKey, publicKey, seckey, pubkey } = await this.rpcCall<{
|
|
seckey: string;
|
|
pubkey: string;
|
|
privateKey: string;
|
|
publicKey: string;
|
|
}>("get_waku_v2_private_v1_asymmetric_keypair", []);
|
|
|
|
// To be removed once https://github.com/vacp2p/rfc/issues/507 is fixed
|
|
if (seckey) {
|
|
return { privateKey: seckey, publicKey: pubkey };
|
|
} else {
|
|
return { privateKey, publicKey };
|
|
}
|
|
}
|
|
|
|
async postAsymmetricMessage(
|
|
message: MessageRpcQuery,
|
|
publicKey: Uint8Array,
|
|
pubSubTopic?: string
|
|
): Promise<boolean> {
|
|
this.checkProcess();
|
|
|
|
if (!message.payload) {
|
|
throw "Attempting to send empty message";
|
|
}
|
|
|
|
return this.rpcCall<boolean>("post_waku_v2_private_v1_asymmetric_message", [
|
|
pubSubTopic ? pubSubTopic : DefaultPubSubTopic,
|
|
message,
|
|
"0x" + bytesToHex(publicKey),
|
|
]);
|
|
}
|
|
|
|
async getAsymmetricMessages(
|
|
privateKey: Uint8Array,
|
|
pubSubTopic?: string
|
|
): Promise<MessageRpcResponse[]> {
|
|
this.checkProcess();
|
|
|
|
return await this.rpcCall<MessageRpcResponse[]>(
|
|
"get_waku_v2_private_v1_asymmetric_messages",
|
|
[
|
|
pubSubTopic ? pubSubTopic : DefaultPubSubTopic,
|
|
"0x" + bytesToHex(privateKey),
|
|
]
|
|
);
|
|
}
|
|
|
|
async getSymmetricKey(): Promise<Uint8Array> {
|
|
this.checkProcess();
|
|
|
|
return this.rpcCall<string>(
|
|
"get_waku_v2_private_v1_symmetric_key",
|
|
[]
|
|
).then(hexToBytes);
|
|
}
|
|
|
|
async postSymmetricMessage(
|
|
message: MessageRpcQuery,
|
|
symKey: Uint8Array,
|
|
pubSubTopic?: string
|
|
): Promise<boolean> {
|
|
this.checkProcess();
|
|
|
|
if (!message.payload) {
|
|
throw "Attempting to send empty message";
|
|
}
|
|
|
|
return this.rpcCall<boolean>("post_waku_v2_private_v1_symmetric_message", [
|
|
pubSubTopic ? pubSubTopic : DefaultPubSubTopic,
|
|
message,
|
|
"0x" + bytesToHex(symKey),
|
|
]);
|
|
}
|
|
|
|
async getSymmetricMessages(
|
|
symKey: Uint8Array,
|
|
pubSubTopic?: string
|
|
): Promise<MessageRpcResponse[]> {
|
|
this.checkProcess();
|
|
|
|
return await this.rpcCall<MessageRpcResponse[]>(
|
|
"get_waku_v2_private_v1_symmetric_messages",
|
|
[
|
|
pubSubTopic ? pubSubTopic : DefaultPubSubTopic,
|
|
"0x" + bytesToHex(symKey),
|
|
]
|
|
);
|
|
}
|
|
|
|
async getPeerId(): Promise<PeerId> {
|
|
if (this.peerId) return this.peerId;
|
|
this.peerId = await this._getPeerId();
|
|
return this.peerId;
|
|
}
|
|
|
|
async getMultiaddrWithId(): Promise<Multiaddr> {
|
|
if (this.multiaddrWithId) return this.multiaddrWithId;
|
|
|
|
const peerId = await this.getPeerId();
|
|
|
|
this.multiaddrWithId = multiaddr(
|
|
`/ip4/127.0.0.1/tcp/${this.websocketPort}/ws/p2p/${peerId.toString()}`
|
|
);
|
|
return this.multiaddrWithId;
|
|
}
|
|
|
|
private async _getPeerId(): Promise<PeerId> {
|
|
if (this.peerId) {
|
|
return this.peerId;
|
|
}
|
|
const res = await this.info();
|
|
const multiaddrWithId = res.listenAddresses
|
|
.map((ma) => multiaddr(ma))
|
|
.find((ma) => ma.protoNames().includes("ws"));
|
|
if (!multiaddrWithId) throw `${this.type} did not return a ws multiaddr`;
|
|
const peerIdStr = multiaddrWithId.getPeerId();
|
|
if (!peerIdStr) throw `${this.type} multiaddr does not contain peerId`;
|
|
this.peerId = peerIdFromString(peerIdStr);
|
|
|
|
return this.peerId;
|
|
}
|
|
|
|
get rpcUrl(): string {
|
|
return `http://127.0.0.1:${this.rpcPort}/`;
|
|
}
|
|
|
|
private async rpcCall<T>(
|
|
method: string,
|
|
params: Array<string | number | unknown>
|
|
): Promise<T> {
|
|
log("RPC Query: ", method, params);
|
|
const res = await fetch(this.rpcUrl, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method,
|
|
params,
|
|
}),
|
|
headers: new Headers({ "Content-Type": "application/json" }),
|
|
});
|
|
const json = await res.json();
|
|
log(`RPC Response: `, JSON.stringify(json));
|
|
return json.result;
|
|
}
|
|
|
|
private checkProcess(): void {
|
|
if (!this.docker?.container) {
|
|
throw `${this.type} container hasn't started`;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function defaultArgs(): Args {
|
|
return {
|
|
listenAddress: "0.0.0.0",
|
|
rpc: true,
|
|
relay: false,
|
|
rpcAdmin: true,
|
|
websocketSupport: true,
|
|
logLevel: LogLevel.Trace,
|
|
};
|
|
}
|
|
|
|
interface RpcInfoResponse {
|
|
// multiaddrs including peer id.
|
|
listenAddresses: string[];
|
|
enrUri?: string;
|
|
}
|
|
|
|
export function base64ToUtf8(b64: string): string {
|
|
return Buffer.from(b64, "base64").toString("utf-8");
|
|
}
|