@moonsong-labs/moonwall-cli
Version:
Testing framework for the Moon family of projects
673 lines (664 loc) • 23.6 kB
JavaScript
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
});