@moonsong-labs/moonwall-cli
Version:
Testing framework for the Moon family of projects
674 lines (665 loc) • 23.3 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/lib/contextHelpers.ts
var contextHelpers_exports = {};
__export(contextHelpers_exports, {
createBlock: () => createBlock,
extractError: () => extractError,
extractInfo: () => extractInfo,
filterAndApply: () => filterAndApply,
getDispatchError: () => getDispatchError,
isExtrinsicSuccessful: () => isExtrinsicSuccessful
});
module.exports = __toCommonJS(contextHelpers_exports);
var import_api_augment3 = require("@moonbeam-network/api-augment");
var import_moonwall_util2 = require("@moonsong-labs/moonwall-util");
// 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/lib/contextHelpers.ts
var import_vitest = require("vitest");
var import_debug4 = __toESM(require("debug"), 1);
var debug2 = (0, import_debug4.default)("context");
async function createBlock(w3Api, pjsApi, transactions, options = {}) {
(0, import_vitest.assert)(
MoonwallContext.getContext().foundation == "dev",
"createBlock should only be used on DevMode foundations"
);
const results = [];
const txs = transactions == void 0 ? [] : Array.isArray(transactions) ? transactions : [transactions];
for await (const call of txs) {
if (typeof call == "string") {
results.push({
type: "eth",
hash: (await (0, import_moonwall_util2.customWeb3Request)(w3Api, "eth_sendRawTransaction", [call])).result
});
} else if (call.isSigned) {
const tx = pjsApi.tx(call);
debug2(
`- Signed: ${tx.method.section}.${tx.method.method}(${tx.args.map((d) => d.toHuman()).join("; ")}) [ nonce: ${tx.nonce}]`
);
results.push({
type: "sub",
hash: (await call.send()).toString()
});
} else {
const tx = pjsApi.tx(call);
debug2(
`- Unsigned: ${tx.method.section}.${tx.method.method}(${tx.args.map((d) => d.toHuman()).join("; ")}) [ nonce: ${tx.nonce}]`
);
results.push({
type: "sub",
hash: (await call.signAndSend(import_moonwall_util2.alith)).toString()
});
}
}
const { parentHash, finalize } = options;
const blockResult = await (0, import_moonwall_util2.createAndFinalizeBlock)(pjsApi, parentHash, finalize);
if (results.length == 0) {
return {
block: blockResult,
result: null
};
}
const allRecords = await (await pjsApi.at(blockResult.hash)).query.system.events();
const blockData = await pjsApi.rpc.chain.getBlock(blockResult.hash);
const result = results.map((result2) => {
const extrinsicIndex = result2.type == "eth" ? allRecords.find(
({ phase, event: { section, method, data } }) => phase.isApplyExtrinsic && section == "ethereum" && method == "Executed" && data[2].toString() == result2.hash
)?.phase?.asApplyExtrinsic?.toNumber() : blockData.block.extrinsics.findIndex((ext) => ext.hash.toHex() == result2.hash);
const events = allRecords.filter(
({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.toNumber() === extrinsicIndex
);
const failure = extractError(events);
return {
extrinsic: extrinsicIndex >= 0 ? blockData.block.extrinsics[extrinsicIndex] : null,
events,
error: failure && (failure.isModule && pjsApi.registry.findMetaError(failure.asModule) || { name: failure.toString() }),
successful: extrinsicIndex !== void 0 && !failure,
hash: result2.hash
};
});
if (results.find((r) => r.type == "eth")) {
await new Promise((resolve) => setTimeout(resolve, 2));
}
return {
block: blockResult,
result: Array.isArray(transactions) ? result : result[0]
};
}
function filterAndApply(events, section, methods, onFound) {
return events.filter(({ event }) => section === event.section && methods.includes(event.method)).map((record) => onFound(record));
}
function getDispatchError({
event: {
data: [dispatchError]
}
}) {
return dispatchError;
}
function getDispatchInfo({ event: { data, method } }) {
return method === "ExtrinsicSuccess" ? data[0] : data[1];
}
function extractError(events = []) {
return filterAndApply(events, "system", ["ExtrinsicFailed"], getDispatchError)[0];
}
function isExtrinsicSuccessful(events = []) {
return filterAndApply(events, "system", ["ExtrinsicSuccess"], () => true).length > 0;
}
function extractInfo(events = []) {
return filterAndApply(
events,
"system",
["ExtrinsicFailed", "ExtrinsicSuccess"],
getDispatchInfo
)[0];
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createBlock,
extractError,
extractInfo,
filterAndApply,
getDispatchError,
isExtrinsicSuccessful
});