mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 13:53:12 +00:00
* feat(browser-tests): simplify, refactor, update dockerized browser node * Update packages/browser-tests/web/index.ts * fix: remove comments and console.logs from tests * fix: add temporary logging * fix: debugging static sharding * fix: replace console with logger * fix: remove use of any * fix: log dial error * fix: replace any with libp2p options * fix: remove unused logic around sourcing address.env * fix: uncomment log * fix: add more logging and fix tests * feat: add types for test-config * fix: add types to server.ts * fix: remove more uses of any * fix: remove use of any in endpoint handlers
432 lines
12 KiB
TypeScript
432 lines
12 KiB
TypeScript
import {
|
|
createLightNode,
|
|
LightNode,
|
|
Protocols,
|
|
NetworkConfig,
|
|
CreateNodeOptions,
|
|
} from "@waku/sdk";
|
|
import {
|
|
AutoSharding,
|
|
DEFAULT_CLUSTER_ID,
|
|
DEFAULT_NUM_SHARDS,
|
|
ShardId,
|
|
StaticSharding,
|
|
ShardInfo,
|
|
CreateLibp2pOptions,
|
|
IEncoder,
|
|
ILightPush,
|
|
SDKProtocolResult,
|
|
Failure,
|
|
} from "@waku/interfaces";
|
|
import { bootstrap } from "@libp2p/bootstrap";
|
|
import { EnrDecoder, TransportProtocol } from "@waku/enr";
|
|
import type { Multiaddr } from "@multiformats/multiaddr";
|
|
import type { ITestBrowser } from "../types/global.js";
|
|
import { Logger, StaticShardingRoutingInfo } from "@waku/utils";
|
|
import type { PeerId } from "@libp2p/interface";
|
|
|
|
const log = new Logger("waku-headless");
|
|
|
|
export interface SerializableSDKProtocolResult {
|
|
successes: string[];
|
|
failures: Array<{
|
|
error: string;
|
|
peerId?: string;
|
|
}>;
|
|
myPeerId?: string;
|
|
}
|
|
|
|
function makeSerializable(result: SDKProtocolResult): SerializableSDKProtocolResult {
|
|
return {
|
|
...result,
|
|
successes: result.successes.map((peerId: PeerId) => peerId.toString()),
|
|
failures: result.failures.map((failure: Failure) => ({
|
|
error: failure.error || failure.toString(),
|
|
peerId: failure.peerId ? failure.peerId.toString() : undefined,
|
|
})),
|
|
};
|
|
}
|
|
|
|
async function convertEnrToMultiaddrs(enrString: string): Promise<string[]> {
|
|
try {
|
|
const enr = await EnrDecoder.fromString(enrString);
|
|
const allMultiaddrs = enr.getAllLocationMultiaddrs();
|
|
const multiaddrs: string[] = [];
|
|
|
|
for (const multiaddr of allMultiaddrs) {
|
|
const maStr = multiaddr.toString();
|
|
multiaddrs.push(maStr);
|
|
}
|
|
if (multiaddrs.length === 0) {
|
|
const tcpMultiaddr = enr.getFullMultiaddr(TransportProtocol.TCP);
|
|
if (tcpMultiaddr) {
|
|
const tcpStr = tcpMultiaddr.toString();
|
|
multiaddrs.push(tcpStr);
|
|
}
|
|
const udpMultiaddr = enr.getFullMultiaddr(TransportProtocol.UDP);
|
|
if (udpMultiaddr) {
|
|
const udpStr = udpMultiaddr.toString();
|
|
multiaddrs.push(udpStr);
|
|
}
|
|
}
|
|
|
|
return multiaddrs;
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export class WakuHeadless {
|
|
waku: LightNode | null;
|
|
networkConfig: NetworkConfig;
|
|
lightpushNode: string | null;
|
|
enrBootstrap: string | null;
|
|
constructor(
|
|
networkConfig?: Partial<NetworkConfig>,
|
|
lightpushNode?: string | null,
|
|
enrBootstrap?: string | null,
|
|
) {
|
|
this.waku = null;
|
|
this.networkConfig = this.buildNetworkConfig(networkConfig);
|
|
log.info("Network config on construction:", this.networkConfig);
|
|
this.lightpushNode = lightpushNode || null;
|
|
this.enrBootstrap = enrBootstrap || null;
|
|
|
|
if (this.lightpushNode) {
|
|
log.info(`Configured preferred lightpush node: ${this.lightpushNode}`);
|
|
}
|
|
if (this.enrBootstrap) {
|
|
log.info(`Configured ENR bootstrap: ${this.enrBootstrap}`);
|
|
}
|
|
}
|
|
|
|
private shouldUseCustomBootstrap(options: CreateNodeOptions): boolean {
|
|
const hasEnr = Boolean(this.enrBootstrap);
|
|
const isDefaultBootstrap = Boolean(options.defaultBootstrap);
|
|
|
|
return hasEnr && !isDefaultBootstrap;
|
|
}
|
|
|
|
private async getBootstrapMultiaddrs(): Promise<string[]> {
|
|
if (!this.enrBootstrap) {
|
|
return [];
|
|
}
|
|
|
|
const enrList = this.enrBootstrap.split(",").map((enr) => enr.trim());
|
|
const allMultiaddrs: string[] = [];
|
|
|
|
for (const enr of enrList) {
|
|
const multiaddrs = await convertEnrToMultiaddrs(enr);
|
|
if (multiaddrs.length > 0) {
|
|
allMultiaddrs.push(...multiaddrs);
|
|
}
|
|
}
|
|
|
|
return allMultiaddrs;
|
|
}
|
|
|
|
private buildNetworkConfig(
|
|
providedConfig?: Partial<NetworkConfig> | Partial<ShardInfo>,
|
|
): NetworkConfig {
|
|
const clusterId = providedConfig?.clusterId ?? DEFAULT_CLUSTER_ID;
|
|
|
|
const staticShards = (providedConfig as Partial<ShardInfo>)?.shards;
|
|
if (
|
|
staticShards &&
|
|
Array.isArray(staticShards) &&
|
|
staticShards.length > 0
|
|
) {
|
|
log.info("Using static sharding with shards:", staticShards);
|
|
return {
|
|
clusterId,
|
|
} as StaticSharding;
|
|
}
|
|
|
|
const numShardsInCluster =
|
|
(providedConfig as Partial<AutoSharding>)?.numShardsInCluster ?? DEFAULT_NUM_SHARDS;
|
|
log.info(
|
|
"Using auto sharding with num shards in cluster:",
|
|
numShardsInCluster,
|
|
);
|
|
return {
|
|
clusterId,
|
|
numShardsInCluster,
|
|
} as AutoSharding;
|
|
}
|
|
|
|
private async send(
|
|
lightPush: ILightPush,
|
|
encoder: IEncoder,
|
|
payload: Uint8Array,
|
|
): Promise<SDKProtocolResult> {
|
|
return lightPush.send(encoder, {
|
|
payload,
|
|
timestamp: new Date(),
|
|
});
|
|
}
|
|
|
|
async pushMessageV3(
|
|
contentTopic: string,
|
|
payload: string,
|
|
pubsubTopic: string,
|
|
): Promise<SerializableSDKProtocolResult> {
|
|
if (!this.waku) {
|
|
throw new Error("Waku node not started");
|
|
}
|
|
log.info(
|
|
"Pushing message via v3 lightpush:",
|
|
contentTopic,
|
|
payload,
|
|
pubsubTopic,
|
|
);
|
|
log.info("Waku node:", this.waku);
|
|
log.info("Network config:", this.networkConfig);
|
|
|
|
let processedPayload: Uint8Array;
|
|
try {
|
|
const binaryString = atob(payload);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
processedPayload = bytes;
|
|
} catch (e) {
|
|
processedPayload = new TextEncoder().encode(payload);
|
|
}
|
|
|
|
try {
|
|
const lightPush = this.waku.lightPush;
|
|
if (!lightPush) {
|
|
throw new Error("Lightpush service not available");
|
|
}
|
|
|
|
let shardId: ShardId | undefined;
|
|
if (pubsubTopic) {
|
|
const staticShardingRoutingInfo =
|
|
StaticShardingRoutingInfo.fromPubsubTopic(
|
|
pubsubTopic,
|
|
this.networkConfig as StaticSharding,
|
|
);
|
|
shardId = staticShardingRoutingInfo?.shardId;
|
|
}
|
|
|
|
const encoder = this.waku.createEncoder({
|
|
contentTopic,
|
|
shardId,
|
|
});
|
|
log.info("Encoder:", encoder);
|
|
log.info("Pubsub topic:", pubsubTopic);
|
|
log.info("Encoder pubsub topic:", encoder.pubsubTopic);
|
|
|
|
if (pubsubTopic && pubsubTopic !== encoder.pubsubTopic) {
|
|
log.warn(
|
|
`Explicit pubsubTopic ${pubsubTopic} provided, but auto-sharding determined ${encoder.pubsubTopic}. Using auto-sharding.`,
|
|
);
|
|
}
|
|
|
|
let result;
|
|
if (this.lightpushNode) {
|
|
try {
|
|
const preferredPeerId = this.getPeerIdFromMultiaddr(
|
|
this.lightpushNode,
|
|
);
|
|
if (preferredPeerId) {
|
|
result = await this.send(lightPush, encoder, processedPayload);
|
|
log.info("✅ Message sent via preferred lightpush node");
|
|
} else {
|
|
throw new Error(
|
|
"Could not extract peer ID from preferred node address",
|
|
);
|
|
}
|
|
} catch (error) {
|
|
log.error(
|
|
"Couldn't send message via preferred lightpush node:",
|
|
error,
|
|
);
|
|
result = await this.send(lightPush, encoder, processedPayload);
|
|
}
|
|
} else {
|
|
result = await this.send(lightPush, encoder, processedPayload);
|
|
}
|
|
|
|
const serializableResult = makeSerializable(result);
|
|
|
|
return serializableResult;
|
|
} catch (error) {
|
|
log.error("Error sending message via v3 lightpush:", error);
|
|
throw new Error(
|
|
`Failed to send v3 message: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async waitForPeers(
|
|
timeoutMs: number = 10000,
|
|
protocols: Protocols[] = [Protocols.LightPush, Protocols.Filter],
|
|
) {
|
|
if (!this.waku) {
|
|
throw new Error("Waku node not started");
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await this.waku.waitForPeers(protocols, timeoutMs);
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
const peers = this.waku.libp2p.getPeers();
|
|
|
|
return {
|
|
success: true,
|
|
peersFound: peers.length,
|
|
protocolsRequested: protocols,
|
|
timeElapsed: elapsed,
|
|
};
|
|
} catch (error) {
|
|
const elapsed = Date.now() - startTime;
|
|
log.error(`Failed to find peers after ${elapsed}ms:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async createWakuNode(options: CreateNodeOptions) {
|
|
try {
|
|
if (this.waku) {
|
|
await this.waku.stop();
|
|
}
|
|
} catch (e) {
|
|
log.warn("ignore previous waku stop error");
|
|
}
|
|
|
|
let libp2pConfig: CreateLibp2pOptions = {
|
|
...options.libp2p,
|
|
filterMultiaddrs: false,
|
|
};
|
|
|
|
if (this.enrBootstrap) {
|
|
const multiaddrs = await this.getBootstrapMultiaddrs();
|
|
|
|
if (multiaddrs.length > 0) {
|
|
libp2pConfig.peerDiscovery = [
|
|
bootstrap({ list: multiaddrs }),
|
|
...(options.libp2p?.peerDiscovery || []),
|
|
];
|
|
}
|
|
}
|
|
|
|
const createOptions = {
|
|
...options,
|
|
networkConfig: this.networkConfig,
|
|
libp2p: libp2pConfig,
|
|
};
|
|
|
|
this.waku = await createLightNode(createOptions);
|
|
return { success: true };
|
|
}
|
|
|
|
async startNode() {
|
|
if (!this.waku) {
|
|
throw new Error("Waku node not created");
|
|
}
|
|
await this.waku.start();
|
|
|
|
if (this.lightpushNode) {
|
|
await this.dialPreferredLightpushNode();
|
|
}
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
private async dialPreferredLightpushNode() {
|
|
if (!this.waku || !this.lightpushNode) {
|
|
log.info("Skipping dial: waku or lightpushNode not set");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
log.info("Attempting to dial preferred lightpush node:", this.lightpushNode);
|
|
await this.waku.dial(this.lightpushNode);
|
|
log.info("Successfully dialed preferred lightpush node:", this.lightpushNode);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
log.error(
|
|
"Failed to dial preferred lightpush node:",
|
|
this.lightpushNode,
|
|
message
|
|
);
|
|
}
|
|
}
|
|
|
|
private getPeerIdFromMultiaddr(multiaddr: string): string | null {
|
|
const parts = multiaddr.split("/");
|
|
const p2pIndex = parts.indexOf("p2p");
|
|
return p2pIndex !== -1 && p2pIndex + 1 < parts.length
|
|
? parts[p2pIndex + 1]
|
|
: null;
|
|
}
|
|
|
|
async stopNode() {
|
|
if (!this.waku) {
|
|
throw new Error("Waku node not created");
|
|
}
|
|
await this.waku.stop();
|
|
return { success: true };
|
|
}
|
|
|
|
getPeerInfo() {
|
|
if (!this.waku) {
|
|
throw new Error("Waku node not started");
|
|
}
|
|
|
|
const addrs = this.waku.libp2p.getMultiaddrs();
|
|
return {
|
|
peerId: this.waku.libp2p.peerId.toString(),
|
|
multiaddrs: addrs.map((a: Multiaddr) => a.toString()),
|
|
peers: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
(() => {
|
|
try {
|
|
log.info("Initializing WakuHeadless...");
|
|
|
|
const testWindow = window as ITestBrowser;
|
|
const globalNetworkConfig = testWindow.__WAKU_NETWORK_CONFIG;
|
|
const globalLightpushNode = testWindow.__WAKU_LIGHTPUSH_NODE;
|
|
const globalEnrBootstrap = testWindow.__WAKU_ENR_BOOTSTRAP;
|
|
|
|
log.info("Global config from window:", {
|
|
networkConfig: globalNetworkConfig,
|
|
lightpushNode: globalLightpushNode,
|
|
enrBootstrap: globalEnrBootstrap
|
|
});
|
|
|
|
const instance = new WakuHeadless(
|
|
globalNetworkConfig,
|
|
globalLightpushNode,
|
|
globalEnrBootstrap,
|
|
);
|
|
|
|
testWindow.wakuApi = instance;
|
|
log.info("WakuHeadless initialized successfully:", !!testWindow.wakuApi);
|
|
} catch (error) {
|
|
log.error("Error initializing WakuHeadless:", error);
|
|
const testWindow = window as ITestBrowser;
|
|
// Create a stub wakuApi that will reject all method calls
|
|
testWindow.wakuApi = {
|
|
waku: null,
|
|
networkConfig: { clusterId: 0, numShardsInCluster: 0 },
|
|
lightpushNode: null,
|
|
enrBootstrap: null,
|
|
error,
|
|
createWakuNode: () => Promise.reject(new Error("WakuHeadless failed to initialize")),
|
|
startNode: () => Promise.reject(new Error("WakuHeadless failed to initialize")),
|
|
stopNode: () => Promise.reject(new Error("WakuHeadless failed to initialize")),
|
|
pushMessageV3: () => Promise.reject(new Error("WakuHeadless failed to initialize")),
|
|
waitForPeers: () => Promise.reject(new Error("WakuHeadless failed to initialize")),
|
|
getPeerInfo: () => { throw new Error("WakuHeadless failed to initialize"); },
|
|
} as unknown as WakuHeadless;
|
|
}
|
|
})();
|