js-waku/src/test_utils/nim_waku.ts

366 lines
8.7 KiB
TypeScript
Raw Normal View History

2021-05-10 15:53:05 +10:00
/**
* @hidden
* @module
*/
2021-03-10 17:39:53 +11:00
import { ChildProcess, spawn } from 'child_process';
import { randomInt } from 'crypto';
2021-03-10 17:39:53 +11:00
import appRoot from 'app-root-path';
2021-03-10 17:39:53 +11:00
import axios from 'axios';
2021-04-06 11:00:40 +10:00
import debug from 'debug';
2021-04-20 16:51:04 +10:00
import { Multiaddr, multiaddr } from 'multiaddr';
2021-03-10 17:39:53 +11:00
import PeerId from 'peer-id';
import { WakuMessage } from '../lib/waku_message';
import { DefaultPubsubTopic } from '../lib/waku_relay';
import * as proto from '../proto/waku/v2/message';
import { existsAsync, mkdirAsync, openAsync } from './async_fs';
import waitForLine from './log_file';
2021-03-10 17:39:53 +11:00
const dbg = debug('waku:nim-waku');
2021-04-06 11:00:40 +10:00
const NIM_WAKU_DEFAULT_P2P_PORT = 60000;
const NIM_WAKU_DEFAULT_RPC_PORT = 8545;
const NIM_WAKU_DIR = appRoot + '/nim-waku';
const NIM_WAKU_BIN = NIM_WAKU_DIR + '/build/wakunode2';
2021-03-10 17:39:53 +11:00
const LOG_DIR = './log';
2021-03-11 10:54:35 +11:00
export interface Args {
staticnode?: string;
nat?: 'none';
listenAddress?: string;
relay?: boolean;
rpc?: boolean;
rpcAdmin?: boolean;
nodekey?: string;
portsShift?: number;
2021-04-09 11:23:00 +10:00
logLevel?: LogLevel;
2021-05-06 10:37:12 +10:00
persistMessages?: boolean;
2021-05-19 11:00:43 +10:00
lightpush?: boolean;
topics?: string;
rpcPrivate?: boolean;
2021-04-09 11:23:00 +10:00
}
export enum LogLevel {
Error = 'error',
Info = 'info',
Warn = 'warn',
Debug = 'debug',
Trace = 'trace',
Notice = 'notice',
Fatal = 'fatal',
2021-03-11 10:54:35 +11:00
}
export interface KeyPair {
privateKey: string;
publicKey: string;
}
export interface WakuRelayMessage {
payload: string;
contentTopic?: string;
}
2021-03-10 17:39:53 +11:00
export class NimWaku {
private process?: ChildProcess;
2021-03-26 13:07:47 +11:00
private pid?: number;
private portsShift: number;
private peerId?: PeerId;
private multiaddrWithId?: Multiaddr;
private logPath: string;
2021-03-10 17:39:53 +11:00
2021-03-25 15:49:07 +11:00
constructor(logName: string) {
this.portsShift = randomInt(0, 5000);
2021-03-25 15:49:07 +11:00
this.logPath = `${LOG_DIR}/nim-waku_${logName}.log`;
}
async start(args?: Args): Promise<void> {
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
}
}
const logFile = await openAsync(this.logPath, 'w');
2021-03-11 10:54:35 +11:00
const mergedArgs = defaultArgs();
// Object.assign overrides the properties with the source (if there are conflicts)
2021-04-09 11:23:00 +10:00
Object.assign(
mergedArgs,
{ portsShift: this.portsShift, logLevel: LogLevel.Trace },
args
);
const argsArray = argsToArray(mergedArgs);
this.process = spawn(NIM_WAKU_BIN, argsArray, {
cwd: NIM_WAKU_DIR,
2021-03-11 10:54:35 +11:00
stdio: [
'ignore', // stdin
logFile, // stdout
logFile, // stderr
2021-03-10 17:39:53 +11:00
],
2021-03-11 10:54:35 +11:00
});
2021-03-26 13:07:47 +11:00
this.pid = this.process.pid;
2021-04-06 11:00:40 +10:00
dbg(
2021-03-26 13:07:47 +11:00
`nim-waku ${
this.process.pid
} started at ${new Date().toLocaleTimeString()}`
);
2021-03-10 17:39:53 +11:00
2021-03-25 16:29:35 +11:00
this.process.on('exit', (signal) => {
2021-04-06 11:00:40 +10:00
dbg(
2021-03-26 13:07:47 +11:00
`nim-waku ${
this.process ? this.process.pid : this.pid
} process exited with ${signal} at ${new Date().toLocaleTimeString()}`
);
2021-03-25 16:29:35 +11:00
});
this.process.on('error', (err) => {
2021-03-26 13:07:47 +11:00
console.log(
`nim-waku ${
this.process ? this.process.pid : this.pid
} process encountered an error: ${err} at ${new Date().toLocaleTimeString()}`
);
2021-03-25 16:29:35 +11:00
});
dbg("Waiting to see 'RPC Server started' in nim-waku logs");
await this.waitForLog('RPC Server started');
dbg('nim-waku node has been started');
}
public stop(): void {
2021-04-06 11:06:10 +10:00
dbg(
2021-03-26 13:07:47 +11:00
`nim-waku ${
this.process ? this.process.pid : this.pid
} getting SIGINT at ${new Date().toLocaleTimeString()}`
);
this.process ? this.process.kill('SIGINT') : null;
this.process = undefined;
}
async waitForLog(msg: string): Promise<void> {
return waitForLine(this.logPath, msg);
2021-03-10 17:39:53 +11:00
}
/** Calls nim-waku2 JSON-RPC API `get_waku_v2_admin_v1_peers` to check
* for known peers
* @throws if nim-waku2 isn't started.
*/
async peers(): Promise<string[]> {
2021-03-10 17:39:53 +11:00
this.checkProcess();
return this.rpcCall<string[]>('get_waku_v2_admin_v1_peers', []);
2021-03-10 17:39:53 +11:00
}
async info(): Promise<RpcInfoResponse> {
2021-03-10 17:39:53 +11:00
this.checkProcess();
return this.rpcCall<RpcInfoResponse>('get_waku_v2_debug_v1_info', []);
2021-03-10 17:39:53 +11:00
}
async sendMessage(
message: WakuMessage,
pubsubTopic?: string
): Promise<boolean> {
this.checkProcess();
if (!message.payload) {
throw 'Attempting to send empty message';
}
const rpcMessage = {
2021-03-15 16:26:07 +11:00
payload: bufToHex(message.payload),
contentTopic: message.contentTopic,
};
return this.rpcCall<boolean>('post_waku_v2_relay_v1_message', [
pubsubTopic ? pubsubTopic : DefaultPubsubTopic,
rpcMessage,
]);
}
async messages(): Promise<WakuMessage[]> {
this.checkProcess();
2021-07-07 11:23:56 +10:00
const isDefined = (msg: WakuMessage | undefined): msg is WakuMessage => {
return !!msg;
};
const protoMsgs = await this.rpcCall<proto.WakuMessage[]>(
'get_waku_v2_relay_v1_messages',
[DefaultPubsubTopic]
);
const msgs = await Promise.all(
protoMsgs.map(async (protoMsg) => await WakuMessage.decodeProto(protoMsg))
);
return msgs.filter(isDefined);
}
async getAsymmetricKeyPair(): Promise<KeyPair> {
this.checkProcess();
const { seckey, pubkey } = await this.rpcCall<{
seckey: string;
pubkey: string;
}>('get_waku_v2_private_v1_asymmetric_keypair', []);
return { privateKey: seckey, publicKey: pubkey };
}
async postAsymmetricMessage(
message: WakuRelayMessage,
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' + bufToHex(publicKey),
]);
}
async getAsymmetricMessages(
privateKey: Uint8Array,
pubsubTopic?: string
): Promise<WakuRelayMessage[]> {
this.checkProcess();
return await this.rpcCall<WakuRelayMessage[]>(
'get_waku_v2_private_v1_asymmetric_messages',
[
pubsubTopic ? pubsubTopic : DefaultPubsubTopic,
'0x' + bufToHex(privateKey),
]
);
}
async getPeerId(): Promise<PeerId> {
return await this.setPeerId().then((res) => res.peerId);
}
async getMultiaddrWithId(): Promise<Multiaddr> {
return await this.setPeerId().then((res) => res.multiaddrWithId);
}
private async setPeerId(): Promise<{
peerId: PeerId;
multiaddrWithId: Multiaddr;
}> {
if (this.peerId && this.multiaddrWithId) {
return { peerId: this.peerId, multiaddrWithId: this.multiaddrWithId };
}
const res = await this.info();
this.multiaddrWithId = multiaddr(res.listenStr);
const peerIdStr = this.multiaddrWithId.getPeerId();
2021-04-20 16:51:04 +10:00
if (!peerIdStr) throw 'Nim-waku multiaddr does not contain peerId';
this.peerId = PeerId.createFromB58String(peerIdStr);
return { peerId: this.peerId, multiaddrWithId: this.multiaddrWithId };
2021-03-10 17:39:53 +11:00
}
get multiaddr(): Multiaddr {
const port = NIM_WAKU_DEFAULT_P2P_PORT + this.portsShift;
return multiaddr(`/ip4/127.0.0.1/tcp/${port}/`);
}
get rpcUrl(): string {
const port = NIM_WAKU_DEFAULT_RPC_PORT + this.portsShift;
return `http://localhost:${port}/`;
2021-03-10 17:39:53 +11:00
}
private async rpcCall<T>(
2021-04-13 15:22:29 +10:00
method: string,
params: Array<string | number | unknown>
): Promise<T> {
2021-03-10 17:39:53 +11:00
const res = await axios.post(
this.rpcUrl,
2021-03-10 17:39:53 +11:00
{
jsonrpc: '2.0',
id: 1,
method,
params,
},
{
headers: { 'Content-Type': 'application/json' },
}
);
return res.data.result;
2021-03-10 17:39:53 +11:00
}
private checkProcess(): void {
2021-03-10 17:39:53 +11:00
if (!this.process) {
throw "Nim Waku isn't started";
}
}
}
2021-03-11 10:54:35 +11:00
export function argsToArray(args: Args): Array<string> {
const array = [];
for (const [key, value] of Object.entries(args)) {
// Change the key from camelCase to kebab-case
const kebabKey = key.replace(/([A-Z])/, (_, capital) => {
return '-' + capital.toLowerCase();
});
const arg = `--${kebabKey}=${value}`;
array.push(arg);
}
return array;
}
export function defaultArgs(): Args {
2021-03-11 10:54:35 +11:00
return {
nat: 'none',
listenAddress: '127.0.0.1',
relay: true,
rpc: true,
rpcAdmin: true,
};
}
2021-03-12 10:44:47 +11:00
export function strToHex(str: string): string {
let hex: string;
try {
hex = unescape(encodeURIComponent(str))
.split('')
.map(function (v) {
2021-03-12 10:44:47 +11:00
return v.charCodeAt(0).toString(16);
})
.join('');
} catch (e) {
hex = str;
console.log('invalid text input: ' + str);
}
return hex;
}
export function bufToHex(buffer: Uint8Array): string {
return Array.prototype.map
.call(buffer, (x) => ('00' + x.toString(16)).slice(-2))
.join('');
}
interface RpcInfoResponse {
// multiaddr including id.
listenStr: string;
}