UNPKG

@moonsong-labs/moonwall-cli

Version:

Testing framework for the Moon family of projects

673 lines (664 loc) 23.6 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; // src/internal/chopsticksHelpers.ts var chopsticksHelpers_exports = {}; __export(chopsticksHelpers_exports, { chopForkToFinalizedHead: () => chopForkToFinalizedHead, createChopsticksBlock: () => createChopsticksBlock, getWsFromConfig: () => getWsFromConfig, sendNewBlockAndCheck: () => sendNewBlockAndCheck, sendNewBlockRequest: () => sendNewBlockRequest, sendSetHeadRequest: () => sendSetHeadRequest, sendSetStorageRequest: () => sendSetStorageRequest }); module.exports = __toCommonJS(chopsticksHelpers_exports); var import_promises2 = require("timers/promises"); // src/lib/globalContext.ts var import_api_augment2 = require("@moonbeam-network/api-augment"); // src/internal/providers.ts var import_moonbeam_types_bundle = require("moonbeam-types-bundle"); var import_api = require("@polkadot/api"); var import_web3 = require("web3"); var import_web3_providers_ws = require("web3-providers-ws"); var import_ethers = require("ethers"); var import_debug = __toESM(require("debug"), 1); var import_chalk = __toESM(require("chalk"), 1); var import_moonwall_util = require("@moonsong-labs/moonwall-util"); var debug = (0, import_debug.default)("global:providers"); function prepareProviders(providerConfigs) { return providerConfigs.map(({ name, endpoints, type }) => { const url = endpoints.includes("ENV_VAR") ? process.env.WSS_URL : endpoints[0]; switch (type) { case "polkadotJs": debug(`\u{1F7E2} PolkadotJs provider ${name} details prepared`); return { name, type, connect: async () => { const api = await import_api.ApiPromise.create({ provider: new import_api.WsProvider(url), initWasm: false, noInitWarn: true }); await api.isReady; return api; }, ws: () => new import_api.WsProvider(url) }; case "moon": debug(`\u{1F7E2} Moonbeam provider ${name} details prepared`); return { name, type, connect: async () => { const moonApi = await import_api.ApiPromise.create({ provider: new import_api.WsProvider(url), rpc: import_moonbeam_types_bundle.rpcDefinitions, typesBundle: import_moonbeam_types_bundle.types, noInitWarn: true }); await moonApi.isReady; return moonApi; }, ws: () => new import_api.WsProvider(url) }; case "web3": debug(`\u{1F7E2} Web3 provider ${name} details prepared`); return { name, type, connect: () => { const provider = new import_web3_providers_ws.WebSocketProvider( url, {}, { delay: 50, autoReconnect: false, maxAttempts: 10 } ); provider.on("error", () => { throw new Error( `Cannot connect to Web3 provider ${import_chalk.default.bgWhiteBright.blackBright(url)}` ); }); return new import_web3.Web3(provider); } }; case "ethers": debug(`\u{1F7E2} Ethers provider ${name} details prepared`); return { name, type, connect: () => { const provider = new import_ethers.ethers.WebSocketProvider(url); return new import_ethers.Wallet(import_moonwall_util.ALITH_PRIVATE_KEY, provider); } }; default: return { name, type, connect: () => console.log(`\u{1F6A7} provider ${name} not yet implemented`) }; } }); } async function populateProviderInterface(name, type, connect) { switch (type) { case "polkadotJs": const pjsApi = await connect(); return { name, api: pjsApi, type, greet: () => { debug( `\u{1F44B} Provider ${name} is connected to chain ${pjsApi.consts.system.version.specName.toString()} RT${pjsApi.consts.system.version.specVersion.toNumber()}` ); return { rtVersion: pjsApi.consts.system.version.specVersion.toNumber(), rtName: pjsApi.consts.system.version.specName.toString() }; }, disconnect: async () => pjsApi.disconnect() }; case "moon": const mbApi = await connect(); return { name, api: mbApi, type, greet: () => { debug( `\u{1F44B} Provider ${name} is connected to chain ${mbApi.consts.system.version.specName.toString()} RT${mbApi.consts.system.version.specVersion.toNumber()}` ); return { rtVersion: mbApi.consts.system.version.specVersion.toNumber(), rtName: mbApi.consts.system.version.specName.toString() }; }, disconnect: async () => mbApi.disconnect() }; case "ethers": const ethApi = await connect(); return { name, api: ethApi, type, greet: async () => debug( `\u{1F44B} Provider ${name} is connected to chain ` + (await ethApi.provider.getNetwork()).chainId ), disconnect: async () => { ethApi.provider.destroy(); } }; case "web3": const web3Api = await connect(); return { name, api: web3Api, type, greet: async () => console.log( `\u{1F44B} Provider ${name} is connected to chain ` + await web3Api.eth.getChainId() ), disconnect: async () => { web3Api.currentProvider.disconnect(); } }; default: throw new Error("UNKNOWN TYPE"); } } // src/internal/localNode.ts var import_child_process = require("child_process"); var import_chalk2 = __toESM(require("chalk"), 1); var import_debug2 = __toESM(require("debug"), 1); var import_fs = require("fs"); var debugNode = (0, import_debug2.default)("global:node"); async function launchNode(cmd, args, name) { if (cmd.includes("moonbeam") && !(0, import_fs.existsSync)(cmd)) { throw new Error( `No binary file found at location: ${cmd} Are you sure your ${import_chalk2.default.bgWhiteBright.blackBright( "moonwall.config.json" )} file has the correct "binPath" in launchSpec?` ); } let runningNode; const onProcessExit = () => { runningNode && runningNode.kill(); }; const onProcessInterrupt = () => { runningNode && runningNode.kill(); }; process.once("exit", onProcessExit); process.once("SIGINT", onProcessInterrupt); runningNode = (0, import_child_process.spawn)(cmd, args); runningNode.once("exit", () => { process.removeListener("exit", onProcessExit); process.removeListener("SIGINT", onProcessInterrupt); debugNode(`Exiting dev node: ${name}`); }); runningNode.on("error", (err) => { if (err.errno == "ENOENT") { console.error( `\x1B[31mMissing Moonbeam binary at(${cmd}). Please compile the Moonbeam project\x1B[0m` ); } else { console.error(err); } process.exit(1); }); const binaryLogs = []; await new Promise((resolve, reject) => { const timer = setTimeout(() => { console.error(import_chalk2.default.redBright("Failed to start Moonbeam Test Node.")); console.error(`Command: ${cmd} ${args.join(" ")}`); console.error(`Logs:`); console.error(binaryLogs.map((chunk) => chunk.toString()).join("\n")); reject("Failed to launch node"); }, 1e4); const onData = async (chunk) => { debugNode(chunk.toString()); binaryLogs.push(chunk); if (chunk.toString().match(/Development Service Ready/) || chunk.toString().match(/ RPC listening on port/)) { clearTimeout(timer); runningNode.stderr.off("data", onData); runningNode.stdout.off("data", onData); resolve(); } }; runningNode.stderr.on("data", onData); runningNode.stdout.on("data", onData); }); return runningNode; } // src/lib/configReader.ts var import_api_augment = require("@moonbeam-network/api-augment"); var import_promises = __toESM(require("fs/promises"), 1); var import_path = __toESM(require("path"), 1); async function importJsonConfig() { const filePath = import_path.default.join(process.cwd(), "moonwall.config.json"); try { const file = await import_promises.default.readFile(filePath, "utf8"); const json = JSON.parse(file); return json; } catch (e) { console.error(e); throw new Error(`Error import config at ${filePath}`); } } // src/internal/foundations.ts function parseRunCmd(launchSpec) { const launch = !!!launchSpec.running ? true : launchSpec.running; const cmd = launchSpec.binPath; let args = launchSpec.options ? [...launchSpec.options] : [ "--no-hardware-benchmarks", "--no-telemetry", "--reserved-only", "--rpc-cors=all", "--no-grandpa", "--sealing=manual", "--force-authoring", "--no-prometheus", "--alice", "--chain=moonbase-dev", "--in-peers=0", "--out-peers=0", "--tmp" ]; `ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID) * 100}`; if (launchSpec.ports) { const ports = launchSpec.ports; if (ports.p2pPort) { args.push(`--port=${ports.p2pPort}`); } if (ports.wsPort) { args.push(`--ws-port=${ports.wsPort}`); } if (ports.rpcPort) { args.push(`--rpc-port=${ports.rpcPort}`); } } else { args.push( `--port=${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100 + 2}` ); args.push( `--ws-port=${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}` ); args.push( `--rpc-port=${1e4 + (Number(process.env.VITEST_POOL_ID || 1) * 100 + 1)}` ); } return { cmd, args, launch }; } function parseChopsticksRunCmd(launchSpecs) { const launch = !!!launchSpecs[0].running ? true : launchSpecs[0].running; if (launchSpecs.length === 1) { const chopsticksCmd2 = "node"; const chopsticksArgs2 = [ "node_modules/@acala-network/chopsticks/chopsticks.js", "dev", `--config=${launchSpecs[0].configPath}` ]; const mode = launchSpecs[0].buildBlockMode ? launchSpecs[0].buildBlockMode : "manual"; const num = mode == "batch" ? 0 : mode == "instant" ? 1 : 2; chopsticksArgs2.push(`--build-block-mode=${num}`); if (launchSpecs[0].wsPort) { chopsticksArgs2.push(`--port=${launchSpecs[0].wsPort}`); } if (launchSpecs[0].wasmOverride) { chopsticksArgs2.push(`--wasm-override=${launchSpecs[0].wasmOverride}`); } return { cmd: chopsticksCmd2, args: chopsticksArgs2, launch }; } const chopsticksCmd = "node"; const chopsticksArgs = [ "node_modules/@acala-network/chopsticks/chopsticks.js", "xcm" ]; launchSpecs.forEach((spec) => { const type = spec.type ? spec.type : "parachain"; switch (type) { case "parachain": chopsticksArgs.push(`--parachain=${spec.configPath}`); break; case "relaychain": chopsticksArgs.push(`--relaychain=${spec.configPath}`); } }); return { cmd: chopsticksCmd, args: chopsticksArgs, launch // rtUpgradePath: launchSpecs[0].rtUpgradePath // ? launchSpecs[0].rtUpgradePath // : "", }; } // src/lib/globalContext.ts var import_debug3 = __toESM(require("debug"), 1); var debugSetup = (0, import_debug3.default)("global:context"); var _MoonwallContext = class { environment; providers; nodes; foundation; _finalizedHead; rtUpgradePath; constructor(config) { this.environment; this.providers = []; this.nodes = []; const env = config.environments.find(({ name }) => name == process.env.MOON_TEST_ENV); const blob = { name: env.name, context: {}, providers: [], nodes: [], foundationType: env.foundation.type }; switch (env.foundation.type) { case "read_only": if (!env.connections) { throw new Error( `${env.name} env config is missing connections specification, required by foundation READ_ONLY` ); } else { blob.providers = prepareProviders(env.connections); } debugSetup(`\u{1F7E2} Foundation "${env.foundation.type}" parsed for environment: ${env.name}`); break; case "chopsticks": blob.nodes.push(parseChopsticksRunCmd(env.foundation.launchSpec)); blob.providers.push(...prepareProviders(env.connections)); this.rtUpgradePath = env.foundation.rtUpgradePath; debugSetup(`\u{1F7E2} Foundation "${env.foundation.type}" parsed for environment: ${env.name}`); break; case "dev": const { cmd, args, launch } = parseRunCmd(env.foundation.launchSpec[0]); blob.nodes.push({ name: env.foundation.launchSpec[0].name, cmd, args, launch }); blob.providers = env.connections ? prepareProviders(env.connections) : prepareProviders([ { name: "w3", type: "web3", endpoints: [ `ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}` ] }, { name: "eth", type: "ethers", endpoints: [ `ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}` ] }, { name: "mb", type: "moon", endpoints: [ `ws://127.0.0.1:${1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100}` ] } ]); debugSetup(`\u{1F7E2} Foundation "${env.foundation.type}" parsed for environment: ${env.name}`); break; default: debugSetup(`\u{1F6A7} Foundation "${env.foundation.type}" unsupported, skipping`); return; } this.environment = blob; } get genesis() { if (this._finalizedHead) { return this._finalizedHead; } else { return ""; } } set genesis(hash) { if (hash.length !== 66) { throw new Error("Cannot set genesis to invalid hash"); } this._finalizedHead = hash; } async startNetwork() { const activeNodes = this.nodes.filter((node) => !node.killed); if (activeNodes.length > 0) { console.log("Nodes already started! Skipping command"); return _MoonwallContext.getContext(); } const nodes = _MoonwallContext.getContext().environment.nodes; const promises = nodes.map(async ({ cmd, args, name, launch }) => { return launch && this.nodes.push(await launchNode(cmd, args, name)); }); await Promise.all(promises); } async stopNetwork() { if (this.nodes.length === 0) { console.log("Nodes already stopped! Skipping command"); return _MoonwallContext.getContext(); } this.nodes.forEach((node) => node.kill()); await this.wipeNodes(); } async connectEnvironment(environmentName) { if (this.providers.length > 0) { console.log("Providers already connected! Skipping command"); return _MoonwallContext.getContext(); } const globalConfig = await importJsonConfig(); const promises = this.environment.providers.map( async ({ name, type, connect }) => new Promise(async (resolve) => { this.providers.push(await populateProviderInterface(name, type, connect)); resolve(""); }) ); await Promise.all(promises); this.foundation = globalConfig.environments.find( ({ name }) => name == environmentName ).foundation.type; if (this.foundation == "dev") { this.genesis = (await this.providers.find(({ type }) => type == "polkadotJs" || type == "moon").api.rpc.chain.getBlockHash(0)).toString(); } if (this.foundation == "chopsticks") { this.genesis = (await this.providers.find(({ type }) => type == "polkadotJs" || type == "moon").api.rpc.chain.getFinalizedHead()).toString(); } } async wipeNodes() { this.nodes = []; } async disconnect(providerName) { if (providerName) { this.providers.find(({ name }) => name === providerName).disconnect(); this.providers.filter(({ name }) => name !== providerName); } else { await Promise.all(this.providers.map((prov) => prov.disconnect())); this.providers = []; } } static printStats() { if (_MoonwallContext) { console.dir(_MoonwallContext.getContext(), { depth: 1 }); } else { console.log("Global context not created!"); } } static getContext(config, force = false) { if (!_MoonwallContext.instance || force) { if (global.moonInstance && !force) { _MoonwallContext.instance = global.moonInstance; return _MoonwallContext.instance; } if (!config) { console.error("\u274C Config must be provided on Global Context instantiation"); return void 0; } _MoonwallContext.instance = new _MoonwallContext(config); debugSetup(`\u{1F7E2} Moonwall context "${config.label}" created`); } return _MoonwallContext.instance; } static async destroy() { const ctx = _MoonwallContext.getContext(); try { ctx.disconnect(); } catch { console.log("\u{1F6D1} All connections disconnected"); } const promises = ctx.nodes.map((process2) => { return new Promise((resolve) => { process2.kill(); if (process2.killed) { resolve(`process ${process2.pid} killed`); } }); }); await Promise.all(promises); } }; var MoonwallContext = _MoonwallContext; __publicField(MoonwallContext, "instance"); // src/internal/chopsticksHelpers.ts var import_chalk3 = __toESM(require("chalk"), 1); var import_vitest = require("vitest"); async function getWsFromConfig(providerName) { return providerName ? MoonwallContext.getContext().environment.providers.find(({ name }) => name == providerName).ws() : MoonwallContext.getContext().environment.providers.find(({ type }) => type == "moon" || type == "polkadotJs").ws(); } async function sendNewBlockAndCheck(context, expectedEvents) { const newBlock = await sendNewBlockRequest(); const api = context.getSubstrateApi(); const apiAt = await api.at(newBlock); const actualEvents = await apiAt.query.system.events(); const match = expectedEvents.every((eEvt) => { return actualEvents.map((aEvt) => { if (api.events.system.ExtrinsicSuccess.is(aEvt.event) && aEvt.event.data.dispatchInfo.class.toString() !== "Normal") { return false; } return eEvt.is(aEvt.event); }).reduce((acc, curr) => acc || curr, false); }); return { match, events: actualEvents }; } async function createChopsticksBlock(context, options = { allowFailures: false }) { const result = await sendNewBlockRequest(options); const apiAt = await context.getSubstrateApi().at(result); const actualEvents = await apiAt.query.system.events(); if (options && options.expectEvents) { const match = options.expectEvents.every((eEvt) => { const found = actualEvents.map((aEvt) => eEvt.is(aEvt.event)).reduce((acc, curr) => acc || curr, false); if (!found) { options.logger ? options.logger( `Event ${import_chalk3.default.bgWhiteBright.blackBright(eEvt.meta.name)} not present in block` ) : console.error( `Event ${import_chalk3.default.bgWhiteBright.blackBright(eEvt.meta.name)} not present in block` ); } return found; }); (0, import_vitest.assert)(match, "Expected events not present in block"); } if (options && options.allowFailures === true) { } else { actualEvents.forEach((event) => { (0, import_vitest.assert)( !context.getSubstrateApi().events.system.ExtrinsicFailed.is(event.event), "ExtrinsicFailed event detected, enable 'allowFailures' if this is expected." ); }); } return { result }; } async function chopForkToFinalizedHead(context) { const api = context.providers.find(({ type }) => type == "moon" || type == "polkadotJs").api; const finalizedHead = context.genesis; await sendSetHeadRequest(finalizedHead); await sendNewBlockRequest(); while (true) { const newHead = (await api.rpc.chain.getFinalizedHead()).toString(); await (0, import_promises2.setTimeout)(50); if (newHead !== finalizedHead) { context.genesis = newHead; break; } } } async function sendSetHeadRequest(newHead, providerName) { const ws = providerName ? await getWsFromConfig(providerName) : await getWsFromConfig(); let result = ""; await ws.isReady; result = await ws.send("dev_setHead", [newHead]); await ws.disconnect(); return result; } async function sendNewBlockRequest(params) { const ws = params ? await getWsFromConfig(params.providerName) : await getWsFromConfig(); let result = ""; while (!ws.isConnected) { await (0, import_promises2.setTimeout)(100); } if (params && params.count || params && params.to) { result = await ws.send("dev_newBlock", [{ count: params.count, to: params.to }]); } else { result = await ws.send("dev_newBlock", [{ count: 1 }]); } await ws.disconnect(); return result; } async function sendSetStorageRequest(params) { const ws = params ? await getWsFromConfig(params.providerName) : await getWsFromConfig(); while (!ws.isConnected) { await (0, import_promises2.setTimeout)(100); } await ws.send("dev_setStorage", [ { [params.module]: { [params.method]: params.methodParams } } ]); await ws.disconnect(); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { chopForkToFinalizedHead, createChopsticksBlock, getWsFromConfig, sendNewBlockAndCheck, sendNewBlockRequest, sendSetHeadRequest, sendSetStorageRequest });